@wendellresearch-ai/benchwarmers 0.6.4 → 0.6.6
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 +60 -26
- package/ui/bench.css +28 -0
- package/ui/bench.js +177 -74
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.
|
|
1
|
+
0.6.6
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wendellresearch-ai/benchwarmers",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
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
|
@@ -2150,9 +2150,10 @@ def first_run() -> str:
|
|
|
2150
2150
|
text = ("Pick a model your endpoints serve. Onboarding registers it, probes it (4 calls) and runs"
|
|
2151
2151
|
" the internal suite, so it takes minutes and spends calls.")
|
|
2152
2152
|
elif not bench_collections.load():
|
|
2153
|
-
step, head, label = "collection", "Make your first collection", "
|
|
2154
|
-
text = ("The models a panel always asks, and what it is for.
|
|
2155
|
-
|
|
2153
|
+
step, head, label = "collection", "Make your first collection", "Make bench/core"
|
|
2154
|
+
text = ("The models a panel always asks, and what it is for. It opens filled in as bench/core, the"
|
|
2155
|
+
" name the set-ups on Clients use, with the models you onboarded ticked: change anything,"
|
|
2156
|
+
f" then save. Callers ask for it through the gateway at {esc(gateway_url())}.")
|
|
2156
2157
|
else:
|
|
2157
2158
|
return ""
|
|
2158
2159
|
return (f"<section class='problems' id='first-run' data-step='{step}' aria-labelledby='first-head'>"
|
|
@@ -2701,7 +2702,14 @@ def vision_card() -> str:
|
|
|
2701
2702
|
come from these alone. <b>Useful, no claims</b> is a verdict without them, which on thicken's
|
|
2702
2703
|
photos means the seat agreed with another seat. Agreement is not a check, since seats fail
|
|
2703
2704
|
together, so it is never counted as right.</dd>
|
|
2704
|
-
<dt>The
|
|
2705
|
+
<dt>The real-work chart</dt><dd>Read as Real work is. The dot is the share of its judged seats your
|
|
2706
|
+
verdict called useful (checked and helped, or useful with no claims), bigger for more judged; the
|
|
2707
|
+
line is where that share likely sits (95%), and the order is by its low end, so a lot of evidence
|
|
2708
|
+
outranks a little. Green when even the low end is over half, red when even the high end is under,
|
|
2709
|
+
grey when there are too few to say. Beside it: useful of judged, how often it answered at all, and
|
|
2710
|
+
the claims checked by hand, the only count of right and wrong. Seats not judged or not answered
|
|
2711
|
+
are in those words and the table, not the dot.</dd>
|
|
2712
|
+
<dt>The number beside a name</dt><dd>How much there is: fields asked on the tasks.</dd>
|
|
2705
2713
|
</dl>"""
|
|
2706
2714
|
held = (f"<p class='hint'>The vision tasks wait until {esc(v['held'])}: no run picks them before then"
|
|
2707
2715
|
" (<code>vision_from</code> in <code>config/suite.json</code>; remove it to start them now).</p>"
|
|
@@ -2709,7 +2717,7 @@ def vision_card() -> str:
|
|
|
2709
2717
|
return (f"<div class='panel'>{SHARE}<h2>Vision</h2><p class='sub'>Reading images: fields read right on pictures with"
|
|
2710
2718
|
" a known answer, then real work on images and how much of it was actually checked.</p>" + held +
|
|
2711
2719
|
"<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,
|
|
2720
|
+
"<h3 class='chart-head'>On real work with images: judged useful, and how sure that is</h3>"
|
|
2713
2721
|
"<div class='chart' id='chart-vision-work'></div>"
|
|
2714
2722
|
"<details class='key'><summary>Table</summary><div class='toolbar'><input type='search'"
|
|
2715
2723
|
" data-filter placeholder='Filter by model or connection' aria-label='Filter rows'>"
|
|
@@ -3224,7 +3232,7 @@ def uptime_data() -> dict:
|
|
|
3224
3232
|
|
|
3225
3233
|
def uptime_card() -> str:
|
|
3226
3234
|
"""Was the provider there when the bench called it: a cell a day, by
|
|
3227
|
-
provider
|
|
3235
|
+
provider or by model, as its By says; 7 days, or 30 on choice (bench.js swaps them)."""
|
|
3228
3236
|
u = uptime_rows()
|
|
3229
3237
|
first, gone, names = UPTIME_DAYS[0], retired(), {}
|
|
3230
3238
|
|
|
@@ -3298,9 +3306,11 @@ def uptime_card() -> str:
|
|
|
3298
3306
|
"<span><i style='background:var(--signal)'></i>some down, 80% or more answered</span>"
|
|
3299
3307
|
"<span><i style='background:var(--err)'></i>under 80% answered</span>"
|
|
3300
3308
|
"<span><i style='background:none;border:1px solid var(--line)'></i>no counted calls</span></div>")
|
|
3309
|
+
# The window and one view at a time: by provider (shorter, the default) or by model (bench.js).
|
|
3301
3310
|
pick = ("<div class='toolbar'><label>Days <select data-uptime-days>"
|
|
3302
3311
|
+ "".join(f"<option value='{w}'>last {w}</option>" for w in UPTIME_DAYS)
|
|
3303
|
-
+
|
|
3312
|
+
+ "</select></label><label>By <select data-uptime-by><option value='providers'>provider</option>"
|
|
3313
|
+
f"<option value='models'>model</option></select></label></div>{legend}")
|
|
3304
3314
|
key = """<dl>
|
|
3305
3315
|
<dt>Down</dt><dd>The provider was busy or overloaded (a 5xx, high demand, a shared pool's
|
|
3306
3316
|
429), unreachable (a broken stream, no reply), timed out (the gateway's 420 s included), or
|
|
@@ -3326,9 +3336,7 @@ def uptime_card() -> str:
|
|
|
3326
3336
|
" and every real call, one cell a day. Busy, unreachable and timed out count as down;"
|
|
3327
3337
|
" our own quota, our own pacing and setup problems are left out.",
|
|
3328
3338
|
"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>",
|
|
3339
|
+
extra=pick + "<div class='chart' id='chart-uptime-providers'></div>",
|
|
3332
3340
|
after="".join(odd(w) for w in UPTIME_DAYS))
|
|
3333
3341
|
|
|
3334
3342
|
|
|
@@ -3655,10 +3663,20 @@ def wrong_card() -> str:
|
|
|
3655
3663
|
|
|
3656
3664
|
def benchmarks_tab() -> str:
|
|
3657
3665
|
data = json.dumps(charts_data(), ensure_ascii=False).replace("</", "<\\/")
|
|
3658
|
-
#
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3666
|
+
# Who to trust, then what moves day to day (Uptime, Trend, Performance), then Real work,
|
|
3667
|
+
# then the rest in the order they had. Each report gets an id; a bar of links to them
|
|
3668
|
+
# stays at the top while the page scrolls, the one in view marked (bench.js jumpSpy).
|
|
3669
|
+
reports = (("trust", "Who to trust", overview_card), ("uptime", "Uptime", uptime_card),
|
|
3670
|
+
("trend", "Trend", trend_card), ("performance", "Performance", perf_card),
|
|
3671
|
+
("work", "Real work", work_card), ("hunt", "Bug hunt", bughunt_card),
|
|
3672
|
+
("wrong", "Confidently wrong", wrong_card), ("code", "Our code", suite_card),
|
|
3673
|
+
("vision", "Vision", vision_card), ("decisions", "Decision models", decision_card),
|
|
3674
|
+
("external", "External", external_card))
|
|
3675
|
+
links = "".join(f"<a href='#report-{key}'>{esc(label)}</a>" for key, label, _ in reports)
|
|
3676
|
+
cards = "".join(card().replace("<div class='panel'", f"<div class='panel report' id='report-{key}'", 1)
|
|
3677
|
+
for key, _, card in reports)
|
|
3678
|
+
return (f"<nav class='jump' aria-label='On this page'><p class='jump-title'>On this page</p>{links}</nav>{cards}"
|
|
3679
|
+
f"<script type='application/json' id='charts-data'>{data}</script>")
|
|
3662
3680
|
|
|
3663
3681
|
|
|
3664
3682
|
def perf_card() -> str:
|
|
@@ -5251,11 +5269,16 @@ def models_page() -> str:
|
|
|
5251
5269
|
" comes from, with the endpoint it is reached through and what is known of its limits. A name"
|
|
5252
5270
|
" opens the model.</p><div class='toolbar'><input type='search' data-filter='page'"
|
|
5253
5271
|
" placeholder='Find a model or connection' aria-label='Filter models'>"
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5272
|
+
f"{fold_toggle(True)}</div></div>" + "".join(cards))
|
|
5273
|
+
|
|
5274
|
+
|
|
5275
|
+
def fold_toggle(all_open: bool) -> str:
|
|
5276
|
+
"""One labelled toggle over a page's folds (Models' connections, Logs' logs), as
|
|
5277
|
+
accordions do it (GOV.UK's "Show all sections"): Collapse all while every one is
|
|
5278
|
+
open, else Expand all. bench.js foldButtons keeps it right as folds change."""
|
|
5279
|
+
return (f"<button class='fold-all' type='button' data-fold-all='{'close' if all_open else 'open'}'"
|
|
5280
|
+
f" aria-expanded='{str(all_open).lower()}'><span class='fold-chev' aria-hidden='true'></span>"
|
|
5281
|
+
f"{'Collapse all' if all_open else 'Expand all'}</button>")
|
|
5259
5282
|
|
|
5260
5283
|
|
|
5261
5284
|
def quota_tiles() -> list[dict]:
|
|
@@ -5431,16 +5454,25 @@ def logs_page() -> str:
|
|
|
5431
5454
|
for p in sorted((DATA / "runs").glob("onboard-*.log"), reverse=True)[:30])
|
|
5432
5455
|
|
|
5433
5456
|
def section(title, body, n, open_=False):
|
|
5434
|
-
# CSV: this log alone, its own columns, the filter applied (bench.js); for a spreadsheet.
|
|
5435
5457
|
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
|
-
|
|
5458
|
+
f" <span class='muted'>· {n}</span></summary>{body}</details>")
|
|
5459
|
+
# Export: a popup for the format and the logs (bench.js), each as the page shows it,
|
|
5460
|
+
# the filter applied. Text, every log chosen in one file to read; CSV, a file per log.
|
|
5461
|
+
picks = "".join(f"<label class='check'><input type='checkbox' name='lx-log' value='{esc(t)}' checked> {esc(t)}</label>"
|
|
5462
|
+
for t in ("Gateway calls", "Changes", "Scheduler", "Onboarding runs"))
|
|
5463
|
+
export = ("<dialog id='logs-export-dialog' aria-labelledby='lx-title'><h2 id='lx-title'>Export logs</h2>"
|
|
5464
|
+
"<fieldset><legend class='field'>Format</legend>"
|
|
5465
|
+
"<label class='check'><input type='radio' name='lx-format' value='txt' checked> Text (.txt)</label>"
|
|
5466
|
+
"<label class='check'><input type='radio' name='lx-format' value='csv'> CSV (.csv)</label></fieldset>"
|
|
5467
|
+
f"<fieldset><legend class='field'>Logs</legend>{picks}</fieldset>"
|
|
5468
|
+
"<p class='rules' id='lx-note'></p><div class='errors' id='lx-errors' role='alert'></div>"
|
|
5469
|
+
"<div class='actions'><button class='btn' type='button' id='lx-cancel'>Cancel</button>"
|
|
5470
|
+
"<button class='btn primary' type='button' id='lx-go'>Export</button></div></dialog>")
|
|
5438
5471
|
return (
|
|
5439
5472
|
"<div class='panel'><div class='toolbar'><input data-filter='page' placeholder='Filter every log on this page'"
|
|
5440
5473
|
" aria-label='Filter logs'>"
|
|
5441
|
-
|
|
5442
|
-
"
|
|
5443
|
-
" filtered, to a text file'>Export as text</button></div></div>"
|
|
5474
|
+
"<button class='btn small' type='button' id='logs-export'>Export…</button>"
|
|
5475
|
+
f"{fold_toggle(False)}</div></div>{export}"
|
|
5444
5476
|
+ section("Gateway calls", "<table><thead><tr>" + th("When") + th("Collection") + th("Shape")
|
|
5445
5477
|
+ th("Seats") + f"</tr></thead><tbody>{''.join(gw)}</tbody></table>", len(gw), True)
|
|
5446
5478
|
+ section("Changes", "<table><thead><tr>" + th("When") + th("By") + th("Model") + th("What")
|
|
@@ -5759,7 +5791,9 @@ class Handler(gateway.Handler):
|
|
|
5759
5791
|
if self.path == "/api/needs": # after an action there: that page, not the whole one
|
|
5760
5792
|
with locked.snapshot():
|
|
5761
5793
|
groups, auto = open_actions()
|
|
5762
|
-
|
|
5794
|
+
# `first`: first run's next step now ("" when set up), so a step done goes at once.
|
|
5795
|
+
self._json(200, {"html": actions_page(groups, auto), "count": open_count(groups),
|
|
5796
|
+
"first": first_run()})
|
|
5763
5797
|
return
|
|
5764
5798
|
if path == "/api/pane": # a tab's page, when first opened (bench.js load)
|
|
5765
5799
|
key = q.get("tab", [""])[0]
|
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 ----
|
|
@@ -851,6 +864,7 @@
|
|
|
851
864
|
count.textContent = n.count;
|
|
852
865
|
tab.setAttribute('aria-label', tab.firstChild.textContent + ', ' + n.count + ' open');
|
|
853
866
|
}
|
|
867
|
+
if ('first' in n) showFirst(n.first);
|
|
854
868
|
}).catch(function () { location.reload(); }); // could not ask: the old way
|
|
855
869
|
}
|
|
856
870
|
// A retired model's rows grey out and hide behind Show retired, as the
|
|
@@ -1932,15 +1946,33 @@
|
|
|
1932
1946
|
// ---- first run: no endpoint yet, no model, no collection (serve.first_run) -
|
|
1933
1947
|
// Each waits for its page: it may not have arrived yet.
|
|
1934
1948
|
var first = $('first-run');
|
|
1949
|
+
// A step done (an endpoint added, a model onboarded, a collection saved) moves first run on
|
|
1950
|
+
// at once: refreshNeeds hands its next step, or "" when the install is set up.
|
|
1951
|
+
function showFirst(html) {
|
|
1952
|
+
var now = $('first-run');
|
|
1953
|
+
if ((now ? now.outerHTML : '') === (html || '')) return;
|
|
1954
|
+
if (now) now.remove();
|
|
1955
|
+
if (html) document.querySelector('main').insertAdjacentHTML('afterbegin', html);
|
|
1956
|
+
first = $('first-run');
|
|
1957
|
+
}
|
|
1935
1958
|
function firstStep() {
|
|
1936
1959
|
if (first.dataset.step === 'onboard') { $('onboard-open').click(); return; }
|
|
1937
|
-
|
|
1960
|
+
// The first collection comes filled in as bench/core, the name the docs, CLIENTS.md and
|
|
1961
|
+
// the Clients page use: up to four of the models just onboarded ticked (two to four is
|
|
1962
|
+
// what a panel wants), never an image generator. Nothing is made until it is saved.
|
|
1963
|
+
if (first.dataset.step === 'collection') {
|
|
1964
|
+
show('collections').then(function () {
|
|
1965
|
+
var members = C.models.filter(function (a) { return fact(a).role !== 'image'; }).slice(0, 4);
|
|
1966
|
+
fillForm(null, {name: 'core', description: 'General second opinions: any question, several models at once.',
|
|
1967
|
+
tags: ['general', 'second-opinion'], members: members});
|
|
1968
|
+
c.name.focus();
|
|
1969
|
+
});
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1938
1972
|
show('endpoints').then(function () { openEndpoint('add'); });
|
|
1939
1973
|
}
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
if (first.dataset.step === 'endpoint') firstStep(); // straight into Add: an empty bench offers nothing else
|
|
1943
|
-
}
|
|
1974
|
+
on('click', 'first-go', firstStep); // on the document: a new step brings a new button
|
|
1975
|
+
if (first && first.dataset.step === 'endpoint') firstStep(); // straight into Add: an empty bench offers nothing else
|
|
1944
1976
|
|
|
1945
1977
|
// ---- collections: what the bench gateway runs together (its own tab) -----
|
|
1946
1978
|
// Its elements, the popup's too, are in the page: found again each time it arrives.
|
|
@@ -2137,13 +2169,16 @@
|
|
|
2137
2169
|
open = null;
|
|
2138
2170
|
}
|
|
2139
2171
|
on('close', 'c-form', function () { open = null; });
|
|
2140
|
-
|
|
2172
|
+
// `row`: a saved collection, opened to edit. `draft`: a new one's values filled in
|
|
2173
|
+
// beforehand (first run's core), still new until saved.
|
|
2174
|
+
function fillForm(row, draft) {
|
|
2141
2175
|
c.errors.textContent = ''; c.done.textContent = ''; c.status.textContent = '';
|
|
2142
2176
|
open = row ? row.name : null; c.del.hidden = !row;
|
|
2143
2177
|
c.head.textContent = row ? 'bench/' + row.name : 'New collection';
|
|
2144
2178
|
if (!c.form.open) c.form.showModal();
|
|
2145
|
-
|
|
2146
|
-
c.
|
|
2179
|
+
var src = row || draft;
|
|
2180
|
+
c.name.value = src ? src.name : ''; c.name.readOnly = !!row;
|
|
2181
|
+
c.desc.value = src ? src.description : ''; c.tags.value = src ? src.tags.join(', ') : '';
|
|
2147
2182
|
c.adHoc.checked = !!row && row.ad_hoc;
|
|
2148
2183
|
c.ownCalls.value = row && row.max_calls !== undefined ? row.max_calls : '';
|
|
2149
2184
|
c.ownExp.value = row && row.max_explorers !== undefined ? row.max_explorers : '';
|
|
@@ -2151,7 +2186,7 @@
|
|
|
2151
2186
|
drawTagPicks();
|
|
2152
2187
|
c.find.value = ''; c.show.value = ''; c.prov.value = '';
|
|
2153
2188
|
$('c-refresh-out').textContent = '';
|
|
2154
|
-
drawMembers(
|
|
2189
|
+
drawMembers(src ? src.members : []);
|
|
2155
2190
|
c.shape.textContent = '';
|
|
2156
2191
|
c.shape.appendChild(el('option', {value: ''}, 'automatic'));
|
|
2157
2192
|
C.shapes.forEach(function (s) {
|
|
@@ -2286,6 +2321,7 @@
|
|
|
2286
2321
|
// above the list, as onboarding and retiring do.
|
|
2287
2322
|
function finished(message) {
|
|
2288
2323
|
c.done.textContent = message; c.status.textContent = message; loadCollections(true);
|
|
2324
|
+
refreshNeeds(); // first run's last step may be done
|
|
2289
2325
|
setTimeout(closeForm, 1200);
|
|
2290
2326
|
}
|
|
2291
2327
|
on('click', 'c-delete', function () {
|
|
@@ -2750,6 +2786,9 @@
|
|
|
2750
2786
|
// the pages share. It was the whole page, which made every chart wider than
|
|
2751
2787
|
// its column beside the sidebar, scaled down, with smaller text.
|
|
2752
2788
|
function width() {
|
|
2789
|
+
// Benchmarks shown with its rail beside the reports: a report's own width, not the column's.
|
|
2790
|
+
var report = document.querySelector('.tab.active .panel.report');
|
|
2791
|
+
if (report && report.clientWidth) return Math.max(520, report.clientWidth - 50);
|
|
2753
2792
|
var col = document.querySelector('.panes') || document.querySelector('main');
|
|
2754
2793
|
return Math.max(520, (col.clientWidth || 900) - 50);
|
|
2755
2794
|
}
|
|
@@ -2941,8 +2980,14 @@
|
|
|
2941
2980
|
]
|
|
2942
2981
|
});
|
|
2943
2982
|
};
|
|
2944
|
-
|
|
2945
|
-
|
|
2983
|
+
// One view at a time, as the card's By says: by provider (the default) or by model.
|
|
2984
|
+
var view = (document.querySelector('select[data-uptime-by]') || {}).value || 'providers';
|
|
2985
|
+
['providers', 'models'].forEach(function (k) {
|
|
2986
|
+
var box = $('chart-uptime-' + k);
|
|
2987
|
+
if (!box) return;
|
|
2988
|
+
box.hidden = k !== view;
|
|
2989
|
+
if (k === view) put('chart-uptime-' + k, function (w) { return uptime(U[k], w); });
|
|
2990
|
+
});
|
|
2946
2991
|
}
|
|
2947
2992
|
if (part === 'uptime') return;
|
|
2948
2993
|
// Decision models: each model's answers grouped by the probability it gave
|
|
@@ -3265,41 +3310,59 @@
|
|
|
3265
3310
|
});
|
|
3266
3311
|
});
|
|
3267
3312
|
|
|
3268
|
-
// Vision, real work:
|
|
3313
|
+
// Vision, real work: read as Real work is. The one question: when its seat was judged,
|
|
3314
|
+
// did it help? The dot is the share judged useful (checked and helped, or useful with no
|
|
3315
|
+
// claims to check), sized by how many were judged; the line where that share likely sits
|
|
3316
|
+
// (95% Wilson), ranked by its low end. Beside it, in words: how often it answered at all,
|
|
3317
|
+
// and the claims checked by hand. Seats never judged or never answered say nothing of
|
|
3318
|
+
// its worth: they are in the words and the table, not the dot.
|
|
3269
3319
|
put('chart-vision-work', function (w) {
|
|
3270
|
-
var W = C.vision.work, kinds = C.vision.seatKinds,
|
|
3320
|
+
var W = C.vision.work, kinds = C.vision.seatKinds, R = [], none = [];
|
|
3271
3321
|
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'};
|
|
3322
|
+
var v = W[m], n = function (k) { return v[k] || 0; }, total = 0;
|
|
3323
|
+
kinds.forEach(function (k) { total += n(k); });
|
|
3324
|
+
if (!total) return;
|
|
3325
|
+
var judged = n('checked, helped') + n('checked, did not help') + n('useful, no claims') + n('no use, no claims');
|
|
3326
|
+
var useful = n('checked, helped') + n('useful, no claims');
|
|
3327
|
+
var r = {model: m, total: total, judged: judged, useful: useful, answered: total - n('no answer'),
|
|
3328
|
+
real: v.real || 0, wrongC: v.false || 0};
|
|
3329
|
+
if (!judged) { none.push(r); return; }
|
|
3330
|
+
var b = wilson(useful, judged);
|
|
3331
|
+
r.p = useful / judged; r.lo = b[0]; r.hi = b[1];
|
|
3332
|
+
r.verdict = b[0] >= 0.5 ? 'helps' : b[1] < 0.5 ? 'does not help' : 'too few to say';
|
|
3333
|
+
R.push(r);
|
|
3287
3334
|
});
|
|
3335
|
+
if (!R.length && !none.length) return el('p', {class: 'hint'}, 'No vision panels through the gateway yet.');
|
|
3336
|
+
R.sort(function (a, b) { return b.lo - a.lo || b.judged - a.judged; });
|
|
3288
3337
|
var narrowV = w < 560, box = el('div');
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3338
|
+
function words(r) {
|
|
3339
|
+
return r.useful + '/' + r.judged + (narrowV ? '' : ' · ' + Math.round(r.answered / r.total * 100) + '% answered'
|
|
3340
|
+
+ (r.real + r.wrongC ? ' · claims ' + r.real + '/' + (r.real + r.wrongC) : ''));
|
|
3341
|
+
}
|
|
3342
|
+
if (R.length) box.appendChild(Plot.plot({
|
|
3343
|
+
width: w, height: 24 * R.length + 90, style: style, marginLeft: narrowV ? 130 : 190,
|
|
3344
|
+
marginRight: narrowV ? 50 : 260, marginTop: 34,
|
|
3345
|
+
x: {domain: [0, 1], label: 'Judged useful, share of its judged seats →', tickFormat: '.0%', ticks: 5, grid: true},
|
|
3346
|
+
y: {domain: R.map(function (r) { return r.model; }), label: null,
|
|
3347
|
+
tickFormat: function (m) { return fit(m, narrowV ? 16 : 26); }},
|
|
3348
|
+
r: {domain: [0, Math.max.apply(null, R.map(function (r) { return r.judged; }))], range: [3, 11]},
|
|
3349
|
+
color: {domain: ['helps', 'too few to say', 'does not help'], range: [good, muted, err], legend: true},
|
|
3294
3350
|
marks: [
|
|
3295
|
-
Plot.
|
|
3296
|
-
|
|
3297
|
-
Plot.
|
|
3298
|
-
|
|
3351
|
+
Plot.ruleX([0.5], {stroke: line}),
|
|
3352
|
+
Plot.ruleY(R, {y: 'model', x1: 'lo', x2: 'hi', stroke: 'verdict', strokeWidth: 2, strokeOpacity: 0.55}),
|
|
3353
|
+
Plot.dot(R, {x: 'p', y: 'model', r: 'judged', fill: 'verdict', stroke: halo, strokeWidth: 1.5,
|
|
3354
|
+
title: function (r) {
|
|
3355
|
+
return r.model + '\njudged useful ' + r.useful + ' of ' + r.judged + ' judged seats ('
|
|
3356
|
+
+ Math.round(r.p * 100) + '%), likely between ' + Math.round(r.lo * 100) + '% and ' + Math.round(r.hi * 100)
|
|
3357
|
+
+ '%\nanswered ' + r.answered + ' of ' + r.total + ' seats on image panels'
|
|
3358
|
+
+ (r.real + r.wrongC ? '\nclaims checked by hand: ' + r.real + ' right of ' + (r.real + r.wrongC) : '');
|
|
3359
|
+
}}),
|
|
3360
|
+
Plot.text(R, {x: 1, y: 'model', dx: 10, textAnchor: 'start', fill: ink, fontSize: 11, text: words})
|
|
3299
3361
|
]
|
|
3300
3362
|
}));
|
|
3301
|
-
if (
|
|
3302
|
-
+
|
|
3363
|
+
if (none.length) box.appendChild(el('p', {class: 'hint'}, 'No seat judged yet, so no dot: '
|
|
3364
|
+
+ none.map(function (r) { return r.model + ' (' + r.total + ' seats, answered ' + r.answered + ')'; }).join(', ')
|
|
3365
|
+
+ '. The table has every seat.'));
|
|
3303
3366
|
return box;
|
|
3304
3367
|
});
|
|
3305
3368
|
|
|
@@ -3338,10 +3401,50 @@
|
|
|
3338
3401
|
});
|
|
3339
3402
|
|
|
3340
3403
|
}
|
|
3341
|
-
onLoad.suite = function () { drawCharts('suite'); };
|
|
3404
|
+
onLoad.suite = function () { drawCharts('suite'); jumpSpy(); };
|
|
3405
|
+
// Benchmarks' jump bar: the report in view is marked (aria-current) and its link kept in
|
|
3406
|
+
// sight in a bar that scrolls sideways. Set up again after a live swap (setUp, onLoad).
|
|
3407
|
+
var spy = null;
|
|
3408
|
+
function jumpSpy() {
|
|
3409
|
+
if (spy) spy.disconnect();
|
|
3410
|
+
var nav = document.querySelector('#tab-suite .jump');
|
|
3411
|
+
if (!nav || !window.IntersectionObserver) return;
|
|
3412
|
+
var links = {}, seen = {}, current = null;
|
|
3413
|
+
nav.querySelectorAll('a').forEach(function (a) { links[a.getAttribute('href').slice(1)] = a; });
|
|
3414
|
+
// A report counts as in view while it crosses a thin band 15-20% down the window: just
|
|
3415
|
+
// under where a jump lands it, so the one jumped to is the one marked.
|
|
3416
|
+
spy = new IntersectionObserver(function (entries) {
|
|
3417
|
+
entries.forEach(function (e) { seen[e.target.id] = e.isIntersecting; });
|
|
3418
|
+
var id = Object.keys(links).filter(function (k) { return seen[k]; })[0];
|
|
3419
|
+
if (!id || id === current) return;
|
|
3420
|
+
if (current) links[current].removeAttribute('aria-current');
|
|
3421
|
+
current = id;
|
|
3422
|
+
links[id].setAttribute('aria-current', 'true');
|
|
3423
|
+
var a = links[id];
|
|
3424
|
+
if (a.offsetLeft < nav.scrollLeft || a.offsetLeft + a.offsetWidth > nav.scrollLeft + nav.clientWidth) {
|
|
3425
|
+
nav.scrollLeft = a.offsetLeft - 16;
|
|
3426
|
+
}
|
|
3427
|
+
}, {rootMargin: '-15% 0px -80% 0px'});
|
|
3428
|
+
document.querySelectorAll('#tab-suite .panel.report').forEach(function (p) { spy.observe(p); });
|
|
3429
|
+
}
|
|
3430
|
+
// A jump scrolls smoothly (unless motion is reduced) and moves focus to the report, so
|
|
3431
|
+
// a keyboard user carries on from there; the address keeps the report's #, as a link would.
|
|
3432
|
+
document.addEventListener('click', function (e) {
|
|
3433
|
+
var a = e.target.closest('.jump a');
|
|
3434
|
+
if (!a) return;
|
|
3435
|
+
var target = $(a.getAttribute('href').slice(1));
|
|
3436
|
+
if (!target) return;
|
|
3437
|
+
e.preventDefault();
|
|
3438
|
+
var still = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
3439
|
+
target.scrollIntoView({behavior: still ? 'auto' : 'smooth', block: 'start'});
|
|
3440
|
+
target.setAttribute('tabindex', '-1');
|
|
3441
|
+
target.focus({preventScroll: true});
|
|
3442
|
+
history.replaceState(null, '', a.getAttribute('href'));
|
|
3443
|
+
});
|
|
3342
3444
|
// Uptime's or Trend's window: that window's tables (and Uptime's list) shown, its charts drawn again.
|
|
3343
3445
|
document.addEventListener('change', function (ev) {
|
|
3344
3446
|
var s = ev.target, part = s.matches('select[data-uptime-days]') ? 'uptime' : s.matches('select[data-trend-days]') ? 'trend' : '';
|
|
3447
|
+
if (s.matches('select[data-uptime-by]')) { drawCharts('uptime'); return; } // the other view, same window
|
|
3345
3448
|
if (!part) return;
|
|
3346
3449
|
s.closest('.panel').querySelectorAll('[data-' + part + '-window]').forEach(function (d) {
|
|
3347
3450
|
d.hidden = d.getAttribute('data-' + part + '-window') !== s.value;
|