openvibers/openvibe.report Commits

Design pass: readable AI boards, exact costs, phone-safe tables (#6)

OP OpenVibers committed 16 days ago · ca79379eaf · 2 files, +34 −6

* Report design pass: readable AI boards and phone-scrolling tables

Lead each /ai card with its leader Elo rating as one headline number, add
gridlines/ticks and label every point on the Pareto chart, keep cents in money,
and give phone tables a 640px min width so they scroll inside their card.

Co-authored-by: CommandCodeBot <[email protected]>

* Keep sub-cent costs exact in Report money formatting

For values under a dollar, only show two decimals when they are exact cents;
otherwise keep the significant figures so $0.027 does not round to $0.03.

Co-authored-by: CommandCodeBot <[email protected]>

---------

Co-authored-by: CommandCodeBot <[email protected]>

Changes

ddiff --git a/public/css/app.css b/public/css/app.css
iindex 46677c0..0f1fd63 100644
---- a/public/css/app.css
++++ b/public/css/app.css
@@@ -37,6 +37,17 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
.rp-ai-room .rp-topic h3 { margin: 0; font-size: 1.05rem; }
.rp-ai-room .rp-topic p { margin: 0; }
.rp-ai-room .rp-topic .rp-fine { margin: 0; }
++.rp-ai-kind { display: flex; flex-direction: column; gap: 8px; padding: 16px; border-radius: 16px; background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); color: var(--text-primary, #e6edf7); }
++.rp-ai-kind-h { display: flex; flex-direction: column; gap: 4px; }
++.rp-ai-kind-h h3 { margin: 0; font-size: 1.05rem; line-height: 1.3; }
++.rp-ai-kind-h h3 a { color: inherit; text-decoration: none; }
++.rp-ai-kind-h h3 a:hover, .rp-ai-kind-h h3 a:focus-visible { text-decoration: underline; }
++.rp-ai-lead { display: flex; align-items: baseline; gap: 10px; margin: 0; flex-wrap: wrap; }
++.rp-ai-lead-v { font-size: 1.9rem; font-weight: 850; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1; }
++.rp-ai-lead-s { font-size: .9rem; color: var(--text-secondary, #96a7c2); }
++.rp-ai-links { gap: 4px 2px; }
++.rp-ai-links a { min-height: 32px; display: inline-flex; align-items: center; padding: 0 4px; color: var(--room-strong); font-weight: 700; text-decoration: none; }
++.rp-ai-links a:hover { text-decoration: underline; }
.rp-ai-scroll { overflow-x: auto; margin: 20px 0; border: 1px solid var(--border, #1f2d47); border-radius: 16px; }
.rp-ai-table { width: 100%; border-collapse: collapse; min-width: 650px; font-variant-numeric: tabular-nums; }
.rp-ai-table th, .rp-ai-table td { padding: 11px 14px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--border, #1f2d47); }
@@@ -47,7 +58,11 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
.rp-ai-chart { margin: 20px 0; padding: 12px; border: 1px solid var(--border, #1f2d47); border-radius: 16px; background: var(--bg-card, #0f1729); }
.rp-ai-chart svg { display: block; width: 100%; height: auto; }
.rp-ai-chart line { stroke: var(--text-muted, #7386a3); stroke-width: 1; }
++.rp-ai-chart .rp-ai-grid { stroke: var(--border, #1f2d47); stroke-width: 1; stroke-dasharray: 3 4; }
.rp-ai-chart text { fill: var(--text-secondary, #96a7c2); font-size: 11px; }
++.rp-ai-chart .rp-ai-tick { fill: var(--text-muted, #7386a3); font-size: 11px; }
++.rp-ai-chart .rp-ai-lab { fill: var(--text-secondary, #96a7c2); font-size: 11px; }
++.rp-ai-chart .rp-ai-lab.is-frontier { fill: var(--room-strong); font-weight: 800; }
.rp-ai-chart .rp-ai-frontier { fill: var(--room-accent); stroke: var(--bg-card, #0f1729); stroke-width: 1; }
.rp-ai-chart .rp-ai-point { fill: var(--text-muted, #7386a3); }
.rp-ai-chart figcaption { margin-top: 6px; font-size: .8rem; color: var(--text-muted, #7386a3); }
@@@ -158,7 +173,7 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
.rp-ai-table a:hover, .rp-ai-table a:focus-visible { text-decoration: underline; }
.rp-ai-chart { overflow-x: auto; }
@media (max-width: 640px) {
-- .rp-ai-table { min-width: 0; font-size: .86rem; }
-- .rp-ai-table th, .rp-ai-table td { padding: 9px 7px; }
++ .rp-ai-table { min-width: 640px; font-size: .88rem; }
++ .rp-ai-table th, .rp-ai-table td { padding: 10px 8px; }
.rp-ai-chart svg { min-width: 720px; }
}
ddiff --git a/server/views/index.js b/server/views/index.js
iindex 1d7fee0..dfe51bd 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -48,6 +48,7 @@ const usd = (v) => {
const a = Math.abs(x);
if (a >= 1000) return `$${x.toLocaleString('en-US', { maximumFractionDigits: 0 })}`;
if (a >= 1) return `$${x.toFixed(2)}`;
++ if (a >= 0.01) return Math.abs(x - Math.round(x * 100) / 100) < 1e-9 ? `$${x.toFixed(2)}` : `$${Number(x.toPrecision(2))}`;
return `$${Number(x.toPrecision(2))}`;
};
/** A share of games won or tasks passed: 0.8333 → 83%. */
@@@ -368,7 +369,11 @@ function aiIndex({ kinds, boards = [] }) {
const sections = [...groups].sort(([a], [b]) => a.localeCompare(b)).map(([room, items]) => `<section class="rp-ai-room" aria-label="${esc(room)}"><h2>${esc(roomName(room))}</h2><ul class="rp-topics">${items.map(({ kind, rows, available }) => {
const leader = rows[0];
const href = aiPath(kind.id);
-- return `<li class="rp-topic"><h3><a href="${href}">${esc(kind.name || kind.id)}</a></h3><p class="rp-fine">${esc(kind.id)} · ${esc(judgeWord(kind.judge))}</p><p>${available ? `Leader: ${leader ? modelLink(leader.subject) : 'No leader yet'} · ${exact(gamesOf(rows))} games` : 'Board temporarily unavailable'}</p><p class="rp-meta"><a href="${href}">Elo</a> · <a href="${href}?tab=pareto">Pareto</a> · <a href="${href}?tab=cost">Cost per solved</a></p></li>`;
++ const headline = available && leader ? `<b class="rp-ai-lead-v">${rating(leader)}</b><span class="rp-ai-lead-s">${modelLink(leader.subject)}${flag(leader)}</span>` : available ? '<b class="rp-ai-lead-v">—</b><span class="rp-ai-lead-s">No leader yet</span>' : '<b class="rp-ai-lead-v">—</b><span class="rp-ai-lead-s">Board temporarily unavailable</span>';
++ return `<li class="rp-ai-kind"><div class="rp-ai-kind-h"><h3><a href="${href}">${esc(kind.name || kind.id)}</a></h3><p class="rp-fine">${esc(kind.id)} · ${esc(judgeWord(kind.judge))}</p></div>`
++ + `<p class="rp-ai-lead">${headline}</p>`
++ + `<p class="rp-fine">${available ? `${exact(gamesOf(rows))} games` : ''}</p>`
++ + `<p class="rp-meta rp-ai-links"><a href="${href}">Elo</a><span aria-hidden="true">·</span><a href="${href}?tab=pareto">Pareto</a><span aria-hidden="true">·</span><a href="${href}?tab=cost">Cost per solved</a></p></li>`;
}).join('')}</ul></section>`).join('');
const body = `<header class="rp-page-h"><p class="rp-kicker">Benchmarks</p><h1>AI boards</h1><p class="rp-lede">Every benchmark kind across OpenVibe rooms, with ratings, quality and cost.</p></header>${benchState(kinds, ...boards)}${sections ? `<div class="rp-ai-rooms">${sections}</div>` : '' }${sections ? '' : '<p class="rp-fine">Bench results are temporarily unavailable.</p>'}`;
return layout({ title: 'AI boards', description: 'AI benchmark leaders and boards across OpenVibe rooms.', body, active: '/ai', path: '/ai' });
@@@ -390,11 +395,19 @@ function paretoChart(rows) {
const maxCost = Math.max(0.001, ...points.map((r) => r.costPerSolved));
const x = (v) => 70 + 650 * v / maxCost;
const y = (v) => 285 - 235 * Math.max(0, Math.min(1, v));
-- const marks = points.map((r) => {
++ const xt = [0, 0.25, 0.5, 0.75, 1].map((f) => ({ v: f * maxCost, px: x(f * maxCost) }));
++ const yt = [0, 0.25, 0.5, 0.75, 1].map((q) => ({ q, py: y(q) }));
++ const grid = yt.map((t) => `<line class="rp-ai-grid" x1="70" y1="${t.py.toFixed(1)}" x2="720" y2="${t.py.toFixed(1)}"/>`).join('')
++ + xt.map((t) => `<line class="rp-ai-grid" x1="${t.px.toFixed(1)}" y1="50" x2="${t.px.toFixed(1)}" y2="285"/>`).join('');
++ const xTicks = xt.map((t) => `<text class="rp-ai-tick" x="${t.px.toFixed(1)}" y="307" text-anchor="middle">${esc(usd(t.v))}</text>`).join('');
++ const yTicks = yt.map((t) => `<text class="rp-ai-tick" x="64" y="${(t.py + 4).toFixed(1)}" text-anchor="end">${Math.round(t.q * 100)}%</text>`).join('');
++ const marks = points.map((r, i) => {
const px = x(r.costPerSolved).toFixed(1), py = y(r.quality).toFixed(1);
-- return `<g><circle cx="${px}" cy="${py}" r="${r.frontier ? 5 : 3}" class="${r.frontier ? 'rp-ai-frontier' : 'rp-ai-point'}"><title>${esc(r.subject)}: ${share(r.quality)} quality, ${usd(r.costPerSolved)} per solved task${r.frontier ? ', frontier' : ''}</title></circle>${r.frontier ? `<text x="${px}" y="${(Number(py) - 9).toFixed(1)}" text-anchor="middle">${esc(r.subject)}</text>` : ''}</g>`;
++ const dy = i % 2 ? 14 : -8;
++ return `<g><circle cx="${px}" cy="${py}" r="${r.frontier ? 5 : 3}" class="${r.frontier ? 'rp-ai-frontier' : 'rp-ai-point'}"><title>${esc(r.subject)}: ${share(r.quality)} quality, ${usd(r.costPerSolved)} per solved task${r.frontier ? ', frontier' : ''}</title></circle>`
++ + `<text class="rp-ai-lab${r.frontier ? ' is-frontier' : ''}" x="${(Number(px) + 6).toFixed(1)}" y="${(Number(py) + dy).toFixed(1)}">${esc(r.subject)}</text></g>`;
}).join('');
-- return `<figure class="rp-ai-chart"><svg viewBox="0 0 800 340" role="img" aria-labelledby="rp-ai-chart-title rp-ai-chart-desc"><title id="rp-ai-chart-title">Quality against cost per solved task</title><desc id="rp-ai-chart-desc">Each point is a model. Labelled points form the best quality and cost frontier. The table below gives exact values.</desc><line x1="70" y1="285" x2="720" y2="285"/><line x1="70" y1="50" x2="70" y2="285"/><text x="70" y="307" text-anchor="middle">$0</text><text x="720" y="307" text-anchor="end">${esc(usd(maxCost))}</text><text x="45" y="285" text-anchor="end">0%</text><text x="45" y="55" text-anchor="end">100%</text><text x="395" y="332" text-anchor="middle">Cost per solved task (USD, lower is better)</text><text x="16" y="170" transform="rotate(-90 16 170)" text-anchor="middle">Quality (higher is better)</text>${marks}</svg><figcaption>Labelled points are on the quality and cost frontier.</figcaption></figure>`;
++ return `<figure class="rp-ai-chart"><svg viewBox="0 0 800 340" role="img" aria-labelledby="rp-ai-chart-title rp-ai-chart-desc"><title id="rp-ai-chart-title">Quality against cost per solved task</title><desc id="rp-ai-chart-desc">Each point is a model. Labelled points form the best quality and cost frontier. The table below gives exact values.</desc>${grid}<line x1="70" y1="285" x2="720" y2="285"/><line x1="70" y1="50" x2="70" y2="285"/>${xTicks}${yTicks}<text x="395" y="332" text-anchor="middle">Cost per solved task (USD, lower is better)</text><text x="16" y="170" transform="rotate(-90 16 170)" text-anchor="middle">Quality (higher is better)</text>${marks}</svg><figcaption>Every point is labelled; solid points are on the quality and cost frontier.</figcaption></figure>`;
}
function aiBoard({ kindId, kind, tab, board, cost, kinds }) {