@crowandraven/evalkeeper 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +6 -2
- package/bin/evalkeeper.mjs +9 -0
- package/dist/attention.d.ts +49 -0
- package/dist/attention.js +49 -0
- package/dist/attention.js.map +1 -0
- package/dist/board.d.ts +57 -0
- package/dist/board.js +2 -0
- package/dist/board.js.map +1 -0
- package/dist/cli/addPages.d.ts +79 -0
- package/dist/cli/addPages.js +181 -0
- package/dist/cli/addPages.js.map +1 -0
- package/dist/cli/main.d.ts +18 -0
- package/dist/cli/main.js +115 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/common.d.ts +72 -0
- package/dist/common.js +276 -0
- package/dist/common.js.map +1 -0
- package/dist/compare.d.ts +85 -0
- package/dist/compare.js +185 -0
- package/dist/compare.js.map +1 -0
- package/dist/decisions.d.ts +84 -0
- package/dist/decisions.js +146 -0
- package/dist/decisions.js.map +1 -0
- package/dist/feedback.d.ts +114 -0
- package/dist/feedback.js +116 -0
- package/dist/feedback.js.map +1 -0
- package/dist/format.d.ts +6 -0
- package/dist/format.js +22 -0
- package/dist/format.js.map +1 -0
- package/dist/glossary.d.ts +46 -0
- package/dist/glossary.js +45 -0
- package/dist/glossary.js.map +1 -0
- package/dist/health.d.ts +40 -0
- package/dist/health.js +101 -0
- package/dist/health.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.d.ts +8 -0
- package/dist/labels.js +15 -0
- package/dist/labels.js.map +1 -0
- package/dist/legs.d.ts +23 -0
- package/dist/legs.js +22 -0
- package/dist/legs.js.map +1 -0
- package/dist/memoryStore.d.ts +21 -0
- package/dist/memoryStore.js +228 -0
- package/dist/memoryStore.js.map +1 -0
- package/dist/models.d.ts +76 -0
- package/dist/models.js +56 -0
- package/dist/models.js.map +1 -0
- package/dist/prune.d.ts +48 -0
- package/dist/prune.js +77 -0
- package/dist/prune.js.map +1 -0
- package/dist/quickSet.d.ts +26 -0
- package/dist/quickSet.js +44 -0
- package/dist/quickSet.js.map +1 -0
- package/dist/runner.d.ts +202 -0
- package/dist/runner.js +734 -0
- package/dist/runner.js.map +1 -0
- package/dist/settings.d.ts +196 -0
- package/dist/settings.js +272 -0
- package/dist/settings.js.map +1 -0
- package/dist/stableJson.d.ts +11 -0
- package/dist/stableJson.js +15 -0
- package/dist/stableJson.js.map +1 -0
- package/dist/stats.d.ts +98 -0
- package/dist/stats.js +119 -0
- package/dist/stats.js.map +1 -0
- package/dist/store.d.ts +151 -0
- package/dist/store.js +3 -0
- package/dist/store.js.map +1 -0
- package/dist/suggest.d.ts +75 -0
- package/dist/suggest.js +152 -0
- package/dist/suggest.js.map +1 -0
- package/dist/summary.d.ts +51 -0
- package/dist/summary.js +61 -0
- package/dist/summary.js.map +1 -0
- package/dist/testing.d.ts +26 -0
- package/dist/testing.js +362 -0
- package/dist/testing.js.map +1 -0
- package/dist/timeout.d.ts +2 -0
- package/dist/timeout.js +15 -0
- package/dist/timeout.js.map +1 -0
- package/dist/types.d.ts +225 -0
- package/dist/types.js +46 -0
- package/dist/types.js.map +1 -0
- package/dist/upkeepRules.d.ts +28 -0
- package/dist/upkeepRules.js +40 -0
- package/dist/upkeepRules.js.map +1 -0
- package/dist/util.d.ts +8 -0
- package/dist/util.js +18 -0
- package/dist/util.js.map +1 -0
- package/dist/weekly.d.ts +80 -0
- package/dist/weekly.js +113 -0
- package/dist/weekly.js.map +1 -0
- package/package.json +81 -4
- package/src/attention.ts +89 -0
- package/src/board.ts +58 -0
- package/src/cli/addPages.ts +256 -0
- package/src/cli/main.ts +125 -0
- package/src/common.ts +338 -0
- package/src/compare.ts +261 -0
- package/src/decisions.ts +230 -0
- package/src/feedback.ts +225 -0
- package/src/format.ts +19 -0
- package/src/glossary.ts +75 -0
- package/src/health.ts +149 -0
- package/src/index.ts +33 -0
- package/src/labels.ts +18 -0
- package/src/legs.ts +38 -0
- package/src/memoryStore.ts +268 -0
- package/src/models.ts +127 -0
- package/src/prune.ts +95 -0
- package/src/quickSet.ts +55 -0
- package/src/runner.ts +1010 -0
- package/src/settings.ts +441 -0
- package/src/stableJson.ts +24 -0
- package/src/stats.ts +215 -0
- package/src/store.ts +172 -0
- package/src/suggest.ts +247 -0
- package/src/summary.ts +109 -0
- package/src/testing.ts +415 -0
- package/src/timeout.ts +14 -0
- package/src/types.ts +258 -0
- package/src/upkeepRules.ts +46 -0
- package/src/util.ts +17 -0
- package/src/weekly.ts +169 -0
- package/starter/index.ts +86 -0
- package/templates/app/api/cron/evals-weekly/route.ts +24 -0
- package/templates/app/evals/[suite]/page.tsx +449 -0
- package/templates/app/evals/[suite]/runs/[runId]/page.tsx +95 -0
- package/templates/app/evals/feedback/page.tsx +219 -0
- package/templates/app/evals/page.tsx +313 -0
- package/templates/components/evalkeeper/AddCase.tsx +62 -0
- package/templates/components/evalkeeper/AnswerKey.tsx +360 -0
- package/templates/components/evalkeeper/CaseMenu.tsx +122 -0
- package/templates/components/evalkeeper/Fields.tsx +110 -0
- package/templates/components/evalkeeper/RefreshWhile.tsx +25 -0
- package/templates/components/evalkeeper/SettingsHistory.tsx +191 -0
- package/templates/components/evalkeeper/Submit.tsx +58 -0
- package/templates/components/evalkeeper/Thumbs.tsx +117 -0
- package/templates/components/evalkeeper/Tip.tsx +98 -0
- package/templates/components/evalkeeper/WeeklyCheck.tsx +124 -0
- package/templates/components/evalkeeper/evalkeeper.css +71 -0
- package/templates/components/evalkeeper/ui.tsx +103 -0
- package/templates/lib/evalkeeper/actions.ts +263 -0
- package/templates/lib/evalkeeper/cronAuth.ts +18 -0
- package/templates/lib/evalkeeper/paths.ts +11 -0
- package/templates/lib/evalkeeper/time.ts +4 -0
- package/templates/lib/evalkeeper/weekly.ts +51 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import {
|
|
3
|
+
asText,
|
|
4
|
+
thumbsSummary,
|
|
5
|
+
whyNotACase,
|
|
6
|
+
type AnySuite,
|
|
7
|
+
type FeedbackRecord,
|
|
8
|
+
} from "@crowandraven/evalkeeper";
|
|
9
|
+
import { FieldValues } from "@/components/evalkeeper/Fields";
|
|
10
|
+
import { Submit } from "@/components/evalkeeper/Submit";
|
|
11
|
+
import { InfoTip, Tip } from "@/components/evalkeeper/Tip";
|
|
12
|
+
import { BackLink, Card, Chip, pct } from "@/components/evalkeeper/ui";
|
|
13
|
+
import { ThumbsDown, ThumbsUp } from "lucide-react";
|
|
14
|
+
import { feedbackStore, requireAdmin, SUITES } from "@/lib/evalkeeper";
|
|
15
|
+
import { daysAgo } from "@/lib/evalkeeper/time";
|
|
16
|
+
import { dismissFeedback, makeCase } from "@/lib/evalkeeper/actions";
|
|
17
|
+
import { EVALS_BASE } from "@/lib/evalkeeper/paths";
|
|
18
|
+
|
|
19
|
+
export const dynamic = "force-dynamic";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What people thought of the AI's answers: thumbs per feature this week,
|
|
23
|
+
* and each rating waiting for a look, to make into a case or dismiss.
|
|
24
|
+
*/
|
|
25
|
+
export default async function FeedbackPage() {
|
|
26
|
+
await requireAdmin();
|
|
27
|
+
const all = await feedbackStore.listFeedback();
|
|
28
|
+
const summary = thumbsSummary(all, daysAgo(7));
|
|
29
|
+
const open = all
|
|
30
|
+
.filter((f) => f.status === "OPEN")
|
|
31
|
+
.sort((a, b) => Number(b.rating === "DOWN") - Number(a.rating === "DOWN"));
|
|
32
|
+
const suiteFor = (op: string) => SUITES.find((s) => s.operation === op);
|
|
33
|
+
const label = (op: string) => suiteFor(op)?.label ?? op;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="space-y-8">
|
|
37
|
+
<div className="space-y-2">
|
|
38
|
+
<BackLink href={EVALS_BASE}>Evals</BackLink>
|
|
39
|
+
<h1 className="text-2xl font-semibold">Feedback</h1>
|
|
40
|
+
<p className="readable text-muted">
|
|
41
|
+
Thumbs up and down from people using the product. A rating on a kept
|
|
42
|
+
input can become a draft case, so the next model is tested on the
|
|
43
|
+
answer people didn't like.
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<section className="space-y-3">
|
|
48
|
+
<h2 className="text-lg font-semibold">
|
|
49
|
+
This week
|
|
50
|
+
<InfoTip term="thumbs" />
|
|
51
|
+
</h2>
|
|
52
|
+
{summary.length === 0 ? (
|
|
53
|
+
<p className="text-sm text-muted">No ratings this week.</p>
|
|
54
|
+
) : (
|
|
55
|
+
<Card className="overflow-x-auto p-0 lg:overflow-visible">
|
|
56
|
+
<table className="w-full text-sm">
|
|
57
|
+
<thead className="text-left text-muted">
|
|
58
|
+
<tr className="border-b border-line">
|
|
59
|
+
<th className="p-3 font-normal">Feature</th>
|
|
60
|
+
<th className="p-3 font-normal">Thumbs up</th>
|
|
61
|
+
<th className="p-3 font-normal">Thumbs down</th>
|
|
62
|
+
<th className="p-3 font-normal">Share down</th>
|
|
63
|
+
<th className="p-3 font-normal">Waiting</th>
|
|
64
|
+
</tr>
|
|
65
|
+
</thead>
|
|
66
|
+
<tbody>
|
|
67
|
+
{summary.map((s) => (
|
|
68
|
+
<tr
|
|
69
|
+
key={s.operation}
|
|
70
|
+
className="border-b border-line last:border-0"
|
|
71
|
+
>
|
|
72
|
+
<td className="p-3">
|
|
73
|
+
<span className="mr-2 font-medium">
|
|
74
|
+
{label(s.operation)}
|
|
75
|
+
</span>
|
|
76
|
+
{s.alert && (
|
|
77
|
+
<Tip
|
|
78
|
+
text="At least 5 ratings, and at least 20% of them thumbs down: worth a look."
|
|
79
|
+
id={`alert-${s.operation}`}
|
|
80
|
+
side="bottom"
|
|
81
|
+
>
|
|
82
|
+
<Chip tone="warn">Needs a look</Chip>
|
|
83
|
+
</Tip>
|
|
84
|
+
)}
|
|
85
|
+
</td>
|
|
86
|
+
<td className="p-3 tabular-nums">{s.up}</td>
|
|
87
|
+
<td className="p-3 tabular-nums">{s.down}</td>
|
|
88
|
+
<td className="p-3 tabular-nums">
|
|
89
|
+
{s.downShare === null ? "—" : pct(s.downShare)}
|
|
90
|
+
</td>
|
|
91
|
+
<td className="p-3 tabular-nums">{s.open}</td>
|
|
92
|
+
</tr>
|
|
93
|
+
))}
|
|
94
|
+
</tbody>
|
|
95
|
+
</table>
|
|
96
|
+
</Card>
|
|
97
|
+
)}
|
|
98
|
+
</section>
|
|
99
|
+
|
|
100
|
+
<section className="space-y-3">
|
|
101
|
+
<h2 className="text-lg font-semibold">
|
|
102
|
+
Waiting for a look ({open.length})
|
|
103
|
+
</h2>
|
|
104
|
+
{open.length === 0 && (
|
|
105
|
+
<p className="text-sm text-muted">
|
|
106
|
+
Nothing waiting. Rate an answer on the{" "}
|
|
107
|
+
<Link href="/try" className="underline">
|
|
108
|
+
try it
|
|
109
|
+
</Link>{" "}
|
|
110
|
+
page to see one here.
|
|
111
|
+
</p>
|
|
112
|
+
)}
|
|
113
|
+
<div className="space-y-3">
|
|
114
|
+
{open.map((f) => (
|
|
115
|
+
<Rating
|
|
116
|
+
key={f.id}
|
|
117
|
+
fields={suiteFor(f.operation)?.fields}
|
|
118
|
+
f={f}
|
|
119
|
+
feature={label(f.operation)}
|
|
120
|
+
why={whyNotACase(f, suiteFor(f.operation))}
|
|
121
|
+
/>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
</section>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function Rating({
|
|
130
|
+
f,
|
|
131
|
+
fields,
|
|
132
|
+
feature,
|
|
133
|
+
why,
|
|
134
|
+
}: {
|
|
135
|
+
f: FeedbackRecord;
|
|
136
|
+
fields: AnySuite["fields"];
|
|
137
|
+
feature: string;
|
|
138
|
+
why: string | null;
|
|
139
|
+
}) {
|
|
140
|
+
return (
|
|
141
|
+
<article
|
|
142
|
+
aria-label={`Rating: ${feature}${f.reason ? `, ${f.reason}` : ""}`}
|
|
143
|
+
>
|
|
144
|
+
<Card className="space-y-3 text-sm">
|
|
145
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
146
|
+
<span className="flex items-center gap-2">
|
|
147
|
+
<Chip tone={f.rating === "DOWN" ? "serious" : "live"}>
|
|
148
|
+
{f.rating === "DOWN" ? (
|
|
149
|
+
<>
|
|
150
|
+
<ThumbsDown aria-hidden className="h-3.5 w-3.5" /> Thumbs down
|
|
151
|
+
</>
|
|
152
|
+
) : (
|
|
153
|
+
<>
|
|
154
|
+
<ThumbsUp aria-hidden className="h-3.5 w-3.5" /> Thumbs up
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</Chip>
|
|
158
|
+
<span className="font-medium">{feature}</span>
|
|
159
|
+
</span>
|
|
160
|
+
<span className="text-xs text-muted">
|
|
161
|
+
{f.updatedAt.toLocaleString()}
|
|
162
|
+
</span>
|
|
163
|
+
</div>
|
|
164
|
+
{f.reason && <p className="readable">“{f.reason}”</p>}
|
|
165
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
166
|
+
<div className="space-y-1">
|
|
167
|
+
<p className="text-xs font-semibold uppercase tracking-wide text-muted">
|
|
168
|
+
Input
|
|
169
|
+
</p>
|
|
170
|
+
{f.input === null ? (
|
|
171
|
+
<p className="text-muted">Not kept</p>
|
|
172
|
+
) : fields ? (
|
|
173
|
+
<FieldValues
|
|
174
|
+
fields={fields.input}
|
|
175
|
+
values={f.input}
|
|
176
|
+
prefix={`${f.id}-in-`}
|
|
177
|
+
/>
|
|
178
|
+
) : (
|
|
179
|
+
<p className="whitespace-pre-wrap font-mono text-xs">
|
|
180
|
+
{asText(f.input)}
|
|
181
|
+
</p>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
<div className="space-y-1">
|
|
185
|
+
<p className="text-xs font-semibold uppercase tracking-wide text-muted">
|
|
186
|
+
Answer
|
|
187
|
+
</p>
|
|
188
|
+
{fields && f.output !== null ? (
|
|
189
|
+
<FieldValues
|
|
190
|
+
fields={fields.label}
|
|
191
|
+
values={f.output}
|
|
192
|
+
prefix={`${f.id}-out-`}
|
|
193
|
+
/>
|
|
194
|
+
) : (
|
|
195
|
+
<p className="whitespace-pre-wrap font-mono text-xs">
|
|
196
|
+
{asText(f.output) || f.excerpt || "—"}
|
|
197
|
+
</p>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
202
|
+
{why === null ? (
|
|
203
|
+
<form action={makeCase} className="flex items-center gap-1">
|
|
204
|
+
<input type="hidden" name="feedback" value={f.id} />
|
|
205
|
+
<Submit>Make a case</Submit>
|
|
206
|
+
<InfoTip term="makeCase" id={`make-${f.id}`} />
|
|
207
|
+
</form>
|
|
208
|
+
) : (
|
|
209
|
+
<p className="readable text-xs text-muted">{why}</p>
|
|
210
|
+
)}
|
|
211
|
+
<form action={dismissFeedback}>
|
|
212
|
+
<input type="hidden" name="feedback" value={f.id} />
|
|
213
|
+
<Submit variant="quiet">Dismiss</Submit>
|
|
214
|
+
</form>
|
|
215
|
+
</div>
|
|
216
|
+
</Card>
|
|
217
|
+
</article>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import {
|
|
3
|
+
attention,
|
|
4
|
+
perAnswer,
|
|
5
|
+
seconds,
|
|
6
|
+
suiteHealth,
|
|
7
|
+
thumbsSummary,
|
|
8
|
+
type GlossaryTerm,
|
|
9
|
+
type HistoryEntry,
|
|
10
|
+
type Settings,
|
|
11
|
+
type SettingsVersion,
|
|
12
|
+
} from "@crowandraven/evalkeeper";
|
|
13
|
+
import { Card, StatusBadge, pct } from "@/components/evalkeeper/ui";
|
|
14
|
+
import {
|
|
15
|
+
feedbackStore,
|
|
16
|
+
MODELS,
|
|
17
|
+
requireAdmin,
|
|
18
|
+
runner,
|
|
19
|
+
settings,
|
|
20
|
+
SUITES,
|
|
21
|
+
} from "@/lib/evalkeeper";
|
|
22
|
+
import { daysAgo } from "@/lib/evalkeeper/time";
|
|
23
|
+
import { loadWeekly } from "@/lib/evalkeeper/weekly";
|
|
24
|
+
import { WeeklyCheck } from "@/components/evalkeeper/WeeklyCheck";
|
|
25
|
+
import { InfoTip } from "@/components/evalkeeper/Tip";
|
|
26
|
+
import { evalsPath } from "@/lib/evalkeeper/paths";
|
|
27
|
+
import { HistoryEntries } from "@/components/evalkeeper/SettingsHistory";
|
|
28
|
+
import { ChevronDown } from "lucide-react";
|
|
29
|
+
|
|
30
|
+
export const dynamic = "force-dynamic";
|
|
31
|
+
|
|
32
|
+
/** The evals hub: where each suite stands, and what needs attention. */
|
|
33
|
+
export default async function EvalsHub() {
|
|
34
|
+
await requireAdmin();
|
|
35
|
+
const [boards, weekly] = await Promise.all([
|
|
36
|
+
Promise.all(SUITES.map((s) => runner.board(s.key))),
|
|
37
|
+
loadWeekly(),
|
|
38
|
+
]);
|
|
39
|
+
const histories = await Promise.all(
|
|
40
|
+
SUITES.map((s) =>
|
|
41
|
+
Promise.all([
|
|
42
|
+
settings.history(s.operation),
|
|
43
|
+
settings.current(s.operation),
|
|
44
|
+
]),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
const thumbs = thumbsSummary(await feedbackStore.listFeedback(), daysAgo(7));
|
|
48
|
+
const down = thumbs.reduce((t, s) => t + s.down, 0);
|
|
49
|
+
const waiting = thumbs.reduce((t, s) => t + s.open, 0);
|
|
50
|
+
const flagged = thumbs.filter((s) => s.alert).length;
|
|
51
|
+
const needs = attention(
|
|
52
|
+
boards.map((b) => ({
|
|
53
|
+
key: b.suite.key,
|
|
54
|
+
label: b.suite.label,
|
|
55
|
+
rows: b.rows,
|
|
56
|
+
toReview: b.provisional,
|
|
57
|
+
})),
|
|
58
|
+
);
|
|
59
|
+
return (
|
|
60
|
+
<div className="space-y-8">
|
|
61
|
+
<div className="readable space-y-2">
|
|
62
|
+
<h1 className="text-2xl font-semibold">Evals</h1>
|
|
63
|
+
<p className="text-muted">
|
|
64
|
+
Each suite tests one AI feature against answers people confirmed. Test
|
|
65
|
+
models on it, then pick the live one on evidence.
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
70
|
+
<Stat
|
|
71
|
+
title="Most room to improve"
|
|
72
|
+
term="improve"
|
|
73
|
+
value={needs.improve ? pct(needs.improve.best) : "—"}
|
|
74
|
+
detail={
|
|
75
|
+
!needs.improve
|
|
76
|
+
? "Test a suite to see"
|
|
77
|
+
: needs.improve.best >= 1
|
|
78
|
+
? "Every suite has a model scoring 100%"
|
|
79
|
+
: `${needs.improve.label}, best on ${needs.improve.model}`
|
|
80
|
+
}
|
|
81
|
+
/>
|
|
82
|
+
<Stat
|
|
83
|
+
title="Slowest live model"
|
|
84
|
+
term="slowest"
|
|
85
|
+
value={needs.slowest ? seconds(needs.slowest.ms) : "—"}
|
|
86
|
+
detail={
|
|
87
|
+
needs.slowest
|
|
88
|
+
? `${needs.slowest.label} on ${needs.slowest.model}`
|
|
89
|
+
: "—"
|
|
90
|
+
}
|
|
91
|
+
/>
|
|
92
|
+
<Stat
|
|
93
|
+
title="Costliest live model"
|
|
94
|
+
term="costliest"
|
|
95
|
+
value={needs.costliest ? perAnswer(needs.costliest.usd) : "—"}
|
|
96
|
+
detail={
|
|
97
|
+
needs.costliest
|
|
98
|
+
? `${needs.costliest.label} on ${needs.costliest.model}`
|
|
99
|
+
: "—"
|
|
100
|
+
}
|
|
101
|
+
/>
|
|
102
|
+
<Link href={evalsPath("feedback")}>
|
|
103
|
+
<Stat
|
|
104
|
+
title="Thumbs down this week"
|
|
105
|
+
term="thumbs"
|
|
106
|
+
value={String(down)}
|
|
107
|
+
detail={
|
|
108
|
+
flagged
|
|
109
|
+
? `${flagged} feature${flagged === 1 ? "" : "s"} need a look · ${waiting} waiting`
|
|
110
|
+
: `${waiting} waiting for a look`
|
|
111
|
+
}
|
|
112
|
+
/>
|
|
113
|
+
</Link>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<WeeklyCheck weekly={weekly} />
|
|
117
|
+
|
|
118
|
+
<section aria-labelledby="suites" className="space-y-3">
|
|
119
|
+
<h2 id="suites" className="text-lg font-semibold">
|
|
120
|
+
Suites
|
|
121
|
+
<InfoTip term="suites" />
|
|
122
|
+
</h2>
|
|
123
|
+
<div className="space-y-4">
|
|
124
|
+
{boards.map((b, i) => {
|
|
125
|
+
const h = suiteHealth(
|
|
126
|
+
b.rows,
|
|
127
|
+
b.suite.checks,
|
|
128
|
+
b.suite.priority,
|
|
129
|
+
b.cases,
|
|
130
|
+
b.provisional,
|
|
131
|
+
MODELS,
|
|
132
|
+
);
|
|
133
|
+
const [history, current] = histories[i] ?? [[], null];
|
|
134
|
+
return (
|
|
135
|
+
<SuiteCard
|
|
136
|
+
key={b.suite.key}
|
|
137
|
+
board={b}
|
|
138
|
+
health={h}
|
|
139
|
+
history={history}
|
|
140
|
+
current={current}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
})}
|
|
144
|
+
</div>
|
|
145
|
+
</section>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function Stat({
|
|
151
|
+
title,
|
|
152
|
+
term,
|
|
153
|
+
value,
|
|
154
|
+
detail,
|
|
155
|
+
}: {
|
|
156
|
+
title: string;
|
|
157
|
+
term: GlossaryTerm;
|
|
158
|
+
value: string;
|
|
159
|
+
detail: string;
|
|
160
|
+
}) {
|
|
161
|
+
return (
|
|
162
|
+
<Card className="space-y-1">
|
|
163
|
+
<p className="text-sm text-muted">
|
|
164
|
+
{title}
|
|
165
|
+
<InfoTip term={term} />
|
|
166
|
+
</p>
|
|
167
|
+
<p className="text-2xl font-semibold tabular-nums">{value}</p>
|
|
168
|
+
<p className="text-sm text-muted">{detail}</p>
|
|
169
|
+
</Card>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
type Board = Awaited<ReturnType<typeof runner.board>>;
|
|
174
|
+
|
|
175
|
+
const label = (model: string) => MODELS[model]?.label ?? model;
|
|
176
|
+
const day = (d: Date) => d.toLocaleDateString();
|
|
177
|
+
/** Green at 95% and up, amber from 80%, red below. */
|
|
178
|
+
const scoreTone = (rate: number) =>
|
|
179
|
+
rate >= 0.95 ? "text-good" : rate >= 0.8 ? "text-warn" : "text-bad";
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* One feature on the hub: its live version and models, what the live
|
|
183
|
+
* model costs and how fast it is, its latest test, what needs a look, and
|
|
184
|
+
* its settings history.
|
|
185
|
+
*/
|
|
186
|
+
function SuiteCard({
|
|
187
|
+
board: b,
|
|
188
|
+
health: h,
|
|
189
|
+
history,
|
|
190
|
+
current,
|
|
191
|
+
}: {
|
|
192
|
+
board: Board;
|
|
193
|
+
health: ReturnType<typeof suiteHealth>;
|
|
194
|
+
history: HistoryEntry[];
|
|
195
|
+
current: { settings: Settings; version: SettingsVersion | null } | null;
|
|
196
|
+
}) {
|
|
197
|
+
const live = b.rows.find((r) => r.live && !r.variant);
|
|
198
|
+
const t = live?.test;
|
|
199
|
+
const version = current?.version ?? null;
|
|
200
|
+
const since = version
|
|
201
|
+
? (history.find((e) => e.to.id === version.id)?.createdAt ??
|
|
202
|
+
version.createdAt)
|
|
203
|
+
: null;
|
|
204
|
+
const s = current?.settings;
|
|
205
|
+
const price = s ? MODELS[s.model] : undefined;
|
|
206
|
+
const versions = Math.max(1, ...history.map((e) => e.to.number));
|
|
207
|
+
return (
|
|
208
|
+
<Card className="relative space-y-4 transition-colors hover:border-muted">
|
|
209
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
210
|
+
<div className="min-w-0 space-y-2">
|
|
211
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
212
|
+
<h2 className="font-semibold">
|
|
213
|
+
{/* Covers the whole card; the versions toggle sits above it. */}
|
|
214
|
+
<Link
|
|
215
|
+
href={evalsPath(b.suite.key)}
|
|
216
|
+
className="after:absolute after:inset-0 after:rounded-lg"
|
|
217
|
+
>
|
|
218
|
+
{b.suite.label}
|
|
219
|
+
</Link>
|
|
220
|
+
</h2>
|
|
221
|
+
<StatusBadge status={h.status} />
|
|
222
|
+
</div>
|
|
223
|
+
<p className="readable text-sm text-muted">{b.suite.description}</p>
|
|
224
|
+
</div>
|
|
225
|
+
{t && (
|
|
226
|
+
<div className="shrink-0 space-y-0.5 text-right text-sm">
|
|
227
|
+
<p className="font-semibold tabular-nums">
|
|
228
|
+
{perAnswer(t.costPerAnswer)} / answer
|
|
229
|
+
</p>
|
|
230
|
+
<p className="text-xs text-muted">{seconds(t.avgMs)} avg</p>
|
|
231
|
+
{price?.inputPer1M !== undefined &&
|
|
232
|
+
price.outputPer1M !== undefined && (
|
|
233
|
+
<p className="text-xs text-muted">
|
|
234
|
+
${price.inputPer1M} / ${price.outputPer1M} per 1M
|
|
235
|
+
</p>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
{s && (
|
|
242
|
+
<div className="space-y-0.5 text-sm">
|
|
243
|
+
<p>
|
|
244
|
+
<span className="font-medium">v{version?.number ?? 1}</span>{" "}
|
|
245
|
+
<span className="text-muted">
|
|
246
|
+
· {since ? `live since ${day(since)}` : "defaults from code"}
|
|
247
|
+
</span>
|
|
248
|
+
</p>
|
|
249
|
+
<p className="text-xs text-muted">
|
|
250
|
+
{label(s.model)}
|
|
251
|
+
{s.fallbackModel
|
|
252
|
+
? ` · fallback ${label(s.fallbackModel)}`
|
|
253
|
+
: " · no fallback"}
|
|
254
|
+
{s.reasoningEffort ? ` · reasoning ${s.reasoningEffort}` : ""}
|
|
255
|
+
</p>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
<p className="text-sm">
|
|
260
|
+
{t ? (
|
|
261
|
+
<>
|
|
262
|
+
Score{" "}
|
|
263
|
+
<span className={`font-semibold ${scoreTone(t.right / t.total)}`}>
|
|
264
|
+
{pct(t.right / t.total)}
|
|
265
|
+
</span>
|
|
266
|
+
</>
|
|
267
|
+
) : (
|
|
268
|
+
"Live model not tested yet"
|
|
269
|
+
)}{" "}
|
|
270
|
+
· {b.cases} cases · {h.testedModels} models tested ·{" "}
|
|
271
|
+
<Link
|
|
272
|
+
href={evalsPath(b.suite.key)}
|
|
273
|
+
className="relative text-accent hover:underline"
|
|
274
|
+
>
|
|
275
|
+
Compare models
|
|
276
|
+
</Link>
|
|
277
|
+
</p>
|
|
278
|
+
|
|
279
|
+
{h.details.length > 0 && (
|
|
280
|
+
<ul className="readable list-disc space-y-1 pl-5 text-sm text-muted">
|
|
281
|
+
{h.details.slice(0, 3).map((d) => (
|
|
282
|
+
<li key={d}>{d}</li>
|
|
283
|
+
))}
|
|
284
|
+
</ul>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{s && (
|
|
288
|
+
<details className="group/history relative rounded-md bg-tint">
|
|
289
|
+
<summary className="flex list-none items-center justify-between gap-2 p-3 text-sm font-medium">
|
|
290
|
+
Versions
|
|
291
|
+
<span className="flex items-center gap-1 text-xs font-normal text-muted">
|
|
292
|
+
{versions} version{versions === 1 ? "" : "s"}
|
|
293
|
+
<ChevronDown
|
|
294
|
+
aria-hidden
|
|
295
|
+
className="h-4 w-4 transition-transform group-open/history:rotate-180"
|
|
296
|
+
/>
|
|
297
|
+
</span>
|
|
298
|
+
</summary>
|
|
299
|
+
<div className="px-3 pb-3">
|
|
300
|
+
<HistoryEntries
|
|
301
|
+
suite={b.suite.key}
|
|
302
|
+
entries={history}
|
|
303
|
+
liveId={version?.id ?? null}
|
|
304
|
+
label={label}
|
|
305
|
+
defaults={s}
|
|
306
|
+
plain
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
</details>
|
|
310
|
+
)}
|
|
311
|
+
</Card>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Plus } from "lucide-react";
|
|
5
|
+
import { Card } from "./ui";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A section's header row with an "Add a case" button at the top right,
|
|
9
|
+
* beside `tools`; the button opens `form` below the header.
|
|
10
|
+
*/
|
|
11
|
+
export function AddCaseHeader({
|
|
12
|
+
heading,
|
|
13
|
+
tools,
|
|
14
|
+
form,
|
|
15
|
+
}: {
|
|
16
|
+
heading: React.ReactNode;
|
|
17
|
+
tools?: React.ReactNode;
|
|
18
|
+
/** The add-a-case form; null hides the button. */
|
|
19
|
+
form: React.ReactNode;
|
|
20
|
+
}) {
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
25
|
+
{heading}
|
|
26
|
+
<div className="flex items-center gap-2">
|
|
27
|
+
{form && (
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
aria-expanded={open}
|
|
31
|
+
onClick={() => {
|
|
32
|
+
setOpen(!open);
|
|
33
|
+
}}
|
|
34
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-line bg-card px-3 py-1.5 text-sm hover:bg-tint"
|
|
35
|
+
>
|
|
36
|
+
<Plus aria-hidden className="h-4 w-4" />
|
|
37
|
+
Add a case
|
|
38
|
+
</button>
|
|
39
|
+
)}
|
|
40
|
+
{tools}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
{open && form && (
|
|
44
|
+
<Card className="space-y-4">
|
|
45
|
+
<div className="flex items-center justify-between gap-2">
|
|
46
|
+
<h3 className="font-medium">New case</h3>
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
onClick={() => {
|
|
50
|
+
setOpen(false);
|
|
51
|
+
}}
|
|
52
|
+
className="text-sm text-muted hover:text-ink"
|
|
53
|
+
>
|
|
54
|
+
Cancel
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
{form}
|
|
58
|
+
</Card>
|
|
59
|
+
)}
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
}
|