static-fire 0.0.0-stage → 0.54.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +236 -2
  3. package/bin/static-fire.js +46 -0
  4. package/package.json +30 -4
  5. package/pricing.yaml +20 -0
  6. package/src/adapters/index.ts +153 -0
  7. package/src/api.ts +250 -0
  8. package/src/cli.ts +383 -0
  9. package/src/core/apkit-install.ts +47 -0
  10. package/src/core/authored.ts +47 -0
  11. package/src/core/batch-state.ts +25 -0
  12. package/src/core/batch.ts +450 -0
  13. package/src/core/batches.ts +221 -0
  14. package/src/core/checks.ts +63 -0
  15. package/src/core/commands.ts +43 -0
  16. package/src/core/compare.ts +206 -0
  17. package/src/core/config.ts +25 -0
  18. package/src/core/copying.ts +18 -0
  19. package/src/core/coverage.ts +84 -0
  20. package/src/core/db.ts +608 -0
  21. package/src/core/derived.ts +74 -0
  22. package/src/core/engine.ts +85 -0
  23. package/src/core/facets.ts +143 -0
  24. package/src/core/grade.ts +197 -0
  25. package/src/core/grading-versions.ts +281 -0
  26. package/src/core/identity.ts +263 -0
  27. package/src/core/init.ts +321 -0
  28. package/src/core/judge-schema.ts +37 -0
  29. package/src/core/judge.ts +166 -0
  30. package/src/core/label.ts +43 -0
  31. package/src/core/lock.ts +40 -0
  32. package/src/core/migrate.ts +288 -0
  33. package/src/core/new-task.ts +85 -0
  34. package/src/core/outside-tools.ts +129 -0
  35. package/src/core/plan.ts +84 -0
  36. package/src/core/profiles.ts +42 -0
  37. package/src/core/project.ts +140 -0
  38. package/src/core/regrade.ts +242 -0
  39. package/src/core/score.ts +148 -0
  40. package/src/core/settings.ts +44 -0
  41. package/src/core/task-file.ts +95 -0
  42. package/src/core/task-page.ts +54 -0
  43. package/src/core/trial-detail.ts +73 -0
  44. package/src/core/types.ts +195 -0
  45. package/src/core/view-query.ts +102 -0
  46. package/src/core/view.ts +576 -0
  47. package/src/guide.md +624 -0
  48. package/src/server.ts +28 -0
  49. package/web/api.ts +6 -0
  50. package/web/app.tsx +1162 -0
  51. package/web/brand/offset-tokens.css +126 -0
  52. package/web/charts.tsx +190 -0
  53. package/web/compare.tsx +317 -0
  54. package/web/encoding.tsx +55 -0
  55. package/web/filter-menu.ts +195 -0
  56. package/web/how-it-works.tsx +92 -0
  57. package/web/index-switches.ts +29 -0
  58. package/web/index.html +17 -0
  59. package/web/levels.tsx +15 -0
  60. package/web/notices.ts +74 -0
  61. package/web/output-view.ts +61 -0
  62. package/web/pages.ts +42 -0
  63. package/web/run-status.ts +51 -0
  64. package/web/styles.css +368 -0
  65. package/web/task-card.ts +6 -0
  66. package/web/tasks.tsx +122 -0
  67. package/web/wording.ts +38 -0
@@ -0,0 +1,51 @@
1
+ // The run status indicator in the top bar (US-005 of #21): what GET /api/batches says, in one line. It uses the
2
+ // server's batch state (src/core/batch-state.ts) as the Runs page does, so a batch that is not ended and may have
3
+ // stopped ("stalled") is never counted as running. Reads only the batch route (DEC-008).
4
+
5
+ /**
6
+ * How often the dashboard reads the batch route again with no database change. A running batch of another process
7
+ * reads as stalled once 25 minutes pass with no trial movement, which writes nothing, so no event announces it.
8
+ */
9
+ export const STATUS_REFRESH_MS = 60_000;
10
+
11
+ /** The fields of a GET /api/batches row that the indicator reads. */
12
+ export interface StatusBatch {
13
+ batch: { endedAt: string | null };
14
+ counts: Record<"queued" | "running" | "done" | "failed" | "invalid", number>;
15
+ total: number;
16
+ state: "running" | "stalled" | "ended";
17
+ }
18
+
19
+ export type RunStatus =
20
+ | { kind: "none" }
21
+ /** Nothing running. lastEndedAt is null when no batch has ended. */
22
+ | { kind: "idle"; lastEndedAt: string | null; stalled: number }
23
+ /** runs: running batches. ended / total: their trials that ended (done, failed or invalid) of those asked for. */
24
+ | { kind: "running"; runs: number; ended: number; total: number; stalled: number }
25
+ /** The batch route failed; error is its message. */
26
+ | { kind: "unavailable"; error: string };
27
+
28
+ export function runStatus(rows: StatusBatch[]): RunStatus {
29
+ if (!rows.length) return { kind: "none" };
30
+ const stalled = rows.filter((r) => r.state === "stalled").length;
31
+ const running = rows.filter((r) => r.state === "running");
32
+ if (running.length) {
33
+ let ended = 0, total = 0;
34
+ for (const r of running) { ended += r.counts.done + r.counts.failed + r.counts.invalid; total += r.total; }
35
+ return { kind: "running", runs: running.length, ended, total, stalled };
36
+ }
37
+ let lastEndedAt: string | null = null;
38
+ for (const r of rows) if (r.state === "ended" && r.batch.endedAt && (!lastEndedAt || r.batch.endedAt > lastEndedAt)) lastEndedAt = r.batch.endedAt;
39
+ return { kind: "idle", lastEndedAt, stalled };
40
+ }
41
+
42
+ const runs = (n: number) => `${n} run${n === 1 ? "" : "s"}`;
43
+
44
+ /** The indicator's words. The progress figure of a running state ("2 of 6") is drawn apart, next to its bar. */
45
+ export function statusText(s: RunStatus, at: (iso: string) => string): string {
46
+ if (s.kind === "none") return "No runs yet";
47
+ if (s.kind === "unavailable") return `Run status unavailable: ${s.error}`;
48
+ if (s.kind === "running") return `${runs(s.runs)} in progress${s.stalled ? ` · ${runs(s.stalled)} may have stopped` : ""}`;
49
+ return ["No runs in progress", s.lastEndedAt ? `last ended ${at(s.lastEndedAt)}` : "", s.stalled ? `${runs(s.stalled)} not ended, may have stopped` : ""]
50
+ .filter(Boolean).join(" · ");
51
+ }
package/web/styles.css ADDED
@@ -0,0 +1,368 @@
1
+ /* Offset (The AI Launchpad visual system). Every colour, font, radius and shadow comes from
2
+ ./brand/offset-tokens.css; nothing is defined here that is not a token or a token role.
3
+ Light scheme only: Offset's dark scheme is for packaging. */
4
+ @import url("./brand/offset-tokens.css");
5
+
6
+ * { box-sizing: border-box; }
7
+ html, body { margin: 0; background: var(--ground); color: var(--fg); font-family: var(--font-text); font-size: 15px; line-height: var(--leading-text); -webkit-font-smoothing: antialiased; }
8
+ button { font: inherit; color: inherit; cursor: pointer; }
9
+ code { font-family: var(--font-mono); font-size: 0.9em; }
10
+ .app { max-width: 1240px; margin: 0 auto; padding: var(--spacing-24) var(--spacing-16) var(--spacing-64); }
11
+ .muted { color: var(--fg-3); }
12
+ .error { color: var(--fg); background: var(--color-blush); border: var(--outline-width) solid var(--outline); border-radius: var(--radius-xs); padding: var(--spacing-8) var(--spacing-12); font-size: var(--text-sm); }
13
+ .kicker { font-family: var(--font-kicker); font-weight: var(--weight-kicker); font-size: var(--text-sm); letter-spacing: var(--tracking-kicker); text-transform: uppercase; color: var(--fg-3); display: flex; align-items: center; gap: var(--spacing-8); }
14
+
15
+ /* top bar */
16
+ .topbar { display: flex; align-items: center; flex-wrap: wrap; gap: var(--spacing-12); margin-bottom: var(--spacing-32); }
17
+ .brand { font-family: var(--font-display); font-weight: var(--weight-display); letter-spacing: var(--tracking-display); font-size: var(--text-xl); display: flex; align-items: center; gap: var(--spacing-8); }
18
+ .brand::before { content: ""; width: 22px; height: 22px; border-radius: var(--radius-xs); background: var(--accent); border: var(--outline-width) solid var(--outline); }
19
+ .spacer { flex: 1; }
20
+
21
+ /* page navigation (#24): one pill per page, the current one filled */
22
+ .nav { display: flex; flex-wrap: wrap; gap: var(--spacing-4, 4px); }
23
+ .nav a { font-weight: 800; color: var(--fg-3); text-decoration: none; border: var(--outline-width) solid transparent; border-radius: var(--radius-pill); padding: 4px var(--spacing-16); }
24
+ .nav a:hover { color: var(--fg); }
25
+ .nav a.on { color: var(--fg); background: var(--accent); border-color: var(--outline); }
26
+ .page-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: var(--spacing-16); }
27
+ .page-head .sub { color: var(--fg-3); margin: 0; max-width: 72ch; }
28
+ select, .btn, input[type="number"] { border: var(--outline-width) solid var(--outline); background: var(--raised); border-radius: var(--radius-sm); padding: var(--spacing-8) var(--spacing-12); font: inherit; color: var(--fg); max-width: 100%; }
29
+ .btn { font-weight: 700; box-shadow: var(--shadow-sm); transition: transform .08s, box-shadow .08s; }
30
+ .btn:hover:not(:disabled) { transform: translateY(var(--press-y)); box-shadow: var(--shadow-sm-pressed); }
31
+ .btn-primary { background: var(--accent); }
32
+ .btn-quiet { box-shadow: none; background: var(--surface); margin-top: var(--spacing-16); font-size: var(--text-sm); }
33
+ .btn-quiet:hover:not(:disabled) { transform: none; box-shadow: none; background: var(--color-cream); }
34
+ .btn:disabled { opacity: 0.5; cursor: default; }
35
+
36
+ /* hero */
37
+ .hero { margin-bottom: var(--spacing-24); }
38
+ .hero h1 { font-family: var(--font-display); font-weight: var(--weight-display); letter-spacing: var(--tracking-display); line-height: var(--leading-display); font-size: clamp(2rem, 5vw, var(--text-5xl)); margin: var(--spacing-8) 0 var(--spacing-12); }
39
+ .hero h1 .title-link { color: inherit; text-decoration: none; }
40
+ .hero h1 .title-link:hover, .hero h1 .title-link:focus-visible { text-decoration: underline; text-decoration-thickness: 3px; text-underline-offset: 6px; }
41
+ .meta { display: flex; flex-wrap: wrap; gap: var(--spacing-8) var(--spacing-24); color: var(--fg-3); font-size: var(--text-sm); margin-bottom: var(--spacing-16); }
42
+ .meta b { color: var(--fg); }
43
+ .progress { display: flex; align-items: center; gap: var(--spacing-12); }
44
+ .progress-track { flex: 1; height: 12px; border-radius: var(--radius-pill); background: var(--raised); border: var(--outline-width) solid var(--outline); overflow: hidden; }
45
+ .progress-fill { height: 100%; background: var(--accent); transition: width .4s ease; }
46
+ .pill { font-size: var(--text-sm); font-weight: 700; padding: 2px var(--spacing-12); border-radius: var(--radius-pill); border: var(--outline-width) solid var(--outline); background: var(--raised); white-space: nowrap; }
47
+ .pill.live { background: var(--color-mint); }
48
+ .pill.live::before { content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--outline); margin-right: 6px; animation: pulse 1.4s infinite; }
49
+ @keyframes pulse { 50% { opacity: 0.25; } }
50
+
51
+ /* view chips */
52
+ .chips { display: flex; flex-wrap: wrap; gap: var(--spacing-8); margin-bottom: var(--spacing-16); }
53
+ .chip { border: var(--outline-width) solid var(--outline); background: var(--raised); border-radius: var(--radius-pill); padding: 4px var(--spacing-16); font-weight: 700; font-size: var(--text-sm); }
54
+ .chip.on { background: var(--accent); box-shadow: var(--shadow-sm-pressed); }
55
+
56
+ /* filter bar */
57
+ .filters { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--spacing-8) var(--spacing-12); margin-bottom: var(--spacing-24); }
58
+ .filters label { display: flex; flex-direction: column; gap: 4px; font-size: var(--text-sm); font-weight: 700; color: var(--fg-3); min-width: 0; }
59
+ .filters select, .filters input[type="date"] { border: var(--outline-width) solid var(--outline); background: var(--raised); border-radius: var(--radius-sm); padding: 6px var(--spacing-8); font: inherit; font-weight: 500; color: var(--fg); max-width: 260px; }
60
+ .filters .btn-quiet { margin-top: 0; }
61
+
62
+ /* filter bar: one Filter button with a searchable menu, and a tag per active filter (#26) */
63
+ .filter-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--spacing-8); margin-bottom: var(--spacing-24); }
64
+ .filter-bar .pop { position: relative; }
65
+ .filter-btn { display: inline-flex; align-items: center; gap: 6px; padding: 4px var(--spacing-16); border-radius: var(--radius-pill); font-size: var(--text-sm); }
66
+ .filter-btn.on { background: var(--accent); }
67
+ .filter-bar .menu { position: absolute; z-index: 20; top: calc(100% + 8px); left: 0; width: min(340px, calc(100vw - 32px)); background: var(--raised); border: var(--outline-width) solid var(--outline); border-radius: var(--radius-sm); box-shadow: var(--shadow-md); padding: var(--spacing-8); }
68
+ .filter-bar .menu input[type="search"] { width: 100%; box-sizing: border-box; font: inherit; font-size: var(--text-sm); border: 1.5px solid var(--outline); border-radius: var(--radius-xs); padding: 6px var(--spacing-8); margin-bottom: 6px; background: var(--ground); color: var(--fg); }
69
+ .filter-bar .menu-list { max-height: 320px; overflow: auto; }
70
+ .filter-bar .sec, .filter-bar .opt { display: flex; width: 100%; justify-content: space-between; align-items: center; gap: var(--spacing-8); border: 0; background: none; font: inherit; color: var(--fg); text-align: left; cursor: pointer; border-radius: 6px; }
71
+ .filter-bar .sec { padding: 6px; font-weight: 700; font-size: var(--text-sm); }
72
+ .filter-bar .sec i { display: inline-block; width: 14px; font-style: normal; }
73
+ .filter-bar .sec span:last-child { color: var(--fg-3); font-weight: 400; font-size: 12px; text-align: right; }
74
+ .filter-bar .sec span.sec-on { color: var(--fg); font-weight: 700; }
75
+ .filter-bar .sec:hover, .filter-bar .opt:hover, .filter-bar .sec:focus-visible, .filter-bar .opt:focus-visible { background: var(--surface); }
76
+ .filter-bar .opt { padding: 4px 6px; font-size: var(--text-sm); }
77
+ .filter-bar .opt.on { font-weight: 700; }
78
+ .filter-bar .opt em, .filter-bar .menu-flat-dates em { font-style: normal; color: var(--fg-3); }
79
+ .filter-bar .opt small { display: block; color: var(--fg-3); font-weight: 400; font-size: 12px; }
80
+ .filter-bar .opt-count { color: var(--fg-3); font-family: var(--font-mono); font-size: 12px; }
81
+ .filter-bar .in { padding-left: 14px; }
82
+ .filter-bar .grp { font-weight: 800; font-size: var(--text-xs); letter-spacing: var(--tracking-kicker); text-transform: uppercase; color: var(--fg-3); padding: 6px 6px 2px; }
83
+ .filter-bar .menu-dates { display: flex; flex-wrap: wrap; gap: var(--spacing-8); padding: 4px 6px; }
84
+ .filter-bar .menu-dates label { display: flex; flex-direction: column; gap: 2px; font-size: 12px; font-weight: 700; color: var(--fg-3); }
85
+ .filter-bar .menu-dates input { border: 1.5px solid var(--outline); background: var(--raised); border-radius: var(--radius-xs); padding: 4px 6px; font: inherit; font-size: var(--text-sm); color: var(--fg); }
86
+ .filter-bar .menu-flat-dates { padding: 4px 6px; font-size: var(--text-sm); }
87
+ .filter-bar .menu-none { margin: 0; padding: 6px; font-size: var(--text-sm); }
88
+ .tag { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-sm); background: var(--surface); border: 1.5px solid var(--outline); border-radius: var(--radius-xs); padding: 2px 4px 2px 8px; }
89
+ .tag em { font-style: normal; color: var(--fg-3); }
90
+ .tag.grading { background: var(--color-lavender); }
91
+ .tag button { font: inherit; border: 0; background: none; cursor: pointer; padding: 0 4px; font-weight: 800; color: var(--fg); }
92
+ .clear-all { padding: 3px var(--spacing-12); border-radius: var(--radius-pill); border-style: dashed; box-shadow: none; font-size: var(--text-sm); }
93
+ .clear-all:hover:not(:disabled) { transform: none; box-shadow: none; background: var(--surface); }
94
+
95
+ /* cards */
96
+ .card { background: var(--raised); border: var(--outline-width) solid var(--outline); border-radius: var(--radius-md); box-shadow: var(--shadow-md); padding: var(--spacing-24); margin-bottom: var(--spacing-32); min-width: 0; }
97
+ .card h2 { font-family: var(--font-heading); font-weight: var(--weight-heading); letter-spacing: var(--tracking-heading); line-height: var(--leading-heading); font-size: var(--text-2xl); margin: var(--spacing-4) 0 var(--spacing-8); }
98
+ .card h3 { font-family: var(--font-heading); font-weight: var(--weight-heading-card); letter-spacing: -0.02em; font-size: var(--text-lg); margin: 0 0 2px; }
99
+ .card .sub { color: var(--fg-3); font-size: var(--text-sm); margin: 0 0 var(--spacing-16); max-width: 72ch; }
100
+ .grid3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--spacing-24); margin-bottom: var(--spacing-32); }
101
+ .grid3 .card { margin-bottom: 0; }
102
+ @media (max-width: 900px) { .grid3 { grid-template-columns: 1fr; } }
103
+ .note { color: var(--fg-3); font-size: var(--text-sm); margin: var(--spacing-12) 0 0; }
104
+ .note.coverage { margin: 0 0 var(--spacing-8); } /* the one line under the index buttons: what the index covers (#80) */
105
+ .empty { text-align: center; padding: var(--spacing-64) var(--spacing-24); }
106
+ .empty .kicker { justify-content: center; }
107
+ .empty h1 { font-family: var(--font-display); font-weight: var(--weight-display); letter-spacing: var(--tracking-display); font-size: var(--text-4xl); margin: var(--spacing-8) 0; }
108
+
109
+ /* legend */
110
+ .legend { display: flex; flex-wrap: wrap; gap: var(--spacing-8) var(--spacing-16); font-size: var(--text-sm); color: var(--fg-2); margin-bottom: var(--spacing-8); }
111
+ .legend span { display: inline-flex; align-items: center; gap: 6px; }
112
+ .legend .sep { width: 2px; height: 16px; background: var(--line); }
113
+ .swatch { width: 14px; height: 14px; border-radius: 4px; border: var(--outline-width) solid var(--outline); display: inline-block; }
114
+ .marker { flex: none; overflow: visible; }
115
+ .quad-key { width: 18px; height: 12px; background: var(--color-mint); border: var(--outline-width) solid var(--color-mint-2); border-radius: 3px; }
116
+ .pareto-key { width: 20px; border-top: 2px dotted var(--fg); }
117
+ .human-key { width: 20px; border-top: 3px solid var(--color-ink); }
118
+ .chart .human-line { stroke: var(--color-ink); stroke-width: 3; stroke-linecap: round; }
119
+ .chart .human-label { fill: var(--fg); font-size: 12.5px; font-weight: 700; paint-order: stroke; stroke: var(--raised); stroke-width: 4px; stroke-linejoin: round; }
120
+
121
+ /* charts */
122
+ .chart-wrap { position: relative; }
123
+ .chart { width: 100%; display: block; overflow: visible; }
124
+ .chart text { font-family: var(--font-text); fill: var(--fg-3); font-size: 12px; }
125
+ .chart .axis-title { fill: var(--fg-2); font-size: 13px; font-weight: 700; }
126
+ .chart .gridline { stroke: var(--dataviz-grid); }
127
+ .chart .quadrant { fill: var(--color-mint); opacity: 0.55; }
128
+ .chart .pt-label { fill: var(--fg); font-size: 12.5px; font-weight: 500; paint-order: stroke; stroke: var(--raised); stroke-width: 4px; stroke-linejoin: round; }
129
+ .chart .muted { fill: var(--fg-3); }
130
+ .tooltip { position: absolute; pointer-events: none; background: var(--raised); color: var(--fg); border: var(--outline-width) solid var(--outline); box-shadow: var(--shadow-sm); padding: var(--spacing-12); border-radius: var(--radius-sm); font-size: var(--text-sm); line-height: 1.5; min-width: 220px; transform: translate(-50%, calc(-100% - 16px)); z-index: 5; }
131
+ .tooltip .tt-head { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
132
+ .tooltip .row { display: flex; justify-content: space-between; gap: var(--spacing-16); }
133
+ .tooltip .row span:first-child { color: var(--fg-3); }
134
+
135
+ /* bars */
136
+ .bars { display: flex; flex-direction: column; gap: 10px; }
137
+ .bar-row { display: grid; grid-template-columns: minmax(0, 170px) minmax(0, 1fr); gap: var(--spacing-8); align-items: center; font-size: var(--text-sm); }
138
+ .bar-label { display: flex; align-items: center; gap: 6px; line-height: 1.25; color: var(--fg-2); }
139
+ .bar-label .marker { flex: none; }
140
+ .bar-track { position: relative; height: 20px; }
141
+ .bar { position: absolute; left: 0; top: 0; bottom: 0; border: var(--outline-width) solid var(--outline); border-radius: 0 4px 4px 0; min-width: 4px; transition: width .5s ease; }
142
+ .bar-val { position: absolute; top: 50%; transform: translateY(-50%); font-variant-numeric: tabular-nums; font-weight: 700; padding-left: 6px; white-space: nowrap; }
143
+
144
+ /* tables */
145
+ .table-wrap { overflow-x: auto; }
146
+ table { width: 100%; border-collapse: separate; border-spacing: 3px; font-size: var(--text-sm); }
147
+ th { font-weight: 700; color: var(--fg-3); font-size: 12px; text-align: center; padding: 6px 4px; vertical-align: bottom; }
148
+ th:first-child, td:first-child { text-align: left; }
149
+ td { padding: var(--spacing-8) 6px; border-radius: var(--radius-xs); text-align: center; font-variant-numeric: tabular-nums; }
150
+ td.cfg, .cfg { white-space: nowrap; display: flex; align-items: center; gap: var(--spacing-8); }
151
+ td.strong, th.strong { font-weight: 800; }
152
+ td .ci { color: var(--fg-3); font-weight: 500; }
153
+ tr.group td { text-align: left; font-family: var(--font-kicker); font-weight: var(--weight-kicker); text-transform: uppercase; letter-spacing: var(--tracking-kicker); font-size: 12px; color: var(--fg-3); padding-top: var(--spacing-16); }
154
+ table.criteria td.q { font-family: var(--font-mono); font-size: 12px; white-space: nowrap; }
155
+ .tag { display: inline-block; font-size: 11px; font-weight: 700; padding: 0 6px; border-radius: var(--radius-pill); border: var(--outline-width) solid var(--outline); margin-left: 6px; }
156
+ .tag.floor { background: var(--color-cream); }
157
+ .tag.strong { background: var(--color-sky); }
158
+ .tag.ceiling { background: var(--color-lavender); }
159
+ .tag.warn { background: var(--color-blush); }
160
+ td > .tag:first-child { margin-left: 0; }
161
+ td.q .tag { margin-left: var(--spacing-8); }
162
+
163
+ /* trials */
164
+ .trial-task + .trial-task { margin-top: var(--spacing-24); }
165
+ .trial-rows { display: flex; flex-direction: column; gap: var(--spacing-8); margin-top: var(--spacing-8); }
166
+ .trial-row { display: grid; grid-template-columns: minmax(0, 300px) minmax(0, 1fr); gap: var(--spacing-12); align-items: center; font-size: var(--text-sm); }
167
+ @media (max-width: 700px) { .trial-row { grid-template-columns: 1fr; } }
168
+ .trial-cells { display: flex; gap: var(--spacing-8); flex-wrap: wrap; }
169
+ .cell { min-width: 96px; padding: 6px 10px; border-radius: var(--radius-xs); border: var(--outline-width) solid var(--outline); background: var(--raised); font-size: 12px; text-align: left; display: flex; flex-direction: column; }
170
+ .cell .k { color: var(--fg-3); font-size: 11px; }
171
+ .cell .v { font-weight: 800; font-variant-numeric: tabular-nums; }
172
+ .cell.done { box-shadow: var(--shadow-sm); }
173
+ .cell.done:hover { transform: translateY(var(--press-y)); box-shadow: var(--shadow-sm-pressed); }
174
+ .cell.running, .cell.grading { background: var(--color-butter); }
175
+ .cell.running .v, .cell.grading .v { animation: pulse 1.4s infinite; font-weight: 600; }
176
+ .cell.invalid { background: var(--color-sun-2); }
177
+ .cell.failed { background: var(--color-blush); }
178
+ .cell.queued { opacity: 0.5; }
179
+
180
+ /* drawer */
181
+ .scrim { position: fixed; inset: 0; background: color-mix(in srgb, var(--color-ink) 35%, transparent); z-index: 10; }
182
+ .drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(1120px, 100vw); background: var(--ground); border-left: var(--outline-width) solid var(--outline); z-index: 11; display: flex; flex-direction: column; }
183
+ .drawer-head { padding: var(--spacing-16) var(--spacing-24); border-bottom: var(--outline-width) solid var(--outline); display: flex; align-items: flex-start; gap: var(--spacing-12); background: var(--raised); }
184
+ .drawer-head h2 { font-family: var(--font-heading); font-weight: var(--weight-heading); letter-spacing: var(--tracking-heading); font-size: var(--text-2xl); margin: 4px 0 0; }
185
+ /* Three tabs under the head (#68); each panel takes the drawer's whole width and scrolls on its own. */
186
+ .drawer-tabs { display: flex; gap: var(--spacing-4, 4px); padding: var(--spacing-8) var(--spacing-24) 0; border-bottom: var(--outline-width) solid var(--outline); background: var(--raised); }
187
+ .drawer-tab { border: var(--outline-width) solid transparent; border-bottom: none; background: none; border-radius: var(--radius-xs) var(--radius-xs) 0 0; padding: var(--spacing-8) var(--spacing-16); font-weight: 800; color: var(--fg-3); margin-bottom: calc(-1 * var(--outline-width)); }
188
+ .drawer-tab:hover { color: var(--fg); }
189
+ .drawer-tab.on { color: var(--fg); background: var(--ground); border-color: var(--outline); }
190
+ .drawer-panel { flex: 1; min-height: 0; overflow-y: auto; padding: var(--spacing-24); }
191
+ .drawer-panel.output { display: flex; flex-direction: column; }
192
+ .drawer-panel.output > * { flex-shrink: 0; }
193
+ /* An output in a sandboxed frame (web/output-view.ts): it fills the rest of the panel, like a browser window. */
194
+ .output-frame { display: block; width: 100%; flex: 1 0 480px !important; border: var(--outline-width) solid var(--outline); border-radius: var(--radius-xs); background: #fff; }
195
+ .output-frame.prose-frame { background: var(--ground); border-color: var(--line); }
196
+ .level-legend { display: flex; flex-wrap: wrap; align-items: center; gap: var(--spacing-8) var(--spacing-16); font-size: var(--text-sm); color: var(--fg-2); margin: 0 0 var(--spacing-16); }
197
+ .level-legend .k { font-family: var(--font-kicker); font-weight: var(--weight-kicker); text-transform: uppercase; letter-spacing: var(--tracking-kicker); font-size: 11px; color: var(--fg-3); }
198
+ .level-legend .tag { margin-left: 0; }
199
+ .drawer-panel h3 { font-family: var(--font-heading); font-weight: var(--weight-heading); letter-spacing: var(--tracking-heading); font-size: var(--text-xl); margin: 0 0 var(--spacing-12); }
200
+ .stats { display: flex; gap: var(--spacing-24); flex-wrap: wrap; margin-top: var(--spacing-8); }
201
+ .stat .k { color: var(--fg-3); font-size: 12px; }
202
+ .stat .v { font-size: var(--text-lg); font-weight: 800; font-variant-numeric: tabular-nums; }
203
+ .close { margin-left: auto; border: var(--outline-width) solid var(--outline); background: var(--raised); border-radius: var(--radius-pill); width: 36px; height: 36px; font-size: 20px; line-height: 1; }
204
+ .tabs { display: flex; gap: 6px; margin-bottom: var(--spacing-16); }
205
+ .tab { border: var(--outline-width) solid var(--outline); background: var(--raised); border-radius: var(--radius-pill); padding: 2px var(--spacing-12); font-size: var(--text-sm); font-weight: 700; }
206
+ .tab.on { background: var(--accent); }
207
+ /* An image output in the trial drawer: never wider than the drawer. The rendered Markdown is styled in its frame (web/output-view.ts). */
208
+ .output-image { display: block; max-width: 100%; height: auto; border: var(--outline-width) solid var(--outline); border-radius: var(--radius-xs); }
209
+ pre.raw { white-space: pre-wrap; font-family: var(--font-mono); font-size: 12.5px; }
210
+ .verdict-group { margin-bottom: var(--spacing-24); }
211
+ .verdict-group > .crit-head { display: flex; justify-content: space-between; font-size: var(--text-lg); margin-bottom: var(--spacing-8); }
212
+ .crit-score { font-weight: 800; font-variant-numeric: tabular-nums; }
213
+ .crit { background: var(--raised); border: var(--outline-width) solid var(--outline); border-radius: var(--radius-sm); padding: var(--spacing-12); margin-bottom: var(--spacing-8); }
214
+ .crit-top { display: flex; gap: var(--spacing-8); align-items: flex-start; }
215
+ .crit-top .q { font-size: var(--text-sm); font-weight: 600; }
216
+ .crit-meta { display: flex; gap: var(--spacing-8); align-items: center; margin: 6px 0 2px; font-size: 12px; }
217
+ .crit-meta .tag { margin-left: 0; }
218
+ .reason { margin: 4px 0 0; font-size: var(--text-sm); color: var(--fg-2); }
219
+ .badge { flex: none; font-size: 12px; font-weight: 800; padding: 0 8px; border-radius: var(--radius-pill); border: var(--outline-width) solid var(--outline); }
220
+ .badge.pass { background: var(--color-mint); }
221
+ .badge.fail { background: var(--color-blush); }
222
+ .badge.split { background: var(--color-sun-2); }
223
+ .badge.err { background: var(--color-cream); }
224
+ .conditions { color: var(--fg-3); font-size: 12px; font-weight: 400; }
225
+ .conditions-list, .gradings { margin-bottom: var(--spacing-24); }
226
+ .conditions-list dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px var(--spacing-12); margin: 0; font-size: var(--text-sm); }
227
+ .conditions-list dt { color: var(--fg-3); }
228
+ .conditions-list dd { margin: 0; overflow-wrap: anywhere; }
229
+ .gradings ul { list-style: none; margin: 0; padding: 0; }
230
+ .gradings li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px var(--spacing-12); padding: var(--spacing-8) var(--spacing-12); margin-bottom: 6px; background: var(--raised); border: var(--outline-width) solid var(--line); border-radius: var(--radius-sm); font-size: var(--text-sm); }
231
+ .gradings li.counted { border-color: var(--outline); box-shadow: inset 4px 0 0 var(--accent); }
232
+ .gradings .g-name { flex-basis: 100%; font-weight: 700; }
233
+ .gradings .g-score { font-weight: 800; font-variant-numeric: tabular-nums; }
234
+ .gradings .g-status.invalid { color: var(--fg-2); }
235
+ .gradings .g-counted { margin-left: auto; font-size: 12px; font-weight: 800; padding: 0 8px; border-radius: var(--radius-pill); border: var(--outline-width) solid var(--outline); background: var(--accent); }
236
+
237
+ /* modal */
238
+ .modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: min(560px, calc(100vw - 32px)); max-height: calc(100vh - 32px); overflow-y: auto; background: var(--raised); border: var(--outline-width) solid var(--outline); box-shadow: var(--shadow-lg); border-radius: var(--radius-md); padding: var(--spacing-24); z-index: 11; }
239
+ .modal h2 { font-family: var(--font-heading); font-weight: var(--weight-heading); letter-spacing: var(--tracking-heading); font-size: var(--text-2xl); margin: 0 0 var(--spacing-16); }
240
+ .modal fieldset { border: none; padding: 0; margin: 0 0 var(--spacing-16); }
241
+ .modal legend { font-family: var(--font-kicker); font-weight: var(--weight-kicker); text-transform: uppercase; letter-spacing: var(--tracking-kicker); font-size: 12px; color: var(--fg-3); margin-bottom: 4px; }
242
+ .modal label.opt { display: flex; align-items: center; gap: var(--spacing-8); padding: 6px 2px; border-bottom: 1px solid var(--line-soft); font-size: var(--text-sm); }
243
+ .modal label.opt .muted { margin-left: auto; font-size: 12px; }
244
+ .modal select { width: 100%; }
245
+ .modal .row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--spacing-16); margin-top: var(--spacing-12); font-size: var(--text-sm); }
246
+ .modal .row label { display: flex; align-items: center; gap: var(--spacing-8); }
247
+ .modal .row.end { justify-content: flex-end; }
248
+ .modal input[type="number"] { width: 72px; padding: 4px var(--spacing-8); }
249
+ .envelope { background: var(--surface); border: var(--outline-width) solid var(--outline); border-radius: var(--radius-sm); padding: var(--spacing-12) var(--spacing-16); margin-bottom: var(--spacing-16); font-size: var(--text-sm); }
250
+ .envelope .k { font-family: var(--font-kicker); font-weight: var(--weight-kicker); text-transform: uppercase; letter-spacing: var(--tracking-kicker); font-size: 11px; color: var(--fg-3); margin-top: 4px; }
251
+ .envelope .subject { font-weight: 800; font-size: var(--text-lg); }
252
+
253
+ /* batches panel (#13) */
254
+ .batches { margin-bottom: var(--spacing-24); }
255
+ .batch-list { list-style: none; margin: var(--spacing-12) 0 0; padding: 0; display: grid; gap: var(--spacing-12); }
256
+ .batch { border: var(--outline-width) solid var(--outline); border-radius: var(--radius-sm); padding: var(--spacing-12); background: var(--raised); }
257
+ /* one row per batch, in the columns of .batch-cols; stacked on narrow screens */
258
+ .batch-cols, .batch-head { display: grid; grid-template-columns: minmax(10em, 1fr) minmax(0, 2fr) minmax(150px, 1.5fr) 7.5em; gap: var(--spacing-8) var(--spacing-16); align-items: center; }
259
+ .batch-cols { margin: var(--spacing-12) 0 0; padding: 0 calc(var(--spacing-12) + var(--outline-width)); font-family: var(--font-kicker); font-weight: var(--weight-kicker); font-size: 12px; letter-spacing: var(--tracking-kicker); text-transform: uppercase; color: var(--fg-3); }
260
+ .batch-head { width: 100%; text-align: left; background: none; border: 0; padding: 0; }
261
+ .batch-id { display: flex; flex-direction: column; min-width: 0; }
262
+ .batch-id code { color: var(--fg-3); font-size: 12px; }
263
+ .batch-asked { color: var(--fg-3); font-size: var(--text-sm); min-width: 0; }
264
+ .batch-progress { display: flex; flex-direction: column; min-width: 0; }
265
+ .batch-head .batch-state { justify-self: start; }
266
+ @media (max-width: 720px) { .batch-cols { display: none; } .batch-head { grid-template-columns: 1fr; } }
267
+ .batch-state { font-size: var(--text-sm); font-weight: 700; padding: 2px var(--spacing-12); border-radius: var(--radius-pill); border: var(--outline-width) solid var(--outline); background: var(--surface); white-space: nowrap; }
268
+ .batch-state.running { background: var(--color-mint); }
269
+ .batch-state.stalled { background: var(--color-sun-2); }
270
+ .batch-bar { display: flex; height: 10px; margin: 0 0 var(--spacing-4, 4px); border-radius: var(--radius-pill); border: var(--outline-width) solid var(--outline); overflow: hidden; background: var(--surface); }
271
+ .batch-bar .seg { flex-basis: 0; transition: flex-grow .4s ease; }
272
+ .batch-bar .seg.done { background: var(--color-mint); }
273
+ .batch-bar .seg.failed { background: var(--color-blush); }
274
+ .batch-bar .seg.invalid { background: var(--color-sun-2); }
275
+ .batch-bar .seg.running { background: var(--color-butter); }
276
+ .batch-bar .seg.queued { background: var(--surface); }
277
+ .batch-line { font-size: var(--text-sm); }
278
+ .batch-trials { width: 100%; border-collapse: collapse; margin-top: var(--spacing-8); font-size: var(--text-sm); }
279
+ .batch-trials th, .batch-trials td { text-align: left; padding: 2px var(--spacing-8); border-top: 1px solid var(--dataviz-grid); }
280
+ .status.done { color: var(--fg); }
281
+ .status.failed, .status.invalid { font-weight: 700; }
282
+
283
+ /* the one alert in the chart card (#82): a button, and details that float over the page so the card does not grow */
284
+ .notice { position: relative; margin-top: var(--spacing-12); }
285
+ .notice-btn { display: inline-flex; align-items: center; gap: var(--spacing-8); font: inherit; font-size: var(--text-sm); font-weight: 700; color: var(--fg); background: var(--color-cream); border: var(--outline-width) solid var(--outline); border-radius: var(--radius-sm); padding: 4px var(--spacing-12); cursor: pointer; }
286
+ .notice-btn:hover, .notice-btn.on { background: var(--surface); }
287
+ .notice-btn:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
288
+ .notice-more { font-weight: 400; color: var(--fg-3); text-decoration: underline; }
289
+ .notice-details { position: absolute; z-index: 20; top: calc(100% + 8px); left: 0; width: min(560px, calc(100vw - 32px)); max-height: min(60vh, 520px); overflow: auto; box-sizing: border-box; background: var(--raised); border: var(--outline-width) solid var(--outline); border-radius: var(--radius-sm); box-shadow: var(--shadow-md); padding: var(--spacing-12) var(--spacing-16); font-size: var(--text-sm); text-align: left; }
290
+ .notice-details > ul { margin: 0; padding-left: 1.2em; display: grid; gap: var(--spacing-8); }
291
+ .notice-details .notice-action { list-style: none; margin-left: -1.2em; }
292
+ .notice-details .notice-action .btn-quiet { margin-top: 0; }
293
+ .notice-details .notice-action .note { margin-top: 4px; }
294
+ .notice-details .error { margin-top: 4px; }
295
+ .notice-groups { margin: 4px 0; padding-left: 1.2em; }
296
+ .notice-how { display: inline-block; margin-top: var(--spacing-12); color: var(--fg); }
297
+ .empty .notice { text-align: left; max-width: 72ch; margin: var(--spacing-24) auto 0; }
298
+ .linkish { background: none; border: 0; padding: 0; font: inherit; color: var(--fg); text-decoration: underline; cursor: pointer; }
299
+
300
+ /* chart card: Chart / Table toggle and the config table */
301
+ .card-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--spacing-8) var(--spacing-16); }
302
+ .card-head .task-menu { max-width: 360px; font-weight: 700; font-size: var(--text-sm); }
303
+ .view-toggle { display: inline-flex; border: var(--outline-width) solid var(--outline); border-radius: var(--radius-pill); overflow: hidden; background: var(--raised); }
304
+ .view-toggle button { font: inherit; font-weight: 700; font-size: var(--text-sm); color: var(--fg); border: 0; background: transparent; padding: 4px var(--spacing-16); cursor: pointer; }
305
+ .view-toggle button + button { border-left: var(--outline-width) solid var(--outline); }
306
+ .view-toggle button.on { background: var(--accent); }
307
+ table.configs th.num, table.configs td.num { text-align: right; white-space: nowrap; }
308
+ table.configs th:last-child, table.configs td:last-child { text-align: left; white-space: nowrap; }
309
+ table.configs tbody tr { cursor: pointer; }
310
+ table.configs tbody tr:hover td { background: var(--ground); }
311
+ table.configs .why { color: var(--fg-3); font-size: 12px; margin-top: 2px; }
312
+
313
+ /* Compare page (#27): the two picked versions, what changed between them, and the moved criteria */
314
+ .compare-picks { margin-bottom: var(--spacing-16); }
315
+ .compare-picks select { max-width: 420px; }
316
+ .two { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-12); }
317
+ @media (max-width: 720px) { .two { grid-template-columns: 1fr; } }
318
+ .ver { border: var(--outline-width) solid var(--outline); border-radius: var(--radius-sm); padding: var(--spacing-12); background: var(--color-lavender); min-width: 0; }
319
+ .ver .ver-name { display: block; font-size: var(--text-lg); margin: 2px 0; }
320
+ .ver .muted { font-size: var(--text-sm); }
321
+ .pill.current { background: var(--accent); font-size: 11px; padding: 0 var(--spacing-8); letter-spacing: 0; text-transform: none; }
322
+ .changes { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--spacing-12); }
323
+ .change { min-width: 0; }
324
+ .diff { font-family: var(--font-mono); font-size: var(--text-sm); background: var(--ground); border: var(--outline-width) solid var(--outline); border-radius: var(--radius-xs); padding: var(--spacing-8) var(--spacing-12); margin-top: 6px; white-space: pre-wrap; overflow-wrap: anywhere; }
325
+ .diff del { background: var(--color-blush); text-decoration: line-through; }
326
+ .diff ins { background: var(--color-mint); text-decoration: none; }
327
+ .diff .field { color: var(--fg-3); font-weight: 700; }
328
+ .pill.mark { font-size: 11px; padding: 0 var(--spacing-8); }
329
+ .pill.mark.changed, .pill.mark.added, .pill.mark.removed { background: var(--color-lavender); }
330
+ .pill.mark.same { background: var(--color-cream); }
331
+ .delta { font-weight: 700; padding: 1px 6px; border-radius: var(--radius-xs); }
332
+ .delta.up { background: var(--color-mint); }
333
+ .delta.down { background: var(--color-blush); }
334
+ .moved-head { margin-top: var(--spacing-24) !important; }
335
+ .reading { background: var(--color-butter); border: var(--outline-width) solid var(--outline); border-radius: var(--radius-sm); padding: var(--spacing-8) var(--spacing-12); margin: var(--spacing-16) 0 0; font-weight: 600; }
336
+ /* run status indicator in the top bar (#25): a pill that opens the Runs page */
337
+ .run-status { display: flex; align-items: center; gap: var(--spacing-8); font-size: var(--text-sm); font-weight: 600; border: var(--outline-width) solid var(--outline); border-radius: var(--radius-pill); padding: 3px var(--spacing-12); background: var(--raised); max-width: 100%; text-align: left; }
338
+ .run-status:hover { background: var(--color-cream); }
339
+ .run-status .dot { flex: none; width: 9px; height: 9px; border-radius: 50%; border: 1.5px solid var(--outline); background: var(--color-mint-2); }
340
+ .run-status .dot.none { background: var(--surface); }
341
+ .run-status .dot.run { background: var(--color-sun); animation: run-pulse 1.2s infinite; }
342
+ .run-status .dot.stalled, .run-status .dot.unavailable { background: var(--color-blush-2); }
343
+ @keyframes run-pulse { 50% { opacity: .35; } }
344
+ @media (prefers-reduced-motion: reduce) { .run-status .dot.run { animation: none; } }
345
+ .run-status .mini { flex: none; width: 70px; height: 7px; border: 1.5px solid var(--outline); border-radius: 4px; overflow: hidden; background: var(--raised); }
346
+ .run-status .mini b { display: block; height: 100%; background: var(--color-sun); transition: width .4s ease; }
347
+ .run-status-count { white-space: nowrap; font-variant-numeric: tabular-nums; }
348
+
349
+ /* Tasks and How it works pages (#79) */
350
+ .page-sub { color: var(--fg-3); margin: 0; max-width: 72ch; }
351
+ .task-entry { padding-top: var(--spacing-24); margin-top: var(--spacing-24); border-top: 2px solid var(--line); scroll-margin-top: var(--spacing-16); }
352
+ .task-entry h3 { font-size: var(--text-xl); }
353
+ .task-entry h4 { font-size: var(--text-sm); text-transform: uppercase; letter-spacing: var(--tracking-kicker); color: var(--fg-3); margin: var(--spacing-16) 0 var(--spacing-8); }
354
+ .task-version { font-size: var(--text-sm); font-weight: 500; }
355
+ .task-desc { margin: var(--spacing-4, 4px) 0 0; max-width: 72ch; }
356
+ .task-prompt { white-space: pre-wrap; font-family: var(--font-mono); font-size: 13px; background: var(--surface); border: 1.5px solid var(--line); border-radius: var(--radius-xs); padding: var(--spacing-12); margin: 0; max-width: 90ch; }
357
+ .task-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px var(--spacing-16); margin: var(--spacing-12) 0 0; font-size: var(--text-sm); }
358
+ .task-facts dt { font-weight: 700; color: var(--fg-3); }
359
+ .task-facts dd { margin: 0; display: flex; flex-wrap: wrap; gap: 4px var(--spacing-8); overflow-wrap: anywhere; }
360
+ .task-criteria td.question { text-align: left; }
361
+ .task-criteria .group-note { text-transform: none; letter-spacing: 0; font-family: var(--font-text); font-weight: 500; }
362
+ .task-suite, .how-section { scroll-margin-top: var(--spacing-16); }
363
+ .how-it-works { max-width: 80ch; }
364
+ .how-section + .how-section { margin-top: var(--spacing-24); padding-top: var(--spacing-16); border-top: 2px solid var(--line); }
365
+ .how-section p, .how-section li { line-height: 1.55; }
366
+ .how-section ul { padding-left: var(--spacing-24); }
367
+ .how-section li + li { margin-top: 6px; }
368
+ .how-section .tag { margin: 0 var(--spacing-8) 0 0; }
@@ -0,0 +1,6 @@
1
+ // The per-task cards of the Results page (#81, DEC-022 of #71): "Where the points come from" and "Trials" are
2
+ // one card each, with a menu of the tasks in view, so the page does not grow with the number of tasks.
3
+
4
+ /** The task a card shows: the picked one while it is in view, else the first task in view. */
5
+ export const shownTask = (taskIds: string[], picked: string | null): string | null =>
6
+ picked && taskIds.includes(picked) ? picked : taskIds[0] ?? null;
package/web/tasks.tsx ADDED
@@ -0,0 +1,122 @@
1
+ // The Tasks page (#79): each suite and its tasks, from the project's own files. For each task, what the agent gets,
2
+ // where it writes, and how the output is graded. The server sends only what core/task-page.ts allows, so the graders'
3
+ // own material (references, sources, the held-out file) never reaches this page. Each suite and task has an address
4
+ // of its own (web/pages.ts: suiteHref, taskHref), so another page can link to it.
5
+ import { useEffect, useState } from "react";
6
+ import type { TaskListing } from "../src/core/authored";
7
+ import type { TaskPage, TaskPageCriterion } from "../src/core/task-page";
8
+ import type { Suite } from "../src/core/types";
9
+ import { api } from "./api";
10
+ import { LevelTag } from "./levels";
11
+ import { suiteAnchor, taskAnchor, taskHref } from "./pages";
12
+
13
+ const GRADER_TEXT: Record<TaskPageCriterion["grader"], string> = { check: "Check", judge: "Judge", human: "Human" };
14
+
15
+ /** A task as the page has it: its details, or the reason it has none (an invalid task file). */
16
+ type Loaded = { page: TaskPage } | { error: string };
17
+
18
+ export function TasksPage() {
19
+ const [suites, setSuites] = useState<Suite[] | null>(null);
20
+ const [listing, setListing] = useState<TaskListing[]>([]);
21
+ const [tasks, setTasks] = useState<Record<string, Loaded>>({});
22
+ const [error, setError] = useState("");
23
+
24
+ useEffect(() => {
25
+ let live = true;
26
+ Promise.all([api<Suite[]>("/api/suites"), api<TaskListing[]>("/api/tasks")])
27
+ .then(async ([s, l]) => {
28
+ const loaded = await Promise.all(l.map((t) =>
29
+ api<TaskPage>(`/api/tasks/${encodeURIComponent(t.id)}`).then((page): Loaded => ({ page }), (e: Error): Loaded => ({ error: e.message }))));
30
+ if (!live) return;
31
+ setSuites(s); setListing(l);
32
+ setTasks(Object.fromEntries(l.map((t, i) => [t.id, loaded[i]!])));
33
+ })
34
+ .catch((e: Error) => { if (live) setError(e.message); });
35
+ return () => { live = false; };
36
+ }, []);
37
+ // A link to one suite or task opens the page there, once the entries exist.
38
+ useEffect(() => {
39
+ if (!suites || !location.hash) return;
40
+ document.getElementById(decodeURIComponent(location.hash.slice(1)))?.scrollIntoView();
41
+ }, [suites]);
42
+
43
+ if (error) return <div className="empty card"><div className="kicker">Could not load the tasks</div><p className="error" style={{ whiteSpace: "pre-wrap" }}>{error}</p></div>;
44
+ if (!suites) return <div className="empty card"><div className="kicker">Tasks</div><h1>Loading…</h1></div>;
45
+
46
+ // Each task is shown once, under the first suite that lists it; a later suite links to it.
47
+ const shown = new Set<string>();
48
+ const title = (id: string) => listing.find((t) => t.id === id)?.title ?? id;
49
+ const groups = suites.map((s) => ({ suite: s, tasks: s.tasks.map((id) => { const first = !shown.has(id); shown.add(id); return { id, first }; }) }));
50
+ const loose = listing.filter((t) => !shown.has(t.id)).map((t) => t.id);
51
+
52
+ return (
53
+ <>
54
+ <section className="hero">
55
+ <div className="kicker">Tasks</div>
56
+ <h1>Suites and tasks</h1>
57
+ <p className="page-sub">What each task asks of the agent and how its output is graded, from the project's task files. Graders' own material (references, sources, the held-out file) is not shown.</p>
58
+ </section>
59
+ {groups.map(({ suite, tasks: ids }) => (
60
+ <section key={suite.id} id={suiteAnchor(suite.id)} className="card task-suite">
61
+ <div className="kicker">Suite</div>
62
+ <h2>{suite.title}</h2>
63
+ {suite.description && <p className="sub">{suite.description}</p>}
64
+ {ids.length === 0 && <p className="note">This suite lists no tasks.</p>}
65
+ {ids.map(({ id, first }) => first ? <TaskEntry key={id} id={id} title={title(id)} loaded={tasks[id]} />
66
+ : <p key={id} className="note task-ref">{title(id)}: <a href={taskHref(id)}>shown above</a>.</p>)}
67
+ </section>
68
+ ))}
69
+ {loose.length > 0 && (
70
+ <section className="card task-suite">
71
+ <div className="kicker">Not in a suite</div>
72
+ <h2>Other tasks</h2>
73
+ <p className="sub">These tasks are in no suite, so the Results page has no suite index for them.</p>
74
+ {loose.map((id) => <TaskEntry key={id} id={id} title={title(id)} loaded={tasks[id]} />)}
75
+ </section>
76
+ )}
77
+ </>
78
+ );
79
+ }
80
+
81
+ function TaskEntry({ id, title, loaded }: { id: string; title: string; loaded?: Loaded }) {
82
+ if (!loaded || "error" in loaded) {
83
+ return (
84
+ <article id={taskAnchor(id)} className="task-entry">
85
+ <h3>{title}</h3>
86
+ <p className="error" style={{ whiteSpace: "pre-wrap" }}>This task file has errors, so it cannot run:{"\n"}{loaded?.error ?? "not found"}</p>
87
+ </article>
88
+ );
89
+ }
90
+ const t = loaded.page;
91
+ const groups = [{ id: "gates", label: "Gates", note: "If a gate fails, the task score is 0.", criteria: t.gates }, ...t.measures.map((m) => ({ ...m, note: m.weight !== 1 ? `Weight ${m.weight}` : "" }))]
92
+ .filter((g) => g.criteria.length);
93
+ return (
94
+ <article id={taskAnchor(id)} className="task-entry">
95
+ <h3>{t.title}{t.version != null && <span className="muted task-version"> v{t.version}</span>}</h3>
96
+ {t.description && <p className="task-desc">{t.description}</p>}
97
+ <h4>What the agent gets</h4>
98
+ <pre className="task-prompt">{t.prompt}</pre>
99
+ <dl className="task-facts">
100
+ <dt>Input files</dt><dd>{t.inputFiles.length ? t.inputFiles.map((f) => <code key={f}>{f}</code>) : <span className="muted">None</span>}</dd>
101
+ {t.projectFiles && <><dt>Project files</dt><dd>{t.projectFiles.length ? t.projectFiles.map((f) => <code key={f}>{f}</code>) : <span className="muted">None</span>}</dd></>}
102
+ <dt>Output</dt><dd><code>{t.output}</code></dd>
103
+ </dl>
104
+ <h4>How it is graded</h4>
105
+ <div className="table-wrap"><table className="criteria task-criteria">
106
+ <thead><tr><th>Question</th><th>Level</th><th>Grader</th></tr></thead>
107
+ <tbody>
108
+ {groups.map((g) => [
109
+ <tr key={g.id} className="group"><td colSpan={3}>{g.label}{g.note && <span className="group-note"> · {g.note}</span>}</td></tr>,
110
+ ...g.criteria.map((c) => (
111
+ <tr key={`${g.id}-${c.id}`}>
112
+ <td className="question">{c.question}</td>
113
+ <td><LevelTag level={c.level} /></td>
114
+ <td className="muted">{GRADER_TEXT[c.grader]}</td>
115
+ </tr>
116
+ )),
117
+ ])}
118
+ </tbody>
119
+ </table></div>
120
+ </article>
121
+ );
122
+ }
package/web/wording.ts ADDED
@@ -0,0 +1,38 @@
1
+ // The changed words between two wordings of one text (a reworded question, a changed context), for the Compare
2
+ // page to mark. Display only: what changed between two grading versions comes from the server's difference.
3
+
4
+ export interface WordPart { kind: "same" | "added" | "removed"; text: string }
5
+
6
+ /** Words, runs of spaces and single punctuation marks, so "(the subject" and "(subject" share the "(". */
7
+ const tokens = (s: string) => s.match(/\w+|\s+|[^\w\s]/g) ?? [];
8
+
9
+ /** Above this many token pairs the texts are shown as one removed and one added part. */
10
+ const MAX_CELLS = 4_000_000;
11
+
12
+ /**
13
+ * The parts of `b` against `a`, in reading order: kept, removed (only in `a`) and added (only in `b`). In a run of
14
+ * changes the removed part comes first. Joined, the kept and removed parts give `a`; the kept and added parts give `b`.
15
+ */
16
+ export function wordChanges(a: string, b: string): WordPart[] {
17
+ const x = tokens(a), y = tokens(b);
18
+ const parts: WordPart[] = [];
19
+ const push = (kind: WordPart["kind"], text: string) => {
20
+ const last = parts.at(-1);
21
+ if (last?.kind === kind) last.text += text; else if (text) parts.push({ kind, text });
22
+ };
23
+ if (x.length * y.length > MAX_CELLS) { push("removed", a); push("added", b); return parts; }
24
+ // lcs[i][j]: the longest common run of tokens of x from i and y from j.
25
+ const w = y.length + 1, lcs = new Uint32Array((x.length + 1) * w);
26
+ for (let i = x.length - 1; i >= 0; i--) {
27
+ for (let j = y.length - 1; j >= 0; j--) {
28
+ lcs[i * w + j] = x[i] === y[j] ? lcs[(i + 1) * w + j + 1]! + 1 : Math.max(lcs[(i + 1) * w + j]!, lcs[i * w + j + 1]!);
29
+ }
30
+ }
31
+ let i = 0, j = 0;
32
+ while (i < x.length || j < y.length) {
33
+ if (i < x.length && j < y.length && x[i] === y[j]) { push("same", x[i]!); i++; j++; }
34
+ else if (j >= y.length || (i < x.length && lcs[(i + 1) * w + j]! >= lcs[i * w + j + 1]!)) { push("removed", x[i]!); i++; }
35
+ else { push("added", y[j]!); j++; }
36
+ }
37
+ return parts;
38
+ }