@crewhaus/hangar-ui 0.5.0

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.
Files changed (51) hide show
  1. package/README.md +309 -0
  2. package/assets/hangar.css +1726 -0
  3. package/assets/index.html +21 -0
  4. package/assets/js/api.js +368 -0
  5. package/assets/js/app.js +734 -0
  6. package/assets/js/dom.js +228 -0
  7. package/assets/js/keys.js +159 -0
  8. package/assets/js/markdown.js +131 -0
  9. package/assets/js/notify.js +152 -0
  10. package/assets/js/omnibox.js +307 -0
  11. package/assets/js/router.js +280 -0
  12. package/assets/js/routes.js +793 -0
  13. package/assets/js/shapes.js +36 -0
  14. package/assets/js/supervision.js +1334 -0
  15. package/assets/js/util.js +426 -0
  16. package/assets/js/views/activity.js +140 -0
  17. package/assets/js/views/approvals.js +155 -0
  18. package/assets/js/views/channels.js +455 -0
  19. package/assets/js/views/control.js +253 -0
  20. package/assets/js/views/costs.js +103 -0
  21. package/assets/js/views/creds.js +610 -0
  22. package/assets/js/views/data.js +456 -0
  23. package/assets/js/views/deploy.js +106 -0
  24. package/assets/js/views/evals-lab.js +1029 -0
  25. package/assets/js/views/evals.js +342 -0
  26. package/assets/js/views/feedback.js +555 -0
  27. package/assets/js/views/health.js +176 -0
  28. package/assets/js/views/inbox.js +103 -0
  29. package/assets/js/views/inspect.js +751 -0
  30. package/assets/js/views/jobs.js +110 -0
  31. package/assets/js/views/library.js +605 -0
  32. package/assets/js/views/memory-fabric.js +2026 -0
  33. package/assets/js/views/memory.js +228 -0
  34. package/assets/js/views/onboarding.js +253 -0
  35. package/assets/js/views/overview.js +196 -0
  36. package/assets/js/views/panes.js +132 -0
  37. package/assets/js/views/proc.js +280 -0
  38. package/assets/js/views/review.js +164 -0
  39. package/assets/js/views/runs.js +452 -0
  40. package/assets/js/views/runtime.js +450 -0
  41. package/assets/js/views/schedulers.js +133 -0
  42. package/assets/js/views/security.js +925 -0
  43. package/assets/js/views/sessions.js +265 -0
  44. package/assets/js/views/settings.js +489 -0
  45. package/assets/js/views/spec-edit.js +1289 -0
  46. package/assets/js/views/spec.js +66 -0
  47. package/assets/js/views/thredz.js +1853 -0
  48. package/assets/js/views/token.js +49 -0
  49. package/dist/index.d.ts +15 -0
  50. package/dist/index.js +145 -0
  51. package/package.json +36 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Evals tab — run history, one run's per-sample table (pass/fail filter),
3
+ * and one sample's grades + transcript excerpt. Partial runs render
4
+ * deflated (badged, never chartable as regressions); replayed runs are
5
+ * badged not-live; the baseline run carries a star.
6
+ */
7
+
8
+ import { api } from "../api.js";
9
+ import { clear, dot, el, emptyState, jsonPre, skeleton, toast } from "../dom.js";
10
+ import { hrefHarness } from "../router.js";
11
+ import { fmtPct, fmtRelativeTime, fmtUsd } from "../util.js";
12
+ import { actionTwin } from "./control.js";
13
+
14
+ export async function renderEvals(root, ctx) {
15
+ if (ctx.route.runId !== undefined && ctx.route.sampleId !== undefined) {
16
+ await renderSample(root, ctx, ctx.route.runId, ctx.route.sampleId);
17
+ return;
18
+ }
19
+ if (ctx.route.runId !== undefined) {
20
+ await renderRun(root, ctx, ctx.route.runId);
21
+ return;
22
+ }
23
+ await renderHistory(root, ctx);
24
+ }
25
+
26
+ function runsOf(data) {
27
+ if (Array.isArray(data)) return data;
28
+ if (Array.isArray(data?.runs)) return data.runs;
29
+ return [];
30
+ }
31
+
32
+ async function renderHistory(root, ctx) {
33
+ clear(root).appendChild(skeleton(6));
34
+ const data = await api.evals(ctx.id);
35
+ clear(root);
36
+ const runs = runsOf(data);
37
+ if (runs.length === 0) {
38
+ root.appendChild(emptyState("No evals yet", "crewhaus eval"));
39
+ return;
40
+ }
41
+ // Baselines are keyed `<spec>::<dataset>` → the pinned run; star any run
42
+ // that IS a pinned baseline.
43
+ const baselines = data?.baselines && typeof data.baselines === "object" ? data.baselines : {};
44
+ const baselineRunIds = new Set(
45
+ Object.values(baselines)
46
+ .map((b) => (b && typeof b === "object" ? b.runId : null))
47
+ .filter((id) => typeof id === "string"),
48
+ );
49
+ const nowMs = Date.now();
50
+ const reload = () => renderHistory(root, ctx);
51
+ const tbody = el("tbody");
52
+ for (const run of runs) {
53
+ const runId = String(run.runId ?? run.id ?? "");
54
+ const partial = run.partial === true;
55
+ const flaky = run.flaky === true || (typeof run.flakyCount === "number" && run.flakyCount > 0);
56
+ tbody.appendChild(
57
+ el("tr", { class: partial ? "deflated" : null }, [
58
+ el("td", { text: fmtRelativeTime(run.ts ?? run.at ?? null, nowMs) }),
59
+ el("td", null, [
60
+ baselineRunIds.has(runId)
61
+ ? el("span", { class: "baseline-star", title: "baseline run", text: "★ " })
62
+ : null,
63
+ el("a", {
64
+ class: "mono name-link",
65
+ href: hrefHarness(ctx.id, "evals", runId),
66
+ text: runId || "(run)",
67
+ }),
68
+ ]),
69
+ el("td", { class: "mono", text: String(run.datasetName ?? run.dataset ?? "—") }),
70
+ el("td", null, [passDot(run.passRate, run.gatePassed)]),
71
+ el("td", { class: "num", text: fmtScore(run.meanScore) }),
72
+ el("td", {
73
+ class: "num",
74
+ text: fmtUsd(typeof run.costUsd === "number" ? run.costUsd : null),
75
+ }),
76
+ el("td", { class: "cell-caps" }, [
77
+ flaky ? el("span", { class: "chip chip-warn", text: "flaky" }) : null,
78
+ partial ? el("span", { class: "chip chip-warn", text: "partial" }) : null,
79
+ run.replayed === true ? el("span", { class: "chip", text: "replayed" }) : null,
80
+ ]),
81
+ el("td", { class: "cell-edit" }, [
82
+ baselineBtn(ctx.id, runId, baselineRunIds.has(runId), ctx.dir ?? "", reload),
83
+ ]),
84
+ ]),
85
+ );
86
+ }
87
+ root.appendChild(
88
+ el("div", { class: "table-scroll" }, [
89
+ el("table", { class: "fleet" }, [
90
+ el(
91
+ "thead",
92
+ null,
93
+ el(
94
+ "tr",
95
+ null,
96
+ ["When", "Run", "Dataset", "Pass rate", "Mean score", "Cost", "Flags", "Baseline"].map(
97
+ (h) => el("th", { text: h }),
98
+ ),
99
+ ),
100
+ ),
101
+ tbody,
102
+ ]),
103
+ ]),
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Re-pin the eval baseline to this run. The baseline key is
109
+ * `(specName, datasetName)` BY DESIGN — that is what keeps a spec edit gated
110
+ * against the same measurement — so the server copies both off the run's own
111
+ * index entry and this button only names the run.
112
+ */
113
+ function baselineBtn(harnessId, runId, isBaseline, dir, reload) {
114
+ if (isBaseline) return el("span", { class: "chip chip-group", text: "baseline" });
115
+ const btn = el("button", {
116
+ class: "btn btn-ghost",
117
+ type: "button",
118
+ text: "Pin baseline",
119
+ title: "make this run the gate every future run is compared against",
120
+ });
121
+ btn.addEventListener("click", async () => {
122
+ btn.disabled = true;
123
+ try {
124
+ await api.pinBaseline(harnessId, runId);
125
+ } catch (err) {
126
+ btn.disabled = false;
127
+ toast(`Re-pin failed: ${err instanceof Error ? err.message : String(err)}`);
128
+ return;
129
+ }
130
+ toast("Baseline re-pinned", "info");
131
+ reload();
132
+ });
133
+ return el("div", { class: "job-action" }, [btn, actionTwin("baseline", { dir, runId })]);
134
+ }
135
+
136
+ async function renderRun(root, ctx, runId) {
137
+ clear(root).appendChild(skeleton(6));
138
+ const data = await api.evalRun(ctx.id, runId);
139
+ clear(root);
140
+ root.appendChild(
141
+ el("div", { class: "crumb-line" }, [
142
+ el("a", { href: hrefHarness(ctx.id, "evals"), text: "← eval history" }),
143
+ el("span", { class: "mono muted", text: runId }),
144
+ ]),
145
+ );
146
+ if (data === null) {
147
+ root.appendChild(emptyState("Nothing here yet — no run with that id"));
148
+ return;
149
+ }
150
+ // The run view is { runId, summary, sampleIds }: `summary` is the run's
151
+ // parsed results.json (tolerated shape), `sampleIds` the on-disk sample
152
+ // dirs — the navigable authority for the drill-down table.
153
+ const summary = data.summary && typeof data.summary === "object" ? data.summary : {};
154
+ root.appendChild(
155
+ el("div", { class: "card" }, [
156
+ el("div", { class: "mini-row" }, [
157
+ miniStat("pass rate", fmtPct(numOr(summary.passRate))),
158
+ miniStat("mean score", fmtScore(summary.meanScore)),
159
+ miniStat("samples", String(summary.sampleCount ?? summary.samples ?? "—")),
160
+ miniStat("cost", fmtUsd(numOr(summary.costUsd))),
161
+ ]),
162
+ ]),
163
+ );
164
+
165
+ const sampleIds = Array.isArray(data.sampleIds) ? data.sampleIds.map(String) : [];
166
+ if (sampleIds.length === 0) {
167
+ root.appendChild(emptyState("No per-sample results recorded for this run"));
168
+ return;
169
+ }
170
+ // Join per-sample verdicts out of the summary when it carries them.
171
+ const verdicts = new Map();
172
+ const summaryRows = Array.isArray(summary.results)
173
+ ? summary.results
174
+ : Array.isArray(summary.samples)
175
+ ? summary.samples
176
+ : [];
177
+ for (const s of summaryRows) {
178
+ if (!s || typeof s !== "object") continue;
179
+ const sid = String(s.sampleId ?? s.id ?? "");
180
+ if (sid !== "") verdicts.set(sid, s);
181
+ }
182
+ const known = sampleIds.some((sid) => typeof verdicts.get(sid)?.pass === "boolean");
183
+
184
+ let filter = "all";
185
+ const listWrap = el("div");
186
+ const drawList = () => {
187
+ clear(listWrap);
188
+ const visible = sampleIds.filter((sid) => {
189
+ if (filter === "all") return true;
190
+ const pass = verdicts.get(sid)?.pass;
191
+ return filter === "pass" ? pass === true : pass !== true;
192
+ });
193
+ if (visible.length === 0) {
194
+ listWrap.appendChild(emptyState(`No ${filter} samples`));
195
+ return;
196
+ }
197
+ const tbody = el("tbody");
198
+ for (const sid of visible) {
199
+ const s = verdicts.get(sid) ?? {};
200
+ tbody.appendChild(
201
+ el("tr", null, [
202
+ el("td", null, [
203
+ el("a", {
204
+ class: "mono name-link",
205
+ href: hrefHarness(ctx.id, "evals", runId, sid),
206
+ text: sid || "(sample)",
207
+ }),
208
+ ]),
209
+ el("td", null, [passFailDot(s.pass)]),
210
+ el("td", { class: "num", text: fmtScore(s.score) }),
211
+ el("td", { class: "muted", text: String(s.reason ?? s.verdict ?? "") }),
212
+ ]),
213
+ );
214
+ }
215
+ listWrap.appendChild(
216
+ el("div", { class: "table-scroll" }, [
217
+ el("table", { class: "fleet" }, [
218
+ el(
219
+ "thead",
220
+ null,
221
+ el(
222
+ "tr",
223
+ null,
224
+ ["Sample", "Verdict", "Score", "Notes"].map((h) => el("th", { text: h })),
225
+ ),
226
+ ),
227
+ tbody,
228
+ ]),
229
+ ]),
230
+ );
231
+ };
232
+ if (known) {
233
+ const filterBar = el(
234
+ "div",
235
+ { class: "filter-bar", role: "group", "aria-label": "sample filter" },
236
+ ["all", "pass", "fail"].map((f) => {
237
+ const btn = el("button", { class: "btn btn-ghost", type: "button", text: f });
238
+ btn.addEventListener("click", () => {
239
+ filter = f;
240
+ for (const b of filterBar.querySelectorAll("button")) b.classList.remove("active");
241
+ btn.classList.add("active");
242
+ drawList();
243
+ });
244
+ if (f === "all") btn.classList.add("active");
245
+ return btn;
246
+ }),
247
+ );
248
+ root.appendChild(filterBar);
249
+ }
250
+ drawList();
251
+ root.appendChild(listWrap);
252
+ }
253
+
254
+ async function renderSample(root, ctx, runId, sampleId) {
255
+ clear(root).appendChild(skeleton(6));
256
+ const data = await api.evalSample(ctx.id, runId, sampleId);
257
+ clear(root);
258
+ root.appendChild(
259
+ el("div", { class: "crumb-line" }, [
260
+ el("a", { href: hrefHarness(ctx.id, "evals", runId), text: "← run" }),
261
+ el("span", { class: "mono muted", text: `${runId} / ${sampleId}` }),
262
+ ]),
263
+ );
264
+ if (data === null) {
265
+ root.appendChild(emptyState("Nothing here yet — no sample with that id"));
266
+ return;
267
+ }
268
+ // grades.json is passed through tolerantly: a list of grader results or
269
+ // one bare result object.
270
+ const grades = Array.isArray(data.grades)
271
+ ? data.grades
272
+ : data.grades && typeof data.grades === "object"
273
+ ? [data.grades]
274
+ : [];
275
+ const gradeCard = el("div", { class: "card" }, [
276
+ el("h3", { class: "card-title", text: "Grades" }),
277
+ grades.length === 0
278
+ ? el("p", { class: "muted", text: "No grades recorded." })
279
+ : el(
280
+ "ul",
281
+ { class: "check-list" },
282
+ grades.map((g) =>
283
+ el("li", null, [
284
+ passFailDot(g.pass),
285
+ el("span", { text: ` ${String(g.grader ?? g.name ?? "grader")}` }),
286
+ el("span", { class: "num muted", text: ` ${fmtScore(g.score)}` }),
287
+ g.rationale ? el("div", { class: "muted reason", text: String(g.rationale) }) : null,
288
+ ]),
289
+ ),
290
+ ),
291
+ ]);
292
+ root.appendChild(gradeCard);
293
+
294
+ const transcript = Array.isArray(data.transcript) ? data.transcript : [];
295
+ const excerptCard = el("div", { class: "card" }, [
296
+ el("h3", { class: "card-title" }, [
297
+ el("span", { text: "Transcript" }),
298
+ data.transcriptTruncated === true
299
+ ? el("span", { class: "chip chip-warn", text: "truncated" })
300
+ : null,
301
+ ]),
302
+ ]);
303
+ if (transcript.length === 0) {
304
+ excerptCard.appendChild(el("p", { class: "muted", text: "No transcript captured." }));
305
+ } else {
306
+ excerptCard.appendChild(jsonPre(transcript));
307
+ }
308
+ root.appendChild(excerptCard);
309
+ }
310
+
311
+ function miniStat(label, value) {
312
+ return el("div", { class: "mini" }, [
313
+ el("div", { class: "mini-num", text: value }),
314
+ el("div", { class: "mini-label", text: label }),
315
+ ]);
316
+ }
317
+
318
+ function numOr(v) {
319
+ return typeof v === "number" ? v : null;
320
+ }
321
+
322
+ function fmtScore(v) {
323
+ return typeof v === "number" && Number.isFinite(v) ? v.toFixed(2) : "—";
324
+ }
325
+
326
+ function passDot(passRate, gatePassed) {
327
+ const pct = fmtPct(numOr(passRate));
328
+ if (
329
+ gatePassed === false ||
330
+ (typeof passRate === "number" && passRate < 1 && gatePassed !== true)
331
+ ) {
332
+ return dot(typeof passRate === "number" && passRate < 1 ? "warn" : "unknown", pct);
333
+ }
334
+ if (typeof passRate !== "number") return dot("unknown", pct);
335
+ return dot(passRate >= 1 ? "ok" : "warn", pct);
336
+ }
337
+
338
+ function passFailDot(pass) {
339
+ if (pass === true) return dot("ok", "pass");
340
+ if (pass === false) return dot("bad", "fail");
341
+ return dot("unknown", "?");
342
+ }