// Bench page renderer — DOM controller for the researcher experiment // platform surface (#187, docs/design-refs/bench-design-pack-160.md). // // The pure data model lives in bench-model.js. This file owns the DOM: // list rendering (L0), sub-tab filter, detail view (L1) with charts strip // + grid/table for the three kinds, per-cell drill (L2 stub — click writes // a lightweight trace shim into the pane), New-experiment kind picker, // empty state, code_result.json export, and "re-score without re-run". // // Wire policy: today Bench is fixture-driven (mock-first). The pack §7 // tracks the seams G18 / G19 / G20 / G21 / G23 that will replace the // loader once the daemon side lands. Every mock-flavoured chip in the UI // says so explicitly ("local sequential run · G18/G19 pending upstream") // per the honesty rule from the demo tier's mock-annotation contract. // // Charts are hand-rolled SVG so we don't pull a chart library into the // dependency tree. Bars use CSS variables for accent/warn/success so the // palette follows the light/dark theme without dedicated tokens. 'use strict' ;(function () { const M = (typeof require !== 'undefined') ? require('./bench-model.js') : window.__dshBenchModel const FIXTURE = (typeof require !== 'undefined') ? require('./bench-fixture.js') : window.__dshBenchFixture // ---- state -------------------------------------------------------------- const state = M.createBenchState() let root = null //
let started = false let selectedCell = null // { kind, ... } — for the drill panel // ---- boot --------------------------------------------------------------- function show() { if (!root) root = document.querySelector('[data-pane="bench"]') if (!root) return if (!started) { started = true // Load fixture on first show — empty state fallback happens // automatically if the fixture is missing. if (FIXTURE && Array.isArray(FIXTURE.experiments)) { M.loadExperiments(state, FIXTURE) } } render() } function render() { if (!root) return renderList() renderDetail() } // ---- L0 list rendering -------------------------------------------------- function renderList() { const listEl = root.querySelector('[data-bench-list]') const countEl = root.querySelector('[data-bench-list-count]') if (!listEl) return const rows = M.projectL0Rows(state, { subTab: state.subTab }) // Sub-tab chip active state. for (const chip of root.querySelectorAll('[data-bench-subtab]')) { const sel = chip.dataset.benchSubtab === state.subTab chip.classList.toggle('active', sel) chip.setAttribute('aria-selected', sel ? 'true' : 'false') } if (countEl) countEl.textContent = `${rows.length} ${rows.length === 1 ? 'experiment' : 'experiments'}` if (!rows.length) { listEl.innerHTML = '' const empty = renderEmpty() listEl.appendChild(empty) return } listEl.innerHTML = '' const tbl = document.createElement('div') tbl.className = 'bench-table' tbl.setAttribute('role', 'table') tbl.innerHTML = `
Kind Name Status Progress Score Avg P50 lat Created
` for (const row of rows) tbl.appendChild(renderListRow(row)) listEl.appendChild(tbl) } function renderListRow(row) { const el = document.createElement('button') el.type = 'button' el.className = 'bench-row bench-row-data' el.setAttribute('role', 'row') el.dataset.benchExp = row.id if (state.selectedId === row.id) el.classList.add('selected') // Kind chip const kindLabel = row.kind === 'matrix' ? 'Matrix' : row.kind === 'ab' ? 'A/B' : 'Rep' // Status glyph const statusText = row.status === 'running' ? 'running' : row.status === 'queued' ? 'queued' : row.status === 'done' ? 'done' : row.status // Metric slots — vary by kind (pack §3 L0 columns) let scoreTxt = '—', avgTxt = '—' if (row.kind === 'matrix') { scoreTxt = fmtRatio(row.summary.passAtK, 2) if (row.summary.kUsed) scoreTxt = `Pass@${row.summary.kUsed} ${scoreTxt}` avgTxt = fmtRatio(row.summary.aveScore, 2) } else if (row.kind === 'ab') { scoreTxt = fmtDelta(row.summary.dPassRate, 2, 'ΔPass') avgTxt = fmtDelta(row.summary.dScore, 2, 'ΔScore') } else if (row.kind === 'rep') { scoreTxt = `σ ${fmtRatio(row.summary.sigma, 2)}` avgTxt = fmtRatio(row.summary.aveScore, 2) } const p50Ms = row.summary && Number.isFinite(row.summary.p50) ? row.summary.p50 : null const p50Txt = p50Ms == null ? '—' : `${(p50Ms / 1000).toFixed(1)}s` const p50Bucket = row.summary && row.summary.p50Bucket ? row.summary.p50Bucket : 'neutral' const created = row.createdAt ? relTime(row.createdAt) : '—' const progress = row.progress ? `${row.progress.done || 0}/${row.progress.total || 0}` : '—' el.innerHTML = ` ${kindLabel} ${escapeHtml(row.name)} ${statusText} ${progress} ${scoreTxt} ${avgTxt} ${p50Txt} ${created}` el.addEventListener('click', () => { M.selectExperiment(state, row.id) selectedCell = null render() const detail = root.querySelector('[data-bench-detail]') if (detail && typeof detail.scrollIntoView === 'function') { detail.scrollIntoView({ behavior: 'smooth', block: 'start' }) } }) return el } function renderEmpty() { const wrap = document.createElement('div') wrap.className = 'bench-empty' wrap.innerHTML = `
No experiments in this view.
Bench runs one prompt-set against a grid of models, compares two plugin variants, or measures variance across N repetitions of the same input.
` return wrap } // ---- L1 detail ---------------------------------------------------------- function renderDetail() { const holder = root.querySelector('[data-bench-detail]') if (!holder) return if (!state.selectedId) { holder.innerHTML = `
Select an experiment above to see its charts, grid, and per-cell traces.
` return } const exp = M.getExperiment(state, state.selectedId) if (!exp) { holder.innerHTML = ''; return } const parts = [] parts.push(renderDetailHeader(exp)) parts.push(renderChartsStrip(exp)) if (exp.kind === 'matrix') parts.push(renderMatrixGrid(exp)) else if (exp.kind === 'ab') parts.push(renderABTable(exp)) else if (exp.kind === 'rep') parts.push(renderRepetitionTable(exp)) parts.push(renderConfigRecap(exp)) if (selectedCell) parts.push(renderCellDrill(exp, selectedCell)) holder.innerHTML = parts.join('\n') // Wire up post-render event handlers. for (const btn of holder.querySelectorAll('[data-bench-cell]')) { btn.addEventListener('click', () => { const kind = btn.dataset.benchCellKind if (kind === 'matrix') { const [pid, mid] = btn.dataset.benchCell.split('|') selectedCell = { kind: 'matrix-cell', promptId: pid, modelId: mid } } else if (kind === 'ab-row') { selectedCell = { kind: 'ab-row', promptId: btn.dataset.benchCell } } else if (kind === 'rep-run') { selectedCell = { kind: 'rep-run', idx: Number(btn.dataset.benchCell) } } render() const drill = root.querySelector('[data-bench-drill]') if (drill && typeof drill.scrollIntoView === 'function') { drill.scrollIntoView({ behavior: 'smooth', block: 'start' }) } }) } const exportBtn = holder.querySelector('[data-bench-export]') if (exportBtn) exportBtn.addEventListener('click', () => exportCodeResults(exp)) const rescoreBtn = holder.querySelector('[data-bench-rescore]') if (rescoreBtn) rescoreBtn.addEventListener('click', () => showRescoreHint()) } function renderDetailHeader(exp) { return `
${exp.kind === 'matrix' ? 'Matrix' : exp.kind === 'ab' ? 'A/B' : 'Rep'} ${escapeHtml(exp.name)}
local sequential run · G18 / G19 pending upstream
` } function renderChartsStrip(exp) { const charts = M.projectChartStrip(exp) if (!charts) return '' if (exp.kind === 'matrix') return renderMatrixCharts(charts) if (exp.kind === 'ab') return renderABCharts(charts) if (exp.kind === 'rep') return renderRepCharts(charts) return '' } function renderMatrixCharts(charts) { return `
Feedback
${barsGroup(charts.feedback.map(r => ({ label: r.label, primary: r.passRate, secondary: r.aveScore })), { primaryLabel: 'pass', secondaryLabel: 'score', max: 1 })}
Latency
${barsGroup(charts.latency.map(r => ({ label: r.label, primary: r.p50, secondary: r.p99 })), { primaryLabel: 'P50', secondaryLabel: 'P99', fmt: fmtLatMs })}
Tokens
${barsGroup(charts.tokens.map(r => ({ label: r.label, primary: r.tokIn, secondary: r.tokOut })), { primaryLabel: 'in', secondaryLabel: 'out', fmt: fmtIntShort })}
` } function renderABCharts(charts) { return `
Feedback (A vs B)
${barsGroup(charts.feedback.map(r => ({ label: r.label, primary: r.passRate, secondary: r.aveScore })), { primaryLabel: 'pass', secondaryLabel: 'score', max: 1 })}
Latency (A vs B)
${barsGroup(charts.latency.map(r => ({ label: r.label, primary: r.p50, secondary: r.p99 })), { primaryLabel: 'P50', secondaryLabel: 'P99', fmt: fmtLatMs })}
Tokens (A vs B)
${barsGroup(charts.tokens.map(r => ({ label: r.label, primary: r.tokIn, secondary: r.tokOut })), { primaryLabel: 'in', secondaryLabel: 'out', fmt: fmtIntShort })}
` } function renderRepCharts(charts) { return `
Score histogram
${histogramSvg(charts.histogram)}
Latency box-plot
${boxplotSvg(charts.boxplot)}
Resolved / unresolved
${stackedSvg(charts.resolvedStack)}
` } // Two-series grouped bars. `rows` is `[{ label, primary, secondary }, …]`. function barsGroup(rows, opts) { if (!rows.length) return '
no data
' const values = rows.flatMap(r => [Number(r.primary) || 0, Number(r.secondary) || 0]) const max = (opts && opts.max) || Math.max(1e-6, ...values) const fmt = (opts && opts.fmt) || fmtRatio2 const primaryLabel = opts && opts.primaryLabel || 'p' const secondaryLabel = opts && opts.secondaryLabel || 's' // Use explicit pixel heights (60px lane) so the flex-height chain doesn't // collapse the bars. Percent-of-parent worked in isolation but the pair's // parent is a column flex that only takes content height when // align-items != stretch. const H = 60 const bars = rows.map(r => { const p = Math.max(0, Number(r.primary) || 0) / max const s = Math.max(0, Number(r.secondary) || 0) / max const pPx = Math.max(2, Math.round(p * H)) const sPx = Math.max(2, Math.round(s * H)) return `
${escapeHtml(r.label)}
` }).join('') return `
${bars}
${primaryLabel} ${secondaryLabel}
` } function histogramSvg(bins) { if (!bins || !bins.length) return '
no data
' const max = Math.max(1, ...bins.map(b => b.count)) const W = 160, H = 60, padL = 4, padR = 4, padT = 4, padB = 12 const bw = (W - padL - padR) / bins.length const parts = bins.map((b, i) => { const h = ((b.count / max) * (H - padT - padB)) const x = padL + i * bw const y = H - padB - h return `` }).join('') return ` ${parts} 0 1 ` } function boxplotSvg(bp) { if (!bp) return '
no data
' const W = 160, H = 60, padL = 8, padR = 8 const min = bp.min, max = bp.max const span = Math.max(1, max - min) const sc = (v) => padL + ((v - min) / span) * (W - padL - padR) const midY = H / 2 return ` ${fmtLatMs(bp.min)} ${fmtLatMs(bp.max)} ` } function stackedSvg(stack) { if (!stack) return '
no data
' const total = Math.max(1, stack.resolved + stack.unresolved) const W = 160, H = 60, padL = 8, padR = 8 const width = W - padL - padR const resW = (stack.resolved / total) * width return ` ✓ ${stack.resolved} ✗ ${stack.unresolved} ` } // ---- Kind A: matrix grid ------------------------------------------------ function renderMatrixGrid(exp) { const grid = M.projectMatrixGrid(exp) if (!grid.prompts.length) { return `
Grid loads once the first prompt returns.
` } const cols = grid.models.length const head = grid.models.map(m => `
${escapeHtml(m.label || m.id)}
`).join('') const bodyRows = grid.rows.map(row => { const cells = row.cells.map((c, i) => renderMatrixCell(c, grid.models[i])).join('') return `
${escapeHtml(row.prompt.label || row.prompt.id)}
${cells}
` }).join('') const totalCells = grid.totals.map(t => `
Pass@${t.kUsed} ${fmtRatio(t.passAtK, 2)}
Avg ${fmtRatio(t.aveScore, 2)}
Cost ${fmtCost(t.totalCost)}
P50 ${fmtLatMs(t.p50)}
`).join('') return `
${head} ${bodyRows}
Totals
${totalCells}
` } function renderMatrixCell(c, model) { if (!c) { return `
` } if (c.status === 'queued') { return `
queued
` } if (c.status === 'running') { return `
running… ${c.resolvedCount}/${c.N}
` } const glyph = c.resolvedCount > 0 ? '✓' : '✗' const scoreTxt = c.resolvedCount > 0 ? fmtRatio(c.score, 2) : '—' const tint = c.tintBucket || 'neutral' return ` ` } // ---- Kind B: A/B table -------------------------------------------------- function renderABTable(exp) { const rows = M.projectABTable(exp) if (!rows.length) return '
no A/B rows loaded
' const varA = exp.ab.variantA.label || 'A' const varB = exp.ab.variantB.label || 'B' const rowHtml = rows.map(r => { const dirGlyph = r.direction === 'up' ? '↑' : r.direction === 'down' ? '↓' : '·' const dirCls = `bench-ab-delta-${r.direction}` return ` ` }).join('') return `
prompt ${escapeHtml(varA)} resolved ${escapeHtml(varA)} score ${escapeHtml(varB)} resolved ${escapeHtml(varB)} score Δ
${rowHtml}
Click any row to open both traces side-by-side.
` } // ---- Kind C: repetition Average|1..N table ----------------------------- function renderRepetitionTable(exp) { const t = M.projectRepetitionTable(exp) if (!t.N) return '
no repetitions loaded
' const cols = t.headers.length const head = `
Dimension Average ${t.headers.map(h => `${escapeHtml(h)}`).join('')}
` const rowsHtml = t.dims.map(d => { const cells = d.cells.map(c => { const passCls = c.pass ? 'bench-rep-cell-pass' : c.fail ? 'bench-rep-cell-fail' : '' return `${escapeHtml(c.text)}` }).join('') return `
${escapeHtml(d.label)} ${escapeHtml(d.average)} ${cells}
` }).join('') const list = t.list.map(r => ` `).join('') return `
${head} ${rowsHtml}
${list}
` } // ---- config recap + cell drill ----------------------------------------- function renderConfigRecap(exp) { const c = exp.config || {} const parts = [] if (c.profile) parts.push(`profile ${escapeHtml(c.profile)}`) if (c.rubric && c.rubric.id) parts.push(`rubric ${escapeHtml(c.rubric.id)}`) if (exp.promptSet && exp.promptSet.id) parts.push(`prompt-set ${escapeHtml(exp.promptSet.id)} ${escapeHtml(exp.promptSet.version || '')}`) if (exp.N) parts.push(`N ${exp.N}`) if (c.temperature != null) parts.push(`temp ${c.temperature}`) if (c.plugin && c.plugin.id) parts.push(`plugin ${escapeHtml(c.plugin.id)} (vary ${escapeHtml(c.plugin.vary || '?')})`) return `
Configuration recap
${parts.join(' · ')}
` } function renderCellDrill(exp, cell) { if (!cell) return '' let title = '', body = '', codeResult = null, sessionId = null if (cell.kind === 'matrix-cell' && exp.matrix) { const key = `${cell.promptId}|${cell.modelId}` const c = exp.matrix.cells[key] if (!c) return '' title = `${cell.promptId} × ${cell.modelId}` const runs = c.runs.map((r, i) => renderRunLine(r, i + 1)).join('') body = `
${runs}
` const rep = c.resolvedCount > 0 ? c.runs.find(r => r.resolved) : c.runs[0] codeResult = rep ? M.makeCodeResult(rep) : null sessionId = rep ? rep.sessionId : null } else if (cell.kind === 'ab-row' && exp.ab) { const row = exp.ab.rows.find(r => r.promptId === cell.promptId) if (!row) return '' title = `A/B · ${cell.promptId}` body = `
${escapeHtml(exp.ab.variantA.label || 'A')}
${renderRunLine(row.a, 'A')}
${escapeHtml(exp.ab.variantB.label || 'B')}
${renderRunLine(row.b, 'B')}
` codeResult = M.makeCodeResult(row.a) } else if (cell.kind === 'rep-run' && exp.rep) { const r = exp.rep.repetitions.find(rep => rep.idx === cell.idx) if (!r) return '' title = `Repetition #${cell.idx} of ${exp.rep.repetitions.length}` body = renderRunLine(r, cell.idx) codeResult = M.makeCodeResult(r) sessionId = r.sessionId } const crJson = codeResult ? JSON.stringify(codeResult, null, 2) : '' return `
Trace · ${escapeHtml(title)}
${sessionId ? `session ${escapeHtml(sessionId)}` : ''} — trace panel opens in Chat when wire (G6 bench/replay) lands
${body} ${crJson ? `
code_result.json
${escapeHtml(crJson)}
` : ''}
` } function renderRunLine(r, tag) { if (!r) return '' const status = r.resolved ? 'ok' : 'fail' return `
#${escapeHtml(String(tag))} ${r.resolved ? '✓' : '✗'} ${fmtRatio(r.score, 2)} ${fmtLatMs(r.latencyMs)} ${r.tokens ? `${r.tokens.in || 0} in / ${r.tokens.out || 0} out` : ''} ${Number.isFinite(r.cost) ? `${fmtCost(r.cost)}` : ''} ${r.reason ? `${escapeHtml(r.reason)}` : ''}
` } // ---- new-experiment kind picker + toolbar actions ---------------------- function openKindPicker() { const modal = document.createElement('div') modal.className = 'bench-modal-scrim' modal.setAttribute('role', 'dialog') modal.setAttribute('aria-modal', 'true') modal.innerHTML = `
New experiment
` document.body.appendChild(modal) const close = () => modal.remove() modal.querySelector('[data-bench-close]').addEventListener('click', close) modal.addEventListener('click', (e) => { if (e.target === modal) close() }) for (const btn of modal.querySelectorAll('[data-bench-kind]')) { btn.addEventListener('click', () => { const k = btn.dataset.benchKind close() // Pre-select an existing experiment of the picked kind if any, so the // user sees the shape immediately. const existing = M.projectL0Rows(state, { subTab: k }).map(r => r.id) if (existing.length) { M.setSubTab(state, k) M.selectExperiment(state, existing[0]) } else { M.setSubTab(state, k) } render() }) } } function exportCodeResults(exp) { const payload = collectCodeResults(exp) const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `${exp.id}.code_result.json` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } function collectCodeResults(exp) { const out = { experimentId: exp.id, name: exp.name, kind: exp.kind, results: [] } if (exp.kind === 'matrix' && exp.matrix) { for (const key of Object.keys(exp.matrix.cells)) { const c = exp.matrix.cells[key] for (const r of c.runs) { out.results.push({ promptId: c.promptId, modelId: c.modelId, runIdx: r.runIdx, codeResult: M.makeCodeResult(r), }) } } } else if (exp.kind === 'ab' && exp.ab) { for (const row of exp.ab.rows) { out.results.push({ promptId: row.promptId, variant: 'a', codeResult: M.makeCodeResult(row.a) }) out.results.push({ promptId: row.promptId, variant: 'b', codeResult: M.makeCodeResult(row.b) }) } } else if (exp.kind === 'rep' && exp.rep) { for (const r of exp.rep.repetitions) { out.results.push({ idx: r.idx, codeResult: M.makeCodeResult(r) }) } } return out } function showRescoreHint() { const holder = root.querySelector('[data-bench-detail]') if (!holder) return const banner = document.createElement('div') banner.className = 'bench-inline-banner' banner.textContent = 'Re-score without re-run: the demo tier applies the rubric to cached trajectories locally and produces a new experiment id whose runs point at the parent (parentExperimentId). The wire-level `bench/reevaluate` (G18) is pending upstream — this is one of two Bench differentiators over a for-loop.' holder.insertBefore(banner, holder.firstChild) setTimeout(() => banner.remove(), 6000) } // ---- toolbar wiring ----------------------------------------------------- function wireToolbar() { if (!root) return for (const chip of root.querySelectorAll('[data-bench-subtab]')) { chip.addEventListener('click', () => { M.setSubTab(state, chip.dataset.benchSubtab) selectedCell = null render() }) } const newBtn = root.querySelector('[data-bench-new]') if (newBtn) newBtn.addEventListener('click', () => openKindPicker()) const exploreBtn = root.querySelector('[data-bench-explore]') if (exploreBtn) exploreBtn.addEventListener('click', () => { if (!state.experiments.size && FIXTURE) M.loadExperiments(state, FIXTURE) const first = state.order[0] if (first) M.selectExperiment(state, first) render() }) // Delegated handler for the empty-state buttons. root.addEventListener('click', (e) => { const btn = e.target.closest && e.target.closest('[data-bench-action]') if (!btn) return if (btn.dataset.benchAction === 'new') openKindPicker() if (btn.dataset.benchAction === 'load-sample') { if (!state.experiments.size && FIXTURE) M.loadExperiments(state, FIXTURE) M.setSubTab(state, 'all') M.selectExperiment(state, state.order[0]) render() } }) } // ---- formatting helpers ------------------------------------------------ function fmtRatio(v, n) { return Number.isFinite(v) ? Number(v).toFixed(n || 2) : '—' } function fmtRatio2(v) { return fmtRatio(v, 2) } function fmtDelta(v, n, prefix) { if (!Number.isFinite(v)) return '—' const abs = Math.abs(v).toFixed(n || 2) const sign = v > 0 ? '+' : v < 0 ? '-' : '±' return `${prefix ? prefix + ' ' : ''}${sign}${abs}` } function fmtLatMs(v) { if (!Number.isFinite(v)) return '—' if (v < 1000) return `${Math.round(v)}ms` return `${(v / 1000).toFixed(1)}s` } function fmtIntShort(v) { if (!Number.isFinite(v)) return '—' if (v < 1000) return String(Math.round(v)) if (v < 10000) return `${(v / 1000).toFixed(1)}k` return `${Math.round(v / 1000)}k` } function fmtCost(v) { if (!Number.isFinite(v)) return '—' if (v === 0) return '$0.00' if (v < 0.01) return `$${v.toFixed(4)}` return `$${v.toFixed(2)}` } function relTime(ms) { if (!ms) return '—' const dt = Date.now() - ms if (dt < 60 * 1000) return 'just now' if (dt < 60 * 60 * 1000) return `${Math.round(dt / 60000)}m ago` if (dt < 24 * 60 * 60 * 1000) return `${Math.round(dt / 3600000)}h ago` if (dt < 30 * 24 * 60 * 60 * 1000) return `${Math.round(dt / 86400000)}d ago` return new Date(ms).toISOString().slice(0, 10) } // Bench-page previously had an escapeHtml that missed the ' character // AND aliased escapeAttr to it — so any single-quote in a fixture label // used inside a single-quoted attribute context (rows use single-quoted // attrs at bench-page.js:313/435/455) was echoed literally. Fixture // labels are trusted today, but the shared helper below covers all five // OWASP characters, closing the gap in place. See html-escape.js. const __esc = (window.__dshHtmlEscape || {}) const escapeHtml = __esc.escapeHtml || ((s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))) const escapeAttr = __esc.escapeAttr || escapeHtml // ---- public surface ---------------------------------------------------- const api = { show, render, // Test hook: expose the model + state for driving unit tests through the // rendered DOM if we ever want to. Today the pure model has its own // suite; leave this hook for the CDP demo-shots step. __state: state, __openKindPicker: openKindPicker, } if (typeof window !== 'undefined') { window.__dshBench = api // Wire the toolbar once the DOM is ready. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { root = document.querySelector('[data-pane="bench"]') wireToolbar() }) } else { root = document.querySelector('[data-pane="bench"]') wireToolbar() } } if (typeof module !== 'undefined' && module.exports) module.exports = api })()