openvibers/openvibe.bench Commits
Ratings as a ranking table, not a stack of cards (#34)
Changes
@@@ -318,22 +318,43 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex.bt-reveal-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }}--/* The ratings board (B9): raw and style controlled, side by side, with the intervals and the bands. */++/* The ratings board (B9): a ranking table, one scale for every interval, two lines on a phone. */.rt-board { max-width: 1180px; margin: 0 auto; padding: clamp(20px, 4vw, 40px) 16px 64px; }.rt-board > h1 { margin: 0; font-size: clamp(1.8rem, 5vw, 2.8rem); font-weight: 800; letter-spacing: -.03em; line-height: 1.08; }--.rt-list { list-style: none; margin: 20px 0 0; padding: 0; display: grid; gap: 10px; }--.rt-row { display: grid; gap: 10px; padding: 14px; border-radius: 18px; background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); min-width: 0; }--.rt-cell { display: grid; gap: 2px; min-width: 0; align-content: start; }--.rt-label { font-size: .68rem; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted, #7386a3); }--.rt-model { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-weight: 700; overflow-wrap: anywhere; }--.rt-rating { font-size: 1.15rem; font-weight: 800; font-variant-numeric: tabular-nums; }--.rt-ci { color: var(--text-muted, #7386a3); font-size: .82rem; font-variant-numeric: tabular-nums; }--.rt-band { display: inline-block; margin-top: 4px; padding: 2px 10px; border-radius: 999px; background: color-mix(in srgb, var(--room-accent) 14%, transparent); color: var(--room-strong); font-size: .78rem; font-weight: 750; justify-self: start; }--.rt-votes { font-weight: 700; font-variant-numeric: tabular-nums; }++.rt-wrap { overflow-x: visible; }++.rt-table { font-size: .92rem; }++.rt-table td { vertical-align: middle; }++.rt-col-band { width: 7.5em; }++.rt-col-model { width: 20%; }++.rt-col-controlled { width: 32%; }++.rt-col-raw { width: 26%; }++.rt-col-votes { width: 5em; }++.rt-model { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-weight: 700; overflow-wrap: break-word; }++.rt-score { font-weight: 800; font-variant-numeric: tabular-nums; }++.rt-score-muted { color: var(--text-secondary, #96a7c2); font-weight: 650; }++.rt-num { text-align: right; font-variant-numeric: tabular-nums; }++.rt-band { display: inline-block; padding: 2px 10px; border-radius: 999px; background: color-mix(in srgb, var(--room-accent) 14%, transparent); color: var(--room-strong); font-size: .78rem; font-weight: 750; white-space: nowrap; }++.rt-bar { display: block; width: 100%; height: 8px; margin-top: 6px; }++.rt-bar-track { fill: color-mix(in srgb, var(--text-muted, #7386a3) 24%, transparent); }++.rt-bar-range { fill: color-mix(in srgb, var(--room-accent) 70%, transparent); }++.rt-bar-point { fill: var(--room-strong); }++.rt-col-raw .rt-bar-range { fill: color-mix(in srgb, var(--text-muted, #7386a3) 45%, transparent); }++.rt-col-raw .rt-bar-point { fill: var(--text-secondary, #96a7c2); }++.rt-style-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }++.rt-style-list li { padding: 12px 14px; border-radius: 14px; background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); line-height: 1.5; }.rt-effects, .rt-data { margin-top: clamp(24px, 4vw, 36px); }.rt-effects h2, .rt-data h2 { margin: 0 0 10px; font-size: 1.2rem; font-weight: 800; letter-spacing: -.01em; }.rt-data code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-weight: 700; color: var(--room-strong); overflow-wrap: anywhere; }--@media (min-width: 900px) {-- .rt-row { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr) auto; align-items: center; }++@media (max-width: 560px) {++ .rt-table, .rt-table tbody { display: block; width: 100%; }++ .rt-table thead { display: none; }++ .rt-table tr { display: grid; grid-template-columns: auto 1fr auto; gap: 6px 10px; padding: 12px 14px; border-bottom: 1px solid var(--border, #1f2d47); }++ .rt-table tbody tr:last-child { border-bottom: 0; }++ .rt-table td { display: block; padding: 0; border: 0; min-width: 0; width: auto; }++ .rt-col-band { grid-area: 1 / 1 / 2 / 2; }++ .rt-col-model { grid-area: 1 / 2 / 2 / 4; }++ .rt-col-controlled { grid-area: 2 / 1 / 3 / 2; }++ .rt-col-raw { grid-area: 2 / 2 / 3 / 3; }++ .rt-col-votes { grid-area: 2 / 3 / 3 / 4; text-align: right; }++ .rt-bar { height: 6px; margin-top: 4px; }}--@@@ -7,6 +7,10 @@* rating carries a bootstrap interval. Models whose intervals overlap share a band, so the page says "2nd-4th"* instead of pretending the third place is a fact. The open dump next to it carries every vote without voter ids,* and the fit can be re-derived from that file alone.++ *++ * The board is a table, like B2's rankings: a band, a model, the style-controlled score with its interval on one++ * scale (so an overlap shows at a glance), the raw score muted, and the vote count. On a phone each row folds++ * into two lines.*/const express = require('express');@@@ -14,42 +18,100 @@ const { esc, raw } = require('../render/html');const { send } = require('../render/layout');const { board, boardView } = require('../rating/board');--function interval(cell) {-- if (!cell) return '';-- const lo = cell.ci_lower == null ? '' : Math.round(cell.ci_lower);-- const hi = cell.ci_upper == null ? '' : Math.round(cell.ci_upper);-- return '<span class="rt-rating">' + esc(Math.round(cell.rating)) + '</span>'-- + '<span class="rt-ci">' + esc(lo + '–' + hi) + '</span>';++const num = (v, d = 0) => (v == null || Number.isNaN(Number(v)) ? d : Number(v));++++/** The rounded score and its interval, as text. */++function scoreText(cell) {++ if (!cell) return '—';++ const lo = cell.ci_lower == null ? null : Math.round(cell.ci_lower);++ const hi = cell.ci_upper == null ? null : Math.round(cell.ci_upper);++ return Math.round(cell.rating) + (lo == null ? '' : ' (' + lo + '–' + hi + ')');++}++++/**++ * The interval bar: one scale for the whole board (its lowest lower bound to its highest upper bound), so two++ * models that overlap show it at a glance. The numbers stay text next to it.++ */++function bar(cell, scale) {++ if (!cell || !scale || scale.max <= scale.min) return '';++ const at = (v) => Math.max(0, Math.min(100, ((v - scale.min) / (scale.max - scale.min)) * 100));++ const lo = at(cell.ci_lower == null ? cell.rating : cell.ci_lower);++ const hi = at(cell.ci_upper == null ? cell.rating : cell.ci_upper);++ const point = at(cell.rating);++ const width = Math.max(0.6, hi - lo);++ return '<svg class="rt-bar" viewBox="0 0 100 8" preserveAspectRatio="none" aria-hidden="true" focusable="false">'++ + '<rect class="rt-bar-track" x="0" y="3" width="100" height="2" rx="1"></rect>'++ + '<rect class="rt-bar-range" x="' + lo.toFixed(2) + '" y="2.6" width="' + width.toFixed(2) + '" height="2.8" rx="1.4"></rect>'++ + '<rect class="rt-bar-point" x="' + (point - 0.7).toFixed(2) + '" y="0.6" width="1.4" height="6.8" rx="0.7"></rect>'++ + '</svg>';++}++++/** One plain sentence per covariate: what the style did, in words, rounded. */++function styleSentence(key, effect) {++ const points = (v) => Math.round(Math.abs(num(v)));++ if (key === 'length') {++ const n = points(effect.elo_per_1000_chars);++ if (!n) return 'The length of an output made no difference to the votes.';++ return effect.elo_per_1000_chars < 0++ ? 'Longer outputs lost about ' + n + ' points per 1,000 characters here.'++ : 'Longer outputs won about ' + n + ' points per 1,000 characters here.';++ }++ if (key === 'markup') {++ const n = points(effect.elo_per_10_marks);++ if (!n) return 'How much markup an output wore made no difference to the votes.';++ return effect.elo_per_10_marks < 0++ ? 'More markup lost about ' + n + ' points per 10 markup marks here.'++ : 'More markup won about ' + n + ' points per 10 markup marks here.';++ }++ return '';}function rows(view) {if (!view.models.length) {-- return '<div class="bm-empty"><h1>No votes yet</h1><p>The board fills up as people vote in the battles.</p></div>';++ return '<div class="bm-empty"><h1>No battles voted yet</h1>'++ + '<p>The board fills up as people vote in the battles.</p>'++ + '<p class="bm-actions"><a class="bm-btn" href="/battle">Vote in a battle</a></p></div>';++ }++ // The board is ordered by the style-controlled score: the honest one.++ const models = [...view.models].sort((a, b) => b.controlled.rating - a.controlled.rating || a.model.localeCompare(b.model));++ let min = Infinity;++ let max = -Infinity;++ for (const m of models) {++ for (const cell of [m.raw, m.controlled]) {++ if (!cell) continue;++ min = Math.min(min, num(cell.ci_lower, cell.rating));++ max = Math.max(max, num(cell.ci_upper, cell.rating));++ }}-- const body = view.models.map((m) => '<li class="rt-row" data-model="' + esc(m.model) + '">'-- + '<div class="rt-cell rt-cell-model"><span class="rt-label">Model</span>'-- + '<span class="rt-model">' + esc(m.model) + '</span></div>'-- + '<div class="rt-cell"><span class="rt-label">Raw</span>' + interval(m.raw)-- + '<span class="rt-band">' + esc(m.raw.band || '') + '</span></div>'-- + '<div class="rt-cell"><span class="rt-label">Style controlled</span>' + interval(m.controlled)-- + '<span class="rt-band">' + esc(m.controlled.band || '') + '</span></div>'-- + '<div class="rt-cell"><span class="rt-label">Votes</span><span class="rt-votes">' + esc(m.votes) + '</span></div>'-- + '</li>').join('');-- return '<ul class="rt-list">' + body + '</ul>';++ const scale = { min, max };++ const body = models.map((m) => '<tr data-model="' + esc(m.model) + '">'++ + '<td class="rt-col-band"><span class="rt-band">' + esc(m.controlled.band || m.raw.band || '') + '</span></td>'++ + '<td class="rt-col-model"><span class="rt-model">' + esc(m.model) + '</span></td>'++ + '<td class="rt-col-controlled"><span class="rt-score">' + esc(scoreText(m.controlled)) + '</span>'++ + bar(m.controlled, scale) + '</td>'++ + '<td class="rt-col-raw"><span class="rt-score rt-score-muted">' + esc(scoreText(m.raw)) + '</span>'++ + bar(m.raw, scale) + '</td>'++ + '<td class="rt-col-votes rt-num">' + esc(m.votes) + '</td>'++ + '</tr>').join('');++ return '<div class="br-table-wrap rt-wrap"><table class="br-table rt-table">'++ + '<caption class="br-sr">Models in rank bands, with the style-controlled score and its interval, the raw score and its interval, and the number of votes</caption>'++ + '<thead><tr><th scope="col">Band</th><th scope="col">Model</th><th scope="col">Style controlled</th>'++ + '<th scope="col">Raw</th><th scope="col" class="br-num">Votes</th></tr></thead>'++ + '<tbody>' + body + '</tbody></table></div>';}function effects(view) {-- const list = Object.entries(view.effects || {});-- if (!list.length) return '';-- const items = list.map(([key, e]) => '<div><dt>' + esc(key) + '</dt><dd>'-- + (e.elo_per_1000_chars != null ? esc(e.elo_per_1000_chars) + ' Elo per 1000 characters' : '')-- + (e.elo_per_1000_chars != null && e.elo_per_10_marks != null ? '<br>' : '')-- + (e.elo_per_10_marks != null ? esc(e.elo_per_10_marks) + ' Elo per 10 markup marks' : '')-- + '</dd></div>').join('');++ const list = Object.entries(view.effects || {}).map(([key, e]) => styleSentence(key, e)).filter(Boolean);++ const dropped = (view.dropped_covariates || []).map((key) => key === 'markup'++ ? 'Markup moved with the length of the outputs here, so it is not counted a second time.'++ : key + ' was not counted separately.');++ const sentences = list.concat(dropped);++ if (!sentences.length) return '';return '<section class="rt-effects"><h2>How much of the board is style</h2>'-- + '<dl class="br-facts">' + items + '</dl>'-- + '<p class="bm-fine">A positive number means the longer (or the more dressed up) output wins votes it did not '-- + 'earn. The controlled column removes that effect; the raw column is what people actually chose.</p>'++ + '<ul class="rt-style-list">' + sentences.map((s) => '<li>' + esc(s) + '</li>').join('') + '</ul>'++ + '<p class="bm-fine">The raw column is what people chose; the style-controlled column is what the models would '++ + 'score if every output were written the same way. A negative effect means the style lost votes it should not '++ + 'have lost.</p>'+ '</section>';}@@@ -59,11 +121,13 @@ function ratingsView(view) {+ '<p class="bm-lede">Every human vote in Bench is one game between two models. The board is fitted twice: on the '+ 'votes alone (raw) and with the style of each output as covariates (style controlled), so you can see how much '+ 'of a rank is length and formatting.</p>'-- + '<p class="bm-fine">' + esc(view.votes) + ' votes, '-- + esc(view.bootstrap) + ' bootstrap resamples, seed ' + esc(view.seed)-- + (view.voters && view.voters.downweighted ? ', ' + esc(view.voters.downweighted) + ' voters down-weighted (never named)' : '')-- + '</p>'-- + '<div class="rt-table-wrap">' + rows(view) + '</div>'++ + (view.votes++ ? '<p class="bm-fine">' + esc(view.votes) + ' votes, '++ + esc(view.bootstrap) + ' bootstrap resamples, seed ' + esc(view.seed)++ + (view.voters && view.voters.downweighted ? ', ' + esc(view.voters.downweighted) + ' voters down-weighted (never named)' : '')++ + '</p>'++ : '')++ + rows(view)+ effects(view)+ '<section class="rt-data"><h2>The open data</h2>'+ '<p class="bm-lede">Every vote is published as JSONL, without voter ids: '@@@ -96,6 +96,13 @@ const FORM = { 'content-type': 'application/x-www-form-urlencoded' };assert.strictEqual(page.status, 200, page.text);assert.ok(page.text.includes('Raw') && page.text.includes('Style controlled'), 'both columns');assert.ok(page.text.includes('alpha-'), 'a model on the board');++ // The board is a table: a band, a model, the scores with their intervals on one scale, and the votes.++ assert.ok(page.text.includes('class="br-table rt-table"'), 'the ranking table');++ assert.ok(page.text.includes('>Band<') && page.text.includes('>Votes<'), 'the columns');++ assert.ok(page.text.includes('class="rt-bar"'), 'the interval bar');++ assert.ok(page.text.includes('rt-band'), 'the band');++ // The style is explained in words, not as a number.++ assert.ok(!page.text.includes('Elo per 1000 characters'), 'no raw effect numbers: ' + page.text.slice(0, 200));const dump = await t.get('/api/v1/datasets/votes.jsonl');assert.strictEqual(dump.status, 200, dump.text);@@@ -83,6 +83,14 @@ const { boot, check, done } = require('./helpers/boot');assert.ok(one.entries[0].wrapper_url.includes('/render/w/'), 'the sandboxed frame');});++ await check('an empty board says so, and offers a battle', async () => {++ const page = await t.get('/ratings');++ assert.strictEqual(page.status, 200, page.text);++ assert.ok(page.text.includes('No battles voted yet'), 'the empty board: ' + page.text.slice(0, 200));++ assert.ok(page.text.includes('Vote in a battle'), 'the button');++ assert.ok(page.text.includes('href="/battle"'), 'it goes to the battle');++ });++await t.close();done();})().catch((e) => { console.error(e); process.exit(1); });
@@ -318,22 +318,43 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
@@ -318,22 +318,43 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
.bt-reveal-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.bt-reveal-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
}
-/* The ratings board (B9): raw and style controlled, side by side, with the intervals and the bands. */
+/* The ratings board (B9): a ranking table, one scale for every interval, two lines on a phone. */
.rt-board { max-width: 1180px; margin: 0 auto; padding: clamp(20px, 4vw, 40px) 16px 64px; }
.rt-board { max-width: 1180px; margin: 0 auto; padding: clamp(20px, 4vw, 40px) 16px 64px; }
.rt-board > h1 { margin: 0; font-size: clamp(1.8rem, 5vw, 2.8rem); font-weight: 800; letter-spacing: -.03em; line-height: 1.08; }
.rt-board > h1 { margin: 0; font-size: clamp(1.8rem, 5vw, 2.8rem); font-weight: 800; letter-spacing: -.03em; line-height: 1.08; }
-.rt-list { list-style: none; margin: 20px 0 0; padding: 0; display: grid; gap: 10px; }
-.rt-row { display: grid; gap: 10px; padding: 14px; border-radius: 18px; background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); min-width: 0; }
-.rt-cell { display: grid; gap: 2px; min-width: 0; align-content: start; }
-.rt-label { font-size: .68rem; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted, #7386a3); }
-.rt-model { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-weight: 700; overflow-wrap: anywhere; }
-.rt-rating { font-size: 1.15rem; font-weight: 800; font-variant-numeric: tabular-nums; }
-.rt-ci { color: var(--text-muted, #7386a3); font-size: .82rem; font-variant-numeric: tabular-nums; }
-.rt-band { display: inline-block; margin-top: 4px; padding: 2px 10px; border-radius: 999px; background: color-mix(in srgb, var(--room-accent) 14%, transparent); color: var(--room-strong); font-size: .78rem; font-weight: 750; justify-self: start; }
-.rt-votes { font-weight: 700; font-variant-numeric: tabular-nums; }
+.rt-wrap { overflow-x: visible; }
+.rt-table { font-size: .92rem; }
+.rt-table td { vertical-align: middle; }
+.rt-col-band { width: 7.5em; }
+.rt-col-model { width: 20%; }
+.rt-col-controlled { width: 32%; }
+.rt-col-raw { width: 26%; }
+.rt-col-votes { width: 5em; }
+.rt-model { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-weight: 700; overflow-wrap: break-word; }
+.rt-score { font-weight: 800; font-variant-numeric: tabular-nums; }
+.rt-score-muted { color: var(--text-secondary, #96a7c2); font-weight: 650; }
+.rt-num { text-align: right; font-variant-numeric: tabular-nums; }
+.rt-band { display: inline-block; padding: 2px 10px; border-radius: 999px; background: color-mix(in srgb, var(--room-accent) 14%, transparent); color: var(--room-strong); font-size: .78rem; font-weight: 750; white-space: nowrap; }
+.rt-bar { display: block; width: 100%; height: 8px; margin-top: 6px; }
+.rt-bar-track { fill: color-mix(in srgb, var(--text-muted, #7386a3) 24%, transparent); }
+.rt-bar-range { fill: color-mix(in srgb, var(--room-accent) 70%, transparent); }
+.rt-bar-point { fill: var(--room-strong); }
+.rt-col-raw .rt-bar-range { fill: color-mix(in srgb, var(--text-muted, #7386a3) 45%, transparent); }
+.rt-col-raw .rt-bar-point { fill: var(--text-secondary, #96a7c2); }
+.rt-style-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
+.rt-style-list li { padding: 12px 14px; border-radius: 14px; background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); line-height: 1.5; }
.rt-effects, .rt-data { margin-top: clamp(24px, 4vw, 36px); }
.rt-effects, .rt-data { margin-top: clamp(24px, 4vw, 36px); }
.rt-effects h2, .rt-data h2 { margin: 0 0 10px; font-size: 1.2rem; font-weight: 800; letter-spacing: -.01em; }
.rt-effects h2, .rt-data h2 { margin: 0 0 10px; font-size: 1.2rem; font-weight: 800; letter-spacing: -.01em; }
.rt-data code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-weight: 700; color: var(--room-strong); overflow-wrap: anywhere; }
.rt-data code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-weight: 700; color: var(--room-strong); overflow-wrap: anywhere; }
-@media (min-width: 900px) {
- .rt-row { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr) auto; align-items: center; }
+@media (max-width: 560px) {
+ .rt-table, .rt-table tbody { display: block; width: 100%; }
+ .rt-table thead { display: none; }
+ .rt-table tr { display: grid; grid-template-columns: auto 1fr auto; gap: 6px 10px; padding: 12px 14px; border-bottom: 1px solid var(--border, #1f2d47); }
+ .rt-table tbody tr:last-child { border-bottom: 0; }
+ .rt-table td { display: block; padding: 0; border: 0; min-width: 0; width: auto; }
+ .rt-col-band { grid-area: 1 / 1 / 2 / 2; }
+ .rt-col-model { grid-area: 1 / 2 / 2 / 4; }
+ .rt-col-controlled { grid-area: 2 / 1 / 3 / 2; }
+ .rt-col-raw { grid-area: 2 / 2 / 3 / 3; }
+ .rt-col-votes { grid-area: 2 / 3 / 3 / 4; text-align: right; }
+ .rt-bar { height: 6px; margin-top: 4px; }
}
}
-
@@ -7,6 +7,10 @@
@@ -7,6 +7,10 @@
* rating carries a bootstrap interval. Models whose intervals overlap share a band, so the page says "2nd-4th"
* rating carries a bootstrap interval. Models whose intervals overlap share a band, so the page says "2nd-4th"
* instead of pretending the third place is a fact. The open dump next to it carries every vote without voter ids,
* instead of pretending the third place is a fact. The open dump next to it carries every vote without voter ids,
* and the fit can be re-derived from that file alone.
* and the fit can be re-derived from that file alone.
+ *
+ * The board is a table, like B2's rankings: a band, a model, the style-controlled score with its interval on one
+ * scale (so an overlap shows at a glance), the raw score muted, and the vote count. On a phone each row folds
+ * into two lines.
*/
*/
const express = require('express');
const express = require('express');
@@ -14,42 +18,100 @@ const { esc, raw } = require('../render/html');
@@ -14,42 +18,100 @@ const { esc, raw } = require('../render/html');
const { send } = require('../render/layout');
const { send } = require('../render/layout');
const { board, boardView } = require('../rating/board');
const { board, boardView } = require('../rating/board');
-function interval(cell) {
- if (!cell) return '';
- const lo = cell.ci_lower == null ? '' : Math.round(cell.ci_lower);
- const hi = cell.ci_upper == null ? '' : Math.round(cell.ci_upper);
- return '<span class="rt-rating">' + esc(Math.round(cell.rating)) + '</span>'
- + '<span class="rt-ci">' + esc(lo + '–' + hi) + '</span>';
+const num = (v, d = 0) => (v == null || Number.isNaN(Number(v)) ? d : Number(v));
+
+/** The rounded score and its interval, as text. */
+function scoreText(cell) {
+ if (!cell) return '—';
+ const lo = cell.ci_lower == null ? null : Math.round(cell.ci_lower);
+ const hi = cell.ci_upper == null ? null : Math.round(cell.ci_upper);
+ return Math.round(cell.rating) + (lo == null ? '' : ' (' + lo + '–' + hi + ')');
+}
+
+/**
+ * The interval bar: one scale for the whole board (its lowest lower bound to its highest upper bound), so two
+ * models that overlap show it at a glance. The numbers stay text next to it.
+ */
+function bar(cell, scale) {
+ if (!cell || !scale || scale.max <= scale.min) return '';
+ const at = (v) => Math.max(0, Math.min(100, ((v - scale.min) / (scale.max - scale.min)) * 100));
+ const lo = at(cell.ci_lower == null ? cell.rating : cell.ci_lower);
+ const hi = at(cell.ci_upper == null ? cell.rating : cell.ci_upper);
+ const point = at(cell.rating);
+ const width = Math.max(0.6, hi - lo);
+ return '<svg class="rt-bar" viewBox="0 0 100 8" preserveAspectRatio="none" aria-hidden="true" focusable="false">'
+ + '<rect class="rt-bar-track" x="0" y="3" width="100" height="2" rx="1"></rect>'
+ + '<rect class="rt-bar-range" x="' + lo.toFixed(2) + '" y="2.6" width="' + width.toFixed(2) + '" height="2.8" rx="1.4"></rect>'
+ + '<rect class="rt-bar-point" x="' + (point - 0.7).toFixed(2) + '" y="0.6" width="1.4" height="6.8" rx="0.7"></rect>'
+ + '</svg>';
+}
+
+/** One plain sentence per covariate: what the style did, in words, rounded. */
+function styleSentence(key, effect) {
+ const points = (v) => Math.round(Math.abs(num(v)));
+ if (key === 'length') {
+ const n = points(effect.elo_per_1000_chars);
+ if (!n) return 'The length of an output made no difference to the votes.';
+ return effect.elo_per_1000_chars < 0
+ ? 'Longer outputs lost about ' + n + ' points per 1,000 characters here.'
+ : 'Longer outputs won about ' + n + ' points per 1,000 characters here.';
+ }
+ if (key === 'markup') {
+ const n = points(effect.elo_per_10_marks);
+ if (!n) return 'How much markup an output wore made no difference to the votes.';
+ return effect.elo_per_10_marks < 0
+ ? 'More markup lost about ' + n + ' points per 10 markup marks here.'
+ : 'More markup won about ' + n + ' points per 10 markup marks here.';
+ }
+ return '';
}
}
function rows(view) {
function rows(view) {
if (!view.models.length) {
if (!view.models.length) {
- return '<div class="bm-empty"><h1>No votes yet</h1><p>The board fills up as people vote in the battles.</p></div>';
+ return '<div class="bm-empty"><h1>No battles voted yet</h1>'
+ + '<p>The board fills up as people vote in the battles.</p>'
+ + '<p class="bm-actions"><a class="bm-btn" href="/battle">Vote in a battle</a></p></div>';
+ }
+ // The board is ordered by the style-controlled score: the honest one.
+ const models = [...view.models].sort((a, b) => b.controlled.rating - a.controlled.rating || a.model.localeCompare(b.model));
+ let min = Infinity;
+ let max = -Infinity;
+ for (const m of models) {
+ for (const cell of [m.raw, m.controlled]) {
+ if (!cell) continue;
+ min = Math.min(min, num(cell.ci_lower, cell.rating));
+ max = Math.max(max, num(cell.ci_upper, cell.rating));
+ }
}
}
- const body = view.models.map((m) => '<li class="rt-row" data-model="' + esc(m.model) + '">'
- + '<div class="rt-cell rt-cell-model"><span class="rt-label">Model</span>'
- + '<span class="rt-model">' + esc(m.model) + '</span></div>'
- + '<div class="rt-cell"><span class="rt-label">Raw</span>' + interval(m.raw)
- + '<span class="rt-band">' + esc(m.raw.band || '') + '</span></div>'
- + '<div class="rt-cell"><span class="rt-label">Style controlled</span>' + interval(m.controlled)
- + '<span class="rt-band">' + esc(m.controlled.band || '') + '</span></div>'
- + '<div class="rt-cell"><span class="rt-label">Votes</span><span class="rt-votes">' + esc(m.votes) + '</span></div>'
- + '</li>').join('');
- return '<ul class="rt-list">' + body + '</ul>';
+ const scale = { min, max };
+ const body = models.map((m) => '<tr data-model="' + esc(m.model) + '">'
+ + '<td class="rt-col-band"><span class="rt-band">' + esc(m.controlled.band || m.raw.band || '') + '</span></td>'
+ + '<td class="rt-col-model"><span class="rt-model">' + esc(m.model) + '</span></td>'
+ + '<td class="rt-col-controlled"><span class="rt-score">' + esc(scoreText(m.controlled)) + '</span>'
+ + bar(m.controlled, scale) + '</td>'
+ + '<td class="rt-col-raw"><span class="rt-score rt-score-muted">' + esc(scoreText(m.raw)) + '</span>'
+ + bar(m.raw, scale) + '</td>'
+ + '<td class="rt-col-votes rt-num">' + esc(m.votes) + '</td>'
+ + '</tr>').join('');
+ return '<div class="br-table-wrap rt-wrap"><table class="br-table rt-table">'
+ + '<caption class="br-sr">Models in rank bands, with the style-controlled score and its interval, the raw score and its interval, and the number of votes</caption>'
+ + '<thead><tr><th scope="col">Band</th><th scope="col">Model</th><th scope="col">Style controlled</th>'
+ + '<th scope="col">Raw</th><th scope="col" class="br-num">Votes</th></tr></thead>'
+ + '<tbody>' + body + '</tbody></table></div>';
}
}
function effects(view) {
function effects(view) {
- const list = Object.entries(view.effects || {});
- if (!list.length) return '';
- const items = list.map(([key, e]) => '<div><dt>' + esc(key) + '</dt><dd>'
- + (e.elo_per_1000_chars != null ? esc(e.elo_per_1000_chars) + ' Elo per 1000 characters' : '')
- + (e.elo_per_1000_chars != null && e.elo_per_10_marks != null ? '<br>' : '')
- + (e.elo_per_10_marks != null ? esc(e.elo_per_10_marks) + ' Elo per 10 markup marks' : '')
- + '</dd></div>').join('');
+ const list = Object.entries(view.effects || {}).map(([key, e]) => styleSentence(key, e)).filter(Boolean);
+ const dropped = (view.dropped_covariates || []).map((key) => key === 'markup'
+ ? 'Markup moved with the length of the outputs here, so it is not counted a second time.'
+ : key + ' was not counted separately.');
+ const sentences = list.concat(dropped);
+ if (!sentences.length) return '';
return '<section class="rt-effects"><h2>How much of the board is style</h2>'
return '<section class="rt-effects"><h2>How much of the board is style</h2>'
- + '<dl class="br-facts">' + items + '</dl>'
- + '<p class="bm-fine">A positive number means the longer (or the more dressed up) output wins votes it did not '
- + 'earn. The controlled column removes that effect; the raw column is what people actually chose.</p>'
+ + '<ul class="rt-style-list">' + sentences.map((s) => '<li>' + esc(s) + '</li>').join('') + '</ul>'
+ + '<p class="bm-fine">The raw column is what people chose; the style-controlled column is what the models would '
+ + 'score if every output were written the same way. A negative effect means the style lost votes it should not '
+ + 'have lost.</p>'
+ '</section>';
+ '</section>';
}
}
@@ -59,11 +121,13 @@ function ratingsView(view) {
@@ -59,11 +121,13 @@ function ratingsView(view) {
+ '<p class="bm-lede">Every human vote in Bench is one game between two models. The board is fitted twice: on the '
+ '<p class="bm-lede">Every human vote in Bench is one game between two models. The board is fitted twice: on the '
+ 'votes alone (raw) and with the style of each output as covariates (style controlled), so you can see how much '
+ 'votes alone (raw) and with the style of each output as covariates (style controlled), so you can see how much '
+ 'of a rank is length and formatting.</p>'
+ 'of a rank is length and formatting.</p>'
- + '<p class="bm-fine">' + esc(view.votes) + ' votes, '
- + esc(view.bootstrap) + ' bootstrap resamples, seed ' + esc(view.seed)
- + (view.voters && view.voters.downweighted ? ', ' + esc(view.voters.downweighted) + ' voters down-weighted (never named)' : '')
- + '</p>'
- + '<div class="rt-table-wrap">' + rows(view) + '</div>'
+ + (view.votes
+ ? '<p class="bm-fine">' + esc(view.votes) + ' votes, '
+ + esc(view.bootstrap) + ' bootstrap resamples, seed ' + esc(view.seed)
+ + (view.voters && view.voters.downweighted ? ', ' + esc(view.voters.downweighted) + ' voters down-weighted (never named)' : '')
+ + '</p>'
+ : '')
+ + rows(view)
+ effects(view)
+ effects(view)
+ '<section class="rt-data"><h2>The open data</h2>'
+ '<section class="rt-data"><h2>The open data</h2>'
+ '<p class="bm-lede">Every vote is published as JSONL, without voter ids: '
+ '<p class="bm-lede">Every vote is published as JSONL, without voter ids: '
@@ -96,6 +96,13 @@ const FORM = { 'content-type': 'application/x-www-form-urlencoded' };
@@ -96,6 +96,13 @@ const FORM = { 'content-type': 'application/x-www-form-urlencoded' };
assert.strictEqual(page.status, 200, page.text);
assert.strictEqual(page.status, 200, page.text);
assert.ok(page.text.includes('Raw') && page.text.includes('Style controlled'), 'both columns');
assert.ok(page.text.includes('Raw') && page.text.includes('Style controlled'), 'both columns');
assert.ok(page.text.includes('alpha-'), 'a model on the board');
assert.ok(page.text.includes('alpha-'), 'a model on the board');
+ // The board is a table: a band, a model, the scores with their intervals on one scale, and the votes.
+ assert.ok(page.text.includes('class="br-table rt-table"'), 'the ranking table');
+ assert.ok(page.text.includes('>Band<') && page.text.includes('>Votes<'), 'the columns');
+ assert.ok(page.text.includes('class="rt-bar"'), 'the interval bar');
+ assert.ok(page.text.includes('rt-band'), 'the band');
+ // The style is explained in words, not as a number.
+ assert.ok(!page.text.includes('Elo per 1000 characters'), 'no raw effect numbers: ' + page.text.slice(0, 200));
const dump = await t.get('/api/v1/datasets/votes.jsonl');
const dump = await t.get('/api/v1/datasets/votes.jsonl');
assert.strictEqual(dump.status, 200, dump.text);
assert.strictEqual(dump.status, 200, dump.text);
@@ -83,6 +83,14 @@ const { boot, check, done } = require('./helpers/boot');
@@ -83,6 +83,14 @@ const { boot, check, done } = require('./helpers/boot');
assert.ok(one.entries[0].wrapper_url.includes('/render/w/'), 'the sandboxed frame');
assert.ok(one.entries[0].wrapper_url.includes('/render/w/'), 'the sandboxed frame');
});
});
+ await check('an empty board says so, and offers a battle', async () => {
+ const page = await t.get('/ratings');
+ assert.strictEqual(page.status, 200, page.text);
+ assert.ok(page.text.includes('No battles voted yet'), 'the empty board: ' + page.text.slice(0, 200));
+ assert.ok(page.text.includes('Vote in a battle'), 'the button');
+ assert.ok(page.text.includes('href="/battle"'), 'it goes to the battle');
+ });
+
await t.close();
await t.close();
done();
done();
})().catch((e) => { console.error(e); process.exit(1); });
})().catch((e) => { console.error(e); process.exit(1); });