Spaces:
Running
Running
| "use client" | |
| import { useCallback, useEffect, useMemo, useState } from "react" | |
| import { useParams, usePathname, useRouter, useSearchParams } from "next/navigation" | |
| import Link from "next/link" | |
| import { ArrowLeft, ArrowUpRight, BarChart3, Grid3X3, Search } from "lucide-react" | |
| import { Navigation } from "@/components/navigation" | |
| import { EvalDetail } from "@/components/eval-detail" | |
| import { ParamRangePicker } from "@/components/param-range-picker" | |
| import { useAudienceMode } from "@/components/audience-mode-provider" | |
| import type { BenchmarkEvalSummary } from "@/lib/eval-processing" | |
| import { fetchEvalSummary } from "@/lib/dashboard-data-client" | |
| import { PARAM_RANGE_MAX_INDEX, parseParamsBillionsFromModelName, paramStepToNumeric } from "@/lib/param-range" | |
| export default function EvalDetailPage() { | |
| const params = useParams() | |
| const pathname = usePathname() | |
| const router = useRouter() | |
| const searchParams = useSearchParams() | |
| const [summary, setSummary] = useState<BenchmarkEvalSummary | null>(null) | |
| const [subSummaries, setSubSummaries] = useState<BenchmarkEvalSummary[]>([]) | |
| const [loading, setLoading] = useState(true) | |
| const [error, setError] = useState<string | null>(null) | |
| const [matrixSearch, setMatrixSearch] = useState("") | |
| const returnTo = searchParams.get("from") | |
| const currentDetailHref = useMemo(() => { | |
| const params = new URLSearchParams(searchParams.toString()) | |
| params.delete("from") | |
| const query = params.toString() | |
| return query ? `${pathname}?${query}` : pathname | |
| }, [pathname, searchParams]) | |
| const handleBack = useCallback(() => { | |
| if (returnTo?.startsWith("/")) { | |
| router.push(returnTo) | |
| return | |
| } | |
| if (typeof window !== "undefined" && window.history.length > 1) { | |
| router.back() | |
| return | |
| } | |
| router.push("/evals") | |
| }, [returnTo, router]) | |
| useEffect(() => { | |
| const load = async () => { | |
| try { | |
| const evalId = decodeURIComponent(params.id as string) | |
| const found = await fetchEvalSummary(evalId) | |
| setSummary(found) | |
| document.title = `${found.evaluation_name} | Benchmark` | |
| if (found.is_aggregated && found.aggregate_sources?.length) { | |
| const subs = await Promise.all( | |
| found.aggregate_sources.map(async (source) => { | |
| try { | |
| return await fetchEvalSummary(source.evaluation_id) | |
| } catch { | |
| return null | |
| } | |
| }) | |
| ) | |
| setSubSummaries(subs.filter((s): s is BenchmarkEvalSummary => s !== null)) | |
| } | |
| } catch (err) { | |
| console.error(err) | |
| setError("Evaluation not found") | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| load() | |
| }, [params.id]) | |
| if (loading) { | |
| return ( | |
| <div className="min-h-screen bg-background"> | |
| <Navigation /> | |
| <main className="ec-page"> | |
| <div className="flex items-center justify-center h-96"> | |
| <div className="kicker">Loading evaluation record…</div> | |
| </div> | |
| </main> | |
| </div> | |
| ) | |
| } | |
| if (error || !summary) { | |
| return ( | |
| <div className="min-h-screen bg-background"> | |
| <Navigation /> | |
| <main className="ec-page"> | |
| <div className="flex flex-col items-center justify-center h-96 space-y-4"> | |
| <div className="kicker">{error ?? "Evaluation not found"}</div> | |
| <button type="button" onClick={handleBack} className="btn-ec outline"> | |
| <ArrowLeft className="mr-2 h-4 w-4" /> | |
| Evaluations | |
| </button> | |
| </div> | |
| </main> | |
| </div> | |
| ) | |
| } | |
| const isComposite = summary.is_aggregated && (summary.aggregate_sources?.length ?? 0) > 1 | |
| return ( | |
| <div className="min-h-screen bg-background"> | |
| <Navigation /> | |
| <main className="ec-page"> | |
| <button | |
| type="button" | |
| onClick={handleBack} | |
| className="ec-crumb mb-6 inline-flex items-center gap-1.5" | |
| > | |
| <ArrowLeft className="h-3 w-3" /> | |
| Evaluations | |
| </button> | |
| {isComposite ? ( | |
| <CompositeEvalView | |
| summary={summary} | |
| subSummaries={subSummaries} | |
| matrixSearch={matrixSearch} | |
| onMatrixSearchChange={setMatrixSearch} | |
| currentDetailHref={currentDetailHref} | |
| /> | |
| ) : ( | |
| <EvalDetail summary={summary} /> | |
| )} | |
| </main> | |
| </div> | |
| ) | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Composite (suite) view — paper §3.2 "composite reporting unit" | |
| // Surfaces sub-benchmarks as a hairline grid and a per-model × per-metric | |
| // matrix table. Both modes (research / policy) share the same chrome; the | |
| // policy-note panel changes per benchmark, surfaced from the sub-summary card. | |
| // --------------------------------------------------------------------------- | |
| type Tab = "metrics" | "matrix" | |
| function CompositeEvalView({ | |
| summary, | |
| subSummaries, | |
| matrixSearch, | |
| onMatrixSearchChange, | |
| currentDetailHref, | |
| }: { | |
| summary: BenchmarkEvalSummary | |
| subSummaries: BenchmarkEvalSummary[] | |
| matrixSearch: string | |
| onMatrixSearchChange: (v: string) => void | |
| currentDetailHref: string | |
| }) { | |
| const { mode } = useAudienceMode() | |
| const isPolicy = mode === "policy" | |
| const [tab, setTab] = useState<Tab>("metrics") | |
| const sources = summary.aggregate_sources ?? [] | |
| const subBenchmarkCount = sources.length | |
| const card = summary.benchmark_card | |
| const goal = card?.purpose_and_intended_users?.goal?.trim() | |
| const overview = card?.benchmark_details?.overview?.trim() | |
| const limitations = card?.purpose_and_intended_users?.limitations?.trim() | |
| const audience = card?.purpose_and_intended_users?.audience | |
| const audienceText = Array.isArray(audience) ? audience.join("; ") : audience | |
| const lede = isPolicy | |
| ? overview || goal || `Suite aggregating ${subBenchmarkCount} component benchmarks across ${summary.models_count.toLocaleString()} models.` | |
| : goal || overview || `Suite aggregating ${subBenchmarkCount} component benchmarks across ${summary.models_count.toLocaleString()} models.` | |
| return ( | |
| <div className="space-y-10"> | |
| {/* HERO ------------------------------------------------------------- */} | |
| <header className="motion-academic-enter"> | |
| <h1 className="ec-page-h1">{summary.evaluation_name}</h1> | |
| <div | |
| className="mb-5 flex flex-wrap items-center gap-3 font-mono text-[11px] uppercase tracking-[0.12em]" | |
| style={{ color: "var(--fg-muted)" }} | |
| > | |
| {summary.composite_benchmark_name && summary.composite_benchmark_name !== summary.evaluation_name && ( | |
| <> | |
| <span>{summary.composite_benchmark_name}</span> | |
| <span style={{ color: "var(--fg-subtle)" }}>·</span> | |
| </> | |
| )} | |
| <span>{summary.category}</span> | |
| <span style={{ color: "var(--fg-subtle)" }}>·</span> | |
| <span>{summary.metric_config.lower_is_better ? "Lower is better ↓" : "Higher is better ↑"}</span> | |
| </div> | |
| <p className="ec-page-lede">{lede}</p> | |
| <div className="ec-page-meta mt-2"> | |
| <div className="ec-page-meta-item"> | |
| <span className="ec-page-meta-item-l">Components</span> | |
| <span className="ec-page-meta-item-v">{subBenchmarkCount}</span> | |
| </div> | |
| <div className="ec-page-meta-item"> | |
| <span className="ec-page-meta-item-l">Models</span> | |
| <span className="ec-page-meta-item-v">{summary.models_count.toLocaleString()}</span> | |
| </div> | |
| <div className="ec-page-meta-item"> | |
| <span className="ec-page-meta-item-l">Metrics</span> | |
| <span className="ec-page-meta-item-v">{summary.metrics_count ?? subBenchmarkCount}</span> | |
| </div> | |
| {summary.tags?.languages && summary.tags.languages.length > 0 && ( | |
| <div className="ec-page-meta-item"> | |
| <span className="ec-page-meta-item-l">Languages</span> | |
| <span className="ec-page-meta-item-v">{summary.tags.languages.slice(0, 3).join(", ")}</span> | |
| </div> | |
| )} | |
| </div> | |
| </header> | |
| {/* POLICY NOTE (policy mode only) ----------------------------------- */} | |
| {isPolicy && (overview || limitations || audienceText) && ( | |
| <section className="ec-card warm" style={{ padding: "20px 24px" }}> | |
| <div className="kicker mb-3">Policy note</div> | |
| <dl className="grid gap-y-2.5 text-[14px]" style={{ gridTemplateColumns: "max-content 1fr", columnGap: 24 }}> | |
| {overview && ( | |
| <> | |
| <dt className="font-mono text-[10px] uppercase tracking-[0.14em]" style={{ color: "var(--fg-subtle)", paddingTop: 3 }}> | |
| Measures | |
| </dt> | |
| <dd style={{ color: "var(--fg)", lineHeight: 1.6 }}>{overview}</dd> | |
| </> | |
| )} | |
| {limitations && ( | |
| <> | |
| <dt className="font-mono text-[10px] uppercase tracking-[0.14em]" style={{ color: "var(--accent)", paddingTop: 3 }}> | |
| Caveat | |
| </dt> | |
| <dd style={{ color: "var(--fg)", lineHeight: 1.6 }}>{limitations}</dd> | |
| </> | |
| )} | |
| {audienceText && ( | |
| <> | |
| <dt className="font-mono text-[10px] uppercase tracking-[0.14em]" style={{ color: "var(--fg-subtle)", paddingTop: 3 }}> | |
| Intended for | |
| </dt> | |
| <dd style={{ color: "var(--fg)", lineHeight: 1.6 }}>{audienceText}</dd> | |
| </> | |
| )} | |
| </dl> | |
| </section> | |
| )} | |
| {/* TAB SWITCH ------------------------------------------------------- */} | |
| <div> | |
| <div className="section-head"> | |
| <h2>{tab === "metrics" ? "Sub-benchmarks" : "Score breakdown"}</h2> | |
| <div className="flex items-center gap-1"> | |
| <button | |
| type="button" | |
| onClick={() => setTab("metrics")} | |
| className={`ec-pill ${tab === "metrics" ? "on" : ""}`} | |
| > | |
| <span className="inline-flex items-center gap-1.5"> | |
| <BarChart3 className="h-3 w-3" /> | |
| Sub-benchmarks · {subBenchmarkCount} | |
| </span> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setTab("matrix")} | |
| className={`ec-pill ${tab === "matrix" ? "on" : ""}`} | |
| > | |
| <span className="inline-flex items-center gap-1.5"> | |
| <Grid3X3 className="h-3 w-3" /> | |
| Matrix view | |
| </span> | |
| </button> | |
| </div> | |
| </div> | |
| <p | |
| className="text-[13px] leading-[1.6] mb-6" | |
| style={{ color: "var(--fg-muted)", maxWidth: 720 }} | |
| > | |
| {tab === "metrics" | |
| ? "Each card is one component benchmark inside this suite. Click a card to inspect its leaderboard, sub-tasks and benchmark card." | |
| : "Per-model scores across every component metric. Each column is a separately reported measure — distinct measures stay separate instead of collapsing into one number."} | |
| </p> | |
| {tab === "metrics" ? ( | |
| <SubBenchmarkGrid | |
| sources={sources} | |
| subSummaries={subSummaries} | |
| currentDetailHref={currentDetailHref} | |
| /> | |
| ) : ( | |
| <MatrixLeaderboard | |
| summary={summary} | |
| subSummaries={subSummaries} | |
| search={matrixSearch} | |
| onSearchChange={onMatrixSearchChange} | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Sub-benchmark cards (paper-aligned fam-grid) | |
| // --------------------------------------------------------------------------- | |
| function SubBenchmarkGrid({ | |
| sources, | |
| subSummaries, | |
| currentDetailHref, | |
| }: { | |
| sources: NonNullable<BenchmarkEvalSummary["aggregate_sources"]> | |
| subSummaries: BenchmarkEvalSummary[] | |
| currentDetailHref: string | |
| }) { | |
| const subMap = useMemo( | |
| () => new Map(subSummaries.map((s) => [s.evaluation_id, s])), | |
| [subSummaries] | |
| ) | |
| if (sources.length === 0) { | |
| return ( | |
| <div className="ec-card" style={{ padding: 32, textAlign: "center" }}> | |
| <div className="kicker">No component benchmarks reported</div> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div className="fam-grid"> | |
| {sources.map((source) => { | |
| const sub = subMap.get(source.evaluation_id) | |
| const card = sub?.benchmark_card | |
| const overview = card?.benchmark_details?.overview ?? sub?.metric_config?.evaluation_description | |
| const goal = card?.purpose_and_intended_users?.goal | |
| const summaryLine = goal || overview | |
| return ( | |
| <Link | |
| key={source.evaluation_id} | |
| href={`/evals/${source.evaluation_id}?from=${encodeURIComponent(currentDetailHref)}`} | |
| className="fam-card group block" | |
| style={{ textDecoration: "none", color: "inherit" }} | |
| > | |
| <div className="flex items-start justify-between gap-2 mb-1"> | |
| <div className="fam-card-kind">Component benchmark</div> | |
| <div className="fam-card-counts"> | |
| {source.models_count} model{source.models_count === 1 ? "" : "s"} | |
| </div> | |
| </div> | |
| <h3 className="fam-card-name group-hover:text-[color:var(--accent)] transition-colors"> | |
| {card?.benchmark_details?.name ?? source.composite_benchmark_name} | |
| </h3> | |
| <div className="fam-card-org">{source.evaluation_id}</div> | |
| {summaryLine && ( | |
| <p className="fam-card-summary line-clamp-3">{summaryLine}</p> | |
| )} | |
| {sub?.best_model && ( | |
| <div | |
| className="mt-3 pt-3 text-[12px]" | |
| style={{ | |
| borderTop: "1px dashed var(--border-soft)", | |
| color: "var(--fg-muted)", | |
| }} | |
| > | |
| <span | |
| className="font-mono uppercase tracking-[0.12em] mr-2" | |
| style={{ fontSize: 9.5, color: "var(--fg-subtle)" }} | |
| > | |
| Top | |
| </span> | |
| <span style={{ color: "var(--fg)", fontWeight: 600 }}> | |
| {sub.best_model.name} | |
| </span> | |
| <span className="ml-1 font-mono tabular-nums" style={{ color: "var(--fg-muted)" }}> | |
| {(sub.best_model.score * 100).toFixed(1)}% | |
| </span> | |
| </div> | |
| )} | |
| <div | |
| className="mt-3 inline-flex items-center gap-1 font-mono text-[10px] uppercase tracking-[0.12em]" | |
| style={{ color: "var(--accent)" }} | |
| > | |
| Open | |
| <ArrowUpRight className="h-3 w-3" /> | |
| </div> | |
| </Link> | |
| ) | |
| })} | |
| </div> | |
| ) | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Matrix leaderboard (models × metrics) — paper-aligned ec-htable | |
| // --------------------------------------------------------------------------- | |
| function MatrixLeaderboard({ | |
| summary: _summary, | |
| subSummaries, | |
| search, | |
| onSearchChange, | |
| }: { | |
| summary: BenchmarkEvalSummary | |
| subSummaries: BenchmarkEvalSummary[] | |
| search: string | |
| onSearchChange: (v: string) => void | |
| }) { | |
| const [sortCol, setSortCol] = useState<string>("avg") | |
| const [sortAsc, setSortAsc] = useState(false) | |
| const [page, setPage] = useState(1) | |
| const [hiddenCols, setHiddenCols] = useState<Set<string>>(new Set()) | |
| const [minParamStep, setMinParamStep] = useState(0) | |
| const [maxParamStep, setMaxParamStep] = useState(PARAM_RANGE_MAX_INDEX) | |
| const PAGE_SIZE = 50 | |
| const metricDirection = useMemo(() => { | |
| const map = new Map<string, boolean>() | |
| for (const sub of subSummaries) { | |
| map.set(sub.evaluation_name, sub.metric_config.lower_is_better) | |
| } | |
| return map | |
| }, [subSummaries]) | |
| const { models, metrics } = useMemo(() => { | |
| const metricNames = subSummaries.map((s) => s.evaluation_name) | |
| const modelScores = new Map<string, { name: string; developer: string; scores: Map<string, number | null> }>() | |
| for (const sub of subSummaries) { | |
| for (const result of sub.model_results) { | |
| const id = result.model_info.id | |
| const existing = modelScores.get(id) ?? { | |
| name: result.model_info.name, | |
| developer: result.model_info.developer ?? "", | |
| scores: new Map<string, number | null>(), | |
| } | |
| existing.scores.set(sub.evaluation_name, result.score) | |
| modelScores.set(id, existing) | |
| } | |
| } | |
| const modelList = Array.from(modelScores.entries()) | |
| .map(([id, data]) => { | |
| const validScores = Array.from(data.scores.values()).filter( | |
| (s): s is number => s != null && Number.isFinite(s) && s > -99 | |
| ) | |
| const avg = validScores.length > 0 | |
| ? validScores.reduce((a, b) => a + b, 0) / validScores.length | |
| : 0 | |
| const sizeB = | |
| parseParamsBillionsFromModelName(data.name) ?? | |
| parseParamsBillionsFromModelName(id) | |
| return { id, name: data.name, developer: data.developer, avg, scores: data.scores, sizeB } | |
| }) | |
| return { models: modelList, metrics: metricNames } | |
| }, [subSummaries]) | |
| const visibleMetrics = useMemo( | |
| () => metrics.filter((m) => !hiddenCols.has(m)), | |
| [metrics, hiddenCols] | |
| ) | |
| const sortedModels = useMemo(() => { | |
| return [...models].sort((a, b) => { | |
| if (sortCol === "name") { | |
| return sortAsc ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name) | |
| } | |
| const aVal = sortCol === "avg" ? a.avg : (a.scores.get(sortCol) ?? -Infinity) | |
| const bVal = sortCol === "avg" ? b.avg : (b.scores.get(sortCol) ?? -Infinity) | |
| return sortAsc ? aVal - bVal : bVal - aVal | |
| }) | |
| }, [models, sortCol, sortAsc]) | |
| const numericMinParams = paramStepToNumeric(minParamStep, "min") | |
| const numericMaxParams = paramStepToNumeric(maxParamStep, "max") | |
| const [showUnknownSize, setShowUnknownSize] = useState(true) | |
| const hasParameterData = useMemo(() => models.some((m) => m.sizeB != null), [models]) | |
| const query = search.trim().toLowerCase() | |
| const filteredModels = sortedModels.filter((m) => { | |
| if (query && !( | |
| m.name.toLowerCase().includes(query) || | |
| m.developer.toLowerCase().includes(query) || | |
| m.id.toLowerCase().includes(query) | |
| )) return false | |
| if (m.sizeB == null) return showUnknownSize | |
| if (numericMinParams != null && m.sizeB < numericMinParams) return false | |
| if (numericMaxParams != null && m.sizeB > numericMaxParams) return false | |
| return true | |
| }) | |
| const pagedModels = filteredModels.slice(0, page * PAGE_SIZE) | |
| const hasMore = pagedModels.length < filteredModels.length | |
| const metricRanges = useMemo(() => { | |
| const ranges = new Map<string, { min: number; max: number }>() | |
| for (const metric of visibleMetrics) { | |
| const scores = models.map((m) => m.scores.get(metric)).filter( | |
| (s): s is number => s != null && Number.isFinite(s) && s > -99 | |
| ) | |
| if (scores.length > 0) { | |
| ranges.set(metric, { min: Math.min(...scores), max: Math.max(...scores) }) | |
| } | |
| } | |
| return ranges | |
| }, [models, visibleMetrics]) | |
| function isValidScore(score: number | null | undefined): score is number { | |
| return score != null && Number.isFinite(score) && score > -99 | |
| } | |
| function scoreColor(metric: string, score: number): string { | |
| const range = metricRanges.get(metric) | |
| if (!range || range.max === range.min) return "" | |
| const lower = metricDirection.get(metric) ?? false | |
| const pct = lower | |
| ? (range.max - score) / (range.max - range.min) | |
| : (score - range.min) / (range.max - range.min) | |
| if (pct >= 0.8) return "bg-emerald-100 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-200" | |
| if (pct >= 0.6) return "bg-sky-50 dark:bg-sky-950/30 text-sky-800 dark:text-sky-200" | |
| if (pct <= 0.2) return "bg-red-50 dark:bg-red-950/30 text-red-800 dark:text-red-200" | |
| return "" | |
| } | |
| function formatScore(score: number): string { | |
| if (Math.abs(score) >= 100) return score.toFixed(1) | |
| if (Math.abs(score) >= 10) return score.toFixed(2) | |
| return score.toFixed(3).replace(/0+$/g, "").replace(/\.$/, "") | |
| } | |
| function handleSort(col: string) { | |
| if (sortCol === col) setSortAsc(!sortAsc) | |
| else { setSortCol(col); setSortAsc(false) } | |
| } | |
| const sortIndicator = (col: string) => | |
| sortCol === col ? (sortAsc ? " ▲" : " ▼") : "" | |
| function toggleCol(metric: string) { | |
| setHiddenCols((prev) => { | |
| const next = new Set(prev) | |
| if (next.has(metric)) next.delete(metric) | |
| else next.add(metric) | |
| return next | |
| }) | |
| } | |
| if (subSummaries.length === 0) { | |
| return ( | |
| <div className="ec-card" style={{ padding: 32, textAlign: "center" }}> | |
| <div className="kicker">Loading component benchmark data…</div> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div className="space-y-5"> | |
| {/* Filter row */} | |
| <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:flex-wrap"> | |
| <div className="relative w-full max-w-sm"> | |
| <Search | |
| className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2" | |
| style={{ color: "var(--fg-subtle)" }} | |
| /> | |
| <input | |
| value={search} | |
| onChange={(e) => onSearchChange(e.target.value)} | |
| placeholder="Search models…" | |
| className="ec-input" | |
| style={{ paddingLeft: 36 }} | |
| /> | |
| </div> | |
| {hasParameterData && ( | |
| <ParamRangePicker | |
| variant="inline" | |
| headline="Params" | |
| minStep={minParamStep} | |
| maxStep={maxParamStep} | |
| onMinChange={setMinParamStep} | |
| onMaxChange={setMaxParamStep} | |
| onReset={() => { | |
| setMinParamStep(0) | |
| setMaxParamStep(PARAM_RANGE_MAX_INDEX) | |
| }} | |
| showUnknownSize={showUnknownSize} | |
| onShowUnknownSizeChange={setShowUnknownSize} | |
| className="min-w-[260px] flex-1 sm:max-w-[420px]" | |
| /> | |
| )} | |
| <div | |
| className="font-mono uppercase tracking-[0.14em] whitespace-nowrap ml-auto" | |
| style={{ fontSize: 10, color: "var(--fg-subtle)" }} | |
| > | |
| {filteredModels.length} models × {visibleMetrics.length} metrics | |
| </div> | |
| </div> | |
| {/* Column toggles */} | |
| <div className="flex flex-wrap gap-1.5"> | |
| {metrics.map((metric) => { | |
| const off = hiddenCols.has(metric) | |
| return ( | |
| <button | |
| key={metric} | |
| type="button" | |
| onClick={() => toggleCol(metric)} | |
| className="ec-pill" | |
| style={{ | |
| opacity: off ? 0.5 : 1, | |
| textDecoration: off ? "line-through" : "none", | |
| background: off ? "transparent" : "var(--bg)", | |
| }} | |
| > | |
| {metric} | |
| </button> | |
| ) | |
| })} | |
| </div> | |
| {/* Matrix table */} | |
| <div className="overflow-x-auto" style={{ border: "1px solid var(--border-soft)" }}> | |
| <table className="ec-htable" style={{ tableLayout: "fixed", minWidth: "max-content" }}> | |
| <colgroup> | |
| <col style={{ width: 48 }} /> | |
| <col style={{ width: 240 }} /> | |
| <col style={{ width: 90 }} /> | |
| {visibleMetrics.map((m) => ( | |
| <col key={m} style={{ width: 130 }} /> | |
| ))} | |
| </colgroup> | |
| <thead> | |
| <tr> | |
| <th>#</th> | |
| <th | |
| onClick={() => handleSort("name")} | |
| style={{ cursor: "pointer" }} | |
| > | |
| Model{sortIndicator("name")} | |
| </th> | |
| <th | |
| className="num" | |
| onClick={() => handleSort("avg")} | |
| style={{ cursor: "pointer" }} | |
| > | |
| Avg{sortIndicator("avg")} | |
| </th> | |
| {visibleMetrics.map((metric) => ( | |
| <th | |
| key={metric} | |
| className="num" | |
| onClick={() => handleSort(metric)} | |
| style={{ cursor: "pointer" }} | |
| title={metric} | |
| > | |
| <span className="truncate inline-block max-w-full"> | |
| {metric}{sortIndicator(metric)} | |
| </span> | |
| </th> | |
| ))} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {pagedModels.map((model, idx) => ( | |
| <tr key={model.id}> | |
| <td | |
| className="font-mono tabular-nums" | |
| style={{ color: idx < 3 ? "var(--accent)" : "var(--fg-muted)", fontSize: 12 }} | |
| > | |
| {idx + 1} | |
| </td> | |
| <td> | |
| <div className="font-semibold text-[14px] truncate">{model.name}</div> | |
| <div | |
| className="font-mono text-[10px] uppercase tracking-[0.08em] mt-0.5 truncate" | |
| style={{ color: "var(--fg-subtle)" }} | |
| > | |
| {model.developer} | |
| </div> | |
| </td> | |
| <td className="num font-mono tabular-nums" style={{ fontWeight: 600, fontSize: 14 }}> | |
| {formatScore(model.avg)} | |
| </td> | |
| {visibleMetrics.map((metric) => { | |
| const score = model.scores.get(metric) | |
| const valid = isValidScore(score) | |
| return ( | |
| <td | |
| key={metric} | |
| className={`num font-mono tabular-nums ${valid ? scoreColor(metric, score) : ""}`} | |
| style={{ color: valid ? undefined : "var(--fg-subtle)", fontSize: 13 }} | |
| > | |
| {valid ? formatScore(score) : "—"} | |
| </td> | |
| ) | |
| })} | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| {hasMore && ( | |
| <div className="text-center"> | |
| <button | |
| type="button" | |
| className="btn-ec outline" | |
| onClick={() => setPage((p) => p + 1)} | |
| > | |
| Load more ({filteredModels.length - pagedModels.length} remaining) | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |