MedASR-Bench / src /components /model /MetricPanel.tsx
ngoan
MedASR Bench: full platform + Hugging Face Docker Space packaging
d70f132
Raw
History Blame Contribute Delete
5.19 kB
"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>
);
}