Skip to content

Commit e4fa37b

Browse files
committed
Enh(viewer): Add download buttons for objects
1 parent 9291e17 commit e4fa37b

8 files changed

Lines changed: 183 additions & 3 deletions

File tree

.cursor/rules/viewer.mdc

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ The application has two main pages:
376376
- **game.log**: Game-specific logging
377377
- **everything.log**: Combined comprehensive logging
378378
- **players/p1/player.log, players/p2/player.log**: Individual player logs for each player
379-
- Each log foldout includes a "Copy path" button and has its own scroll container with max-height of 500px for large logs
379+
- Each log foldout includes "Copy path" and "Download" buttons and has its own scroll container with max-height of 500px for large logs
380380
- Logs are displayed in a dedicated section with proper styling and scrollbars
381381

382382
#### Analysis Section
@@ -481,6 +481,31 @@ The trajectory template displays:
481481

482482
**Content Preview**: Long message content (>5 lines) shows a preview with expand/collapse functionality to keep the interface manageable.
483483

484+
## Download Functionality
485+
486+
The trajectory viewer provides download capabilities for all files that have "Copy path" buttons (except the current folder path):
487+
488+
### Download Locations
489+
490+
* **Tournament and Game Logs**: All log files in the Overview section (tournament.log, game.log, everything.log, player logs)
491+
* **Metadata Files**: The metadata.json file in the Setup section
492+
* **Trajectory Files**: Individual trajectory files (.traj.json) in the Rounds section
493+
494+
### Implementation Details
495+
496+
* **Download Buttons**: Each "Copy path" button (except current folder) is accompanied by a "Download" button with a download icon
497+
* **Server Endpoint**: `GET /download-file?path=<file_path>` serves files with proper security checks
498+
* **Security**: Files must exist within the logs directory, be actual files (not directories), and be accessible
499+
* **File Naming**: Downloaded files retain their original names
500+
* **JavaScript**: Download functionality is handled by `downloadFile()` function in `clipboard.js`
501+
* **User Feedback**: Download buttons show success/error states similar to copy buttons
502+
503+
### Button Styling
504+
505+
* **Download buttons** use the same styling as copy buttons but with download icon (`bi-download`)
506+
* **Button classes**: `.download-btn` and `.download-btn-small` for consistent styling
507+
* **Placement**: Download buttons appear immediately after their corresponding copy path buttons
508+
484509
## Multi-Select Actions Specification
485510

486511
The game picker includes four batch operations for selected game folders, accessible via the action dropdown:

codeclash/viewer/app.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from pathlib import Path
1212
from typing import Any
1313

14-
from flask import Flask, jsonify, redirect, render_template, request, url_for
14+
from flask import Flask, jsonify, redirect, render_template, request, send_file, url_for
1515

1616
from codeclash.ratings.significance import calculate_p_value
1717
from codeclash.tournaments.utils.git_utils import filter_git_diff, split_git_diff_by_files
@@ -1207,4 +1207,37 @@ def analysis_line_counts():
12071207
return jsonify({"success": False, "error": str(e)})
12081208

12091209

1210+
@app.route("/download-file")
1211+
def download_file():
1212+
"""Download a file with proper security checks"""
1213+
file_path = request.args.get("path")
1214+
1215+
if not file_path:
1216+
return jsonify({"success": False, "error": "No file path provided"}), 400
1217+
1218+
try:
1219+
# Convert to Path object
1220+
file_path_obj = Path(file_path)
1221+
1222+
# Security check: ensure the file exists
1223+
if not file_path_obj.exists():
1224+
return jsonify({"success": False, "error": "File does not exist"}), 404
1225+
1226+
# Security check: ensure the file is not a directory
1227+
if not file_path_obj.is_file():
1228+
return jsonify({"success": False, "error": "Path is not a file"}), 400
1229+
1230+
# Security check: ensure the path is within our expected logs directory
1231+
try:
1232+
file_path_obj.relative_to(LOG_BASE_DIR)
1233+
except ValueError:
1234+
return jsonify({"success": False, "error": "Invalid file path"}), 403
1235+
1236+
# Send the file as an attachment
1237+
return send_file(file_path_obj, as_attachment=True, download_name=file_path_obj.name)
1238+
1239+
except Exception as e:
1240+
return jsonify({"success": False, "error": str(e)}), 500
1241+
1242+
12101243
# Use run_viewer.py to launch the application

codeclash/viewer/static/css/style.css

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,24 @@ summary:focus {
11991199
color: #000000;
12001200
}
12011201

1202+
.download-btn {
1203+
background: var(--accent-color);
1204+
color: #000000;
1205+
border: none;
1206+
padding: 0.5rem 1rem;
1207+
border-radius: 4px;
1208+
cursor: pointer;
1209+
font-size: 0.9rem;
1210+
transition: background-color 0.2s;
1211+
white-space: nowrap;
1212+
font-weight: 600;
1213+
}
1214+
1215+
.download-btn:hover {
1216+
background: var(--accent-hover);
1217+
color: #000000;
1218+
}
1219+
12021220
.delete-experiment-btn {
12031221
background: var(--success-color);
12041222
color: white;
@@ -1337,6 +1355,26 @@ summary:focus {
13371355
font-weight: 600;
13381356
}
13391357

1358+
.download-btn-small {
1359+
background: var(--bg-tertiary);
1360+
color: var(--text-secondary);
1361+
border: 1px solid var(--border-color);
1362+
padding: 0.25rem 0.5rem;
1363+
border-radius: 3px;
1364+
cursor: pointer;
1365+
font-size: 0.8rem;
1366+
margin-left: 0.5rem;
1367+
transition: all 0.2s;
1368+
vertical-align: middle;
1369+
}
1370+
1371+
.download-btn-small:hover {
1372+
background: var(--accent-color);
1373+
color: #000000;
1374+
border-color: var(--accent-color);
1375+
font-weight: 600;
1376+
}
1377+
13401378
/* Responsive adjustments for copy buttons */
13411379
@media (max-width: 768px) {
13421380
.path-display {
@@ -1347,6 +1385,10 @@ summary:focus {
13471385
.copy-path-btn {
13481386
align-self: flex-start;
13491387
}
1388+
1389+
.download-btn {
1390+
align-self: flex-start;
1391+
}
13501392
}
13511393

13521394
/* Navigation button styles */

codeclash/viewer/static/js/clipboard.js

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,53 @@ function copyToClipboard(text, button) {
7575
}
7676
}
7777

78-
// Setup copy button event listeners
78+
// Download file function
79+
function downloadFile(filePath, button) {
80+
function showSuccessMessage() {
81+
if (button) {
82+
const originalText = button.textContent;
83+
const originalColor = button.style.color;
84+
button.textContent = "✓ Downloading";
85+
button.style.color = "green";
86+
setTimeout(() => {
87+
button.textContent = originalText;
88+
button.style.color = originalColor;
89+
}, 1500);
90+
}
91+
}
92+
93+
function showErrorMessage() {
94+
if (button) {
95+
const originalText = button.textContent;
96+
const originalColor = button.style.color;
97+
button.textContent = "✗ Failed";
98+
button.style.color = "red";
99+
setTimeout(() => {
100+
button.textContent = originalText;
101+
button.style.color = originalColor;
102+
}, 1500);
103+
}
104+
}
105+
106+
try {
107+
// Create a temporary link element to trigger download
108+
const downloadUrl = `/download-file?path=${encodeURIComponent(filePath)}`;
109+
const link = document.createElement("a");
110+
link.href = downloadUrl;
111+
link.style.display = "none";
112+
document.body.appendChild(link);
113+
link.click();
114+
document.body.removeChild(link);
115+
116+
showSuccessMessage();
117+
console.log("Downloading file:", filePath);
118+
} catch (err) {
119+
console.error("Failed to download file:", err);
120+
showErrorMessage();
121+
}
122+
}
123+
124+
// Setup copy and download button event listeners
79125
function setupCopyButtons() {
80126
// Add event listeners to all copy path buttons
81127
document
@@ -94,3 +140,23 @@ function setupCopyButtons() {
94140
});
95141
});
96142
}
143+
144+
// Setup download button event listeners
145+
function setupDownloadButtons() {
146+
// Add event listeners to all download buttons
147+
document
148+
.querySelectorAll(".download-btn, .download-btn-small")
149+
.forEach((button) => {
150+
button.addEventListener("click", function (e) {
151+
e.preventDefault();
152+
e.stopPropagation();
153+
154+
const path = this.getAttribute("data-path");
155+
if (path) {
156+
downloadFile(path, this);
157+
} else {
158+
console.error("No path found on download button:", this);
159+
}
160+
});
161+
});
162+
}

codeclash/viewer/templates/includes/overview.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ <h2><i class="bi bi-bar-chart"></i> Overview</h2>
8282
<button class="copy-path-btn-small" data-path="{{ metadata.main_log_path }}" title="Copy file path">
8383
<i class="bi bi-clipboard"></i> Copy path
8484
</button>
85+
<button class="download-btn-small" data-path="{{ metadata.main_log_path }}" title="Download file">
86+
<i class="bi bi-download"></i> Download
87+
</button>
8588
{% endif %}
8689
</summary>
8790
<div class="log-content-container">
@@ -100,6 +103,9 @@ <h2><i class="bi bi-bar-chart"></i> Overview</h2>
100103
<button class="copy-path-btn-small" data-path="{{ log_data.path }}" title="Copy file path">
101104
<i class="bi bi-clipboard"></i> Copy path
102105
</button>
106+
<button class="download-btn-small" data-path="{{ log_data.path }}" title="Download file">
107+
<i class="bi bi-download"></i> Download
108+
</button>
103109
{% endif %}
104110
</summary>
105111
<div class="log-content-container">

codeclash/viewer/templates/includes/setup.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ <h2><i class="bi bi-gear"></i> Setup</h2>
3131
<button class="btn btn-outline-secondary btn-sm copy-path-btn-small" data-path="{{ metadata.metadata_file_path }}" title="Copy metadata file path">
3232
<i class="bi bi-clipboard"></i> Copy path
3333
</button>
34+
<button class="btn btn-outline-secondary btn-sm download-btn-small" data-path="{{ metadata.metadata_file_path }}" title="Download metadata file">
35+
<i class="bi bi-download"></i> Download
36+
</button>
3437
{% endif %}
3538
</summary>
3639
<div class="metadata-display">

codeclash/viewer/templates/includes/trajectory.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ <h3>
2828
<button class="copy-path-btn-small" data-path="{{ trajectory.trajectory_file_path }}" title="Copy trajectory file path">
2929
<i class="bi bi-clipboard"></i> Copy path
3030
</button>
31+
<button class="download-btn-small" data-path="{{ trajectory.trajectory_file_path }}" title="Download trajectory file">
32+
<i class="bi bi-download"></i> Download
33+
</button>
3134
{% endif %}
3235
</summary>
3336
<div class="trajectory-content">

codeclash/viewer/templates/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ <h3>Move/Rename Folder</h3>
8484
document.addEventListener('DOMContentLoaded', function() {
8585
// Setup copy buttons
8686
setupCopyButtons();
87+
// Setup download buttons
88+
setupDownloadButtons();
8789
// Setup readme autosave (only if not in static mode)
8890
{% if not is_static %}
8991
setupReadmeAutosave();

0 commit comments

Comments
 (0)