Skip to content

Commit 5faa61c

Browse files
committed
Merge branch 'main' of github.com:emagedoc/CodeClash
2 parents 6f64f6c + f1c698d commit 5faa61c

9 files changed

Lines changed: 17 additions & 202 deletions

File tree

.cursor/rules/viewer.mdc

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ two AI agent in a round-based game against each other.
1818
* **NEVER use inline JavaScript in HTML templates** - all JavaScript must be in separate .js files in the static/js directory
1919
* Organize JavaScript into logical modules: clipboard.js, experiment.js, readme.js, json-editors.js, etc.
2020
* The application consists of two main pages: the viewer and the game picker
21+
* **Static/Frozen Version Compatibility** - The viewer supports both dynamic (Flask server) and static (frozen) versions. The frozen version is generated using `freeze_viewer.py` and uses different routes (`/game/<path>` instead of `/?folder=`). **Every feature must work in both versions**. When making changes, ensure both the `index()` route and `game_view()` route receive the same data and functionality.
2122

2223
### Design System & Styling
2324

@@ -376,7 +377,7 @@ The application has two main pages:
376377
- **game.log**: Game-specific logging
377378
- **everything.log**: Combined comprehensive logging
378379
- **players/p1/player.log, players/p2/player.log**: Individual player logs for each player
379-
- Each log foldout includes "Copy path" and "Download" buttons and has its own scroll container with max-height of 500px for large logs
380+
- Each log foldout includes a "Copy path" button and has its own scroll container with max-height of 500px for large logs
380381
- Logs are displayed in a dedicated section with proper styling and scrollbars
381382

382383
#### Analysis Section
@@ -481,31 +482,6 @@ The trajectory template displays:
481482

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

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-
509485
## Multi-Select Actions Specification
510486

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

codeclash/viewer/app.py

Lines changed: 3 additions & 34 deletions
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, send_file, url_for
14+
from flask import Flask, jsonify, redirect, render_template, request, 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
@@ -872,6 +872,7 @@ def game_view(folder_path):
872872

873873
# Get analysis data
874874
analysis_data = parser.analyze_line_counts()
875+
sim_wins_data = parser.analyze_sim_wins_per_round()
875876

876877
# Get matrix analysis data
877878
matrix_data = parser.load_matrix_analysis()
@@ -886,6 +887,7 @@ def game_view(folder_path):
886887
metadata=metadata,
887888
trajectories_by_round=trajectories_by_round,
888889
analysis_data=analysis_data,
890+
sim_wins_data=sim_wins_data,
889891
matrix_data=matrix_data,
890892
is_static=STATIC_MODE,
891893
)
@@ -1207,37 +1209,4 @@ def analysis_line_counts():
12071209
return jsonify({"success": False, "error": str(e)})
12081210

12091211

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-
12431212
# Use run_viewer.py to launch the application

codeclash/viewer/static/css/style.css

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,24 +1199,6 @@ 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-
12201202
.delete-experiment-btn {
12211203
background: var(--success-color);
12221204
color: white;
@@ -1355,26 +1337,6 @@ summary:focus {
13551337
font-weight: 600;
13561338
}
13571339

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-
13781340
/* Responsive adjustments for copy buttons */
13791341
@media (max-width: 768px) {
13801342
.path-display {
@@ -1385,10 +1347,6 @@ summary:focus {
13851347
.copy-path-btn {
13861348
align-self: flex-start;
13871349
}
1388-
1389-
.download-btn {
1390-
align-self: flex-start;
1391-
}
13921350
}
13931351

13941352
/* Navigation button styles */

codeclash/viewer/static/js/clipboard.js

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

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
78+
// Setup copy button event listeners
12579
function setupCopyButtons() {
12680
// Add event listeners to all copy path buttons
12781
document
@@ -140,23 +94,3 @@ function setupCopyButtons() {
14094
});
14195
});
14296
}
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: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,6 @@ <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>
8885
{% endif %}
8986
</summary>
9087
<div class="log-content-container">
@@ -103,9 +100,6 @@ <h2><i class="bi bi-bar-chart"></i> Overview</h2>
103100
<button class="copy-path-btn-small" data-path="{{ log_data.path }}" title="Copy file path">
104101
<i class="bi bi-clipboard"></i> Copy path
105102
</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>
109103
{% endif %}
110104
</summary>
111105
<div class="log-content-container">

codeclash/viewer/templates/includes/setup.html

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ <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>
3734
{% endif %}
3835
</summary>
3936
<div class="metadata-display">

codeclash/viewer/templates/includes/trajectory.html

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ <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>
3431
{% endif %}
3532
</summary>
3633
<div class="trajectory-content">

codeclash/viewer/templates/index.html

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

freeze_viewer.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,7 @@ def main():
7777
logs_dir = Path(args.logs_dir).resolve()
7878
output_dir = Path(args.output_dir).resolve()
7979

80-
if not logs_dir.exists():
81-
print(f"Error: Logs directory '{logs_dir}' does not exist")
82-
return 1
80+
assert logs_dir.exists()
8381

8482
# Set the logs directory for the app
8583
set_log_base_directory(logs_dir)
@@ -99,22 +97,16 @@ def main():
9997
freezer = setup_freezer(str(output_dir))
10098

10199
# Generate static site
102-
try:
103-
print("Freezing Flask application...")
104-
freezer.freeze()
105-
106-
# Post-process: Add .html extensions to files that don't have them
107-
print("Post-processing: Adding .html extensions...")
108-
_add_html_extensions(output_dir)
109-
110-
print(f"✅ Static site generated successfully in: {output_dir}")
111-
print(f"📁 Open {output_dir}/index.html in your browser to view the static site")
112-
print(f"💡 For best results, serve via HTTP server: cd {output_dir} && python -m http.server 8000")
113-
114-
return 0
115-
except Exception as e:
116-
print(f"❌ Error generating static site: {e}")
117-
return 1
100+
print("Freezing Flask application...")
101+
freezer.freeze()
102+
103+
# Post-process: Add .html extensions to files that don't have them
104+
print("Post-processing: Adding .html extensions...")
105+
_add_html_extensions(output_dir)
106+
107+
print(f"✅ Static site generated successfully in: {output_dir}")
108+
print(f"📁 Open {output_dir}/index.html in your browser to view the static site")
109+
print(f"💡 For best results, serve via HTTP server: cd {output_dir} && python -m http.server 8000")
118110

119111

120112
def _add_html_extensions(build_dir: Path):

0 commit comments

Comments
 (0)