@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.
- 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 +48 -0
- package/dist/attention.js.map +1 -0
- package/dist/board.d.ts +55 -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 +50 -0
- package/dist/models.js +39 -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 +193 -0
- package/dist/runner.js +706 -0
- package/dist/runner.js.map +1 -0
- package/dist/settings.d.ts +157 -0
- package/dist/settings.js +183 -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 +146 -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 +150 -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 +341 -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 +219 -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 +111 -0
- package/dist/weekly.js.map +1 -0
- package/package.json +81 -4
- package/src/attention.ts +88 -0
- package/src/board.ts +56 -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 +147 -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 +82 -0
- package/src/prune.ts +95 -0
- package/src/quickSet.ts +55 -0
- package/src/runner.ts +971 -0
- package/src/settings.ts +331 -0
- package/src/stableJson.ts +24 -0
- package/src/stats.ts +215 -0
- package/src/store.ts +167 -0
- package/src/suggest.ts +245 -0
- package/src/summary.ts +109 -0
- package/src/testing.ts +392 -0
- package/src/timeout.ts +14 -0
- package/src/types.ts +252 -0
- package/src/upkeepRules.ts +46 -0
- package/src/util.ts +17 -0
- package/src/weekly.ts +167 -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,191 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import {
|
|
3
|
+
describeChange,
|
|
4
|
+
type ChangeKind,
|
|
5
|
+
type HistoryEntry,
|
|
6
|
+
type Settings,
|
|
7
|
+
} from "@crowandraven/evalkeeper";
|
|
8
|
+
import { rollBackSettings } from "@/lib/evalkeeper/actions";
|
|
9
|
+
import { Submit } from "./Submit";
|
|
10
|
+
import { InfoTip, Tip } from "./Tip";
|
|
11
|
+
import { Chip } from "./ui";
|
|
12
|
+
import { evalsPath } from "@/lib/evalkeeper/paths";
|
|
13
|
+
|
|
14
|
+
const KIND: Record<
|
|
15
|
+
ChangeKind,
|
|
16
|
+
{ text: string; tone: "live" | "fallback" | "warn" | "neutral"; help: string }
|
|
17
|
+
> = {
|
|
18
|
+
CHANGE: {
|
|
19
|
+
text: "Changed",
|
|
20
|
+
tone: "neutral",
|
|
21
|
+
help: "A person changed a setting.",
|
|
22
|
+
},
|
|
23
|
+
PROMOTE: {
|
|
24
|
+
text: "Promoted",
|
|
25
|
+
tone: "live",
|
|
26
|
+
help: "Made live or the fallback from a test, with the test as evidence.",
|
|
27
|
+
},
|
|
28
|
+
ROLLBACK: {
|
|
29
|
+
text: "Rolled back",
|
|
30
|
+
tone: "warn",
|
|
31
|
+
help: "An earlier version made live again.",
|
|
32
|
+
},
|
|
33
|
+
DEPLOY: {
|
|
34
|
+
text: "Deployed",
|
|
35
|
+
tone: "fallback",
|
|
36
|
+
help: "The instructions changed in code under the live settings.",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type Evidence = { runId?: string | null; live?: string | null };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A feature's settings history: every switch, newest first, with what
|
|
44
|
+
* changed, who, why, and the evidence; any earlier version can be made
|
|
45
|
+
* live again, with a reason.
|
|
46
|
+
*/
|
|
47
|
+
export function SettingsHistory({
|
|
48
|
+
suite,
|
|
49
|
+
entries,
|
|
50
|
+
liveId,
|
|
51
|
+
label,
|
|
52
|
+
defaults,
|
|
53
|
+
}: {
|
|
54
|
+
suite: string;
|
|
55
|
+
entries: HistoryEntry[];
|
|
56
|
+
liveId: string | null;
|
|
57
|
+
label: (model: string) => string;
|
|
58
|
+
/** The settings in code, shown as v1 until the first change. */
|
|
59
|
+
defaults: Settings;
|
|
60
|
+
}) {
|
|
61
|
+
return (
|
|
62
|
+
<section className="space-y-3" aria-labelledby="history">
|
|
63
|
+
<h2 id="history" className="text-lg font-semibold">
|
|
64
|
+
Versions
|
|
65
|
+
<InfoTip term="versions" />
|
|
66
|
+
</h2>
|
|
67
|
+
<HistoryEntries
|
|
68
|
+
suite={suite}
|
|
69
|
+
entries={entries}
|
|
70
|
+
liveId={liveId}
|
|
71
|
+
label={label}
|
|
72
|
+
defaults={defaults}
|
|
73
|
+
/>
|
|
74
|
+
</section>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The versions in a settings history, newest first, each with a rollback
|
|
80
|
+
* unless it's live. For a page's history section or a card's history toggle.
|
|
81
|
+
*/
|
|
82
|
+
export function HistoryEntries({
|
|
83
|
+
suite,
|
|
84
|
+
entries,
|
|
85
|
+
liveId,
|
|
86
|
+
label,
|
|
87
|
+
defaults,
|
|
88
|
+
plain = false,
|
|
89
|
+
}: {
|
|
90
|
+
suite: string;
|
|
91
|
+
entries: HistoryEntry[];
|
|
92
|
+
liveId: string | null;
|
|
93
|
+
label: (model: string) => string;
|
|
94
|
+
/** The settings in code, shown as v1 until the first change. */
|
|
95
|
+
defaults: Settings;
|
|
96
|
+
/** No tint behind each version, for a list already on a tint. */
|
|
97
|
+
plain?: boolean;
|
|
98
|
+
}) {
|
|
99
|
+
const box = `space-y-2 text-sm ${plain ? "py-2" : "rounded-md border border-line bg-card p-3"}`;
|
|
100
|
+
if (entries.length === 0)
|
|
101
|
+
return (
|
|
102
|
+
<article aria-label="Version 1: Defaults" className={box}>
|
|
103
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
104
|
+
<span className="font-medium">v1</span>
|
|
105
|
+
<Chip tone="neutral">Defaults</Chip>
|
|
106
|
+
<Chip tone="live">Live now</Chip>
|
|
107
|
+
</span>
|
|
108
|
+
<p>
|
|
109
|
+
{label(defaults.model)} live
|
|
110
|
+
{defaults.fallbackModel
|
|
111
|
+
? ` · fallback ${label(defaults.fallbackModel)}`
|
|
112
|
+
: " · no fallback"}
|
|
113
|
+
</p>
|
|
114
|
+
<p className="readable text-muted">
|
|
115
|
+
The settings in code. The first change records them as v1, so
|
|
116
|
+
there's always something to roll back to.
|
|
117
|
+
</p>
|
|
118
|
+
</article>
|
|
119
|
+
);
|
|
120
|
+
return (
|
|
121
|
+
<div className="space-y-2">
|
|
122
|
+
{entries.map((e) => {
|
|
123
|
+
const kind = KIND[e.kind];
|
|
124
|
+
const ev = (e.evidence ?? {}) as Evidence;
|
|
125
|
+
const testId = ev.runId ?? ev.live ?? null;
|
|
126
|
+
const isLive = e.to.id === liveId;
|
|
127
|
+
return (
|
|
128
|
+
<article
|
|
129
|
+
key={e.id}
|
|
130
|
+
aria-label={`Version ${e.to.number}: ${kind.text}`}
|
|
131
|
+
className={box}
|
|
132
|
+
>
|
|
133
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
134
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
135
|
+
<span className="font-medium">v{e.to.number}</span>
|
|
136
|
+
<Tip text={kind.help} id={`kind-${e.id}`}>
|
|
137
|
+
<Chip tone={kind.tone}>{kind.text}</Chip>
|
|
138
|
+
</Tip>
|
|
139
|
+
{isLive && <Chip tone="live">Live now</Chip>}
|
|
140
|
+
</span>
|
|
141
|
+
<span className="text-xs text-muted">
|
|
142
|
+
{e.by ?? "System"} · {e.createdAt.toLocaleString()}
|
|
143
|
+
</span>
|
|
144
|
+
</div>
|
|
145
|
+
<ul className="readable list-disc pl-5">
|
|
146
|
+
{(e.kind === "ROLLBACK"
|
|
147
|
+
? [`Back to v${e.to.number}: ${label(e.to.model)} live`]
|
|
148
|
+
: e.kind === "DEPLOY"
|
|
149
|
+
? ["Same models, new instructions"]
|
|
150
|
+
: describeChange(e.from, e.to, label)
|
|
151
|
+
).map((line) => (
|
|
152
|
+
<li key={line}>{line}</li>
|
|
153
|
+
))}
|
|
154
|
+
</ul>
|
|
155
|
+
{e.reason && <p className="readable text-muted">{e.reason}</p>}
|
|
156
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
157
|
+
{testId && (
|
|
158
|
+
<Link
|
|
159
|
+
href={evalsPath(suite, "runs", testId)}
|
|
160
|
+
className="text-xs underline"
|
|
161
|
+
>
|
|
162
|
+
The test behind it
|
|
163
|
+
</Link>
|
|
164
|
+
)}
|
|
165
|
+
{!isLive && (
|
|
166
|
+
<form
|
|
167
|
+
action={rollBackSettings}
|
|
168
|
+
className="flex flex-wrap items-center gap-2"
|
|
169
|
+
>
|
|
170
|
+
<input type="hidden" name="suite" value={suite} />
|
|
171
|
+
<input type="hidden" name="version" value={e.to.id} />
|
|
172
|
+
<label htmlFor={`why-${e.id}`} className="sr-only">
|
|
173
|
+
Why roll back to v{e.to.number}?
|
|
174
|
+
</label>
|
|
175
|
+
<input
|
|
176
|
+
id={`why-${e.id}`}
|
|
177
|
+
name="reason"
|
|
178
|
+
required
|
|
179
|
+
placeholder="Why roll back?"
|
|
180
|
+
className="rounded-md border border-line bg-card px-2 py-1 text-xs focus:border-accent focus:outline-none"
|
|
181
|
+
/>
|
|
182
|
+
<Submit variant="quiet">Roll back to v{e.to.number}</Submit>
|
|
183
|
+
</form>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</article>
|
|
187
|
+
);
|
|
188
|
+
})}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { LoaderCircle } from "lucide-react";
|
|
4
|
+
import { useFormStatus } from "react-dom";
|
|
5
|
+
|
|
6
|
+
/** A submit button that spins while its form's action runs. */
|
|
7
|
+
export function Submit({
|
|
8
|
+
children,
|
|
9
|
+
variant = "primary",
|
|
10
|
+
}: {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
variant?: "primary" | "quiet";
|
|
13
|
+
}) {
|
|
14
|
+
const { pending } = useFormStatus();
|
|
15
|
+
return (
|
|
16
|
+
<button
|
|
17
|
+
type="submit"
|
|
18
|
+
disabled={pending}
|
|
19
|
+
className={`inline-flex items-center gap-2 whitespace-nowrap rounded-md px-3 py-1.5 text-sm disabled:opacity-60 ${
|
|
20
|
+
variant === "primary"
|
|
21
|
+
? "bg-accent text-white"
|
|
22
|
+
: "border border-line bg-card text-ink hover:bg-tint"
|
|
23
|
+
}`}
|
|
24
|
+
>
|
|
25
|
+
{pending && <Spinner />}
|
|
26
|
+
{children}
|
|
27
|
+
</button>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An icon-only submit button. `label` is its accessible name; wrap it in a
|
|
33
|
+
* `<Tip control>` for the tooltip. The icon turns into the loader while the
|
|
34
|
+
* action runs.
|
|
35
|
+
*/
|
|
36
|
+
export function IconSubmit({
|
|
37
|
+
label,
|
|
38
|
+
icon,
|
|
39
|
+
}: {
|
|
40
|
+
label: string;
|
|
41
|
+
icon: React.ReactNode;
|
|
42
|
+
}) {
|
|
43
|
+
const { pending } = useFormStatus();
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
type="submit"
|
|
47
|
+
aria-label={label}
|
|
48
|
+
disabled={pending}
|
|
49
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-line bg-card text-ink hover:bg-tint disabled:opacity-60"
|
|
50
|
+
>
|
|
51
|
+
{pending ? <Spinner /> : icon}
|
|
52
|
+
</button>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function Spinner() {
|
|
57
|
+
return <LoaderCircle aria-hidden className="h-3.5 w-3.5 animate-spin" />;
|
|
58
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition } from "react";
|
|
4
|
+
import { ThumbsDown, ThumbsUp } from "lucide-react";
|
|
5
|
+
import { formText } from "@crowandraven/evalkeeper";
|
|
6
|
+
|
|
7
|
+
type Rating = "UP" | "DOWN";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Thumbs up or down on one AI answer, with "what was wrong?" after a
|
|
11
|
+
* thumbs down. Clicking the same thumb again takes the rating back.
|
|
12
|
+
* `action` is your server action; it gets targetId, rating, and reason (or
|
|
13
|
+
* clear=1), and looks the answer up itself.
|
|
14
|
+
*/
|
|
15
|
+
export function Thumbs({
|
|
16
|
+
targetId,
|
|
17
|
+
initial = null,
|
|
18
|
+
action,
|
|
19
|
+
}: {
|
|
20
|
+
targetId: string;
|
|
21
|
+
initial?: Rating | null;
|
|
22
|
+
action: (form: FormData) => Promise<void>;
|
|
23
|
+
}) {
|
|
24
|
+
const [rating, setRating] = useState<Rating | null>(initial);
|
|
25
|
+
const [asking, setAsking] = useState(false);
|
|
26
|
+
const [sent, setSent] = useState(false);
|
|
27
|
+
const [pending, start] = useTransition();
|
|
28
|
+
|
|
29
|
+
const send = (fields: Record<string, string>, after: () => void) => {
|
|
30
|
+
const form = new FormData();
|
|
31
|
+
form.set("targetId", targetId);
|
|
32
|
+
for (const [k, v] of Object.entries(fields)) form.set(k, v);
|
|
33
|
+
start(async () => {
|
|
34
|
+
await action(form);
|
|
35
|
+
after();
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const thumb = (value: Rating) => {
|
|
40
|
+
const on = rating === value;
|
|
41
|
+
const label = on
|
|
42
|
+
? "Take back your rating"
|
|
43
|
+
: value === "UP"
|
|
44
|
+
? "Good answer"
|
|
45
|
+
: "Not a good answer";
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
aria-label={label}
|
|
50
|
+
title={label}
|
|
51
|
+
aria-pressed={on}
|
|
52
|
+
disabled={pending}
|
|
53
|
+
onClick={() => {
|
|
54
|
+
send(on ? { rating: value, clear: "1" } : { rating: value }, () => {
|
|
55
|
+
setRating(on ? null : value);
|
|
56
|
+
setAsking(!on && value === "DOWN");
|
|
57
|
+
setSent(false);
|
|
58
|
+
});
|
|
59
|
+
}}
|
|
60
|
+
className={`inline-flex h-8 w-8 items-center justify-center rounded-md border text-sm disabled:opacity-60 ${
|
|
61
|
+
on
|
|
62
|
+
? "border-accent bg-tint text-accent"
|
|
63
|
+
: "border-line bg-card text-muted hover:text-ink"
|
|
64
|
+
}`}
|
|
65
|
+
>
|
|
66
|
+
{value === "UP" ? (
|
|
67
|
+
<ThumbsUp aria-hidden className="h-4 w-4" />
|
|
68
|
+
) : (
|
|
69
|
+
<ThumbsDown aria-hidden className="h-4 w-4" />
|
|
70
|
+
)}
|
|
71
|
+
</button>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="space-y-2">
|
|
77
|
+
<div className="flex items-center gap-1">
|
|
78
|
+
{thumb("UP")}
|
|
79
|
+
{thumb("DOWN")}
|
|
80
|
+
{sent && (
|
|
81
|
+
<span className="ml-2 text-sm text-muted">Thanks: that helps.</span>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
{asking && (
|
|
85
|
+
<form
|
|
86
|
+
className="flex flex-wrap items-end gap-2"
|
|
87
|
+
onSubmit={(e) => {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
const reason = formText(new FormData(e.currentTarget), "reason");
|
|
90
|
+
send({ rating: "DOWN", reason }, () => {
|
|
91
|
+
setAsking(false);
|
|
92
|
+
setSent(true);
|
|
93
|
+
});
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<label htmlFor={`why-${targetId}`} className="sr-only">
|
|
97
|
+
What was wrong?
|
|
98
|
+
</label>
|
|
99
|
+
<input
|
|
100
|
+
id={`why-${targetId}`}
|
|
101
|
+
name="reason"
|
|
102
|
+
maxLength={1000}
|
|
103
|
+
placeholder="What was wrong? (optional)"
|
|
104
|
+
className="min-w-0 flex-1 rounded-md border border-line bg-card px-3 py-1.5 text-sm focus:border-accent focus:outline-none"
|
|
105
|
+
/>
|
|
106
|
+
<button
|
|
107
|
+
type="submit"
|
|
108
|
+
disabled={pending}
|
|
109
|
+
className="rounded-md border border-line bg-card px-3 py-1.5 text-sm hover:bg-tint disabled:opacity-60"
|
|
110
|
+
>
|
|
111
|
+
Send
|
|
112
|
+
</button>
|
|
113
|
+
</form>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Info } from "lucide-react";
|
|
2
|
+
import { GLOSSARY, type GlossaryTerm } from "@crowandraven/evalkeeper";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tooltips that explain how things work. They open on hover and on
|
|
6
|
+
* keyboard focus, and screen readers read the text as the element's
|
|
7
|
+
* description. No JavaScript: CSS shows them. The named group (group/tip)
|
|
8
|
+
* keeps a tooltip from opening when an outer "group" (an open case) is
|
|
9
|
+
* hovered.
|
|
10
|
+
*
|
|
11
|
+
* <InfoTip term="score" /> an (i) after a label
|
|
12
|
+
* <Tip term="live"><Chip …/></Tip> explains the thing it wraps
|
|
13
|
+
* <Tip text="…" control><button …/></Tip> a button's tooltip
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
/** A glossary entry, or your own text with `text`. */
|
|
18
|
+
term?: GlossaryTerm;
|
|
19
|
+
text?: string;
|
|
20
|
+
/** Unique per page when the same term shows more than once. */
|
|
21
|
+
id?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Where it opens. "bottom" inside a scrolling container (a table), whose
|
|
24
|
+
* top edge would cut it off.
|
|
25
|
+
*/
|
|
26
|
+
side?: "top" | "bottom";
|
|
27
|
+
/** "end" near a container's right edge, so it opens leftward. */
|
|
28
|
+
align?: "center" | "end";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function body(props: Props) {
|
|
32
|
+
return props.text ?? (props.term ? GLOSSARY[props.term] : "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Bubble({
|
|
36
|
+
id,
|
|
37
|
+
text,
|
|
38
|
+
side = "top",
|
|
39
|
+
align = "center",
|
|
40
|
+
}: {
|
|
41
|
+
id: string;
|
|
42
|
+
text: string;
|
|
43
|
+
side?: "top" | "bottom" | undefined;
|
|
44
|
+
align?: "center" | "end" | undefined;
|
|
45
|
+
}) {
|
|
46
|
+
return (
|
|
47
|
+
<span
|
|
48
|
+
role="tooltip"
|
|
49
|
+
id={id}
|
|
50
|
+
className={`pointer-events-none invisible absolute ${side === "top" ? "bottom-full mb-2" : "top-full mt-2"} ${align === "end" ? "right-0" : "left-1/2 -translate-x-1/2"} z-20 w-64 rounded-md border border-line bg-card px-3 py-2 text-left text-xs font-normal normal-case leading-snug tracking-normal text-ink opacity-0 shadow-lg transition-opacity group-focus-within/tip:visible group-focus-within/tip:opacity-100 group-hover/tip:visible group-hover/tip:opacity-100`}
|
|
51
|
+
>
|
|
52
|
+
{text}
|
|
53
|
+
</span>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A small (i) button that explains the label before it. */
|
|
58
|
+
export function InfoTip(props: Props) {
|
|
59
|
+
const text = body(props);
|
|
60
|
+
const id = `tip-${props.id ?? props.term ?? "info"}`;
|
|
61
|
+
return (
|
|
62
|
+
<span className="group/tip relative ml-1 inline-flex align-middle">
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
aria-describedby={id}
|
|
66
|
+
aria-label="What's this?"
|
|
67
|
+
className="inline-flex h-4 w-4 items-center justify-center rounded-full text-muted hover:text-ink"
|
|
68
|
+
>
|
|
69
|
+
<Info aria-hidden className="h-3.5 w-3.5" />
|
|
70
|
+
</button>
|
|
71
|
+
<Bubble id={id} text={text} side={props.side} align={props.align} />
|
|
72
|
+
</span>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Explains whatever it wraps (a chip, a number) on hover and focus. */
|
|
77
|
+
export function Tip(
|
|
78
|
+
props: Props & {
|
|
79
|
+
children: React.ReactNode;
|
|
80
|
+
/**
|
|
81
|
+
* The child is a control that takes focus itself (a button), so the
|
|
82
|
+
* wrapper isn't a second tab stop.
|
|
83
|
+
*/
|
|
84
|
+
control?: boolean;
|
|
85
|
+
},
|
|
86
|
+
) {
|
|
87
|
+
const text = body(props);
|
|
88
|
+
const id = `tip-${props.id ?? props.term ?? "info"}`;
|
|
89
|
+
return (
|
|
90
|
+
<span
|
|
91
|
+
className="group/tip relative inline-flex"
|
|
92
|
+
{...(props.control ? {} : { tabIndex: 0, "aria-describedby": id })}
|
|
93
|
+
>
|
|
94
|
+
{props.children}
|
|
95
|
+
<Bubble id={id} text={text} side={props.side} align={props.align} />
|
|
96
|
+
</span>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { money, type WeeklySummary } from "@crowandraven/evalkeeper";
|
|
3
|
+
import { runWeeklyNow } from "@/lib/evalkeeper/actions";
|
|
4
|
+
import { RefreshWhile } from "./RefreshWhile";
|
|
5
|
+
import { Submit } from "./Submit";
|
|
6
|
+
import { InfoTip } from "./Tip";
|
|
7
|
+
import { Card, Chip, pct } from "./ui";
|
|
8
|
+
import { evalsPath } from "@/lib/evalkeeper/paths";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The weekly check on the evals hub: when it last ran, what it found, and
|
|
12
|
+
* a button to run it now. It only reports; a person decides what to change.
|
|
13
|
+
*/
|
|
14
|
+
export function WeeklyCheck({ weekly }: { weekly: WeeklySummary }) {
|
|
15
|
+
return (
|
|
16
|
+
<section aria-labelledby="weekly" className="space-y-3">
|
|
17
|
+
<RefreshWhile active={weekly.running > 0} />
|
|
18
|
+
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
19
|
+
<h2 id="weekly" className="text-lg font-semibold">
|
|
20
|
+
Weekly check
|
|
21
|
+
<InfoTip term="weeklyCheck" />
|
|
22
|
+
</h2>
|
|
23
|
+
<form action={runWeeklyNow}>
|
|
24
|
+
<Submit variant="quiet">Run it now</Submit>
|
|
25
|
+
</form>
|
|
26
|
+
</div>
|
|
27
|
+
<Card className="space-y-4">
|
|
28
|
+
<p className="readable text-sm text-muted">
|
|
29
|
+
{weekly.running > 0
|
|
30
|
+
? `Running now: ${weekly.running} quick set${weekly.running === 1 ? "" : "s"} left. `
|
|
31
|
+
: weekly.lastAt
|
|
32
|
+
? `Last run ${weekly.lastAt.toLocaleDateString("en-US", { month: "long", day: "numeric" })}, ${money(weekly.costUsd)}. `
|
|
33
|
+
: "Not run yet. "}
|
|
34
|
+
Every Monday, each live model answers its suite's quick set
|
|
35
|
+
again, so a quiet change by its provider shows up here. It also flags
|
|
36
|
+
models retiring soon and features people rate poorly.
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
{weekly.clear && (
|
|
40
|
+
<p className="readable text-sm text-good">
|
|
41
|
+
All clear: no drift, no retirements within 60 days, and no feature
|
|
42
|
+
with a high thumbs-down share.
|
|
43
|
+
</p>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
{!weekly.clear && weekly.lastAt && (
|
|
47
|
+
<ul className="readable space-y-2 text-sm">
|
|
48
|
+
{weekly.drift.map((d) => (
|
|
49
|
+
<li
|
|
50
|
+
key={`drift-${d.suite}`}
|
|
51
|
+
className="flex flex-wrap items-baseline gap-2"
|
|
52
|
+
>
|
|
53
|
+
<Chip tone="serious">Drift</Chip>
|
|
54
|
+
<span>
|
|
55
|
+
<Link
|
|
56
|
+
href={evalsPath(d.suite)}
|
|
57
|
+
className="font-medium underline"
|
|
58
|
+
>
|
|
59
|
+
{d.label}
|
|
60
|
+
</Link>{" "}
|
|
61
|
+
on {d.model} fell from {pct(d.previous)} to {pct(d.latest)} on
|
|
62
|
+
its quick set. Test every model there to see if it's
|
|
63
|
+
real.
|
|
64
|
+
</span>
|
|
65
|
+
</li>
|
|
66
|
+
))}
|
|
67
|
+
{weekly.failed.map((f) => (
|
|
68
|
+
<li
|
|
69
|
+
key={`failed-${f.suite}`}
|
|
70
|
+
className="flex flex-wrap items-baseline gap-2"
|
|
71
|
+
>
|
|
72
|
+
<Chip tone="serious">Didn't finish</Chip>
|
|
73
|
+
<span>
|
|
74
|
+
<Link
|
|
75
|
+
href={evalsPath(f.suite)}
|
|
76
|
+
className="font-medium underline"
|
|
77
|
+
>
|
|
78
|
+
{f.label}
|
|
79
|
+
</Link>
|
|
80
|
+
: <span className="text-muted">{f.error}</span>
|
|
81
|
+
</span>
|
|
82
|
+
</li>
|
|
83
|
+
))}
|
|
84
|
+
{weekly.retiring.map((r) => (
|
|
85
|
+
<li
|
|
86
|
+
key={`retiring-${r.suite}-${r.role}`}
|
|
87
|
+
className="flex flex-wrap items-baseline gap-2"
|
|
88
|
+
>
|
|
89
|
+
<Chip tone="warn">Retiring</Chip>
|
|
90
|
+
<span>
|
|
91
|
+
{r.model}, the {r.role} model for{" "}
|
|
92
|
+
<Link
|
|
93
|
+
href={evalsPath(r.suite)}
|
|
94
|
+
className="font-medium underline"
|
|
95
|
+
>
|
|
96
|
+
{r.label}
|
|
97
|
+
</Link>
|
|
98
|
+
, retires {r.when}
|
|
99
|
+
{r.days < 0 ? " (already)" : ` (in ${r.days} days)`}. Pick
|
|
100
|
+
another on its suite page.
|
|
101
|
+
</span>
|
|
102
|
+
</li>
|
|
103
|
+
))}
|
|
104
|
+
{weekly.thumbs.map((t) => (
|
|
105
|
+
<li
|
|
106
|
+
key={`thumbs-${t.operation}`}
|
|
107
|
+
className="flex flex-wrap items-baseline gap-2"
|
|
108
|
+
>
|
|
109
|
+
<Chip tone="warn">Thumbs down</Chip>
|
|
110
|
+
<span>
|
|
111
|
+
{t.operation}: {t.down} of {t.up + t.down} ratings this week.{" "}
|
|
112
|
+
<Link href={evalsPath("feedback")} className="underline">
|
|
113
|
+
Read the reasons
|
|
114
|
+
</Link>
|
|
115
|
+
.
|
|
116
|
+
</span>
|
|
117
|
+
</li>
|
|
118
|
+
))}
|
|
119
|
+
</ul>
|
|
120
|
+
)}
|
|
121
|
+
</Card>
|
|
122
|
+
</section>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Evalkeeper's colors and the "readable" line length, for the eval pages.
|
|
3
|
+
* Import it into your global CSS right after Tailwind:
|
|
4
|
+
* @import "tailwindcss";
|
|
5
|
+
* @import "../components/evalkeeper/evalkeeper.css";
|
|
6
|
+
* Change the colors to your app's; the pages use only these names (bg-card,
|
|
7
|
+
* text-muted, border-line, bg-tint, text-accent, text-good, text-warn,
|
|
8
|
+
* text-bad). Dark mode follows the system.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #f7f7f5;
|
|
13
|
+
--card: #ffffff;
|
|
14
|
+
--ink: #1c1d1f;
|
|
15
|
+
--muted: #6b6f76;
|
|
16
|
+
--line: #e4e4e0;
|
|
17
|
+
--tint: #f0f0ec;
|
|
18
|
+
--accent: #3b5bdb;
|
|
19
|
+
--good: #2f9e44;
|
|
20
|
+
--warn: #e8890c;
|
|
21
|
+
--bad: #e03131;
|
|
22
|
+
}
|
|
23
|
+
@media (prefers-color-scheme: dark) {
|
|
24
|
+
:root {
|
|
25
|
+
--bg: #141517;
|
|
26
|
+
--card: #1c1d20;
|
|
27
|
+
--ink: #ececea;
|
|
28
|
+
--muted: #9a9ea6;
|
|
29
|
+
--line: #2c2e33;
|
|
30
|
+
--tint: #232529;
|
|
31
|
+
--accent: #748ffc;
|
|
32
|
+
--good: #51cf66;
|
|
33
|
+
--warn: #fcc419;
|
|
34
|
+
--bad: #ff6b6b;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@theme inline {
|
|
39
|
+
--color-bg: var(--bg);
|
|
40
|
+
--color-card: var(--card);
|
|
41
|
+
--color-ink: var(--ink);
|
|
42
|
+
--color-muted: var(--muted);
|
|
43
|
+
--color-line: var(--line);
|
|
44
|
+
--color-tint: var(--tint);
|
|
45
|
+
--color-accent: var(--accent);
|
|
46
|
+
--color-good: var(--good);
|
|
47
|
+
--color-warn: var(--warn);
|
|
48
|
+
--color-bad: var(--bad);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Anything you can press shows it. Tailwind's reset leaves buttons with
|
|
52
|
+
the default arrow. */
|
|
53
|
+
@layer base {
|
|
54
|
+
button:not(:disabled),
|
|
55
|
+
[role="button"],
|
|
56
|
+
summary,
|
|
57
|
+
select,
|
|
58
|
+
label[for],
|
|
59
|
+
input[type="checkbox"],
|
|
60
|
+
input[type="radio"] {
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
}
|
|
63
|
+
button:disabled {
|
|
64
|
+
cursor: not-allowed;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Prose never runs the full width of a wide page. */
|
|
69
|
+
.readable {
|
|
70
|
+
max-width: 65ch;
|
|
71
|
+
}
|