openvibers/openvibe.bench Commits

Pick for me: say the job first, the controls in one row, an honest why, rows that fold on a phone (#39)

OP OpenVibers committed 14 days ago · a56a697f66 · 3 files, +44 −16

Design review of #37.
- The words box comes first ("Say the job"); kind, price, Fast and the
  button share one row on a desktop and stack on a phone; one hint line.
- What the words were read as shows as chips of names ("Design page arena",
  "2¢ a solved task", "fast"), never the kind's id.
- The why compares only with a model whose runs can carry a number; with
  none, it says no other model has enough runs to compare yet.
- On a phone each frontier row folds into two lines: the model and its
  score, then its numbers with their names.
- "1 model … has".

Changes

ddiff --git a/public/css/bench.css b/public/css/bench.css
iindex e9a4eb0..e170a1d 100644
---- a/public/css/bench.css
++++ b/public/css/bench.css
@@@ -366,10 +366,30 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
.pk-form label { display: grid; gap: 6px; font-size: .8rem; font-weight: 800; color: var(--text-secondary); }
.pk-form select, .pk-form input[type="text"] { min-height: 44px; padding: 0 12px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg-primary); color: var(--text-primary); font: inherit; font-size: .95rem; font-weight: 500; width: 100%; }
.pk-form select:focus-visible, .pk-form input:focus-visible { outline: 2px solid var(--room-accent); outline-offset: 2px; }
--.pk-check { display: flex; flex-direction: row; align-items: center; gap: 10px; min-height: 44px; }
--.pk-check input { width: 20px; height: 20px; accent-color: var(--room-accent); }
--.pk-words input { font-weight: 500; }
--.pk-notes { margin: 4px 0 0; }
++.pk-controls { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) auto auto; align-items: end; gap: 12px; }
++.pk-controls .bm-btn { min-height: 44px; padding: 0 26px; }
++.pk-form label.pk-check { display: flex; flex-direction: row; align-items: center; gap: 9px; min-height: 44px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg-primary); color: var(--text-primary); font-size: .95rem; font-weight: 600; cursor: pointer; }
++.pk-check input { width: 20px; height: 20px; margin: 0; accent-color: var(--room-accent); }
++.pk-words input { min-height: 50px; font-size: 1.05rem; font-weight: 500; }
++.pk-hint { margin: 0; font-size: .82rem; color: var(--text-muted); }
++.pk-read { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 4px 0 0; }
++.pk-chip { display: inline-flex; align-items: center; min-height: 28px; padding: 0 11px; border-radius: 999px; font-size: .85rem; font-weight: 700; color: var(--room-strong); background: color-mix(in srgb, var(--room-accent) 12%, transparent); border: 1px solid color-mix(in srgb, var(--room-accent) 28%, transparent); }
++@media (max-width: 720px) { .pk-controls { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } .pk-controls .bm-btn { grid-column: 1 / -1; } }
++@media (max-width: 420px) { .pk-controls { grid-template-columns: minmax(0, 1fr); } }
++/* The frontier on a phone: each row is two lines, the model and its score, then its numbers with their names. */
++@media (max-width: 560px) {
++ .pk-table, .pk-table tbody { display: block; width: 100%; }
++ .pk-table thead { display: none; }
++ .pk-table tr { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px; padding: 11px 14px; border-bottom: 1px solid var(--border); }
++ .pk-table tbody tr:last-child { border-bottom: 0; }
++ .pk-table td { display: block; padding: 0; border: 0; min-width: 0; width: auto; white-space: normal; }
++ .pk-table td.pk-c-model { flex: 1 1 auto; order: 1; }
++ .pk-table td.pk-c-score { flex: none; order: 1; text-align: right; }
++ .pk-table tr::after { content: ""; flex-basis: 100%; height: 0; order: 2; }
++ .pk-table td.br-num { flex: none; order: 3; text-align: left; font-size: .88rem; color: var(--text-secondary); }
++ .pk-table td.br-num::after { content: " " attr(data-label); color: var(--text-muted); }
++ .pk-table td .pk-thin-note { display: none; }
++}
.pk-why { margin: 10px 0 14px; padding: 12px 14px; border-radius: 14px; background: color-mix(in srgb, var(--room-accent) 12%, transparent); border: 1px solid color-mix(in srgb, var(--room-accent) 30%, transparent); font-weight: 600; line-height: 1.55; }
.pk-table td, .pk-table th { white-space: nowrap; }
.pk-score { font-weight: 800; }
ddiff --git a/server/pick/pick.js b/server/pick/pick.js
iindex 4570184..12e26a7 100644
---- a/server/pick/pick.js
++++ b/server/pick/pick.js
@@@ -91,7 +91,9 @@ function whySentence(rows, kindName) {
const head = top.model + ' is first on ' + kindName + ' because no other model beats it on both score and cost per solved task: '
+ 'it scores ' + top.score + ' at ' + cents(top.cost_per_solved) + ' a solved task';
if (!rivals.length) return head + ', and it is the only model with enough data to stand on.';
-- const rival = rivals[0];
++ // A rival is compared only when its runs can carry a number: a greyed one is too thin to say "lower" about.
++ const rival = rivals.find((r) => !r.thin);
++ if (!rival) return head + '. No other model has enough runs to compare with it yet.';
const lower = rival.score < top.score
? 'scores lower (' + rival.score + ')'
: 'costs more (' + cents(rival.cost_per_solved) + ')';
ddiff --git a/server/views/pick.js b/server/views/pick.js
iindex 9657fd3..74c345a 100644
---- a/server/views/pick.js
++++ b/server/views/pick.js
@@@ -17,12 +17,16 @@ function pickForm(controls) {
const kinds = KINDS.map((k) => html`<option value="${k.id}"${k.id === controls.kind ? ' selected' : ''}>${k.name}</option>`);
const prices = [html`<option value=""${controls.max_cost == null ? ' selected' : ''}>Any price</option>`].concat(
PRICE_STEPS.map((p) => html`<option value="${p.value}"${controls.max_cost != null && Math.abs(controls.max_cost - p.value) < 1e-9 ? ' selected' : ''}>${p.label}</option>`));
++ // The words first (the way people say a job), then the three controls they set, and the button, in one row.
return html`<form class="pk-form" method="get" action="/pick" data-pk-form="1">
++<label class="pk-words">Say the job<input type="text" name="q" value="${controls.q || ''}" placeholder="a landing page, under 2 cents, fast" autocomplete="off"></label>
++<div class="pk-controls">
<label>Kind<select name="kind">${kinds}</select></label>
<label>Top price per solved task<select name="max_cost">${prices}</select></label>
--<label class="pk-check"><input type="checkbox" name="fast" value="1"${controls.fast ? ' checked' : ''}> Fast</label>
--<label class="pk-words">Or say it in words<input type="text" name="q" value="${controls.q || ''}" placeholder="landing page under 2 cents, fast"></label>
++<label class="pk-check"><input type="checkbox" name="fast" value="1"${controls.fast ? ' checked' : ''}><span>Fast</span></label>
<button class="bm-btn" type="submit">Pick</button>
++</div>
++<p class="pk-hint">The words are read by a plain parser, never by a model: they set the three controls.</p>
</form>`;
}
@@@ -32,12 +36,13 @@ function pickTable(a) {
? 'No model on this frontier has both a score and a cost per solved task yet, so there is no answer to give. Nothing is guessed here.'
: 'No model has run this kind yet, so there is nothing to compare. The answer appears as soon as runs are published.'}</p></div>`;
}
++ // data-label names a number on a phone, where each row folds into two lines (bench.css).
const rows = a.rows.map((r) => html`<tr class="${r.thin ? 'pk-thin' : ''}">
--<td><a class="br-model" href="/models/${r.model}">${r.model}</a></td>
--<td><span class="pk-score">${r.score}</span> <span class="pk-band">${r.band ? 'band ' + r.band : 'no band yet'}</span></td>
--<td class="br-num">${cents(r.cost_per_solved)}</td>
--<td class="br-num">${msText(r.median_ms)}</td>
--<td class="br-num">${r.runs}${r.thin ? html` <span class="pk-thin-note">too few runs</span>` : ''}</td>
++<td class="pk-c-model"><a class="br-model" href="/models/${r.model}">${r.model}</a></td>
++<td class="pk-c-score"><span class="pk-score">${r.score}</span> <span class="pk-band">${r.band ? 'band ' + r.band : 'no band yet'}</span></td>
++<td class="br-num" data-label="a solved task">${cents(r.cost_per_solved)}</td>
++<td class="br-num" data-label="median">${msText(r.median_ms)}</td>
++<td class="br-num" data-label="${(r.runs === 1 ? 'run' : 'runs') + (r.thin ? ', too few' : '')}">${r.runs}${r.thin ? html` <span class="pk-thin-note">too few runs</span>` : ''}</td>
</tr>`);
return html`<div class="br-table-wrap"><table class="br-table pk-table"><thead><tr>
<th scope="col">Model</th><th scope="col">Score</th><th scope="col" class="br-num">Cost per solved task</th>
@@@ -58,12 +63,14 @@ function pickSamples(a) {
}
function pickPage(a) {
-- const notes = (a.controls.notes || []).length
-- ? html`<p class="pk-notes bm-fine">Read from the words: ${a.controls.notes.join('; ')}.</p>`
++ // What the words were read as, by name (the API keeps the parser's notes): the kind, the price, fast.
++ const price = a.controls.max_cost == null ? 'any price' : ((PRICE_STEPS.find((p) => Math.abs(p.value - a.controls.max_cost) < 1e-9) || {}).label || cents(a.controls.max_cost) + ' a solved task');
++ const notes = a.controls.q
++ ? html`<p class="pk-read"><span class="bm-fine">From your words:</span> <span class="pk-chip">${a.kind.name}</span><span class="pk-chip">${price}</span>${a.controls.fast ? html`<span class="pk-chip">fast</span>` : ''}</p>`
: '';
const thin = a.rows.filter((r) => r.thin).length;
const thinNote = thin
-- ? html`<p class="pk-thin-note bm-fine">${thin} model${thin === 1 ? '' : 's'} on this frontier have fewer than ${a.filters.min_runs} runs. Their numbers are shown greyed, because that is all the data there is.</p>`
++ ? html`<p class="pk-thin-note bm-fine">${thin === 1 ? '1 model on this frontier has' : `${thin} models on this frontier have`} fewer than ${a.filters.min_runs} runs. Their numbers are shown greyed, because that is all the data there is.</p>`
: '';
const fastNote = a.filters.fast && a.filters.fast_cap_ms != null
? html`<p class="bm-fine">Fast: a median time of ${msText(a.filters.fast_cap_ms)} a task or under, the middle of the run times.</p>`
@@@ -75,7 +82,6 @@ function pickPage(a) {
<header class="bm-page-h"><p class="bm-kicker">OpenVibe.Bench</p><h1>Pick for me</h1>
<p class="bm-lede">Say the job with plain controls. No model is called and nothing is charged: the answer comes from the runs Bench has published.</p></header>
${pickForm(a.controls)}
--<p class="bm-fine">The free-text box is read by a plain parser, never by a model. It only sets the three controls above.</p>
${notes}
<section class="bm-section pk-answer" aria-labelledby="pk-answer-h">
<h2 id="pk-answer-h">The frontier for ${a.kind.name}</h2>