openvibers/openvibe.report Commits
openvibe-shared v4.228.0 (was v4.225.0) (#208)
Changes
@@@ -262,7 +262,7 @@ HMAC. See [SECURITY.md](SECURITY.md).<!-- versions:start -->- openvibe-contracts: v0.417.0- openvibe-sdk: v0.161.0--- openvibe-shared: v4.225.0++- openvibe-shared: v4.228.0- openvibe-publishing: v1.7.0<!-- versions:end -->@@@ -10,12 +10,12 @@"domain": "openvibe.report","contracts": "openvibe-contracts v0.417.0","sdk": "openvibe-sdk v0.161.0",-- "shared": "openvibe-shared v4.225.0",++ "shared": "openvibe-shared v4.228.0","packages": ["openvibe-contracts v0.417.0","openvibe-publishing v1.7.0","openvibe-sdk v0.161.0",-- "openvibe-shared v4.225.0",++ "openvibe-shared v4.228.0","pg v8.23.1","pglite v0.5.13 (dev)"],@@@ -16,7 +16,7 @@"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.417.0","openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.225.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.228.0","pg": "^8.23.1"},"devDependencies": {@@@ -1200,9 +1200,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.225.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.225.0",-- "integrity": "sha512-zuZIgYODwJJ7UpDBTVnT+czEI5l6YFWqt1nbfbPeEsW6/NlmDZHJWPSGpBv223jRpvhJYH7Duea6nuRr8KZ3sg==",++ "version": "4.228.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.228.0",++ "integrity": "sha512-HR0Hac/K+qKmwMf2QZWCFxW+uVja36nyAtxVbNFgPGUDFdmWhmb6RMoP/CkU0+GSCPsC50k6aTS+oPx2czDgGg==","license": "MIT","dependencies": {"clean-css": "^5.3.3",@@@ -22,7 +22,7 @@"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.417.0","openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.225.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.228.0","pg": "^8.23.1"},"engines": {@@@ -8,6 +8,7 @@// - speed and uptime are measured on the Exchange's real traffic, shown only where traffic went.// A column or section earns its place: it shows when some row has a value, and a missing value reads in words.const kit = require('openvibe-shared/kit');++const { parts, PARTS_CSS } = require('openvibe-shared/showcase');const marks = require('openvibe-shared/ai-marks');const { asset } = require('../assets');const { layout } = require('./index');@@@ -110,7 +111,7 @@ function board(rows, { full = false } = {}) {}function page(title, description, body, path) {-- return layout({ title, description, body: `<link rel="stylesheet" href="${asset('css/ai-comparison.css')}"><style>${marks.CSS}</style><div class="ai-page">${body}</div>`, active: '/ai', path });++ return layout({ title, description, body: `<link rel="stylesheet" href="${asset('css/ai-comparison.css')}"><style>${marks.CSS}\n${PARTS_CSS}</style><div class="ai-page">${body}</div>`, active: '/ai', path });}const dataLinks = () => `<p class="ai-data">Data: <a href="/api/v1/ai/leaderboard">JSON</a> · <a href="/api/v1/ai/leaderboard?format=csv&limit=100">CSV</a> · <a href="/ai/method">How it is measured</a></p>`;@@@ -146,13 +147,17 @@ function hub({ rows = [], picks = [], providers = [], others = 0, month = new Da+ kit.stats([[ratedRows.length, 'rated'], [rows.length, 'models listed'], [providerNames.size, 'providers'], [measuredOffers.length, 'measured endpoints']], { label: 'The leaderboard in numbers' })+ `</header>`+ (pickList || pending.length ? kit.section({ title: 'Top picks', body: `${pickList ? `<ul class="ai-picks">${pickList}</ul>` : ''}${pending.length ? `<p class="ai-fine">Not enough data yet for ${esc(pending.join(', '))}.</p>` : ''}` }) : '')-- + kit.section({ title: 'Leaderboard', count: rows.length, actions: [act('/ai/leaderboard', rows.length > 25 ? `All ${rows.length} models` : 'Filter and sort')], body: board(rows.slice(0, 25)) + (others ? `<p class="ai-fine">Chat models only. ${others} embedding and speech ${others === 1 ? 'model is' : 'models are'} in the <a href="/ai/leaderboard?type=all">full list</a>.</p>` : '') + dataLinks() })-- + (arena && (arena.answers || []).length ? kit.section({ title: 'One prompt, many answers', body: `<p class="ai-prompt">${esc(arena.prompt || '')}</p><div class="ai-answers">${arena.answers.slice(0, 3).map((a) => `<article>${modelCell({ id: a.model || 'Model', maker: a.maker })}<p>${esc(a.text || '')}</p></article>`).join('')}</div>` }) : '')-- + (tasks.length ? kit.section({ title: 'Boards by task', body: `<ul class="ai-chips">${tasks.slice(0, 16).map((x) => `<li><a href="/ai/${encodeURIComponent(x.id)}">${esc(x.name || x.id)}</a></li>`).join('')}</ul>` }) : '')-- + (measuredOffers.length ? kit.section({ title: 'Providers', actions: [act('/ai/providers', 'Every provider')], body: providerTable(measuredOffers.slice(0, 6)) }) : '')-- + (pairs.length ? kit.section({ title: 'Popular comparisons', body: `<ul class="ai-chips">${pairs.map(([a, b]) => `<li><a href="/ai/compare/${encodeURIComponent(a.id)}-vs-${encodeURIComponent(b.id)}">${esc(shortName(a.id))} vs ${esc(shortName(b.id))}</a></li>`).join('')}</ul>` }) : '')-- + (changed.length ? kit.section({ title: 'What changed this month', body: `<ul>${changed.slice(0, 8).map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` }) : '')-- + kit.section({ title: 'Questions', body: faq.map(([q, a]) => kit.fold({ title: q, body: `<p>${esc(a)}</p>` })).join('') })++ // A phone shows the parts below the top picks one at a time as tabs (the owner, 11-11: "self-contained tabs, so++ // the page isn't so freaking huge … at least on mobile"); every part stays in the HTML, a wide screen stacks them.++ + parts({ name: 'board', label: 'The leaderboard\'s parts', items: [++ { id: 'leaderboard', label: 'Leaderboard', count: rows.length, html: kit.section({ title: 'Leaderboard', count: rows.length, actions: [act('/ai/leaderboard', rows.length > 25 ? `All ${rows.length} models` : 'Filter and sort')], body: board(rows.slice(0, 25)) + (others ? `<p class="ai-fine">Chat models only. ${others} embedding and speech ${others === 1 ? 'model is' : 'models are'} in the <a href="/ai/leaderboard?type=all">full list</a>.</p>` : '') + dataLinks() }) },++ { id: 'answers', label: 'Answers', html: (arena && (arena.answers || []).length ? kit.section({ title: 'One prompt, many answers', body: `<p class="ai-prompt">${esc(arena.prompt || '')}</p><div class="ai-answers">${arena.answers.slice(0, 3).map((a) => `<article>${modelCell({ id: a.model || 'Model', maker: a.maker })}<p>${esc(a.text || '')}</p></article>`).join('')}</div>` }) : '') },++ { id: 'tasks', label: 'By task', count: tasks.length || '', html: (tasks.length ? kit.section({ title: 'Boards by task', body: `<ul class="ai-chips">${tasks.slice(0, 16).map((x) => `<li><a href="/ai/${encodeURIComponent(x.id)}">${esc(x.name || x.id)}</a></li>`).join('')}</ul>` }) : '') },++ { id: 'providers', label: 'Providers', count: measuredOffers.length || '', html: (measuredOffers.length ? kit.section({ title: 'Providers', actions: [act('/ai/providers', 'Every provider')], body: providerTable(measuredOffers.slice(0, 6)) }) : '') },++ { id: 'compare', label: 'Compare', html: (pairs.length ? kit.section({ title: 'Popular comparisons', body: `<ul class="ai-chips">${pairs.map(([a, b]) => `<li><a href="/ai/compare/${encodeURIComponent(a.id)}-vs-${encodeURIComponent(b.id)}">${esc(shortName(a.id))} vs ${esc(shortName(b.id))}</a></li>`).join('')}</ul>` }) : '') },++ { id: 'changes', label: 'Changes', html: (changed.length ? kit.section({ title: 'What changed this month', body: `<ul>${changed.slice(0, 8).map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` }) : '') },++ { id: 'questions', label: 'Questions', html: kit.section({ title: 'Questions', body: faq.map(([q, a]) => kit.fold({ title: q, body: `<p>${esc(a)}</p>` })).join('') }) },++ ] })+ `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map(([q, a]) => ({ '@type': 'Question', name: q, acceptedAnswer: { '@type': 'Answer', text: a } })) }).replace(/</g, '\\u003c')}</script>`+ `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'Dataset', name: 'OpenVibe AI model leaderboard', url: 'https://openvibe.report/ai', variableMeasured: ['Bench rating', 'Provider price', 'Provider reply time', 'Provider uptime'], distribution: { '@type': 'DataDownload', contentUrl: 'https://openvibe.report/api/v1/ai/leaderboard', encodingFormat: 'application/json' } }).replace(/</g, '\\u003c')}</script>`;return page(`AI model leaderboard — ${month}`, `${rows.length} AI models compared: ratings from blind votes, provider prices and measured speed.`, body, '/ai');@@@ -1,4 +1,5 @@'use strict';++const { pick } = require('openvibe-shared/showcase');/*** The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.*@@@ -21,13 +22,11 @@ const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</** pickBlock({ key, label, head, panes }) panes: [{ id, label, html }]*/--function pickBlock({ key, label, head = '', panes }) {-- // A pane with nothing to show is left out, so the chips and the rules that show them match the page.-- panes = (panes || []).filter((p) => p.html);-- if (!panes.length) return '';-- const chips = panes.map((p, i) => '<input type="radio" name="pick-' + esc(key) + '" id="pk-' + esc(key) + '-' + esc(p.id) + '" value="' + esc(p.id) + '"' + (i === 0 ? ' checked' : '') + '><label for="pk-' + esc(key) + '-' + esc(p.id) + '">' + esc(p.label) + '</label>').join('');-- const body = panes.map((p) => '<div class="pick-pane" id="' + esc(p.id) + '">' + 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>';++// 4.228.0: Shared's showcase.pick() draws the block (tabs on a phone, every pane in the page, a link to a pane opens++// it); this copy only jumped to a pane with chips, so a phone showed them all (the owner, 11-12). The ids are this++// site's own, so its links keep working.++function pickBlock({ key, label, head = '', headId = '', bodyClass = '', panes, checked = '' }) {++ return pick({ key, label, head, headId, bodyClass, checked: checked ? checked : '', panes: (panes || []).map((p) => ({ id: p.id, label: p.label, html: p.html })) });}/** The rule that shows the picked block of a pick block: one per block, so the CSS is written for the page. */@@@ -42,6 +42,11 @@ const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot')assert.strictEqual(r.status, 200);for (const s of ['AI model leaderboard', 'openai/gpt-4o', 'Best overall', 'Dataset', 'FAQPage']) assert.ok(r.text.includes(s), s);assert.ok(!r.text.includes('Page <design>'));++ // A phone shows the parts below the top picks as tabs, the leaderboard first; every part stays in the HTML.++ assert.ok(r.text.includes('<div class="ov-parts" data-parts='), 'the parts are tabs on a phone');++ assert.ok(r.text.includes('<label for="ovp-board-leaderboard">Leaderboard <span class="ov-parts-n">'), 'the leaderboard tab names its count');++ assert.ok(r.text.indexOf('ovp-board-leaderboard') < r.text.indexOf('ovp-board-questions'), 'the leaderboard first, the questions last');++ assert.ok(r.text.includes('.ov-parts-bar{display:flex}'), 'its look is in the page, so nothing jumps');});await check('Shared Elo shows ranks, intervals and win rates', async () => {@@@ -46,7 +46,7 @@ const { boot, check, done } = require('./helpers/boot');for (const rel of ['css/app.css', 'js/app.js']) {const plain = await healthy.get(`/${rel}`);assert.strictEqual(plain.status, 200);-- assert.strictEqual(plain.headers.get('cache-control'), 'public, max-age=300, stale-while-revalidate=86400');++ assert.strictEqual(plain.headers.get('cache-control'), 'public, max-age=300, stale-while-revalidate=300');const pinned = await healthy.get(`/${rel}?v=${assetVersion(rel)}`);assert.strictEqual(pinned.status, 200);assert.strictEqual(pinned.headers.get('cache-control'), 'public, max-age=31536000, immutable');
@@ -262,7 +262,7 @@ HMAC. See [SECURITY.md](SECURITY.md).
@@ -262,7 +262,7 @@ HMAC. See [SECURITY.md](SECURITY.md).
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.417.0
- openvibe-contracts: v0.417.0
- openvibe-sdk: v0.161.0
- openvibe-sdk: v0.161.0
-- openvibe-shared: v4.225.0
+- openvibe-shared: v4.228.0
- openvibe-publishing: v1.7.0
- openvibe-publishing: v1.7.0
<!-- versions:end -->
<!-- versions:end -->
@@ -10,12 +10,12 @@
@@ -10,12 +10,12 @@
"domain": "openvibe.report",
"domain": "openvibe.report",
"contracts": "openvibe-contracts v0.417.0",
"contracts": "openvibe-contracts v0.417.0",
"sdk": "openvibe-sdk v0.161.0",
"sdk": "openvibe-sdk v0.161.0",
- "shared": "openvibe-shared v4.225.0",
+ "shared": "openvibe-shared v4.228.0",
"packages": [
"packages": [
"openvibe-contracts v0.417.0",
"openvibe-contracts v0.417.0",
"openvibe-publishing v1.7.0",
"openvibe-publishing v1.7.0",
"openvibe-sdk v0.161.0",
"openvibe-sdk v0.161.0",
- "openvibe-shared v4.225.0",
+ "openvibe-shared v4.228.0",
"pg v8.23.1",
"pg v8.23.1",
"pglite v0.5.13 (dev)"
"pglite v0.5.13 (dev)"
],
],
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.417.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.417.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.225.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.228.0",
"pg": "^8.23.1"
"pg": "^8.23.1"
},
},
"devDependencies": {
"devDependencies": {
@@ -1200,9 +1200,9 @@
@@ -1200,9 +1200,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.225.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.225.0",
- "integrity": "sha512-zuZIgYODwJJ7UpDBTVnT+czEI5l6YFWqt1nbfbPeEsW6/NlmDZHJWPSGpBv223jRpvhJYH7Duea6nuRr8KZ3sg==",
+ "version": "4.228.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.228.0",
+ "integrity": "sha512-HR0Hac/K+qKmwMf2QZWCFxW+uVja36nyAtxVbNFgPGUDFdmWhmb6RMoP/CkU0+GSCPsC50k6aTS+oPx2czDgGg==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"clean-css": "^5.3.3",
"clean-css": "^5.3.3",
@@ -22,7 +22,7 @@
@@ -22,7 +22,7 @@
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.417.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.417.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.225.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.228.0",
"pg": "^8.23.1"
"pg": "^8.23.1"
},
},
"engines": {
"engines": {
@@ -8,6 +8,7 @@
@@ -8,6 +8,7 @@
// - speed and uptime are measured on the Exchange's real traffic, shown only where traffic went.
// - speed and uptime are measured on the Exchange's real traffic, shown only where traffic went.
// A column or section earns its place: it shows when some row has a value, and a missing value reads in words.
// A column or section earns its place: it shows when some row has a value, and a missing value reads in words.
const kit = require('openvibe-shared/kit');
const kit = require('openvibe-shared/kit');
+const { parts, PARTS_CSS } = require('openvibe-shared/showcase');
const marks = require('openvibe-shared/ai-marks');
const marks = require('openvibe-shared/ai-marks');
const { asset } = require('../assets');
const { asset } = require('../assets');
const { layout } = require('./index');
const { layout } = require('./index');
@@ -110,7 +111,7 @@ function board(rows, { full = false } = {}) {
@@ -110,7 +111,7 @@ function board(rows, { full = false } = {}) {
}
}
function page(title, description, body, path) {
function page(title, description, body, path) {
- return layout({ title, description, body: `<link rel="stylesheet" href="${asset('css/ai-comparison.css')}"><style>${marks.CSS}</style><div class="ai-page">${body}</div>`, active: '/ai', path });
+ return layout({ title, description, body: `<link rel="stylesheet" href="${asset('css/ai-comparison.css')}"><style>${marks.CSS}\n${PARTS_CSS}</style><div class="ai-page">${body}</div>`, active: '/ai', path });
}
}
const dataLinks = () => `<p class="ai-data">Data: <a href="/api/v1/ai/leaderboard">JSON</a> · <a href="/api/v1/ai/leaderboard?format=csv&limit=100">CSV</a> · <a href="/ai/method">How it is measured</a></p>`;
const dataLinks = () => `<p class="ai-data">Data: <a href="/api/v1/ai/leaderboard">JSON</a> · <a href="/api/v1/ai/leaderboard?format=csv&limit=100">CSV</a> · <a href="/ai/method">How it is measured</a></p>`;
@@ -146,13 +147,17 @@ function hub({ rows = [], picks = [], providers = [], others = 0, month = new Da
@@ -146,13 +147,17 @@ function hub({ rows = [], picks = [], providers = [], others = 0, month = new Da
+ kit.stats([[ratedRows.length, 'rated'], [rows.length, 'models listed'], [providerNames.size, 'providers'], [measuredOffers.length, 'measured endpoints']], { label: 'The leaderboard in numbers' })
+ kit.stats([[ratedRows.length, 'rated'], [rows.length, 'models listed'], [providerNames.size, 'providers'], [measuredOffers.length, 'measured endpoints']], { label: 'The leaderboard in numbers' })
+ `</header>`
+ `</header>`
+ (pickList || pending.length ? kit.section({ title: 'Top picks', body: `${pickList ? `<ul class="ai-picks">${pickList}</ul>` : ''}${pending.length ? `<p class="ai-fine">Not enough data yet for ${esc(pending.join(', '))}.</p>` : ''}` }) : '')
+ (pickList || pending.length ? kit.section({ title: 'Top picks', body: `${pickList ? `<ul class="ai-picks">${pickList}</ul>` : ''}${pending.length ? `<p class="ai-fine">Not enough data yet for ${esc(pending.join(', '))}.</p>` : ''}` }) : '')
- + kit.section({ title: 'Leaderboard', count: rows.length, actions: [act('/ai/leaderboard', rows.length > 25 ? `All ${rows.length} models` : 'Filter and sort')], body: board(rows.slice(0, 25)) + (others ? `<p class="ai-fine">Chat models only. ${others} embedding and speech ${others === 1 ? 'model is' : 'models are'} in the <a href="/ai/leaderboard?type=all">full list</a>.</p>` : '') + dataLinks() })
- + (arena && (arena.answers || []).length ? kit.section({ title: 'One prompt, many answers', body: `<p class="ai-prompt">${esc(arena.prompt || '')}</p><div class="ai-answers">${arena.answers.slice(0, 3).map((a) => `<article>${modelCell({ id: a.model || 'Model', maker: a.maker })}<p>${esc(a.text || '')}</p></article>`).join('')}</div>` }) : '')
- + (tasks.length ? kit.section({ title: 'Boards by task', body: `<ul class="ai-chips">${tasks.slice(0, 16).map((x) => `<li><a href="/ai/${encodeURIComponent(x.id)}">${esc(x.name || x.id)}</a></li>`).join('')}</ul>` }) : '')
- + (measuredOffers.length ? kit.section({ title: 'Providers', actions: [act('/ai/providers', 'Every provider')], body: providerTable(measuredOffers.slice(0, 6)) }) : '')
- + (pairs.length ? kit.section({ title: 'Popular comparisons', body: `<ul class="ai-chips">${pairs.map(([a, b]) => `<li><a href="/ai/compare/${encodeURIComponent(a.id)}-vs-${encodeURIComponent(b.id)}">${esc(shortName(a.id))} vs ${esc(shortName(b.id))}</a></li>`).join('')}</ul>` }) : '')
- + (changed.length ? kit.section({ title: 'What changed this month', body: `<ul>${changed.slice(0, 8).map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` }) : '')
- + kit.section({ title: 'Questions', body: faq.map(([q, a]) => kit.fold({ title: q, body: `<p>${esc(a)}</p>` })).join('') })
+ // A phone shows the parts below the top picks one at a time as tabs (the owner, 11-11: "self-contained tabs, so
+ // the page isn't so freaking huge … at least on mobile"); every part stays in the HTML, a wide screen stacks them.
+ + parts({ name: 'board', label: 'The leaderboard\'s parts', items: [
+ { id: 'leaderboard', label: 'Leaderboard', count: rows.length, html: kit.section({ title: 'Leaderboard', count: rows.length, actions: [act('/ai/leaderboard', rows.length > 25 ? `All ${rows.length} models` : 'Filter and sort')], body: board(rows.slice(0, 25)) + (others ? `<p class="ai-fine">Chat models only. ${others} embedding and speech ${others === 1 ? 'model is' : 'models are'} in the <a href="/ai/leaderboard?type=all">full list</a>.</p>` : '') + dataLinks() }) },
+ { id: 'answers', label: 'Answers', html: (arena && (arena.answers || []).length ? kit.section({ title: 'One prompt, many answers', body: `<p class="ai-prompt">${esc(arena.prompt || '')}</p><div class="ai-answers">${arena.answers.slice(0, 3).map((a) => `<article>${modelCell({ id: a.model || 'Model', maker: a.maker })}<p>${esc(a.text || '')}</p></article>`).join('')}</div>` }) : '') },
+ { id: 'tasks', label: 'By task', count: tasks.length || '', html: (tasks.length ? kit.section({ title: 'Boards by task', body: `<ul class="ai-chips">${tasks.slice(0, 16).map((x) => `<li><a href="/ai/${encodeURIComponent(x.id)}">${esc(x.name || x.id)}</a></li>`).join('')}</ul>` }) : '') },
+ { id: 'providers', label: 'Providers', count: measuredOffers.length || '', html: (measuredOffers.length ? kit.section({ title: 'Providers', actions: [act('/ai/providers', 'Every provider')], body: providerTable(measuredOffers.slice(0, 6)) }) : '') },
+ { id: 'compare', label: 'Compare', html: (pairs.length ? kit.section({ title: 'Popular comparisons', body: `<ul class="ai-chips">${pairs.map(([a, b]) => `<li><a href="/ai/compare/${encodeURIComponent(a.id)}-vs-${encodeURIComponent(b.id)}">${esc(shortName(a.id))} vs ${esc(shortName(b.id))}</a></li>`).join('')}</ul>` }) : '') },
+ { id: 'changes', label: 'Changes', html: (changed.length ? kit.section({ title: 'What changed this month', body: `<ul>${changed.slice(0, 8).map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` }) : '') },
+ { id: 'questions', label: 'Questions', html: kit.section({ title: 'Questions', body: faq.map(([q, a]) => kit.fold({ title: q, body: `<p>${esc(a)}</p>` })).join('') }) },
+ ] })
+ `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map(([q, a]) => ({ '@type': 'Question', name: q, acceptedAnswer: { '@type': 'Answer', text: a } })) }).replace(/</g, '\\u003c')}</script>`
+ `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map(([q, a]) => ({ '@type': 'Question', name: q, acceptedAnswer: { '@type': 'Answer', text: a } })) }).replace(/</g, '\\u003c')}</script>`
+ `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'Dataset', name: 'OpenVibe AI model leaderboard', url: 'https://openvibe.report/ai', variableMeasured: ['Bench rating', 'Provider price', 'Provider reply time', 'Provider uptime'], distribution: { '@type': 'DataDownload', contentUrl: 'https://openvibe.report/api/v1/ai/leaderboard', encodingFormat: 'application/json' } }).replace(/</g, '\\u003c')}</script>`;
+ `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'Dataset', name: 'OpenVibe AI model leaderboard', url: 'https://openvibe.report/ai', variableMeasured: ['Bench rating', 'Provider price', 'Provider reply time', 'Provider uptime'], distribution: { '@type': 'DataDownload', contentUrl: 'https://openvibe.report/api/v1/ai/leaderboard', encodingFormat: 'application/json' } }).replace(/</g, '\\u003c')}</script>`;
return page(`AI model leaderboard — ${month}`, `${rows.length} AI models compared: ratings from blind votes, provider prices and measured speed.`, body, '/ai');
return page(`AI model leaderboard — ${month}`, `${rows.length} AI models compared: ratings from blind votes, provider prices and measured speed.`, body, '/ai');
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
'use strict';
'use strict';
+const { pick } = require('openvibe-shared/showcase');
/**
/**
* The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.
* The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.
*
*
@@ -21,13 +22,11 @@ const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</
@@ -21,13 +22,11 @@ const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</
*
*
* pickBlock({ key, label, head, panes }) panes: [{ id, label, html }]
* pickBlock({ key, label, head, panes }) panes: [{ id, label, html }]
*/
*/
-function pickBlock({ key, label, head = '', panes }) {
- // A pane with nothing to show is left out, so the chips and the rules that show them match the page.
- panes = (panes || []).filter((p) => p.html);
- if (!panes.length) return '';
- const chips = panes.map((p, i) => '<input type="radio" name="pick-' + esc(key) + '" id="pk-' + esc(key) + '-' + esc(p.id) + '" value="' + esc(p.id) + '"' + (i === 0 ? ' checked' : '') + '><label for="pk-' + esc(key) + '-' + esc(p.id) + '">' + esc(p.label) + '</label>').join('');
- const body = panes.map((p) => '<div class="pick-pane" id="' + esc(p.id) + '">' + 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>';
+// 4.228.0: Shared's showcase.pick() draws the block (tabs on a phone, every pane in the page, a link to a pane opens
+// it); this copy only jumped to a pane with chips, so a phone showed them all (the owner, 11-12). The ids are this
+// site's own, so its links keep working.
+function pickBlock({ key, label, head = '', headId = '', bodyClass = '', panes, checked = '' }) {
+ return pick({ key, label, head, headId, bodyClass, checked: checked ? checked : '', panes: (panes || []).map((p) => ({ id: p.id, label: p.label, html: p.html })) });
}
}
/** The rule that shows the picked block of a pick block: one per block, so the CSS is written for the page. */
/** The rule that shows the picked block of a pick block: one per block, so the CSS is written for the page. */
@@ -42,6 +42,11 @@ const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot')
@@ -42,6 +42,11 @@ const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot')
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
for (const s of ['AI model leaderboard', 'openai/gpt-4o', 'Best overall', 'Dataset', 'FAQPage']) assert.ok(r.text.includes(s), s);
for (const s of ['AI model leaderboard', 'openai/gpt-4o', 'Best overall', 'Dataset', 'FAQPage']) assert.ok(r.text.includes(s), s);
assert.ok(!r.text.includes('Page <design>'));
assert.ok(!r.text.includes('Page <design>'));
+ // A phone shows the parts below the top picks as tabs, the leaderboard first; every part stays in the HTML.
+ assert.ok(r.text.includes('<div class="ov-parts" data-parts='), 'the parts are tabs on a phone');
+ assert.ok(r.text.includes('<label for="ovp-board-leaderboard">Leaderboard <span class="ov-parts-n">'), 'the leaderboard tab names its count');
+ assert.ok(r.text.indexOf('ovp-board-leaderboard') < r.text.indexOf('ovp-board-questions'), 'the leaderboard first, the questions last');
+ assert.ok(r.text.includes('.ov-parts-bar{display:flex}'), 'its look is in the page, so nothing jumps');
});
});
await check('Shared Elo shows ranks, intervals and win rates', async () => {
await check('Shared Elo shows ranks, intervals and win rates', async () => {
@@ -46,7 +46,7 @@ const { boot, check, done } = require('./helpers/boot');
@@ -46,7 +46,7 @@ const { boot, check, done } = require('./helpers/boot');
for (const rel of ['css/app.css', 'js/app.js']) {
for (const rel of ['css/app.css', 'js/app.js']) {
const plain = await healthy.get(`/${rel}`);
const plain = await healthy.get(`/${rel}`);
assert.strictEqual(plain.status, 200);
assert.strictEqual(plain.status, 200);
- assert.strictEqual(plain.headers.get('cache-control'), 'public, max-age=300, stale-while-revalidate=86400');
+ assert.strictEqual(plain.headers.get('cache-control'), 'public, max-age=300, stale-while-revalidate=300');
const pinned = await healthy.get(`/${rel}?v=${assetVersion(rel)}`);
const pinned = await healthy.get(`/${rel}?v=${assetVersion(rel)}`);
assert.strictEqual(pinned.status, 200);
assert.strictEqual(pinned.status, 200);
assert.strictEqual(pinned.headers.get('cache-control'), 'public, max-age=31536000, immutable');
assert.strictEqual(pinned.headers.get('cache-control'), 'public, max-age=31536000, immutable');