const API_URL = window.location.origin; // Global state let currentEpisodes = []; let chartInstance = null; // DOM Elements const statusIndicator = document.getElementById('api-status'); const statusText = document.getElementById('api-status-text'); // Initialize document.addEventListener('DOMContentLoaded', () => { initNavigation(); checkApiStatus(); setInterval(checkApiStatus, 10000); }); // Navigation function initNavigation() { document.querySelectorAll('.nav-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); const target = e.currentTarget; target.classList.add('active'); const tabId = target.getAttribute('data-tab'); document.getElementById(`tab-${tabId}`).classList.add('active'); // Tab specific logic if (tabId === 'dashboard') loadStats(); if (tabId === 'runner') loadScenariosForRunner(); if (tabId === 'replay') loadRecentReplays(); if (tabId === 'scenarios') loadScenarios(); }); }); } // API Health async function checkApiStatus() { try { const res = await fetch(`${API_URL}/`); if (res.ok) { statusIndicator.className = 'status-indicator online'; statusText.textContent = 'API Online'; if (currentEpisodes.length === 0) { // Initial load loadStats(); loadScenariosForRunner(); } } else { throw new Error('Bad response'); } } catch (e) { statusIndicator.className = 'status-indicator error'; statusText.textContent = 'API Offline'; } } async function apiGet(path) { try { const res = await fetch(`${API_URL}${path}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { console.error(`API GET ${path} failed:`, err); return null; } } // ========================================== // DASHBOARD (Chart.js & Stats) // ========================================== async function loadStats() { try { const stats = await apiGet('/stats'); if (!stats) return; document.getElementById('stat-total-episodes').textContent = stats.total_episodes || 0; let totalEps = 0; let totalSolved = 0; let totalReward = 0; Object.values(stats.levels || {}).forEach(l => { totalEps += l.count; totalSolved += l.solve_rate * l.count; totalReward += l.mean_reward * l.count; }); if (totalEps > 0) { document.getElementById('stat-overall-solve-rate').textContent = `${((totalSolved / totalEps) * 100).toFixed(1)}%`; document.getElementById('stat-mean-reward').textContent = (totalReward / totalEps).toFixed(1); } else { document.getElementById('stat-overall-solve-rate').textContent = `0%`; document.getElementById('stat-mean-reward').textContent = `0.0`; } const recentData = await apiGet('/recent?n=100'); if (recentData && recentData.episodes && recentData.episodes.length > 0) { currentEpisodes = recentData.episodes; const rev = [...recentData.episodes].reverse(); let sumSolved = 0; let chartLabels = []; let rewardData = []; let solveData = []; rev.forEach((ep, i) => { if (ep.solved) sumSolved++; chartLabels.push(`Ep ${ep.episode_id.slice(0,4)}`); rewardData.push(ep.total_reward); solveData.push((sumSolved / (i+1)) * 100); }); renderChart(chartLabels, rewardData, solveData); } renderLevelCards(stats.levels || {}); } catch (e) { console.error('Failed to load stats', e); } } function renderChart(labels, rewardData, solveData) { const ctx = document.getElementById('rewardChart').getContext('2d'); if (chartInstance) chartInstance.destroy(); chartInstance = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [ { label: 'Total Reward', data: rewardData, borderColor: '#00f0ff', backgroundColor: 'rgba(0, 240, 255, 0.1)', borderWidth: 2, tension: 0.3, fill: true, yAxisID: 'y' }, { label: 'Solve Rate (%)', data: solveData, borderColor: '#00ff9d', borderWidth: 2, borderDash: [5, 5], tension: 0.3, yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, scales: { x: { display: false }, y: { type: 'linear', display: true, position: 'left', grid: { color: 'rgba(255,255,255,0.05)' } }, y1: { type: 'linear', display: true, position: 'right', grid: { drawOnChartArea: false }, min: 0, max: 100 } }, plugins: { legend: { labels: { color: '#e2e8f0', font: { family: 'JetBrains Mono' } } } } } }); } function renderLevelCards(levels) { const container = document.getElementById('level-cards-container'); container.innerHTML = ''; [1, 2, 3].forEach(level => { const stats = levels[level] || { solve_rate: 0, count: 0 }; const pct = (stats.solve_rate * 100).toFixed(1); const div = document.createElement('div'); div.className = 'level-stat-row'; div.innerHTML = `
Level ${level} ${pct}% (${stats.count} runs)
`; container.appendChild(div); }); } // ========================================== // RUNNER & TERMINAL // ========================================== async function loadScenariosForRunner() { const data = await apiGet('/scenarios'); if (!data) return; const select = document.getElementById('scenario-select'); select.innerHTML = ''; data.scenarios.forEach(scen => { const opt = document.createElement('option'); opt.value = scen.id; opt.textContent = `${scen.id} (Level ${scen.level})`; select.appendChild(opt); }); } document.getElementById('run-episode-btn').addEventListener('click', async (e) => { const btn = e.currentTarget; btn.disabled = true; btn.innerHTML = 'EXECUTING...'; const sid = document.getElementById('scenario-select').value; const lvl = document.getElementById('level-select').value; const output = document.getElementById('runner-output'); output.innerHTML = '
_ Initializing Docker sandbox...
'; try { const body = {}; if (sid) body.scenario_id = sid; if (lvl) body.level = parseInt(lvl); const res = await fetch(`${API_URL}/episode/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const data = await res.json(); if (!res.ok) throw new Error("Failed to run episode."); renderTerminalExecution(output, data); loadStats(); } catch (err) { output.innerHTML = `
Execution Failed: ${err.message}
`; } finally { btn.disabled = false; btn.innerHTML = 'INITIALIZE RUN'; } }); function renderTerminalExecution(container, data) { container.innerHTML = ''; const title = document.createElement('div'); title.style.marginBottom = '1rem'; title.innerHTML = `Scenario: ${data.scenario_id}`; container.appendChild(title); data.steps.forEach(step => { const block = document.createElement('div'); block.className = 'term-step'; let statusClass = 'term-success'; if (step.execution_result?.blocked) statusClass = 'term-blocked'; else if (step.execution_result?.exit_code !== 0) statusClass = 'term-error'; const rewardClass = step.reward >= 0 ? 'pos' : 'neg'; const rewardSign = step.reward >= 0 ? '+' : ''; let statusText = step.execution_result?.blocked ? '⚠ BLOCKED' : step.solved ? '✓ SOLVED' : step.execution_result?.exit_code === 0 ? 'ok (exit 0)' : `failed (exit ${step.execution_result?.exit_code})`; block.innerHTML = `
${step.action}
↳ ${statusText} ${rewardSign}${step.reward.toFixed(1)}
`; container.appendChild(block); }); const sum = document.createElement('div'); sum.className = 'term-summary'; const finalStatus = data.solved ? 'SOLVED ✓' : 'FAILED ✗'; sum.innerHTML = ` Result: ${finalStatus}
Steps: ${data.total_steps}
Total Reward: ${data.total_reward.toFixed(1)} `; container.appendChild(sum); } // ========================================== // REPLAY VIEWER // ========================================== async function loadRecentReplays() { const data = await apiGet('/recent?n=20'); if (!data) return; const list = document.getElementById('replay-recent-list'); list.innerHTML = ''; data.episodes.forEach(ep => { const item = document.createElement('div'); item.className = 'history-item'; const rClass = ep.total_reward >= 0 ? 'pos' : 'neg'; const rSign = ep.total_reward >= 0 ? '+' : ''; const bClass = ep.solved ? 'win' : ''; const bText = ep.solved ? 'SOLVED' : 'FAILED'; item.innerHTML = `
${ep.episode_id.split('-')[0]} ${rSign}${ep.total_reward.toFixed(1)}
${ep.scenario_id} ${bText}
`; item.addEventListener('click', async () => { const term = document.getElementById('replay-viewer'); document.getElementById('replay-title').textContent = `replay_${ep.episode_id.split('-')[0]}.sh`; try { const epData = await apiGet(`/replay/${ep.episode_id}`); renderTerminalExecution(term, epData); } catch (e) { term.innerHTML = '
Failed to load replay data
'; } }); list.appendChild(item); }); } // ========================================== // SCENARIOS REGISTRY // ========================================== async function loadScenarios() { const scData = await apiGet('/scenarios'); const stData = await apiGet('/stats'); if (!scData || !stData) return; const list = document.getElementById('scenarios-list'); list.innerHTML = ''; scData.scenarios.forEach(sc => { const scStats = (stData.scenarios && stData.scenarios[sc.id]) || { count: 0, solve_rate: 0 }; const card = document.createElement('div'); card.className = 'glass-panel scenario-card'; card.innerHTML = `
${sc.id}
${sc.description}
${(sc.hint_commands||[]).map(h => `${h}`).join('')}
Level ${sc.level} ${(scStats.solve_rate * 100).toFixed(1)}% Solved (${scStats.count})
`; list.appendChild(card); }); } function escapeHtml(unsafe) { return (unsafe||'').replace(/&/g, "&").replace(//g, ">"); }