Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useState } from "react"; | |
| import MetricCard from "@/components/ui/MetricCard"; | |
| import SplitToggle from "@/components/ui/SplitToggle"; | |
| import { NORMALIZER_VERSION, splitLabels } from "@/lib/data"; | |
| import type { | |
| ModelRow, | |
| Provenance, | |
| Split, | |
| SplitInfo, | |
| SplitMetrics, | |
| } from "@/lib/types"; | |
| import SectionLabel from "./SectionLabel"; | |
| export interface MetricPanelProps { | |
| /** dataset these results are scoped to, e.g. "ViMedCSS" — the hub hosts | |
| multiple datasets, so the section header names the one being shown */ | |
| datasetName: string; | |
| metrics?: ModelRow["metrics"]; | |
| splits: { test: SplitInfo; hard: SplitInfo }; | |
| provenance: Provenance; | |
| } | |
| const VALUE_KEYS = [ | |
| "wer", | |
| "csWer", | |
| "nWer", | |
| "cer", | |
| "mtrExact", | |
| "rtfx", | |
| ] as const; | |
| const METRIC_CHIPS = ["WER", "CS-WER", "N-WER", "CER", "MTR", "RTFx"] as const; | |
| function emptyNote(provenance: Provenance, split: Split): string { | |
| if (provenance === "running") | |
| return "No committed measurements yet — the pilot run executing now will populate this panel."; | |
| if (provenance === "planned") | |
| return "No measurements yet — this model is on the evaluation roadmap."; | |
| return `No measurements recorded on the ${splitLabels[split]} split yet.`; | |
| } | |
| /** Per-card microcopy for a missing value, by row provenance. */ | |
| function gapReason(provenance: Provenance): string { | |
| if (provenance === "paper-imported") return "not published"; | |
| if (provenance === "running" || provenance === "planned") | |
| return "awaiting pilot run"; | |
| return "not recorded"; | |
| } | |
| /** | |
| * Split-toggleable metric grid: WER / CS-WER / N-WER / CER / MTR (exact) / | |
| * RTFx as count-up MetricCards, with 95% CI sub-lines where measured. | |
| */ | |
| export default function MetricPanel({ | |
| datasetName, | |
| metrics, | |
| splits, | |
| provenance, | |
| }: MetricPanelProps) { | |
| const [split, setSplit] = useState<Split>("test"); | |
| const m: SplitMetrics | undefined = metrics?.[split]; | |
| const info = splits[split]; | |
| const hasAny = VALUE_KEYS.some((k) => typeof m?.[k] === "number"); | |
| return ( | |
| <section className="flex flex-col gap-4"> | |
| <div className="flex flex-wrap items-end justify-between gap-3"> | |
| <SectionLabel | |
| index="02" | |
| title={`Results — ${datasetName}`} | |
| hint={`${splitLabels[split]} — ${info.hours} h · ${info.utts.toLocaleString("en-US")} utterances`} | |
| /> | |
| <SplitToggle value={split} onChange={setSplit} /> | |
| </div> | |
| {hasAny ? ( | |
| /* Stable keys: toggling splits updates values in place instead of | |
| remounting cards and replaying the one-shot count-up. */ | |
| <div className="glass depth-2 grid grid-cols-2 gap-3 rounded-sm p-3 md:grid-cols-3"> | |
| <MetricCard | |
| key="wer" | |
| label="WER" | |
| value={m?.wer} | |
| suffix="%" | |
| ci={m?.werCi} | |
| gapReason={gapReason(provenance)} | |
| /> | |
| <MetricCard | |
| key="cswer" | |
| label="CS-WER" | |
| value={m?.csWer} | |
| suffix="%" | |
| ci={m?.csWerCi} | |
| gapReason={gapReason(provenance)} | |
| /> | |
| <MetricCard | |
| key="nwer" | |
| label="N-WER" | |
| value={m?.nWer} | |
| suffix="%" | |
| gapReason={gapReason(provenance)} | |
| /> | |
| <MetricCard | |
| key="cer" | |
| label="CER" | |
| value={m?.cer} | |
| suffix="%" | |
| gapReason={gapReason(provenance)} | |
| /> | |
| <MetricCard | |
| key="mtr" | |
| label="MTR (exact)" | |
| value={m?.mtrExact} | |
| suffix="%" | |
| gapReason={gapReason(provenance)} | |
| /> | |
| <MetricCard | |
| key="rtfx" | |
| label="RTFx" | |
| value={m?.rtfx} | |
| suffix="×" | |
| gapReason={gapReason(provenance)} | |
| /> | |
| </div> | |
| ) : ( | |
| /* flatlined-ECG empty state: one composed panel instead of a | |
| checkerboard of em-dash cards */ | |
| <div className="hairline depth-1 rounded-sm bg-surface px-5 py-6"> | |
| <div aria-hidden className="flex items-center"> | |
| <span className="h-px flex-1 bg-accent/20" /> | |
| <span className="pulse-dot size-2 shrink-0 rounded-full bg-accent" /> | |
| </div> | |
| <p className="mt-4 font-display text-lg italic text-text"> | |
| no trace yet — awaiting platform run | |
| </p> | |
| <p className="mt-1.5 max-w-xl text-[13px] leading-relaxed text-muted"> | |
| {emptyNote(provenance, split)} | |
| </p> | |
| <ul className="mt-4 flex flex-wrap gap-1.5" aria-label="Metrics this run will record"> | |
| {METRIC_CHIPS.map((c) => ( | |
| <li | |
| key={c} | |
| className="num rounded-sm border border-line/70 px-1.5 py-0.5 text-[11px] tracking-wide text-muted" | |
| > | |
| {c} | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| <p className="num text-[11px] text-muted"> | |
| WER family in %, lower is better · MTR higher is better · RTFx = audio | |
| s ÷ wall s, higher is faster · normalizer {NORMALIZER_VERSION} | |
| </p> | |
| </section> | |
| ); | |
| } | |