Skip to content

Commit c9f5031

Browse files
committed
Revert "Revert "Enh(viewer): Add download buttons for objects""
This reverts commit 4cef3f5.
1 parent 040ca42 commit c9f5031

8 files changed

Lines changed: 179 additions & 15 deletions

File tree

.cursor/rules/viewer.mdc

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

383383
#### Analysis Section
@@ -482,6 +482,31 @@ The trajectory template displays:
482482

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

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

487512
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
@@ -1209,4 +1209,37 @@ def analysis_line_counts():
12091209
return jsonify({"success": False, "error": str(e)})
12101210

12111211

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

codeclash/viewer/static/css/style.css

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,20 +1199,22 @@ summary:focus {
11991199
color: #000000;
12001200
}
12011201

1202-
/* AWS Command Display Styling */
1203-
.aws-command-display {
1204-
margin-top: 1rem;
1205-
padding-top: 1rem;
1206-
border-top: 1px solid var(--border-color);
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;
12071213
}
12081214

1209-
.aws-command-display h4 {
1210-
margin: 0 0 0.5rem 0;
1211-
font-size: 1rem;
1212-
color: var(--accent-color);
1213-
display: flex;
1214-
align-items: center;
1215-
gap: 0.5rem;
1215+
.download-btn:hover {
1216+
background: var(--accent-hover);
1217+
color: #000000;
12161218
}
12171219

12181220
.delete-experiment-btn {
@@ -1353,6 +1355,26 @@ summary:focus {
13531355
font-weight: 600;
13541356
}
13551357

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+
13561378
/* Responsive adjustments for copy buttons */
13571379
@media (max-width: 768px) {
13581380
.path-display {
@@ -1363,6 +1385,10 @@ summary:focus {
13631385
.copy-path-btn {
13641386
align-self: flex-start;
13651387
}
1388+
1389+
.download-btn {
1390+
align-self: flex-start;
1391+
}
13661392
}
13671393

13681394
/* 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
@@ -111,3 +157,23 @@ function setupCopyButtons() {
111157
});
112158
});
113159
}
160+
161+
// Setup download button event listeners
162+
function setupDownloadButtons() {
163+
// Add event listeners to all download buttons
164+
document
165+
.querySelectorAll(".download-btn, .download-btn-small")
166+
.forEach((button) => {
167+
button.addEventListener("click", function (e) {
168+
e.preventDefault();
169+
e.stopPropagation();
170+
171+
const path = this.getAttribute("data-path");
172+
if (path) {
173+
downloadFile(path, this);
174+
} else {
175+
console.error("No path found on download button:", this);
176+
}
177+
});
178+
});
179+
}

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)