From bdc2f8971293b781b51b9592fd3bbef1ab047fe1 Mon Sep 17 00:00:00 2001 From: spinsirr Date: Sat, 27 Jun 2026 17:04:43 -0700 Subject: [PATCH] fix(calibration): replace dark server SVG charts with light native visuals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bottom calibration charts were gbrain's dark-themed server-rendered SVGs (#0a0a0f bg) embedded as on Lore's light page — black boxes with dim, hard-to-read text, and the per-domain/abandoned ones were empty anyway. Render them natively from the profile JSON instead, on Lore's light theme: - Brier vs baseline gauge (0–0.5 track, 0.25 baseline marker, green ≤0.25 else amber) - Accuracy gauge (0–100%) - Outcomes split bar (correct/incorrect/partial), reconstructed from the scorecard aggregates via calibrationOutcomes() (pure, unit-tested) Drops the CHARTS fan-out (4 dark SVGs). typecheck + vitest (75) + next build all green; routes stay dynamic. Co-Authored-By: Zypher Agent --- src/app/globals.css | 77 +++++++++++++++++++------- src/components/admin/panels.tsx | 98 ++++++++++++++++++++++++++++----- src/lib/admin-format.ts | 26 +++++++++ tests/admin-format.test.ts | 17 ++++++ 4 files changed, 185 insertions(+), 33 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 7591f6c..c9ac06e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2447,30 +2447,69 @@ a.ext { font-size: 14px; line-height: 1.5; } -.admin-charts { +/* Light-themed calibration visuals (replaced the dark server-rendered SVGs). */ +.calibration-grid-charts { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} +.calibration-gauge { display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 16px; - margin-top: 16px; + gap: 8px; } -.admin-chart { - margin: 0; - border: 1px solid var(--hairline); - border-radius: 12px; - padding: 12px; +.calibration-gauge-track { + position: relative; + height: 10px; + border-radius: 999px; + background: var(--hairline); + /* visible so the baseline marker can extend a touch above/below the track */ + overflow: visible; } -.admin-chart img { - width: 100%; - height: auto; - display: block; +.calibration-gauge-fill { + position: absolute; + inset: 0 auto 0 0; + height: 100%; + border-radius: 999px; + min-width: 2px; } -.admin-chart figcaption { - font-family: var(--font-mono); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.02em; +.calibration-gauge-ok { + background: #1a7f37; +} +.calibration-gauge-warn { + background: #d29922; +} +.calibration-gauge-neutral { + background: var(--link); +} +.calibration-gauge-marker { + position: absolute; + top: -2px; + bottom: -2px; + width: 2px; + background: var(--ink); + z-index: 1; +} +.calibration-gauge small { color: var(--muted); - margin-top: 8px; + font-size: 12px; +} +.calibration-split { + display: flex; + height: 12px; + border-radius: 999px; + overflow: hidden; + background: var(--hairline); +} +.calibration-split .seg { + display: block; + height: 100%; +} +.calibration-split .seg-ok { + background: #1a7f37; +} +.calibration-split .seg-partial { + background: #d29922; +} +.calibration-split .seg-bad { + background: #c0392b; } .admin-toggle { display: flex; diff --git a/src/components/admin/panels.tsx b/src/components/admin/panels.tsx index 6666e42..3e86570 100644 --- a/src/components/admin/panels.tsx +++ b/src/components/admin/panels.tsx @@ -10,6 +10,7 @@ import { agentOptions, calibrationGeneratedAt, calibrationIssues, + calibrationOutcomes, decimal, dollars, formatParams, @@ -358,12 +359,40 @@ export function JobsPanel() { ); } -const CHARTS = [ - { type: "brier-trend", label: "Brier trend" }, - { type: "pattern-statements", label: "Pattern statements" }, - { type: "domain-bars", label: "Per-domain accuracy" }, - { type: "abandoned-threads", label: "Abandoned threads" }, -]; +// Light-themed horizontal gauge: a value on a 0..max track with an optional +// baseline marker. Replaces the dark server-rendered SVG charts so the panel +// stays on Lore's light theme (built from the profile JSON, no gbrain images). +function CalibrationGauge({ + value, + max, + marker, + tone, + caption, +}: { + value: number | null | undefined; + max: number; + marker?: number; + tone: "ok" | "warn" | "neutral"; + caption: string; +}) { + const pct = + value == null || !Number.isFinite(value) ? 0 : Math.max(0, Math.min(1, value / max)) * 100; + const markerPct = marker != null ? Math.max(0, Math.min(1, marker / max)) * 100 : null; + return ( +
+
+ + {markerPct != null && ( + + )} +
+ {caption} +
+ ); +} function CalibrationMetric({ label, @@ -410,6 +439,7 @@ export function CalibrationPanel() { const issues = p ? calibrationIssues(p) : []; const patterns = p?.pattern_statements ?? []; const biasTags = p?.active_bias_tags ?? []; + const outcomes = p ? calibrationOutcomes(p) : null; return ( No pattern statements returned.

)} -
- {CHARTS.map((c) => ( -
- {/* server-proxied SVG, chart-type allowlisted */} - {c.label} -
{c.label}
-
- ))} +
+
+

Brier vs baseline

+ +
+
+

Accuracy

+ +
+
+

Outcomes

+ {outcomes ? ( + <> + + + {outcomes.correct} correct · {outcomes.incorrect} wrong + {outcomes.partial > 0 ? ` · ${outcomes.partial} partial` : ""} · n= + {outcomes.total} + + + ) : ( +

No resolved outcomes yet.

+ )} +
Raw profile diff --git a/src/lib/admin-format.ts b/src/lib/admin-format.ts index 5c2e0a5..02102df 100644 --- a/src/lib/admin-format.ts +++ b/src/lib/admin-format.ts @@ -67,6 +67,32 @@ export interface CalibrationIssue { detail: string; } +export interface CalibrationOutcomes { + correct: number; + incorrect: number; + partial: number; + total: number; +} + +// Reconstruct the correct/incorrect/partial counts from the scorecard's own +// aggregates — the inverse of how gbrain computes them: +// total = correct + incorrect + partial +// partial_rate = partial / total +// accuracy = correct / (correct + incorrect) (partial excluded) +// Exact for clean samples (integer rounding for the rest). Returns null when +// there's nothing to show or accuracy is unavailable. +export function calibrationOutcomes(profile: CalibrationProfile): CalibrationOutcomes | null { + const total = profile.total_resolved ?? 0; + if (total <= 0) return null; + const acc = profile.accuracy; + if (acc == null || !Number.isFinite(acc)) return null; + const partial = Math.min(total, Math.max(0, Math.round((profile.partial_rate ?? 0) * total))); + const nonPartial = total - partial; + const correct = Math.min(nonPartial, Math.max(0, Math.round(acc * nonPartial))); + const incorrect = nonPartial - correct; + return { correct, incorrect, partial, total }; +} + // "just now" / "5m ago" / "3h ago" / "2d ago". export function relativeTime(iso: string | null | undefined, now = Date.now()): string { if (!iso) return "—"; diff --git a/tests/admin-format.test.ts b/tests/admin-format.test.ts index 62f3ba3..116bba3 100644 --- a/tests/admin-format.test.ts +++ b/tests/admin-format.test.ts @@ -4,6 +4,7 @@ import { agentOptions, calibrationGeneratedAt, calibrationIssues, + calibrationOutcomes, decimal, dollars, formatParams, @@ -14,6 +15,22 @@ import { } from "../src/lib/admin-format.js"; import { agents, calibrationProfile, jobsSnapshot, requestsPage } from "./fixtures/admin.js"; +test("calibrationOutcomes reconstructs correct/incorrect/partial from scorecard aggregates", () => { + // 8 resolved, 50% accuracy, 0 partial → 4 correct / 4 wrong (exact) + expect( + calibrationOutcomes({ ...calibrationProfile, total_resolved: 8, accuracy: 0.5, partial_rate: 0 }), + ).toEqual({ correct: 4, incorrect: 4, partial: 0, total: 8 }); + // with partials: 10 resolved, 20% partial (=2), accuracy 75% over the 8 non-partial → 6/2 + expect( + calibrationOutcomes({ ...calibrationProfile, total_resolved: 10, accuracy: 0.75, partial_rate: 0.2 }), + ).toEqual({ correct: 6, incorrect: 2, partial: 2, total: 10 }); + // guards: no data / no accuracy → null + expect(calibrationOutcomes({ ...calibrationProfile, total_resolved: 0 })).toBeNull(); + expect( + calibrationOutcomes({ ...calibrationProfile, total_resolved: 8, accuracy: null }), + ).toBeNull(); +}); + test("formatParams matches upstream summary (query / slug / ~partial / limit / +N)", () => { expect(formatParams(null)).toBeNull(); expect(formatParams({})).toBeNull();