Skip to content

Commit b5357d7

Browse files
committed
Ref(viewer): Move js out of html file
1 parent d16a806 commit b5357d7

7 files changed

Lines changed: 335 additions & 241 deletions

File tree

.cursor/rules/viewer.mdc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ two AI agent in a round-based game against each other.
1515
* Your input is a game folder (described below)
1616
* Use clean modern CSS, but don't go too crazy, simplicity is key
1717
* CSS and JS should be in separate static files, not inline in templates
18+
* **NEVER use inline JavaScript in HTML templates** - all JavaScript must be in separate .js files in the static/js directory
19+
* Organize JavaScript into logical modules: clipboard.js, experiment.js, readme.js, json-editors.js, etc.
1820
* The application consists of two main pages: the viewer and the game picker
1921

2022
## Input folder

codeclash/viewer/static/js/app.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,73 @@ function scrollToRound(roundNum) {
247247
}
248248
}
249249

250+
// Setup button event listeners
251+
function setupButtonEventListeners() {
252+
// Pick game button
253+
const pickGameBtn = document.getElementById("pick-game-btn");
254+
if (pickGameBtn) {
255+
pickGameBtn.addEventListener("mousedown", handlePickerClick);
256+
}
257+
258+
// Pick game new tab button
259+
const pickGameNewTabBtn = document.getElementById("pick-game-new-tab-btn");
260+
if (pickGameNewTabBtn) {
261+
pickGameNewTabBtn.addEventListener("click", openGamePickerInNewTab);
262+
}
263+
264+
// Theme toggle button
265+
const themeToggle = document.getElementById("theme-toggle");
266+
if (themeToggle) {
267+
themeToggle.addEventListener("click", toggleTheme);
268+
}
269+
270+
// Delete experiment button
271+
const deleteBtn = document.querySelector(".delete-experiment-btn");
272+
if (deleteBtn) {
273+
deleteBtn.addEventListener("click", function () {
274+
const folderPath = this.getAttribute("data-folder-path");
275+
if (folderPath) {
276+
deleteExperiment(folderPath);
277+
}
278+
});
279+
}
280+
281+
// Round navigation buttons
282+
document.querySelectorAll(".nav-to-round-btn").forEach((button) => {
283+
button.addEventListener("click", function () {
284+
const roundNum = this.getAttribute("data-round");
285+
if (roundNum) {
286+
scrollToRound(parseInt(roundNum));
287+
}
288+
});
289+
});
290+
291+
// Message expand/collapse buttons
292+
document.querySelectorAll(".clickable-message").forEach((element) => {
293+
element.addEventListener("click", function () {
294+
if (this.classList.contains("message-preview-short")) {
295+
expandMessage(this);
296+
} else if (this.classList.contains("collapse-indicator")) {
297+
collapseMessage(this);
298+
}
299+
});
300+
});
301+
302+
// Collapse trajectory messages buttons
303+
document.querySelectorAll(".btn-collapse-messages").forEach((button) => {
304+
button.addEventListener("click", function () {
305+
collapseTrajectoryMessages(this);
306+
});
307+
});
308+
}
309+
250310
// Initialize everything when DOM is loaded
251311
document.addEventListener("DOMContentLoaded", function () {
252312
initializeTheme();
253313
initializeFoldouts();
254314
initializeKeyboardShortcuts();
255315
initializePerformanceMonitoring();
316+
setupButtonEventListeners();
256317

257318
console.log("CodeClash Trajectory Viewer initialized");
258319
console.log("Keyboard shortcuts:");
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Clipboard functionality for CodeClash Trajectory Viewer
2+
3+
// Copy to clipboard function
4+
function copyToClipboard(text, button) {
5+
function showSuccessMessage() {
6+
if (button) {
7+
const originalText = button.textContent;
8+
const originalColor = button.style.color;
9+
button.textContent = "✓ Copied";
10+
button.style.color = "green";
11+
setTimeout(() => {
12+
button.textContent = originalText;
13+
button.style.color = originalColor;
14+
}, 1500);
15+
}
16+
}
17+
18+
// Check if modern clipboard API is available
19+
if (navigator.clipboard && navigator.clipboard.writeText) {
20+
navigator.clipboard
21+
.writeText(text)
22+
.then(function () {
23+
showSuccessMessage();
24+
console.log("Copied to clipboard:", text);
25+
})
26+
.catch(function (err) {
27+
console.error("Failed to copy text with clipboard API: ", err);
28+
// Fall back to legacy method
29+
fallbackCopy();
30+
});
31+
} else {
32+
// Use fallback method directly
33+
fallbackCopy();
34+
}
35+
36+
function fallbackCopy() {
37+
try {
38+
const textArea = document.createElement("textarea");
39+
textArea.value = text;
40+
textArea.style.position = "fixed";
41+
textArea.style.left = "-9999px";
42+
textArea.style.top = "-9999px";
43+
document.body.appendChild(textArea);
44+
textArea.focus();
45+
textArea.select();
46+
47+
const successful = document.execCommand("copy");
48+
document.body.removeChild(textArea);
49+
50+
if (successful) {
51+
showSuccessMessage();
52+
console.log("Copied to clipboard (fallback):", text);
53+
} else {
54+
console.error("Failed to copy text with fallback method");
55+
if (button) {
56+
button.textContent = "✗ Failed";
57+
button.style.color = "red";
58+
setTimeout(() => {
59+
button.textContent = button.getAttribute("title") || "Copy";
60+
button.style.color = "";
61+
}, 1500);
62+
}
63+
}
64+
} catch (err) {
65+
console.error("Fallback copy failed:", err);
66+
if (button) {
67+
button.textContent = "✗ Failed";
68+
button.style.color = "red";
69+
setTimeout(() => {
70+
button.textContent = button.getAttribute("title") || "Copy";
71+
button.style.color = "";
72+
}, 1500);
73+
}
74+
}
75+
}
76+
}
77+
78+
// Setup copy button event listeners
79+
function setupCopyButtons() {
80+
// Add event listeners to all copy path buttons
81+
document
82+
.querySelectorAll(".copy-path-btn, .copy-path-btn-small")
83+
.forEach((button) => {
84+
button.addEventListener("click", function (e) {
85+
e.preventDefault();
86+
e.stopPropagation();
87+
88+
const path = this.getAttribute("data-path");
89+
if (path) {
90+
copyToClipboard(path, this);
91+
} else {
92+
console.error("No path found on button:", this);
93+
}
94+
});
95+
});
96+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Experiment management functionality for CodeClash Trajectory Viewer
2+
3+
// Delete experiment function
4+
function deleteExperiment(folderPath) {
5+
const confirmed = confirm(
6+
"Are you sure you want to delete this experiment? This action cannot be undone.\n\nFolder: " +
7+
folderPath,
8+
);
9+
if (confirmed) {
10+
fetch("/delete-experiment", {
11+
method: "POST",
12+
headers: {
13+
"Content-Type": "application/json",
14+
},
15+
body: JSON.stringify({
16+
folder_path: folderPath,
17+
}),
18+
})
19+
.then((response) => response.json())
20+
.then((data) => {
21+
if (data.success) {
22+
alert("Experiment deleted successfully.");
23+
// Redirect to picker
24+
window.location.href = "/picker";
25+
} else {
26+
alert(
27+
"Error deleting experiment: " + (data.error || "Unknown error"),
28+
);
29+
}
30+
})
31+
.catch((error) => {
32+
console.error("Error:", error);
33+
alert("Error deleting experiment: " + error.message);
34+
});
35+
}
36+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// JSON Editor initialization for CodeClash Trajectory Viewer
2+
3+
// Initialize JSON editors when page loads
4+
function initializeJSONEditors(metadata) {
5+
// Initialize metadata JSON editor
6+
const metadataContainer = document.getElementById("metadata-jsoneditor");
7+
if (metadataContainer) {
8+
const metadataEditor = new JSONEditor(metadataContainer, {
9+
mode: "view",
10+
modes: ["view", "tree"],
11+
name: "metadata",
12+
});
13+
metadataEditor.set(metadata.results);
14+
}
15+
16+
// Initialize round results JSON editors
17+
metadata.rounds.forEach((roundData) => {
18+
if (roundData.results) {
19+
const roundContainer = document.getElementById(
20+
`round-${roundData.round_num}-results-jsoneditor`,
21+
);
22+
if (roundContainer) {
23+
const roundEditor = new JSONEditor(roundContainer, {
24+
mode: "view",
25+
modes: ["view", "tree"],
26+
name: `round_${roundData.round_num}_results`,
27+
});
28+
roundEditor.set(roundData.results);
29+
}
30+
}
31+
});
32+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Readme functionality for CodeClash Trajectory Viewer
2+
3+
let readmeTimeout = null;
4+
let readmeTextarea = null;
5+
let readmeStatus = null;
6+
7+
function loadReadme() {
8+
const urlParams = new URLSearchParams(window.location.search);
9+
const folder = urlParams.get("folder");
10+
11+
if (!folder) return;
12+
13+
fetch("/load-readme?folder=" + encodeURIComponent(folder))
14+
.then((response) => response.json())
15+
.then((data) => {
16+
if (data.success) {
17+
readmeTextarea.value = data.content;
18+
readmeStatus.textContent = "Loaded";
19+
readmeStatus.className = "readme-status";
20+
} else {
21+
readmeStatus.textContent = "Error loading: " + data.error;
22+
readmeStatus.className = "readme-status error";
23+
}
24+
})
25+
.catch((error) => {
26+
console.error("Error loading readme:", error);
27+
readmeStatus.textContent = "Error loading readme";
28+
readmeStatus.className = "readme-status error";
29+
});
30+
}
31+
32+
function saveReadme() {
33+
const urlParams = new URLSearchParams(window.location.search);
34+
const folder = urlParams.get("folder");
35+
36+
if (!folder) return;
37+
38+
readmeStatus.textContent = "Saving...";
39+
readmeStatus.className = "readme-status saving";
40+
41+
fetch("/save-readme", {
42+
method: "POST",
43+
headers: {
44+
"Content-Type": "application/json",
45+
},
46+
body: JSON.stringify({
47+
selected_folder: folder,
48+
content: readmeTextarea.value,
49+
}),
50+
})
51+
.then((response) => response.json())
52+
.then((data) => {
53+
if (data.success) {
54+
readmeStatus.textContent = "Saved";
55+
readmeStatus.className = "readme-status saved";
56+
} else {
57+
readmeStatus.textContent = "Error: " + data.error;
58+
readmeStatus.className = "readme-status error";
59+
}
60+
})
61+
.catch((error) => {
62+
console.error("Error saving readme:", error);
63+
readmeStatus.textContent = "Error saving";
64+
readmeStatus.className = "readme-status error";
65+
});
66+
}
67+
68+
function setupReadmeAutosave() {
69+
readmeTextarea = document.getElementById("readme-textarea");
70+
readmeStatus = document.getElementById("readme-status");
71+
72+
if (!readmeTextarea || !readmeStatus) return;
73+
74+
// Load existing content
75+
loadReadme();
76+
77+
// Setup autosave on input
78+
readmeTextarea.addEventListener("input", function () {
79+
// Clear existing timeout
80+
if (readmeTimeout) {
81+
clearTimeout(readmeTimeout);
82+
}
83+
84+
// Set new timeout for autosave (1 second delay)
85+
readmeTimeout = setTimeout(saveReadme, 1000);
86+
87+
// Show typing indicator
88+
readmeStatus.textContent = "Typing...";
89+
readmeStatus.className = "readme-status";
90+
});
91+
}

0 commit comments

Comments
 (0)