Skip to content

Commit f356116

Browse files
committed
Enh(viewer): Add readme, delete functionality
1 parent e05deb8 commit f356116

4 files changed

Lines changed: 310 additions & 1 deletion

File tree

.cursor/rules/viewer.mdc

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,19 @@ The application has two main pages:
253253
- Tooltip explains middle-click functionality
254254
* **"↗" button**: Always open picker in new tab
255255
* **Dark/light mode toggle button**
256-
* **Current folder path display** with copy functionality
256+
* **Current folder path display** with copy functionality and delete button
257+
- **Delete button**: 🗑️ icon with simple confirmation prompt to delete the entire experiment folder (rm -r)
257258

258259
### Main body
259260

261+
#### Readme Section
262+
263+
* **First section** after the header: A "Readme" section with editable text area
264+
* Automatically loads content from `readme.txt` in the experiment folder (if it exists)
265+
* **Autosave functionality**: Saves changes to `readme.txt` after 1 second of inactivity
266+
* Shows status indicator: "Loading...", "Typing...", "Saving...", "Saved", "Error"
267+
* Simple, unstyled text area for adding experiment notes and observations
268+
260269
#### Overall results
261270

262271
* At the top, we show the content of `metadata.json`

codeclash/viewer/app.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77

88
import json
9+
import shutil
910
from dataclasses import dataclass
1011
from pathlib import Path
1112
from typing import Any
@@ -568,4 +569,95 @@ def trajectory_detail(player_name: str, round_num: int):
568569
return render_template("trajectory.html", trajectory=trajectory)
569570

570571

572+
@app.route("/delete-experiment", methods=["POST"])
573+
def delete_experiment():
574+
"""Delete an experiment folder"""
575+
try:
576+
data = request.get_json()
577+
folder_path = data.get("folder_path")
578+
579+
if not folder_path:
580+
return jsonify({"success": False, "error": "No folder path provided"})
581+
582+
# Convert to Path object and validate it's within our logs directory
583+
folder_path_obj = Path(folder_path)
584+
585+
# Ensure the path exists and is a directory
586+
if not folder_path_obj.exists():
587+
return jsonify({"success": False, "error": "Folder does not exist"})
588+
589+
if not folder_path_obj.is_dir():
590+
return jsonify({"success": False, "error": "Path is not a directory"})
591+
592+
# Security check: ensure the path is within our expected logs directory
593+
try:
594+
# Check if it's a subdirectory of LOG_BASE_DIR
595+
folder_path_obj.relative_to(LOG_BASE_DIR)
596+
except ValueError:
597+
return jsonify({"success": False, "error": "Invalid folder path"})
598+
599+
# Delete the folder
600+
shutil.rmtree(folder_path_obj)
601+
602+
return jsonify({"success": True, "message": "Experiment deleted successfully"})
603+
604+
except Exception as e:
605+
return jsonify({"success": False, "error": str(e)})
606+
607+
608+
@app.route("/save-readme", methods=["POST"])
609+
def save_readme():
610+
"""Save readme content to readme.txt in the experiment folder"""
611+
try:
612+
data = request.get_json()
613+
selected_folder = data.get("selected_folder")
614+
content = data.get("content", "")
615+
616+
if not selected_folder:
617+
return jsonify({"success": False, "error": "No folder specified"})
618+
619+
# Get the folder path
620+
folder_path = LOG_BASE_DIR / selected_folder
621+
622+
if not folder_path.exists() or not folder_path.is_dir():
623+
return jsonify({"success": False, "error": "Invalid folder"})
624+
625+
# Save to readme.txt
626+
readme_file = folder_path / "readme.txt"
627+
readme_file.write_text(content)
628+
629+
return jsonify({"success": True, "message": "Readme saved successfully"})
630+
631+
except Exception as e:
632+
return jsonify({"success": False, "error": str(e)})
633+
634+
635+
@app.route("/load-readme")
636+
def load_readme():
637+
"""Load readme content from readme.txt in the experiment folder"""
638+
try:
639+
selected_folder = request.args.get("folder")
640+
641+
if not selected_folder:
642+
return jsonify({"success": False, "error": "No folder specified"})
643+
644+
# Get the folder path
645+
folder_path = LOG_BASE_DIR / selected_folder
646+
647+
if not folder_path.exists() or not folder_path.is_dir():
648+
return jsonify({"success": False, "error": "Invalid folder"})
649+
650+
# Load from readme.txt
651+
readme_file = folder_path / "readme.txt"
652+
content = ""
653+
654+
if readme_file.exists():
655+
content = readme_file.read_text()
656+
657+
return jsonify({"success": True, "content": content})
658+
659+
except Exception as e:
660+
return jsonify({"success": False, "error": str(e)})
661+
662+
571663
# Use run_viewer.py to launch the application

codeclash/viewer/static/css/style.css

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,14 @@ body {
176176
}
177177

178178
/* Sections */
179+
.readme-section,
179180
.setup-section,
180181
.overview-section,
181182
.rounds-section {
182183
margin-bottom: 3rem;
183184
}
184185

186+
.readme-section h2,
185187
.setup-section h2,
186188
.overview-section h2,
187189
.rounds-section h2 {
@@ -918,6 +920,77 @@ summary:focus {
918920
background: var(--accent-hover);
919921
}
920922

923+
.delete-experiment-btn {
924+
background: var(--danger-color);
925+
color: white;
926+
border: none;
927+
padding: 0.5rem 1rem;
928+
border-radius: 4px;
929+
cursor: pointer;
930+
font-size: 0.9rem;
931+
transition: background-color 0.2s;
932+
white-space: nowrap;
933+
}
934+
935+
.delete-experiment-btn:hover {
936+
background: #c82333;
937+
}
938+
939+
/* Readme Section */
940+
.readme-container {
941+
background-color: var(--bg-secondary);
942+
border: 1px solid var(--border-color);
943+
border-radius: 0.5rem;
944+
padding: 1rem;
945+
box-shadow: var(--shadow);
946+
}
947+
948+
.readme-textarea {
949+
width: 100%;
950+
min-height: 120px;
951+
padding: 0.75rem;
952+
border: 1px solid var(--border-color);
953+
border-radius: 0.375rem;
954+
background-color: var(--bg-primary);
955+
color: var(--text-primary);
956+
font-family:
957+
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif;
958+
font-size: 0.9rem;
959+
line-height: 1.5;
960+
resize: vertical;
961+
transition: border-color 0.2s ease;
962+
}
963+
964+
.readme-textarea:focus {
965+
outline: none;
966+
border-color: var(--accent-color);
967+
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.1);
968+
}
969+
970+
.readme-textarea::placeholder {
971+
color: var(--text-muted);
972+
}
973+
974+
.readme-status {
975+
margin-top: 0.5rem;
976+
font-size: 0.8rem;
977+
color: var(--text-secondary);
978+
text-align: right;
979+
transition: color 0.2s ease;
980+
}
981+
982+
.readme-status.saving {
983+
color: var(--warning-color);
984+
}
985+
986+
.readme-status.saved {
987+
color: var(--success-color);
988+
}
989+
990+
.readme-status.error {
991+
color: var(--danger-color);
992+
}
993+
921994
.copy-path-btn-small {
922995
background: var(--bg-tertiary);
923996
color: var(--text-secondary);

codeclash/viewer/templates/index.html

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,22 @@ <h3>📁 Current Folder:</h3>
4343
<button class="copy-path-btn" data-path="{{ selected_folder_path }}" title="Copy folder path">
4444
📋 Copy path
4545
</button>
46+
<button class="delete-experiment-btn" onclick="deleteExperiment('{{ selected_folder_path }}')" title="Delete this experiment">
47+
🗑️ Delete
48+
</button>
4649
</div>
4750
</div>
4851
</section>
4952

53+
<!-- Readme Section -->
54+
<section class="readme-section">
55+
<h2>📝 Readme</h2>
56+
<div class="readme-container">
57+
<textarea id="readme-textarea" class="readme-textarea" placeholder="Add notes about this experiment..."></textarea>
58+
<div class="readme-status" id="readme-status">Loading...</div>
59+
</div>
60+
</section>
61+
5062
<!-- Setup Section -->
5163
<section class="setup-section">
5264
<h2>⚙️ Setup</h2>
@@ -598,10 +610,133 @@ <h3>
598610
});
599611
}
600612

613+
// Delete experiment function
614+
function deleteExperiment(folderPath) {
615+
const confirmed = confirm('Are you sure you want to delete this experiment? This action cannot be undone.\n\nFolder: ' + folderPath);
616+
if (confirmed) {
617+
fetch('/delete-experiment', {
618+
method: 'POST',
619+
headers: {
620+
'Content-Type': 'application/json',
621+
},
622+
body: JSON.stringify({
623+
folder_path: folderPath
624+
})
625+
})
626+
.then(response => response.json())
627+
.then(data => {
628+
if (data.success) {
629+
alert('Experiment deleted successfully.');
630+
// Redirect to picker
631+
window.location.href = '/picker';
632+
} else {
633+
alert('Error deleting experiment: ' + (data.error || 'Unknown error'));
634+
}
635+
})
636+
.catch(error => {
637+
console.error('Error:', error);
638+
alert('Error deleting experiment: ' + error.message);
639+
});
640+
}
641+
}
642+
643+
// Readme functionality
644+
let readmeTimeout = null;
645+
let readmeTextarea = null;
646+
let readmeStatus = null;
647+
648+
function loadReadme() {
649+
const urlParams = new URLSearchParams(window.location.search);
650+
const folder = urlParams.get('folder');
651+
652+
if (!folder) return;
653+
654+
fetch('/load-readme?folder=' + encodeURIComponent(folder))
655+
.then(response => response.json())
656+
.then(data => {
657+
if (data.success) {
658+
readmeTextarea.value = data.content;
659+
readmeStatus.textContent = 'Loaded';
660+
readmeStatus.className = 'readme-status';
661+
} else {
662+
readmeStatus.textContent = 'Error loading: ' + data.error;
663+
readmeStatus.className = 'readme-status error';
664+
}
665+
})
666+
.catch(error => {
667+
console.error('Error loading readme:', error);
668+
readmeStatus.textContent = 'Error loading readme';
669+
readmeStatus.className = 'readme-status error';
670+
});
671+
}
672+
673+
function saveReadme() {
674+
const urlParams = new URLSearchParams(window.location.search);
675+
const folder = urlParams.get('folder');
676+
677+
if (!folder) return;
678+
679+
readmeStatus.textContent = 'Saving...';
680+
readmeStatus.className = 'readme-status saving';
681+
682+
fetch('/save-readme', {
683+
method: 'POST',
684+
headers: {
685+
'Content-Type': 'application/json',
686+
},
687+
body: JSON.stringify({
688+
selected_folder: folder,
689+
content: readmeTextarea.value
690+
})
691+
})
692+
.then(response => response.json())
693+
.then(data => {
694+
if (data.success) {
695+
readmeStatus.textContent = 'Saved';
696+
readmeStatus.className = 'readme-status saved';
697+
} else {
698+
readmeStatus.textContent = 'Error: ' + data.error;
699+
readmeStatus.className = 'readme-status error';
700+
}
701+
})
702+
.catch(error => {
703+
console.error('Error saving readme:', error);
704+
readmeStatus.textContent = 'Error saving';
705+
readmeStatus.className = 'readme-status error';
706+
});
707+
}
708+
709+
function setupReadmeAutosave() {
710+
readmeTextarea = document.getElementById('readme-textarea');
711+
readmeStatus = document.getElementById('readme-status');
712+
713+
if (!readmeTextarea || !readmeStatus) return;
714+
715+
// Load existing content
716+
loadReadme();
717+
718+
// Setup autosave on input
719+
readmeTextarea.addEventListener('input', function() {
720+
// Clear existing timeout
721+
if (readmeTimeout) {
722+
clearTimeout(readmeTimeout);
723+
}
724+
725+
// Set new timeout for autosave (1 second delay)
726+
readmeTimeout = setTimeout(saveReadme, 1000);
727+
728+
// Show typing indicator
729+
readmeStatus.textContent = 'Typing...';
730+
readmeStatus.className = 'readme-status';
731+
});
732+
}
733+
601734
// Initialize JSON editors when page loads
602735
document.addEventListener('DOMContentLoaded', function() {
603736
// Setup copy buttons
604737
setupCopyButtons();
738+
// Setup readme autosave
739+
setupReadmeAutosave();
605740
// Initialize metadata JSON editor
606741
const metadataEditor = new JSONEditor(document.getElementById('metadata-jsoneditor'), {
607742
mode: 'view',

0 commit comments

Comments
 (0)