build-small-hackathon/Hackathon-IA-VisualNovel
5
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>VN Debug Dashboard</title>7 <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js"></script>8 <style>9 :root {10 --bg: #0f1117;11 --surface: #1a1d27;12 --border: #252836;13 --text: #dde1f0;14 --muted: #6e7891;15 --blue: #4c9be8;16 --red: #e05c5c;17 --green: #56b87a;18 --yellow: #e0a840;19 --orange: #e09440;20 --purple: #b07de8;21 --gray: #4e5568;22 }23 * { box-sizing: border-box; margin: 0; padding: 0; }24 body {25 background: var(--bg);26 color: var(--text);27 font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;28 font-size: 13px;29 padding: 16px;30 min-height: 100vh;31 }32 33 /* ── Header ─────────────────────────────────────────────────── */34 header {35 display: flex;36 align-items: center;37 justify-content: space-between;38 margin-bottom: 16px;39 }40 header h1 {41 font-size: 17px;42 font-weight: 700;43 letter-spacing: -.02em;44 }45 header h1 span { color: var(--blue); }46 .status {47 display: flex;48 align-items: center;49 gap: 7px;50 font-size: 12px;51 color: var(--muted);52 }53 .dot {54 width: 8px; height: 8px;55 border-radius: 50%;56 background: var(--gray);57 flex-shrink: 0;58 transition: background 0.3s;59 }60 .dot.live { background: var(--green); box-shadow: 0 0 6px var(--green); }61 .dot.error { background: var(--red); }62 63 /* ── KPI cards ──────────────────────────────────────────────── */64 .kpi-grid {65 display: grid;66 grid-template-columns: repeat(4, 1fr);67 gap: 10px;68 margin-bottom: 12px;69 }70 @media (max-width: 800px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } }71 .kpi {72 background: var(--surface);73 border: 1px solid var(--border);74 border-radius: 8px;75 padding: 12px 14px;76 }77 .kpi-label {78 font-size: 10px;79 font-weight: 600;80 text-transform: uppercase;81 letter-spacing: .07em;82 color: var(--muted);83 margin-bottom: 5px;84 }85 .kpi-value {86 font-size: 28px;87 font-weight: 800;88 line-height: 1;89 font-variant-numeric: tabular-nums;90 color: var(--text);91 }92 .kpi-sub {93 font-size: 11px;94 color: var(--muted);95 margin-top: 5px;96 }97 .c-blue { color: var(--blue) !important; }98 .c-green { color: var(--green) !important; }99 .c-yellow { color: var(--yellow) !important; }100 .c-red { color: var(--red) !important; }101 102 /* ── Charts ──────────────────────────────────────────────────── */103 .charts-grid {104 display: grid;105 grid-template-columns: 1fr 1fr;106 gap: 10px;107 margin-bottom: 12px;108 }109 @media (max-width: 900px) { .charts-grid { grid-template-columns: 1fr; } }110 .card {111 background: var(--surface);112 border: 1px solid var(--border);113 border-radius: 8px;114 padding: 14px;115 }116 .card h2 {117 font-size: 10px;118 font-weight: 700;119 text-transform: uppercase;120 letter-spacing: .07em;121 color: var(--muted);122 margin-bottom: 10px;123 }124 125 /* ── Operations table ────────────────────────────────────────── */126 table {127 width: 100%;128 border-collapse: collapse;129 font-size: 12px;130 }131 th {132 text-align: right;133 font-size: 10px;134 font-weight: 700;135 text-transform: uppercase;136 letter-spacing: .05em;137 color: var(--muted);138 padding: 0 8px 8px;139 border-bottom: 1px solid var(--border);140 }141 th:first-child { text-align: left; }142 td {143 padding: 6px 8px;144 border-bottom: 1px solid var(--border);145 text-align: right;146 font-variant-numeric: tabular-nums;147 color: var(--text);148 }149 td:first-child { text-align: left; color: var(--muted); }150 tr:last-child td { border-bottom: none; }151 tr:hover td { background: rgba(255,255,255,.025); }152 .total-row td { font-weight: 700; color: var(--text) !important; }153 154 /* ── Actions ─────────────────────────────────────────────────── */155 .actions {156 display: flex;157 align-items: center;158 gap: 10px;159 margin-top: 12px;160 }161 button {162 background: var(--blue);163 color: #fff;164 border: none;165 border-radius: 6px;166 padding: 7px 14px;167 font-size: 12px;168 font-weight: 700;169 cursor: pointer;170 opacity: 1;171 transition: opacity .15s;172 }173 button:hover:not(:disabled) { opacity: .82; }174 button:disabled { opacity: .35; cursor: default; }175 #report-msg { font-size: 12px; color: var(--green); }176 </style>177</head>178<body>179 180<!-- ── Header ─────────────────────────────────────────────────────── -->181<header>182 <h1>VN <span>Debug</span> Dashboard</h1>183 <div class="status">184 <div class="dot" id="dot"></div>185 <span id="status-text">Connecting…</span>186 </div>187</header>188 189<!-- ── KPI cards ──────────────────────────────────────────────────── -->190<div class="kpi-grid">191 <div class="kpi">192 <div class="kpi-label">Turns</div>193 <div class="kpi-value c-blue" id="kpi-turns">—</div>194 <div class="kpi-sub">completed</div>195 </div>196 <div class="kpi">197 <div class="kpi-label">LLM avg</div>198 <div class="kpi-value" id="kpi-llm">—</div>199 <div class="kpi-sub" id="kpi-llm-sub">p95 —</div>200 </div>201 <div class="kpi">202 <div class="kpi-label">Image cache</div>203 <div class="kpi-value" id="kpi-cache">—</div>204 <div class="kpi-sub" id="kpi-cache-sub">hits / total</div>205 </div>206 <div class="kpi">207 <div class="kpi-label">Session</div>208 <div class="kpi-value c-blue" id="kpi-elapsed">—</div>209 <div class="kpi-sub" id="kpi-gpu">—</div>210 </div>211</div>212 213<!-- ── Charts ─────────────────────────────────────────────────────── -->214<div class="charts-grid">215 <div class="card">216 <h2>System Resources — last 2 min</h2>217 <canvas id="res-chart" height="170"></canvas>218 </div>219 <div class="card">220 <h2>Latency per Turn (ms, stacked)</h2>221 <canvas id="lat-chart" height="170"></canvas>222 </div>223</div>224 225<!-- ── Operations table ───────────────────────────────────────────── -->226<div class="card">227 <h2>Operations</h2>228 <table>229 <thead>230 <tr>231 <th>Operation</th>232 <th>n</th>233 <th>avg ms</th>234 <th>p95 ms</th>235 <th>max ms</th>236 </tr>237 </thead>238 <tbody id="ops-body"></tbody>239 </table>240</div>241 242<!-- ── Actions ────────────────────────────────────────────────────── -->243<div class="actions">244 <button id="save-btn" onclick="saveReport()">Save report now</button>245 <span id="report-msg"></span>246</div>247 248<script>249// ── Chart defaults ───────────────────────────────────────────────────────250Chart.defaults.color = '#6e7891';251Chart.defaults.borderColor = '#252836';252Chart.defaults.font.size = 10;253 254const GRID_COLOR = 'rgba(255,255,255,.04)';255const BASE_SCALES = {256 x: { ticks: { color: '#555', maxTicksLimit: 8 }, grid: { color: GRID_COLOR } },257 y: { ticks: { color: '#555' }, grid: { color: GRID_COLOR } },258};259 260// ── Resource chart (line) ────────────────────────────────────────────────261const resCtx = document.getElementById('res-chart').getContext('2d');262const resChart = new Chart(resCtx, {263 type: 'line',264 data: {265 labels: [],266 datasets: [267 {268 label: 'RAM %', yAxisID: 'yPct', data: [],269 borderColor: '#4c9be8', backgroundColor: 'rgba(76,155,232,.06)',270 borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3,271 },272 {273 label: 'CPU %', yAxisID: 'yPct', data: [],274 borderColor: '#e05c5c', backgroundColor: 'rgba(224,92,92,.05)',275 borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3,276 },277 {278 label: 'VRAM', yAxisID: 'yVram', data: [],279 borderColor: '#e09440', backgroundColor: 'rgba(224,148,64,.05)',280 borderWidth: 1.5, pointRadius: 0, fill: false, tension: 0.3,281 },282 ]283 },284 options: {285 animation: false,286 responsive: true,287 interaction: { mode: 'index', intersect: false },288 plugins: {289 legend: { labels: { color: '#888', boxWidth: 10, font: { size: 10 } } },290 },291 scales: {292 x: BASE_SCALES.x,293 yPct: {294 type: 'linear', position: 'left', min: 0, suggestedMax: 100,295 ticks: { color: '#555', callback: v => v + '%' },296 grid: { color: GRID_COLOR },297 },298 yVram: {299 type: 'linear', position: 'right', min: 0,300 ticks: { color: '#7a6040' },301 grid: { drawOnChartArea: false },302 display: false, // shown only when VRAM data present303 },304 },305 },306});307 308// ── Latency stacked bar ──────────────────────────────────────────────────309const latCtx = document.getElementById('lat-chart').getContext('2d');310const latChart = new Chart(latCtx, {311 type: 'bar',312 data: {313 labels: [],314 datasets: [315 { label: 'LLM', data: [], backgroundColor: '#4c9be8', stack: 's' },316 { label: 'Painter', data: [], backgroundColor: '#56b87a', stack: 's' },317 { label: 'STT', data: [], backgroundColor: '#b07de8', stack: 's' },318 { label: 'Compact', data: [], backgroundColor: '#e09440', stack: 's' },319 { label: 'Other', data: [], backgroundColor: '#4e5568', stack: 's' },320 ]321 },322 options: {323 animation: false,324 responsive: true,325 plugins: {326 legend: { labels: { color: '#888', boxWidth: 10, font: { size: 10 } } },327 tooltip: { callbacks: { label: ctx => ` ${ctx.dataset.label}: ${Math.round(ctx.raw)} ms` } },328 },329 scales: {330 x: { ...BASE_SCALES.x, stacked: true },331 y: {332 ...BASE_SCALES.y, stacked: true,333 title: { display: true, text: 'ms', color: '#555', font: { size: 10 } },334 },335 },336 },337});338 339// ── Helpers ──────────────────────────────────────────────────────────────340function fmtMs(ms) {341 if (ms == null) return '—';342 return ms >= 1000 ? (ms / 1000).toFixed(1) + 's' : Math.round(ms) + 'ms';343}344function fmtElapsed(s) {345 if (s == null) return '—';346 const m = Math.floor(s / 60), sec = Math.floor(s % 60);347 return m > 0 ? `${m}m ${sec}s` : `${sec}s`;348}349function fmtTime(ts) {350 const d = new Date(ts * 1000);351 return d.getMinutes().toString().padStart(2, '0') + ':' + d.getSeconds().toString().padStart(2, '0');352}353function latencyClass(ms) {354 if (ms == null) return '';355 if (ms < 2000) return 'c-green';356 if (ms < 6000) return 'c-yellow';357 return 'c-red';358}359function cacheClass(rate) {360 if (rate == null) return '';361 if (rate >= 0.7) return 'c-green';362 if (rate >= 0.4) return 'c-yellow';363 return 'c-red';364}365 366const GPU_LABELS = { nvml: 'NVIDIA GPU', rocm: 'AMD ROCm', mps: 'Apple MPS (unified)' };367 368const OP_ORDER = [369 'total_turn', 'llm_direct', 'compact_memory', 'context_assembly',370 'apply_directives', 'save_memory', 'painter_backdrop', 'painter_sprite', 'stt',371];372const OP_NAMES = {373 total_turn: 'Turn total',374 llm_direct: 'LLM direct_turn',375 compact_memory: 'Compact memory (LLM)',376 context_assembly: 'Context assembly',377 apply_directives: 'apply_directives',378 save_memory: 'save_memory',379 painter_backdrop: 'Painter — backdrop',380 painter_sprite: 'Painter — sprite',381 stt: 'STT transcribe',382};383 384// ── Main update ──────────────────────────────────────────────────────────385function update(data) {386 // KPI: turns387 document.getElementById('kpi-turns').textContent = data.turns_completed ?? '—';388 389 // KPI: LLM390 const llm = data.ops?.llm_direct;391 const kpiLlm = document.getElementById('kpi-llm');392 kpiLlm.textContent = fmtMs(llm?.avg_ms);393 kpiLlm.className = 'kpi-value ' + latencyClass(llm?.avg_ms);394 document.getElementById('kpi-llm-sub').textContent =395 llm ? `p95 ${fmtMs(llm.p95_ms)} · max ${fmtMs(llm.max_ms)}` : 'no data yet';396 397 // KPI: cache398 const rate = data.cache_hit_rate;399 const kpiCache = document.getElementById('kpi-cache');400 kpiCache.textContent = rate != null ? Math.round(rate * 100) + '%' : '—';401 kpiCache.className = 'kpi-value ' + cacheClass(rate);402 const tot = (data.cache_hits ?? 0) + (data.cache_misses ?? 0);403 document.getElementById('kpi-cache-sub').textContent =404 tot > 0 ? `${data.cache_hits} hits / ${tot} total` : 'no renders yet';405 406 // KPI: elapsed + GPU backend407 document.getElementById('kpi-elapsed').textContent = fmtElapsed(data.session_elapsed_s);408 document.getElementById('kpi-gpu').textContent =409 data.gpu_backend ? (GPU_LABELS[data.gpu_backend] ?? data.gpu_backend) : 'No GPU detected';410 411 // Resource chart412 const samples = data.samples ?? [];413 resChart.data.labels = samples.map(s => fmtTime(s.ts));414 resChart.data.datasets[0].data = samples.map(s => s.ram_pct ?? null);415 resChart.data.datasets[1].data = samples.map(s => s.cpu_pct ?? null);416 417 const hasVram = samples.some(s => s.vram_mb != null);418 if (hasVram) {419 // For nvml/rocm: use vram_pct on left axis if available.420 // For MPS (unified): express process RSS as % of total system RAM (ram_mb / ram_pct * 100).421 resChart.data.datasets[2].data = samples.map(s => {422 if (s.vram_mb == null) return null;423 if (s.vram_pct != null) return s.vram_pct; // nvml / rocm424 // MPS: derive total RAM from ram_mb + ram_pct, express RSS as %425 if (s.ram_pct > 0) return (s.vram_mb / (s.ram_mb / (s.ram_pct / 100))) * 100;426 return null;427 });428 const vramLabel = data.gpu_backend === 'mps' ? 'RSS % (MPS)' : 'VRAM %';429 resChart.data.datasets[2].label = vramLabel;430 resChart.options.scales.yVram.display = false; // keep right axis hidden, same % scale431 } else {432 resChart.data.datasets[2].data = samples.map(() => null);433 }434 resChart.update('none');435 436 // Latency stacked bar437 const turns = data.turn_breakdown ?? [];438 latChart.data.labels = turns.map(t => `T${t.turn}`);439 latChart.data.datasets[0].data = turns.map(t => t.llm_direct ?? 0);440 latChart.data.datasets[1].data = turns.map(t => (t.painter_backdrop ?? 0) + (t.painter_sprite ?? 0));441 latChart.data.datasets[2].data = turns.map(t => t.stt ?? 0);442 latChart.data.datasets[3].data = turns.map(t => t.compact_memory ?? 0);443 latChart.data.datasets[4].data = turns.map(t => {444 const acc = (t.llm_direct ?? 0) + (t.painter_backdrop ?? 0) + (t.painter_sprite ?? 0)445 + (t.stt ?? 0) + (t.compact_memory ?? 0)446 + (t.apply_directives ?? 0) + (t.save_memory ?? 0);447 return Math.max(0, (t.total_turn ?? 0) - acc);448 });449 latChart.update('none');450 451 // Ops table452 const ops = data.ops ?? {};453 const keys = [...OP_ORDER.filter(k => k in ops), ...Object.keys(ops).filter(k => !OP_ORDER.includes(k))];454 const tbody = document.getElementById('ops-body');455 tbody.innerHTML = keys.map(k => {456 const o = ops[k];457 if (!o) return '';458 const isTot = k === 'total_turn';459 return `<tr class="${isTot ? 'total-row' : ''}">460 <td>${OP_NAMES[k] ?? k}</td>461 <td>${o.count}</td>462 <td class="${latencyClass(o.avg_ms)}">${Math.round(o.avg_ms)}</td>463 <td>${Math.round(o.p95_ms)}</td>464 <td>${Math.round(o.max_ms)}</td>465 </tr>`;466 }).join('');467}468 469// ── SSE connection with auto-reconnect ───────────────────────────────────470const dot = document.getElementById('dot');471const statusTxt = document.getElementById('status-text');472let src = null;473 474function connect() {475 if (src) { src.close(); src = null; }476 dot.className = 'dot';477 statusTxt.textContent = 'Connecting…';478 479 src = new EventSource('/debug/stream');480 src.onopen = () => {481 dot.className = 'dot live';482 statusTxt.textContent = 'Live';483 };484 src.onmessage = e => {485 try { update(JSON.parse(e.data)); } catch { /* ignore parse errors */ }486 };487 src.onerror = () => {488 dot.className = 'dot error';489 statusTxt.textContent = 'Reconnecting in 3s…';490 src.close(); src = null;491 setTimeout(connect, 3000);492 };493}494 495// ── Save report ──────────────────────────────────────────────────────────496async function saveReport() {497 const btn = document.getElementById('save-btn');498 const msg = document.getElementById('report-msg');499 btn.disabled = true;500 msg.style.color = 'var(--muted)';501 msg.textContent = 'Saving…';502 try {503 const r = await fetch('/debug/report');504 const d = await r.json();505 msg.style.color = 'var(--green)';506 msg.textContent = d.status === 'ok' ? '✓ Saved to runs/debug_*/' : '✗ Save failed';507 } catch {508 msg.style.color = 'var(--red)';509 msg.textContent = '✗ Request failed';510 }511 setTimeout(() => { msg.textContent = ''; btn.disabled = false; }, 4000);512}513 514connect();515</script>516</body>517</html>518 