@ancha-labs/cli 0.1.0 → 0.1.1
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/lib/ancha.mjs +3554 -3165
- package/lib/assets/examples/playground-agent/tasks.json +28 -2
- package/lib/assets/harbor/companion.mjs +2609 -2276
- package/lib/assets/harbor/driver.py +4 -4
- package/lib/assets/templates/starter/oracle.mjs +5 -1
- package/lib/assets/ui/app.js +341 -248
- package/lib/assets/ui/style.css +31 -0
- package/lib/mcp-entry.mjs +2584 -2254
- package/lib/worker.mjs +2730 -2396
- package/package.json +1 -1
package/lib/assets/ui/app.js
CHANGED
|
@@ -159,7 +159,7 @@ function seedData(scene, selected) {
|
|
|
159
159
|
return `<h2>${h(toolName(selected))}</h2>${body || empty("No data seeded")}${details("Full starting data", s)}${selected === "datadog" ? details("Generated telemetry definitions", { metrics: scene.world_rules?.metrics, logs: scene.world_rules?.logs, monitors: scene.world_rules?.monitors }) : ""}`;
|
|
160
160
|
}
|
|
161
161
|
var countLabel = (count, noun, plural = noun + "s") => `${count} ${count === 1 ? noun : plural}`;
|
|
162
|
-
var toolNames = { slack: "Slack", datadog: "Datadog", pagerduty: "PagerDuty", github: "GitHub", "linkedin-ads": "LinkedIn Ads", salesforce: "Salesforce", gong: "Gong", "google-ads": "Google Ads", "chatgpt-ads": "ChatGPT Ads", hubspot: "HubSpot", ga4: "Google Analytics" };
|
|
162
|
+
var toolNames = { slack: "Slack", "open-meteo": "Open-Meteo", datadog: "Datadog", pagerduty: "PagerDuty", github: "GitHub", "linkedin-ads": "LinkedIn Ads", salesforce: "Salesforce", gong: "Gong", "google-ads": "Google Ads", "chatgpt-ads": "ChatGPT Ads", hubspot: "HubSpot", ga4: "Google Analytics" };
|
|
163
163
|
var toolName = (name) => toolNames[name] ?? name;
|
|
164
164
|
var toolLinks = (names) => names.map((name) => `<a class="tool-chip" href="/tools/${encodeURIComponent(name)}" data-nav>${h(toolName(name))}</a>`).join("");
|
|
165
165
|
var toolDescription = (name) => ({ slack: "Channels, messages and threads", datadog: "Monitors, metrics and logs", pagerduty: "Incidents, on-call and escalation", github: "Repositories, pull requests and deployments" })[name] ?? "API capabilities";
|
|
@@ -325,240 +325,6 @@ function toolLogo(name) {
|
|
|
325
325
|
return svg ? `<img class="catalog-brand" src="data:image/svg+xml,${encodeURIComponent(svg)}" alt="" width="36" height="36">` : '<span class="catalog-brand" aria-hidden="true">\u25C7</span>';
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
-
// frontend/src/world-workspace.ts
|
|
329
|
-
var enc = encodeURIComponent;
|
|
330
|
-
var label = (task, index) => task.title && task.title !== String(task.prompt).split("\n")[0]?.slice(0, 120) ? task.title : "Task " + (index + 1);
|
|
331
|
-
function worldSetups(tests, worldId, agentId) {
|
|
332
|
-
return tests.filter((test) => !test.archived && test.worldId === worldId && (!agentId || test.agentId === agentId));
|
|
333
|
-
}
|
|
334
|
-
function setupRuns(runs, test, exactRevision = true) {
|
|
335
|
-
return runs.filter((run) => !run.entries?.length && run.test?.id === test.id && (!exactRevision || run.test.revision === test.revision) && run.agentId === test.agentId && run.world?.id === test.worldId && (!exactRevision || run.world.revision === test.worldRevision)).sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
|
|
336
|
-
}
|
|
337
|
-
function setupHistory(runs, test) {
|
|
338
|
-
const singles = new Set(setupRuns(runs, test, false).map((run) => run.id));
|
|
339
|
-
return runs.flatMap((run) => {
|
|
340
|
-
if (singles.has(run.id)) return [{ run, attemptCount: run.attempts?.length ?? 0, earlier: run.test.revision !== test.revision || run.world.revision !== test.worldRevision, grouped: false }];
|
|
341
|
-
const entries = (run.entries ?? []).filter((entry) => entry.test?.id === test.id && entry.agentId === test.agentId && entry.world?.id === test.worldId && entry.test.agentId === test.agentId && entry.test.worldId === test.worldId && entry.test.worldRevision === entry.world.revision);
|
|
342
|
-
if (!entries.length) return [];
|
|
343
|
-
const ids = new Set(entries.map((entry) => entry.id));
|
|
344
|
-
return [{
|
|
345
|
-
run,
|
|
346
|
-
attemptCount: (run.attempts ?? []).filter((attempt) => ids.has(attempt.entryId)).length,
|
|
347
|
-
earlier: entries.some((entry) => entry.test.revision !== test.revision || entry.world.revision !== test.worldRevision),
|
|
348
|
-
grouped: true
|
|
349
|
-
}];
|
|
350
|
-
}).sort((a, b) => String(b.run.createdAt).localeCompare(String(a.run.createdAt)));
|
|
351
|
-
}
|
|
352
|
-
function worldHistory(runs, worldId, agentId) {
|
|
353
|
-
return runs.flatMap((run) => {
|
|
354
|
-
if (!run.entries?.length) return run.world?.id === worldId && (!agentId || run.agentId === agentId) ? [{ run, attemptCount: run.attempts?.length ?? 0, grouped: false }] : [];
|
|
355
|
-
const entries = run.entries.filter((entry) => entry.world?.id === worldId && (!agentId || entry.agentId === agentId));
|
|
356
|
-
if (!entries.length) return [];
|
|
357
|
-
const ids = new Set(entries.map((entry) => entry.id));
|
|
358
|
-
return [{ run, attemptCount: (run.attempts ?? []).filter((attempt) => ids.has(attempt.entryId)).length, grouped: true }];
|
|
359
|
-
}).sort((a, b) => String(b.run.createdAt).localeCompare(String(a.run.createdAt)));
|
|
360
|
-
}
|
|
361
|
-
function workspaceDefinition(draft) {
|
|
362
|
-
return {
|
|
363
|
-
name: draft.name,
|
|
364
|
-
agentId: draft.agentId,
|
|
365
|
-
worldId: draft.worldId,
|
|
366
|
-
worldRevision: Number(draft.worldRevision),
|
|
367
|
-
tasks: draft.tasks.map((task) => ({ id: task.id, ...String(task.title ?? "").trim() ? { title: String(task.title).trim() } : {}, prompt: task.prompt, attempts: Number(task.attempts), timeoutMs: Number(task.timeoutMs) })),
|
|
368
|
-
concurrency: Number(draft.concurrency),
|
|
369
|
-
seed: draft.seed ?? "playground-1"
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
function mountWorldWorkspace(node, options) {
|
|
373
|
-
const { world, agents, api: api2, go: go2 } = options, base = "/worlds/" + enc(world.id), q = new URLSearchParams(location.search);
|
|
374
|
-
let tests = [], runs = [], errors = [], selected, agentId = q.get("agent") ?? "", configure = q.has("configure"), showHistory = false, saving = false, disposed = false, error = "", loaded = false;
|
|
375
|
-
const revisions = /* @__PURE__ */ new Map([[world.revision, world]]);
|
|
376
|
-
let draft, timer;
|
|
377
|
-
const launchPending = () => {
|
|
378
|
-
if (!selected) return false;
|
|
379
|
-
try {
|
|
380
|
-
return sessionStorage.getItem("ancha-saved-test-run:" + selected.id) !== null;
|
|
381
|
-
} catch {
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
};
|
|
385
|
-
const key = () => "ancha-world-setup:" + world.id + ":" + (selected?.id ?? "new:" + agentId);
|
|
386
|
-
const remember = () => {
|
|
387
|
-
if (draft) try {
|
|
388
|
-
sessionStorage.setItem(key(), JSON.stringify(draft));
|
|
389
|
-
} catch {
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
function getDraft() {
|
|
393
|
-
if (draft) return draft;
|
|
394
|
-
try {
|
|
395
|
-
const stored = JSON.parse(sessionStorage.getItem(key()) ?? "null");
|
|
396
|
-
if (stored && Array.isArray(stored.tasks)) draft = stored;
|
|
397
|
-
} catch {
|
|
398
|
-
}
|
|
399
|
-
draft ??= selected ? { ...selected, tasks: structuredClone(selected.tasks) } : { name: world.name, agentId, worldId: world.id, worldRevision: world.revision, concurrency: 1, seed: "playground-1", tasks: [{ id: "task_" + crypto.randomUUID(), title: "", prompt: "", attempts: 1, timeoutMs: 3e5 }] };
|
|
400
|
-
return draft;
|
|
401
|
-
}
|
|
402
|
-
function capture() {
|
|
403
|
-
const form = node.querySelector(".ww-form");
|
|
404
|
-
if (!form) return;
|
|
405
|
-
const d = getDraft(), values = new FormData(form);
|
|
406
|
-
d.name = String(values.get("name") ?? "");
|
|
407
|
-
d.agentId = String(values.get("agentId") ?? "");
|
|
408
|
-
d.concurrency = Number(values.get("concurrency"));
|
|
409
|
-
d.worldRevision = Number(values.get("worldRevision"));
|
|
410
|
-
d.tasks = [...form.querySelectorAll(".ww-task-editor")].map((row) => ({ id: row.dataset.taskId, title: row.querySelector("[name=title]").value, prompt: row.querySelector("[name=prompt]").value, attempts: Number(row.querySelector("[name=attempts]").value), timeoutMs: Math.round(Number(row.querySelector("[name=minutes]").value) * 6e4) }));
|
|
411
|
-
remember();
|
|
412
|
-
}
|
|
413
|
-
function href2(test, agent = agentId, extra = "") {
|
|
414
|
-
return base + "?" + new URLSearchParams({ ...agent ? { agent } : {}, ...test ? { test: test.id } : {}, ...extra ? { [extra]: "1" } : {} });
|
|
415
|
-
}
|
|
416
|
-
function configuration2() {
|
|
417
|
-
const d = getDraft(), pinned = Number(d.worldRevision) !== world.revision, startingWorld = revisions.get(Number(d.worldRevision));
|
|
418
|
-
return `<section class="ww-configure"><div class="ww-section-heading"><h2>Configure</h2><button type="button" data-ww="close">Close</button></div><form class="ww-form"><div class="form-grid"><label>Agent<select name="agentId" required><option value="">Choose agent</option>${agents.map((a) => `<option value="${h(a.id)}" ${d.agentId === a.id ? "selected" : ""}>${h(a.name)}</option>`).join("")}</select></label><label>Configuration name<input name="name" required maxlength="120" value="${h(d.name)}"></label></div><section class="ww-tools"><div class="ww-section-heading"><h3>Tools</h3><a data-nav href="${base}?edit=1">Edit world</a></div><div class="ww-tool-list">${(startingWorld?.configuration.preset.datasets ?? []).map((item) => `<a data-nav href="/tools/${enc(item.tool)}">${toolLogo(item.tool)}${h(toolName(item.tool))}</a>`).join("")}</div>${pinned ? `<label>Starting data<select name="worldRevision"><option value="${h(d.worldRevision)}">Saved revision ${h(d.worldRevision)}</option><option value="${world.revision}">Current revision ${world.revision}</option></select></label>` : `<input type="hidden" name="worldRevision" value="${h(d.worldRevision)}">`}</section><div class="ww-section-heading"><h3>Tasks</h3><button type="button" data-ww="add-task" ${d.tasks.length >= 20 ? "disabled" : ""}>Add task</button></div><div class="ww-task-fields">${d.tasks.map((task, index) => `<article class="ww-task-editor" data-task-id="${h(task.id)}"><div class="ww-section-heading"><h3>Task ${index + 1}</h3>${d.tasks.length > 1 ? `<button type="button" data-ww="remove-task" data-value="${h(task.id)}">Remove</button>` : ""}</div><label>Task name <span class="muted">optional</span><input name="title" maxlength="120" value="${h(task.title ?? "")}" placeholder="Task ${index + 1}"></label><label>Instructions<textarea name="prompt" required maxlength="20000" rows="4">${h(task.prompt)}</textarea></label><div class="ww-task-options"><label>Repeat<input type="number" name="attempts" min="1" max="100" value="${h(task.attempts)}" required><span>times</span></label><label>Time limit <span class="muted">minutes</span><input type="number" name="minutes" min="0.016666666666666666" max="60" step="any" value="${h(task.timeoutMs / 6e4)}" required></label></div></article>`).join("")}</div><div class="ww-footer"><label>Run at once<input name="concurrency" type="number" min="1" max="10" value="${h(d.concurrency)}" required></label><button type="submit" class="primary" ${saving ? "disabled" : ""}>${saving ? "Saving\u2026" : "Save configuration"}</button></div></form></section>`;
|
|
419
|
-
}
|
|
420
|
-
function render() {
|
|
421
|
-
if (disposed) return;
|
|
422
|
-
const currentRuns = selected ? setupRuns(runs, selected) : [], run = currentRuns[0], attempts = run?.attempts ?? [], tasks = selected?.tasks ?? [], choices = worldSetups(tests, world.id, agentId);
|
|
423
|
-
node.innerHTML = `<div class="ww-workspace"><header class="ww-header"><h1>${h(world.name)}</h1><div class="ww-world-actions"><label class="ww-agent-select">Agent<select data-ww-select="agent"><option value="" disabled>Choose agent</option>${agents.map((a) => `<option value="${h(a.id)}" ${a.id === agentId ? "selected" : ""}>${h(a.name)}</option>`).join("")}</select></label><button type="button" data-ww="configure">Configure</button>${selected ? options.runActions?.(selected.id) ?? button("Run tasks", "run-saved-test", selected.id, "primary") : ""}</div></header><p class="ww-error" role="alert" ${error ? "" : "hidden"}>${h(error)}</p>${errors.map((e) => `<p class="ww-error" role="alert">${h(e.message)}</p>`).join("")}${!loaded ? '<p role="status">Loading tasks\u2026</p>' : ""}${choices.length > 1 ? `<label class="ww-setup-select">Configuration<select data-ww-select="test">${!selected ? '<option value="" selected disabled>New configuration</option>' : ""}${choices.map((t) => `<option value="${h(t.id)}" ${t.id === selected?.id ? "selected" : ""}>${h(t.name)}</option>`).join("")}</select></label>` : ""}${loaded && configure ? configuration2() : loaded ? `<section class="ww-summary-widgets"><article><span>Success rate</span><strong class="muted">\u2014</strong></article><article><span>Attempts</span><strong>${attempts.length}</strong></article><article><span>Ungraded</span><strong>${attempts.length}</strong></article></section><div class="ww-section-heading"><h2>Tasks${tasks.length ? ` <span class="ww-task-count">${tasks.length}</span>` : ""}</h2><div class="actions">${worldHistory(runs, world.id, agentId || void 0).length || selected ? `<button type="button" data-ww="history" aria-expanded="${showHistory}">History</button>` : ""}<button type="button" data-ww="configure">${tasks.length ? "Edit tasks" : "Add tasks"}</button></div></div>${tasks.length ? `<section class="ww-task-table"><div class="ww-table-head"><span>Task</span><span>Success rate</span><span>Attempts</span><span></span></div>${tasks.map((task, index) => {
|
|
424
|
-
const list2 = attempts.filter((a) => a.taskId === task.id), taskURL = run ? `/playgrounds/${enc(run.id)}?task=${enc(task.id)}${list2[0] ? "&attempt=" + enc(list2[0].id) : ""}` : "";
|
|
425
|
-
return `<article class="ww-task-row"><div class="ww-task-identity">${taskURL ? `<a data-nav class="ww-task-name" href="${taskURL}"><span class="ww-ordinal">${index + 1}</span><strong>${h(label(task, index))}</strong></a>` : `<strong class="ww-task-name"><span class="ww-ordinal">${index + 1}</span>${h(label(task, index))}</strong>`}<p class="ww-task-prompt" title="${h(task.prompt)}">${h(task.prompt)}</p></div><div class="ww-rate">${list2.length ? "Ungraded" : "\u2014"}</div><div class="ww-attempt-marks" role="group" aria-label="Attempts for ${h(label(task, index))}">${list2.map((a) => `<a data-nav class="ww-attempt-mark ${h(a.status)}" href="/playgrounds/${enc(run.id)}?task=${enc(task.id)}&attempt=${enc(a.id)}" title="Attempt ${a.number}: ${h(attemptLabel(a.status))} \xB7 Ungraded" aria-label="Attempt ${a.number}: ${h(attemptLabel(a.status))} \xB7 Ungraded">${a.status === "error" ? "!" : ["running", "starting"].includes(a.status) ? "\u25CC" : "\u2212"}</a>`).join("") || `<span class="muted">Repeat ${h(task.attempts)} times</span>`}</div>${taskURL ? `<a data-nav class="ww-row-inspect" href="${taskURL}" aria-label="Inspect ${h(label(task, index))}">\u2192</a>` : "<span></span>"}</article>`;
|
|
426
|
-
}).join("")}</section>` : `<div class="quiet-empty">${agents.length ? "Add tasks to this world." : '<a data-nav href="/agents">Connect an agent</a>'}</div>`}${selected && !currentRuns.length && setupHistory(runs, selected).some((item) => item.earlier) ? '<p class="ww-snapshot-notice">This configuration has changed. Earlier attempts are in History.</p>' : ""}${showHistory ? history2() : ""}` : ""}</div>`;
|
|
427
|
-
}
|
|
428
|
-
function history2() {
|
|
429
|
-
const all = worldHistory(runs, world.id, agentId || void 0);
|
|
430
|
-
return `<section class="ww-history"><div class="ww-section-heading"><h2>History</h2><button type="button" data-ww="new">New configuration</button></div>${all.length ? all.map(({ run, attemptCount, grouped }) => `<a data-nav class="ww-history-row" href="/playgrounds/${enc(run.id)}"><time>${h(new Date(run.createdAt).toLocaleString())}</time><span>${h(attemptLabel(run.status))}</span><span>${attemptCount} attempts</span>${grouped ? "<span>Group</span>" : ""}${run.test?.name || run.groupName ? `<span>${h(run.groupName ?? run.test.name)}</span>` : ""}<span>\u2192</span></a>`).join("") : '<p class="quiet-empty">No attempts yet.</p>'}</section>`;
|
|
431
|
-
}
|
|
432
|
-
async function refresh(initial = false) {
|
|
433
|
-
try {
|
|
434
|
-
const [definitions, batches] = await Promise.all([api2("/playground-tests"), api2("/playgrounds")]);
|
|
435
|
-
if (disposed) return;
|
|
436
|
-
tests = definitions.tests ?? [];
|
|
437
|
-
runs = batches.runs ?? [];
|
|
438
|
-
errors = [...definitions.errors ?? [], ...batches.errors ?? []];
|
|
439
|
-
if (initial) {
|
|
440
|
-
const requested = q.get("test");
|
|
441
|
-
if (requested) {
|
|
442
|
-
selected = worldSetups(tests, world.id).find((test) => test.id === requested);
|
|
443
|
-
if (!selected || agentId && selected.agentId !== agentId) {
|
|
444
|
-
selected = void 0;
|
|
445
|
-
error = "This configuration is unavailable for this world and agent.";
|
|
446
|
-
} else agentId = selected.agentId;
|
|
447
|
-
} else if (q.has("new")) {
|
|
448
|
-
selected = void 0;
|
|
449
|
-
agentId ||= agents[0]?.id ?? "";
|
|
450
|
-
} else {
|
|
451
|
-
selected = worldSetups(tests, world.id, agentId || void 0)[0];
|
|
452
|
-
const historical = worldHistory(runs, world.id)[0]?.run;
|
|
453
|
-
agentId ||= selected?.agentId ?? historical?.entries?.find((entry) => entry.world?.id === world.id)?.agentId ?? historical?.agentId ?? agents[0]?.id ?? "";
|
|
454
|
-
}
|
|
455
|
-
} else if (selected) selected = tests.find((test) => test.id === selected.id);
|
|
456
|
-
if (initial && !selected && worldHistory(runs, world.id, agentId || void 0).length) showHistory = true;
|
|
457
|
-
if (configure) getDraft();
|
|
458
|
-
options.onContext?.(selected);
|
|
459
|
-
const pinnedRevision = Number(draft?.worldRevision ?? selected?.worldRevision ?? world.revision);
|
|
460
|
-
if (!revisions.has(pinnedRevision)) {
|
|
461
|
-
const savedWorld = await api2(`/worlds/${enc(world.id)}?revision=${pinnedRevision}`);
|
|
462
|
-
if (disposed) return;
|
|
463
|
-
revisions.set(pinnedRevision, savedWorld);
|
|
464
|
-
}
|
|
465
|
-
loaded = true;
|
|
466
|
-
if (!configure) render();
|
|
467
|
-
else if (initial) render();
|
|
468
|
-
if (runs.some((run) => !terminal(run.status))) timer = setTimeout(() => {
|
|
469
|
-
void refresh();
|
|
470
|
-
}, 2e3);
|
|
471
|
-
} catch (e) {
|
|
472
|
-
if (!disposed) {
|
|
473
|
-
error = e instanceof Error ? e.message : String(e);
|
|
474
|
-
loaded = true;
|
|
475
|
-
render();
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
node.addEventListener("input", capture);
|
|
480
|
-
node.addEventListener("change", (event) => {
|
|
481
|
-
const control = event.target;
|
|
482
|
-
if (control.dataset.wwSelect === "agent") {
|
|
483
|
-
capture();
|
|
484
|
-
const match = worldSetups(tests, world.id, control.value)[0];
|
|
485
|
-
go2(href2(match, control.value));
|
|
486
|
-
} else if (control.dataset.wwSelect === "test") go2(href2(tests.find((t) => t.id === control.value)));
|
|
487
|
-
else {
|
|
488
|
-
capture();
|
|
489
|
-
if (control.name === "worldRevision") render();
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
node.addEventListener("click", (event) => {
|
|
493
|
-
const control = event.target.closest("[data-ww]");
|
|
494
|
-
if (!control) return;
|
|
495
|
-
event.preventDefault();
|
|
496
|
-
event.stopPropagation();
|
|
497
|
-
if (saving) return;
|
|
498
|
-
capture();
|
|
499
|
-
error = "";
|
|
500
|
-
switch (control.dataset.ww) {
|
|
501
|
-
case "configure":
|
|
502
|
-
if (launchPending()) {
|
|
503
|
-
error = "Resolve the pending run before editing this configuration.";
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
configure = true;
|
|
507
|
-
break;
|
|
508
|
-
case "close":
|
|
509
|
-
configure = false;
|
|
510
|
-
break;
|
|
511
|
-
case "history":
|
|
512
|
-
showHistory = !showHistory;
|
|
513
|
-
break;
|
|
514
|
-
case "new":
|
|
515
|
-
go2(href2(void 0, agentId, "new") + "&configure=1");
|
|
516
|
-
return;
|
|
517
|
-
case "add-task":
|
|
518
|
-
if (getDraft().tasks.length < 20) getDraft().tasks.push({ id: "task_" + crypto.randomUUID(), title: "", prompt: "", attempts: 1, timeoutMs: 3e5 });
|
|
519
|
-
break;
|
|
520
|
-
case "remove-task":
|
|
521
|
-
if (getDraft().tasks.length > 1) getDraft().tasks = getDraft().tasks.filter((t) => t.id !== control.dataset.value);
|
|
522
|
-
break;
|
|
523
|
-
}
|
|
524
|
-
remember();
|
|
525
|
-
render();
|
|
526
|
-
});
|
|
527
|
-
node.addEventListener("submit", (event) => {
|
|
528
|
-
const form = event.target;
|
|
529
|
-
if (!form.matches(".ww-form")) return;
|
|
530
|
-
event.preventDefault();
|
|
531
|
-
event.stopPropagation();
|
|
532
|
-
if (saving) return;
|
|
533
|
-
if (launchPending()) {
|
|
534
|
-
error = "Resolve the pending run before editing this configuration.";
|
|
535
|
-
render();
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
capture();
|
|
539
|
-
saving = true;
|
|
540
|
-
error = "";
|
|
541
|
-
render();
|
|
542
|
-
const d = getDraft(), body = workspaceDefinition(d), oldKey = key();
|
|
543
|
-
void api2(selected ? `/playground-tests/${enc(selected.id)}/update` : "/playground-tests", { ...body, ...selected ? { revision: d.revision } : {} }).then((saved) => {
|
|
544
|
-
sessionStorage.removeItem(oldKey);
|
|
545
|
-
if (!disposed) go2(href2(saved, saved.agentId));
|
|
546
|
-
}).catch((e) => {
|
|
547
|
-
if (!disposed) {
|
|
548
|
-
error = e instanceof Error ? e.message : String(e);
|
|
549
|
-
saving = false;
|
|
550
|
-
render();
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
});
|
|
554
|
-
render();
|
|
555
|
-
void refresh(true);
|
|
556
|
-
return () => {
|
|
557
|
-
disposed = true;
|
|
558
|
-
clearTimeout(timer);
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
328
|
// frontend/src/experience.ts
|
|
563
329
|
var list = (value) => Array.isArray(value) ? value.filter((v) => v && typeof v === "object") : [];
|
|
564
330
|
var text = (value) => typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" ? String(value) : "";
|
|
@@ -915,7 +681,15 @@ function taskDisplayName(task, index) {
|
|
|
915
681
|
const title2 = typeof task.title === "string" ? task.title.trim().replace(/\s+/g, " ") : "";
|
|
916
682
|
return title2 && title2.length <= 80 ? title2 : `Task ${index + 1}`;
|
|
917
683
|
}
|
|
684
|
+
function attemptOutcome(attempt) {
|
|
685
|
+
if (attempt.outcome === "passed") return { key: "passed", label: "Passed", tone: "pass", mark: "\u2713" };
|
|
686
|
+
if (attempt.outcome === "failed") return { key: "failed", label: "Failed", tone: "fail", mark: "\u2717" };
|
|
687
|
+
return { key: "ungraded", label: "Ungraded", tone: "neutral", mark: "\u2013" };
|
|
688
|
+
}
|
|
918
689
|
function executionSummary(attempts) {
|
|
690
|
+
const finished = attempts.filter((a) => ["completed", "error", "stopped"].includes(a.status));
|
|
691
|
+
const durations = finished.map((a) => Date.parse(a.finishedAt) - Date.parse(a.startedAt)).filter((ms) => Number.isFinite(ms) && ms >= 0).sort((x, y) => x - y);
|
|
692
|
+
const passed = attempts.filter((a) => a.outcome === "passed").length, failed = attempts.filter((a) => a.outcome === "failed").length;
|
|
919
693
|
return {
|
|
920
694
|
total: attempts.length,
|
|
921
695
|
completed: attempts.filter((a) => a.status === "completed").length,
|
|
@@ -923,17 +697,61 @@ function executionSummary(attempts) {
|
|
|
923
697
|
queued: attempts.filter((a) => a.status === "queued").length,
|
|
924
698
|
error: attempts.filter((a) => a.status === "error").length,
|
|
925
699
|
stopped: attempts.filter((a) => a.status === "stopped").length,
|
|
926
|
-
|
|
700
|
+
finished: finished.length,
|
|
701
|
+
passed,
|
|
702
|
+
failed,
|
|
703
|
+
graded: passed + failed,
|
|
704
|
+
ungraded: attempts.length - passed - failed,
|
|
705
|
+
toolCalls: attempts.reduce((sum, a) => sum + (Number(a.toolCalls) || 0), 0),
|
|
706
|
+
medianMs: durations.length ? durations[Math.floor((durations.length - 1) / 2)] : void 0
|
|
927
707
|
};
|
|
928
708
|
}
|
|
709
|
+
var percent = (part, whole) => whole ? `${Math.round(part / whole * 100)}%` : "\u2014";
|
|
710
|
+
var duration = (ms) => ms === void 0 ? "\u2014" : ms < 1e4 ? `${(ms / 1e3).toFixed(1)}s` : ms < 12e4 ? `${Math.round(ms / 1e3)}s` : `${Math.round(ms / 6e4)}m`;
|
|
711
|
+
function summaryWidgets(attempts, wrapper = "aw-widgets", item = 'section class="aw-widget"') {
|
|
712
|
+
const s = executionSummary(attempts), tag = item.split(" ")[0];
|
|
713
|
+
const cell = (label3, value, note) => `<${item}><span>${h(label3)}</span><strong>${h(value)}</strong><small>${h(note)}</small></${tag}>`;
|
|
714
|
+
const progress = s.active || s.queued ? [s.active && `${s.active} running`, s.queued && `${s.queued} queued`].filter(Boolean).join(" \xB7 ") : s.error ? `${s.error} ended with an error` : s.total ? "All finished" : "No attempts yet";
|
|
715
|
+
const rate = s.graded ? `${s.passed} of ${s.graded} passed` : s.total ? "Add checks to a task to grade it" : "No attempts yet";
|
|
716
|
+
return `<div class="${wrapper} four">${cell("Success rate", percent(s.passed, s.graded), rate)}${cell("Finished", `${s.finished}/${s.total}`, progress)}${cell("Tool calls", String(s.toolCalls), s.finished ? `${(s.toolCalls / s.finished).toFixed(1)} per attempt` : "\u2014")}${cell("Median time", duration(s.medianMs), "per attempt")}</div>${progressBar(attempts)}`;
|
|
717
|
+
}
|
|
718
|
+
function attemptState(attempt) {
|
|
719
|
+
const outcome = attemptOutcome(attempt);
|
|
720
|
+
if (outcome.key !== "ungraded") return outcome.key;
|
|
721
|
+
return ["queued", "starting", "running", "completed", "stopped", "error"].includes(attempt.status) ? attempt.status === "starting" ? "running" : attempt.status : "queued";
|
|
722
|
+
}
|
|
723
|
+
function progressBar(attempts) {
|
|
724
|
+
if (!attempts.length) return "";
|
|
725
|
+
const s = executionSummary(attempts), label3 = `${s.finished} of ${s.total} attempts finished${s.graded ? `, ${s.passed} passed, ${s.failed} failed` : ""}`;
|
|
726
|
+
return `<div class="run-progress" role="img" aria-label="${h(label3)}" title="${h(label3)}">${attempts.map((a) => `<span class="seg ${attemptState(a)}"></span>`).join("")}</div>`;
|
|
727
|
+
}
|
|
728
|
+
function taskOutcome(attempts) {
|
|
729
|
+
const s = executionSummary(attempts);
|
|
730
|
+
if (s.graded) return { value: percent(s.passed, s.graded), note: `${s.passed} of ${s.graded} passed`, tone: s.passed === s.graded ? "pass" : s.passed ? "mixed" : "fail" };
|
|
731
|
+
if (!s.total) return { value: "\u2014", note: "No attempts", tone: "neutral" };
|
|
732
|
+
return { value: `${s.finished}/${s.total}`, note: s.active || s.queued ? "running" : "finished \xB7 no checks", tone: "neutral" };
|
|
733
|
+
}
|
|
929
734
|
function executionBadge(attempt) {
|
|
930
735
|
const status = String(attempt.status ?? "unavailable");
|
|
931
736
|
const tone = status === "error" ? "error" : ["running", "starting"].includes(status) ? "active" : "neutral";
|
|
932
737
|
return `<span class="aw-execution ${tone}">${h(attemptLabel(status))}</span>`;
|
|
933
738
|
}
|
|
739
|
+
function outcomeBadge(attempt) {
|
|
740
|
+
const outcome = attemptOutcome(attempt);
|
|
741
|
+
return `<span class="aw-outcome ${outcome.tone}">${h(outcome.label)}</span>`;
|
|
742
|
+
}
|
|
743
|
+
function railMark(attempt) {
|
|
744
|
+
const outcome = attemptOutcome(attempt);
|
|
745
|
+
return `<span class="aw-rail-mark ${outcome.tone}" title="${h(outcome.label)}" aria-label="${h(outcome.label)}">${outcome.mark}</span>`;
|
|
746
|
+
}
|
|
747
|
+
function checksView(attempt) {
|
|
748
|
+
const checks2 = records(attempt.checks);
|
|
749
|
+
if (!checks2.length) return "";
|
|
750
|
+
const passed = checks2.filter((c) => c.passed).length;
|
|
751
|
+
return `<section class="aw-checks"><h3>Checks <span>${passed} of ${checks2.length} passed</span></h3><ul>${checks2.map((c) => `<li class="${c.passed ? "pass" : "fail"}"><span class="aw-check-mark" aria-hidden="true">${c.passed ? "\u2713" : "\u2717"}</span><div><strong>${h(c.id)}</strong><p>${h(c.expected)}</p><p class="muted">${h(c.error ? `Error: ${c.error}` : c.actual)}</p></div></li>`).join("")}</ul></section>`;
|
|
752
|
+
}
|
|
934
753
|
function widgets(attempts) {
|
|
935
|
-
|
|
936
|
-
return `<div class="aw-widgets"><section class="aw-widget"><span>Success rate</span><strong>\u2014</strong></section><section class="aw-widget"><span>Attempts</span><strong>${summary.total}</strong></section><section class="aw-widget"><span>Ungraded</span><strong>${summary.ungraded}</strong></section></div>`;
|
|
754
|
+
return summaryWidgets(attempts);
|
|
937
755
|
}
|
|
938
756
|
function errorMessage(value) {
|
|
939
757
|
if (!value) return "";
|
|
@@ -947,8 +765,9 @@ function entrySections(batch) {
|
|
|
947
765
|
function markers(batch, task, attempts) {
|
|
948
766
|
return `<div class="aw-attempt-strip" aria-label="${h(task.title || "Task")} attempts">${attempts.map((attempt) => {
|
|
949
767
|
const state = ["queued", "starting", "running", "completed", "stopped", "error"].includes(attempt.status) ? attempt.status : "unavailable";
|
|
950
|
-
const
|
|
951
|
-
|
|
768
|
+
const outcome = attemptOutcome(attempt);
|
|
769
|
+
const label3 = `Attempt ${attempt.number} \xB7 ${attemptLabel(state)} \xB7 ${outcome.label}`;
|
|
770
|
+
return `<a data-nav class="aw-attempt-marker execution-${h(state)} outcome-${outcome.key}" href="${h(taskHref(batch, task, { attempt: attempt.id }))}" title="${h(label3)}" aria-label="${h(label3)}"><span>${h(attempt.number)}</span></a>`;
|
|
952
771
|
}).join("")}</div>`;
|
|
953
772
|
}
|
|
954
773
|
function comparisonForm(batch, task, attempts, selected) {
|
|
@@ -964,8 +783,8 @@ function renderRunOverview(batch, actionsHtml = "", options = {}) {
|
|
|
964
783
|
const shown = selected ? tasks.filter((t) => t.id === selected.id) : tasks;
|
|
965
784
|
if (!shown.length) return "";
|
|
966
785
|
return `<section class="aw-task-section">${groups.length > 1 ? `<header class="aw-entry-header"><h2>${h(entry?.agentName ?? entry?.name ?? "Agent")}</h2><span>${h(entry?.world?.name ?? entry?.test?.world?.name ?? "")}</span></header>` : ""}<div class="aw-section-heading"><h2>${selected ? "Attempts" : "Tasks"}</h2>${!selected ? `<span>${tasks.length}</span>` : ""}</div>${selected ? `<section class="aw-task-description"><p>${h(selected.prompt)}</p></section>` : ""}<div class="aw-task-table">${shown.map((task) => {
|
|
967
|
-
const taskAttempts = attempts.filter((a) => a.taskId === task.id), index = tasks.indexOf(task), first = taskAttempts[0];
|
|
968
|
-
return `<article class="aw-task-row"><div class="aw-task-name">${navLink(taskDisplayName(task, index), taskHref(batch, task, first ? { attempt: first.id } : {}))}${!selected ? `<p class="aw-prompt-excerpt">${h(task.prompt)}</p>` : ""}</div><div class="aw-task-outcome"><strong
|
|
786
|
+
const taskAttempts = attempts.filter((a) => a.taskId === task.id), index = tasks.indexOf(task), first = taskAttempts[0], outcome = taskOutcome(taskAttempts);
|
|
787
|
+
return `<article class="aw-task-row"><div class="aw-task-name">${navLink(taskDisplayName(task, index), taskHref(batch, task, first ? { attempt: first.id } : {}))}${!selected ? `<p class="aw-prompt-excerpt">${h(task.prompt)}</p>` : ""}</div><div class="aw-task-outcome ${outcome.tone}"><strong>${h(outcome.value)}</strong><span>${h(outcome.note)}</span></div>${markers(batch, task, taskAttempts)}${taskAttempts.length ? "" : '<p class="quiet-empty">No attempts recorded.</p>'}${comparisonForm(batch, task, taskAttempts)}</article>`;
|
|
969
788
|
}).join("")}</div></section>`;
|
|
970
789
|
}).join("") || '<p class="quiet-empty">No tasks recorded.</p>'}</div>`;
|
|
971
790
|
}
|
|
@@ -1082,7 +901,7 @@ function renderAttemptWorkspace(batch, attempt, evidence, options = {}) {
|
|
|
1082
901
|
const query = options.query ?? {};
|
|
1083
902
|
const toolsNav = `<nav class="tool-nav" aria-label="Attempt tools">${tools.map((t) => navLink(toolName(t), taskHref(batch, task, { attempt: attempt.id, view: "tools", tool: t }), t === tool ? "active" : "")).join("")}</nav>`;
|
|
1084
903
|
const content = view === "tools" ? toolsNav + (tool ? capturedToolView(observation2, tool, { ...query, baseHref: attemptBase }) : '<p class="quiet-empty">No captured tool data.</p>') : managedResponse(fullEvidence) + activityView(observation2, { baseHref: attemptBase });
|
|
1085
|
-
return `<div class="aw-task-detail">${navLink("\u2190 Tasks", basePath(batch), "back")}<header class="aw-header"><h1>${h(taskDisplayName(task, Math.max(index, 0)))}</h1><div class="aw-actions">${options.actionsHtml ?? ""}</div></header><section class="aw-task-description"><p>${h(task.prompt)}</p></section>${errorMessage(batch.error)}<div class="aw-workspace"><aside class="aw-attempt-rail"><h2>Attempts</h2><nav class="aw-attempt-list" aria-label="Task attempts">${attempts.map((a) => `<a data-nav class="aw-attempt-item ${a.id === attempt.id ? "selected" : ""}" ${a.id === attempt.id ? 'aria-current="page"' : ""} href="${h(taskHref(batch, task, { attempt: a.id, view }))}"><span class="aw-attempt-number">Attempt ${h(a.number)}</span>${executionBadge(a)}</a>`).join("")}</nav></aside><section class="aw-evidence"><header class="aw-evidence-header"><div><h2>Attempt ${h(attempt.number)}</h2>${executionBadge(attempt)}
|
|
904
|
+
return `<div class="aw-task-detail">${navLink("\u2190 Tasks", basePath(batch), "back")}<header class="aw-header"><h1>${h(taskDisplayName(task, Math.max(index, 0)))}</h1><div class="aw-actions">${options.actionsHtml ?? ""}</div></header><section class="aw-task-description"><p>${h(task.prompt)}</p></section>${errorMessage(batch.error)}<div class="aw-workspace"><aside class="aw-attempt-rail"><h2>Attempts</h2><nav class="aw-attempt-list" aria-label="Task attempts">${attempts.map((a) => `<a data-nav class="aw-attempt-item ${a.id === attempt.id ? "selected" : ""}" ${a.id === attempt.id ? 'aria-current="page"' : ""} href="${h(taskHref(batch, task, { attempt: a.id, view }))}"><span class="aw-attempt-number">Attempt ${h(a.number)}</span>${executionBadge(a)}${a.outcome ? railMark(a) : ""}</a>`).join("")}</nav></aside><section class="aw-evidence"><header class="aw-evidence-header"><div><h2>Attempt ${h(attempt.number)}</h2>${executionBadge(attempt)}${outcomeBadge(attempt)}</div>${comparisonForm(batch, task, attempts, attempt)}</header>${checksView(attempt)}<nav class="aw-tabs" aria-label="Attempt evidence">${navLink("Activity", attemptBase, view === "activity" ? "active" : "").replace("<a ", `<a ${view === "activity" ? 'aria-current="page" ' : ""}`)}${navLink("Tools", taskHref(batch, task, { attempt: attempt.id, view: "tools" }), view === "tools" ? "active" : "").replace("<a ", `<a ${view === "tools" ? 'aria-current="page" ' : ""}`)}</nav><div id="attempt-evidence" class="aw-evidence-body">${errorMessage(fullEvidence.error)}${content}</div></section></div></div>`;
|
|
1086
905
|
}
|
|
1087
906
|
function renderManagedComparison(data) {
|
|
1088
907
|
const sides = records(data.attempts ?? [data.left, data.right]);
|
|
@@ -1091,7 +910,279 @@ function renderManagedComparison(data) {
|
|
|
1091
910
|
const summary = data.stateComparison;
|
|
1092
911
|
const stateSummary = !summary?.available ? "Final tool data is unavailable for one or both attempts." : !records(summary.differences).length ? "Same final tool data" : `Different final data: ${[...new Set(records(summary.differences).map((d) => String(d.path).split("/")[1]).filter(Boolean))].map(toolName).join(" \xB7 ")}${summary.truncated ? " \xB7 More differences in saved evidence" : ""}`;
|
|
1093
912
|
const cells = (render, extra = "") => sides.map((side) => `<section class="aw-compare-cell ${extra}">${render(side)}</section>`).join("");
|
|
1094
|
-
return `<div class="aw-managed-comparison"><p class="aw-comparison-summary">${h(stateSummary)}</p><div class="aw-comparison">${cells((side) => `<h2>Attempt ${h(side.number)}</h2>${executionBadge(side)}
|
|
913
|
+
return `<div class="aw-managed-comparison"><p class="aw-comparison-summary">${h(stateSummary)}</p><div class="aw-comparison">${cells((side) => `<h2>Attempt ${h(side.number)}</h2>${executionBadge(side)}${outcomeBadge(side)}${navLink("Open attempt", url("/playgrounds/" + encodeURIComponent(String(data.runId)), { task: data.taskId ?? side.taskId, attempt: side.id }))}`, "aw-compare-head")}${cells((side) => errorMessage(side.error) + managedResponse(side), "aw-compare-response")}${cells((side) => activityView({ ...side, trace: side.toolActivity ?? side.trace ?? [], state: side.state ?? {} }, { baseHref: url("/playgrounds/" + encodeURIComponent(String(data.runId)), { task: data.taskId ?? side.taskId, attempt: side.id }) }), "aw-compare-activity")}</div></div>`;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// frontend/src/world-workspace.ts
|
|
917
|
+
var enc = encodeURIComponent;
|
|
918
|
+
var label = (task, index) => task.title && task.title !== String(task.prompt).split("\n")[0]?.slice(0, 120) ? task.title : "Task " + (index + 1);
|
|
919
|
+
function worldSetups(tests, worldId, agentId) {
|
|
920
|
+
return tests.filter((test) => !test.archived && test.worldId === worldId && (!agentId || test.agentId === agentId));
|
|
921
|
+
}
|
|
922
|
+
function setupRuns(runs, test, exactRevision = true) {
|
|
923
|
+
return runs.filter((run) => !run.entries?.length && run.test?.id === test.id && (!exactRevision || run.test.revision === test.revision) && run.agentId === test.agentId && run.world?.id === test.worldId && (!exactRevision || run.world.revision === test.worldRevision)).sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
|
|
924
|
+
}
|
|
925
|
+
function setupHistory(runs, test) {
|
|
926
|
+
const singles = new Set(setupRuns(runs, test, false).map((run) => run.id));
|
|
927
|
+
return runs.flatMap((run) => {
|
|
928
|
+
if (singles.has(run.id)) return [{ run, attemptCount: run.attempts?.length ?? 0, earlier: run.test.revision !== test.revision || run.world.revision !== test.worldRevision, grouped: false }];
|
|
929
|
+
const entries = (run.entries ?? []).filter((entry) => entry.test?.id === test.id && entry.agentId === test.agentId && entry.world?.id === test.worldId && entry.test.agentId === test.agentId && entry.test.worldId === test.worldId && entry.test.worldRevision === entry.world.revision);
|
|
930
|
+
if (!entries.length) return [];
|
|
931
|
+
const ids = new Set(entries.map((entry) => entry.id));
|
|
932
|
+
return [{
|
|
933
|
+
run,
|
|
934
|
+
attemptCount: (run.attempts ?? []).filter((attempt) => ids.has(attempt.entryId)).length,
|
|
935
|
+
earlier: entries.some((entry) => entry.test.revision !== test.revision || entry.world.revision !== test.worldRevision),
|
|
936
|
+
grouped: true
|
|
937
|
+
}];
|
|
938
|
+
}).sort((a, b) => String(b.run.createdAt).localeCompare(String(a.run.createdAt)));
|
|
939
|
+
}
|
|
940
|
+
function worldHistory(runs, worldId, agentId) {
|
|
941
|
+
return runs.flatMap((run) => {
|
|
942
|
+
if (!run.entries?.length) return run.world?.id === worldId && (!agentId || run.agentId === agentId) ? [{ run, attemptCount: run.attempts?.length ?? 0, grouped: false }] : [];
|
|
943
|
+
const entries = run.entries.filter((entry) => entry.world?.id === worldId && (!agentId || entry.agentId === agentId));
|
|
944
|
+
if (!entries.length) return [];
|
|
945
|
+
const ids = new Set(entries.map((entry) => entry.id));
|
|
946
|
+
return [{ run, attemptCount: (run.attempts ?? []).filter((attempt) => ids.has(attempt.entryId)).length, grouped: true }];
|
|
947
|
+
}).sort((a, b) => String(b.run.createdAt).localeCompare(String(a.run.createdAt)));
|
|
948
|
+
}
|
|
949
|
+
function checksText(task) {
|
|
950
|
+
if (typeof task.checksText === "string") return task.checksText;
|
|
951
|
+
return Array.isArray(task.checks) && task.checks.length ? `[
|
|
952
|
+
${task.checks.map((check) => " " + JSON.stringify(check)).join(",\n")}
|
|
953
|
+
]` : "";
|
|
954
|
+
}
|
|
955
|
+
function taskChecks(task, index) {
|
|
956
|
+
if (typeof task.checksText !== "string") return Array.isArray(task.checks) && task.checks.length ? task.checks : void 0;
|
|
957
|
+
const text2 = task.checksText.trim();
|
|
958
|
+
if (!text2) return void 0;
|
|
959
|
+
let parsed;
|
|
960
|
+
try {
|
|
961
|
+
parsed = JSON.parse(text2);
|
|
962
|
+
} catch {
|
|
963
|
+
throw new Error(`Task ${index + 1}: checks must be valid JSON, a list like [{"id": "posted", "trace": {"called": "POST /api/chat.postMessage"}}]`);
|
|
964
|
+
}
|
|
965
|
+
const list2 = Array.isArray(parsed) ? parsed : [parsed];
|
|
966
|
+
if (list2.some((item) => !item || typeof item !== "object" || Array.isArray(item))) throw new Error(`Task ${index + 1}: each check must be an object with an id and a state or trace check`);
|
|
967
|
+
return list2;
|
|
968
|
+
}
|
|
969
|
+
function workspaceDefinition(draft) {
|
|
970
|
+
return {
|
|
971
|
+
name: draft.name,
|
|
972
|
+
agentId: draft.agentId,
|
|
973
|
+
worldId: draft.worldId,
|
|
974
|
+
worldRevision: Number(draft.worldRevision),
|
|
975
|
+
tasks: draft.tasks.map((task, index) => {
|
|
976
|
+
const checks2 = taskChecks(task, index);
|
|
977
|
+
return { id: task.id, ...String(task.title ?? "").trim() ? { title: String(task.title).trim() } : {}, prompt: task.prompt, attempts: Number(task.attempts), timeoutMs: Number(task.timeoutMs), ...checks2?.length ? { checks: checks2 } : {} };
|
|
978
|
+
}),
|
|
979
|
+
concurrency: Number(draft.concurrency),
|
|
980
|
+
seed: draft.seed ?? "playground-1"
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function mountWorldWorkspace(node, options) {
|
|
984
|
+
const { world, agents, api: api2, go: go2 } = options, base = "/worlds/" + enc(world.id), q = new URLSearchParams(location.search);
|
|
985
|
+
let tests = [], runs = [], errors = [], selected, agentId = q.get("agent") ?? "", configure = q.has("configure"), showHistory = false, saving = false, disposed = false, error = "", loaded = false;
|
|
986
|
+
const revisions = /* @__PURE__ */ new Map([[world.revision, world]]);
|
|
987
|
+
let draft, timer;
|
|
988
|
+
const launchPending = () => {
|
|
989
|
+
if (!selected) return false;
|
|
990
|
+
try {
|
|
991
|
+
return sessionStorage.getItem("ancha-saved-test-run:" + selected.id) !== null;
|
|
992
|
+
} catch {
|
|
993
|
+
return true;
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
const key = () => "ancha-world-setup:" + world.id + ":" + (selected?.id ?? "new:" + agentId);
|
|
997
|
+
const remember = () => {
|
|
998
|
+
if (draft) try {
|
|
999
|
+
sessionStorage.setItem(key(), JSON.stringify(draft));
|
|
1000
|
+
} catch {
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
function getDraft() {
|
|
1004
|
+
if (draft) return draft;
|
|
1005
|
+
try {
|
|
1006
|
+
const stored = JSON.parse(sessionStorage.getItem(key()) ?? "null");
|
|
1007
|
+
if (stored && Array.isArray(stored.tasks)) draft = stored;
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
draft ??= selected ? { ...selected, tasks: structuredClone(selected.tasks) } : { name: world.name, agentId, worldId: world.id, worldRevision: world.revision, concurrency: 1, seed: "playground-1", tasks: [{ id: "task_" + crypto.randomUUID(), title: "", prompt: "", attempts: 1, timeoutMs: 3e5 }] };
|
|
1011
|
+
return draft;
|
|
1012
|
+
}
|
|
1013
|
+
function capture() {
|
|
1014
|
+
const form = node.querySelector(".ww-form");
|
|
1015
|
+
if (!form) return;
|
|
1016
|
+
const d = getDraft(), values = new FormData(form);
|
|
1017
|
+
d.name = String(values.get("name") ?? "");
|
|
1018
|
+
d.agentId = String(values.get("agentId") ?? "");
|
|
1019
|
+
d.concurrency = Number(values.get("concurrency"));
|
|
1020
|
+
d.worldRevision = Number(values.get("worldRevision"));
|
|
1021
|
+
d.tasks = [...form.querySelectorAll(".ww-task-editor")].map((row) => ({ id: row.dataset.taskId, title: row.querySelector("[name=title]").value, prompt: row.querySelector("[name=prompt]").value, attempts: Number(row.querySelector("[name=attempts]").value), timeoutMs: Math.round(Number(row.querySelector("[name=minutes]").value) * 6e4), checksText: row.querySelector("[name=checks]")?.value ?? "" }));
|
|
1022
|
+
remember();
|
|
1023
|
+
}
|
|
1024
|
+
function href2(test, agent = agentId, extra = "") {
|
|
1025
|
+
return base + "?" + new URLSearchParams({ ...agent ? { agent } : {}, ...test ? { test: test.id } : {}, ...extra ? { [extra]: "1" } : {} });
|
|
1026
|
+
}
|
|
1027
|
+
function configuration2() {
|
|
1028
|
+
const d = getDraft(), pinned = Number(d.worldRevision) !== world.revision, startingWorld = revisions.get(Number(d.worldRevision));
|
|
1029
|
+
return `<section class="ww-configure"><div class="ww-section-heading"><h2>Configure</h2><button type="button" data-ww="close">Close</button></div><form class="ww-form"><div class="form-grid"><label>Agent<select name="agentId" required><option value="">Choose agent</option>${agents.map((a) => `<option value="${h(a.id)}" ${d.agentId === a.id ? "selected" : ""}>${h(a.name)}</option>`).join("")}</select></label><label>Configuration name<input name="name" required maxlength="120" value="${h(d.name)}"></label></div><section class="ww-tools"><div class="ww-section-heading"><h3>Tools</h3><a data-nav href="${base}?edit=1">Edit world</a></div><div class="ww-tool-list">${(startingWorld?.configuration.preset.datasets ?? []).map((item) => `<a data-nav href="/tools/${enc(item.tool)}">${toolLogo(item.tool)}${h(toolName(item.tool))}</a>`).join("")}</div>${pinned ? `<label>Starting data<select name="worldRevision"><option value="${h(d.worldRevision)}">Saved revision ${h(d.worldRevision)}</option><option value="${world.revision}">Current revision ${world.revision}</option></select></label>` : `<input type="hidden" name="worldRevision" value="${h(d.worldRevision)}">`}</section><div class="ww-section-heading"><h3>Tasks</h3><button type="button" data-ww="add-task" ${d.tasks.length >= 20 ? "disabled" : ""}>Add task</button></div><div class="ww-task-fields">${d.tasks.map((task, index) => `<article class="ww-task-editor" data-task-id="${h(task.id)}"><div class="ww-section-heading"><h3>Task ${index + 1}</h3>${d.tasks.length > 1 ? `<button type="button" data-ww="remove-task" data-value="${h(task.id)}">Remove</button>` : ""}</div><label>Task name <span class="muted">optional</span><input name="title" maxlength="120" value="${h(task.title ?? "")}" placeholder="Task ${index + 1}"></label><label>Instructions<textarea name="prompt" required maxlength="20000" rows="4">${h(task.prompt)}</textarea></label><div class="ww-task-options"><label>Repeat<input type="number" name="attempts" min="1" max="100" value="${h(task.attempts)}" required><span>times</span></label><label>Time limit <span class="muted">minutes</span><input type="number" name="minutes" min="0.016666666666666666" max="60" step="any" value="${h(task.timeoutMs / 6e4)}" required></label></div><label class="ww-checks-field">Checks <span class="muted">optional \xB7 JSON list, see <a href="/docs#checks" target="_blank" rel="noopener">Docs</a></span><textarea name="checks" rows="${Math.min(12, Math.max(3, checksText(task).split("\n").length))}" spellcheck="false" maxlength="20000" placeholder='[{"id": "posted", "trace": {"called": "POST /api/chat.postMessage"}}]'>${h(checksText(task))}</textarea></label></article>`).join("")}</div><div class="ww-footer"><label>Run at once<input name="concurrency" type="number" min="1" max="10" value="${h(d.concurrency)}" required></label><button type="submit" class="primary" ${saving ? "disabled" : ""}>${saving ? "Saving\u2026" : "Save configuration"}</button></div></form></section>`;
|
|
1030
|
+
}
|
|
1031
|
+
function render() {
|
|
1032
|
+
if (disposed) return;
|
|
1033
|
+
const currentRuns = selected ? setupRuns(runs, selected) : [], run = currentRuns[0], attempts = run?.attempts ?? [], tasks = selected?.tasks ?? [], choices = worldSetups(tests, world.id, agentId);
|
|
1034
|
+
node.innerHTML = `<div class="ww-workspace"><header class="ww-header"><h1>${h(world.name)}</h1><div class="ww-world-actions"><label class="ww-agent-select">Agent<select data-ww-select="agent"><option value="" disabled>Choose agent</option>${agents.map((a) => `<option value="${h(a.id)}" ${a.id === agentId ? "selected" : ""}>${h(a.name)}</option>`).join("")}</select></label><button type="button" data-ww="configure">Configure</button>${selected ? options.runActions?.(selected.id) ?? button("Run tasks", "run-saved-test", selected.id, "primary") : ""}</div></header><p class="ww-error" role="alert" ${error ? "" : "hidden"}>${h(error)}</p>${errors.map((e) => `<p class="ww-error" role="alert">${h(e.message)}</p>`).join("")}${!loaded ? '<p role="status">Loading tasks\u2026</p>' : ""}${choices.length > 1 ? `<label class="ww-setup-select">Configuration<select data-ww-select="test">${!selected ? '<option value="" selected disabled>New configuration</option>' : ""}${choices.map((t) => `<option value="${h(t.id)}" ${t.id === selected?.id ? "selected" : ""}>${h(t.name)}</option>`).join("")}</select></label>` : ""}${loaded && configure ? configuration2() : loaded ? `${summaryWidgets(attempts, "ww-summary-widgets", "article")}<div class="ww-section-heading"><h2>Tasks${tasks.length ? ` <span class="ww-task-count">${tasks.length}</span>` : ""}</h2><div class="actions">${worldHistory(runs, world.id, agentId || void 0).length || selected ? `<button type="button" data-ww="history" aria-expanded="${showHistory}">History</button>` : ""}<button type="button" data-ww="configure">${tasks.length ? "Edit tasks" : "Add tasks"}</button></div></div>${tasks.length ? `<section class="ww-task-table"><div class="ww-table-head"><span>Task</span><span>Success rate</span><span>Attempts</span><span></span></div>${tasks.map((task, index) => {
|
|
1035
|
+
const list2 = attempts.filter((a) => a.taskId === task.id), taskURL = run ? `/playgrounds/${enc(run.id)}?task=${enc(task.id)}${list2[0] ? "&attempt=" + enc(list2[0].id) : ""}` : "";
|
|
1036
|
+
return `<article class="ww-task-row"><div class="ww-task-identity">${taskURL ? `<a data-nav class="ww-task-name" href="${taskURL}"><span class="ww-ordinal">${index + 1}</span><strong>${h(label(task, index))}</strong></a>` : `<strong class="ww-task-name"><span class="ww-ordinal">${index + 1}</span>${h(label(task, index))}</strong>`}<p class="ww-task-prompt" title="${h(task.prompt)}">${h(task.prompt)}</p></div>${(() => {
|
|
1037
|
+
const o = taskOutcome(list2);
|
|
1038
|
+
return `<div class="ww-rate ${o.tone}"><strong>${h(o.value)}</strong><span>${h(o.note)}</span></div>`;
|
|
1039
|
+
})()}<div class="ww-attempt-marks" role="group" aria-label="Attempts for ${h(label(task, index))}">${list2.map((a) => {
|
|
1040
|
+
const o = attemptOutcome(a);
|
|
1041
|
+
return `<a data-nav class="ww-attempt-mark ${h(a.status)} outcome-${o.key}" href="/playgrounds/${enc(run.id)}?task=${enc(task.id)}&attempt=${enc(a.id)}" title="Attempt ${a.number}: ${h(attemptLabel(a.status))} \xB7 ${o.label}" aria-label="Attempt ${a.number}: ${h(attemptLabel(a.status))} \xB7 ${o.label}">${o.key !== "ungraded" ? o.mark : a.status === "error" ? "!" : ["running", "starting"].includes(a.status) ? "\u25CC" : "\u2212"}</a>`;
|
|
1042
|
+
}).join("") || `<span class="muted">Repeat ${h(task.attempts)} times</span>`}</div>${taskURL ? `<a data-nav class="ww-row-inspect" href="${taskURL}" aria-label="Inspect ${h(label(task, index))}">\u2192</a>` : "<span></span>"}</article>`;
|
|
1043
|
+
}).join("")}</section>` : `<div class="quiet-empty">${agents.length ? "Add tasks to this world." : '<a data-nav href="/agents">Connect an agent</a>'}</div>`}${selected && !currentRuns.length && setupHistory(runs, selected).some((item) => item.earlier) ? '<p class="ww-snapshot-notice">This configuration has changed. Earlier attempts are in History.</p>' : ""}${showHistory ? history2() : ""}` : ""}</div>`;
|
|
1044
|
+
}
|
|
1045
|
+
function history2() {
|
|
1046
|
+
const all = worldHistory(runs, world.id, agentId || void 0);
|
|
1047
|
+
return `<section class="ww-history"><div class="ww-section-heading"><h2>History</h2><button type="button" data-ww="new">New configuration</button></div>${all.length ? all.map(({ run, attemptCount, grouped }) => `<a data-nav class="ww-history-row" href="/playgrounds/${enc(run.id)}"><time>${h(new Date(run.createdAt).toLocaleString())}</time><span>${h(attemptLabel(run.status))}</span><span>${attemptCount} attempts</span>${grouped ? "<span>Group</span>" : ""}${run.test?.name || run.groupName ? `<span>${h(run.groupName ?? run.test.name)}</span>` : ""}<span>\u2192</span></a>`).join("") : '<p class="quiet-empty">No attempts yet.</p>'}</section>`;
|
|
1048
|
+
}
|
|
1049
|
+
async function refresh(initial = false) {
|
|
1050
|
+
try {
|
|
1051
|
+
const [definitions, batches] = await Promise.all([api2("/playground-tests"), api2("/playgrounds")]);
|
|
1052
|
+
if (disposed) return;
|
|
1053
|
+
tests = definitions.tests ?? [];
|
|
1054
|
+
runs = batches.runs ?? [];
|
|
1055
|
+
errors = [...definitions.errors ?? [], ...batches.errors ?? []];
|
|
1056
|
+
if (initial) {
|
|
1057
|
+
const requested = q.get("test");
|
|
1058
|
+
if (requested) {
|
|
1059
|
+
selected = worldSetups(tests, world.id).find((test) => test.id === requested);
|
|
1060
|
+
if (!selected || agentId && selected.agentId !== agentId) {
|
|
1061
|
+
selected = void 0;
|
|
1062
|
+
error = "This configuration is unavailable for this world and agent.";
|
|
1063
|
+
} else agentId = selected.agentId;
|
|
1064
|
+
} else if (q.has("new")) {
|
|
1065
|
+
selected = void 0;
|
|
1066
|
+
agentId ||= agents[0]?.id ?? "";
|
|
1067
|
+
} else {
|
|
1068
|
+
selected = worldSetups(tests, world.id, agentId || void 0)[0];
|
|
1069
|
+
const historical = worldHistory(runs, world.id)[0]?.run;
|
|
1070
|
+
agentId ||= selected?.agentId ?? historical?.entries?.find((entry) => entry.world?.id === world.id)?.agentId ?? historical?.agentId ?? agents[0]?.id ?? "";
|
|
1071
|
+
}
|
|
1072
|
+
} else if (selected) selected = tests.find((test) => test.id === selected.id);
|
|
1073
|
+
if (initial && !selected && worldHistory(runs, world.id, agentId || void 0).length) showHistory = true;
|
|
1074
|
+
if (configure) getDraft();
|
|
1075
|
+
options.onContext?.(selected);
|
|
1076
|
+
const pinnedRevision = Number(draft?.worldRevision ?? selected?.worldRevision ?? world.revision);
|
|
1077
|
+
if (!revisions.has(pinnedRevision)) {
|
|
1078
|
+
const savedWorld = await api2(`/worlds/${enc(world.id)}?revision=${pinnedRevision}`);
|
|
1079
|
+
if (disposed) return;
|
|
1080
|
+
revisions.set(pinnedRevision, savedWorld);
|
|
1081
|
+
}
|
|
1082
|
+
loaded = true;
|
|
1083
|
+
if (!configure) render();
|
|
1084
|
+
else if (initial) render();
|
|
1085
|
+
if (runs.some((run) => !terminal(run.status))) timer = setTimeout(() => {
|
|
1086
|
+
void refresh();
|
|
1087
|
+
}, 2e3);
|
|
1088
|
+
} catch (e) {
|
|
1089
|
+
if (!disposed) {
|
|
1090
|
+
error = e instanceof Error ? e.message : String(e);
|
|
1091
|
+
loaded = true;
|
|
1092
|
+
render();
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
node.addEventListener("input", capture);
|
|
1097
|
+
node.addEventListener("change", (event) => {
|
|
1098
|
+
const control = event.target;
|
|
1099
|
+
if (control.dataset.wwSelect === "agent") {
|
|
1100
|
+
capture();
|
|
1101
|
+
const match = worldSetups(tests, world.id, control.value)[0];
|
|
1102
|
+
go2(href2(match, control.value));
|
|
1103
|
+
} else if (control.dataset.wwSelect === "test") go2(href2(tests.find((t) => t.id === control.value)));
|
|
1104
|
+
else {
|
|
1105
|
+
capture();
|
|
1106
|
+
if (control.name === "worldRevision") render();
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
node.addEventListener("click", (event) => {
|
|
1110
|
+
const control = event.target.closest("[data-ww]");
|
|
1111
|
+
if (!control) return;
|
|
1112
|
+
event.preventDefault();
|
|
1113
|
+
event.stopPropagation();
|
|
1114
|
+
if (saving) return;
|
|
1115
|
+
capture();
|
|
1116
|
+
error = "";
|
|
1117
|
+
switch (control.dataset.ww) {
|
|
1118
|
+
case "configure":
|
|
1119
|
+
if (launchPending()) {
|
|
1120
|
+
error = "Resolve the pending run before editing this configuration.";
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
configure = true;
|
|
1124
|
+
break;
|
|
1125
|
+
case "close":
|
|
1126
|
+
configure = false;
|
|
1127
|
+
break;
|
|
1128
|
+
case "history":
|
|
1129
|
+
showHistory = !showHistory;
|
|
1130
|
+
break;
|
|
1131
|
+
case "new":
|
|
1132
|
+
go2(href2(void 0, agentId, "new") + "&configure=1");
|
|
1133
|
+
return;
|
|
1134
|
+
case "add-task":
|
|
1135
|
+
if (getDraft().tasks.length < 20) getDraft().tasks.push({ id: "task_" + crypto.randomUUID(), title: "", prompt: "", attempts: 1, timeoutMs: 3e5 });
|
|
1136
|
+
break;
|
|
1137
|
+
case "remove-task":
|
|
1138
|
+
if (getDraft().tasks.length > 1) getDraft().tasks = getDraft().tasks.filter((t) => t.id !== control.dataset.value);
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1141
|
+
remember();
|
|
1142
|
+
render();
|
|
1143
|
+
});
|
|
1144
|
+
node.addEventListener("submit", (event) => {
|
|
1145
|
+
const form = event.target;
|
|
1146
|
+
if (!form.matches(".ww-form")) return;
|
|
1147
|
+
event.preventDefault();
|
|
1148
|
+
event.stopPropagation();
|
|
1149
|
+
if (saving) return;
|
|
1150
|
+
if (launchPending()) {
|
|
1151
|
+
error = "Resolve the pending run before editing this configuration.";
|
|
1152
|
+
render();
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
capture();
|
|
1156
|
+
saving = true;
|
|
1157
|
+
error = "";
|
|
1158
|
+
render();
|
|
1159
|
+
const d = getDraft(), oldKey = key();
|
|
1160
|
+
let body;
|
|
1161
|
+
try {
|
|
1162
|
+
body = workspaceDefinition(d);
|
|
1163
|
+
} catch (e) {
|
|
1164
|
+
error = e instanceof Error ? e.message : String(e);
|
|
1165
|
+
saving = false;
|
|
1166
|
+
render();
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
void api2(selected ? `/playground-tests/${enc(selected.id)}/update` : "/playground-tests", { ...body, ...selected ? { revision: d.revision } : {} }).then((saved) => {
|
|
1170
|
+
sessionStorage.removeItem(oldKey);
|
|
1171
|
+
if (!disposed) go2(href2(saved, saved.agentId));
|
|
1172
|
+
}).catch((e) => {
|
|
1173
|
+
if (!disposed) {
|
|
1174
|
+
error = e instanceof Error ? e.message : String(e);
|
|
1175
|
+
saving = false;
|
|
1176
|
+
render();
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
});
|
|
1180
|
+
render();
|
|
1181
|
+
void refresh(true);
|
|
1182
|
+
return () => {
|
|
1183
|
+
disposed = true;
|
|
1184
|
+
clearTimeout(timer);
|
|
1185
|
+
};
|
|
1095
1186
|
}
|
|
1096
1187
|
|
|
1097
1188
|
// frontend/src/workspace-pages.ts
|
|
@@ -1210,7 +1301,8 @@ function mountTestFlow(node, options) {
|
|
|
1210
1301
|
if (form) {
|
|
1211
1302
|
draft.name = form.elements.namedItem("name").value;
|
|
1212
1303
|
draft.concurrency = Number(form.elements.namedItem("concurrency").value);
|
|
1213
|
-
draft.tasks
|
|
1304
|
+
const kept = new Map((draft.tasks ?? []).map((t) => [t.id, t]));
|
|
1305
|
+
draft.tasks = [...form.querySelectorAll(".task-editor")].map((row) => ({ ...kept.get(row.dataset.taskId) ?? {}, id: row.dataset.taskId, prompt: row.querySelector("[name=prompt]").value, attempts: Number(row.querySelector("[name=attempts]").value), timeoutMs: Math.round(Number(row.querySelector("[name=minutes]").value) * 6e4) }));
|
|
1214
1306
|
}
|
|
1215
1307
|
remember();
|
|
1216
1308
|
}
|
|
@@ -1474,7 +1566,8 @@ function mountGroupFlow(node, options) {
|
|
|
1474
1566
|
const world = options.worlds.find((value) => value.id === draft.worldId);
|
|
1475
1567
|
draft.worldRevision = world?.revision ?? 0;
|
|
1476
1568
|
draft.concurrency = Number(values.get("concurrency") ?? 1);
|
|
1477
|
-
draft.tasks
|
|
1569
|
+
const kept = new Map((draft.tasks ?? []).map((t) => [t.id, t]));
|
|
1570
|
+
draft.tasks = [...form.querySelectorAll(".task-editor")].map((row) => ({ ...kept.get(row.dataset.taskId) ?? {}, id: row.dataset.taskId, prompt: row.querySelector("[name=prompt]").value, attempts: Number(row.querySelector("[name=attempts]").value), timeoutMs: Math.round(Number(row.querySelector("[name=minutes]").value) * 6e4) }));
|
|
1478
1571
|
remember();
|
|
1479
1572
|
}
|
|
1480
1573
|
function showError(message) {
|
|
@@ -1634,10 +1727,10 @@ function comparisonSetup(runId, runs) {
|
|
|
1634
1727
|
return `<p class="hint">Choose a completed run with matching scenarios and test data.</p><form id="compare-form"><input type="hidden" name="current" value="${h(runId)}"><label>Baseline<select name="baseline" required>${baselines.map((r) => `<option value="${h(r.runId)}">${h(r.metadata?.label ?? "CLI test")} \xB7 ${h(r.runId)}</option>`).join("")}</select></label><button class="primary">Compare</button></form>`;
|
|
1635
1728
|
}
|
|
1636
1729
|
function comparisonResults(result, labels) {
|
|
1637
|
-
const
|
|
1730
|
+
const percent2 = (rate) => `${(rate * 100).toLocaleString("en-US", { maximumFractionDigits: 1 })}%`;
|
|
1638
1731
|
const change = (delta) => delta === 0 ? "No change" : Math.abs(delta * 100) < 0.1 ? `${delta > 0 ? "Increase" : "Decrease"} under 0.1 percentage points` : `${delta > 0 ? "+" : "\u2212"}${Math.abs(delta * 100).toLocaleString("en-US", { maximumFractionDigits: 1 })} percentage points`;
|
|
1639
1732
|
const attempts = (count) => `${count} ${count === 1 ? "attempt" : "attempts"}`;
|
|
1640
|
-
return `<p>${result.regressed ? "Pass rate decreased in one or more scenarios." : "No decrease in pass rate."}</p><div class="table-wrap comparison-table"><table><thead><tr><th scope="col">Scenario</th><th scope="col">Before</th><th scope="col">After</th><th scope="col">Change</th></tr></thead><tbody>${result.scenes.map((s, index) => `<tr><th scope="row">${h(labels[s.sceneId] ?? `Scenario ${index + 1}`)}</th><td><strong>${
|
|
1733
|
+
return `<p>${result.regressed ? "Pass rate decreased in one or more scenarios." : "No decrease in pass rate."}</p><div class="table-wrap comparison-table"><table><thead><tr><th scope="col">Scenario</th><th scope="col">Before</th><th scope="col">After</th><th scope="col">Change</th></tr></thead><tbody>${result.scenes.map((s, index) => `<tr><th scope="row">${h(labels[s.sceneId] ?? `Scenario ${index + 1}`)}</th><td><strong>${percent2(s.before)}</strong><span class="comparison-attempts">${attempts(s.beforeTrials)}</span></td><td><strong>${percent2(s.after)}</strong><span class="comparison-attempts">${attempts(s.afterTrials)}</span></td><td>${badge(change(s.delta), s.delta > 0 ? "good" : s.delta < 0 ? "bad" : "")}</td></tr>`).join("")}</tbody></table></div><p class="hint">Observed pass rates; no claim of statistical significance.</p>${details("Comparison details", result)}`;
|
|
1641
1734
|
}
|
|
1642
1735
|
function dataSelection(item) {
|
|
1643
1736
|
if (!item) return "";
|