openvibers/openvibe.bench Commits

Bench: the promises and the kinds for developers fold on a phone (#47)

OP OpenVibers committed 14 days ago · faaf2d870f · 4 files, +162 −9

* Bench: the promises and the kinds for developers fold on a phone

The home was 5.5 screens at 390 px. The two parts a reader rarely needs — what
Bench promises and the arena kinds for developers — are folds now: closed on a
phone, where the title row opens them, and open where there is room, where they
look like the sections they replace. A checkbox and a label, no script, and both
parts stay in the page for the crawlers. The benchmarks, the main thing, stay
where they were. 2.6 screens at 390 px.

* Bench: the fold rules only hide a part's own heading on a phone

Where there is room the fold is open and the part keeps the heading it had;
only on a phone does the title row name it instead.

* Bench: the fold test names the class the rules use

* Bench: re-run the checks

* Bench: merge the main branch into the short-home branch

Changes

ddiff --git a/public/css/bench.css b/public/css/bench.css
iindex e32ad2a..2eacfd2 100644
---- a/public/css/bench.css
++++ b/public/css/bench.css
@@@ -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; }
ddiff --git a/server/render/short.js b/server/render/short.js
nnew file mode 100644
iindex 0000000..ec74c00
---- /dev/null
++++ b/server/render/short.js
@@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
++
++/** 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 };
ddiff --git a/server/views/index.js b/server/views/index.js
iindex 0b20e3f..0b1e2ef 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -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>`;
}
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
nnew file mode 100644
iindex 0000000..e49b09e
---- /dev/null
++++ b/test/home-short.test.js
@@@ -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); });