@wendellresearch-ai/benchwarmers 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/package.json +1 -1
- package/serve.py +53 -22
- package/ui/bench.css +28 -0
- package/ui/bench.js +145 -65
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.
|
|
1
|
+
0.6.5
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wendellresearch-ai/benchwarmers",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Self-hosted panel gateway for LLMs: ask a collection of models at once, keep every answer side by side, rotate explorer models in, and let your verdicts pick the next panel.",
|
|
5
5
|
"mcpName": "io.github.WendellResearch-AI/benchwarmers",
|
|
6
6
|
"license": "MIT",
|
package/serve.py
CHANGED
|
@@ -2701,7 +2701,14 @@ def vision_card() -> str:
|
|
|
2701
2701
|
come from these alone. <b>Useful, no claims</b> is a verdict without them, which on thicken's
|
|
2702
2702
|
photos means the seat agreed with another seat. Agreement is not a check, since seats fail
|
|
2703
2703
|
together, so it is never counted as right.</dd>
|
|
2704
|
-
<dt>The
|
|
2704
|
+
<dt>The real-work chart</dt><dd>Read as Real work is. The dot is the share of its judged seats your
|
|
2705
|
+
verdict called useful (checked and helped, or useful with no claims), bigger for more judged; the
|
|
2706
|
+
line is where that share likely sits (95%), and the order is by its low end, so a lot of evidence
|
|
2707
|
+
outranks a little. Green when even the low end is over half, red when even the high end is under,
|
|
2708
|
+
grey when there are too few to say. Beside it: useful of judged, how often it answered at all, and
|
|
2709
|
+
the claims checked by hand, the only count of right and wrong. Seats not judged or not answered
|
|
2710
|
+
are in those words and the table, not the dot.</dd>
|
|
2711
|
+
<dt>The number beside a name</dt><dd>How much there is: fields asked on the tasks.</dd>
|
|
2705
2712
|
</dl>"""
|
|
2706
2713
|
held = (f"<p class='hint'>The vision tasks wait until {esc(v['held'])}: no run picks them before then"
|
|
2707
2714
|
" (<code>vision_from</code> in <code>config/suite.json</code>; remove it to start them now).</p>"
|
|
@@ -2709,7 +2716,7 @@ def vision_card() -> str:
|
|
|
2709
2716
|
return (f"<div class='panel'>{SHARE}<h2>Vision</h2><p class='sub'>Reading images: fields read right on pictures with"
|
|
2710
2717
|
" a known answer, then real work on images and how much of it was actually checked.</p>" + held +
|
|
2711
2718
|
"<h3 class='chart-head'>On the vision tasks, field by field</h3><div class='chart' id='chart-vision-fields'></div>"
|
|
2712
|
-
"<h3 class='chart-head'>On real work with images,
|
|
2719
|
+
"<h3 class='chart-head'>On real work with images: judged useful, and how sure that is</h3>"
|
|
2713
2720
|
"<div class='chart' id='chart-vision-work'></div>"
|
|
2714
2721
|
"<details class='key'><summary>Table</summary><div class='toolbar'><input type='search'"
|
|
2715
2722
|
" data-filter placeholder='Filter by model or connection' aria-label='Filter rows'>"
|
|
@@ -3224,7 +3231,7 @@ def uptime_data() -> dict:
|
|
|
3224
3231
|
|
|
3225
3232
|
def uptime_card() -> str:
|
|
3226
3233
|
"""Was the provider there when the bench called it: a cell a day, by
|
|
3227
|
-
provider
|
|
3234
|
+
provider or by model, as its By says; 7 days, or 30 on choice (bench.js swaps them)."""
|
|
3228
3235
|
u = uptime_rows()
|
|
3229
3236
|
first, gone, names = UPTIME_DAYS[0], retired(), {}
|
|
3230
3237
|
|
|
@@ -3298,9 +3305,11 @@ def uptime_card() -> str:
|
|
|
3298
3305
|
"<span><i style='background:var(--signal)'></i>some down, 80% or more answered</span>"
|
|
3299
3306
|
"<span><i style='background:var(--err)'></i>under 80% answered</span>"
|
|
3300
3307
|
"<span><i style='background:none;border:1px solid var(--line)'></i>no counted calls</span></div>")
|
|
3308
|
+
# The window and one view at a time: by provider (shorter, the default) or by model (bench.js).
|
|
3301
3309
|
pick = ("<div class='toolbar'><label>Days <select data-uptime-days>"
|
|
3302
3310
|
+ "".join(f"<option value='{w}'>last {w}</option>" for w in UPTIME_DAYS)
|
|
3303
|
-
+
|
|
3311
|
+
+ "</select></label><label>By <select data-uptime-by><option value='providers'>provider</option>"
|
|
3312
|
+
f"<option value='models'>model</option></select></label></div>{legend}")
|
|
3304
3313
|
key = """<dl>
|
|
3305
3314
|
<dt>Down</dt><dd>The provider was busy or overloaded (a 5xx, high demand, a shared pool's
|
|
3306
3315
|
429), unreachable (a broken stream, no reply), timed out (the gateway's 420 s included), or
|
|
@@ -3326,9 +3335,7 @@ def uptime_card() -> str:
|
|
|
3326
3335
|
" and every real call, one cell a day. Busy, unreachable and timed out count as down;"
|
|
3327
3336
|
" our own quota, our own pacing and setup problems are left out.",
|
|
3328
3337
|
"chart-uptime-models", "".join(window(w) for w in UPTIME_DAYS), key,
|
|
3329
|
-
extra=pick + "<
|
|
3330
|
-
"<div class='chart' id='chart-uptime-providers'></div>"
|
|
3331
|
-
"<h3 class='chart-head'>By model</h3>",
|
|
3338
|
+
extra=pick + "<div class='chart' id='chart-uptime-providers'></div>",
|
|
3332
3339
|
after="".join(odd(w) for w in UPTIME_DAYS))
|
|
3333
3340
|
|
|
3334
3341
|
|
|
@@ -3655,10 +3662,20 @@ def wrong_card() -> str:
|
|
|
3655
3662
|
|
|
3656
3663
|
def benchmarks_tab() -> str:
|
|
3657
3664
|
data = json.dumps(charts_data(), ensure_ascii=False).replace("</", "<\\/")
|
|
3658
|
-
#
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3665
|
+
# Who to trust, then what moves day to day (Uptime, Trend, Performance), then Real work,
|
|
3666
|
+
# then the rest in the order they had. Each report gets an id; a bar of links to them
|
|
3667
|
+
# stays at the top while the page scrolls, the one in view marked (bench.js jumpSpy).
|
|
3668
|
+
reports = (("trust", "Who to trust", overview_card), ("uptime", "Uptime", uptime_card),
|
|
3669
|
+
("trend", "Trend", trend_card), ("performance", "Performance", perf_card),
|
|
3670
|
+
("work", "Real work", work_card), ("hunt", "Bug hunt", bughunt_card),
|
|
3671
|
+
("wrong", "Confidently wrong", wrong_card), ("code", "Our code", suite_card),
|
|
3672
|
+
("vision", "Vision", vision_card), ("decisions", "Decision models", decision_card),
|
|
3673
|
+
("external", "External", external_card))
|
|
3674
|
+
links = "".join(f"<a href='#report-{key}'>{esc(label)}</a>" for key, label, _ in reports)
|
|
3675
|
+
cards = "".join(card().replace("<div class='panel'", f"<div class='panel report' id='report-{key}'", 1)
|
|
3676
|
+
for key, _, card in reports)
|
|
3677
|
+
return (f"<nav class='jump' aria-label='On this page'><p class='jump-title'>On this page</p>{links}</nav>{cards}"
|
|
3678
|
+
f"<script type='application/json' id='charts-data'>{data}</script>")
|
|
3662
3679
|
|
|
3663
3680
|
|
|
3664
3681
|
def perf_card() -> str:
|
|
@@ -5251,11 +5268,16 @@ def models_page() -> str:
|
|
|
5251
5268
|
" comes from, with the endpoint it is reached through and what is known of its limits. A name"
|
|
5252
5269
|
" opens the model.</p><div class='toolbar'><input type='search' data-filter='page'"
|
|
5253
5270
|
" placeholder='Find a model or connection' aria-label='Filter models'>"
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5271
|
+
f"{fold_toggle(True)}</div></div>" + "".join(cards))
|
|
5272
|
+
|
|
5273
|
+
|
|
5274
|
+
def fold_toggle(all_open: bool) -> str:
|
|
5275
|
+
"""One labelled toggle over a page's folds (Models' connections, Logs' logs), as
|
|
5276
|
+
accordions do it (GOV.UK's "Show all sections"): Collapse all while every one is
|
|
5277
|
+
open, else Expand all. bench.js foldButtons keeps it right as folds change."""
|
|
5278
|
+
return (f"<button class='fold-all' type='button' data-fold-all='{'close' if all_open else 'open'}'"
|
|
5279
|
+
f" aria-expanded='{str(all_open).lower()}'><span class='fold-chev' aria-hidden='true'></span>"
|
|
5280
|
+
f"{'Collapse all' if all_open else 'Expand all'}</button>")
|
|
5259
5281
|
|
|
5260
5282
|
|
|
5261
5283
|
def quota_tiles() -> list[dict]:
|
|
@@ -5431,16 +5453,25 @@ def logs_page() -> str:
|
|
|
5431
5453
|
for p in sorted((DATA / "runs").glob("onboard-*.log"), reverse=True)[:30])
|
|
5432
5454
|
|
|
5433
5455
|
def section(title, body, n, open_=False):
|
|
5434
|
-
# CSV: this log alone, its own columns, the filter applied (bench.js); for a spreadsheet.
|
|
5435
5456
|
return (f"<details class='panel'{' open' if open_ else ''} data-log='{esc(title)}'><summary><b>{esc(title)}</b>"
|
|
5436
|
-
f" <span class='muted'>· {n}</span>
|
|
5437
|
-
|
|
5457
|
+
f" <span class='muted'>· {n}</span></summary>{body}</details>")
|
|
5458
|
+
# Export: a popup for the format and the logs (bench.js), each as the page shows it,
|
|
5459
|
+
# the filter applied. Text, every log chosen in one file to read; CSV, a file per log.
|
|
5460
|
+
picks = "".join(f"<label class='check'><input type='checkbox' name='lx-log' value='{esc(t)}' checked> {esc(t)}</label>"
|
|
5461
|
+
for t in ("Gateway calls", "Changes", "Scheduler", "Onboarding runs"))
|
|
5462
|
+
export = ("<dialog id='logs-export-dialog' aria-labelledby='lx-title'><h2 id='lx-title'>Export logs</h2>"
|
|
5463
|
+
"<fieldset><legend class='field'>Format</legend>"
|
|
5464
|
+
"<label class='check'><input type='radio' name='lx-format' value='txt' checked> Text (.txt)</label>"
|
|
5465
|
+
"<label class='check'><input type='radio' name='lx-format' value='csv'> CSV (.csv)</label></fieldset>"
|
|
5466
|
+
f"<fieldset><legend class='field'>Logs</legend>{picks}</fieldset>"
|
|
5467
|
+
"<p class='rules' id='lx-note'></p><div class='errors' id='lx-errors' role='alert'></div>"
|
|
5468
|
+
"<div class='actions'><button class='btn' type='button' id='lx-cancel'>Cancel</button>"
|
|
5469
|
+
"<button class='btn primary' type='button' id='lx-go'>Export</button></div></dialog>")
|
|
5438
5470
|
return (
|
|
5439
5471
|
"<div class='panel'><div class='toolbar'><input data-filter='page' placeholder='Filter every log on this page'"
|
|
5440
5472
|
" aria-label='Filter logs'>"
|
|
5441
|
-
|
|
5442
|
-
"
|
|
5443
|
-
" filtered, to a text file'>Export as text</button></div></div>"
|
|
5473
|
+
"<button class='btn small' type='button' id='logs-export'>Export…</button>"
|
|
5474
|
+
f"{fold_toggle(False)}</div></div>{export}"
|
|
5444
5475
|
+ section("Gateway calls", "<table><thead><tr>" + th("When") + th("Collection") + th("Shape")
|
|
5445
5476
|
+ th("Seats") + f"</tr></thead><tbody>{''.join(gw)}</tbody></table>", len(gw), True)
|
|
5446
5477
|
+ section("Changes", "<table><thead><tr>" + th("When") + th("By") + th("Model") + th("What")
|
package/ui/bench.css
CHANGED
|
@@ -536,6 +536,34 @@ pre.log.answer { max-height: 40vh; }
|
|
|
536
536
|
.chart.muted { font-size: .875rem; }
|
|
537
537
|
h3.chart-head { font-size: 1rem; margin: 1rem 0 .2rem; }
|
|
538
538
|
.chart-label { font-size: .875rem; font-weight: 600; margin: .9rem 0 .1rem; }
|
|
539
|
+
/* Benchmarks' "On this page": a link per report, the one in view marked (bench.js jumpSpy),
|
|
540
|
+
after the U.S. Web Design System's in-page navigation. Dressed as the sidebar is: the
|
|
541
|
+
theme's frosted rail, muted links, the highlighter stroke on the current one.
|
|
542
|
+
Narrow: a bar floating at the top of the page, sideways when it does not fit.
|
|
543
|
+
Wide: a rail beside the reports, sticky, every link on its own line. */
|
|
544
|
+
.jump { position: sticky; top: .75rem; z-index: 4; display: flex; align-items: center; gap: 1rem;
|
|
545
|
+
margin: .9rem 1.2rem 0; padding: .45rem 1.1rem; overflow-x: auto; scrollbar-width: none;
|
|
546
|
+
background: var(--rail, var(--paper-2)); border: 1px solid var(--line); border-radius: 999px;
|
|
547
|
+
-webkit-backdrop-filter: var(--glass, blur(10px)); backdrop-filter: var(--glass, blur(10px));
|
|
548
|
+
box-shadow: 0 10px 28px -16px rgb(0 0 0 / .4); }
|
|
549
|
+
.jump::-webkit-scrollbar { display: none; }
|
|
550
|
+
.jump .jump-title { display: none; margin: 0 0 .2rem; font-size: .75rem; font-weight: 700; letter-spacing: .08em;
|
|
551
|
+
text-transform: uppercase; color: color-mix(in oklab, var(--brand) 72%, var(--ink)); }
|
|
552
|
+
.jump a { flex: none; padding: .1rem 0; line-height: 1.15; color: var(--muted); text-decoration: none;
|
|
553
|
+
font-size: .9375rem; font-weight: 500; white-space: nowrap; }
|
|
554
|
+
.jump a:hover, .jump a:focus-visible { color: var(--ink); }
|
|
555
|
+
.jump a[aria-current] { color: var(--ink); font-weight: 700; box-shadow: inset 0 -.42em 0 var(--signal); }
|
|
556
|
+
.panel.report { scroll-margin-top: 4.2rem; } /* a jump lands under the bar, not behind it */
|
|
557
|
+
@media (min-width: 75rem) {
|
|
558
|
+
#tab-suite:has(> .jump) { display: grid; grid-template-columns: minmax(0, 1fr) 11rem; }
|
|
559
|
+
#tab-suite:has(> .jump) > * { grid-column: 1; }
|
|
560
|
+
#tab-suite > .jump { grid-column: 2; grid-row: 1 / span 40; align-self: start; top: 1rem;
|
|
561
|
+
flex-direction: column; align-items: flex-start; gap: .6rem; overflow: visible;
|
|
562
|
+
margin: 1.4rem 1rem 1.4rem 0; padding: 1rem 1.1rem; border-radius: 14px; box-shadow: none; }
|
|
563
|
+
#tab-suite > .jump .jump-title { display: block; }
|
|
564
|
+
#tab-suite > .jump a { white-space: normal; }
|
|
565
|
+
.panel.report { scroll-margin-top: 1rem; }
|
|
566
|
+
}
|
|
539
567
|
/* Models: each connection a fold, its heading the summary */
|
|
540
568
|
.model-card > summary h3.chart-head { display: inline; margin: 0; }
|
|
541
569
|
/* Expand all / Collapse all: a text toggle with a chevron that turns, like a fold's own marker */
|
package/ui/bench.js
CHANGED
|
@@ -279,8 +279,9 @@
|
|
|
279
279
|
// ---- Expand all / Collapse all: every fold of the page's groups (Models' connections).
|
|
280
280
|
// One labelled toggle, as accordions have it: Collapse all while every one is open,
|
|
281
281
|
// else Expand all. Kept right after any fold opens or shuts, by hand, by it or by a live swap.
|
|
282
|
+
var FOLDS = 'details.model-card, details[data-log]'; // the groups a page folds: Models' connections, Logs' logs
|
|
282
283
|
function foldButtons(tab) {
|
|
283
|
-
var folds = tab.querySelectorAll(
|
|
284
|
+
var folds = tab.querySelectorAll(FOLDS), open = 0;
|
|
284
285
|
folds.forEach(function (d) { if (d.open) open++; });
|
|
285
286
|
var all = open === folds.length;
|
|
286
287
|
tab.querySelectorAll('[data-fold-all]').forEach(function (b) {
|
|
@@ -293,11 +294,11 @@
|
|
|
293
294
|
var b = e.target.closest('[data-fold-all]');
|
|
294
295
|
if (!b) return;
|
|
295
296
|
var tab = b.closest('.tab');
|
|
296
|
-
tab.querySelectorAll(
|
|
297
|
+
tab.querySelectorAll(FOLDS).forEach(function (d) { d.open = b.dataset.foldAll === 'open'; });
|
|
297
298
|
foldButtons(tab);
|
|
298
299
|
});
|
|
299
300
|
document.addEventListener('toggle', function (e) { // toggle does not bubble: caught on its way down
|
|
300
|
-
if (e.target.matches && e.target.matches(
|
|
301
|
+
if (e.target.matches && e.target.matches(FOLDS)) foldButtons(e.target.closest('.tab'));
|
|
301
302
|
}, true);
|
|
302
303
|
|
|
303
304
|
// ---- Logs: Export as text (every log in one .txt, to read) and CSV (one log, its
|
|
@@ -306,42 +307,54 @@
|
|
|
306
307
|
var now = new Date(), two = function (n) { return String(n).padStart(2, '0'); };
|
|
307
308
|
return now.getFullYear() + '-' + two(now.getMonth() + 1) + '-' + two(now.getDate()) + '-' + two(now.getHours()) + two(now.getMinutes());
|
|
308
309
|
}
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (!b) return;
|
|
313
|
-
e.preventDefault(); // in the summary: no fold
|
|
314
|
-
var d = b.closest('details'), rows = [];
|
|
310
|
+
// A log as the page shows it (the filter applied): its header and rows, as cells.
|
|
311
|
+
// A log of lines (Scheduler, Onboarding runs) is one column, Line.
|
|
312
|
+
function logRows(d) {
|
|
315
313
|
var cells = function (tr) {
|
|
316
314
|
return Array.prototype.map.call(tr.children, function (c) { return c.textContent.trim().replace(/\s+/g, ' '); });
|
|
317
315
|
};
|
|
318
|
-
var head = d.querySelector('thead tr');
|
|
319
|
-
|
|
320
|
-
rows.push(cells(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
saveFile(new Blob([csv(rows)], {type: 'text/csv;charset=utf-8'}), 'benchwarmers-' + slug + '-' + stampNow() + '.csv');
|
|
328
|
-
});
|
|
316
|
+
var head = d.querySelector('thead tr'), rows = [head ? cells(head) : ['Line']];
|
|
317
|
+
d.querySelectorAll(head ? 'tbody tr' : '.log-line').forEach(function (r) {
|
|
318
|
+
if (!r.hidden) rows.push(head ? cells(r) : [r.textContent.trim()]);
|
|
319
|
+
});
|
|
320
|
+
return rows;
|
|
321
|
+
}
|
|
322
|
+
// Export…: the popup picks the format and the logs. Text: every log chosen in one
|
|
323
|
+
// .txt to read, tab-separated. CSV: a .csv per log (its own columns), through Share's
|
|
324
|
+
// saveFile(blob, name) and csv(rows) (RFC 4180, with a BOM).
|
|
329
325
|
on('click', 'logs-export', function () {
|
|
330
|
-
var
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
326
|
+
var q = ($('tab-logs').querySelector('input[data-filter]') || {}).value;
|
|
327
|
+
$('lx-note').textContent = q ? 'Only the lines that match the filter, "' + q + '", as the page shows them.'
|
|
328
|
+
: 'Every line the page shows: the last 200 of each log.';
|
|
329
|
+
$('lx-errors').textContent = '';
|
|
330
|
+
$('logs-export-dialog').showModal();
|
|
331
|
+
});
|
|
332
|
+
on('click', 'lx-cancel', function () { $('logs-export-dialog').close(); });
|
|
333
|
+
on('click', 'lx-go', function () {
|
|
334
|
+
var tab = $('tab-logs'), dlg = $('logs-export-dialog');
|
|
335
|
+
var format = dlg.querySelector('input[name=lx-format]:checked').value;
|
|
336
|
+
var want = Array.prototype.filter.call(dlg.querySelectorAll('input[name=lx-log]'), function (x) { return x.checked; })
|
|
337
|
+
.map(function (x) { return x.value; });
|
|
338
|
+
if (!want.length) { $('lx-errors').textContent = 'Choose at least one log.'; return; }
|
|
339
|
+
var logs = Array.prototype.filter.call(tab.querySelectorAll('details[data-log]'), function (d) {
|
|
340
|
+
return want.indexOf(d.dataset.log) !== -1;
|
|
341
|
+
});
|
|
342
|
+
var stamp = stampNow(), q = (tab.querySelector('input[data-filter]') || {}).value;
|
|
343
|
+
if (format === 'csv') {
|
|
344
|
+
logs.forEach(function (d) {
|
|
345
|
+
var slug = d.dataset.log.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
346
|
+
saveFile(new Blob([csv(logRows(d))], {type: 'text/csv;charset=utf-8'}), 'benchwarmers-' + slug + '-' + stamp + '.csv');
|
|
342
347
|
});
|
|
343
|
-
}
|
|
344
|
-
|
|
348
|
+
} else {
|
|
349
|
+
var out = ['Benchwarmers logs, exported ' + new Date().toString()];
|
|
350
|
+
if (q) out.push('Filtered by: ' + q);
|
|
351
|
+
logs.forEach(function (d) {
|
|
352
|
+
out.push('', '== ' + d.dataset.log + ' ==');
|
|
353
|
+
logRows(d).forEach(function (r) { out.push(r.join('\t')); });
|
|
354
|
+
});
|
|
355
|
+
saveFile(new Blob([out.join('\n') + '\n'], {type: 'text/plain;charset=utf-8'}), 'benchwarmers-logs-' + stamp + '.txt');
|
|
356
|
+
}
|
|
357
|
+
dlg.close();
|
|
345
358
|
});
|
|
346
359
|
|
|
347
360
|
// ---- a link to another page ----
|
|
@@ -2750,6 +2763,9 @@
|
|
|
2750
2763
|
// the pages share. It was the whole page, which made every chart wider than
|
|
2751
2764
|
// its column beside the sidebar, scaled down, with smaller text.
|
|
2752
2765
|
function width() {
|
|
2766
|
+
// Benchmarks shown with its rail beside the reports: a report's own width, not the column's.
|
|
2767
|
+
var report = document.querySelector('.tab.active .panel.report');
|
|
2768
|
+
if (report && report.clientWidth) return Math.max(520, report.clientWidth - 50);
|
|
2753
2769
|
var col = document.querySelector('.panes') || document.querySelector('main');
|
|
2754
2770
|
return Math.max(520, (col.clientWidth || 900) - 50);
|
|
2755
2771
|
}
|
|
@@ -2941,8 +2957,14 @@
|
|
|
2941
2957
|
]
|
|
2942
2958
|
});
|
|
2943
2959
|
};
|
|
2944
|
-
|
|
2945
|
-
|
|
2960
|
+
// One view at a time, as the card's By says: by provider (the default) or by model.
|
|
2961
|
+
var view = (document.querySelector('select[data-uptime-by]') || {}).value || 'providers';
|
|
2962
|
+
['providers', 'models'].forEach(function (k) {
|
|
2963
|
+
var box = $('chart-uptime-' + k);
|
|
2964
|
+
if (!box) return;
|
|
2965
|
+
box.hidden = k !== view;
|
|
2966
|
+
if (k === view) put('chart-uptime-' + k, function (w) { return uptime(U[k], w); });
|
|
2967
|
+
});
|
|
2946
2968
|
}
|
|
2947
2969
|
if (part === 'uptime') return;
|
|
2948
2970
|
// Decision models: each model's answers grouped by the probability it gave
|
|
@@ -3265,41 +3287,59 @@
|
|
|
3265
3287
|
});
|
|
3266
3288
|
});
|
|
3267
3289
|
|
|
3268
|
-
// Vision, real work:
|
|
3290
|
+
// Vision, real work: read as Real work is. The one question: when its seat was judged,
|
|
3291
|
+
// did it help? The dot is the share judged useful (checked and helped, or useful with no
|
|
3292
|
+
// claims to check), sized by how many were judged; the line where that share likely sits
|
|
3293
|
+
// (95% Wilson), ranked by its low end. Beside it, in words: how often it answered at all,
|
|
3294
|
+
// and the claims checked by hand. Seats never judged or never answered say nothing of
|
|
3295
|
+
// its worth: they are in the words and the table, not the dot.
|
|
3269
3296
|
put('chart-vision-work', function (w) {
|
|
3270
|
-
var W = C.vision.work, kinds = C.vision.seatKinds,
|
|
3297
|
+
var W = C.vision.work, kinds = C.vision.seatKinds, R = [], none = [];
|
|
3271
3298
|
Object.keys(W).forEach(function (m) {
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
// The claims checked by hand, beside the bar: the only right-or-wrong count real work has.
|
|
3285
|
-
var claims = models.filter(function (m) { return W[m].real + W[m].false; }).map(function (m) {
|
|
3286
|
-
return {model: m, text: W[m].real + ' of ' + (W[m].real + W[m].false) + ' claims right'};
|
|
3299
|
+
var v = W[m], n = function (k) { return v[k] || 0; }, total = 0;
|
|
3300
|
+
kinds.forEach(function (k) { total += n(k); });
|
|
3301
|
+
if (!total) return;
|
|
3302
|
+
var judged = n('checked, helped') + n('checked, did not help') + n('useful, no claims') + n('no use, no claims');
|
|
3303
|
+
var useful = n('checked, helped') + n('useful, no claims');
|
|
3304
|
+
var r = {model: m, total: total, judged: judged, useful: useful, answered: total - n('no answer'),
|
|
3305
|
+
real: v.real || 0, wrongC: v.false || 0};
|
|
3306
|
+
if (!judged) { none.push(r); return; }
|
|
3307
|
+
var b = wilson(useful, judged);
|
|
3308
|
+
r.p = useful / judged; r.lo = b[0]; r.hi = b[1];
|
|
3309
|
+
r.verdict = b[0] >= 0.5 ? 'helps' : b[1] < 0.5 ? 'does not help' : 'too few to say';
|
|
3310
|
+
R.push(r);
|
|
3287
3311
|
});
|
|
3312
|
+
if (!R.length && !none.length) return el('p', {class: 'hint'}, 'No vision panels through the gateway yet.');
|
|
3313
|
+
R.sort(function (a, b) { return b.lo - a.lo || b.judged - a.judged; });
|
|
3288
3314
|
var narrowV = w < 560, box = el('div');
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3315
|
+
function words(r) {
|
|
3316
|
+
return r.useful + '/' + r.judged + (narrowV ? '' : ' · ' + Math.round(r.answered / r.total * 100) + '% answered'
|
|
3317
|
+
+ (r.real + r.wrongC ? ' · claims ' + r.real + '/' + (r.real + r.wrongC) : ''));
|
|
3318
|
+
}
|
|
3319
|
+
if (R.length) box.appendChild(Plot.plot({
|
|
3320
|
+
width: w, height: 24 * R.length + 90, style: style, marginLeft: narrowV ? 130 : 190,
|
|
3321
|
+
marginRight: narrowV ? 50 : 260, marginTop: 34,
|
|
3322
|
+
x: {domain: [0, 1], label: 'Judged useful, share of its judged seats →', tickFormat: '.0%', ticks: 5, grid: true},
|
|
3323
|
+
y: {domain: R.map(function (r) { return r.model; }), label: null,
|
|
3324
|
+
tickFormat: function (m) { return fit(m, narrowV ? 16 : 26); }},
|
|
3325
|
+
r: {domain: [0, Math.max.apply(null, R.map(function (r) { return r.judged; }))], range: [3, 11]},
|
|
3326
|
+
color: {domain: ['helps', 'too few to say', 'does not help'], range: [good, muted, err], legend: true},
|
|
3294
3327
|
marks: [
|
|
3295
|
-
Plot.
|
|
3296
|
-
|
|
3297
|
-
Plot.
|
|
3298
|
-
|
|
3328
|
+
Plot.ruleX([0.5], {stroke: line}),
|
|
3329
|
+
Plot.ruleY(R, {y: 'model', x1: 'lo', x2: 'hi', stroke: 'verdict', strokeWidth: 2, strokeOpacity: 0.55}),
|
|
3330
|
+
Plot.dot(R, {x: 'p', y: 'model', r: 'judged', fill: 'verdict', stroke: halo, strokeWidth: 1.5,
|
|
3331
|
+
title: function (r) {
|
|
3332
|
+
return r.model + '\njudged useful ' + r.useful + ' of ' + r.judged + ' judged seats ('
|
|
3333
|
+
+ Math.round(r.p * 100) + '%), likely between ' + Math.round(r.lo * 100) + '% and ' + Math.round(r.hi * 100)
|
|
3334
|
+
+ '%\nanswered ' + r.answered + ' of ' + r.total + ' seats on image panels'
|
|
3335
|
+
+ (r.real + r.wrongC ? '\nclaims checked by hand: ' + r.real + ' right of ' + (r.real + r.wrongC) : '');
|
|
3336
|
+
}}),
|
|
3337
|
+
Plot.text(R, {x: 1, y: 'model', dx: 10, textAnchor: 'start', fill: ink, fontSize: 11, text: words})
|
|
3299
3338
|
]
|
|
3300
3339
|
}));
|
|
3301
|
-
if (
|
|
3302
|
-
+
|
|
3340
|
+
if (none.length) box.appendChild(el('p', {class: 'hint'}, 'No seat judged yet, so no dot: '
|
|
3341
|
+
+ none.map(function (r) { return r.model + ' (' + r.total + ' seats, answered ' + r.answered + ')'; }).join(', ')
|
|
3342
|
+
+ '. The table has every seat.'));
|
|
3303
3343
|
return box;
|
|
3304
3344
|
});
|
|
3305
3345
|
|
|
@@ -3338,10 +3378,50 @@
|
|
|
3338
3378
|
});
|
|
3339
3379
|
|
|
3340
3380
|
}
|
|
3341
|
-
onLoad.suite = function () { drawCharts('suite'); };
|
|
3381
|
+
onLoad.suite = function () { drawCharts('suite'); jumpSpy(); };
|
|
3382
|
+
// Benchmarks' jump bar: the report in view is marked (aria-current) and its link kept in
|
|
3383
|
+
// sight in a bar that scrolls sideways. Set up again after a live swap (setUp, onLoad).
|
|
3384
|
+
var spy = null;
|
|
3385
|
+
function jumpSpy() {
|
|
3386
|
+
if (spy) spy.disconnect();
|
|
3387
|
+
var nav = document.querySelector('#tab-suite .jump');
|
|
3388
|
+
if (!nav || !window.IntersectionObserver) return;
|
|
3389
|
+
var links = {}, seen = {}, current = null;
|
|
3390
|
+
nav.querySelectorAll('a').forEach(function (a) { links[a.getAttribute('href').slice(1)] = a; });
|
|
3391
|
+
// A report counts as in view while it crosses a thin band 15-20% down the window: just
|
|
3392
|
+
// under where a jump lands it, so the one jumped to is the one marked.
|
|
3393
|
+
spy = new IntersectionObserver(function (entries) {
|
|
3394
|
+
entries.forEach(function (e) { seen[e.target.id] = e.isIntersecting; });
|
|
3395
|
+
var id = Object.keys(links).filter(function (k) { return seen[k]; })[0];
|
|
3396
|
+
if (!id || id === current) return;
|
|
3397
|
+
if (current) links[current].removeAttribute('aria-current');
|
|
3398
|
+
current = id;
|
|
3399
|
+
links[id].setAttribute('aria-current', 'true');
|
|
3400
|
+
var a = links[id];
|
|
3401
|
+
if (a.offsetLeft < nav.scrollLeft || a.offsetLeft + a.offsetWidth > nav.scrollLeft + nav.clientWidth) {
|
|
3402
|
+
nav.scrollLeft = a.offsetLeft - 16;
|
|
3403
|
+
}
|
|
3404
|
+
}, {rootMargin: '-15% 0px -80% 0px'});
|
|
3405
|
+
document.querySelectorAll('#tab-suite .panel.report').forEach(function (p) { spy.observe(p); });
|
|
3406
|
+
}
|
|
3407
|
+
// A jump scrolls smoothly (unless motion is reduced) and moves focus to the report, so
|
|
3408
|
+
// a keyboard user carries on from there; the address keeps the report's #, as a link would.
|
|
3409
|
+
document.addEventListener('click', function (e) {
|
|
3410
|
+
var a = e.target.closest('.jump a');
|
|
3411
|
+
if (!a) return;
|
|
3412
|
+
var target = $(a.getAttribute('href').slice(1));
|
|
3413
|
+
if (!target) return;
|
|
3414
|
+
e.preventDefault();
|
|
3415
|
+
var still = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
3416
|
+
target.scrollIntoView({behavior: still ? 'auto' : 'smooth', block: 'start'});
|
|
3417
|
+
target.setAttribute('tabindex', '-1');
|
|
3418
|
+
target.focus({preventScroll: true});
|
|
3419
|
+
history.replaceState(null, '', a.getAttribute('href'));
|
|
3420
|
+
});
|
|
3342
3421
|
// Uptime's or Trend's window: that window's tables (and Uptime's list) shown, its charts drawn again.
|
|
3343
3422
|
document.addEventListener('change', function (ev) {
|
|
3344
3423
|
var s = ev.target, part = s.matches('select[data-uptime-days]') ? 'uptime' : s.matches('select[data-trend-days]') ? 'trend' : '';
|
|
3424
|
+
if (s.matches('select[data-uptime-by]')) { drawCharts('uptime'); return; } // the other view, same window
|
|
3345
3425
|
if (!part) return;
|
|
3346
3426
|
s.closest('.panel').querySelectorAll('[data-' + part + '-window]').forEach(function (d) {
|
|
3347
3427
|
d.hidden = d.getAttribute('data-' + part + '-window') !== s.value;
|