Skip to content

Commit 68a7b92

Browse files
committed
Enh(viewer): Add 'copy path' buttons
1 parent 282b2fa commit 68a7b92

3 files changed

Lines changed: 244 additions & 9 deletions

File tree

codeclash/viewer/app.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class GameMetadata:
4747

4848
results: dict[str, Any]
4949
main_log: str
50+
main_log_path: str
51+
metadata_file_path: str
5052
rounds: list[dict[str, Any]]
5153

5254

@@ -65,6 +67,7 @@ class TrajectoryInfo:
6567
diff: str | None = None
6668
incremental_diff: str | None = None
6769
modified_files: dict[str, str] | None = None
70+
trajectory_file_path: str | None = None
6871

6972

7073
class LogParser:
@@ -80,8 +83,10 @@ def parse_game_metadata(self) -> GameMetadata:
8083
metadata_file = self.log_dir / "metadata.json"
8184
if metadata_file.exists():
8285
results = json.loads(metadata_file.read_text())
86+
metadata_file_path = str(metadata_file)
8387
else:
8488
results = {"status": "No metadata file found"}
89+
metadata_file_path = ""
8590

8691
# Store player metadata for later use
8792
self._player_metadata = {}
@@ -93,6 +98,7 @@ def parse_game_metadata(self) -> GameMetadata:
9398
# Parse tournament.log if it exists
9499
main_log_file = self.log_dir / "tournament.log"
95100
main_log = main_log_file.read_text() if main_log_file.exists() else "No tournament log found"
101+
main_log_path = str(main_log_file) if main_log_file.exists() else ""
96102

97103
# Parse round directories and their sim logs
98104
rounds = []
@@ -110,7 +116,7 @@ def parse_game_metadata(self) -> GameMetadata:
110116

111117
for sim_file in sim_files:
112118
sim_content = sim_file.read_text()
113-
sim_logs.append({"filename": sim_file.name, "content": sim_content})
119+
sim_logs.append({"filename": sim_file.name, "content": sim_content, "full_path": str(sim_file)})
114120

115121
# Check for round results
116122
results_file = round_dir / "results.json"
@@ -120,7 +126,13 @@ def parse_game_metadata(self) -> GameMetadata:
120126

121127
rounds.append({"round_num": round_num, "sim_logs": sim_logs, "results": round_results})
122128

123-
return GameMetadata(results=results, main_log=main_log, rounds=rounds)
129+
return GameMetadata(
130+
results=results,
131+
main_log=main_log,
132+
main_log_path=main_log_path,
133+
metadata_file_path=metadata_file_path,
134+
rounds=rounds,
135+
)
124136

125137
def parse_trajectory(self, player_id: int, round_num: int) -> TrajectoryInfo | None:
126138
"""Parse a specific trajectory file"""
@@ -163,6 +175,7 @@ def parse_trajectory(self, player_id: int, round_num: int) -> TrajectoryInfo | N
163175
diff=diff,
164176
incremental_diff=incremental_diff,
165177
modified_files=modified_files,
178+
trajectory_file_path=str(traj_file),
166179
)
167180
except (json.JSONDecodeError, KeyError) as e:
168181
print(f"Error parsing {traj_file}: {e}")
@@ -271,11 +284,15 @@ def index():
271284
if trajectory:
272285
trajectories_by_round[round_num].append(trajectory)
273286

287+
# Get the full path of the selected folder
288+
selected_folder_path = str(logs_dir / selected_folder) if selected_folder else ""
289+
274290
return render_template(
275291
"index.html",
276292
log_folders=log_folders,
277293
log_folders_info=log_folders_info,
278294
selected_folder=selected_folder,
295+
selected_folder_path=selected_folder_path,
279296
metadata=metadata,
280297
trajectories_by_round=trajectories_by_round,
281298
)

codeclash/viewer/static/css/style.css

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,3 +665,84 @@ summary:focus {
665665
[data-theme="dark"] .jsoneditor-menu {
666666
background-color: var(--bg-tertiary) !important;
667667
}
668+
669+
/* Copy path buttons */
670+
.folder-path-section {
671+
margin-bottom: 2rem;
672+
padding: 1rem;
673+
background: var(--bg-secondary);
674+
border-radius: 8px;
675+
border: 1px solid var(--border-color);
676+
}
677+
678+
.folder-path-container h3 {
679+
margin: 0 0 0.5rem 0;
680+
color: var(--text-primary);
681+
font-size: 1.1rem;
682+
}
683+
684+
.path-display {
685+
display: flex;
686+
align-items: center;
687+
gap: 0.5rem;
688+
flex-wrap: wrap;
689+
}
690+
691+
.folder-path {
692+
background: var(--code-bg);
693+
padding: 0.5rem;
694+
border-radius: 4px;
695+
border: 1px solid var(--border-color);
696+
font-family: "Courier New", monospace;
697+
font-size: 0.9rem;
698+
word-break: break-all;
699+
flex: 1;
700+
min-width: 0;
701+
}
702+
703+
.copy-path-btn {
704+
background: var(--accent-color);
705+
color: white;
706+
border: none;
707+
padding: 0.5rem 1rem;
708+
border-radius: 4px;
709+
cursor: pointer;
710+
font-size: 0.9rem;
711+
transition: background-color 0.2s;
712+
white-space: nowrap;
713+
}
714+
715+
.copy-path-btn:hover {
716+
background: var(--accent-hover);
717+
}
718+
719+
.copy-path-btn-small {
720+
background: var(--bg-tertiary);
721+
color: var(--text-secondary);
722+
border: 1px solid var(--border-color);
723+
padding: 0.25rem 0.5rem;
724+
border-radius: 3px;
725+
cursor: pointer;
726+
font-size: 0.8rem;
727+
margin-left: 0.5rem;
728+
transition: all 0.2s;
729+
vertical-align: middle;
730+
}
731+
732+
.copy-path-btn-small:hover {
733+
background: var(--accent-color);
734+
color: white;
735+
border-color: var(--accent-color);
736+
}
737+
738+
/* Responsive adjustments for copy buttons */
739+
@media (max-width: 768px) {
740+
.path-display {
741+
flex-direction: column;
742+
align-items: stretch;
743+
}
744+
745+
.copy-path-btn {
746+
align-self: flex-start;
747+
}
748+
}

codeclash/viewer/templates/index.html

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,48 @@ <h1>🎮 CodeClash Trajectory Viewer</h1>
3737
</header>
3838

3939
<main class="main-content">
40+
<!-- Current Folder Path -->
41+
<section class="folder-path-section">
42+
<div class="folder-path-container">
43+
<h3>📁 Current Folder:</h3>
44+
<div class="path-display">
45+
<code class="folder-path">{{ selected_folder_path }}</code>
46+
<button class="copy-path-btn" data-path="{{ selected_folder_path }}" title="Copy folder path">
47+
📋 Copy path
48+
</button>
49+
</div>
50+
</div>
51+
</section>
52+
4053
<!-- Overall Results Section -->
4154
<section class="results-section">
4255
<h2>📊 Game Results</h2>
4356

44-
<div class="metadata-display">
45-
<div id="metadata-jsoneditor" class="json-viewer"></div>
46-
</div>
57+
<!-- Metadata Foldout -->
58+
<details class="foldout">
59+
<summary>
60+
📋 Metadata
61+
{% if metadata.metadata_file_path %}
62+
<button class="copy-path-btn-small" data-path="{{ metadata.metadata_file_path }}" title="Copy metadata file path">
63+
📋 Copy path
64+
</button>
65+
{% endif %}
66+
</summary>
67+
<div class="metadata-display">
68+
<div id="metadata-jsoneditor" class="json-viewer"></div>
69+
</div>
70+
</details>
4771

4872
<!-- Main Log Foldout -->
4973
<details class="foldout">
50-
<summary>📝 Tournament Log</summary>
74+
<summary>
75+
📝 Tournament Log
76+
{% if metadata.main_log_path %}
77+
<button class="copy-path-btn-small" data-path="{{ metadata.main_log_path }}" title="Copy file path">
78+
📋 Copy path
79+
</button>
80+
{% endif %}
81+
</summary>
5182
<div class="log-content">
5283
<pre><code>{{ metadata.main_log }}</code></pre>
5384
</div>
@@ -77,7 +108,12 @@ <h2>🎯 Game Rounds</h2>
77108
<div class="log-content">
78109
{% for sim_log in round_data.sim_logs %}
79110
<details class="foldout sim-foldout">
80-
<summary>{{ sim_log.filename }}</summary>
111+
<summary>
112+
{{ sim_log.filename }}
113+
<button class="copy-path-btn-small" data-path="{{ sim_log.full_path }}" title="Copy file path">
114+
📋 Copy path
115+
</button>
116+
</summary>
81117
<div class="log-content">
82118
<pre><code>{{ sim_log.content }}</code></pre>
83119
</div>
@@ -92,7 +128,9 @@ <h2>🎯 Game Rounds</h2>
92128

93129
<!-- Trajectory Container with Overview and Messages -->
94130
<div class="trajectory-header">
95-
<h3>🤖 Player {{ trajectory.player_id }} Round {{ round_num }}</h3>
131+
<h3>
132+
🤖 Player {{ trajectory.player_id }} Round {{ round_num }}
133+
</h3>
96134
<div class="trajectory-stats">
97135
<div class="stat-item">
98136
<span class="stat-label">API Calls:</span>
@@ -112,7 +150,14 @@ <h3>🤖 Player {{ trajectory.player_id }} Round {{ round_num }}</h3>
112150

113151
<!-- Messages Foldout inside the same container -->
114152
<details class="trajectory-messages-foldout">
115-
<summary>💬 Messages ({{ trajectory.messages|length }} total)</summary>
153+
<summary>
154+
💬 Trajectory ({{ trajectory.messages|length }} total)
155+
{% if trajectory.trajectory_file_path %}
156+
<button class="copy-path-btn-small" data-path="{{ trajectory.trajectory_file_path }}" title="Copy trajectory file path">
157+
📋 Copy path
158+
</button>
159+
{% endif %}
160+
</summary>
116161
<div class="trajectory-content">
117162
<div class="messages-container">
118163
{% for message in trajectory.messages %}
@@ -318,8 +363,100 @@ <h3>🤖 Player {{ trajectory.player_id }} Round {{ round_num }}</h3>
318363
<script src="https://unpkg.com/jsoneditor@latest/dist/jsoneditor.min.js"></script>
319364
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
320365
<script>
366+
// Copy to clipboard function
367+
function copyToClipboard(text, button) {
368+
function showSuccessMessage() {
369+
if (button) {
370+
const originalText = button.textContent;
371+
const originalColor = button.style.color;
372+
button.textContent = '✓ Copied';
373+
button.style.color = 'green';
374+
setTimeout(() => {
375+
button.textContent = originalText;
376+
button.style.color = originalColor;
377+
}, 1500);
378+
}
379+
}
380+
381+
// Check if modern clipboard API is available
382+
if (navigator.clipboard && navigator.clipboard.writeText) {
383+
navigator.clipboard.writeText(text).then(function() {
384+
showSuccessMessage();
385+
console.log('Copied to clipboard:', text);
386+
}).catch(function(err) {
387+
console.error('Failed to copy text with clipboard API: ', err);
388+
// Fall back to legacy method
389+
fallbackCopy();
390+
});
391+
} else {
392+
// Use fallback method directly
393+
fallbackCopy();
394+
}
395+
396+
function fallbackCopy() {
397+
try {
398+
const textArea = document.createElement('textarea');
399+
textArea.value = text;
400+
textArea.style.position = 'fixed';
401+
textArea.style.left = '-9999px';
402+
textArea.style.top = '-9999px';
403+
document.body.appendChild(textArea);
404+
textArea.focus();
405+
textArea.select();
406+
407+
const successful = document.execCommand('copy');
408+
document.body.removeChild(textArea);
409+
410+
if (successful) {
411+
showSuccessMessage();
412+
console.log('Copied to clipboard (fallback):', text);
413+
} else {
414+
console.error('Failed to copy text with fallback method');
415+
if (button) {
416+
button.textContent = '✗ Failed';
417+
button.style.color = 'red';
418+
setTimeout(() => {
419+
button.textContent = button.getAttribute('title') || 'Copy';
420+
button.style.color = '';
421+
}, 1500);
422+
}
423+
}
424+
} catch (err) {
425+
console.error('Fallback copy failed:', err);
426+
if (button) {
427+
button.textContent = '✗ Failed';
428+
button.style.color = 'red';
429+
setTimeout(() => {
430+
button.textContent = button.getAttribute('title') || 'Copy';
431+
button.style.color = '';
432+
}, 1500);
433+
}
434+
}
435+
}
436+
}
437+
438+
// Setup copy button event listeners
439+
function setupCopyButtons() {
440+
// Add event listeners to all copy path buttons
441+
document.querySelectorAll('.copy-path-btn, .copy-path-btn-small').forEach(button => {
442+
button.addEventListener('click', function(e) {
443+
e.preventDefault();
444+
e.stopPropagation();
445+
446+
const path = this.getAttribute('data-path');
447+
if (path) {
448+
copyToClipboard(path, this);
449+
} else {
450+
console.error('No path found on button:', this);
451+
}
452+
});
453+
});
454+
}
455+
321456
// Initialize JSON editors when page loads
322457
document.addEventListener('DOMContentLoaded', function() {
458+
// Setup copy buttons
459+
setupCopyButtons();
323460
// Initialize metadata JSON editor
324461
const metadataEditor = new JSONEditor(document.getElementById('metadata-jsoneditor'), {
325462
mode: 'view',

0 commit comments

Comments
 (0)