openvibers/openvibe.bench Commits
Bench: the promises and the kinds for developers fold on a phone (#47)
Changes
@@@ -571,6 +571,38 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex.br-sections #br-rank .br-chart { display: none; }}++/* The fold (the owner's short-pages rule, 2026-10-30): a part a reader rarely needs. On a phone it is closed — the++ title row is a 52 px target that opens it — and where there is room it is open and looks like the section it++ replaces. A checkbox and a label, no script: the part stays in the page for the crawlers. */++.fold { margin-top: 22px; }++.fold-toggle { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }++.fold-h { display: flex; align-items: center; gap: 10px; min-height: 52px; padding: 0 16px; cursor: pointer; border: 1px solid var(--border, #1f2d47); border-radius: 16px; background: var(--bg-card, #0f1729); }++.fold-h > h2 { margin: 0; font-size: 1.05rem; line-height: 1.3; flex: 1 1 auto; }++.fold-h:focus-within { outline: 2px solid var(--room-accent, #3b82f6); outline-offset: 2px; }++.fold-mark { flex: none; }++.fold-mark::after { content: "+"; font-size: 1.25rem; font-weight: 800; color: var(--room-strong, #60a5fa); }++.fold-toggle:checked ~ .fold-h .fold-mark::after { content: "\2212"; }++.fold-body { display: none; }++.fold-toggle:checked ~ .fold-body { display: block; }++.fold-body > .bm-section { margin-top: 14px; }++/* The title row names the part, so a part that keeps its own heading gives way to it on a phone (and keeps it where++ there is room). */++@media (max-width: 640px) {++ .fold-bare .fold-body > section > h2, .fold-bare .fold-body > .sc-sec > h2, .fold-bare .fold-body .ov-shipped-home h3 { display: none; }++}++.fold:not(.fold-bare) .fold-body .ov-shipped-home h3 { display: none; }++@media (min-width: 641px) {++ .fold { margin-top: clamp(28px, 4vw, 44px); }++ .fold-toggle { display: none; }++ .fold-h { align-items: baseline; justify-content: space-between; gap: 12px; min-height: 0; padding: 0; margin-bottom: 12px; cursor: default; border: 0; border-radius: 0; background: none; }++ .fold-h > h2 { margin: 0; font-size: 1.2rem; }++ .fold-mark { display: none; }++ .fold-body { display: block; }++ .fold-bare > .fold-h { display: none; }++ .fold-bare { margin-top: 0; }++ .fold:not(.fold-bare) .fold-body > .bm-section { margin-top: 0; }++}++/* ---- B13: fresh monthly sets, contamination and the time horizon ---- */.br-since { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 0 0 14px; }.br-since .br-field { margin: 0; }nnew file mode 100644@@@ -0,0 +1,60 @@++'use strict';++/**++ * The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.++ *++ * foldBlock() a part a reader rarely needs: closed on a phone, where the title row opens it, and open where there++ * is room, where it looks like the section it replaces. A checkbox and a label, no script: the part++ * stays in the page for the crawlers, and a browser without CSS shows it as it was.++ * pickBlock() several blocks of the same kind, one at a time on a phone, picked with a row of chips. Radio inputs++ * and :has(), no script: every block stays in the page, and a wider screen (or a browser without++ * :has()) shows them all and no chips.++ *++ * The CSS for both lives in the site's own stylesheet; the rule that shows the picked block is written for the page,++ * because there is one per block.++ */++const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');++++/** A piece of markup as a string: a plain string, or the raw html a template returned. */++const str = (v) => (v && typeof v === 'object' && typeof v.s === 'string') ? v.s : String(v == null ? '' : v);++++/**++ * A fold. The head is the heading the block had (an <h2>, so it stays a heading on a phone too); the html is what it++ * holds. `bare` is for a part that keeps its own heading in the body: where there is room the title row gives way to++ * it, so the part looks exactly as it did.++ *++ * foldBlock({ id, head, html, bare })++ */++function foldBlock({ id, head, html, bare = false }) {++ const key = `fold-${esc(id)}`;++ return `<div class="fold${bare ? ' fold-bare' : ''}" id="${key}">`++ + `<input class="fold-toggle" type="checkbox" id="${key}-open">`++ + `<label class="fold-h" for="${key}-open">${str(head)}<span class="fold-mark" aria-hidden="true"></span></label>`++ + `<div class="fold-body">${str(html)}</div></div>`;++}++++/**++ * A pick block. The panes are the page's own blocks; each one keeps its heading on a wide screen, and on a phone the++ * heading gives way to the chip that names it.++ *++ * pickBlock({ key, label, head, panes, checked }) panes: [{ id, label, html }]; checked: the pane the first++ * chip picks (the first one by default)++ */++function pickBlock({ key, label, head = '', panes, checked = '' }) {++ if (!panes.length) return '';++ const first = panes.find((p) => p.id === checked) ? checked : panes[0].id;++ const chips = panes.map((p) => `<input type="radio" name="pick-${esc(key)}" id="pk-${esc(key)}-${esc(p.id)}" value="${esc(p.id)}"${p.id === first ? ' checked' : ''}><label for="pk-${esc(key)}-${esc(p.id)}">${esc(p.label)}</label>`).join('');++ const body = panes.map((p) => `<div class="pick-pane" id="pp-${esc(key)}-${esc(p.id)}">${str(p.html)}</div>`).join('');++ return `<div class="pick" id="pick-${esc(key)}"><div class="pick-bar"><div class="pick-head"><h2>${esc(head || label)}</h2></div><div class="pick-row" role="radiogroup" aria-label="${esc(label)}">${chips}</div></div><div class="pick-body">${body}</div></div>`;++}++++/** The rule that shows the picked block of a pick block: one per block, so the CSS is written for the page. */++function pickRule(key, id) {++ return `.pick:has(#pk-${key}-${id}:checked) #pp-${key}-${id}{display:block}`;++}++++/** The rules for every block of a pick block, in the order they appear. */++function pickRules(key, ids) {++ return ids.map((id) => pickRule(key, id)).join('');++}++++module.exports = { foldBlock, pickBlock, pickRule, pickRules };@@@ -5,6 +5,7 @@* /docs for the API. The results pages are in ./results.js. Ids and classes start with `bm-`.*/const { esc } = require('../render/html');++const { foldBlock } = require('../render/short');function kindRow(k) {return `<li class="bm-kind"><a class="bm-kind-link" href="/api/v1/boards/${esc(k.id)}?type=elo">${esc(k.id)}</a><span class="bm-kind-meta">${esc(k.room)} · ${esc(k.judge)} · ${esc(k.name)}</span></li>`;@@@ -13,21 +14,36 @@ function kindRow(k) {function home({ kinds = [], counts = { kinds: 0 }, benchmarks = [] } = {}) {const { benchmarkCard } = require('./results');const cards = benchmarks.slice(0, 6).map((b) => String(benchmarkCard(b))).join('');-- return `<div class="bm-wrap bm-home" id="bm-home">--<header class="bm-hero"><p class="bm-kicker">OpenVibe.Bench</p><h1>See what AI really makes</h1>--<p class="bm-lede">Every model on the same tasks: real pages, games, scenes and fixes, shown live side by side. Every run is published with what it cost and how long it took, and anyone can run it again.</p>--<div class="bm-actions"><a class="bm-btn" href="/benchmarks">Browse benchmarks</a><a class="bm-btn bm-btn-quiet" href="/benchmarks/new">Make your own prompts</a><a class="bm-btn bm-btn-quiet" href="/showcase">Vote in the showcase</a></div></header>--<ul class="bm-why" aria-label="What Bench promises">++ const kindCount = Number(counts.kinds) || kinds.length;++ // The two parts a reader rarely needs (the owner's short-pages rule, 2026-10-30): the promises and the kinds for++ // developers fold closed on a phone, and where there is room they look like the sections they replace. Everything++ // stays in the page, so the crawlers and the Markdown twin still read it.++ const why = foldBlock({++ id: 'bm-why',++ bare: true,++ head: '<h2 id="bm-why-h">What Bench promises</h2>',++ html: `<ul class="bm-why" aria-label="What Bench promises"><li><b>Outputs, not just scores</b><span>The pages, games and drawings themselves, live in sandboxed frames, next to the numbers.</span></li><li><b>Every run published</b><span>Whatever it scored. No private variants, no best of ten: the service that ran it publishes it.</span></li><li><b>Value you can read</b><span>Score against cost per run, with the frontier marked, so the cheapest good model is obvious.</span></li><li><b>Free and open</b><span>Votes, runs and outputs download as JSONL. Versions are frozen, so every result can be repeated.</span></li>--</ul>++</ul>`,++ });++ const kindsBlock = foldBlock({++ id: 'bm-kinds',++ head: `<h2 id="bm-kinds-h">For developers: the arena kinds</h2><span class="bm-count">${kindCount}</span>`,++ html: `<section class="bm-section" id="bm-kinds" aria-labelledby="bm-kinds-h">++<p class="bm-fine">Each kind is a room's arena with its judge and entry type. Its board is JSON at <code>/api/v1/boards/:kind</code>, and the <a href="/docs">API</a> covers the rest.</p>++${kinds.length ? `<ul class="bm-kinds">${kinds.map(kindRow).join('')}</ul>` : '<p class="bm-muted">No kinds are seeded yet.</p>'}</section>`,++ });++ return `<div class="bm-wrap bm-home" id="bm-home">++<header class="bm-hero"><p class="bm-kicker">OpenVibe.Bench</p><h1>See what AI really makes</h1>++<p class="bm-lede">Every model on the same tasks: real pages, games, scenes and fixes, shown live side by side. Every run is published with what it cost and how long it took, and anyone can run it again.</p>++<div class="bm-actions"><a class="bm-btn" href="/benchmarks">Browse benchmarks</a><a class="bm-btn bm-btn-quiet" href="/benchmarks/new">Make your own prompts</a><a class="bm-btn bm-btn-quiet" href="/showcase">Vote in the showcase</a></div></header>++${why}<section class="bm-section" aria-labelledby="bm-bench-h"><div class="bm-head-row"><h2 id="bm-bench-h">Benchmarks</h2><a class="bm-more" href="/benchmarks">All of them</a></div>${cards ? `<ul class="br-cards">${cards}</ul>` : '<p class="bm-muted">No benchmark is published yet.</p>'}</section>--<section class="bm-section" id="bm-kinds" aria-labelledby="bm-kinds-h"><div class="bm-head-row"><h2 id="bm-kinds-h">For developers: the arena kinds</h2><span class="bm-count">${Number(counts.kinds) || kinds.length}</span></div>--<p class="bm-fine">Each kind is a room's arena with its judge and entry type. Its board is JSON at <code>/api/v1/boards/:kind</code>, and the <a href="/docs">API</a> covers the rest.</p>--${kinds.length ? `<ul class="bm-kinds">${kinds.map(kindRow).join('')}</ul>` : '<p class="bm-muted">No kinds are seeded yet.</p>'}</section>++${kindsBlock}</div>`;}nnew file mode 100644@@@ -0,0 +1,45 @@++'use strict';++// The home on a phone (the owner's short-pages rule, 2026-10-30): the parts a reader rarely needs — the promises++// and the kinds for developers — fold closed, with a checkbox and a label and no script. Everything stays in the++// page for the crawlers, and where there is room the fold is open and looks like the section it replaces.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { boot, check, done } = require('./helpers/boot');++++(async () => {++ const t = await boot();++++ await check('the home folds the promises and the kinds for developers, and keeps both in the page', async () => {++ const r = await t.get('/');++ assert.strictEqual(r.status, 200, r.text.slice(0, 300));++ const home = r.text;++ for (const [id, title] of [['bm-why', 'What Bench promises'], ['bm-kinds', 'For developers: the arena kinds']]) {++ const re = new RegExp(`<div class="fold[^"]*" id="fold-${id}"><input class="fold-toggle" type="checkbox" id="fold-${id}-open">`++ + `<label class="fold-h" for="fold-${id}-open"><h2[^>]*>[^<]*${title}[^<]*</h2>.*?<div class="fold-body">`);++ assert.ok(re.test(home), `the ${title} fold is on the home`);++ }++ // The parts themselves are in the page, not removed: the four promises and every kind.++ assert.strictEqual((home.match(/<li><b>(Outputs, not just scores|Every run published|Value you can read|Free and open)<\/b>/g) || []).length, 4, 'the four promises are in the page');++ const kinds = (home.match(/class="bm-kind-link"/g) || []).length;++ assert.ok(kinds >= 9, `the kinds are in the page (${kinds})`);++ assert.match(home, /<h2 id="bm-kinds-h">For developers: the arena kinds<\/h2>/, 'the kinds keep their heading');++ // The benchmarks are the main thing on the home: they stay as they were.++ assert.match(home, /<section class="bm-section" aria-labelledby="bm-bench-h">/, 'the benchmarks section is where it was');++ });++++ await check('the fold is closed on a phone and open where there is room', async () => {++ const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'bench.css'), 'utf8');++ const fold = css.slice(css.indexOf('/* The fold (the owner'));++ assert.ok(fold.length > 0, 'the fold rules are in bench.css');++ assert.match(fold, /\.fold-body \{ display: none; \}/, 'the body is closed by default');++ assert.match(fold, /\.fold-toggle:checked ~ \.fold-body \{ display: block; \}/, 'the title row opens it');++ assert.match(fold, /\.fold-toggle \{ display: none; \}/, 'the toggle is hidden where there is room');++ assert.match(fold, /\.fold-body \{ display: block; \}/, 'the body is open where there is room');++ assert.match(fold, /\.fold-bare > \.fold-h \{ display: none; \}/, 'a part with no heading of its own looks as it did');++ assert.ok(!/#[0-9a-f]{3,6}\b/i.test(fold.replace(/#[0-9a-f]{3,6}\b/gi, '')), 'no new colours in the fold rules');++ });++++ await t.close();++ done();++})().catch((e) => { console.error(e); process.exit(1); });
@@ -571,6 +571,38 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
@@ -571,6 +571,38 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
.br-sections #br-rank .br-chart { display: none; }
.br-sections #br-rank .br-chart { display: none; }
}
}
+/* The fold (the owner's short-pages rule, 2026-10-30): a part a reader rarely needs. On a phone it is closed — the
+ title row is a 52 px target that opens it — and where there is room it is open and looks like the section it
+ replaces. A checkbox and a label, no script: the part stays in the page for the crawlers. */
+.fold { margin-top: 22px; }
+.fold-toggle { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
+.fold-h { display: flex; align-items: center; gap: 10px; min-height: 52px; padding: 0 16px; cursor: pointer; border: 1px solid var(--border, #1f2d47); border-radius: 16px; background: var(--bg-card, #0f1729); }
+.fold-h > h2 { margin: 0; font-size: 1.05rem; line-height: 1.3; flex: 1 1 auto; }
+.fold-h:focus-within { outline: 2px solid var(--room-accent, #3b82f6); outline-offset: 2px; }
+.fold-mark { flex: none; }
+.fold-mark::after { content: "+"; font-size: 1.25rem; font-weight: 800; color: var(--room-strong, #60a5fa); }
+.fold-toggle:checked ~ .fold-h .fold-mark::after { content: "\2212"; }
+.fold-body { display: none; }
+.fold-toggle:checked ~ .fold-body { display: block; }
+.fold-body > .bm-section { margin-top: 14px; }
+/* The title row names the part, so a part that keeps its own heading gives way to it on a phone (and keeps it where
+ there is room). */
+@media (max-width: 640px) {
+ .fold-bare .fold-body > section > h2, .fold-bare .fold-body > .sc-sec > h2, .fold-bare .fold-body .ov-shipped-home h3 { display: none; }
+}
+.fold:not(.fold-bare) .fold-body .ov-shipped-home h3 { display: none; }
+@media (min-width: 641px) {
+ .fold { margin-top: clamp(28px, 4vw, 44px); }
+ .fold-toggle { display: none; }
+ .fold-h { align-items: baseline; justify-content: space-between; gap: 12px; min-height: 0; padding: 0; margin-bottom: 12px; cursor: default; border: 0; border-radius: 0; background: none; }
+ .fold-h > h2 { margin: 0; font-size: 1.2rem; }
+ .fold-mark { display: none; }
+ .fold-body { display: block; }
+ .fold-bare > .fold-h { display: none; }
+ .fold-bare { margin-top: 0; }
+ .fold:not(.fold-bare) .fold-body > .bm-section { margin-top: 0; }
+}
+
/* ---- B13: fresh monthly sets, contamination and the time horizon ---- */
/* ---- B13: fresh monthly sets, contamination and the time horizon ---- */
.br-since { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 0 0 14px; }
.br-since { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 0 0 14px; }
.br-since .br-field { margin: 0; }
.br-since .br-field { margin: 0; }
new file mode 100644
new file mode 100644
@@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
+'use strict';
+/**
+ * The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.
+ *
+ * foldBlock() a part a reader rarely needs: closed on a phone, where the title row opens it, and open where there
+ * is room, where it looks like the section it replaces. A checkbox and a label, no script: the part
+ * stays in the page for the crawlers, and a browser without CSS shows it as it was.
+ * pickBlock() several blocks of the same kind, one at a time on a phone, picked with a row of chips. Radio inputs
+ * and :has(), no script: every block stays in the page, and a wider screen (or a browser without
+ * :has()) shows them all and no chips.
+ *
+ * The CSS for both lives in the site's own stylesheet; the rule that shows the picked block is written for the page,
+ * because there is one per block.
+ */
+const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
+
+/** A piece of markup as a string: a plain string, or the raw html a template returned. */
+const str = (v) => (v && typeof v === 'object' && typeof v.s === 'string') ? v.s : String(v == null ? '' : v);
+
+/**
+ * A fold. The head is the heading the block had (an <h2>, so it stays a heading on a phone too); the html is what it
+ * holds. `bare` is for a part that keeps its own heading in the body: where there is room the title row gives way to
+ * it, so the part looks exactly as it did.
+ *
+ * foldBlock({ id, head, html, bare })
+ */
+function foldBlock({ id, head, html, bare = false }) {
+ const key = `fold-${esc(id)}`;
+ return `<div class="fold${bare ? ' fold-bare' : ''}" id="${key}">`
+ + `<input class="fold-toggle" type="checkbox" id="${key}-open">`
+ + `<label class="fold-h" for="${key}-open">${str(head)}<span class="fold-mark" aria-hidden="true"></span></label>`
+ + `<div class="fold-body">${str(html)}</div></div>`;
+}
+
+/**
+ * A pick block. The panes are the page's own blocks; each one keeps its heading on a wide screen, and on a phone the
+ * heading gives way to the chip that names it.
+ *
+ * pickBlock({ key, label, head, panes, checked }) panes: [{ id, label, html }]; checked: the pane the first
+ * chip picks (the first one by default)
+ */
+function pickBlock({ key, label, head = '', panes, checked = '' }) {
+ if (!panes.length) return '';
+ const first = panes.find((p) => p.id === checked) ? checked : panes[0].id;
+ const chips = panes.map((p) => `<input type="radio" name="pick-${esc(key)}" id="pk-${esc(key)}-${esc(p.id)}" value="${esc(p.id)}"${p.id === first ? ' checked' : ''}><label for="pk-${esc(key)}-${esc(p.id)}">${esc(p.label)}</label>`).join('');
+ const body = panes.map((p) => `<div class="pick-pane" id="pp-${esc(key)}-${esc(p.id)}">${str(p.html)}</div>`).join('');
+ return `<div class="pick" id="pick-${esc(key)}"><div class="pick-bar"><div class="pick-head"><h2>${esc(head || label)}</h2></div><div class="pick-row" role="radiogroup" aria-label="${esc(label)}">${chips}</div></div><div class="pick-body">${body}</div></div>`;
+}
+
+/** The rule that shows the picked block of a pick block: one per block, so the CSS is written for the page. */
+function pickRule(key, id) {
+ return `.pick:has(#pk-${key}-${id}:checked) #pp-${key}-${id}{display:block}`;
+}
+
+/** The rules for every block of a pick block, in the order they appear. */
+function pickRules(key, ids) {
+ return ids.map((id) => pickRule(key, id)).join('');
+}
+
+module.exports = { foldBlock, pickBlock, pickRule, pickRules };
@@ -5,6 +5,7 @@
@@ -5,6 +5,7 @@
* /docs for the API. The results pages are in ./results.js. Ids and classes start with `bm-`.
* /docs for the API. The results pages are in ./results.js. Ids and classes start with `bm-`.
*/
*/
const { esc } = require('../render/html');
const { esc } = require('../render/html');
+const { foldBlock } = require('../render/short');
function kindRow(k) {
function kindRow(k) {
return `<li class="bm-kind"><a class="bm-kind-link" href="/api/v1/boards/${esc(k.id)}?type=elo">${esc(k.id)}</a><span class="bm-kind-meta">${esc(k.room)} · ${esc(k.judge)} · ${esc(k.name)}</span></li>`;
return `<li class="bm-kind"><a class="bm-kind-link" href="/api/v1/boards/${esc(k.id)}?type=elo">${esc(k.id)}</a><span class="bm-kind-meta">${esc(k.room)} · ${esc(k.judge)} · ${esc(k.name)}</span></li>`;
@@ -13,21 +14,36 @@ function kindRow(k) {
@@ -13,21 +14,36 @@ function kindRow(k) {
function home({ kinds = [], counts = { kinds: 0 }, benchmarks = [] } = {}) {
function home({ kinds = [], counts = { kinds: 0 }, benchmarks = [] } = {}) {
const { benchmarkCard } = require('./results');
const { benchmarkCard } = require('./results');
const cards = benchmarks.slice(0, 6).map((b) => String(benchmarkCard(b))).join('');
const cards = benchmarks.slice(0, 6).map((b) => String(benchmarkCard(b))).join('');
- return `<div class="bm-wrap bm-home" id="bm-home">
-<header class="bm-hero"><p class="bm-kicker">OpenVibe.Bench</p><h1>See what AI really makes</h1>
-<p class="bm-lede">Every model on the same tasks: real pages, games, scenes and fixes, shown live side by side. Every run is published with what it cost and how long it took, and anyone can run it again.</p>
-<div class="bm-actions"><a class="bm-btn" href="/benchmarks">Browse benchmarks</a><a class="bm-btn bm-btn-quiet" href="/benchmarks/new">Make your own prompts</a><a class="bm-btn bm-btn-quiet" href="/showcase">Vote in the showcase</a></div></header>
-<ul class="bm-why" aria-label="What Bench promises">
+ const kindCount = Number(counts.kinds) || kinds.length;
+ // The two parts a reader rarely needs (the owner's short-pages rule, 2026-10-30): the promises and the kinds for
+ // developers fold closed on a phone, and where there is room they look like the sections they replace. Everything
+ // stays in the page, so the crawlers and the Markdown twin still read it.
+ const why = foldBlock({
+ id: 'bm-why',
+ bare: true,
+ head: '<h2 id="bm-why-h">What Bench promises</h2>',
+ html: `<ul class="bm-why" aria-label="What Bench promises">
<li><b>Outputs, not just scores</b><span>The pages, games and drawings themselves, live in sandboxed frames, next to the numbers.</span></li>
<li><b>Outputs, not just scores</b><span>The pages, games and drawings themselves, live in sandboxed frames, next to the numbers.</span></li>
<li><b>Every run published</b><span>Whatever it scored. No private variants, no best of ten: the service that ran it publishes it.</span></li>
<li><b>Every run published</b><span>Whatever it scored. No private variants, no best of ten: the service that ran it publishes it.</span></li>
<li><b>Value you can read</b><span>Score against cost per run, with the frontier marked, so the cheapest good model is obvious.</span></li>
<li><b>Value you can read</b><span>Score against cost per run, with the frontier marked, so the cheapest good model is obvious.</span></li>
<li><b>Free and open</b><span>Votes, runs and outputs download as JSONL. Versions are frozen, so every result can be repeated.</span></li>
<li><b>Free and open</b><span>Votes, runs and outputs download as JSONL. Versions are frozen, so every result can be repeated.</span></li>
-</ul>
+</ul>`,
+ });
+ const kindsBlock = foldBlock({
+ id: 'bm-kinds',
+ head: `<h2 id="bm-kinds-h">For developers: the arena kinds</h2><span class="bm-count">${kindCount}</span>`,
+ html: `<section class="bm-section" id="bm-kinds" aria-labelledby="bm-kinds-h">
+<p class="bm-fine">Each kind is a room's arena with its judge and entry type. Its board is JSON at <code>/api/v1/boards/:kind</code>, and the <a href="/docs">API</a> covers the rest.</p>
+${kinds.length ? `<ul class="bm-kinds">${kinds.map(kindRow).join('')}</ul>` : '<p class="bm-muted">No kinds are seeded yet.</p>'}</section>`,
+ });
+ return `<div class="bm-wrap bm-home" id="bm-home">
+<header class="bm-hero"><p class="bm-kicker">OpenVibe.Bench</p><h1>See what AI really makes</h1>
+<p class="bm-lede">Every model on the same tasks: real pages, games, scenes and fixes, shown live side by side. Every run is published with what it cost and how long it took, and anyone can run it again.</p>
+<div class="bm-actions"><a class="bm-btn" href="/benchmarks">Browse benchmarks</a><a class="bm-btn bm-btn-quiet" href="/benchmarks/new">Make your own prompts</a><a class="bm-btn bm-btn-quiet" href="/showcase">Vote in the showcase</a></div></header>
+${why}
<section class="bm-section" aria-labelledby="bm-bench-h"><div class="bm-head-row"><h2 id="bm-bench-h">Benchmarks</h2><a class="bm-more" href="/benchmarks">All of them</a></div>
<section class="bm-section" aria-labelledby="bm-bench-h"><div class="bm-head-row"><h2 id="bm-bench-h">Benchmarks</h2><a class="bm-more" href="/benchmarks">All of them</a></div>
${cards ? `<ul class="br-cards">${cards}</ul>` : '<p class="bm-muted">No benchmark is published yet.</p>'}</section>
${cards ? `<ul class="br-cards">${cards}</ul>` : '<p class="bm-muted">No benchmark is published yet.</p>'}</section>
-<section class="bm-section" id="bm-kinds" aria-labelledby="bm-kinds-h"><div class="bm-head-row"><h2 id="bm-kinds-h">For developers: the arena kinds</h2><span class="bm-count">${Number(counts.kinds) || kinds.length}</span></div>
-<p class="bm-fine">Each kind is a room's arena with its judge and entry type. Its board is JSON at <code>/api/v1/boards/:kind</code>, and the <a href="/docs">API</a> covers the rest.</p>
-${kinds.length ? `<ul class="bm-kinds">${kinds.map(kindRow).join('')}</ul>` : '<p class="bm-muted">No kinds are seeded yet.</p>'}</section>
+${kindsBlock}
</div>`;
</div>`;
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+'use strict';
+// The home on a phone (the owner's short-pages rule, 2026-10-30): the parts a reader rarely needs — the promises
+// and the kinds for developers — fold closed, with a checkbox and a label and no script. Everything stays in the
+// page for the crawlers, and where there is room the fold is open and looks like the section it replaces.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { boot, check, done } = require('./helpers/boot');
+
+(async () => {
+ const t = await boot();
+
+ await check('the home folds the promises and the kinds for developers, and keeps both in the page', async () => {
+ const r = await t.get('/');
+ assert.strictEqual(r.status, 200, r.text.slice(0, 300));
+ const home = r.text;
+ for (const [id, title] of [['bm-why', 'What Bench promises'], ['bm-kinds', 'For developers: the arena kinds']]) {
+ const re = new RegExp(`<div class="fold[^"]*" id="fold-${id}"><input class="fold-toggle" type="checkbox" id="fold-${id}-open">`
+ + `<label class="fold-h" for="fold-${id}-open"><h2[^>]*>[^<]*${title}[^<]*</h2>.*?<div class="fold-body">`);
+ assert.ok(re.test(home), `the ${title} fold is on the home`);
+ }
+ // The parts themselves are in the page, not removed: the four promises and every kind.
+ assert.strictEqual((home.match(/<li><b>(Outputs, not just scores|Every run published|Value you can read|Free and open)<\/b>/g) || []).length, 4, 'the four promises are in the page');
+ const kinds = (home.match(/class="bm-kind-link"/g) || []).length;
+ assert.ok(kinds >= 9, `the kinds are in the page (${kinds})`);
+ assert.match(home, /<h2 id="bm-kinds-h">For developers: the arena kinds<\/h2>/, 'the kinds keep their heading');
+ // The benchmarks are the main thing on the home: they stay as they were.
+ assert.match(home, /<section class="bm-section" aria-labelledby="bm-bench-h">/, 'the benchmarks section is where it was');
+ });
+
+ await check('the fold is closed on a phone and open where there is room', async () => {
+ const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'bench.css'), 'utf8');
+ const fold = css.slice(css.indexOf('/* The fold (the owner'));
+ assert.ok(fold.length > 0, 'the fold rules are in bench.css');
+ assert.match(fold, /\.fold-body \{ display: none; \}/, 'the body is closed by default');
+ assert.match(fold, /\.fold-toggle:checked ~ \.fold-body \{ display: block; \}/, 'the title row opens it');
+ assert.match(fold, /\.fold-toggle \{ display: none; \}/, 'the toggle is hidden where there is room');
+ assert.match(fold, /\.fold-body \{ display: block; \}/, 'the body is open where there is room');
+ assert.match(fold, /\.fold-bare > \.fold-h \{ display: none; \}/, 'a part with no heading of its own looks as it did');
+ assert.ok(!/#[0-9a-f]{3,6}\b/i.test(fold.replace(/#[0-9a-f]{3,6}\b/gi, '')), 'no new colours in the fold rules');
+ });
+
+ await t.close();
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });