@crowandraven/evalkeeper 0.0.0-stage → 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.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/bin/evalkeeper.mjs +9 -0
  4. package/dist/attention.d.ts +49 -0
  5. package/dist/attention.js +48 -0
  6. package/dist/attention.js.map +1 -0
  7. package/dist/board.d.ts +55 -0
  8. package/dist/board.js +2 -0
  9. package/dist/board.js.map +1 -0
  10. package/dist/cli/addPages.d.ts +79 -0
  11. package/dist/cli/addPages.js +181 -0
  12. package/dist/cli/addPages.js.map +1 -0
  13. package/dist/cli/main.d.ts +18 -0
  14. package/dist/cli/main.js +115 -0
  15. package/dist/cli/main.js.map +1 -0
  16. package/dist/common.d.ts +72 -0
  17. package/dist/common.js +276 -0
  18. package/dist/common.js.map +1 -0
  19. package/dist/compare.d.ts +85 -0
  20. package/dist/compare.js +185 -0
  21. package/dist/compare.js.map +1 -0
  22. package/dist/decisions.d.ts +84 -0
  23. package/dist/decisions.js +146 -0
  24. package/dist/decisions.js.map +1 -0
  25. package/dist/feedback.d.ts +114 -0
  26. package/dist/feedback.js +116 -0
  27. package/dist/feedback.js.map +1 -0
  28. package/dist/format.d.ts +6 -0
  29. package/dist/format.js +22 -0
  30. package/dist/format.js.map +1 -0
  31. package/dist/glossary.d.ts +46 -0
  32. package/dist/glossary.js +45 -0
  33. package/dist/glossary.js.map +1 -0
  34. package/dist/health.d.ts +40 -0
  35. package/dist/health.js +101 -0
  36. package/dist/health.js.map +1 -0
  37. package/dist/index.d.ts +31 -0
  38. package/dist/index.js +34 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/labels.d.ts +8 -0
  41. package/dist/labels.js +15 -0
  42. package/dist/labels.js.map +1 -0
  43. package/dist/legs.d.ts +23 -0
  44. package/dist/legs.js +22 -0
  45. package/dist/legs.js.map +1 -0
  46. package/dist/memoryStore.d.ts +21 -0
  47. package/dist/memoryStore.js +228 -0
  48. package/dist/memoryStore.js.map +1 -0
  49. package/dist/models.d.ts +50 -0
  50. package/dist/models.js +39 -0
  51. package/dist/models.js.map +1 -0
  52. package/dist/prune.d.ts +48 -0
  53. package/dist/prune.js +77 -0
  54. package/dist/prune.js.map +1 -0
  55. package/dist/quickSet.d.ts +26 -0
  56. package/dist/quickSet.js +44 -0
  57. package/dist/quickSet.js.map +1 -0
  58. package/dist/runner.d.ts +193 -0
  59. package/dist/runner.js +706 -0
  60. package/dist/runner.js.map +1 -0
  61. package/dist/settings.d.ts +157 -0
  62. package/dist/settings.js +183 -0
  63. package/dist/settings.js.map +1 -0
  64. package/dist/stableJson.d.ts +11 -0
  65. package/dist/stableJson.js +15 -0
  66. package/dist/stableJson.js.map +1 -0
  67. package/dist/stats.d.ts +98 -0
  68. package/dist/stats.js +119 -0
  69. package/dist/stats.js.map +1 -0
  70. package/dist/store.d.ts +146 -0
  71. package/dist/store.js +3 -0
  72. package/dist/store.js.map +1 -0
  73. package/dist/suggest.d.ts +75 -0
  74. package/dist/suggest.js +150 -0
  75. package/dist/suggest.js.map +1 -0
  76. package/dist/summary.d.ts +51 -0
  77. package/dist/summary.js +61 -0
  78. package/dist/summary.js.map +1 -0
  79. package/dist/testing.d.ts +26 -0
  80. package/dist/testing.js +341 -0
  81. package/dist/testing.js.map +1 -0
  82. package/dist/timeout.d.ts +2 -0
  83. package/dist/timeout.js +15 -0
  84. package/dist/timeout.js.map +1 -0
  85. package/dist/types.d.ts +219 -0
  86. package/dist/types.js +46 -0
  87. package/dist/types.js.map +1 -0
  88. package/dist/upkeepRules.d.ts +28 -0
  89. package/dist/upkeepRules.js +40 -0
  90. package/dist/upkeepRules.js.map +1 -0
  91. package/dist/util.d.ts +8 -0
  92. package/dist/util.js +18 -0
  93. package/dist/util.js.map +1 -0
  94. package/dist/weekly.d.ts +80 -0
  95. package/dist/weekly.js +111 -0
  96. package/dist/weekly.js.map +1 -0
  97. package/package.json +81 -4
  98. package/src/attention.ts +88 -0
  99. package/src/board.ts +56 -0
  100. package/src/cli/addPages.ts +256 -0
  101. package/src/cli/main.ts +125 -0
  102. package/src/common.ts +338 -0
  103. package/src/compare.ts +261 -0
  104. package/src/decisions.ts +230 -0
  105. package/src/feedback.ts +225 -0
  106. package/src/format.ts +19 -0
  107. package/src/glossary.ts +75 -0
  108. package/src/health.ts +147 -0
  109. package/src/index.ts +33 -0
  110. package/src/labels.ts +18 -0
  111. package/src/legs.ts +38 -0
  112. package/src/memoryStore.ts +268 -0
  113. package/src/models.ts +82 -0
  114. package/src/prune.ts +95 -0
  115. package/src/quickSet.ts +55 -0
  116. package/src/runner.ts +971 -0
  117. package/src/settings.ts +331 -0
  118. package/src/stableJson.ts +24 -0
  119. package/src/stats.ts +215 -0
  120. package/src/store.ts +167 -0
  121. package/src/suggest.ts +245 -0
  122. package/src/summary.ts +109 -0
  123. package/src/testing.ts +392 -0
  124. package/src/timeout.ts +14 -0
  125. package/src/types.ts +252 -0
  126. package/src/upkeepRules.ts +46 -0
  127. package/src/util.ts +17 -0
  128. package/src/weekly.ts +167 -0
  129. package/starter/index.ts +86 -0
  130. package/templates/app/api/cron/evals-weekly/route.ts +24 -0
  131. package/templates/app/evals/[suite]/page.tsx +449 -0
  132. package/templates/app/evals/[suite]/runs/[runId]/page.tsx +95 -0
  133. package/templates/app/evals/feedback/page.tsx +219 -0
  134. package/templates/app/evals/page.tsx +313 -0
  135. package/templates/components/evalkeeper/AddCase.tsx +62 -0
  136. package/templates/components/evalkeeper/AnswerKey.tsx +360 -0
  137. package/templates/components/evalkeeper/CaseMenu.tsx +122 -0
  138. package/templates/components/evalkeeper/Fields.tsx +110 -0
  139. package/templates/components/evalkeeper/RefreshWhile.tsx +25 -0
  140. package/templates/components/evalkeeper/SettingsHistory.tsx +191 -0
  141. package/templates/components/evalkeeper/Submit.tsx +58 -0
  142. package/templates/components/evalkeeper/Thumbs.tsx +117 -0
  143. package/templates/components/evalkeeper/Tip.tsx +98 -0
  144. package/templates/components/evalkeeper/WeeklyCheck.tsx +124 -0
  145. package/templates/components/evalkeeper/evalkeeper.css +71 -0
  146. package/templates/components/evalkeeper/ui.tsx +103 -0
  147. package/templates/lib/evalkeeper/actions.ts +263 -0
  148. package/templates/lib/evalkeeper/cronAuth.ts +18 -0
  149. package/templates/lib/evalkeeper/paths.ts +11 -0
  150. package/templates/lib/evalkeeper/time.ts +4 -0
  151. package/templates/lib/evalkeeper/weekly.ts +51 -0
@@ -0,0 +1,449 @@
1
+ import Link from "next/link";
2
+ import { notFound } from "next/navigation";
3
+ import {
4
+ FAVOR_TEXT,
5
+ checkBar,
6
+ meanCalibrationError,
7
+ money,
8
+ probabilityQuality,
9
+ perAnswer,
10
+ type ReasoningEffort,
11
+ retiresLabel,
12
+ seconds,
13
+ seriousMistakes,
14
+ suggest,
15
+ } from "@crowandraven/evalkeeper";
16
+ import { AnswerKey } from "@/components/evalkeeper/AnswerKey";
17
+ import { RefreshWhile } from "@/components/evalkeeper/RefreshWhile";
18
+ import { IconSubmit, Spinner, Submit } from "@/components/evalkeeper/Submit";
19
+ import { RotateCw } from "lucide-react";
20
+ import { BackLink, Card, Chip, Score, pct } from "@/components/evalkeeper/ui";
21
+ import {
22
+ MODELS,
23
+ requireAdmin,
24
+ runner,
25
+ settings,
26
+ SUITE_BY_KEY,
27
+ } from "@/lib/evalkeeper";
28
+ import { SettingsHistory } from "@/components/evalkeeper/SettingsHistory";
29
+ import {
30
+ makeFallback,
31
+ makeLive,
32
+ rescore,
33
+ testAll,
34
+ testModel,
35
+ useSuggestion,
36
+ } from "@/lib/evalkeeper/actions";
37
+ import { InfoTip, Tip } from "@/components/evalkeeper/Tip";
38
+ import { EVALS_BASE, evalsPath } from "@/lib/evalkeeper/paths";
39
+
40
+ export const dynamic = "force-dynamic";
41
+
42
+ /** A share as a whole percent, or a dash when there's nothing to show. */
43
+ const asPct = (x: number | null) => (x === null ? "—" : pct(x));
44
+
45
+ const VS: Record<string, string> = {
46
+ BETTER: "More accurate",
47
+ SAME: "About as accurate",
48
+ WORSE: "Less accurate",
49
+ TOO_FEW: "Too few cases to tell",
50
+ };
51
+
52
+ export default async function SuitePage({
53
+ params,
54
+ }: {
55
+ params: Promise<{ suite: string }>;
56
+ }) {
57
+ await requireAdmin();
58
+ const { suite: key } = await params;
59
+ if (!SUITE_BY_KEY[key]) notFound();
60
+ const b = await runner.board(key);
61
+ // Records a deploy if the instructions changed under the live settings.
62
+ await settings.syncPrompt(b.suite.operation);
63
+ const { suite, rows } = b;
64
+ const s = suggest(rows, suite.checks, suite.priority, MODELS);
65
+ const testing = rows.some((r) => r.testing);
66
+ const [history, liveSettings] = await Promise.all([
67
+ settings.history(suite.operation),
68
+ settings.current(suite.operation),
69
+ ]);
70
+ // What testing each model would cost now, counting answers it would reuse.
71
+ const estimates = new Map(
72
+ await Promise.all(
73
+ rows.map(
74
+ async (r) =>
75
+ [
76
+ r.key,
77
+ await runner.estimateRun(key, r.model, "ALL", 1, {
78
+ reasoningEffort: r.effort as ReasoningEffort | null,
79
+ }),
80
+ ] as const,
81
+ ),
82
+ ),
83
+ );
84
+ const live = rows.find((r) => r.live);
85
+
86
+ return (
87
+ <div className="space-y-8">
88
+ <RefreshWhile active={testing} />
89
+ <div className="space-y-2">
90
+ <BackLink href={EVALS_BASE}>Evals</BackLink>
91
+ <h1 className="text-2xl font-semibold">{suite.label}</h1>
92
+ <p className="readable text-muted">{suite.description}</p>
93
+ <p className="readable text-sm text-muted">
94
+ Priority: {FAVOR_TEXT[suite.priority.favor]}.
95
+ <InfoTip term="priority" />
96
+ </p>
97
+ </div>
98
+
99
+ {s && !s.same && (
100
+ <Card className="space-y-3">
101
+ <div className="flex flex-wrap items-start justify-between gap-3">
102
+ <h2 className="font-semibold">
103
+ Suggested
104
+ <InfoTip term="suggestion" />
105
+ </h2>
106
+ <form action={useSuggestion}>
107
+ <input type="hidden" name="suite" value={key} />
108
+ <Submit>Use these</Submit>
109
+ </form>
110
+ </div>
111
+ <p>
112
+ Live: <strong>{s.live.label}</strong>
113
+ {s.fallback && (
114
+ <>
115
+ {" "}
116
+ · Fallback: <strong>{s.fallback.label}</strong>
117
+ </>
118
+ )}
119
+ </p>
120
+ <ul className="readable list-disc space-y-1 pl-5 text-sm text-muted">
121
+ {s.reasons.map((r) => (
122
+ <li key={r}>{r}</li>
123
+ ))}
124
+ </ul>
125
+ </Card>
126
+ )}
127
+
128
+ <section className="space-y-3">
129
+ <div className="flex items-center justify-between gap-4">
130
+ <h2 className="text-lg font-semibold">Models</h2>
131
+ <form action={testAll}>
132
+ <input type="hidden" name="suite" value={key} />
133
+ <Submit variant="quiet">Test every model</Submit>
134
+ </form>
135
+ </div>
136
+ <Card className="overflow-x-auto p-0 lg:overflow-visible">
137
+ <table className="w-full text-sm">
138
+ <thead className="text-left text-muted">
139
+ <tr className="border-b border-line">
140
+ <th className="p-3 font-normal">Model</th>
141
+ <th className="p-3 font-normal">
142
+ <Tip term="score" side="bottom">
143
+ Score
144
+ </Tip>
145
+ </th>
146
+ <th className="p-3 font-normal">
147
+ <Tip term="seriousMistakes" side="bottom">
148
+ Serious mistakes
149
+ </Tip>
150
+ </th>
151
+ <th className="p-3 font-normal">
152
+ <Tip term="time" side="bottom" align="end">
153
+ Time
154
+ </Tip>
155
+ </th>
156
+ {suite.decisions && (
157
+ <>
158
+ <th className="p-3 font-normal">
159
+ <Tip term="probabilityQuality" side="bottom">
160
+ Probability quality
161
+ </Tip>
162
+ </th>
163
+ <th className="p-3 font-normal">
164
+ <Tip term="calibrationError" side="bottom" align="end">
165
+ Calibration error
166
+ </Tip>
167
+ </th>
168
+ </>
169
+ )}
170
+ <th className="p-3 font-normal">
171
+ <Tip term="cost" side="bottom" align="end">
172
+ Cost
173
+ </Tip>
174
+ </th>
175
+ <th className="p-3 font-normal">
176
+ <Tip term="againstLive" side="bottom" align="end">
177
+ Against live
178
+ </Tip>
179
+ </th>
180
+ <th className="p-3" />
181
+ </tr>
182
+ </thead>
183
+ <tbody>
184
+ {rows.map((r) => (
185
+ <tr key={r.key} className="border-b border-line last:border-0">
186
+ <td className="p-3">
187
+ <div className="flex flex-wrap items-center gap-2">
188
+ <span
189
+ className={r.live ? "font-semibold" : "font-medium"}
190
+ >
191
+ {r.label}
192
+ </span>
193
+ {r.live && (
194
+ <Tip term="live" side="bottom" id={`live-${r.key}`}>
195
+ <Chip tone="live">Live</Chip>
196
+ </Tip>
197
+ )}
198
+ {r.fallback && (
199
+ <Tip
200
+ term="fallback"
201
+ side="bottom"
202
+ id={`fallback-${r.key}`}
203
+ >
204
+ <Chip tone="fallback">Fallback</Chip>
205
+ </Tip>
206
+ )}
207
+ {retiresLabel(MODELS, r.model) && (
208
+ <Tip
209
+ term="retiring"
210
+ side="bottom"
211
+ id={`retiring-${r.key}`}
212
+ >
213
+ <Chip tone="warn">
214
+ Retires {retiresLabel(MODELS, r.model)}
215
+ </Chip>
216
+ </Tip>
217
+ )}
218
+ {suite.decisions && MODELS[r.model]?.decisions && (
219
+ <Tip
220
+ term={
221
+ MODELS[r.model]?.decisions === "native"
222
+ ? "nativeProbabilities"
223
+ : "promptedProbabilities"
224
+ }
225
+ side="bottom"
226
+ id={`decisions-${r.key}`}
227
+ >
228
+ <Chip tone="neutral">
229
+ {MODELS[r.model]?.decisions === "native"
230
+ ? "Native probabilities"
231
+ : "Prompted"}
232
+ </Chip>
233
+ </Tip>
234
+ )}
235
+ {r.test?.stale && (
236
+ <Tip
237
+ term="outOfDate"
238
+ side="bottom"
239
+ id={`stale-${r.key}`}
240
+ >
241
+ <Chip tone="neutral">Out of date</Chip>
242
+ </Tip>
243
+ )}
244
+ </div>
245
+ </td>
246
+ <td className="p-3">
247
+ {r.testing ? (
248
+ <span
249
+ role="status"
250
+ title="Testing"
251
+ className="inline-flex text-accent"
252
+ >
253
+ <Spinner />
254
+ </span>
255
+ ) : r.test ? (
256
+ <Link href={evalsPath(key, "runs", r.test.runId)}>
257
+ <Score right={r.test.right} total={r.test.total} />
258
+ </Link>
259
+ ) : (
260
+ <span className="text-muted">
261
+ {r.failed ?? "Not tested"}
262
+ </span>
263
+ )}
264
+ </td>
265
+ <td className="p-3 tabular-nums">
266
+ {r.test ? seriousMistakes(r, suite.checks) : "—"}
267
+ </td>
268
+ <td className="p-3 tabular-nums">
269
+ {r.test ? seconds(r.test.avgMs) : "—"}
270
+ </td>
271
+ {suite.decisions && (
272
+ <>
273
+ <td className="p-3 tabular-nums">
274
+ {asPct(
275
+ r.test ? probabilityQuality(r.test.measures) : null,
276
+ )}
277
+ </td>
278
+ <td className="p-3 tabular-nums">
279
+ {asPct(
280
+ r.test ? meanCalibrationError(r.test.measures) : null,
281
+ )}
282
+ </td>
283
+ </>
284
+ )}
285
+ <td className="p-3 tabular-nums">
286
+ {r.test ? perAnswer(r.test.costPerAnswer) : "—"}
287
+ </td>
288
+ <td className="p-3">{r.vsLive ? VS[r.vsLive] : "—"}</td>
289
+ <td className="p-3">
290
+ <div className="flex flex-col items-end gap-0.5">
291
+ <div className="flex justify-end gap-2">
292
+ {r.canMakeFallback && (
293
+ <form action={makeFallback}>
294
+ <input type="hidden" name="suite" value={key} />
295
+ <input type="hidden" name="model" value={r.model} />
296
+ <Submit variant="quiet">Make fallback</Submit>
297
+ </form>
298
+ )}
299
+ {!r.live && r.canMakeLive && (
300
+ <form action={makeLive}>
301
+ <input type="hidden" name="suite" value={key} />
302
+ <input type="hidden" name="model" value={r.model} />
303
+ <Submit variant="quiet">Make live</Submit>
304
+ </form>
305
+ )}
306
+ <form action={testModel}>
307
+ <input type="hidden" name="suite" value={key} />
308
+ <input type="hidden" name="model" value={r.model} />
309
+ <Tip
310
+ control
311
+ id={`test-${r.key}`}
312
+ text={
313
+ r.test
314
+ ? "Test this model again on every case in use."
315
+ : "Test this model on every case in use."
316
+ }
317
+ side="bottom"
318
+ align="end"
319
+ >
320
+ <IconSubmit
321
+ label="Test"
322
+ icon={
323
+ <RotateCw aria-hidden className="h-4 w-4" />
324
+ }
325
+ />
326
+ </Tip>
327
+ </form>
328
+ </div>
329
+ <EstimateNote
330
+ id={r.key}
331
+ estimate={estimates.get(r.key)}
332
+ />
333
+ </div>
334
+ </td>
335
+ </tr>
336
+ ))}
337
+ </tbody>
338
+ </table>
339
+ </Card>
340
+ </section>
341
+
342
+ <SettingsHistory
343
+ suite={key}
344
+ entries={history}
345
+ liveId={liveSettings.version?.id ?? null}
346
+ label={(m) => MODELS[m]?.label ?? m}
347
+ defaults={liveSettings.settings}
348
+ />
349
+
350
+ <section className="space-y-3">
351
+ <h2 className="text-lg font-semibold">
352
+ Checks
353
+ <InfoTip term="checks" />
354
+ </h2>
355
+ <div className="grid gap-3 md:grid-cols-2">
356
+ {suite.checks.map((c) => {
357
+ const rate = live?.test?.byCheck[c.key];
358
+ return (
359
+ <Card key={c.key} className="space-y-1">
360
+ <div className="flex justify-between gap-4">
361
+ <h3 className="font-medium">{c.label}</h3>
362
+ <Tip term={c.severity} id={`severity-${c.key}`}>
363
+ <Chip
364
+ tone={c.severity === "serious" ? "serious" : "neutral"}
365
+ >
366
+ {c.severity}
367
+ </Chip>
368
+ </Tip>
369
+ </div>
370
+ <p className="readable text-sm text-muted">{c.description}</p>
371
+ <p className="text-sm">
372
+ {rate && rate.n > 0 && (
373
+ <>
374
+ Live:{" "}
375
+ <span
376
+ className={
377
+ rate.passed / rate.n >= checkBar(c)
378
+ ? "text-good"
379
+ : "text-warn"
380
+ }
381
+ >
382
+ {pct(rate.passed / rate.n)}
383
+ </span>{" "}
384
+ ({rate.passed}/{rate.n}) ·{" "}
385
+ </>
386
+ )}
387
+ <Tip term="bar" id={`bar-${c.key}`}>
388
+ <span className="text-muted">Bar: {pct(checkBar(c))}</span>
389
+ </Tip>
390
+ </p>
391
+ </Card>
392
+ );
393
+ })}
394
+ </div>
395
+ </section>
396
+
397
+ <section className="space-y-3">
398
+ <AnswerKey
399
+ suite={suite}
400
+ rows={rows}
401
+ heading={
402
+ <h2 className="text-lg font-semibold">
403
+ Answer key ({b.cases} cases in use)
404
+ <InfoTip term="answerKey" />
405
+ </h2>
406
+ }
407
+ tools={
408
+ <form action={rescore}>
409
+ <input type="hidden" name="suite" value={key} />
410
+ <Tip control term="rescore" side="bottom" align="end">
411
+ <IconSubmit
412
+ label="Rescore tests"
413
+ icon={<RotateCw aria-hidden className="h-4 w-4" />}
414
+ />
415
+ </Tip>
416
+ </form>
417
+ }
418
+ />
419
+ </section>
420
+ </div>
421
+ );
422
+ }
423
+
424
+ /** "Free: all 16 reused" or "~$0.03, 4 of 16 reused", under a model's buttons. */
425
+ function EstimateNote({
426
+ id,
427
+ estimate,
428
+ }: {
429
+ id: string;
430
+ estimate:
431
+ { answers: number; reused: number; costUsd: number | null } | undefined;
432
+ }) {
433
+ if (!estimate || estimate.answers === 0) return null;
434
+ const { answers, reused, costUsd } = estimate;
435
+ const text =
436
+ reused === answers
437
+ ? `Free: all ${answers} reused`
438
+ : `${costUsd === null ? "Cost unknown" : `~${money(costUsd)}`}${reused ? `, ${reused} of ${answers} reused` : ""}`;
439
+ return (
440
+ <Tip
441
+ text="What testing this model costs now. Answers an identical request gave in the last 7 days are reused for free."
442
+ id={`estimate-${id}`}
443
+ side="bottom"
444
+ align="end"
445
+ >
446
+ <span className="whitespace-nowrap text-xs text-muted">{text}</span>
447
+ </Tip>
448
+ );
449
+ }
@@ -0,0 +1,95 @@
1
+ import { notFound } from "next/navigation";
2
+ import { allPassed, money, summarize } from "@crowandraven/evalkeeper";
3
+ import { BackLink, Card, pct } from "@/components/evalkeeper/ui";
4
+ import { MODELS, requireAdmin, runner, store } from "@/lib/evalkeeper";
5
+ import { evalsPath } from "@/lib/evalkeeper/paths";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ /** One test: every answer, with what each check found. */
10
+ export default async function RunPage({
11
+ params,
12
+ }: {
13
+ params: Promise<{ suite: string; runId: string }>;
14
+ }) {
15
+ await requireAdmin();
16
+ const { suite: key, runId } = await params;
17
+ const run = await store.getRun(runId);
18
+ if (!run || run.suite !== key) notFound();
19
+ const suite = runner.suiteOrThrow(key);
20
+ const [outputs, cases] = await Promise.all([
21
+ store.listOutputs(runId),
22
+ store.listCases(key).then((cs) => new Map(cs.map((c) => [c.id, c]))),
23
+ ]);
24
+ const titleOf = (caseId: string) => {
25
+ const c = cases.get(caseId);
26
+ return c ? suite.title(suite.input.parse(c.input)) : "A retired case";
27
+ };
28
+ const metrics = run.metrics;
29
+ const summary = metrics ? summarize(metrics, suite.checks) : null;
30
+ const label = (k: string) =>
31
+ suite.checks.find((c) => c.key === k)?.label ?? k;
32
+ type Check = { key: string; pass: boolean | null; note?: string };
33
+
34
+ return (
35
+ <div className="space-y-6">
36
+ <BackLink href={evalsPath(key)}>{suite.label}</BackLink>
37
+ <div className="space-y-1">
38
+ <h1 className="text-2xl font-semibold">
39
+ {MODELS[run.model]?.label ?? run.model} on {suite.label}
40
+ </h1>
41
+ <p className="text-sm text-muted">
42
+ {run.startedAt.toLocaleString()} · {run.status.toLowerCase()}
43
+ {metrics && ` · spent ${money(metrics.costUsd)}`}
44
+ </p>
45
+ {run.error && <p className="readable text-bad">{run.error}</p>}
46
+ </div>
47
+ {summary && (
48
+ <Card className="space-y-2">
49
+ <p className="text-lg">
50
+ {summary.right} of {summary.total} answers fully right (
51
+ {pct(summary.total ? summary.right / summary.total : 0)})
52
+ </p>
53
+ <p className="readable text-sm text-muted">{summary.trust}</p>
54
+ {summary.problems.length > 0 && (
55
+ <ul className="readable list-disc pl-5 text-sm">
56
+ {summary.problems.map((p) => (
57
+ <li key={p.key}>
58
+ {p.label}: failed {p.failed} of {p.of}
59
+ </li>
60
+ ))}
61
+ </ul>
62
+ )}
63
+ </Card>
64
+ )}
65
+ <Card className="space-y-3">
66
+ {outputs.map((o) => {
67
+ const checks = (o.scores as Check[] | null) ?? null;
68
+ const ok = allPassed(checks);
69
+ return (
70
+ <div key={o.id} className="rounded-md bg-tint p-3 text-sm">
71
+ <div className="flex justify-between gap-4">
72
+ <span className="font-medium">{titleOf(o.caseId)}</span>
73
+ <span
74
+ className={
75
+ o.error ? "text-bad" : ok ? "text-good" : "text-warn"
76
+ }
77
+ >
78
+ {o.error ? "No answer" : ok ? "Right" : "Wrong"}
79
+ </span>
80
+ </div>
81
+ {o.error && <p className="text-bad">{o.error}</p>}
82
+ {checks
83
+ ?.filter((c) => c.pass === false)
84
+ .map((c) => (
85
+ <p key={c.key} className="readable text-muted">
86
+ {label(c.key)}: {c.note ?? "failed"}
87
+ </p>
88
+ ))}
89
+ </div>
90
+ );
91
+ })}
92
+ </Card>
93
+ </div>
94
+ );
95
+ }