printf-sourav's picture
readme fixed
3a2d933
Raw
History Blame Contribute Delete
13.4 kB
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 = `
<div class="top">
<span class="name">Level ${level}</span>
<span class="rate">${pct}% (${stats.count} runs)</span>
</div>
<div class="progress-track">
<div class="progress-fill" style="width: ${pct}%"></div>
</div>
`;
container.appendChild(div);
});
}
// ==========================================
// RUNNER & TERMINAL
// ==========================================
async function loadScenariosForRunner() {
const data = await apiGet('/scenarios');
if (!data) return;
const select = document.getElementById('scenario-select');
select.innerHTML = '<option value="">(Random Scenario)</option>';
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 = '<span class="btn-text">EXECUTING...</span><span class="btn-icon spinner"></span>';
const sid = document.getElementById('scenario-select').value;
const lvl = document.getElementById('level-select').value;
const output = document.getElementById('runner-output');
output.innerHTML = '<div class="term-empty-state"><span class="blink">_</span> Initializing Docker sandbox...</div>';
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 = `<div class="term-error">Execution Failed: ${err.message}</div>`;
} finally {
btn.disabled = false;
btn.innerHTML = '<span class="btn-text">INITIALIZE RUN</span><span class="btn-icon">⚡</span>';
}
});
function renderTerminalExecution(container, data) {
container.innerHTML = '';
const title = document.createElement('div');
title.style.marginBottom = '1rem';
title.innerHTML = `<strong>Scenario:</strong> <span style="color:#00f0ff">${data.scenario_id}</span>`;
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 = `
<div class="term-cmd">${step.action}</div>
<div class="term-result ${statusClass}">
${statusText}
<span class="term-reward ${rewardClass}">${rewardSign}${step.reward.toFixed(1)}</span>
</div>
`;
container.appendChild(block);
});
const sum = document.createElement('div');
sum.className = 'term-summary';
const finalStatus = data.solved ? '<span class="term-success">SOLVED ✓</span>' : '<span class="term-error">FAILED ✗</span>';
sum.innerHTML = `
<strong>Result:</strong> ${finalStatus}<br>
<strong>Steps:</strong> ${data.total_steps}<br>
<strong>Total Reward:</strong> ${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 = `
<div class="hi-top">
<span class="hi-id">${ep.episode_id.split('-')[0]}</span>
<span class="hi-reward ${rClass}">${rSign}${ep.total_reward.toFixed(1)}</span>
</div>
<div class="hi-scenario">${ep.scenario_id} <span class="hi-badge ${bClass}">${bText}</span></div>
`;
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 = '<div class="term-error">Failed to load replay data</div>';
}
});
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 = `
<div class="sc-name">${sc.id}</div>
<div class="sc-desc">${sc.description}</div>
<div class="sc-hints">
${(sc.hint_commands||[]).map(h => `<span>${h}</span>`).join('')}
</div>
<div class="sc-stats">
<span>Level ${sc.level}</span>
<span style="color:var(--success)">${(scStats.solve_rate * 100).toFixed(1)}% Solved (${scStats.count})</span>
</div>
`;
list.appendChild(card);
});
}
function escapeHtml(unsafe) {
return (unsafe||'').replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}