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)
Changes
@@@ -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; }@@@ -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) + ')';@@@ -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>
@@ -366,10 +366,30 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
@@ -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 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, .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-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-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-table td, .pk-table th { white-space: nowrap; }
.pk-score { font-weight: 800; }
.pk-score { font-weight: 800; }
@@ -91,7 +91,9 @@ function whySentence(rows, kindName) {
@@ -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: '
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';
+ '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.';
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
const lower = rival.score < top.score
? 'scores lower (' + rival.score + ')'
? 'scores lower (' + rival.score + ')'
: 'costs more (' + cents(rival.cost_per_solved) + ')';
: 'costs more (' + cents(rival.cost_per_solved) + ')';
@@ -17,12 +17,16 @@ function pickForm(controls) {
@@ -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 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(
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>`));
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">
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>Kind<select name="kind">${kinds}</select></label>
<label>Top price per solved task<select name="max_cost">${prices}</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>
<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>`;
</form>`;
}
}
@@ -32,12 +36,13 @@ function pickTable(a) {
@@ -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 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>`;
: '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' : ''}">
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>`);
</tr>`);
return html`<div class="br-table-wrap"><table class="br-table pk-table"><thead><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>
<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) {
@@ -58,12 +63,14 @@ function pickSamples(a) {
}
}
function pickPage(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 thin = a.rows.filter((r) => r.thin).length;
const thinNote = thin
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
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>`
? 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) {
@@ -75,7 +82,6 @@ function pickPage(a) {
<header class="bm-page-h"><p class="bm-kicker">OpenVibe.Bench</p><h1>Pick for me</h1>
<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>
<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)}
${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}
${notes}
<section class="bm-section pk-answer" aria-labelledby="pk-answer-h">
<section class="bm-section pk-answer" aria-labelledby="pk-answer-h">
<h2 id="pk-answer-h">The frontier for ${a.kind.name}</h2>
<h2 id="pk-answer-h">The frontier for ${a.kind.name}</h2>