Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>WhyDidItFail β Playground</title> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #0d0d0d; | |
| --surface: #141414; | |
| --surface-2: #1a1a1a; | |
| --surface-3: #222; | |
| --border: #2a2a2a; | |
| --border-hi: #444; | |
| --text: #e5e5e5; | |
| --muted: #666; | |
| --subtle: #3a3a3a; | |
| --blue: #3b82f6; | |
| --blue-dim: rgba(59,130,246,0.12); | |
| --amber: #f59e0b; | |
| --amber-dim: rgba(245,158,11,0.12); | |
| --green: #22c55e; | |
| --green-dim: rgba(34,197,94,0.10); | |
| --yellow: #eab308; | |
| --yellow-dim: rgba(234,179,8,0.10); | |
| --red: #ef4444; | |
| --red-dim: rgba(239,68,68,0.10); | |
| --mono: 'JetBrains Mono','Fira Code',monospace; | |
| --r: 6px; | |
| --r-sm: 4px; | |
| } | |
| html, body { height: 100%; background: var(--bg); color: var(--text); | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| font-size: 14px; line-height: 1.5; } | |
| #app { display: flex; flex-direction: column; height: 100vh; overflow: hidden; } | |
| /* ββ Header βββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| #header { | |
| height: 52px; border-bottom: 1px solid var(--border); | |
| display: flex; align-items: center; justify-content: space-between; | |
| padding: 0 20px; flex-shrink: 0; background: var(--surface); | |
| } | |
| .logo { font-weight: 600; font-size: 15px; } | |
| .logo-sub { font-size: 12px; color: var(--muted); margin-left: 8px; } | |
| .header-right { display: flex; align-items: center; gap: 10px; } | |
| #step-counter { font-family: var(--mono); font-size: 12px; color: var(--muted); } | |
| .spill { | |
| font-size: 11px; font-weight: 500; padding: 2px 8px; | |
| border-radius: 99px; border: 1px solid transparent; | |
| } | |
| .spill-active { background: var(--green-dim); color: var(--green); border-color: rgba(34,197,94,.2); } | |
| .spill-done { background: var(--amber-dim); color: var(--amber); border-color: rgba(245,158,11,.2); } | |
| .spill-error { background: var(--red-dim); color: var(--red); border-color: rgba(239,68,68,.2); } | |
| /* ββ Main scroll area βββββββββββββββββββββββββββββββββββββββ */ | |
| #main { | |
| flex: 1; overflow-y: auto; padding: 24px 0 12px; | |
| display: flex; flex-direction: column; align-items: center; | |
| } | |
| /* ββ Empty state βββββββββββββββββββββββββββββββββββββββββββ */ | |
| #empty { | |
| flex: 1; display: flex; flex-direction: column; | |
| align-items: center; justify-content: center; | |
| text-align: center; padding: 40px; color: var(--muted); | |
| } | |
| .empty-icon { font-size: 40px; margin-bottom: 16px; opacity: .35; } | |
| #empty h2 { font-size: 18px; font-weight: 500; color: var(--text); margin-bottom: 8px; } | |
| #empty p { margin-bottom: 24px; } | |
| /* ββ Task banner βββββββββββββββββββββββββββββββββββββββββββ */ | |
| #task-banner { | |
| width: 100%; max-width: 700px; padding: 0 20px; margin-bottom: 16px; | |
| } | |
| .task-card { | |
| background: var(--surface-2); border: 1px solid var(--border); | |
| border-radius: var(--r); padding: 14px 16px; | |
| } | |
| .tiny-label { | |
| font-size: 10px; text-transform: uppercase; letter-spacing: .8px; | |
| color: var(--muted); font-weight: 600; | |
| } | |
| .task-card p { margin-top: 6px; font-size: 13px; line-height: 1.6; } | |
| .task-meta { margin-top: 8px; display: flex; gap: 6px; } | |
| /* ββ Timeline βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| #timeline { | |
| width: 100%; max-width: 700px; padding: 0 20px; | |
| display: flex; flex-direction: column; | |
| } | |
| /* ββ Step card βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .sc { display: flex; gap: 14px; padding-bottom: 14px; animation: fadeUp .18s ease-out; } | |
| @keyframes fadeUp { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:none; } } | |
| .sc-aside { display: flex; flex-direction: column; align-items: center; width: 32px; flex-shrink: 0; } | |
| .sc-dot { | |
| width: 10px; height: 10px; border-radius: 50%; | |
| background: var(--subtle); border: 2px solid var(--bg); | |
| margin-top: 14px; flex-shrink: 0; | |
| } | |
| .sc-dot.inspect { background: var(--blue); } | |
| .sc-dot.submit { background: var(--amber); } | |
| .sc-dot.success { background: var(--green); } | |
| .sc-dot.fail { background: var(--red); } | |
| .sc-dot.start { background: var(--muted); } | |
| .sc-line { width: 2px; flex: 1; background: var(--border); min-height: 16px; } | |
| .sc-body { | |
| flex: 1; background: var(--surface); border: 1px solid var(--border); | |
| border-radius: var(--r); overflow: hidden; min-width: 0; | |
| } | |
| /* card header */ | |
| .sc-head { | |
| display: flex; align-items: center; gap: 8px; | |
| padding: 10px 14px; border-bottom: 1px solid var(--border); | |
| } | |
| .sc-num { font-size: 11px; color: var(--muted); font-family: var(--mono); min-width: 22px; } | |
| .badge { | |
| font-size: 11px; font-weight: 500; padding: 2px 8px; | |
| border-radius: var(--r-sm); font-family: var(--mono); | |
| border: 1px solid transparent; | |
| } | |
| .badge-inspect { background: var(--blue-dim); color: var(--blue); border-color: rgba(59,130,246,.2); } | |
| .badge-submit { background: var(--amber-dim); color: var(--amber); border-color: rgba(245,158,11,.2); } | |
| .badge-start { background: var(--surface-3); color: var(--muted); border-color: var(--border); } | |
| .sc-reward { margin-left: auto; font-family: var(--mono); font-size: 12px; font-weight: 600; } | |
| .rp { color: var(--green); } | |
| .rn { color: var(--red); } | |
| .r0 { color: var(--muted); } | |
| .rh { color: var(--green); } | |
| .rm { color: var(--yellow); } | |
| .rl { color: var(--red); } | |
| /* card sections */ | |
| .sc-sect { padding: 12px 14px; border-bottom: 1px solid var(--border); } | |
| .sc-sect:last-child { border-bottom: none; } | |
| /* data tables */ | |
| .dtable { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 12px; } | |
| .dtable th { text-align:left; padding: 3px 8px; color: var(--muted); font-weight:500; | |
| border-bottom: 1px solid var(--border); font-size: 11px; } | |
| .dtable td { padding: 3px 8px; color: var(--text); } | |
| .dtable tr:hover td { background: var(--surface-2); } | |
| .anomaly { color: var(--red) ; font-weight: 600; } | |
| .warn { color: var(--yellow) ; } | |
| .good { color: var(--green) ; } | |
| /* config kv */ | |
| .kv { display: grid; grid-template-columns: max-content 1fr; gap: 2px 16px; | |
| font-family: var(--mono); font-size: 12px; } | |
| .kv-k { color: var(--muted); } | |
| .kv-v { color: var(--text); } | |
| /* score */ | |
| .score-row { display: flex; align-items: center; gap: 14px; } | |
| .score-num { font-size: 26px; font-weight: 700; font-family: var(--mono); } | |
| .score-bars { flex: 1; } | |
| .bar-track { height: 5px; background: var(--surface-3); border-radius: 99px; overflow: hidden; } | |
| .bar-fill { height: 100%; border-radius: 99px; transition: width .5s ease; } | |
| .bar-lbl { font-size: 11px; color: var(--muted); margin-top: 3px; } | |
| /* feedback */ | |
| .fb-text { font-size: 13px; color: var(--muted); font-style: italic; line-height: 1.5; } | |
| /* footer buttons */ | |
| .sc-foot { display: flex; gap: 6px; padding: 9px 14px; flex-wrap: wrap; } | |
| .btn-g { | |
| background: none; border: 1px solid var(--border); color: var(--muted); | |
| padding: 3px 10px; border-radius: var(--r-sm); font-size: 12px; | |
| cursor: pointer; transition: all .12s; font-family: inherit; | |
| } | |
| .btn-g:hover { border-color: var(--border-hi); color: var(--text); background: var(--surface-2); } | |
| /* annotation */ | |
| .ann-box { padding: 10px 14px; border-top: 1px solid var(--border); background: var(--surface-2); } | |
| .ann-lbl { font-size: 10px; text-transform: uppercase; letter-spacing:.6px; | |
| color: var(--muted); font-weight: 600; margin-bottom: 6px; } | |
| .ann-ta { | |
| width: 100%; background: var(--surface-3); border: 1px solid var(--border); | |
| border-radius: var(--r-sm); color: var(--text); padding: 7px 9px; | |
| font-size: 13px; font-family: inherit; resize: none; outline: none; | |
| } | |
| .ann-ta:focus { border-color: var(--border-hi); } | |
| /* debug */ | |
| .dbg-block { padding: 10px 14px; border-top: 1px solid var(--border); background: #080808; } | |
| .dbg-pre { font-family: var(--mono); font-size: 11px; color: #86efac; | |
| white-space: pre; overflow-x: auto; max-height: 180px; } | |
| /* loading */ | |
| #loading { display: flex; align-items: center; gap: 10px; color: var(--muted); | |
| font-size: 13px; padding: 16px 0; } | |
| .spinner { width:15px; height:15px; border:2px solid var(--border); | |
| border-top-color: var(--blue); border-radius:50%; animation: spin .65s linear infinite; } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| /* ββ Action bar ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| #action-bar { | |
| border-top: 1px solid var(--border); background: var(--surface); | |
| flex-shrink: 0; | |
| } | |
| #ab-inner { max-width: 740px; margin: 0 auto; padding: 12px 20px; } | |
| .pills { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } | |
| .pills-lbl { font-size: 10px; text-transform: uppercase; letter-spacing:.6px; | |
| color: var(--muted); font-weight: 600; margin-right: 2px; } | |
| .pill { | |
| border: 1px solid var(--border); background: transparent; color: var(--muted); | |
| padding: 5px 12px; border-radius: var(--r-sm); font-size: 12px; | |
| cursor: pointer; transition: all .12s; font-family: inherit; | |
| } | |
| .pill:hover:not(:disabled) { border-color: var(--border-hi); color: var(--text); } | |
| .pill:disabled { opacity: .35; cursor: not-allowed; } | |
| .pill.inspect.on { background: var(--blue-dim); border-color: var(--blue); color: var(--blue); } | |
| .pill.submit.on { background: var(--amber-dim); border-color: var(--amber); color: var(--amber); } | |
| #diag-form { margin-top: 10px; display: flex; gap: 8px; } | |
| #diag-form textarea { | |
| flex: 1; background: var(--surface-2); border: 1px solid var(--border); | |
| border-radius: var(--r-sm); color: var(--text); padding: 7px 10px; | |
| font-size: 13px; font-family: inherit; resize: none; outline: none; | |
| } | |
| #diag-form textarea:focus { border-color: var(--border-hi); } | |
| #diag-form textarea::placeholder { color: var(--subtle); } | |
| .ab-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; } | |
| #ab-hint { font-size: 12px; color: var(--muted); } | |
| /* ββ Buttons ββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .btn-primary { | |
| background: var(--blue); color: #fff; border: none; | |
| padding: 8px 20px; border-radius: var(--r-sm); | |
| font-size: 13px; font-weight: 500; cursor: pointer; transition: opacity .12s; | |
| } | |
| .btn-primary:hover { opacity: .85; } | |
| #step-btn { | |
| background: var(--blue); color: #fff; border: none; | |
| padding: 7px 18px; border-radius: var(--r-sm); | |
| font-size: 13px; font-weight: 500; font-family: var(--mono); | |
| cursor: pointer; transition: opacity .12s; min-width: 82px; | |
| } | |
| #step-btn:hover:not(:disabled) { opacity: .85; } | |
| #step-btn:disabled { opacity: .3; cursor: not-allowed; } | |
| #new-btn { | |
| background: var(--surface-2); border: 1px solid var(--border); color: var(--text); | |
| padding: 5px 13px; border-radius: var(--r-sm); font-size: 13px; cursor: pointer; | |
| } | |
| #new-btn:hover { border-color: var(--border-hi); background: var(--surface-3); } | |
| .hidden { display: none ; } | |
| ::-webkit-scrollbar { width: 5px; height: 5px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--surface-3); border-radius: 99px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <!-- Header --> | |
| <header id="header"> | |
| <div> | |
| <span class="logo">WhyDidItFail</span> | |
| <span class="logo-sub">Playground</span> | |
| </div> | |
| <div class="header-right"> | |
| <div id="session-info" class="hidden"> | |
| <span id="step-counter">Step 0</span> | |
| <span id="session-spill" class="spill spill-active">Active</span> | |
| </div> | |
| <button id="new-btn" onclick="newSession()">βΊ New Session</button> | |
| </div> | |
| </header> | |
| <!-- Scroll area --> | |
| <main id="main"> | |
| <div id="empty"> | |
| <div class="empty-icon">β‘</div> | |
| <h2>ML Training Debugger</h2> | |
| <p>Inspect logs, config, and gradients to diagnose a failed training run.</p> | |
| <button class="btn-primary" onclick="newSession()">Start Session</button> | |
| </div> | |
| <div id="task-banner" class="hidden"> | |
| <div class="task-card"> | |
| <div class="tiny-label">Task</div> | |
| <p id="task-text"></p> | |
| <div class="task-meta" id="task-meta"></div> | |
| </div> | |
| </div> | |
| <div id="timeline"></div> | |
| <div id="loading" class="hidden" style="padding-left:40px;"> | |
| <div class="spinner"></div> | |
| <span>Processing...</span> | |
| </div> | |
| </main> | |
| <!-- Action bar --> | |
| <footer id="action-bar"> | |
| <div id="ab-inner"> | |
| <div class="pills"> | |
| <span class="pills-lbl">Action</span> | |
| <button class="pill inspect" data-a="inspect_logs" onclick="pick('inspect_logs')" disabled>Inspect Logs</button> | |
| <button class="pill inspect" data-a="inspect_config" onclick="pick('inspect_config')" disabled>Inspect Config</button> | |
| <button class="pill inspect" data-a="inspect_gradients" onclick="pick('inspect_gradients')" disabled>Inspect Gradients</button> | |
| <button class="pill submit" data-a="submit_diagnosis" onclick="pick('submit_diagnosis')" disabled>Submit Diagnosis</button> | |
| </div> | |
| <div id="diag-form" class="hidden"> | |
| <textarea id="diag-in" placeholder="Root cause diagnosisβ¦" rows="2"></textarea> | |
| <textarea id="fix-in" placeholder="Suggested fixβ¦" rows="2"></textarea> | |
| </div> | |
| <div class="ab-foot"> | |
| <span id="ab-hint">Start a session to begin</span> | |
| <button id="step-btn" onclick="step()" disabled>Step β</button> | |
| </div> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| // ββ state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const S = { active: false, done: false, steps: 0, action: null }; | |
| // ββ api βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function apiReset() { | |
| const r = await fetch('/reset', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' }); | |
| if (!r.ok) throw new Error(`/reset ${r.status}`); | |
| return r.json(); | |
| } | |
| async function apiStep(body) { | |
| const r = await fetch('/step', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) }); | |
| if (!r.ok) throw new Error(`/step ${r.status}`); | |
| return r.json(); | |
| } | |
| // ββ new session βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function newSession() { | |
| setLoading(true); | |
| try { | |
| const data = await apiReset(); | |
| const obs = data.observation ?? data; | |
| Object.assign(S, { active:true, done:false, steps:0, action:null }); | |
| // clear | |
| document.getElementById('empty').classList.add('hidden'); | |
| document.getElementById('timeline').innerHTML = ''; | |
| document.querySelectorAll('.pill').forEach(p => { p.disabled = false; p.classList.remove('on'); }); | |
| document.getElementById('diag-form').classList.add('hidden'); | |
| document.getElementById('diag-in').value = ''; | |
| document.getElementById('fix-in').value = ''; | |
| document.getElementById('step-btn').disabled = true; | |
| pick(null); | |
| // task banner | |
| const desc = obs.task_description ?? ''; | |
| document.getElementById('task-text').textContent = desc; | |
| const diff = (desc.match(/Difficulty:\s*(\w+)/i) ?? [])[1]?.toLowerCase() ?? ''; | |
| document.getElementById('task-meta').innerHTML = diff | |
| ? `<span class="badge badge-inspect" style="font-family:inherit">${diff}</span>` : ''; | |
| document.getElementById('task-banner').classList.remove('hidden'); | |
| // session header | |
| document.getElementById('session-info').classList.remove('hidden'); | |
| refreshHeader(); | |
| // hint card | |
| addStartCard(obs.visible_data?.hint ?? obs.feedback ?? 'Investigation started.'); | |
| hint('Select an action to inspect the training run'); | |
| } catch(e) { hint('Error: ' + e.message); } | |
| finally { setLoading(false); } | |
| } | |
| // ββ step ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function step() { | |
| if (!S.action || S.done) return; | |
| const body = { action_type: S.action }; | |
| if (S.action === 'submit_diagnosis') { | |
| const d = document.getElementById('diag-in').value.trim(); | |
| if (!d) { hint('β Diagnosis required'); return; } | |
| body.diagnosis = d; | |
| const f = document.getElementById('fix-in').value.trim(); | |
| if (f) body.suggested_fix = f; | |
| } | |
| setLoading(true); | |
| pills(false); | |
| document.getElementById('step-btn').disabled = true; | |
| try { | |
| const data = await apiStep(body); | |
| const obs = data.observation ?? data; | |
| const reward = data.reward ?? obs.reward ?? 0; | |
| const done = data.done ?? obs.done ?? false; | |
| S.steps++; | |
| S.done = done; | |
| addStepCard(S.steps, body, obs, reward, done); | |
| refreshHeader(); | |
| if (done) { | |
| hint('Episode complete β start a new session to try again'); | |
| pills(false); | |
| const sp = document.getElementById('session-spill'); | |
| sp.className = 'spill ' + (reward >= 0.7 ? 'spill-active' : reward >= 0.4 ? 'spill-done' : 'spill-error'); | |
| sp.textContent = reward >= 0.7 ? 'Solved' : 'Done'; | |
| } else { | |
| pick(null); | |
| pills(true); | |
| document.getElementById('diag-in').value = ''; | |
| document.getElementById('fix-in').value = ''; | |
| hint('Good. Choose your next action.'); | |
| } | |
| // scroll down | |
| const m = document.getElementById('main'); | |
| setTimeout(() => m.scrollTo({ top: m.scrollHeight, behavior:'smooth' }), 60); | |
| } catch(e) { | |
| hint('Error: ' + e.message); | |
| pills(true); | |
| document.getElementById('step-btn').disabled = false; | |
| } finally { setLoading(false); } | |
| } | |
| // ββ pill selection ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function pick(a) { | |
| S.action = a; | |
| document.querySelectorAll('.pill').forEach(p => p.classList.toggle('on', p.dataset.a === a)); | |
| document.getElementById('diag-form').classList.toggle('hidden', a !== 'submit_diagnosis'); | |
| document.getElementById('step-btn').disabled = !a; | |
| if (a && a !== 'submit_diagnosis') { | |
| const labels = { inspect_logs:'Inspect training logs', inspect_config:'Inspect hyperparameter config', inspect_gradients:'Inspect gradient norms' }; | |
| hint(labels[a] ?? a); | |
| } else if (a === 'submit_diagnosis') { | |
| hint('Fill in diagnosis + optional fix, then Step'); | |
| } | |
| } | |
| // ββ start card ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function addStartCard(msg) { | |
| const tl = document.getElementById('timeline'); | |
| const card = mkCard('start', '', 'start'); | |
| const head = card.querySelector('.sc-head'); | |
| const num = head.querySelector('.sc-num'); num.textContent = 'Start'; | |
| const badge = mkEl('span', 'badge badge-start', 'session reset'); head.appendChild(badge); | |
| const body = card.querySelector('.sc-body'); | |
| const sect = mkEl('div', 'sc-sect'); | |
| const fb = mkEl('div', 'fb-text', msg); sect.appendChild(fb); body.appendChild(sect); | |
| tl.appendChild(card); | |
| } | |
| // ββ step card βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function addStepCard(n, action, obs, reward, done) { | |
| const tl = document.getElementById('timeline'); | |
| const isS = action.action_type === 'submit_diagnosis'; | |
| const dot = done && reward >= 0.7 ? 'success' : isS ? (reward < 0.4 ? 'fail' : 'submit') : reward < 0 ? 'fail' : 'inspect'; | |
| const card = mkCard(n, action.action_type, dot); | |
| const body = card.querySelector('.sc-body'); | |
| // header badges | |
| const head = card.querySelector('.sc-head'); | |
| const ab = mkEl('span', `badge ${isS ? 'badge-submit' : 'badge-inspect'}`, action.action_type.replace(/_/g,' ')); | |
| const rb = mkEl('span', 'sc-reward ' + rewardCls(reward, isS), | |
| isS ? `Score: ${reward.toFixed(2)}` : (reward > 0 ? `+${reward.toFixed(2)}` : reward.toFixed(2))); | |
| head.appendChild(ab); head.appendChild(rb); | |
| // data section | |
| const vd = obs.visible_data ?? {}; | |
| if (!isS && Object.keys(vd).length) { | |
| const sect = mkEl('div', 'sc-sect'); | |
| const lbl = mkEl('div', 'tiny-label', dataLabel(action.action_type)); | |
| lbl.style.marginBottom = '8px'; | |
| sect.appendChild(lbl); | |
| sect.appendChild(renderData(vd, action.action_type)); | |
| body.appendChild(sect); | |
| } | |
| // submitted diagnosis / fix | |
| if (isS && (action.diagnosis || action.suggested_fix)) { | |
| const sect = mkEl('div', 'sc-sect'); | |
| if (action.diagnosis) { | |
| sect.appendChild(mkEl('div', 'tiny-label', 'Diagnosis')); | |
| const t = mkEl('div', '', action.diagnosis); | |
| t.style.cssText = 'font-size:13px;margin-top:4px;'; | |
| sect.appendChild(t); | |
| } | |
| if (action.suggested_fix) { | |
| const fl = mkEl('div', 'tiny-label', 'Suggested Fix'); | |
| fl.style.marginTop = '10px'; sect.appendChild(fl); | |
| const t = mkEl('div', '', action.suggested_fix); | |
| t.style.cssText = 'font-size:13px;color:var(--muted);margin-top:4px;'; | |
| sect.appendChild(t); | |
| } | |
| body.appendChild(sect); | |
| } | |
| // score bar (submit only) | |
| if (isS) { | |
| const sect = mkEl('div', 'sc-sect'); | |
| sect.appendChild(mkEl('div', 'tiny-label', 'Score')); | |
| const row = mkEl('div', 'score-row'); row.style.marginTop = '8px'; | |
| const num = mkEl('span', 'score-num', reward.toFixed(2)); | |
| num.style.color = rewardColor(reward); | |
| const bars = mkEl('div', 'score-bars'); | |
| const trk = mkEl('div', 'bar-track'); | |
| const fil = mkEl('div', 'bar-fill'); | |
| fil.style.width = `${Math.round(reward*100)}%`; | |
| fil.style.background = rewardColor(reward); | |
| trk.appendChild(fil); bars.appendChild(trk); | |
| bars.appendChild(mkEl('div', 'bar-lbl', reward >= 0.8 ? 'Excellent' : reward >= 0.5 ? 'Partial credit' : 'Incorrect')); | |
| row.appendChild(num); row.appendChild(bars); sect.appendChild(row); body.appendChild(sect); | |
| } | |
| // feedback | |
| if (obs.feedback) { | |
| const sect = mkEl('div', 'sc-sect'); | |
| sect.appendChild(mkEl('div', 'tiny-label', 'Feedback')); | |
| const fb = mkEl('div', 'fb-text', obs.feedback); fb.style.marginTop = '4px'; | |
| sect.appendChild(fb); body.appendChild(sect); | |
| } | |
| // footer | |
| const foot = mkEl('div', 'sc-foot'); | |
| const dbgBtn = mkEl('button', 'btn-g', 'View Debug Data'); | |
| dbgBtn.onclick = () => toggleDbg(n, action, obs, reward, done); | |
| foot.appendChild(dbgBtn); | |
| if (!isS) { | |
| const dBtn = mkEl('button', 'btn-g', '+ Add Diagnosis'); | |
| dBtn.onclick = () => toggleAnn(n, 'diagnosis', dBtn); | |
| const fBtn = mkEl('button', 'btn-g', '+ Suggest Fix'); | |
| fBtn.onclick = () => toggleAnn(n, 'fix', fBtn); | |
| foot.appendChild(dBtn); foot.appendChild(fBtn); | |
| } | |
| body.appendChild(foot); | |
| // annotation container | |
| const annC = mkEl('div', 'hidden'); annC.id = `ann-${n}`; body.appendChild(annC); | |
| // debug container | |
| const dbgC = mkEl('div', 'hidden'); dbgC.id = `dbg-${n}`; | |
| const dbgB = mkEl('div', 'dbg-block'); | |
| const pre = mkEl('pre', 'dbg-pre', JSON.stringify({ action, observation:obs, reward, done }, null, 2)); | |
| dbgB.appendChild(pre); dbgC.appendChild(dbgB); body.appendChild(dbgC); | |
| tl.appendChild(card); | |
| } | |
| // ββ render data βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function renderData(vd, actionType) { | |
| if (actionType === 'inspect_logs' && vd.training_logs) return logsTable(vd.training_logs); | |
| if (actionType === 'inspect_config' && vd.config) return kvGrid(vd.config); | |
| if (actionType === 'inspect_gradients' && vd.gradient_norms) return gradTable(vd.gradient_norms); | |
| return kvGrid(vd); // fallback | |
| } | |
| function logsTable(rows) { | |
| if (!rows?.length) return mkEl('span', '', 'No data'); | |
| const keys = Object.keys(rows[0]).filter(k => k !== 'epoch'); | |
| const t = document.createElement('table'); t.className = 'dtable'; | |
| const thead = document.createElement('thead'); | |
| const hr = document.createElement('tr'); | |
| ['epoch', ...keys].forEach(k => { const th = document.createElement('th'); th.textContent = k.replace(/_/g,' '); hr.appendChild(th); }); | |
| thead.appendChild(hr); t.appendChild(thead); | |
| const tbody = document.createElement('tbody'); | |
| rows.forEach(row => { | |
| const tr = document.createElement('tr'); | |
| ['epoch', ...keys].forEach(k => { | |
| const td = document.createElement('td'); | |
| const v = row[k]; const s = v == null ? 'β' : String(v); | |
| td.textContent = s; | |
| const lo = s.toLowerCase(); | |
| if (lo === 'nan' || lo === 'inf' || lo === '-inf') td.className = 'anomaly'; | |
| tr.appendChild(td); | |
| }); | |
| tbody.appendChild(tr); | |
| }); | |
| t.appendChild(tbody); return t; | |
| } | |
| function gradTable(rows) { | |
| if (!rows?.length) return mkEl('span', '', 'No data'); | |
| const keys = Object.keys(rows[0]).filter(k => k !== 'step'); | |
| const t = document.createElement('table'); t.className = 'dtable'; | |
| const thead = document.createElement('thead'); | |
| const hr = document.createElement('tr'); | |
| ['step', ...keys].forEach(k => { const th = document.createElement('th'); th.textContent = k.replace(/_/g,' '); hr.appendChild(th); }); | |
| thead.appendChild(hr); t.appendChild(thead); | |
| const tbody = document.createElement('tbody'); | |
| rows.forEach(row => { | |
| const tr = document.createElement('tr'); | |
| ['step', ...keys].forEach(k => { | |
| const td = document.createElement('td'); | |
| const v = row[k]; const s = v == null ? 'β' : String(v); const n = parseFloat(v); | |
| td.textContent = s; | |
| if (k !== 'step' && !isNaN(n)) { | |
| if (n > 10) td.className = 'anomaly'; | |
| else if (n < 0.001) td.className = 'warn'; | |
| else td.className = 'good'; | |
| } | |
| tr.appendChild(td); | |
| }); | |
| tbody.appendChild(tr); | |
| }); | |
| t.appendChild(tbody); return t; | |
| } | |
| function kvGrid(obj) { | |
| const g = mkEl('div', 'kv'); | |
| Object.entries(obj).forEach(([k, v]) => { | |
| g.appendChild(mkEl('span', 'kv-k', k.replace(/_/g,' '))); | |
| g.appendChild(mkEl('span', 'kv-v', typeof v === 'object' ? JSON.stringify(v) : String(v))); | |
| }); | |
| return g; | |
| } | |
| // ββ annotation toggle βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function toggleAnn(n, type, btn) { | |
| const c = document.getElementById(`ann-${n}`); | |
| const existing = c.querySelector(`[data-type="${type}"]`); | |
| if (existing) { existing.remove(); if (!c.children.length) c.classList.add('hidden'); return; } | |
| const box = mkEl('div', 'ann-box'); box.dataset.type = type; | |
| box.appendChild(mkEl('div', 'ann-lbl', type === 'diagnosis' ? 'Diagnosis Note' : 'Fix Note')); | |
| const ta = document.createElement('textarea'); ta.className = 'ann-ta'; ta.rows = 2; | |
| ta.placeholder = type === 'diagnosis' ? 'What do you think caused this?' : 'What would fix it?'; | |
| box.appendChild(ta); c.appendChild(box); c.classList.remove('hidden'); ta.focus(); | |
| } | |
| // ββ debug toggle ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function toggleDbg(n) { | |
| document.getElementById(`dbg-${n}`).classList.toggle('hidden'); | |
| } | |
| // ββ helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function mkCard(n, actionType, dotClass) { | |
| const card = document.createElement('div'); card.className = 'sc'; card.id = `sc-${n}`; | |
| const aside = mkEl('div', 'sc-aside'); | |
| const dot = mkEl('div', `sc-dot ${dotClass}`); | |
| const line = mkEl('div', 'sc-line'); | |
| aside.appendChild(dot); aside.appendChild(line); | |
| const body = mkEl('div', 'sc-body'); | |
| const head = mkEl('div', 'sc-head'); | |
| const num = mkEl('span', 'sc-num', typeof n === 'number' ? `#${n}` : n); | |
| head.appendChild(num); body.appendChild(head); | |
| card.appendChild(aside); card.appendChild(body); | |
| return card; | |
| } | |
| function mkEl(tag, cls, txt) { | |
| const el = document.createElement(tag); | |
| if (cls) el.className = cls; | |
| if (txt !== undefined) el.textContent = txt; | |
| return el; | |
| } | |
| function rewardCls(r, isSubmit) { | |
| if (isSubmit) return r >= 0.7 ? 'rh' : r >= 0.4 ? 'rm' : 'rl'; | |
| return r > 0 ? 'rp' : r < 0 ? 'rn' : 'r0'; | |
| } | |
| function rewardColor(r) { | |
| return r >= 0.7 ? 'var(--green)' : r >= 0.4 ? 'var(--yellow)' : 'var(--red)'; | |
| } | |
| function dataLabel(a) { | |
| return { inspect_logs:'Training Logs', inspect_config:'Hyperparameter Config', inspect_gradients:'Gradient Norms' }[a] ?? 'Data'; | |
| } | |
| function setLoading(v) { document.getElementById('loading').classList.toggle('hidden', !v); } | |
| function pills(enable) { document.querySelectorAll('.pill').forEach(p => p.disabled = !enable); } | |
| function hint(msg) { document.getElementById('ab-hint').textContent = msg; } | |
| function refreshHeader() { | |
| document.getElementById('step-counter').textContent = `Step ${S.steps}`; | |
| } | |
| </script> | |
| </body> | |
| </html> |