| const taskSelect = document.getElementById("task-select"); | |
| const taskSummary = document.getElementById("task-summary"); | |
| const currentScore = document.getElementById("current-score"); | |
| const currentSteps = document.getElementById("current-steps"); | |
| const currentStatus = document.getElementById("current-status"); | |
| const allScore = document.getElementById("all-score"); | |
| const allResults = document.getElementById("all-results"); | |
| const episodeLog = document.getElementById("episode-log"); | |
| const rewardChart = document.getElementById("reward-chart"); | |
| const finalSummary = document.getElementById("final-summary"); | |
| let taskCatalog = []; | |
| function renderTaskSummary(task) { | |
| taskSummary.innerHTML = ` | |
| <h3>${task.name}</h3> | |
| <p><strong>Difficulty:</strong> ${task.difficulty}</p> | |
| <p>${task.objective}</p> | |
| <p><strong>Max steps:</strong> ${task.max_steps}</p> | |
| `; | |
| } | |
| function buildLineChart(logs) { | |
| if (!logs.length) { | |
| rewardChart.innerHTML = "No rewards available."; | |
| return; | |
| } | |
| const width = 380; | |
| const height = 220; | |
| const padding = 28; | |
| const values = logs.map((entry) => entry.reward); | |
| const maxReward = Math.max(...values, 1); | |
| const minReward = Math.min(...values, 0); | |
| const range = Math.max(maxReward - minReward, 0.25); | |
| const toX = (index) => { | |
| if (logs.length === 1) { | |
| return width / 2; | |
| } | |
| return padding + (index * (width - padding * 2)) / (logs.length - 1); | |
| }; | |
| const toY = (value) => { | |
| return height - padding - ((value - minReward) / range) * (height - padding * 2); | |
| }; | |
| const linePoints = logs | |
| .map((entry, index) => `${toX(index)},${toY(entry.reward)}`) | |
| .join(" "); | |
| const horizontalGuides = [0, 0.25, 0.5, 0.75, 1] | |
| .map((ratio) => { | |
| const y = padding + ratio * (height - padding * 2); | |
| return `<line class="chart-grid" x1="${padding}" y1="${y}" x2="${width - padding}" y2="${y}"></line>`; | |
| }) | |
| .join(""); | |
| const labels = logs | |
| .map((entry, index) => { | |
| const x = toX(index); | |
| return `<text class="chart-label" x="${x}" y="${height - 8}" text-anchor="middle">S${entry.step}</text>`; | |
| }) | |
| .join(""); | |
| const points = logs | |
| .map((entry, index) => { | |
| const x = toX(index); | |
| const y = toY(entry.reward); | |
| return ` | |
| <circle class="chart-point" cx="${x}" cy="${y}" r="5"></circle> | |
| <text class="chart-label" x="${x}" y="${y - 10}" text-anchor="middle">${entry.reward.toFixed(2)}</text> | |
| `; | |
| }) | |
| .join(""); | |
| rewardChart.innerHTML = ` | |
| <svg viewBox="0 0 ${width} ${height}" aria-label="Reward line chart"> | |
| ${horizontalGuides} | |
| <line class="chart-axis" x1="${padding}" y1="${height - padding}" x2="${width - padding}" y2="${height - padding}"></line> | |
| <line class="chart-axis" x1="${padding}" y1="${padding}" x2="${padding}" y2="${height - padding}"></line> | |
| <polyline class="chart-line" points="${linePoints}"></polyline> | |
| ${points} | |
| ${labels} | |
| </svg> | |
| `; | |
| } | |
| function renderEpisode(data) { | |
| currentScore.textContent = data.score.toFixed(4); | |
| currentSteps.textContent = String(data.steps_taken); | |
| currentStatus.textContent = data.success ? "Contained" : "Needs work"; | |
| buildLineChart(data.logs); | |
| finalSummary.innerHTML = ` | |
| <div class="summary-grid"> | |
| <div class="summary-pill"> | |
| <span>Final score</span> | |
| <strong>${data.score.toFixed(4)}</strong> | |
| </div> | |
| <div class="summary-pill"> | |
| <span>Status</span> | |
| <strong>${data.success ? "Success" : "Needs improvement"}</strong> | |
| </div> | |
| <div class="summary-pill"> | |
| <span>Steps used</span> | |
| <strong>${data.steps_taken}</strong> | |
| </div> | |
| <div class="summary-pill"> | |
| <span>Quarantine quality</span> | |
| <strong>${(data.final_info.quarantine_score ?? 0).toFixed(4)}</strong> | |
| </div> | |
| </div> | |
| <div class="summary-card"> | |
| <strong>Containment outcome</strong> | |
| <div>All affected nodes notified: ${data.final_info.all_affected_nodes_notified ? "Yes" : "No"}</div> | |
| <div>All affected stock quarantined: ${data.final_info.all_affected_stock_quarantined ? "Yes" : "No"}</div> | |
| </div> | |
| <div class="summary-card"> | |
| <strong>Grader focus</strong> | |
| <div>Notification score: ${(data.final_info.notification_score ?? 0).toFixed(4)}</div> | |
| <div>Investigation score: ${(data.final_info.investigation_score ?? 0).toFixed(4)}</div> | |
| <div>Efficiency score: ${(data.final_info.efficiency_score ?? 0).toFixed(4)}</div> | |
| </div> | |
| `; | |
| const logMarkup = data.logs.map((entry) => { | |
| const actionType = entry.action.type || "action"; | |
| const detailBits = []; | |
| if (entry.action.node_id) detailBits.push(`Node: ${entry.action.node_id}`); | |
| if (entry.action.lot_id) detailBits.push(`Lot: ${entry.action.lot_id}`); | |
| if (entry.action.quantity) detailBits.push(`Qty: ${entry.action.quantity}`); | |
| return ` | |
| <div class="log-step"> | |
| <div class="log-title"> | |
| <strong>Step ${entry.step}</strong> | |
| <span class="action-chip">${actionType.replace("_", " ")}</span> | |
| </div> | |
| <div class="action-meta"> | |
| <div>${detailBits.length ? detailBits.join(" | ") : "No extra parameters"}</div> | |
| <div>Reward: ${entry.reward.toFixed(4)}</div> | |
| <div>Message: ${entry.message || "-"}</div> | |
| </div> | |
| </div> | |
| `; | |
| }).join(""); | |
| episodeLog.innerHTML = ` | |
| <div class="log-step"> | |
| <strong>Task:</strong> ${data.task.name} | |
| </div> | |
| ${logMarkup} | |
| `; | |
| } | |
| function renderRunAll(data) { | |
| allScore.textContent = data.average_score.toFixed(4); | |
| allResults.innerHTML = data.episodes.map((episode) => ` | |
| <div class="log-step"> | |
| <strong>${episode.task.name}</strong> | |
| <div>Difficulty: ${episode.task.difficulty}</div> | |
| <div>Score: ${episode.score.toFixed(4)}</div> | |
| <div>Steps: ${episode.steps_taken}</div> | |
| <div>Status: ${episode.success ? "Success" : "Needs work"}</div> | |
| </div> | |
| `).join(""); | |
| } | |
| async function fetchTasks() { | |
| const response = await fetch("/api/tasks"); | |
| const data = await response.json(); | |
| taskCatalog = data.tasks; | |
| taskSelect.innerHTML = taskCatalog.map((task) => ` | |
| <option value="${task.task_id}">${task.difficulty.toUpperCase()} - ${task.name}</option> | |
| `).join(""); | |
| renderTaskSummary(taskCatalog[0]); | |
| } | |
| async function resetTask() { | |
| const taskId = taskSelect.value; | |
| const response = await fetch(`/reset?task_id=${encodeURIComponent(taskId)}`); | |
| const data = await response.json(); | |
| currentScore.textContent = "-"; | |
| currentSteps.textContent = String(data.steps_taken || 0); | |
| currentStatus.textContent = "Reset"; | |
| rewardChart.innerHTML = "Task reset. Run a task to render the reward trajectory."; | |
| finalSummary.innerHTML = "Readable scoring highlights will appear here."; | |
| episodeLog.textContent = JSON.stringify(data, null, 2); | |
| } | |
| async function runEpisode() { | |
| const response = await fetch("/api/run_episode", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ task_id: taskSelect.value }), | |
| }); | |
| const data = await response.json(); | |
| renderEpisode(data); | |
| } | |
| async function runAllTasks() { | |
| const response = await fetch("/api/run_all"); | |
| const data = await response.json(); | |
| renderRunAll(data); | |
| } | |
| taskSelect.addEventListener("change", () => { | |
| const task = taskCatalog.find((item) => item.task_id === taskSelect.value); | |
| if (task) { | |
| renderTaskSummary(task); | |
| } | |
| }); | |
| document.getElementById("reset-button").addEventListener("click", resetTask); | |
| document.getElementById("run-button").addEventListener("click", runEpisode); | |
| document.getElementById("run-all-button").addEventListener("click", runAllTasks); | |
| fetchTasks(); | |