openvibers/openvibe.bench Commits

Benchmark page on phones: section chips, compact tasks, one-row runs; no badges on a private benchmark (#42)

OP OpenVibers committed 14 days ago · 5eb8e3b4bb · 6 files, +147 −8

Changes

ddiff --git a/public/css/bench.css b/public/css/bench.css
iindex 5c40221..ccc529e 100644
---- a/public/css/bench.css
++++ b/public/css/bench.css
@@@ -493,3 +493,74 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
.br-prompt-table .rt-col-band { width: 5em; }
.br-prompt-table .rt-col-raw { width: 7em; }
.br-prompt-table .rt-col-votes { width: 3.2em; }
++
++/* ---- B11 v2: phone chips for the benchmark page, a compact task list and one-row runs ---- */
++.br-task-n { display: none; }
++.br-sec-pick { display: none; }
++.br-usethis-details { border: 1px solid var(--border); border-radius: 16px; background: var(--bg-card); }
++.br-usethis-details > summary { display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 8px 16px; cursor: pointer; list-style: none; }
++.br-usethis-details > summary::-webkit-details-marker { display: none; }
++.br-usethis-details > summary h2 { margin: 0; }
++.br-usethis-details > summary::after { content: "+"; margin-left: auto; font-size: 1.2rem; font-weight: 800; color: var(--room-strong); }
++.br-usethis-details[open] > summary::after { content: "\2212"; }
++.br-usethis-details > .br-fine { padding: 0 16px; }
++.br-usethis-details .br-usethis-box { margin: 12px 16px; }
++.br-usethis-details .br-follow { margin: 0 16px 14px; }
++
++@supports selector(:has(*)) {
++ @media (max-width: 640px) {
++ .br-sec-pick { display: flex; gap: 8px; overflow-x: auto; margin: 14px -16px 0; padding: 2px 16px 4px; scrollbar-width: none; overscroll-behavior-x: contain; }
++ .br-sec-pick::-webkit-scrollbar { display: none; }
++ .br-sec-pick input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
++ .br-sec-pick label { display: inline-flex; align-items: center; flex: none; min-height: 44px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--border); color: var(--text-secondary); font-size: .86rem; font-weight: 700; white-space: nowrap; cursor: pointer; }
++ .br-sec-pick input:checked + label { color: var(--text-primary); border-color: var(--room-accent); background: color-mix(in srgb, var(--room-accent) 14%, transparent); }
++ .br-sec-pick input:focus-visible + label { outline: 2px solid var(--room-accent); outline-offset: 2px; }
++ .br-sections > section { display: none; margin-top: 16px; }
++ .br-sections:has(#br-view-rank:checked) #br-rank,
++ .br-sections:has(#br-view-tasks:checked) #br-tasks,
++ .br-sections:has(#br-view-runs:checked) #br-runs { display: block; }
++
++ /* Tasks: one compact line each, number and text, no card, two lines at most. */
++ #br-tasks .br-tasks { gap: 0; margin-top: 4px; }
++ #br-tasks .br-task { display: grid; grid-template-columns: 1.35em minmax(0, 1fr); gap: 8px; align-items: baseline; padding: 7px 0; border: 0; border-bottom: 1px solid var(--border); border-radius: 0; background: transparent; }
++ #br-tasks .br-task:last-child { border-bottom: 0; }
++ #br-tasks .br-task-id { display: none; }
++ #br-tasks .br-task-n { display: inline; font-size: .78rem; font-weight: 800; color: var(--text-muted); text-align: right; }
++ #br-tasks .br-task p { font-size: .9rem; line-height: 1.35; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
++ #br-tasks .br-task-expected { display: none; }
++ #br-tasks .br-task.is-hidden { grid-template-columns: minmax(0, 1fr); }
++ #br-tasks .br-task.is-hidden .br-task-id { display: inline-flex; }
++
++ /* Runs: one row each, with the model, harness, date, score bar and Verified on it. */
++ #br-runs .br-runs { gap: 0; margin-top: 4px; }
++ #br-runs .br-run { border: 0; border-bottom: 1px solid var(--border); border-radius: 0; background: transparent; padding: 0; gap: 6px; }
++ #br-runs .br-run:last-child { border-bottom: 0; }
++ #br-runs .br-run-link { min-height: 40px; padding: 5px 0; gap: 8px; }
++ #br-runs .br-run-main { display: flex; align-items: baseline; gap: 6px; flex: 1; min-width: 0; }
++ #br-runs .br-run-model { font-size: .9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
++ #br-runs .br-run-sub { display: inline; font-size: .74rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
++ #br-runs .br-score { min-width: 0; flex: none; flex-wrap: nowrap; }
++ #br-runs .br-score-n { font-size: .9rem; }
++ #br-runs .br-score-of { font-size: .66rem; }
++ #br-runs .br-score .br-bar { flex: 0 0 24px; height: 3px; margin: 0 0 0 3px; align-self: center; }
++ #br-runs .br-prov { flex: none; padding: 1px 6px; font-size: .68rem; }
++ }
++}
++.br-fold-toggle { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
++.br-fold-h { display: flex; align-items: center; gap: 10px; cursor: pointer; }
++.br-fold-mark { margin-left: auto; }
++.br-fold-mark::after { content: "+"; font-size: 1.15rem; font-weight: 800; color: var(--room-strong); }
++.br-fold-toggle:checked ~ .br-fold-h .br-fold-mark::after { content: "\2212"; }
++@media (max-width: 640px) {
++ .br-fold-body { display: none; }
++ .br-fold-toggle:checked ~ .br-fold-body { display: block; }
++}
++@media (min-width: 641px) {
++ .br-fold-toggle { display: none; }
++ .br-fold-h { cursor: default; pointer-events: none; }
++ .br-fold-mark { display: none; }
++ .br-fold-body { display: block; }
++}
++@media (max-width: 640px) {
++ .br-sections #br-rank .br-chart { display: none; }
++}
ddiff --git a/server/http/badge.js b/server/http/badge.js
iindex 720eae1..43d231b 100644
---- a/server/http/badge.js
++++ b/server/http/badge.js
@@@ -69,6 +69,8 @@ async function badgeFor(ctx, benchmarkId, model) {
const registry = ctx.registry;
const benchmark = await registry.getBenchmark(String(benchmarkId || ''));
if (!benchmark) return noData('unknown benchmark');
++ // A private benchmark has no publishable badge for anyone, its owner included: the badge is a public URL.
++ if (benchmark.visibility === 'private') return noData('private benchmark');
const rankings = await registry.rankings({ benchmark: benchmark.id, metric: 'score' });
const entries = (rankings && rankings.entries) || [];
const row = entries.find((e) => String(e.model) === String(model || ''));
ddiff --git a/server/views/results.js b/server/views/results.js
iindex bf314eb..fbdfca8 100644
---- a/server/views/results.js
++++ b/server/views/results.js
@@@ -184,11 +184,14 @@ ${ownEntries.length ? valueChart(ownEntries) : ''}
${promptLeaderboard(ownEntries, { emptyText: 'No runs yet. Start it on 3 to 6 models; every run lands here, whatever it scores.' })}
</section>
<section class="br-duo-pane" aria-labelledby="br-public-h">
--<div class="bm-head-row"><h2 id="br-public-h">The public board for this kind</h2>${publicBoard && publicBoard.benchmark ? html`<a class="bm-more" href="${benchmarkHref(publicBoard.benchmark.id)}">${publicBoard.benchmark.title}</a>` : ''}</div>
++<div class="br-fold br-public-details"><input class="br-fold-toggle" type="checkbox" id="br-public-open"><label class="br-fold-h" for="br-public-open"><h2 id="br-public-h">The public board for this kind</h2><span class="br-fold-mark" aria-hidden="true"></span></label>
++<div class="br-fold-body">
++${publicBoard && publicBoard.benchmark ? html`<p class="br-fine"><a href="${benchmarkHref(publicBoard.benchmark.id)}">${publicBoard.benchmark.title}</a></p>` : ''}
<p class="br-fine">Other people's runs of the public set, ranked the same way. Your prompts are not in it.</p>
${publicEntries.length
? promptLeaderboard(publicEntries, { emptyText: '' })
: empty('No public board yet', 'No public benchmark of this kind has verified runs yet, so there is nothing to stand beside. Your own board still counts your runs.')}
++</div></div>
</section>
</div>
</section>`;
@@@ -258,7 +261,8 @@ function shareBox({ benchmark, shareUrl = null, error = null }) {
? 'Unlisted: anyone with the link can see it. It is not in any list, search, sitemap or the open data.'
: 'Public: it is in the benchmark list, the sitemap and the open data, with every run published.';
return html`<section class="bm-section br-share" id="br-share" aria-labelledby="br-share-h">
--<div class="bm-head-row"><h2 id="br-share-h">Sharing</h2><span class="br-chip br-vis">${visibility}</span></div>
++<div class="br-fold br-share-details"><input class="br-fold-toggle" type="checkbox" id="br-share-open"><label class="br-fold-h" for="br-share-open"><h2 id="br-share-h">Sharing</h2><span class="br-chip br-vis">${visibility}</span><span class="br-fold-mark" aria-hidden="true"></span></label>
++<div class="br-fold-body">
<p class="br-fine">${note}</p>
${error ? html`<div class="bm-note is-bad" role="alert">${error}</div>` : ''}
${shareUrl ? html`<div class="bm-note br-share-now" role="status"><p>Your unlisted link is ready. It is shown once; copy it now. Share it again for a new link; the old one stops working.</p><label class="br-field"><span>The link</span><input class="br-share-link" type="text" readonly value="${shareUrl}" onfocus="this.select()"></label></div>` : ''}
@@@ -268,6 +272,7 @@ ${shareUrl ? html`<div class="bm-note br-share-now" role="status"><p>Your unlist
<button class="bm-btn bm-btn-quiet" type="submit" name="visibility" value="private">Make private</button>
</form>
<p class="br-fine">A person with the unlisted link can read the prompts and the runs. Only you can start a run on it.</p>
++</div></div>
</section>`;
}
@@@ -332,17 +337,24 @@ function benchmarkPage({ benchmark, definition, version, rankings, metric = 'sco
${privateNote}
<dl class="br-facts"><div><dt>Tasks</dt><dd>${tasks.length}${hidden ? html` <span class="br-fine">(${hidden} hidden)</span>` : ''}</dd></div><div><dt>Judged by</dt><dd>${judges.length ? judges.map((j) => String(j.type).replace(/_/g, ' ')).join(', ') : '—'}</dd></div><div><dt>Limits</dt><dd>${[limits.ms ? fmtMs(limits.ms) : null, limits.tokens ? `${fmtTokens(limits.tokens)} tokens` : null, limits.usd ? fmtUsd(limits.usd) : null].filter(Boolean).join(' · ') || '—'}</dd></div><div><dt>Verified runs</dt><dd>${entries.reduce((n, e) => n + e.runs, 0)}</dd></div></dl>
${owner ? shareBox({ benchmark, shareUrl: share && share.shareUrl, error: share && share.error }) : ''}
++<div class="br-sections">
++<div class="br-sec-pick" role="radiogroup" aria-label="Benchmark sections">
++<input type="radio" name="br-view" id="br-view-rank" value="rank" checked><label for="br-view-rank">Rankings</label>
++<input type="radio" name="br-view" id="br-view-tasks" value="tasks"><label for="br-view-tasks">Tasks</label>
++<input type="radio" name="br-view" id="br-view-runs" value="runs"><label for="br-view-runs">Runs</label>
++</div>
${ownBoards
? promptBoards(ownBoards)
: html`<section class="bm-section" id="br-rank" aria-labelledby="br-rank-h"><div class="bm-head-row"><h2 id="br-rank-h">Rankings</h2>${metricTabs(base, metric)}</div>
${valueChart(entries)}
${entries.length ? rankingsTable(entries, metric) : empty('No verified runs yet', 'A run counts here once openvibe.codes, Run or a Node worker ran it and published it. Submitted runs are listed below but never ranked.')}
</section>`}
--<section class="bm-section" aria-labelledby="br-tasks-h"><div class="bm-head-row"><h2 id="br-tasks-h">Tasks</h2><span class="bm-count">${tasks.length}</span></div>
--<ol class="br-tasks">${shown.map((t) => html`<li class="br-task"><span class="br-task-id br-mono">${t.id}</span><p>${t.prompt}</p>${t.expected && t.expected.answer ? html`<p class="br-fine">Expected: ${String(t.expected.answer).slice(0, 300)}</p>` : ''}</li>`)}${hidden ? html`<li class="br-task is-hidden"><span class="br-task-id br-mono">${plural(hidden, 'task')}</span><p>Held out: only the executor sees ${hidden === 1 ? 'it' : 'them'}, so nobody can tune for the answer.</p></li>` : ''}</ol></section>
--<section class="bm-section" aria-labelledby="br-latest-h"><div class="bm-head-row"><h2 id="br-latest-h">Latest runs</h2>${runs.length > 1 ? html`<a class="bm-more" href="${compareHref(runs.slice(0, 2).map((r) => r.id))}">Compare the newest two</a>` : ''}</div>
++<section class="bm-section" id="br-tasks" aria-labelledby="br-tasks-h"><div class="bm-head-row"><h2 id="br-tasks-h">Tasks</h2><span class="bm-count">${tasks.length}</span></div>
++<ol class="br-tasks">${shown.map((t, i) => html`<li class="br-task"><span class="br-task-n">${i + 1}</span><span class="br-task-id br-mono">${t.id}</span><p>${t.prompt}</p>${t.expected && t.expected.answer ? html`<p class="br-fine br-task-expected">Expected: ${String(t.expected.answer).slice(0, 300)}</p>` : ''}</li>`)}${hidden ? html`<li class="br-task is-hidden"><span class="br-task-id br-mono">${plural(hidden, 'task')}</span><p>Held out: only the executor sees ${hidden === 1 ? 'it' : 'them'}, so nobody can tune for the answer.</p></li>` : ''}</ol></section>
++<section class="bm-section" id="br-runs" aria-labelledby="br-latest-h"><div class="bm-head-row"><h2 id="br-latest-h">Latest runs</h2>${runs.length > 1 ? html`<a class="bm-more" href="${compareHref(runs.slice(0, 2).map((r) => r.id))}">Compare the newest two</a>` : ''}</div>
${runs.length ? runsList(runs) : empty('No runs yet', 'The first run shows up here, verified or submitted.')}</section>
--${benchmarkUseThis({ baseUrl, benchmark, entries, viewer })}
++</div>
++${benchmark.visibility === 'private' ? '' : benchmarkUseThis({ baseUrl, benchmark, entries, viewer })}
<p class="br-fine br-api">The same data as JSON: <a href="/api/v1/benchmarks/${encodeURIComponent(benchmark.id)}">definition</a> · <a href="/api/v1/rankings?benchmark=${encodeURIComponent(benchmark.id)}&amp;metric=${metric}">rankings</a></p>
</div>`;
}
ddiff --git a/server/views/usethis.js b/server/views/usethis.js
iindex db1ee96..b21fc5d 100644
---- a/server/views/usethis.js
++++ b/server/views/usethis.js
@@@ -53,11 +53,12 @@ function benchmarkUseThis({ baseUrl, benchmark, entries = [], viewer = null, bac
const s = badgeSnippets(baseUrl, benchmark.id, e.model, benchmark.title);
return copyBox({ heading: e.model, pairs: [{ label: 'Markdown', value: s.markdown }, { label: 'HTML', value: s.html }] });
});
-- return html`<section class="bm-section" aria-labelledby="br-use-h"><h2 id="br-use-h">Use this</h2>
++ // Closed by default: the badge snippets are useful, but they are not the page's reason to exist.
++ return html`<section class="bm-section br-usethis" aria-labelledby="br-use-h"><details class="br-usethis-details"><summary><h2 id="br-use-h">Badges and embeds</h2></summary>
<p class="br-fine">A badge for your README, and a watch that tells you when this benchmark moves.</p>
${boxes}
${followForm({ benchmarkId: benchmark.id, back: back || ('/benchmarks/' + benchmark.id), signedIn: Boolean(viewer && viewer.kind === 'user') })}
--</section>`;
++</details></section>`;
}
/** The model page: a badge for each benchmark the model ran, and a watch for each. */
ddiff --git a/test/badge.test.js b/test/badge.test.js
iindex 26f3a45..03c3a0c 100644
---- a/test/badge.test.js
++++ b/test/badge.test.js
@@@ -24,6 +24,23 @@ const { badgeSvg } = require('../server/http/badge');
const t = await boot();
const list = (await t.get('/api/v1/benchmarks')).json();
const benchmark = (list.benchmarks || list)[0];
++ const alice = t.network.addUser('alice');
++ const publisher = t.network.serviceToken({ sub: 'svc:codes', audience: 'openvibe.bench', cap: ['bench.run.publish', 'bench.benchmark.write'] });
++ const madePrivate = await t.get('/api/v1/benchmarks', { as: alice, json: {
++ title: 'Private badge arena', description: 'Not for publication.', kind: 'design.page', tags: [], visibility: 'private',
++ tasks: [{ id: 't1', prompt: 'A private task.', files: [], expected: null, holdout: false }],
++ judges: [{ type: 'human_pairwise', version: '1.0', config: {} }], scoring: { method: 'mean', scale: { min: 0, max: 100 } },
++ limits: { ms: 60000, tokens: 0, usd: 0 }, matrix: { models: [], harnesses: [] },
++ } });
++ assert.strictEqual(madePrivate.status, 201, madePrivate.text);
++ const privateId = madePrivate.json().benchmark.id;
++ assert.strictEqual((await t.get(`/api/v1/benchmarks/${privateId}/versions`, { as: alice, json: {} })).status, 201);
++ assert.strictEqual((await t.get('/api/v1/runs', { bearer: publisher, json: {
++ benchmark: `${privateId}@1`, model: 'model-private', route: { provider: 'acme' }, provider: 'acme',
++ harness: { name: 'claude-code', version: '1.0' }, settings: {}, executor: { kind: 'codes', id: 'tsk_badge' },
++ tasks: [{ task_id: 't1', score: 90, cost_usd: 0.01, ms: 1000, tokens: 100, artifacts: [] }],
++ score: 90, cost_usd: 0.01, ms: 1000, tokens: 100,
++ } })).status, 201);
await check('an unknown benchmark gets a grey badge and a 404', async () => {
const r = await t.get('/badge/bmk_nope/nope.svg');
@@@ -36,6 +53,12 @@ const { badgeSvg } = require('../server/http/badge');
assert.ok(r.text.includes('<svg'), 'and it is still a picture');
});
++ await check('a private benchmark has no badge, even when it has runs', async () => {
++ const r = await t.get(`/badge/${privateId}/model-private.svg`);
++ assert.strictEqual(r.status, 404, r.text.slice(0, 200));
++ assert.ok(r.text.includes('private benchmark'), 'the badge says why');
++ });
++
await check('a model with no runs gets a grey badge and a 404', async () => {
const r = await t.get('/badge/' + benchmark.id + '/no-such-model.svg');
assert.strictEqual(r.status, 404);
ddiff --git a/test/results-pages.test.js b/test/results-pages.test.js
iindex a108b7b..56150e9 100644
---- a/test/results-pages.test.js
++++ b/test/results-pages.test.js
@@@ -80,6 +80,8 @@ function runDoc(benchmark, over = {}) {
await publish(`${pub.id}@${pub.version}`, { model: 'model-worse', score: 55, cost_usd: 0.04, harness: { name: 'opencode', version: '1.4.2' } });
const priv = await create({ title: 'Alice unlisted prompts', visibility: 'unlisted' }, alice);
await publish(`${priv.id}@${priv.version}`, { model: 'model-best', score: 12, cost_usd: 0.01 });
++ const privatePage = await create({ title: 'Alice private prompts', visibility: 'private', tags: ['your-prompts'] }, alice);
++ await publish(`${privatePage.id}@${privatePage.version}`, { model: 'model-best', score: 77, cost_usd: 0.01 });
await check('the benchmarks page lists the public benchmarks and escapes their titles', async () => {
const res = await t.get('/benchmarks');
@@@ -101,6 +103,34 @@ function runDoc(benchmark, over = {}) {
assert.ok(value.text.includes('aria-current="page"') && value.text.includes('/ $'), 'the value tab is current and shows score per dollar');
});
++ await check('the phone sections are chips, one per section, with Rankings picked first', async () => {
++ const res = await t.get(`/benchmarks/${pub.id}`);
++ const chips = [...res.text.matchAll(/<input type="radio" name="br-view" id="br-view-([a-z]+)" value="[a-z]+"( checked)?>/g)];
++ assert.deepStrictEqual(chips.map((m) => m[1]), ['rank', 'tasks', 'runs'], 'one chip per section, in order');
++ assert.deepStrictEqual(chips.filter((m) => m[2]).map((m) => m[1]), ['rank'], 'Rankings is picked first');
++ assert.match(res.text, /<div class="br-sections">[\s\S]*id="br-rank"[\s\S]*id="br-tasks"[\s\S]*id="br-runs"[\s\S]*<\/div>/, 'every section stays in the page');
++ const css = await t.get('/css/bench.css');
++ assert.ok(css.text.includes('@supports selector(:has(*))'), 'the chips are gated on :has()');
++ assert.ok(css.text.includes('.br-sections:has(#br-view-rank:checked) #br-rank'), 'Rankings shows when picked');
++ assert.ok(css.text.includes('.br-sections:has(#br-view-tasks:checked) #br-tasks'), 'Tasks shows when picked');
++ assert.ok(css.text.includes('.br-sections:has(#br-view-runs:checked) #br-runs'), 'Runs shows when picked');
++ });
++
++ await check('badges and embeds are one closed details on a public page, and absent on a private one', async () => {
++ const pubPage = await t.get(`/benchmarks/${pub.id}`);
++ assert.ok(pubPage.text.includes('<details class="br-usethis-details">'), 'one details');
++ assert.ok(!pubPage.text.includes('<details class="br-usethis-details" open'), 'closed by default');
++ assert.ok(pubPage.text.includes('Badges and embeds'), 'the summary');
++ assert.ok(pubPage.text.includes('model-best'), 'the models are inside');
++ const mine = await t.get(`/benchmarks/${privatePage.id}`, { as: alice });
++ assert.strictEqual(mine.status, 200, mine.text.slice(0, 200));
++ assert.ok(!mine.text.includes('Badges and embeds'), 'no badge section on a private benchmark');
++ assert.ok(!mine.text.includes('br-usethis'), 'no use-this markup at all');
++ const badge = await t.get(`/badge/${privatePage.id}/model-best.svg`);
++ assert.strictEqual(badge.status, 404, badge.text.slice(0, 200));
++ assert.ok(badge.text.includes('private benchmark'), 'the badge says why');
++ });
++
await check('a run page shows the environment, the score, the provenance and the outputs', async () => {
const res = await t.get(`/runs/${best}`);
assert.strictEqual(res.status, 200, res.text.slice(0, 300));