| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| const BASELINE_IDS = new Set(['baseline:flag_everything', 'baseline:no_evidence']); |
|
|
| |
| |
| |
| |
| |
| |
| const CURATED_MODELS = [ |
| 'anthropic/claude-opus-5', |
| 'openai/gpt-5.6-sol', |
| 'deepseek/deepseek-v4-pro', |
| 'deepseek/deepseek-v4-flash', |
| 'qwen/qwen3-30b-a3b-instruct-2507', |
| ]; |
|
|
| |
| |
| |
| const MIN_SEEDS_FOR_VARIANCE = 5; |
|
|
| |
| |
| |
| const EST_TOKENS_PER_STEP = 85000; |
|
|
| |
| |
| |
| |
| const TOOL_ORDER = [ |
| 'query_ledger', 'get_entry', 'list_documents', 'open_document', 'get_policy', |
| 'get_master_data', 'recompute', 'aggregate', 'compare_period', 'request_info', |
| 'disposition', 'submit', |
| ]; |
|
|
| const Console = { |
| state: { |
| task: null, knobs: {}, tools: [], |
| |
| |
| |
| models: ['deepseek/deepseek-v4-flash'], |
| seedCount: 5, |
| stepBudget: 0, tokenCeiling: 0, |
| estimate: null, |
| |
| |
| promptVersion: 'standard', |
| }, |
| tasks: [], |
| modelCatalog: [], |
| toolUnion: [], |
|
|
| async init() { |
| checkHealth(); |
| const [tasks, models] = await Promise.all([ |
| fetch('/api/tasks').then((r) => r.json()), |
| fetch('/api/models').then((r) => r.json()), |
| ]); |
| this.tasks = tasks; |
| this.modelCatalog = models; |
| this.toolUnion = unionTools(tasks); |
|
|
| renderTaskList(tasks); |
| initModelPicker(); |
| initPromptPreview(); |
|
|
| |
| |
| const firstRow = document.querySelector('#task-list .crow'); |
| if (firstRow) firstRow.click(); |
|
|
| const seedCountEl = document.getElementById('seed-count'); |
| seedCountEl.value = this.state.seedCount; |
| document.getElementById('seed-count-val').textContent = this.state.seedCount; |
| seedCountEl.addEventListener('input', onSeedCountChange); |
|
|
| document.getElementById('prompt-version').addEventListener('change', (e) => { |
| this.state.promptVersion = e.target.value; |
| this.refreshPreview(); |
| refreshPromptPreview(); |
| }); |
| document.getElementById('launch').addEventListener('click', () => this.launch()); |
| for (const btn of document.querySelectorAll('.raw-toggle')) { |
| btn.addEventListener('click', () => onRawToggle(btn)); |
| } |
| document.getElementById('history-refresh') |
| .addEventListener('click', loadRunHistory); |
| loadRunHistory(); |
| this.refreshPreview(); |
| }, |
|
|
| selectTask(task) { |
| this.state.task = task; |
| this.state.knobs = { ...task.defaults.knobs }; |
| this.state.tools = [...task.tools]; |
| this.state.stepBudget = task.defaults.step_budget; |
| this.state.tokenCeiling = task.defaults.token_ceiling; |
| clearLaunchError(); |
| renderTaskDetail(task, this.state); |
| this.refreshPreview(); |
| refreshPromptPreview(); |
| }, |
|
|
| buildRunRequest() { |
| const s = this.state; |
| return { |
| run_id: `run-${crypto.randomUUID().slice(0, 8)}`, |
| task_id: s.task.id, |
| models: s.models, |
| seed_count: s.seedCount, |
| knobs: s.knobs, |
| tools_enabled: s.tools, |
| step_budget: s.stepBudget, |
| token_ceiling: s.tokenCeiling, |
| prompt_version: s.promptVersion, |
| }; |
| }, |
|
|
| refreshPreview() { |
| const pre = document.getElementById('config-preview'); |
| const launchBtn = document.getElementById('launch'); |
| if (!this.state.task) { |
| pre.textContent = '// select a task to build a run config'; |
| launchBtn.disabled = true; |
| } else { |
| pre.textContent = JSON.stringify(this.buildRunRequest(), null, 2); |
| launchBtn.disabled = this.state.models.length === 0; |
| } |
| renderSweepEstimate(); |
| }, |
|
|
| async launch() { |
| clearLaunchError(); |
| const body = this.buildRunRequest(); |
| let resp; |
| try { |
| resp = await fetch('/api/runs', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(body), |
| }); |
| } catch (err) { |
| showLaunchError({ field: 'request', error: 'network error. Is the backend running?' }); |
| return; |
| } |
| if (!resp.ok) { |
| let err; |
| try { err = await resp.json(); } catch { err = {}; } |
| showLaunchError(normalizeError(resp, err)); |
| return; |
| } |
| const { contract } = await resp.json(); |
| loadRunHistory(); |
| openLiveView(body.run_id, contract); |
| }, |
| }; |
|
|
| |
|
|
| function escapeHtml(s) { |
| return String(s).replace(/[&<>"']/g, (c) => ({ |
| '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', |
| }[c])); |
| } |
|
|
| function teaser(brief, n) { |
| return brief.length > n ? `${brief.slice(0, n)}…` : brief; |
| } |
|
|
| |
| |
| |
| function slugModel(model) { |
| return model.replace(/\//g, '_').replace(/:/g, '_'); |
| } |
|
|
| |
| function statusBase(status) { |
| const i = status.indexOf(':'); |
| return i === -1 ? status : status.slice(0, i); |
| } |
|
|
| function failureReason(status) { |
| const i = status.indexOf(':'); |
| return i === -1 ? '' : status.slice(i + 1).trim(); |
| } |
|
|
| const STATUS_GLYPH = { |
| queued: '○', running: '◐', finished: '✓', failed: '✕', aborted: '■', interrupted: '‼', |
| }; |
|
|
| function statusGlyph(base) { |
| return STATUS_GLYPH[base] || '?'; |
| } |
|
|
| function fmtPrice(pricePerToken) { |
| return `$${(pricePerToken * 1e6).toFixed(2)}`; |
| } |
|
|
| function fmtUsd(x) { |
| return x < 1 ? x.toFixed(4) : x.toFixed(2); |
| } |
|
|
| |
|
|
| function unionTools(tasks) { |
| const seen = new Set(); |
| for (const t of tasks) for (const tool of t.tools) seen.add(tool); |
| const known = TOOL_ORDER.filter((t) => seen.has(t)); |
| const unknown = [...seen].filter((t) => !TOOL_ORDER.includes(t)).sort(); |
| return [...known, ...unknown]; |
| } |
|
|
| function renderTaskList(tasks) { |
| const root = document.getElementById('task-list'); |
| root.innerHTML = ''; |
| for (const task of tasks) { |
| const row = document.createElement('button'); |
| row.type = 'button'; |
| row.className = 'crow'; |
| row.dataset.taskId = task.id; |
| row.title = task.brief; |
| row.innerHTML = ` |
| <span class="tier ${task.tier}">${task.tier}</span> |
| <span class="tid mono">${task.id}</span> |
| <span class="ctitle">${escapeHtml(teaser(task.brief, 70))}</span> |
| `; |
| row.addEventListener('click', () => { |
| for (const el of root.querySelectorAll('.crow')) el.classList.remove('on'); |
| row.classList.add('on'); |
| Console.selectTask(task); |
| }); |
| root.appendChild(row); |
| } |
| } |
|
|
| function renderTaskDetail(task, state) { |
| document.getElementById('knob-box').hidden = false; |
| document.getElementById('tool-box').hidden = false; |
| renderKnobs(task, state); |
| renderTools(task, state); |
| syncBudgetInputs(state); |
| } |
|
|
| |
|
|
| function rangeNumberRow(label, id, min, max, value, disabled, onChange) { |
| const row = document.createElement('div'); |
| row.className = 'field-row'; |
| row.innerHTML = ` |
| <span class="fieldlabel">${label}</span> |
| <input type="range" id="${id}-range" min="${min}" max="${max}" step="1" |
| value="${value}" ${disabled ? 'disabled' : ''}> |
| <input type="number" id="${id}-num" min="${min}" max="${max}" step="1" |
| value="${value}" class="mono" ${disabled ? 'disabled' : ''}> |
| `; |
| const range = row.querySelector(`#${id}-range`); |
| const num = row.querySelector(`#${id}-num`); |
| range.addEventListener('input', () => onChange(Number(range.value), range, num)); |
| num.addEventListener('input', () => onChange(Number(num.value), range, num)); |
| return row; |
| } |
|
|
| function clampKnob(value, meta) { |
| if (Number.isNaN(value)) return meta.min; |
| return Math.min(meta.max, Math.max(meta.min, value)); |
| } |
|
|
| function renderKnobs(task, state) { |
| const wrap = document.getElementById('knobs'); |
| wrap.innerHTML = ''; |
| for (const [name, meta] of Object.entries(task.knob_meta)) { |
| const disabled = name === 'population' && !task.population_editable; |
| const label = name.replace(/_/g, ' '); |
| const row = rangeNumberRow(label.charAt(0).toUpperCase() + label.slice(1), `knob-${name}`, |
| meta.min, meta.max, state.knobs[name], disabled, (v, range, num) => { |
| const clamped = clampKnob(v, meta); |
| range.value = clamped; |
| num.value = clamped; |
| state.knobs[name] = clamped; |
| if (name === 'population') recalcBudgets(clamped); |
| Console.refreshPreview(); |
| }); |
| wrap.appendChild(row); |
| } |
| } |
|
|
| |
| |
| |
| function recalcBudgets(population) { |
| const stepBudget = Math.max(60, Math.ceil(4.5 * population)); |
| const tokenCeiling = 10 * stepBudget * EST_TOKENS_PER_STEP; |
| Console.state.stepBudget = stepBudget; |
| Console.state.tokenCeiling = tokenCeiling; |
| syncBudgetInputs(Console.state); |
| schedulePromptPreviewRefresh(); |
| } |
|
|
| function syncBudgetInputs(state) { |
| document.getElementById('step-budget-range').value = state.stepBudget; |
| document.getElementById('step-budget-num').value = state.stepBudget; |
| } |
|
|
| |
| |
| |
| |
| function onStepBudgetChange(v) { |
| Console.state.stepBudget = v; |
| Console.state.tokenCeiling = 10 * v * EST_TOKENS_PER_STEP; |
| document.getElementById('step-budget-range').value = v; |
| document.getElementById('step-budget-num').value = v; |
| Console.refreshPreview(); |
| schedulePromptPreviewRefresh(); |
| } |
|
|
| function onSeedCountChange(e) { |
| Console.state.seedCount = Number(e.target.value); |
| document.getElementById('seed-count-val').textContent = Console.state.seedCount; |
| Console.refreshPreview(); |
| } |
|
|
| |
|
|
| function renderTools(task, state) { |
| const wrap = document.getElementById('tools'); |
| wrap.innerHTML = ''; |
| for (const tool of Console.toolUnion) { |
| const inTask = task.tools.includes(tool); |
| const on = state.tools.includes(tool); |
| const label = document.createElement('label'); |
| label.className = `ctool${inTask ? '' : ' off'}`; |
| label.innerHTML = `<input type="checkbox" value="${escapeHtml(tool)}" |
| ${on ? 'checked' : ''} ${inTask ? '' : 'disabled'}> |
| <span class="mono">${tool}</span>`; |
| label.querySelector('input').addEventListener('change', () => { |
| state.tools = [...wrap.querySelectorAll('input:checked')].map((cb) => cb.value); |
| Console.refreshPreview(); |
| }); |
| wrap.appendChild(label); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const SEARCH_RESULT_CAP = 50; |
|
|
| function realModels(ids) { |
| return ids.filter((id) => !BASELINE_IDS.has(id)); |
| } |
|
|
| function catalogById() { |
| return new Map(Console.modelCatalog.map((m) => [m.id, m])); |
| } |
|
|
| function curatedModelList() { |
| const byId = catalogById(); |
| return CURATED_MODELS.map((id) => byId.get(id)).filter(Boolean); |
| } |
|
|
| |
| |
| |
| |
| function renderCuratedNote(searching) { |
| const note = document.getElementById('curated-note'); |
| const missing = searching ? 0 |
| : CURATED_MODELS.filter((id) => !catalogById().has(id)).length; |
| note.hidden = missing === 0; |
| note.textContent = missing > 0 |
| ? `${missing} curated model${missing === 1 ? '' : 's'} unavailable ` + |
| '(model catalog offline); showing what is available.' |
| : ''; |
| } |
|
|
| function initModelPicker() { |
| const btn = document.getElementById('model-dd-btn'); |
| const panel = document.getElementById('model-dd'); |
| const search = document.getElementById('model-search'); |
|
|
| btn.addEventListener('click', () => { |
| const open = panel.hidden; |
| panel.hidden = !open; |
| btn.setAttribute('aria-expanded', String(open)); |
| if (open) { |
| search.value = ''; |
| renderModelDropdown(''); |
| search.focus(); |
| } |
| }); |
| search.addEventListener('input', () => renderModelDropdown(search.value)); |
| document.addEventListener('click', (e) => { |
| if (panel.hidden) return; |
| if (!panel.contains(e.target) && e.target !== btn && !btn.contains(e.target)) { |
| closeModelDropdown(); |
| } |
| }); |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'Escape' && !panel.hidden) { |
| closeModelDropdown(); |
| btn.focus(); |
| } |
| }); |
|
|
| for (const boxId of ['baseline-flag', 'baseline-noev']) { |
| const cb = document.getElementById(boxId); |
| cb.checked = Console.state.models.includes(cb.value); |
| cb.addEventListener('change', () => { |
| const others = Console.state.models.filter((id) => id !== cb.value); |
| Console.state.models = cb.checked ? [...others, cb.value] : others; |
| Console.refreshPreview(); |
| }); |
| } |
|
|
| renderModelSelection(); |
| } |
|
|
| function closeModelDropdown() { |
| document.getElementById('model-dd').hidden = true; |
| document.getElementById('model-dd-btn').setAttribute('aria-expanded', 'false'); |
| } |
|
|
| function searchCatalog(q) { |
| const hits = Console.modelCatalog.filter((m) => |
| !BASELINE_IDS.has(m.id) && `${m.id} ${m.name}`.toLowerCase().includes(q)); |
| return { shown: hits.slice(0, SEARCH_RESULT_CAP), total: hits.length }; |
| } |
|
|
| function renderModelDropdown(query) { |
| const q = query.trim().toLowerCase(); |
| const list = document.getElementById('model-list'); |
| const hint = document.getElementById('model-dd-hint'); |
| renderCuratedNote(q.length > 0); |
|
|
| let models; |
| if (q.length === 0) { |
| models = curatedModelList(); |
| hint.textContent = 'Curated shortlist. Type to search the full catalog.'; |
| } else { |
| const { shown, total } = searchCatalog(q); |
| models = shown; |
| hint.textContent = total === 0 |
| ? 'No models match.' |
| : total > shown.length |
| ? `Showing ${shown.length} of ${total} matches. Keep typing to narrow.` |
| : `${total} match${total === 1 ? '' : 'es'}.`; |
| } |
|
|
| list.innerHTML = ''; |
| for (const model of models) { |
| const launchable = model.launchable !== false; |
| const row = document.createElement('label'); |
| row.className = 'mrow' + (launchable ? '' : ' mrow-disabled'); |
| row.innerHTML = ` |
| <input type="checkbox" value="${escapeHtml(model.id)}" |
| ${Console.state.models.includes(model.id) ? 'checked' : ''} |
| ${launchable ? '' : 'disabled'}> |
| <span class="mrow-main"> |
| <span class="model-name">${escapeHtml(model.name)}</span> |
| <span class="model-id mono">${escapeHtml(model.id)}</span> |
| </span> |
| <span class="model-price mono">${fmtPrice(model.prompt_price)} / ${fmtPrice(model.completion_price)} /Mtok</span> |
| `; |
| row.querySelector('input').addEventListener('change', (e) => { |
| toggleModel(model.id, e.target.checked); |
| }); |
| list.appendChild(row); |
| } |
| } |
|
|
| function toggleModel(id, on) { |
| const without = Console.state.models.filter((m) => m !== id); |
| Console.state.models = on ? [...without, id] : without; |
| renderModelSelection(); |
| Console.refreshPreview(); |
| } |
|
|
| function renderModelSelection() { |
| const selected = realModels(Console.state.models); |
| const label = document.getElementById('model-dd-label'); |
| const byId = catalogById(); |
| if (selected.length === 0) { |
| label.textContent = 'Select models'; |
| } else if (selected.length === 1) { |
| const m = byId.get(selected[0]); |
| label.textContent = m ? m.name : selected[0]; |
| } else { |
| label.textContent = `${selected.length} models selected`; |
| } |
|
|
| const tags = document.getElementById('model-tags'); |
| tags.innerHTML = ''; |
| for (const id of selected) { |
| const m = byId.get(id); |
| const tag = document.createElement('span'); |
| tag.className = 'mtag'; |
| tag.innerHTML = ` |
| <span>${escapeHtml(m ? m.name : id)}</span> |
| <button type="button" class="mtag-x" aria-label="Remove ${escapeHtml(id)}">×</button> |
| `; |
| tag.querySelector('.mtag-x').addEventListener('click', () => { |
| toggleModel(id, false); |
| const panel = document.getElementById('model-dd'); |
| if (!panel.hidden) { |
| renderModelDropdown(document.getElementById('model-search').value); |
| } |
| }); |
| tags.appendChild(tag); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
|
|
| let promptPreviewTimer = null; |
|
|
| function initPromptPreview() { |
| document.getElementById('prompt-view-btn').addEventListener('click', () => { |
| const box = document.getElementById('prompt-preview'); |
| const btn = document.getElementById('prompt-view-btn'); |
| box.hidden = !box.hidden; |
| btn.textContent = box.hidden ? 'view' : 'hide'; |
| btn.setAttribute('aria-pressed', String(!box.hidden)); |
| if (!box.hidden) refreshPromptPreview(); |
| }); |
| } |
|
|
| function schedulePromptPreviewRefresh() { |
| clearTimeout(promptPreviewTimer); |
| promptPreviewTimer = setTimeout(refreshPromptPreview, 300); |
| } |
|
|
| async function refreshPromptPreview() { |
| const box = document.getElementById('prompt-preview'); |
| if (box.hidden || !Console.state.task) return; |
| const sys = document.getElementById('prompt-preview-system'); |
| const brief = document.getElementById('prompt-preview-brief'); |
| const params = new URLSearchParams({ |
| version: Console.state.promptVersion, |
| step_budget: String(Console.state.stepBudget), |
| }); |
| try { |
| const resp = await fetch( |
| `/api/tasks/${Console.state.task.id}/prompt?${params}`); |
| if (!resp.ok) { |
| const err = await resp.json().catch(() => ({})); |
| sys.textContent = ''; |
| brief.textContent = `unavailable: ${err.error || `HTTP ${resp.status}`}`; |
| return; |
| } |
| const data = await resp.json(); |
| sys.textContent = data.system_prompt; |
| brief.textContent = data.brief; |
| } catch { |
| sys.textContent = ''; |
| brief.textContent = 'unavailable: network error'; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| let estimateTimer = null; |
|
|
| function scheduleEstimateRefresh() { |
| clearTimeout(estimateTimer); |
| estimateTimer = setTimeout(refreshEstimate, 400); |
| } |
|
|
| async function refreshEstimate() { |
| const s = Console.state; |
| if (!s.models.length || !s.stepBudget) { |
| Console.state.estimate = null; |
| renderHeadroom(); |
| return; |
| } |
| const params = new URLSearchParams({ |
| models: s.models.join(','), |
| step_budget: String(s.stepBudget), |
| seeds: String(s.seedCount), |
| }); |
| try { |
| const r = await fetch(`/api/estimate?${params}`); |
| Console.state.estimate = r.ok ? await r.json() : null; |
| } catch { |
| Console.state.estimate = null; |
| } |
| renderHeadroom(); |
| } |
|
|
| function renderHeadroom() { |
| const est = Console.state.estimate; |
| const line = document.getElementById('headroom-line'); |
| if (line) { |
| line.hidden = !(est && est.headroom_usd != null); |
| if (!line.hidden) { |
| line.textContent = `key headroom $${fmtUsd(est.headroom_usd)} remaining`; |
| } |
| } |
| const warning = document.getElementById('headroom-warning'); |
| warning.hidden = !(est && est.exceeds); |
| if (!warning.hidden) { |
| warning.textContent = |
| `Estimated cost ≈ $${fmtUsd(est.total_usd)} exceeds the key's remaining ` |
| + `credit $${fmtUsd(est.headroom_usd)} — the run would die partway. ` |
| + 'Launching stays possible; lower seeds/budget or raise the key limit.'; |
| } |
| } |
|
|
| function costRangeUsd(state, modelCatalog) { |
| const byId = new Map(modelCatalog.map((m) => [m.id, m])); |
| const prices = state.models |
| .filter((id) => !BASELINE_IDS.has(id)) |
| .map((id) => byId.get(id)) |
| .filter(Boolean) |
| .map((m) => m.prompt_price); |
| if (prices.length === 0) return null; |
| const totalTokens = prices.length * state.seedCount * state.stepBudget * EST_TOKENS_PER_STEP; |
| return { low: totalTokens * Math.min(...prices), high: totalTokens * Math.max(...prices) }; |
| } |
|
|
| function renderSweepEstimate() { |
| const s = Console.state; |
| const box = document.getElementById('sweep-estimate'); |
| const episodes = s.models.length * s.seedCount; |
| const stepCeiling = episodes * s.stepBudget; |
| const range = costRangeUsd(s, Console.modelCatalog); |
| let costLine; |
| if (!s.models.length) costLine = '-'; |
| else if (!range) costLine = '$0.00 (baselines only)'; |
| else if (range.low === range.high) costLine = `$${fmtUsd(range.low)}`; |
| else costLine = `$${fmtUsd(range.low)} – $${fmtUsd(range.high)}`; |
| box.innerHTML = ` |
| <div class="kvline mono"><b>${episodes}</b> episode${episodes === 1 ? '' : 's'} · ${s.models.length} model${s.models.length === 1 ? '' : 's'} × ${s.seedCount} seed${s.seedCount === 1 ? '' : 's'}</div> |
| <div class="kvline mono">≤ <b>${stepCeiling}</b> agent steps total</div> |
| <div class="kvline mono">${costLine}</div> |
| <div class="kvline mono" id="headroom-line" hidden></div> |
| <div class="fieldnote">rough, input-token dominated, before caching discounts</div> |
| `; |
| scheduleEstimateRefresh(); |
|
|
| const seedWarning = document.getElementById('seed-warning'); |
| seedWarning.hidden = s.seedCount >= MIN_SEEDS_FOR_VARIANCE; |
| seedWarning.textContent = 'Fewer than 5 seeds: variance not measurable.'; |
|
|
| const withheldCount = s.task ? s.task.tools.length - s.tools.length : 0; |
| const withheldNote = document.getElementById('withheld-note'); |
| withheldNote.hidden = withheldCount <= 0; |
| withheldNote.textContent = |
| `${withheldCount} tool(s) withheld: harder variant, scored as a separate condition.`; |
| } |
|
|
| |
|
|
| const FIELD_TO_BOX = { |
| knobs: 'knob-box', |
| tools_enabled: 'tool-box', |
| models: 'model-box', |
| }; |
|
|
| function normalizeError(resp, body) { |
| if (resp.status === 409) { |
| return { field: 'run_id', error: body.detail || 'duplicate run_id' }; |
| } |
| if (typeof body.field === 'string' && typeof body.error === 'string') return body; |
| return { field: 'request', error: body.detail || body.error || `launch failed (${resp.status})` }; |
| } |
|
|
| function showLaunchError(err) { |
| clearLaunchError(); |
| const p = document.getElementById('launch-error'); |
| p.textContent = `${err.field}: ${err.error}`; |
| p.hidden = false; |
| const boxId = FIELD_TO_BOX[err.field]; |
| if (boxId) document.getElementById(boxId).classList.add('invalid'); |
| } |
|
|
| function clearLaunchError() { |
| const p = document.getElementById('launch-error'); |
| p.hidden = true; |
| p.textContent = ''; |
| for (const id of Object.values(FIELD_TO_BOX)) { |
| document.getElementById(id).classList.remove('invalid'); |
| } |
| } |
|
|
| |
|
|
| async function checkHealth() { |
| const dot = document.getElementById('health-dot'); |
| try { |
| const r = await fetch('/api/health'); |
| dot.classList.toggle('ok', r.ok); |
| dot.classList.toggle('down', !r.ok); |
| dot.title = r.ok ? 'backend online' : 'backend unhealthy'; |
| } catch { |
| dot.classList.add('down'); |
| dot.title = 'backend unreachable'; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| async function loadRunHistory() { |
| let runs; |
| try { |
| runs = await fetch('/api/runs').then((r) => r.json()); |
| } catch { |
| return; |
| } |
| renderRunHistory(runs); |
| } |
|
|
| function historyModelsLabel(models) { |
| const real = models.filter((id) => !BASELINE_IDS.has(id)); |
| const parts = real.map((id) => id.split('/').pop()); |
| const baselines = models.length - real.length; |
| if (baselines) parts.push(baselines === 1 ? '1 baseline' : `${baselines} baselines`); |
| return parts.join(', ') || '-'; |
| } |
|
|
| function renderRunHistory(runs) { |
| const list = document.getElementById('history-list'); |
| document.getElementById('history-empty').hidden = runs.length > 0; |
| list.hidden = runs.length === 0; |
| list.innerHTML = ''; |
| for (const run of runs) { |
| const base = statusBase(run.status); |
| const row = document.createElement('button'); |
| row.type = 'button'; |
| row.className = 'crow history-row'; |
| row.innerHTML = ` |
| <span class="mono history-id">${escapeHtml(run.run_id)}</span> |
| <span class="history-task">${escapeHtml(run.task_id)} |
| <span class="tid">${escapeHtml(run.tier || '')}</span></span> |
| <span class="history-models" title="${escapeHtml(run.models.join(', '))}">${escapeHtml(historyModelsLabel(run.models))}</span> |
| <span class="mono history-seeds">${run.seed_count} seed${run.seed_count === 1 ? '' : 's'}</span> |
| <span class="mono history-cost">${run.cost_usd != null ? `$${fmtUsd(run.cost_usd)}` : '-'}</span> |
| <span class="stat status-${escapeHtml(base)}">${escapeHtml(base)}</span> |
| `; |
| row.addEventListener('click', () => openPastRun(run.run_id)); |
| list.appendChild(row); |
| } |
| } |
|
|
| async function openPastRun(runId) { |
| let snap; |
| try { |
| snap = await fetch(`/api/runs/${runId}`).then((r) => r.json()); |
| } catch { |
| return; |
| } |
| if (!snap || !snap.contract) return; |
| openLiveView(runId, snap.contract); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| function pulseOnce(el) { |
| if (!el) return; |
| el.classList.remove('deep-flash'); |
| void el.offsetWidth; |
| el.classList.add('deep-flash'); |
| setTimeout(() => el.classList.remove('deep-flash'), 1600); |
| } |
|
|
| async function openDeepLinkRun(runId) { |
| await loadRunHistory(); |
| await openPastRun(runId); |
| if (!Console.live || Console.live.runId !== runId) return; |
| pulseOnce(document.querySelector('#live .panel')); |
| const idEl = [...document.querySelectorAll('#history-list .history-id')] |
| .find((el) => el.textContent === runId); |
| if (idEl) pulseOnce(idEl.closest('.history-row')); |
| } |
|
|
| window.addEventListener('je:open-run', (e) => openDeepLinkRun(e.detail)); |
|
|
| |
| |
| |
| |
| |
| |
|
|
| function connectEvents(runId) { |
| let lastSeq = Number(sessionStorage.getItem(`seq:${runId}`) || 0); |
| const es = new EventSource(`/api/runs/${runId}/events?after=${lastSeq}`); |
| es.onopen = async () => { |
| setHealth('live'); |
| try { |
| const snap = await fetch(`/api/runs/${runId}`).then((r) => r.json()); |
| await applySnapshot(snap); |
| } catch { |
| |
| } |
| }; |
| es.onmessage = (ev) => { |
| const seq = Number(ev.lastEventId); |
| if (seq <= lastSeq) return; |
| lastSeq = seq; |
| sessionStorage.setItem(`seq:${runId}`, String(seq)); |
| applyEvent(JSON.parse(ev.data)); |
| }; |
| es.onerror = () => setHealth('reconnecting'); |
| return es; |
| } |
|
|
| function setHealth(state) { |
| const el = document.getElementById('live-conn'); |
| if (!el) return; |
| el.textContent = state; |
| el.className = `conn-badge conn-${state}`; |
| } |
|
|
| |
|
|
| function openLiveView(runId, contract) { |
| if (Console.live && Console.live.es) Console.live.es.close(); |
| document.getElementById('results').hidden = true; |
| Console.results = null; |
|
|
| const models = contract.sweep.models; |
| const seedCount = contract.sweep.seeds; |
| const tiles = {}; |
| for (const model of models) { |
| for (let seed = 1; seed <= seedCount; seed++) { |
| const key = `${slugModel(model)}/seed${seed}`; |
| tiles[key] = { |
| key, model, seed, status: 'queued', stepsByIdx: {}, detail: {}, cost: 0, |
| |
| |
| |
| |
| steps: 0, tokensIn: 0, tokensOut: 0, |
| }; |
| } |
| } |
| Console.live = { |
| runId, contract, tiles, tileEls: {}, focusedKey: null, rowEls: {}, |
| runStatus: 'queued', runCostFinal: null, terminalHandled: false, |
| es: null, trajCache: {}, detailTimer: null, |
| }; |
|
|
| renderLiveShell(runId, tiles); |
| const live = document.getElementById('live'); |
| live.hidden = false; |
| live.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
|
|
| Console.live.es = connectEvents(runId); |
| const firstKey = Object.keys(tiles)[0]; |
| if (firstKey) selectEpisode(firstKey); |
| } |
|
|
| function renderLiveShell(runId, tiles) { |
| document.getElementById('live-run-id').textContent = runId; |
| setRunStatusChip('queued'); |
| setHealth('connecting'); |
| document.getElementById('live-banner').hidden = true; |
| document.getElementById('live-terminal-note').hidden = true; |
| document.getElementById('player-grid').hidden = true; |
| document.getElementById('step-feed').innerHTML = ''; |
|
|
| const grid = document.getElementById('episode-grid'); |
| grid.innerHTML = ''; |
| for (const key of Object.keys(tiles)) { |
| const tile = tiles[key]; |
| const card = document.createElement('button'); |
| card.type = 'button'; |
| card.className = 'ep-tile'; |
| card.innerHTML = ` |
| <span class="ep-tile-chip status-queued"> |
| <span class="ep-tile-glyph">${statusGlyph('queued')}</span>queued |
| </span> |
| <span class="ep-tile-name">${escapeHtml(tile.model)}</span> |
| <span class="ep-tile-seed mono">seed ${tile.seed}</span> |
| <span class="ep-tile-counters mono"> |
| <span class="ep-tile-steps">0 steps</span><span class="ep-tile-tokens">0 tok</span> |
| </span> |
| <span class="ep-tile-reason"></span> |
| `; |
| card.addEventListener('click', () => selectEpisode(key)); |
| grid.appendChild(card); |
| Console.live.tileEls[key] = card; |
| } |
| renderScoreboard(); |
| } |
|
|
| function setRunStatusChip(status) { |
| const el = document.getElementById('live-status'); |
| const base = statusBase(status); |
| el.textContent = status; |
| el.className = `stat status-${base}`; |
| } |
|
|
| function showLiveBanner(msg) { |
| const b = document.getElementById('live-banner'); |
| b.textContent = msg; |
| b.hidden = false; |
| } |
|
|
| function showLiveTerminalNote(status) { |
| const note = document.getElementById('live-terminal-note'); |
| note.textContent = `Run ended without results (${status}). ` + |
| `failed/interrupted runs never produce a results.json.`; |
| note.hidden = false; |
| setHealth('closed'); |
| } |
|
|
| function tileStepCount(tile) { |
| return tile.steps; |
| } |
|
|
| function tileTokenTotal(tile) { |
| return tile.tokensIn + tile.tokensOut; |
| } |
|
|
| |
| |
| |
| |
| |
| function setTileStep(tile, idx, rec) { |
| const prev = tile.stepsByIdx[idx]; |
| const prevIn = prev ? (prev.tokens_in || 0) : 0; |
| const prevOut = prev ? (prev.tokens_out || 0) : 0; |
| tile.stepsByIdx[idx] = rec; |
| if (!prev) tile.steps += 1; |
| tile.tokensIn += (rec.tokens_in || 0) - prevIn; |
| tile.tokensOut += (rec.tokens_out || 0) - prevOut; |
| } |
|
|
| function renderTile(key) { |
| const tile = Console.live.tiles[key]; |
| const card = Console.live.tileEls[key]; |
| if (!tile || !card) return; |
| const base = statusBase(tile.status); |
| const chip = card.querySelector('.ep-tile-chip'); |
| chip.className = `ep-tile-chip status-${base}`; |
| chip.innerHTML = `<span class="ep-tile-glyph">${statusGlyph(base)}</span>${escapeHtml(base)}`; |
| card.querySelector('.ep-tile-steps').textContent = `${tileStepCount(tile)} steps`; |
| card.querySelector('.ep-tile-tokens').textContent = `${tileTokenTotal(tile)} tok`; |
| card.querySelector('.ep-tile-reason').textContent = |
| base === 'failed' ? failureReason(tile.status) : ''; |
| card.classList.toggle('ep-tile-focused', Console.live.focusedKey === key); |
| } |
|
|
| function renderAllTiles() { |
| for (const key of Object.keys(Console.live.tiles)) renderTile(key); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| function selectEpisode(key) { |
| Console.live.focusedKey = key; |
| renderAllTiles(); |
| document.getElementById('player-grid').hidden = false; |
| document.getElementById('step-feed-title').textContent = key; |
| renderStepLog(key, { forceBottom: true }); |
| refreshFocusedDetail(key); |
| } |
|
|
| async function refreshFocusedDetail(key) { |
| const live = Console.live; |
| const tile = live.tiles[key]; |
| if (!tile) return; |
| try { |
| const [modelslug, seedTag] = key.split('/'); |
| const seed = seedTag.replace('seed', ''); |
| const resp = await fetch(`/api/runs/${live.runId}/episodes/${modelslug}/${seed}/trajectory`); |
| if (!resp.ok) return; |
| const traj = await resp.json(); |
| for (const rec of traj) { |
| setTileStep(tile, rec.step_idx, rec); |
| tile.detail[rec.step_idx] = rec; |
| } |
| live.trajCache[key] = traj; |
| renderTile(key); |
| if (live.focusedKey === key) renderStepLog(key); |
| } catch { |
| |
| } |
| } |
|
|
| function scheduleFocusedDetailRefresh(key) { |
| const live = Console.live; |
| if (!live || live.focusedKey !== key) return; |
| clearTimeout(live.detailTimer); |
| live.detailTimer = setTimeout(() => refreshFocusedDetail(key), 400); |
| } |
|
|
| function compactJson(v) { |
| const s = JSON.stringify(v); |
| return s.length > 90 ? `${s.slice(0, 90)}…` : s; |
| } |
|
|
| function stepBorderColor(rec) { |
| if (!rec.ok) return 'var(--l4)'; |
| if (rec.tool === 'submit') return 'var(--accent)'; |
| return 'var(--line-2)'; |
| } |
|
|
| function stepLogRow(idx, rec) { |
| const hasDetail = 'args' in rec; |
| const row = document.createElement('div'); |
| row.className = 'rstep'; |
| row.style.borderLeftColor = stepBorderColor(rec); |
| row.innerHTML = ` |
| <div class="rhead"> |
| <span class="rn mono">${idx}</span> |
| <span class="mono rtool">${escapeHtml(rec.tool)}</span> |
| <span class="mono rargs">${hasDetail ? escapeHtml(compactJson(rec.args)) : '…'}</span> |
| <span class="mono rtok">${rec.tokens_in || 0}→${rec.tokens_out || 0}</span> |
| </div> |
| ${hasDetail ? `<div class="robs">${escapeHtml(compactJson(rec.observation))}</div>` : ''} |
| `; |
| return row; |
| } |
|
|
| |
| |
| |
| |
| |
| function isNearBottom(el, threshold = 4) { |
| return el.scrollHeight - el.scrollTop - el.clientHeight <= threshold; |
| } |
|
|
| |
| |
| |
| |
| |
| function renderStepLog(key, { forceBottom = false } = {}) { |
| const tile = Console.live.tiles[key]; |
| if (!tile) return; |
| const feed = document.getElementById('step-feed'); |
| const stayAtBottom = forceBottom || isNearBottom(feed); |
| feed.innerHTML = ''; |
| const idxs = Object.keys(tile.stepsByIdx).map(Number).sort((a, b) => a - b); |
| const rowEls = {}; |
| let lastRow = null; |
| for (const idx of idxs) { |
| const row = stepLogRow(idx, tile.stepsByIdx[idx]); |
| feed.appendChild(row); |
| rowEls[idx] = row; |
| lastRow = row; |
| } |
| Console.live.rowEls = rowEls; |
| if (stayAtBottom && lastRow) lastRow.scrollIntoView({ block: 'nearest' }); |
| } |
|
|
| |
| |
| |
| |
| |
| function upsertStepRow(key, idx, rec) { |
| const live = Console.live; |
| if (!live || live.focusedKey !== key) return; |
| const feed = document.getElementById('step-feed'); |
| const stayAtBottom = isNearBottom(feed); |
| const row = stepLogRow(idx, rec); |
| const existing = live.rowEls[idx]; |
| if (existing) existing.replaceWith(row); |
| else feed.appendChild(row); |
| live.rowEls[idx] = row; |
| if (stayAtBottom) row.scrollIntoView({ block: 'nearest' }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| function renderScoreboard() { |
| const live = Console.live; |
| if (!live) return; |
| const tiles = Object.values(live.tiles); |
| const totalSteps = tiles.reduce((s, t) => s + tileStepCount(t), 0); |
| const totalBudget = tiles.length * (live.contract.step_budget || 0); |
| const totalTokens = tiles.reduce((s, t) => s + tileTokenTotal(t), 0); |
| const costSoFar = live.runCostFinal != null |
| ? live.runCostFinal |
| : tiles.reduce((s, t) => s + (t.cost || 0), 0); |
| const counts = {}; |
| for (const t of tiles) { |
| const b = statusBase(t.status); |
| counts[b] = (counts[b] || 0) + 1; |
| } |
| const board = document.getElementById('scoreboard-body'); |
| if (!board) return; |
| board.innerHTML = ` |
| <table class="kv"> |
| <tr><td>Steps used</td><td class="mono num">${totalSteps} / ${totalBudget}</td></tr> |
| <tr><td>Cumulative tokens</td><td class="mono num">${totalTokens}</td></tr> |
| <tr><td>Run cost so far</td><td class="mono num">$${fmtUsd(costSoFar)}</td></tr> |
| </table> |
| <div class="lab" style="margin-top:10px">Episodes</div> |
| <div class="statusline mono">${Object.entries(counts) |
| .map(([b, n]) => `${statusGlyph(b)} ${n} ${escapeHtml(b)}`).join(' ')}</div> |
| `; |
| } |
|
|
| |
|
|
| function applyEvent(evt) { |
| const live = Console.live; |
| if (!live) return; |
| if (evt.type === 'step') { |
| const tile = live.tiles[evt.episode]; |
| if (!tile) return; |
| const idx = evt.data.step_idx; |
| const rec = { ...tile.detail[idx], ...evt.data }; |
| setTileStep(tile, idx, rec); |
| renderTile(evt.episode); |
| if (live.focusedKey === evt.episode) { |
| upsertStepRow(evt.episode, idx, rec); |
| scheduleFocusedDetailRefresh(evt.episode); |
| } |
| renderScoreboard(); |
| } else if (evt.type === 'episode') { |
| const tile = live.tiles[evt.episode]; |
| if (!tile) return; |
| tile.status = evt.data.status; |
| if (evt.data.cost_usd != null) tile.cost = evt.data.cost_usd; |
| renderTile(evt.episode); |
| renderScoreboard(); |
| } else if (evt.type === 'run') { |
| if (evt.data.status) { |
| live.runStatus = evt.data.status; |
| setRunStatusChip(evt.data.status); |
| maybeHandleTerminal(evt.data.status); |
| } else if (evt.data.abort_reason) { |
| showLiveBanner(`Run aborted: ${evt.data.abort_reason}`); |
| } else if (evt.data.warning) { |
| const who = evt.episode ? `${evt.episode}: ` : ''; |
| showLiveBanner(`${who}${evt.data.warning}` + |
| (evt.data.error ? `: ${evt.data.error}` : '')); |
| } |
| } |
| } |
|
|
| async function applySnapshot(status) { |
| const live = Console.live; |
| if (!live) return; |
| live.runStatus = status.status; |
| setRunStatusChip(status.status); |
| if (status.cost_usd != null) live.runCostFinal = status.cost_usd; |
| if (status.abort_reason) showLiveBanner(`Run aborted: ${status.abort_reason}`); |
| const episodes = status.episodes || {}; |
| for (const [key, epStatus] of Object.entries(episodes)) { |
| if (live.tiles[key]) live.tiles[key].status = epStatus; |
| } |
| renderAllTiles(); |
| |
| |
| |
| |
| |
| await Promise.all(Object.keys(episodes) |
| .filter((key) => episodes[key] !== 'queued' && live.tiles[key]) |
| .map((key) => refreshFocusedDetail(key))); |
| renderScoreboard(); |
| maybeHandleTerminal(status.status); |
| } |
|
|
| function maybeHandleTerminal(status) { |
| const live = Console.live; |
| if (!live || live.terminalHandled) return; |
| const base = statusBase(status); |
| if (base === 'queued' || base === 'running') return; |
| live.terminalHandled = true; |
| if (live.es) { live.es.close(); live.es = null; } |
| |
| |
| |
| setHealth('closed'); |
| finalizeLiveCost(live.runId); |
| loadRunHistory(); |
| if (base === 'finished' || base === 'aborted') { |
| loadResults(live.runId); |
| } else { |
| |
| |
| showLiveTerminalNote(status); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function finalizeLiveCost(runId) { |
| try { |
| const snap = await fetch(`/api/runs/${runId}`).then((r) => r.json()); |
| const live = Console.live; |
| if (!live || live.runId !== runId) return; |
| if (snap.cost_usd != null) live.runCostFinal = snap.cost_usd; |
| renderScoreboard(); |
| } catch { |
| |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| function onActivateKey(handler) { |
| return (e) => { |
| if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') { |
| e.preventDefault(); |
| handler(); |
| } |
| }; |
| } |
|
|
| async function loadResults(runId) { |
| let resp; |
| try { |
| resp = await fetch(`/api/runs/${runId}/results`); |
| } catch { |
| showLiveTerminalNote('results unreachable: network error'); |
| return; |
| } |
| if (!resp.ok) { |
| |
| |
| |
| showLiveTerminalNote(`results unavailable (HTTP ${resp.status})`); |
| return; |
| } |
| const data = await resp.json(); |
| const statusSnap = await fetch(`/api/runs/${runId}`).then((r) => r.json()) |
| .catch(() => null); |
| Console.results = { runId, data, statusSnap, model: null, seed: null, trajCache: {} }; |
| renderResultsModelTable(); |
| document.getElementById('results').hidden = false; |
| document.getElementById('results-level-2').hidden = true; |
| document.getElementById('results-level-3').hidden = true; |
| document.getElementById('results').scrollIntoView({ behavior: 'smooth', block: 'start' }); |
| } |
|
|
| function fmtStat(stat) { |
| if (!stat || stat.mean == null) return '-'; |
| return `${stat.mean.toFixed(3)} ± ${(stat.std ?? 0).toFixed(3)}`; |
| } |
|
|
| function fmtMean(stat) { |
| if (!stat || stat.mean == null) return '-'; |
| return stat.mean.toFixed(3); |
| } |
|
|
| function fmtPassed(passed) { |
| if (passed === true) return 'pass'; |
| if (passed === false) return 'fail'; |
| return '-'; |
| } |
|
|
| function renderResultsModelTable() { |
| const { data } = Console.results; |
| const wrap = document.getElementById('results-model-table-wrap'); |
| const models = Object.keys(data); |
| let rows = ''; |
| for (const model of models) { |
| const agg = data[model].aggregate; |
| rows += ` |
| <tr class="results-model-row" data-model="${escapeHtml(model)}" tabindex="0" role="button"> |
| <td class="mono">${escapeHtml(model)}</td> |
| <td class="mono num">${fmtStat(agg.final)}</td> |
| <td class="mono num">${fmtMean(agg.outcome)}</td> |
| <td class="mono num">${fmtMean(agg.process)}</td> |
| <td class="mono num">${(agg.fail_rate * 100).toFixed(0)}%</td> |
| <td class="mono num">${agg.n}</td> |
| <td>${agg.variance_measurable ? 'yes' : 'no'}</td> |
| <td class="rationale-cell">${escapeHtml(agg.rationale)}</td> |
| </tr>`; |
| } |
| wrap.innerHTML = ` |
| <table class="results-table"> |
| <thead><tr> |
| <th>Model</th><th>Final (mean ± std)</th><th>Outcome</th><th>Process</th> |
| <th>Fail rate</th><th>n</th><th>Variance measurable</th><th>Rationale</th> |
| </tr></thead> |
| <tbody>${rows}</tbody> |
| </table>`; |
| for (const row of wrap.querySelectorAll('.results-model-row')) { |
| row.addEventListener('click', () => selectModel(row.dataset.model)); |
| row.addEventListener('keydown', onActivateKey(() => selectModel(row.dataset.model))); |
| } |
| document.getElementById('results-raw-1').textContent = JSON.stringify(data, null, 2); |
| } |
|
|
| function selectModel(model) { |
| Console.results.model = model; |
| Console.results.seed = null; |
| renderSeedRows(model); |
| document.getElementById('results-level-2').hidden = false; |
| document.getElementById('results-level-3').hidden = true; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function zipSeedsToReports(model, seedCount, episodes, reports) { |
| const slug = slugModel(model); |
| const queue = [...reports]; |
| const rows = []; |
| for (let seed = 1; seed <= seedCount; seed++) { |
| const key = `${slug}/seed${seed}`; |
| const status = episodes[key] || 'unknown'; |
| const base = statusBase(status); |
| const report = base === 'finished' ? queue.shift() : null; |
| rows.push({ seed, key, status, base, report }); |
| } |
| return rows; |
| } |
|
|
| function renderSeedRows(model) { |
| const { data, statusSnap, runId } = Console.results; |
| const modelData = data[model]; |
| const seedCount = (statusSnap && statusSnap.request && statusSnap.request.seed_count) || |
| (Console.live && Console.live.contract.sweep.seeds) || 0; |
| const episodes = (statusSnap && statusSnap.episodes) || {}; |
| const rows = zipSeedsToReports(model, seedCount, episodes, modelData.reports); |
| Console.results.seedRows = rows; |
|
|
| document.getElementById('results-level-2-title').textContent = model; |
| const wrap = document.getElementById('results-seed-table-wrap'); |
| let body = ''; |
| for (const r of rows) { |
| if (r.report) { |
| body += ` |
| <tr class="results-seed-row" data-seed="${r.seed}" tabindex="0" role="button"> |
| <td class="mono">${r.seed}</td> |
| <td class="mono num">${r.report.final.toFixed(3)}</td> |
| <td class="mono num">${r.report.outcome.toFixed(3)}</td> |
| <td class="mono num">${r.report.process.toFixed(3)}</td> |
| <td class="mono num">${r.report.fp_rate.toFixed(3)}</td> |
| <td>${r.report.auto_fail ? '<span class="stat bad">auto-fail</span>' : ''}</td> |
| <td>${fmtPassed(r.report.passed)}</td> |
| <td class="mono" data-seed-forced="${r.seed}">…</td> |
| </tr>`; |
| } else { |
| body += ` |
| <tr class="results-seed-row results-seed-row-noreport" data-seed="${r.seed}"> |
| <td class="mono">${r.seed}</td> |
| <td colspan="5" class="mono">${escapeHtml(r.status)}</td> |
| <td></td> |
| <td class="mono" data-seed-forced="${r.seed}">…</td> |
| </tr>`; |
| } |
| } |
| wrap.innerHTML = ` |
| <table class="results-table"> |
| <thead><tr> |
| <th>Seed</th><th>Final</th><th>Outcome</th><th>Process</th><th>FP rate</th> |
| <th>Auto-fail</th><th>Passed</th><th>Forced submit</th> |
| </tr></thead> |
| <tbody>${body}</tbody> |
| </table>`; |
| for (const row of wrap.querySelectorAll('.results-seed-row:not(.results-seed-row-noreport)')) { |
| row.addEventListener('click', () => selectSeed(model, Number(row.dataset.seed))); |
| row.addEventListener('keydown', onActivateKey(() => selectSeed(model, Number(row.dataset.seed)))); |
| } |
| document.getElementById('results-raw-2').textContent = JSON.stringify(modelData, null, 2); |
| document.getElementById('results-breakdown').innerHTML = ''; |
| document.getElementById('results-breakdown').hidden = true; |
|
|
| for (const r of rows) { |
| if (r.base === 'queued') continue; |
| fillForcedSubmitMarker(runId, model, r.seed); |
| } |
| } |
|
|
| function isForcedSubmit(traj) { |
| if (!traj.length) return false; |
| const last = traj[traj.length - 1]; |
| return last.tool === 'submit' && typeof last.reasoning_trace === 'string' && |
| last.reasoning_trace.startsWith('FORCED'); |
| } |
|
|
| async function fetchTrajectory(runId, model, seed) { |
| const slug = slugModel(model); |
| const key = `${slug}/seed${seed}`; |
| const cached = (Console.live && Console.live.trajCache[key]) || |
| Console.results.trajCache[key]; |
| if (cached) return cached; |
| const resp = await fetch(`/api/runs/${runId}/episodes/${slug}/${seed}/trajectory`); |
| if (!resp.ok) throw new Error(`trajectory unavailable (HTTP ${resp.status})`); |
| const traj = await resp.json(); |
| Console.results.trajCache[key] = traj; |
| return traj; |
| } |
|
|
| async function fillForcedSubmitMarker(runId, model, seed) { |
| const cell = document.querySelector(`[data-seed-forced="${seed}"]`); |
| try { |
| const traj = await fetchTrajectory(runId, model, seed); |
| if (cell) cell.textContent = isForcedSubmit(traj) ? 'forced' : 'no'; |
| } catch { |
| if (cell) cell.textContent = '-'; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function renderScoreBreakdown(report) { |
| const wrap = document.getElementById('results-breakdown'); |
| if (!report) { |
| wrap.hidden = true; |
| wrap.innerHTML = ''; |
| return; |
| } |
| const weights = (Console.results.statusSnap && |
| Console.results.statusSnap.contract.scoring.weights) || { outcome: 0, process: 0, rationale: 0 }; |
| const pct = (x) => (x * 100).toFixed(0); |
| const fabrications = report.fabrications || []; |
| wrap.hidden = false; |
| wrap.innerHTML = ` |
| <div class="box breakdown"> |
| <div class="wbar"> |
| <i style="width:${pct(weights.outcome)}%;background:var(--l2)"></i> |
| <i style="width:${pct(weights.process)}%;background:var(--l3)"></i> |
| <i style="width:${pct(weights.rationale)}%;background:var(--accent)"></i> |
| </div> |
| <table class="results-table"> |
| <thead><tr><th>Component</th><th>Weight</th><th>Score</th></tr></thead> |
| <tbody> |
| <tr><td>Outcome</td><td class="mono num">${pct(weights.outcome)}%</td><td class="mono num">${report.outcome.toFixed(3)}</td></tr> |
| <tr><td>Process</td><td class="mono num">${pct(weights.process)}%</td><td class="mono num">${report.process.toFixed(3)}</td></tr> |
| <tr><td>Rationale</td><td class="mono num">${pct(weights.rationale)}%</td><td class="rationale-cell">${escapeHtml(report.rationale)}</td></tr> |
| <tr><td><b>Final</b></td><td></td><td class="mono num"><b>${report.final.toFixed(3)}</b></td></tr> |
| </tbody> |
| </table> |
| <div class="kvline mono"> |
| <span>fp_rate ${report.fp_rate.toFixed(3)}</span> |
| <span class="stat ${report.auto_fail ? 'bad' : 'ok'}">${report.auto_fail ? 'auto-fail' : 'no auto-fail'}</span> |
| </div> |
| <div class="lab" style="margin-top:8px">Fabrications</div> |
| <div class="dials">${fabrications.length |
| ? fabrications.map((f) => `<span class="dial mono">${escapeHtml(f)}</span>`).join('') |
| : '<span class="fieldnote">none</span>'}</div> |
| </div>`; |
| } |
|
|
| async function selectSeed(model, seed) { |
| Console.results.seed = seed; |
| const runId = Console.results.runId; |
| const row = (Console.results.seedRows || []).find((r) => r.seed === seed); |
| renderScoreBreakdown(row && row.report); |
| let traj; |
| try { |
| traj = await fetchTrajectory(runId, model, seed); |
| } catch (e) { |
| const wrap = document.getElementById('results-step-table-wrap'); |
| wrap.innerHTML = `<p class="note note-error">${escapeHtml(e.message)}</p>`; |
| document.getElementById('results-level-3-title').textContent = `${model} / seed ${seed}`; |
| document.getElementById('results-level-3').hidden = false; |
| return; |
| } |
| renderStepTimeline(model, seed, traj); |
| document.getElementById('results-level-3').hidden = false; |
| } |
|
|
| function renderStepTimeline(model, seed, traj) { |
| document.getElementById('results-level-3-title').textContent = `${model} / seed ${seed}`; |
| const wrap = document.getElementById('results-step-table-wrap'); |
| wrap.innerHTML = ''; |
| const table = document.createElement('table'); |
| table.className = 'results-table'; |
| table.innerHTML = '<thead><tr><th>Step</th><th>Tool</th><th>Args</th><th>Status</th></tr></thead>'; |
| const tbody = document.createElement('tbody'); |
| for (const rec of traj) { |
| const tr = document.createElement('tr'); |
| tr.className = 'step-timeline-row' + (rec.ok ? '' : ' step-timeline-row-error'); |
| tr.tabIndex = 0; |
| tr.setAttribute('role', 'button'); |
|
|
| const argsStr = JSON.stringify(rec.args); |
| const summary = argsStr.length > 80 ? `${argsStr.slice(0, 80)}…` : argsStr; |
|
|
| const tdStep = document.createElement('td'); |
| tdStep.className = 'mono'; |
| tdStep.textContent = `#${rec.step_idx}`; |
| const tdTool = document.createElement('td'); |
| tdTool.textContent = rec.tool; |
| const tdArgs = document.createElement('td'); |
| tdArgs.className = 'mono'; |
| tdArgs.textContent = summary; |
| const tdOk = document.createElement('td'); |
| const badge = document.createElement('span'); |
| badge.className = `badge ${rec.ok ? 'badge-ok' : 'badge-error'}`; |
| badge.textContent = rec.ok ? 'ok' : 'error'; |
| tdOk.appendChild(badge); |
|
|
| tr.append(tdStep, tdTool, tdArgs, tdOk); |
| tr.addEventListener('click', () => toggleStepDetail(tr, rec)); |
| tr.addEventListener('keydown', onActivateKey(() => toggleStepDetail(tr, rec))); |
| tbody.appendChild(tr); |
| } |
| table.appendChild(tbody); |
| wrap.appendChild(table); |
| document.getElementById('results-raw-3').textContent = JSON.stringify(traj, null, 2); |
| } |
|
|
| function toggleStepDetail(tr, rec) { |
| const next = tr.nextElementSibling; |
| if (next && next.classList.contains('step-detail-row')) { |
| next.remove(); |
| return; |
| } |
| const table = tr.closest('table'); |
| const openDetail = table.querySelector('.step-detail-row'); |
| if (openDetail) openDetail.remove(); |
|
|
| const detailRow = document.createElement('tr'); |
| detailRow.className = 'step-detail-row'; |
| const td = document.createElement('td'); |
| td.colSpan = 4; |
| const pre = document.createElement('pre'); |
| pre.textContent = JSON.stringify({ args: rec.args, observation: rec.observation }, null, 2); |
| td.appendChild(pre); |
| detailRow.appendChild(td); |
| tr.after(detailRow); |
| } |
|
|
| |
|
|
| const RAW_TOGGLE_WRAP_ID = { |
| 1: 'results-model-table-wrap', |
| 2: 'results-seed-table-wrap', |
| 3: 'results-step-table-wrap', |
| }; |
|
|
| function onRawToggle(btn) { |
| const level = btn.dataset.level; |
| const pre = document.getElementById(`results-raw-${level}`); |
| const wrap = document.getElementById(RAW_TOGGLE_WRAP_ID[level]); |
| const showingRaw = !pre.hidden; |
| pre.hidden = showingRaw; |
| wrap.hidden = !showingRaw; |
| btn.textContent = showingRaw ? 'Raw JSON' : 'Rendered'; |
| } |
|
|
| |
| |
| |
| document.getElementById('step-budget-range').addEventListener('input', (e) => |
| onStepBudgetChange(Number(e.target.value))); |
| document.getElementById('step-budget-num').addEventListener('input', (e) => |
| onStepBudgetChange(Number(e.target.value))); |
|
|
| Console.init(); |
|
|