Spaces:
Running
Running
| "use client" | |
| import { useAudienceMode } from "@/components/audience-mode-provider" | |
| import { Fragment, useEffect, useMemo, useState } from "react" | |
| import Link from "next/link" | |
| import { Badge } from "@/components/ui/badge" | |
| import { Button } from "@/components/ui/button" | |
| import { CompletenessPanel } from "@/components/signals/completeness-panel" | |
| import { ComparabilityPanel } from "@/components/signals/comparability-panel" | |
| import { ReproducibilityPanel } from "@/components/signals/reproducibility-panel" | |
| import { SignalsRowBadges } from "@/components/signals/signals-row-badges" | |
| import { RowSignalsCompact } from "@/components/signals/row-signals-compact" | |
| import { SignalTooltip } from "@/components/signals/signal-tooltip" | |
| import { getCompletenessPopulatedCount } from "@/components/signals/signal-utils" | |
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" | |
| import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogDescription, | |
| DialogHeader, | |
| DialogTitle, | |
| } from "@/components/ui/dialog" | |
| import { Input } from "@/components/ui/input" | |
| import { | |
| DropdownMenu, | |
| DropdownMenuCheckboxItem, | |
| DropdownMenuContent, | |
| DropdownMenuItem, | |
| DropdownMenuLabel, | |
| DropdownMenuSeparator, | |
| DropdownMenuTrigger, | |
| } from "@/components/ui/dropdown-menu" | |
| import { Progress } from "@/components/ui/progress" | |
| import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" | |
| import { getModelFamilyRouteId } from "@/lib/model-family" | |
| import { cn } from "@/lib/utils" | |
| import { | |
| AlertTriangle, | |
| BarChart3, | |
| BookOpen, | |
| ChevronDown, | |
| ChevronUp, | |
| Database, | |
| ExternalLink, | |
| FileText, | |
| Globe, | |
| Medal, | |
| Scale, | |
| Search, | |
| Shield, | |
| SlidersHorizontal, | |
| Tag, | |
| X, | |
| } from "lucide-react" | |
| import type { BenchmarkCard } from "@/lib/benchmark-schema" | |
| import type { BenchmarkEvalSummary, ModelResultForBenchmark } from "@/lib/eval-processing" | |
| import { PolicyOverview } from "@/components/policy-overview" | |
| import { ResearcherReproducibilityCard } from "@/components/researcher-reproducibility-card" | |
| import { KnownIssuesPanel } from "@/components/known-issues-panel" | |
| import { getKnownIssues, type KnownIssue } from "@/lib/known-issues" | |
| import { ApplesToApplesBanner } from "@/components/apples-to-apples-banner" | |
| import { FlagScoreButton } from "@/components/flag-score-button" | |
| interface EvalDetailProps { | |
| summary: BenchmarkEvalSummary | |
| } | |
| interface LeaderboardRow { | |
| key: string | |
| rank: number | |
| modelResult: ModelResultForBenchmark | |
| normalizedScore: number | |
| } | |
| type LeaderboardMetric = NonNullable<BenchmarkEvalSummary["leaderboard_metrics"]>[number] | |
| type LeaderboardMatrixRow = NonNullable<BenchmarkEvalSummary["leaderboard_rows"]>[number] | |
| /** | |
| * Pick a representative row-level annotation for the matrix view. | |
| * | |
| * Reproducibility and provenance are typically constant across all metrics for | |
| * a given (model, benchmark) pair, so rendering them in every cell is just | |
| * noise. This helper grabs the first non-null annotation across visible metrics | |
| * and returns it for the row-level badge strip. | |
| */ | |
| function getRowLevelAnnotations( | |
| row: LeaderboardMatrixRow, | |
| visibleMetrics: LeaderboardMetric[] | |
| ) { | |
| const annotationsByMetric = row.annotations_by_metric | |
| if (!annotationsByMetric) { | |
| return null | |
| } | |
| for (const metric of visibleMetrics) { | |
| const annotations = annotationsByMetric[metric.column_key] | |
| if (annotations) { | |
| return annotations | |
| } | |
| } | |
| return null | |
| } | |
| const SLICE_PILL_THRESHOLD = 5 | |
| interface SliceTab { | |
| key: string | |
| label: string | |
| } | |
| /** | |
| * Slice picker that adapts to slice count. | |
| * | |
| * - <= SLICE_PILL_THRESHOLD: render every slice as a pill (current familiar UX). | |
| * - > SLICE_PILL_THRESHOLD: render "All slices" + currently-selected pill + | |
| * a "Browse N slices" button that opens a searchable dialog. Hundreds of | |
| * subtasks (e.g. AIRBench's 374) fit cleanly. | |
| */ | |
| function SliceSelector({ | |
| activeSubtaskTab, | |
| onChange, | |
| tabs, | |
| }: { | |
| activeSubtaskTab: string | |
| onChange: (key: string) => void | |
| tabs: SliceTab[] | |
| }) { | |
| const [browserOpen, setBrowserOpen] = useState(false) | |
| const [search, setSearch] = useState("") | |
| const useBrowser = tabs.length > SLICE_PILL_THRESHOLD | |
| const activeTab = tabs.find((tab) => tab.key === activeSubtaskTab) | |
| const filteredTabs = useMemo(() => { | |
| const query = search.trim().toLowerCase() | |
| if (!query) return tabs | |
| return tabs.filter((tab) => tab.label.toLowerCase().includes(query)) | |
| }, [search, tabs]) | |
| if (!useBrowser) { | |
| return ( | |
| <div> | |
| <div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground"> | |
| Benchmark slices | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| <Button | |
| type="button" | |
| size="sm" | |
| variant={activeSubtaskTab === "all" ? "default" : "outline"} | |
| onClick={() => onChange("all")} | |
| > | |
| All slices | |
| </Button> | |
| {tabs.map((tab) => ( | |
| <Button | |
| key={tab.key} | |
| type="button" | |
| size="sm" | |
| variant={activeSubtaskTab === tab.key ? "default" : "outline"} | |
| onClick={() => onChange(tab.key)} | |
| > | |
| {tab.label} | |
| </Button> | |
| ))} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div> | |
| <div className="mb-2 flex items-center justify-between gap-2"> | |
| <div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground"> | |
| Benchmark slices | |
| </div> | |
| <span className="text-xs text-muted-foreground">{tabs.length} total</span> | |
| </div> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| <Button | |
| type="button" | |
| size="sm" | |
| variant={activeSubtaskTab === "all" ? "default" : "outline"} | |
| onClick={() => onChange("all")} | |
| > | |
| All slices | |
| </Button> | |
| {activeTab && ( | |
| <Button | |
| type="button" | |
| size="sm" | |
| variant="default" | |
| onClick={() => onChange("all")} | |
| className="max-w-[18rem] truncate" | |
| title={`Active: ${activeTab.label}. Click to clear.`} | |
| > | |
| {activeTab.label} | |
| <X className="ml-1.5 h-3 w-3 shrink-0" /> | |
| </Button> | |
| )} | |
| <Button | |
| type="button" | |
| size="sm" | |
| variant="outline" | |
| onClick={() => setBrowserOpen(true)} | |
| className="gap-1.5" | |
| > | |
| <Search className="h-3.5 w-3.5" /> | |
| {activeTab ? "Change slice" : `Browse ${tabs.length} slices`} | |
| </Button> | |
| </div> | |
| <Dialog | |
| open={browserOpen} | |
| onOpenChange={(open) => { | |
| setBrowserOpen(open) | |
| if (!open) setSearch("") | |
| }} | |
| > | |
| <DialogContent className="max-w-2xl"> | |
| <DialogHeader> | |
| <DialogTitle>Browse benchmark slices</DialogTitle> | |
| <DialogDescription> | |
| {tabs.length} slices in this benchmark. Pick one to filter the leaderboard, | |
| or close to keep showing all slices. | |
| </DialogDescription> | |
| </DialogHeader> | |
| <Input | |
| value={search} | |
| onChange={(event) => setSearch(event.target.value)} | |
| placeholder="Search slices..." | |
| autoFocus | |
| /> | |
| <div className="max-h-[60vh] overflow-y-auto rounded-md border"> | |
| <button | |
| type="button" | |
| onClick={() => { | |
| onChange("all") | |
| setBrowserOpen(false) | |
| }} | |
| className={cn( | |
| "flex w-full items-center justify-between border-b px-4 py-2.5 text-left text-sm transition-colors hover:bg-muted/40", | |
| activeSubtaskTab === "all" && "bg-muted/40 font-semibold" | |
| )} | |
| > | |
| <span>All slices (no filter)</span> | |
| {activeSubtaskTab === "all" && <span className="text-xs text-muted-foreground">selected</span>} | |
| </button> | |
| {filteredTabs.length === 0 ? ( | |
| <div className="px-4 py-6 text-center text-sm text-muted-foreground"> | |
| No slices match "{search}". | |
| </div> | |
| ) : ( | |
| filteredTabs.map((tab) => ( | |
| <button | |
| key={tab.key} | |
| type="button" | |
| onClick={() => { | |
| onChange(tab.key) | |
| setBrowserOpen(false) | |
| }} | |
| className={cn( | |
| "flex w-full items-center justify-between border-b px-4 py-2 text-left text-sm transition-colors hover:bg-muted/40 last:border-b-0", | |
| activeSubtaskTab === tab.key && "bg-muted/40 font-semibold" | |
| )} | |
| > | |
| <span className="min-w-0 truncate pr-2">{tab.label}</span> | |
| {activeSubtaskTab === tab.key && ( | |
| <span className="shrink-0 text-xs text-muted-foreground">selected</span> | |
| )} | |
| </button> | |
| )) | |
| )} | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| </div> | |
| ) | |
| } | |
| const PARAM_RANGE_VALUES = [1, 2, 3, 4, 6, 8, 10, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 500] as const | |
| const PARAM_RANGE_MARKERS = [ | |
| { label: "< 1B", step: 0 }, | |
| { label: "6B", step: PARAM_RANGE_VALUES.indexOf(6) }, | |
| { label: "12B", step: PARAM_RANGE_VALUES.indexOf(12) }, | |
| { label: "32B", step: PARAM_RANGE_VALUES.indexOf(32) }, | |
| { label: "128B", step: PARAM_RANGE_VALUES.indexOf(128) }, | |
| { label: "> 500B", step: PARAM_RANGE_VALUES.length - 1 }, | |
| ] as const | |
| function formatParamBoundLabel(step: number, bound: "min" | "max") { | |
| const maxStepIndex = PARAM_RANGE_VALUES.length - 1 | |
| if (bound === "min" && step <= 0) { | |
| return "< 1B" | |
| } | |
| if (bound === "max" && step >= maxStepIndex) { | |
| return "> 500B" | |
| } | |
| const value = PARAM_RANGE_VALUES[step] | |
| return value != null ? `${value}B` : "Not reported" | |
| } | |
| function parseParamsBillionsFromText(value: string | null | undefined) { | |
| if (!value) { | |
| return null | |
| } | |
| const normalized = value.trim().toLowerCase() | |
| if (!normalized) { | |
| return null | |
| } | |
| const compact = normalized.replace(/,/g, "") | |
| const tokenMatch = compact.match(/(\d+(?:\.\d+)?)\s*(trillion|tn|t|billion|bn|b|million|mn|m|thousand|k)\b/) | |
| if (tokenMatch) { | |
| const amount = Number.parseFloat(tokenMatch[1]) | |
| if (!Number.isFinite(amount)) { | |
| return null | |
| } | |
| const unit = tokenMatch[2] | |
| if (unit === "trillion" || unit === "tn" || unit === "t") { | |
| return amount * 1000 | |
| } | |
| if (unit === "billion" || unit === "bn" || unit === "b") { | |
| return amount | |
| } | |
| if (unit === "million" || unit === "mn" || unit === "m") { | |
| return amount / 1000 | |
| } | |
| if (unit === "thousand" || unit === "k") { | |
| return amount / 1_000_000 | |
| } | |
| } | |
| const numeric = Number.parseFloat(compact) | |
| return Number.isFinite(numeric) ? numeric : null | |
| } | |
| function parseParamsBillionsFromModelName(modelName: string | null | undefined) { | |
| if (!modelName) { | |
| return null | |
| } | |
| const sizeTokens = Array.from(modelName.matchAll(/\b(\d+(?:\.\d+)?)\s*([tmbk])\b/gi)) | |
| if (sizeTokens.length === 0) { | |
| return null | |
| } | |
| const lastToken = sizeTokens[sizeTokens.length - 1] | |
| const numericValue = Number.parseFloat(lastToken[1]) | |
| if (!Number.isFinite(numericValue)) { | |
| return null | |
| } | |
| const unit = lastToken[2].toLowerCase() | |
| if (unit === "t") { | |
| return numericValue * 1000 | |
| } | |
| if (unit === "b") { | |
| return numericValue | |
| } | |
| if (unit === "m") { | |
| return numericValue / 1000 | |
| } | |
| if (unit === "k") { | |
| return numericValue / 1_000_000 | |
| } | |
| return null | |
| } | |
| function getParamsBillionsFromModelInfo(modelInfo: ModelResultForBenchmark["model_info"]) { | |
| const additionalDetails = modelInfo.additional_details | |
| const rawParamsBillions = | |
| additionalDetails?.params_billions ?? | |
| additionalDetails?.parameter_count ?? | |
| additionalDetails?.num_parameters ?? | |
| additionalDetails?.params | |
| if (typeof rawParamsBillions === "number") { | |
| return rawParamsBillions | |
| } | |
| if (typeof rawParamsBillions === "string") { | |
| const parsed = parseParamsBillionsFromText(rawParamsBillions) | |
| if (Number.isFinite(parsed)) { | |
| return parsed | |
| } | |
| } | |
| if (typeof modelInfo.parameter_count === "string") { | |
| const parsed = parseParamsBillionsFromText(modelInfo.parameter_count) | |
| if (Number.isFinite(parsed)) { | |
| return parsed | |
| } | |
| } | |
| return parseParamsBillionsFromModelName(modelInfo.name) | |
| } | |
| function getParamsBillions(modelResult: ModelResultForBenchmark) { | |
| return getParamsBillionsFromModelInfo(modelResult.model_info) | |
| } | |
| function formatMetadataValue(value: unknown): string { | |
| if (value == null) { | |
| return "N/A" | |
| } | |
| if (typeof value === "string") { | |
| return value | |
| } | |
| if (typeof value === "number") { | |
| return Number.isFinite(value) ? value.toLocaleString() : String(value) | |
| } | |
| if (typeof value === "boolean") { | |
| return value ? "true" : "false" | |
| } | |
| if (Array.isArray(value)) { | |
| return value.map((item) => formatMetadataValue(item)).join(", ") | |
| } | |
| try { | |
| return JSON.stringify(value) | |
| } catch { | |
| return String(value) | |
| } | |
| } | |
| function formatDate(ts: string) { | |
| if (!ts || !ts.trim()) { | |
| return "Unknown" | |
| } | |
| const numeric = Number(ts) | |
| const parsedDate = !Number.isNaN(numeric) && !ts.includes("-") ? new Date(numeric * 1000) : new Date(ts) | |
| if (Number.isNaN(parsedDate.getTime())) { | |
| return "Unknown" | |
| } | |
| try { | |
| return parsedDate.toLocaleDateString("en-US", { | |
| year: "numeric", | |
| month: "short", | |
| day: "numeric", | |
| }) | |
| } catch { | |
| return ts | |
| } | |
| } | |
| function formatRawScore(score: number, unit?: string) { | |
| const suffix = unit ? ` ${unit}` : "" | |
| return `${score.toFixed(2)}${suffix}` | |
| } | |
| function isNumericScore(value: number | null | undefined): value is number { | |
| return typeof value === "number" && Number.isFinite(value) | |
| } | |
| function metricLabelReadsAsPercentage(metricLabel: string, unit?: string) { | |
| const normalized = `${metricLabel} ${unit ?? ""}`.toLowerCase() | |
| return unit === "%" || /percent|percentage|accuracy|exact match|win rate|pass@|precision|recall|f1/.test(normalized) | |
| } | |
| function describeLeaderboardMetric(metric: LeaderboardMetric) { | |
| const metricLabel = getCompactMetricLabel(metric.display_name) | |
| const metricPhrase = metricLabelReadsAsPercentage(metricLabel, metric.unit) | |
| ? `${metricLabel} percentage` | |
| : metricLabel | |
| if (metric.scope === "subtask" && metric.subtask_name) { | |
| return `${metricPhrase} for ${metric.subtask_name}` | |
| } | |
| return metric.canonical_display_name || metric.display_name | |
| } | |
| function getCompactMetricLabel(value: string | undefined) { | |
| if (!value) { | |
| return "Metric" | |
| } | |
| const parts = value | |
| .split("/") | |
| .map((part) => part.trim()) | |
| .filter(Boolean) | |
| return parts[parts.length - 1] ?? value | |
| } | |
| function getRankBadgeClass(rank: number) { | |
| if (rank === 1) { | |
| return "border-amber-300 bg-amber-100 text-amber-800" | |
| } | |
| if (rank === 2) { | |
| return "border-slate-300 bg-slate-100 text-slate-700" | |
| } | |
| if (rank === 3) { | |
| return "border-orange-300 bg-orange-100 text-orange-800" | |
| } | |
| return "border-border bg-background text-foreground" | |
| } | |
| export function EvalDetail({ summary }: EvalDetailProps) { | |
| const { mode } = useAudienceMode() | |
| const isResearchView = mode === "research" | |
| const hasMultiMetricLeaderboard = | |
| (summary.leaderboard_metrics?.length ?? 0) > 1 && | |
| (summary.leaderboard_rows?.length ?? 0) > 0 | |
| const [overviewOpen, setOverviewOpen] = useState(true) | |
| // Collapse the dense technical overview by default in policy mode; expand | |
| // for researchers. Reset whenever the user switches modes. | |
| useEffect(() => { | |
| setOverviewOpen(isResearchView) | |
| }, [isResearchView]) | |
| const [expandedRows, setExpandedRows] = useState<Record<string, boolean>>({}) | |
| const [leaderboardPage, setLeaderboardPage] = useState(1) | |
| const [minParamStep, setMinParamStep] = useState(0) | |
| const [maxParamStep, setMaxParamStep] = useState(PARAM_RANGE_VALUES.length - 1) | |
| const maxScore = summary.metric_config.max_score ?? 1 | |
| const minScore = summary.metric_config.min_score ?? 0 | |
| const range = maxScore - minScore | |
| const normalizeScore = (raw: number) => (range > 0 ? (raw - minScore) / range : raw) | |
| const formatPercent = (normalized: number) => `${(normalized * 100).toFixed(1)}%` | |
| const maxParamStepIndex = PARAM_RANGE_VALUES.length - 1 | |
| const minHandlePercent = (minParamStep / maxParamStepIndex) * 100 | |
| const maxHandlePercent = (maxParamStep / maxParamStepIndex) * 100 | |
| const numericMinParams = useMemo(() => { | |
| if (minParamStep <= 0) { | |
| return null | |
| } | |
| return PARAM_RANGE_VALUES[minParamStep] ?? null | |
| }, [minParamStep]) | |
| const numericMaxParams = useMemo(() => { | |
| if (maxParamStep >= PARAM_RANGE_VALUES.length - 1) { | |
| return null | |
| } | |
| return PARAM_RANGE_VALUES[maxParamStep] ?? null | |
| }, [maxParamStep]) | |
| const sortedResults = useMemo( | |
| () => | |
| [...summary.model_results].sort((a, b) => | |
| summary.metric_config.lower_is_better ? a.score - b.score : b.score - a.score | |
| ), | |
| [summary.model_results, summary.metric_config.lower_is_better] | |
| ) | |
| const hasParameterData = useMemo( | |
| () => sortedResults.some((result) => getParamsBillions(result) != null), | |
| [sortedResults] | |
| ) | |
| const filteredResults = useMemo(() => { | |
| return sortedResults.filter((modelResult) => { | |
| const paramsBillions = getParamsBillions(modelResult) | |
| if (numericMinParams != null && (paramsBillions == null || paramsBillions < numericMinParams)) { | |
| return false | |
| } | |
| if (numericMaxParams != null && (paramsBillions == null || paramsBillions > numericMaxParams)) { | |
| return false | |
| } | |
| return true | |
| }) | |
| }, [numericMaxParams, numericMinParams, sortedResults]) | |
| const leaderboardRows = useMemo<LeaderboardRow[]>(() => { | |
| let currentRank = 0 | |
| let previousScore: number | null = null | |
| return filteredResults.map((modelResult, index) => { | |
| if (previousScore === null || Math.abs(modelResult.score - previousScore) > 1e-9) { | |
| currentRank = index + 1 | |
| previousScore = modelResult.score | |
| } | |
| return { | |
| key: `${modelResult.model_info.id}-${index}`, | |
| rank: currentRank, | |
| modelResult, | |
| normalizedScore: normalizeScore(modelResult.score), | |
| } | |
| }) | |
| }, [filteredResults]) | |
| const LEADERBOARD_PAGE_SIZE = 50 | |
| const pagedLeaderboardRows = useMemo( | |
| () => leaderboardRows.slice(0, leaderboardPage * LEADERBOARD_PAGE_SIZE), | |
| [leaderboardRows, leaderboardPage] | |
| ) | |
| const avgScoreLabel = formatRawScore(summary.avg_score, summary.metric_config.unit) | |
| const scoreDirectionLabel = summary.metric_config.lower_is_better ? "Lower scores rank higher" : "Higher scores rank higher" | |
| const leaderboardTitle = isResearchView ? "Leaderboard" : "Reporting Comparison" | |
| const sourceDatasetLabel = summary.source_data?.hf_repo ?? summary.source_data?.dataset_name ?? "Summary source" | |
| const instanceDataLabel = summary.instance_data?.available | |
| ? `${summary.instance_data.url_count.toLocaleString()} linked URL${summary.instance_data.url_count === 1 ? "" : "s"}` | |
| : "Not linked" | |
| const leaderboardDescription = isResearchView | |
| ? summary.is_aggregated | |
| ? "Models ranked by average raw score across the contributing composite benchmarks." | |
| : "Models ranked by raw score for this benchmark." | |
| : summary.is_aggregated | |
| ? "Averaged model results across the contributing composite benchmarks, with drill-down to each component score." | |
| : "Model results with benchmark context, source dataset detail, and optional instance-data links." | |
| const reportingCompleteness = summary.evalcards?.annotations?.reporting_completeness | |
| const benchmarkComparability = summary.evalcards?.annotations?.benchmark_comparability | |
| const documentationPopulatedCount = reportingCompleteness | |
| ? getCompletenessPopulatedCount(reportingCompleteness) | |
| : null | |
| const toggleRow = (key: string) => | |
| setExpandedRows((current) => ({ | |
| ...current, | |
| [key]: !current[key], | |
| })) | |
| return ( | |
| <div className="space-y-6"> | |
| {!isResearchView && <PolicyOverview summary={summary} />} | |
| <Card className="overflow-hidden"> | |
| <Collapsible open={overviewOpen} onOpenChange={setOverviewOpen}> | |
| <CollapsibleTrigger asChild> | |
| <button | |
| type="button" | |
| className="flex w-full items-center justify-between gap-4 border-b bg-muted/10 px-4 py-3 text-left transition-colors hover:bg-muted/15 sm:px-5" | |
| > | |
| <div className="min-w-0 space-y-1"> | |
| <div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| {isResearchView ? "Benchmark overview" : "Technical details"} | |
| </div> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| <span className="text-base font-semibold tracking-tight sm:text-lg">{summary.evaluation_name}</span> | |
| <Badge variant="secondary" className="font-normal"> | |
| {summary.models_count} models | |
| </Badge> | |
| <Badge variant="secondary" className="font-normal"> | |
| {hasMultiMetricLeaderboard | |
| ? `${summary.metrics_count ?? summary.leaderboard_metrics?.length ?? 1} measures` | |
| : `${summary.metrics_count ?? 1} ${(summary.metrics_count ?? 1) === 1 ? "measure" : "measures"}`} | |
| </Badge> | |
| {reportingCompleteness && ( | |
| <SignalTooltip | |
| content={`${documentationPopulatedCount} of ${reportingCompleteness.total_fields_evaluated} EvalCards documentation fields populated for this benchmark.`} | |
| > | |
| <Badge variant="outline" className="border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-900/50 dark:bg-emerald-950/30 dark:text-emerald-200"> | |
| Documentation {Math.round(reportingCompleteness.completeness_score * 100)}% | |
| </Badge> | |
| </SignalTooltip> | |
| )} | |
| </div> | |
| </div> | |
| {overviewOpen ? ( | |
| <ChevronUp className="h-4 w-4 shrink-0 text-muted-foreground" /> | |
| ) : ( | |
| <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" /> | |
| )} | |
| </button> | |
| </CollapsibleTrigger> | |
| <CollapsibleContent> | |
| <CardContent className="space-y-4 p-4 sm:p-5"> | |
| <div className="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between"> | |
| <div className="space-y-2.5"> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| <Badge variant="outline" className="border-border/60 bg-background/80 text-[11px] uppercase tracking-[0.18em]"> | |
| {summary.is_aggregated ? "Merged Benchmark" : "Single Benchmark"} | |
| </Badge> | |
| {summary.is_aggregated ? ( | |
| <Badge variant="secondary" className="font-normal"> | |
| {summary.aggregate_sources?.length ?? 0} composite benchmarks | |
| </Badge> | |
| ) : ( | |
| <Badge variant="secondary" className="font-normal"> | |
| Composite: {summary.composite_benchmark_name} | |
| </Badge> | |
| )} | |
| <Badge variant="secondary" className="font-normal capitalize"> | |
| {summary.metric_config.score_type} | |
| </Badge> | |
| <Badge variant="secondary" className="font-normal"> | |
| {summary.metric_config.lower_is_better ? "Lower is better" : "Higher is better"} | |
| </Badge> | |
| {summary.tags?.languages && summary.tags.languages.length > 0 && ( | |
| <Badge variant="secondary" className="font-normal"> | |
| {summary.tags.languages.join(", ")} | |
| </Badge> | |
| )} | |
| </div> | |
| <p className="max-w-3xl text-sm leading-6 text-muted-foreground"> | |
| {summary.metric_config.evaluation_description} | |
| </p> | |
| </div> | |
| <div className="grid w-full grid-cols-2 gap-2 xl:grid-cols-4"> | |
| <div className="rounded-xl border border-sky-200/80 bg-sky-50/80 px-3 py-2.5 dark:border-sky-900/40 dark:bg-sky-950/20"> | |
| <div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-700 dark:text-sky-200">Models</div> | |
| <div className="mt-1 text-xl font-semibold text-sky-950 dark:text-sky-50">{summary.models_count}</div> | |
| </div> | |
| <div className="rounded-xl border border-border/70 bg-muted/20 px-3 py-2.5"> | |
| <div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground"> | |
| {hasMultiMetricLeaderboard ? "Measures" : isResearchView ? "Avg score" : "Measures"} | |
| </div> | |
| <div className="mt-1 text-xl font-semibold"> | |
| {hasMultiMetricLeaderboard ? summary.metrics_count ?? summary.leaderboard_metrics?.length ?? 1 : isResearchView ? avgScoreLabel : summary.metrics_count ?? 1} | |
| </div> | |
| </div> | |
| <div className="rounded-xl border border-emerald-200/80 bg-emerald-50/80 px-3 py-2.5 dark:border-emerald-900/40 dark:bg-emerald-950/20"> | |
| <div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-emerald-700 dark:text-emerald-200"> | |
| {hasMultiMetricLeaderboard || !isResearchView ? "Source dataset" : "Top model"} | |
| </div> | |
| <div className="mt-1 text-sm font-semibold text-emerald-950 dark:text-emerald-50"> | |
| {hasMultiMetricLeaderboard || !isResearchView | |
| ? sourceDatasetLabel | |
| : summary.best_model?.name ?? "Unknown"} | |
| </div> | |
| {!hasMultiMetricLeaderboard && isResearchView && summary.best_model && ( | |
| <div className="mt-1 text-xs text-emerald-700/80 dark:text-emerald-200/80"> | |
| {formatRawScore(summary.best_model.score, summary.metric_config.unit)} | |
| </div> | |
| )} | |
| </div> | |
| <div className="rounded-xl border border-amber-200/80 bg-amber-50/80 px-3 py-2.5 dark:border-amber-900/40 dark:bg-amber-950/20"> | |
| <div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-700 dark:text-amber-200"> | |
| {hasMultiMetricLeaderboard || !isResearchView ? "Instance data" : "Bottom model"} | |
| </div> | |
| <div className="mt-1 text-sm font-semibold text-amber-950 dark:text-amber-50"> | |
| {hasMultiMetricLeaderboard || !isResearchView | |
| ? instanceDataLabel | |
| : summary.worst_model?.name ?? "Unknown"} | |
| </div> | |
| {!hasMultiMetricLeaderboard && isResearchView && summary.worst_model && ( | |
| <div className="mt-1 text-xs text-amber-700/80 dark:text-amber-200/80"> | |
| {formatRawScore(summary.worst_model.score, summary.metric_config.unit)} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="rounded-2xl border bg-muted/10 p-3.5"> | |
| <div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| {isResearchView ? "Metric specification" : "Reading context"} | |
| </div> | |
| <dl className="mt-3 grid gap-x-5 gap-y-3 text-sm sm:grid-cols-2 xl:grid-cols-5"> | |
| <div> | |
| <dt className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground"> | |
| Composite benchmark | |
| </dt> | |
| <dd className="mt-1 break-words font-medium"> | |
| {summary.is_aggregated | |
| ? summary.aggregate_sources?.map((source) => source.composite_benchmark_name).join(", ") || "Multiple composite benchmarks" | |
| : summary.composite_benchmark_name} | |
| </dd> | |
| </div> | |
| <div> | |
| <dt className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground"> | |
| {isResearchView ? "Single benchmark ID" : "What this covers"} | |
| </dt> | |
| <dd className="mt-1 break-words font-medium"> | |
| {isResearchView ? summary.evaluation_id : summary.metric_config.evaluation_description} | |
| </dd> | |
| </div> | |
| <div> | |
| <dt className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground"> | |
| {isResearchView ? "Score scale" : "How to read scores"} | |
| </dt> | |
| <dd className="mt-1 font-medium"> | |
| {isResearchView | |
| ? `${summary.metric_config.min_score ?? 0} - ${summary.metric_config.max_score ?? 1}` | |
| : scoreDirectionLabel} | |
| </dd> | |
| </div> | |
| {summary.tags?.domains && summary.tags.domains.length > 0 && ( | |
| <div> | |
| <dt className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">Domain tags</dt> | |
| <dd className="mt-1 font-medium capitalize"> | |
| {summary.tags.domains.slice(0, 2).join(", ")} | |
| {summary.tags.domains.length > 2 ? ` +${summary.tags.domains.length - 2} more` : ""} | |
| </dd> | |
| </div> | |
| )} | |
| <div> | |
| <dt className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground"> | |
| {isResearchView ? "Source dataset" : "Instance data"} | |
| </dt> | |
| <dd className="mt-1 font-medium"> | |
| {isResearchView ? sourceDatasetLabel : instanceDataLabel} | |
| </dd> | |
| </div> | |
| </dl> | |
| </div> | |
| <CompletenessPanel completeness={reportingCompleteness} /> | |
| <ComparabilityPanel | |
| comparability={benchmarkComparability} | |
| summary={summary.comparability_summary} | |
| /> | |
| {!hasMultiMetricLeaderboard && (summary.root_metrics?.length || summary.subtasks?.length) ? ( | |
| <section className="rounded-2xl border bg-muted/5 p-3.5"> | |
| <div className="space-y-1"> | |
| <div className="text-sm font-semibold">Benchmark structure</div> | |
| <div className="text-xs text-muted-foreground"> | |
| Benchmark-level summary metrics and subtask slices grouped in one compact section. | |
| </div> | |
| </div> | |
| {summary.root_metrics && summary.root_metrics.length > 0 && ( | |
| <div className="mt-4 space-y-2.5"> | |
| <div> | |
| <div className="text-sm font-semibold">Benchmark-level metrics</div> | |
| <div className="text-xs text-muted-foreground"> | |
| Benchmark summary metrics used in this evaluation view. | |
| </div> | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| {summary.root_metrics.map((metric) => ( | |
| <span | |
| key={metric.metric_summary_id} | |
| className="rounded-full border border-border/70 bg-background px-3 py-1.5 text-xs font-medium" | |
| title={metric.canonical_display_name || metric.display_name} | |
| > | |
| {getCompactMetricLabel(metric.display_name)} | |
| {typeof metric.top_score === "number" ? ` · ${formatRawScore(metric.top_score, metric.unit)}` : ""} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {summary.subtasks && summary.subtasks.length > 0 && ( | |
| <div className="mt-4 space-y-2.5"> | |
| <div className="text-sm font-semibold">Subtask breakdown</div> | |
| <div className="grid gap-3 lg:grid-cols-2"> | |
| {summary.subtasks.map((subtask) => ( | |
| <div key={subtask.subtask_key} className="rounded-xl border bg-background p-3.5"> | |
| <div className="font-semibold">{subtask.display_name || subtask.subtask_name}</div> | |
| <div className="mt-1 text-xs text-muted-foreground" title={subtask.canonical_display_name || subtask.display_name}> | |
| {subtask.canonical_display_name || subtask.display_name} | |
| </div> | |
| <div className="mt-3 flex flex-wrap gap-2"> | |
| {subtask.metrics.map((metric) => ( | |
| <span | |
| key={metric.metric_summary_id} | |
| className="rounded-full border border-border/70 bg-muted/20 px-2.5 py-1 text-[11px] font-medium" | |
| title={metric.canonical_display_name || metric.display_name} | |
| > | |
| {getCompactMetricLabel(metric.display_name)} | |
| {typeof metric.top_score === "number" ? ` · ${formatRawScore(metric.top_score, metric.unit)}` : ""} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </section> | |
| ) : null} | |
| {summary.benchmark_card && ( | |
| <BenchmarkCardCollapsible | |
| card={summary.benchmark_card} | |
| isResearchView={isResearchView} | |
| defaultOpen | |
| defaultRisksOpen={!isResearchView} | |
| knownIssues={getKnownIssues( | |
| summary.evaluation_name, | |
| summary.composite_benchmark_name, | |
| summary.composite_benchmark_key, | |
| summary.benchmark_family_key, | |
| summary.benchmark_leaf_key, | |
| summary.benchmark_card.benchmark_details?.name, | |
| )} | |
| /> | |
| )} | |
| </CardContent> | |
| </CollapsibleContent> | |
| </Collapsible> | |
| </Card> | |
| {hasMultiMetricLeaderboard ? ( | |
| <MultiMetricLeaderboard summary={summary} isResearchView={isResearchView} /> | |
| ) : ( | |
| <Card className="overflow-hidden"> | |
| <CardHeader className="border-b bg-muted/10 space-y-3"> | |
| <ApplesToApplesBanner | |
| summary={summary.comparability_summary} | |
| detailsAnchorId="comparability-panel" | |
| /> | |
| <div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between"> | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2"> | |
| <Medal className="h-5 w-5 text-primary" /> | |
| <CardTitle className="text-xl">{leaderboardTitle}</CardTitle> | |
| </div> | |
| <CardDescription>{leaderboardDescription}</CardDescription> | |
| </div> | |
| <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground"> | |
| <Badge variant="secondary"> | |
| {leaderboardRows.length === summary.models_count | |
| ? `${summary.models_count} models` | |
| : `${leaderboardRows.length} of ${summary.models_count} models`} | |
| </Badge> | |
| <Badge variant="outline">{scoreDirectionLabel}</Badge> | |
| {hasParameterData && (numericMinParams != null || numericMaxParams != null) && ( | |
| <Badge variant="outline"> | |
| Params {formatParamBoundLabel(minParamStep, "min")} to {formatParamBoundLabel(maxParamStep, "max")} | |
| </Badge> | |
| )} | |
| {isResearchView && ( | |
| <Badge variant="outline"> | |
| Scale {summary.metric_config.min_score ?? 0} - {summary.metric_config.max_score ?? 1} | |
| </Badge> | |
| )} | |
| </div> | |
| </div> | |
| </CardHeader> | |
| <CardContent className="p-0"> | |
| {hasParameterData && ( | |
| <div className="border-b bg-background px-5 py-4 sm:px-6"> | |
| <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"> | |
| <div className="space-y-1"> | |
| <div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground"> | |
| Parameter range | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Narrow the leaderboard to comparable model sizes. | |
| </div> | |
| </div> | |
| <div className="flex min-w-0 flex-1 items-center gap-4 lg:max-w-[40rem]"> | |
| <div className="min-w-0 flex-1"> | |
| <div className="mb-2 flex items-center justify-between text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground"> | |
| {PARAM_RANGE_MARKERS.map((marker) => ( | |
| <span key={marker.label} className="text-center"> | |
| {marker.label} | |
| </span> | |
| ))} | |
| </div> | |
| <div className="relative h-4"> | |
| <div className="absolute inset-x-1.5 top-1/2 h-[3px] -translate-y-1/2 rounded-full bg-border/80" /> | |
| <div className="absolute inset-x-1.5 top-1/2 h-[3px] -translate-y-1/2"> | |
| <div | |
| className="absolute inset-y-0 rounded-full bg-foreground transition-[left,right] duration-300 ease-[var(--ease-out-quint)]" | |
| style={{ | |
| left: `${minHandlePercent}%`, | |
| right: `${Math.max(100 - maxHandlePercent, 0)}%`, | |
| }} | |
| /> | |
| </div> | |
| <div className="absolute inset-x-1.5 top-1/2 -translate-y-1/2"> | |
| {PARAM_RANGE_VALUES.map((_, stepIndex) => ( | |
| <span | |
| key={`param-tick-${stepIndex}`} | |
| className="absolute top-0 h-2 w-px -translate-x-1/2 rounded-full bg-border" | |
| style={{ left: `${(stepIndex / maxParamStepIndex) * 100}%` }} | |
| aria-hidden="true" | |
| /> | |
| ))} | |
| </div> | |
| <input | |
| type="range" | |
| min={0} | |
| max={maxParamStepIndex} | |
| step={1} | |
| value={minParamStep} | |
| onChange={(event) => { | |
| const nextMin = Number(event.target.value) | |
| setMinParamStep(Math.min(nextMin, maxParamStep)) | |
| }} | |
| className="param-range-input" | |
| aria-label="Minimum parameter filter" | |
| /> | |
| <input | |
| type="range" | |
| min={0} | |
| max={maxParamStepIndex} | |
| step={1} | |
| value={maxParamStep} | |
| onChange={(event) => { | |
| const nextMax = Number(event.target.value) | |
| setMaxParamStep(Math.max(nextMax, minParamStep)) | |
| }} | |
| className="param-range-input" | |
| aria-label="Maximum parameter filter" | |
| /> | |
| </div> | |
| </div> | |
| <span className="shrink-0 text-[11px] text-muted-foreground"> | |
| {formatParamBoundLabel(minParamStep, "min")} to {formatParamBoundLabel(maxParamStep, "max")} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <Table className="min-w-[980px]"> | |
| <TableHeader> | |
| <TableRow className="hover:bg-transparent"> | |
| <TableHead className="w-20 px-4">Rank</TableHead> | |
| <TableHead className="min-w-[260px]">Model</TableHead> | |
| <TableHead className="hidden min-w-[180px] lg:table-cell"> | |
| {isResearchView ? "Developer" : "Provider"} | |
| </TableHead> | |
| <TableHead className="text-right">Score</TableHead> | |
| {isResearchView ? ( | |
| <TableHead className="hidden min-w-[220px] md:table-cell">Performance</TableHead> | |
| ) : ( | |
| <TableHead className="hidden min-w-[220px] md:table-cell">Source type</TableHead> | |
| )} | |
| <TableHead className="hidden min-w-[180px] xl:table-cell"> | |
| {isResearchView ? "Evaluator" : "Reporting Org"} | |
| </TableHead> | |
| <TableHead className="hidden min-w-[120px] lg:table-cell">Updated</TableHead> | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {pagedLeaderboardRows.map(({ key, rank, modelResult, normalizedScore }) => { | |
| const isExpanded = expandedRows[key] ?? false | |
| const subtasks = modelResult.score_details.details | |
| ? Object.entries(modelResult.score_details.details).filter(([, value]) => typeof value === "number") | |
| : [] | |
| const hasExpandableDetails = | |
| isResearchView || | |
| (modelResult.aggregate_components && modelResult.aggregate_components.length > 1) || | |
| subtasks.length > 1 | |
| const datasetName = Array.isArray(modelResult.source_data) | |
| ? undefined | |
| : modelResult.source_data.dataset_name | |
| const samples = Array.isArray(modelResult.source_data) | |
| ? undefined | |
| : modelResult.source_data.samples_number | |
| const rowAnnotations = modelResult.result.evalcards?.annotations | |
| return ( | |
| <Fragment key={key}> | |
| <TableRow | |
| id={modelResult.model_route_id ? `row-${modelResult.model_route_id}` : undefined} | |
| className={cn("group", isExpanded && "bg-muted/15")} | |
| > | |
| <TableCell className="px-4"> | |
| <div | |
| className={cn( | |
| "inline-flex h-9 w-9 items-center justify-center rounded-full border text-sm font-semibold", | |
| getRankBadgeClass(rank) | |
| )} | |
| > | |
| {rank} | |
| </div> | |
| </TableCell> | |
| <TableCell className="whitespace-normal"> | |
| <div className="space-y-1"> | |
| <div className="flex items-center gap-1.5 font-semibold leading-tight"> | |
| {hasExpandableDetails && ( | |
| <button | |
| type="button" | |
| onClick={() => toggleRow(key)} | |
| aria-label={isExpanded ? "Collapse details" : "Expand details"} | |
| aria-expanded={isExpanded} | |
| className="-ml-1 inline-flex h-5 w-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" | |
| > | |
| {isExpanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />} | |
| </button> | |
| )} | |
| <Link | |
| href={`/models/${getModelFamilyRouteId(modelResult.model_info)}`} | |
| className="underline decoration-dotted underline-offset-4 hover:text-primary" | |
| > | |
| {modelResult.model_info.name} | |
| </Link> | |
| </div> | |
| <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground"> | |
| {modelResult.model_info.parameter_count && ( | |
| <Badge variant="secondary" className="font-normal"> | |
| {modelResult.model_info.parameter_count} | |
| </Badge> | |
| )} | |
| {modelResult.model_info.architecture && ( | |
| <Badge variant="outline" className="font-normal"> | |
| {modelResult.model_info.architecture} | |
| </Badge> | |
| )} | |
| <span className="lg:hidden"> | |
| {modelResult.model_info.developer ?? "Unknown developer"} | |
| </span> | |
| {modelResult.aggregate_components && modelResult.aggregate_components.length > 1 && ( | |
| <Badge variant="outline" className="font-normal"> | |
| Avg of {modelResult.aggregate_components.length} | |
| </Badge> | |
| )} | |
| </div> | |
| </div> | |
| </TableCell> | |
| <TableCell className="hidden whitespace-normal lg:table-cell"> | |
| <div className="max-w-[180px] truncate text-sm text-muted-foreground"> | |
| {modelResult.model_info.developer ?? "Unknown developer"} | |
| </div> | |
| </TableCell> | |
| <TableCell className="text-right"> | |
| <div className="flex items-center justify-end gap-1.5"> | |
| <div className="text-xl font-semibold tabular-nums">{formatRawScore(modelResult.score, summary.metric_config.unit)}</div> | |
| <RowSignalsCompact annotations={rowAnnotations} /> | |
| </div> | |
| </TableCell> | |
| {isResearchView ? ( | |
| <TableCell className="hidden md:table-cell"> | |
| <div className="min-w-[220px]"> | |
| <Progress value={normalizedScore * 100} className="h-2" /> | |
| </div> | |
| </TableCell> | |
| ) : ( | |
| <TableCell className="hidden md:table-cell"> | |
| <div className="text-sm text-muted-foreground capitalize"> | |
| {modelResult.aggregate_components && modelResult.aggregate_components.length > 1 | |
| ? `average of ${modelResult.aggregate_components.length} composite scores` | |
| : datasetName ?? "Detailed result source"} | |
| </div> | |
| </TableCell> | |
| )} | |
| <TableCell className="hidden whitespace-normal xl:table-cell"> | |
| {modelResult.aggregate_components && modelResult.aggregate_components.length > 1 ? ( | |
| <div className="space-y-1"> | |
| <div className="font-medium"> | |
| {Array.from(new Set(modelResult.aggregate_components.map((component) => component.source_organization_name))).join(", ")} | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| {modelResult.aggregate_components | |
| .map((component) => component.composite_benchmark_name) | |
| .join(", ")} | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="space-y-1"> | |
| <div className="font-medium">{datasetName ?? sourceDatasetLabel}</div> | |
| <div className="text-xs text-muted-foreground"> | |
| {Array.isArray(modelResult.source_data) | |
| ? "Detailed result source" | |
| : modelResult.source_data.hf_repo ?? modelResult.source_data.source_type ?? "Detailed result source"} | |
| </div> | |
| </div> | |
| )} | |
| </TableCell> | |
| <TableCell className="hidden lg:table-cell"> | |
| <div className="text-sm text-muted-foreground">{formatDate(modelResult.evaluation_timestamp)}</div> | |
| </TableCell> | |
| </TableRow> | |
| {isExpanded && ( | |
| <TableRow className="hover:bg-transparent"> | |
| <TableCell colSpan={7} className="bg-muted/10 px-0 py-0"> | |
| <div className="space-y-5 px-4 py-5 sm:px-6"> | |
| <div className="grid gap-4 xl:grid-cols-3"> | |
| <DetailPanel | |
| title={isResearchView ? "Model Profile" : "System Overview"} | |
| subtitle={ | |
| isResearchView | |
| ? "Model metadata for the ranked entry." | |
| : "Basic system information for this reported result." | |
| } | |
| > | |
| {isResearchView && <MetaRow label="Model ID" value={modelResult.model_info.id} />} | |
| <MetaRow label="Developer" value={modelResult.model_info.developer ?? "Unknown"} /> | |
| <MetaRow label="Release Date" value={modelResult.model_info.release_date ?? "Unknown"} /> | |
| <MetaRow label="Architecture" value={modelResult.model_info.architecture ?? "Unknown"} /> | |
| <MetaRow label="Parameter Count" value={modelResult.model_info.parameter_count ?? "Unknown"} /> | |
| <MetaRow label="Inference Engine" value={modelResult.model_info.inference_engine ?? "Unknown"} /> | |
| </DetailPanel> | |
| <DetailPanel | |
| title={isResearchView ? "Evaluation Provenance" : "Source & Accountability"} | |
| subtitle={ | |
| isResearchView | |
| ? "Who ran the evaluation and what dataset was used." | |
| : "Reporting organization, relationship, and dataset context." | |
| } | |
| > | |
| <MetaRow | |
| label="Organization" | |
| value={modelResult.source_metadata.source_organization_name} | |
| /> | |
| <MetaRow | |
| label="Relationship" | |
| value={modelResult.source_metadata.evaluator_relationship.replace(/_/g, " ")} | |
| /> | |
| <MetaRow label="Source Type" value={modelResult.source_metadata.source_type} /> | |
| <MetaRow label="Dataset" value={datasetName ?? "Not specified"} /> | |
| <MetaRow | |
| label="Samples" | |
| value={samples != null ? samples.toLocaleString() : "Unknown"} | |
| /> | |
| <MetaRow | |
| label="Published" | |
| value={ | |
| modelResult.source_metadata.publication_date | |
| ? formatDate(modelResult.source_metadata.publication_date) | |
| : formatDate(modelResult.evaluation_timestamp) | |
| } | |
| /> | |
| {modelResult.source_metadata.source_url && ( | |
| <MetaRow | |
| label="Source URL" | |
| value={ | |
| <a | |
| className="inline-flex items-center gap-1 text-primary underline-offset-4 hover:underline" | |
| href={modelResult.source_metadata.source_url} | |
| target="_blank" | |
| rel="noreferrer" | |
| > | |
| View source | |
| <ExternalLink className="h-3.5 w-3.5" /> | |
| </a> | |
| } | |
| /> | |
| )} | |
| </DetailPanel> | |
| {!isResearchView && ( | |
| <ReproducibilityPanel gap={rowAnnotations?.reproducibility_gap} /> | |
| )} | |
| <DetailPanel | |
| title={isResearchView ? "Score Breakdown" : "Metric Summary"} | |
| subtitle={ | |
| isResearchView | |
| ? "Raw score and scale." | |
| : "Raw performance plus uncertainty and sample details." | |
| } | |
| > | |
| <MetaRow | |
| label={modelResult.aggregate_components ? "Average Raw Score" : "Raw Score"} | |
| value={formatRawScore(modelResult.score, summary.metric_config.unit)} | |
| /> | |
| <MetaRow label="Score Type" value={modelResult.result.metric_config.score_type} /> | |
| <MetaRow label="Range" value={`${minScore} - ${maxScore}`} /> | |
| {!isResearchView && ( | |
| <> | |
| <MetaRow | |
| label="Sample Size" | |
| value={modelResult.score_details.sample_size ?? "Unknown"} | |
| /> | |
| <MetaRow | |
| label="Standard Error" | |
| value={modelResult.score_details.standard_error ?? "Unknown"} | |
| /> | |
| {modelResult.score_details.confidence_interval && ( | |
| <MetaRow | |
| label="Confidence Interval" | |
| value={`${modelResult.score_details.confidence_interval.lower} - ${modelResult.score_details.confidence_interval.upper} (${modelResult.score_details.confidence_interval.confidence_level}%)`} | |
| /> | |
| )} | |
| </> | |
| )} | |
| </DetailPanel> | |
| </div> | |
| {modelResult.aggregate_components && modelResult.aggregate_components.length > 1 && ( | |
| <div className="space-y-2"> | |
| <div className="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| Composite Score Breakdown | |
| </div> | |
| <div className="overflow-hidden rounded-xl border"> | |
| <table className="w-full text-sm"> | |
| <thead> | |
| <tr className="border-b bg-muted/30"> | |
| <th className="px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Benchmark</th> | |
| <th className="px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Source</th> | |
| <th className="px-3 py-2 text-right text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Raw</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {modelResult.aggregate_components.map((component, i) => ( | |
| <tr key={`${component.evaluation_id}-${i}`} className="border-b last:border-0 hover:bg-muted/10"> | |
| <td className="px-3 py-2 font-medium">{component.composite_benchmark_name}</td> | |
| <td className="px-3 py-2 text-muted-foreground">{component.source_organization_name}</td> | |
| <td className="px-3 py-2 text-right tabular-nums text-muted-foreground">{formatRawScore(component.score)}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| {subtasks.length > 1 && ( | |
| <div className="space-y-2"> | |
| <div className="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| Subtask Breakdown | |
| </div> | |
| <div className="overflow-hidden rounded-xl border"> | |
| <table className="w-full text-sm"> | |
| <thead> | |
| <tr className="border-b bg-muted/30"> | |
| <th className="px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Subtask</th> | |
| <th className="px-3 py-2 text-right text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Raw</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {subtasks.map(([subtaskName, value]) => { | |
| const numericValue = value as number | |
| return ( | |
| <tr key={subtaskName} className="border-b last:border-0 hover:bg-muted/10"> | |
| <td className="px-3 py-2 font-medium capitalize">{subtaskName.replace(/_/g, " ")}</td> | |
| <td className="px-3 py-2 text-right tabular-nums text-muted-foreground">{formatRawScore(numericValue, summary.metric_config.unit)}</td> | |
| </tr> | |
| ) | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| {isResearchView ? ( | |
| <div className="space-y-3"> | |
| <ResearcherReproducibilityCard | |
| modelResult={modelResult} | |
| benchmarkKey={summary.benchmark_leaf_key ?? summary.composite_benchmark_key} | |
| evalName={summary.evaluation_name} | |
| /> | |
| <div className="flex justify-end"> | |
| <FlagScoreButton | |
| modelName={modelResult.model_info.name} | |
| modelId={modelResult.model_info.id} | |
| benchmarkName={summary.evaluation_name} | |
| benchmarkId={summary.evaluation_id} | |
| score={formatRawScore(modelResult.score, summary.metric_config.unit)} | |
| sourceUrl={modelResult.source_metadata.source_url} | |
| sourceRecordUrl={modelResult.source_record_url} | |
| /> | |
| </div> | |
| </div> | |
| ) : ( | |
| modelResult.result.generation_config && ( | |
| <div className="space-y-3"> | |
| <div> | |
| <div className="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| Generation Config | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Evaluation-time generation parameters. | |
| </div> | |
| </div> | |
| <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3"> | |
| {modelResult.result.generation_config.generation_args && | |
| Object.entries(modelResult.result.generation_config.generation_args).map(([key, value]) => ( | |
| <div key={key} className="rounded-xl border bg-background/70 p-4"> | |
| <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground"> | |
| {key.replace(/_/g, " ")} | |
| </div> | |
| <div className="mt-2 text-sm font-medium"> | |
| {formatMetadataValue(value)} | |
| </div> | |
| </div> | |
| ))} | |
| {modelResult.result.generation_config.additional_details && ( | |
| <div className="rounded-xl border bg-background/70 p-4 md:col-span-2 xl:col-span-3"> | |
| <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground"> | |
| Additional Details | |
| </div> | |
| <div className="mt-2 text-sm font-medium whitespace-pre-wrap"> | |
| {formatMetadataValue(modelResult.result.generation_config.additional_details)} | |
| </div> | |
| </div> | |
| )} | |
| {modelResult.result.generation_config.prompt_template && ( | |
| <div className="rounded-xl border bg-background/70 p-4 md:col-span-2 xl:col-span-3"> | |
| <div className="text-xs uppercase tracking-[0.16em] text-muted-foreground"> | |
| Prompt Template | |
| </div> | |
| <div className="mt-2 text-sm font-medium whitespace-pre-wrap"> | |
| {formatMetadataValue(modelResult.result.generation_config.prompt_template)} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| )} | |
| </div> | |
| </TableCell> | |
| </TableRow> | |
| )} | |
| </Fragment> | |
| ) | |
| })} | |
| {leaderboardRows.length === 0 && ( | |
| <TableRow> | |
| <TableCell colSpan={7} className="px-6 py-12 text-center text-sm text-muted-foreground"> | |
| No leaderboard entries match the selected parameter range. | |
| </TableCell> | |
| </TableRow> | |
| )} | |
| </TableBody> | |
| </Table> | |
| {pagedLeaderboardRows.length < leaderboardRows.length && ( | |
| <div className="border-t px-6 py-4 text-center"> | |
| <Button | |
| variant="outline" | |
| onClick={() => setLeaderboardPage((p) => p + 1)} | |
| > | |
| Load more ({leaderboardRows.length - pagedLeaderboardRows.length} remaining) | |
| </Button> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| )} | |
| </div> | |
| ) | |
| } | |
| function MultiMetricLeaderboard({ | |
| summary, | |
| isResearchView, | |
| }: { | |
| summary: BenchmarkEvalSummary | |
| isResearchView: boolean | |
| }) { | |
| const [page, setPage] = useState(1) | |
| const [sortKey, setSortKey] = useState<string>("coverage") | |
| const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc") | |
| const [activeSubtaskTab, setActiveSubtaskTab] = useState<string>("all") | |
| const [minParamStep, setMinParamStep] = useState(0) | |
| const [maxParamStep, setMaxParamStep] = useState(PARAM_RANGE_VALUES.length - 1) | |
| const [expandedRows, setExpandedRows] = useState<Record<string, boolean>>({}) | |
| // Index ModelResultForBenchmark entries by model_info.id so we can power the | |
| // research-mode reproducibility card from a multi-metric row. There may be | |
| // several entries per model (one per metric); we prefer one with a recorded | |
| // generation_config so the card has the most data to show. | |
| const modelResultByModelId = useMemo(() => { | |
| const map = new Map<string, ModelResultForBenchmark>() | |
| for (const result of summary.model_results) { | |
| const id = result.model_info.id | |
| const existing = map.get(id) | |
| if (!existing) { | |
| map.set(id, result) | |
| continue | |
| } | |
| const existingHasGen = existing.result.generation_config != null | |
| const candidateHasGen = result.result.generation_config != null | |
| if (!existingHasGen && candidateHasGen) { | |
| map.set(id, result) | |
| } | |
| } | |
| return map | |
| }, [summary.model_results]) | |
| const toggleExpandedRow = (key: string) => | |
| setExpandedRows((current) => ({ ...current, [key]: !current[key] })) | |
| const leaderboardMetrics = summary.leaderboard_metrics ?? [] | |
| const leaderboardRows = summary.leaderboard_rows ?? [] | |
| const allMetricKeys = useMemo(() => leaderboardMetrics.map((metric) => metric.column_key), [leaderboardMetrics]) | |
| // Cap default visible columns to avoid hangs on benchmarks with hundreds of metrics | |
| // (e.g. helm_air_bench has 374 subtask×metric pairs). Users can opt in to more. | |
| const DEFAULT_VISIBLE_METRIC_CAP = 24 | |
| const defaultVisibleMetricKeys = useMemo( | |
| () => allMetricKeys.slice(0, DEFAULT_VISIBLE_METRIC_CAP), | |
| [allMetricKeys] | |
| ) | |
| const [visibleMetricKeys, setVisibleMetricKeys] = useState<string[]>(() => defaultVisibleMetricKeys) | |
| const maxParamStepIndex = PARAM_RANGE_VALUES.length - 1 | |
| const leaderboardMetricMap = useMemo( | |
| () => new Map(leaderboardMetrics.map((metric) => [metric.column_key, metric])), | |
| [leaderboardMetrics] | |
| ) | |
| const visibleMetricKeySet = useMemo(() => new Set(visibleMetricKeys), [visibleMetricKeys]) | |
| const subtaskMetricCounts = useMemo(() => { | |
| const counts = new Map<string, number>() | |
| for (const metric of leaderboardMetrics) { | |
| if (metric.scope === "subtask" && metric.subtask_key) { | |
| counts.set(metric.subtask_key, (counts.get(metric.subtask_key) ?? 0) + 1) | |
| } | |
| } | |
| return counts | |
| }, [leaderboardMetrics]) | |
| const singleMetricSubtaskTabs = useMemo(() => { | |
| return leaderboardMetrics | |
| .filter((metric) => metric.scope === "subtask" && metric.subtask_key && subtaskMetricCounts.get(metric.subtask_key) === 1) | |
| .map((metric) => ({ | |
| key: metric.subtask_key as string, | |
| label: metric.subtask_name ?? getCompactMetricLabel(metric.display_name), | |
| })) | |
| }, [leaderboardMetrics, subtaskMetricCounts]) | |
| const hasSubtaskTabs = singleMetricSubtaskTabs.length > 1 | |
| const visibleMetrics = useMemo( | |
| () => | |
| leaderboardMetrics.filter((metric) => { | |
| if (!visibleMetricKeySet.has(metric.column_key)) { | |
| return false | |
| } | |
| if (!hasSubtaskTabs || activeSubtaskTab === "all") { | |
| return true | |
| } | |
| return metric.scope === "subtask" && metric.subtask_key === activeSubtaskTab | |
| }), | |
| [activeSubtaskTab, hasSubtaskTabs, leaderboardMetrics, visibleMetricKeySet] | |
| ) | |
| const visibleMetricColumnKeySet = useMemo( | |
| () => new Set(visibleMetrics.map((metric) => metric.column_key)), | |
| [visibleMetrics] | |
| ) | |
| const numericMinParams = useMemo(() => { | |
| if (minParamStep <= 0) { | |
| return null | |
| } | |
| return PARAM_RANGE_VALUES[minParamStep] ?? null | |
| }, [minParamStep]) | |
| const numericMaxParams = useMemo(() => { | |
| if (maxParamStep >= PARAM_RANGE_VALUES.length - 1) { | |
| return null | |
| } | |
| return PARAM_RANGE_VALUES[maxParamStep] ?? null | |
| }, [maxParamStep]) | |
| const filteredRows = useMemo(() => { | |
| return leaderboardRows | |
| .filter((row) => { | |
| const paramsBillions = getParamsBillionsFromModelInfo(row.model_info) | |
| if (numericMinParams != null && (paramsBillions == null || paramsBillions < numericMinParams)) { | |
| return false | |
| } | |
| if (numericMaxParams != null && (paramsBillions == null || paramsBillions > numericMaxParams)) { | |
| return false | |
| } | |
| return true | |
| }) | |
| }, [leaderboardRows, numericMaxParams, numericMinParams]) | |
| const sortedRows = useMemo(() => { | |
| const rows = [...filteredRows] | |
| const compareNames = (left: LeaderboardMatrixRow, right: LeaderboardMatrixRow) => | |
| left.model_info.name.localeCompare(right.model_info.name) || | |
| (left.model_info.developer ?? "").localeCompare(right.model_info.developer ?? "") | |
| const compareTimestamps = (left: string, right: string) => { | |
| const leftNumeric = Number(left) | |
| const rightNumeric = Number(right) | |
| const leftTimestamp = !Number.isNaN(leftNumeric) && !left.includes("-") | |
| ? leftNumeric * 1000 | |
| : new Date(left).getTime() | |
| const rightTimestamp = !Number.isNaN(rightNumeric) && !right.includes("-") | |
| ? rightNumeric * 1000 | |
| : new Date(right).getTime() | |
| return leftTimestamp - rightTimestamp | |
| } | |
| rows.sort((left, right) => { | |
| if (sortKey === "model") { | |
| const comparison = compareNames(left, right) | |
| return sortDirection === "asc" ? comparison : -comparison | |
| } | |
| if (sortKey === "developer") { | |
| const comparison = | |
| (left.model_info.developer ?? "").localeCompare(right.model_info.developer ?? "") || compareNames(left, right) | |
| return sortDirection === "asc" ? comparison : -comparison | |
| } | |
| if (sortKey === "coverage") { | |
| const comparison = left.metrics_present - right.metrics_present || compareNames(left, right) | |
| return sortDirection === "asc" ? comparison : -comparison | |
| } | |
| if (sortKey === "updated") { | |
| const comparison = compareTimestamps(left.evaluation_timestamp, right.evaluation_timestamp) || compareNames(left, right) | |
| return sortDirection === "asc" ? comparison : -comparison | |
| } | |
| const metric = leaderboardMetricMap.get(sortKey) | |
| if (metric) { | |
| const leftValue = left.values[sortKey] | |
| const rightValue = right.values[sortKey] | |
| const leftHasValue = isNumericScore(leftValue) | |
| const rightHasValue = isNumericScore(rightValue) | |
| if (leftHasValue && rightHasValue) { | |
| const comparison = leftValue - rightValue || compareNames(left, right) | |
| return sortDirection === "asc" ? comparison : -comparison | |
| } | |
| if (leftHasValue !== rightHasValue) { | |
| return leftHasValue ? -1 : 1 | |
| } | |
| } | |
| return compareNames(left, right) | |
| }) | |
| return rows | |
| }, [filteredRows, leaderboardMetricMap, sortDirection, sortKey]) | |
| useEffect(() => { | |
| setPage(1) | |
| }, [maxParamStep, minParamStep, sortDirection, sortKey]) | |
| useEffect(() => { | |
| setVisibleMetricKeys(defaultVisibleMetricKeys) | |
| }, [defaultVisibleMetricKeys, summary.evaluation_id]) | |
| useEffect(() => { | |
| setActiveSubtaskTab("all") | |
| }, [summary.evaluation_id]) | |
| useEffect(() => { | |
| if (leaderboardMetricMap.has(sortKey) && !visibleMetricColumnKeySet.has(sortKey)) { | |
| setSortKey("coverage") | |
| setSortDirection("desc") | |
| } | |
| }, [leaderboardMetricMap, sortKey, visibleMetricColumnKeySet]) | |
| useEffect(() => { | |
| if (!hasSubtaskTabs) { | |
| if (activeSubtaskTab !== "all") { | |
| setActiveSubtaskTab("all") | |
| } | |
| return | |
| } | |
| if (activeSubtaskTab === "all") { | |
| return | |
| } | |
| if (!singleMetricSubtaskTabs.some((tab) => tab.key === activeSubtaskTab)) { | |
| setActiveSubtaskTab("all") | |
| } | |
| }, [activeSubtaskTab, hasSubtaskTabs, singleMetricSubtaskTabs]) | |
| const hasParameterData = useMemo( | |
| () => leaderboardRows.some((row) => getParamsBillionsFromModelInfo(row.model_info) != null), | |
| [leaderboardRows] | |
| ) | |
| const metricRanges = useMemo(() => { | |
| const ranges = new Map<string, { min: number; max: number }>() | |
| for (const metric of leaderboardMetrics) { | |
| const scores = filteredRows | |
| .map((row) => row.values[metric.column_key]) | |
| .filter(isNumericScore) | |
| if (scores.length > 0) { | |
| ranges.set(metric.column_key, { | |
| min: Math.min(...scores), | |
| max: Math.max(...scores), | |
| }) | |
| } | |
| } | |
| return ranges | |
| }, [filteredRows, leaderboardMetrics]) | |
| const pagedRows = useMemo( | |
| () => sortedRows.slice(0, page * 50), | |
| [page, sortedRows] | |
| ) | |
| const rankByModelId = useMemo( | |
| () => new Map(sortedRows.map((row, index) => [row.model_info.id, index + 1])), | |
| [sortedRows] | |
| ) | |
| const setMetricVisibility = (metricKey: string, nextVisible: boolean) => { | |
| setVisibleMetricKeys((current) => { | |
| if (nextVisible) { | |
| return allMetricKeys.filter((key) => key === metricKey || current.includes(key)) | |
| } | |
| return current.filter((key) => key !== metricKey) | |
| }) | |
| } | |
| const getVisibleMetricCount = (row: LeaderboardMatrixRow) => | |
| visibleMetrics.reduce( | |
| (count, metric) => count + (isNumericScore(row.values[metric.column_key]) ? 1 : 0), | |
| 0 | |
| ) | |
| const getDefaultSortDirection = (key: string): "asc" | "desc" => { | |
| if (key === "model" || key === "developer") { | |
| return "asc" | |
| } | |
| if (key === "updated" || key === "coverage") { | |
| return "desc" | |
| } | |
| return leaderboardMetricMap.get(key)?.lower_is_better ? "asc" : "desc" | |
| } | |
| const handleSort = (key: string) => { | |
| if (sortKey === key) { | |
| setSortDirection((current) => (current === "asc" ? "desc" : "asc")) | |
| return | |
| } | |
| setSortKey(key) | |
| setSortDirection(getDefaultSortDirection(key)) | |
| } | |
| const getSortIndicator = (key: string) => { | |
| if (sortKey !== key) { | |
| return "" | |
| } | |
| return sortDirection === "asc" ? " ▲" : " ▼" | |
| } | |
| return ( | |
| <Card className="overflow-hidden"> | |
| <CardHeader className="border-b bg-muted/10 space-y-3"> | |
| <ApplesToApplesBanner | |
| summary={summary.comparability_summary} | |
| detailsAnchorId="comparability-panel" | |
| /> | |
| <div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between"> | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2"> | |
| <BarChart3 className="h-5 w-5 text-primary" /> | |
| <CardTitle className="text-xl">{isResearchView ? "Leaderboard" : "Reporting Comparison"}</CardTitle> | |
| </div> | |
| <CardDescription> | |
| {isResearchView | |
| ? "Each column is a reported benchmark measure. Distinct measures stay separate instead of collapsing into a single raw score." | |
| : "Each column is a separately reported measure so the benchmark can be read without flattening different results into one number."} | |
| </CardDescription> | |
| </div> | |
| <div className="flex flex-wrap items-center justify-end gap-2 text-xs text-muted-foreground"> | |
| <Badge variant="secondary"> | |
| {filteredRows.length === leaderboardRows.length | |
| ? `${leaderboardRows.length} models` | |
| : `${filteredRows.length} of ${leaderboardRows.length} models`} | |
| </Badge> | |
| <Badge variant="outline"> | |
| {visibleMetrics.length === leaderboardMetrics.length | |
| ? `${leaderboardMetrics.length} measures` | |
| : `${visibleMetrics.length} of ${leaderboardMetrics.length} measures`} | |
| </Badge> | |
| {hasParameterData && (numericMinParams != null || numericMaxParams != null) && ( | |
| <Badge variant="outline"> | |
| Params {formatParamBoundLabel(minParamStep, "min")} to {formatParamBoundLabel(maxParamStep, "max")} | |
| </Badge> | |
| )} | |
| <DropdownMenu> | |
| <DropdownMenuTrigger asChild> | |
| <Button variant="outline" size="sm" className="h-8 gap-2"> | |
| <SlidersHorizontal className="h-3.5 w-3.5" /> | |
| Columns | |
| </Button> | |
| </DropdownMenuTrigger> | |
| <DropdownMenuContent align="end" className="w-80"> | |
| <DropdownMenuLabel>Visible measure columns</DropdownMenuLabel> | |
| <DropdownMenuItem onSelect={() => setVisibleMetricKeys(allMetricKeys)}> | |
| Show all | |
| </DropdownMenuItem> | |
| <DropdownMenuSeparator /> | |
| {leaderboardMetrics.map((metric) => { | |
| const isVisible = visibleMetricKeySet.has(metric.column_key) | |
| const isLastVisible = isVisible && visibleMetrics.length === 1 | |
| const visibleLabel = metric.scope === "subtask" && metric.subtask_key && subtaskMetricCounts.get(metric.subtask_key) === 1 && metric.subtask_name | |
| ? metric.subtask_name | |
| : getCompactMetricLabel(metric.display_name) | |
| return ( | |
| <DropdownMenuCheckboxItem | |
| key={metric.column_key} | |
| checked={isVisible} | |
| disabled={isLastVisible} | |
| onCheckedChange={(checked) => setMetricVisibility(metric.column_key, checked === true)} | |
| className="items-start" | |
| > | |
| <div className="flex min-w-0 flex-col gap-0.5"> | |
| <span className="font-medium leading-tight text-foreground">{visibleLabel}</span> | |
| <span className="text-xs leading-tight text-muted-foreground">{describeLeaderboardMetric(metric)}</span> | |
| </div> | |
| </DropdownMenuCheckboxItem> | |
| ) | |
| })} | |
| </DropdownMenuContent> | |
| </DropdownMenu> | |
| </div> | |
| </div> | |
| </CardHeader> | |
| <CardContent className="p-0"> | |
| {hasSubtaskTabs && ( | |
| <div className="border-b bg-background px-5 py-3 sm:px-6"> | |
| <SliceSelector | |
| activeSubtaskTab={activeSubtaskTab} | |
| onChange={setActiveSubtaskTab} | |
| tabs={singleMetricSubtaskTabs} | |
| /> | |
| </div> | |
| )} | |
| {hasParameterData && ( | |
| <div className="border-b bg-background px-5 py-4 sm:px-6"> | |
| <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"> | |
| <div className="space-y-1"> | |
| <div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground"> | |
| Parameter range | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Narrow the matrix to comparable model sizes. | |
| </div> | |
| </div> | |
| <div className="flex min-w-0 flex-1 items-center gap-4 lg:max-w-[40rem]"> | |
| <div className="min-w-0 flex-1"> | |
| <div className="mb-2 flex items-center justify-between text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground"> | |
| {PARAM_RANGE_MARKERS.map((marker) => ( | |
| <span key={marker.label} className="text-center"> | |
| {marker.label} | |
| </span> | |
| ))} | |
| </div> | |
| <div className="relative h-4"> | |
| <div className="absolute inset-x-1.5 top-1/2 h-[3px] -translate-y-1/2 rounded-full bg-border/80" /> | |
| <div className="absolute inset-x-1.5 top-1/2 h-[3px] -translate-y-1/2"> | |
| <div | |
| className="absolute inset-y-0 rounded-full bg-foreground transition-[left,right] duration-300 ease-[var(--ease-out-quint)]" | |
| style={{ | |
| left: `${(minParamStep / maxParamStepIndex) * 100}%`, | |
| right: `${Math.max(100 - (maxParamStep / maxParamStepIndex) * 100, 0)}%`, | |
| }} | |
| /> | |
| </div> | |
| <div className="absolute inset-x-1.5 top-1/2 -translate-y-1/2"> | |
| {PARAM_RANGE_VALUES.map((_, stepIndex) => ( | |
| <span | |
| key={`param-matrix-tick-${stepIndex}`} | |
| className="absolute top-0 h-2 w-px -translate-x-1/2 rounded-full bg-border" | |
| style={{ left: `${(stepIndex / maxParamStepIndex) * 100}%` }} | |
| aria-hidden="true" | |
| /> | |
| ))} | |
| </div> | |
| <input | |
| type="range" | |
| min={0} | |
| max={maxParamStepIndex} | |
| step={1} | |
| value={minParamStep} | |
| onChange={(event) => { | |
| const nextMin = Number(event.target.value) | |
| setMinParamStep(Math.min(nextMin, maxParamStep)) | |
| }} | |
| className="param-range-input" | |
| aria-label="Minimum parameter filter" | |
| /> | |
| <input | |
| type="range" | |
| min={0} | |
| max={maxParamStepIndex} | |
| step={1} | |
| value={maxParamStep} | |
| onChange={(event) => { | |
| const nextMax = Number(event.target.value) | |
| setMaxParamStep(Math.max(nextMax, minParamStep)) | |
| }} | |
| className="param-range-input" | |
| aria-label="Maximum parameter filter" | |
| /> | |
| </div> | |
| </div> | |
| <span className="shrink-0 text-[11px] text-muted-foreground"> | |
| {formatParamBoundLabel(minParamStep, "min")} to {formatParamBoundLabel(maxParamStep, "max")} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div className="overflow-x-auto"> | |
| <Table className="min-w-[1080px]"> | |
| <TableHeader> | |
| <TableRow className="hover:bg-transparent"> | |
| <TableHead className="w-20 px-4">Rank</TableHead> | |
| <TableHead className="min-w-[260px] px-4"> | |
| <button | |
| type="button" | |
| onClick={() => handleSort("model")} | |
| className="w-full text-left font-semibold transition-colors hover:text-primary" | |
| > | |
| Model{getSortIndicator("model")} | |
| </button> | |
| </TableHead> | |
| <TableHead className="hidden min-w-[180px] lg:table-cell"> | |
| <button | |
| type="button" | |
| onClick={() => handleSort("developer")} | |
| className="w-full text-left font-semibold transition-colors hover:text-primary" | |
| > | |
| {isResearchView ? "Developer" : "Provider"} | |
| {getSortIndicator("developer")} | |
| </button> | |
| </TableHead> | |
| <TableHead className="text-right"> | |
| <button | |
| type="button" | |
| onClick={() => handleSort("coverage")} | |
| className="w-full text-right font-semibold transition-colors hover:text-primary" | |
| > | |
| Measures present{getSortIndicator("coverage")} | |
| </button> | |
| </TableHead> | |
| {visibleMetrics.map((metric) => ( | |
| <TableHead key={metric.column_key} className="min-w-[150px] text-right"> | |
| <button | |
| type="button" | |
| onClick={() => handleSort(metric.column_key)} | |
| aria-label={describeLeaderboardMetric(metric)} | |
| className="group relative flex w-full flex-col items-end leading-tight transition-colors hover:text-primary focus-visible:text-primary" | |
| > | |
| {!hasSubtaskTabs && !(metric.scope === "subtask" && metric.subtask_key && subtaskMetricCounts.get(metric.subtask_key) === 1) && metric.scope === "subtask" && metric.subtask_name && ( | |
| <span className="text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground"> | |
| {metric.subtask_name} | |
| </span> | |
| )} | |
| <span> | |
| {metric.scope === "subtask" && metric.subtask_key && subtaskMetricCounts.get(metric.subtask_key) === 1 && metric.subtask_name | |
| ? metric.subtask_name | |
| : getCompactMetricLabel(metric.display_name)} | |
| {getSortIndicator(metric.column_key)} | |
| </span> | |
| <span className="pointer-events-none absolute right-0 top-full z-20 mt-2 w-56 rounded-md border border-border/70 bg-background px-2.5 py-2 text-left text-[11px] font-normal normal-case tracking-normal text-foreground opacity-0 shadow-lg transition-opacity duration-150 group-hover:opacity-100 group-focus-visible:opacity-100"> | |
| {describeLeaderboardMetric(metric)} | |
| </span> | |
| </button> | |
| </TableHead> | |
| ))} | |
| <TableHead className="hidden min-w-[120px] xl:table-cell"> | |
| <button | |
| type="button" | |
| onClick={() => handleSort("updated")} | |
| className="w-full text-left font-semibold transition-colors hover:text-primary" | |
| > | |
| Updated{getSortIndicator("updated")} | |
| </button> | |
| </TableHead> | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {pagedRows.map((row) => { | |
| const rank = rankByModelId.get(row.model_info.id) ?? 0 | |
| const expandKey = row.model_info.id | |
| const isExpanded = expandedRows[expandKey] ?? false | |
| const matchingResult = modelResultByModelId.get(row.model_info.id) | |
| return ( | |
| <Fragment key={row.model_info.id}> | |
| <TableRow className={cn("hover:bg-muted/10", isExpanded && "bg-muted/15")}> | |
| <TableCell className="px-4"> | |
| <div | |
| className={cn( | |
| "inline-flex h-9 w-9 items-center justify-center rounded-full border text-sm font-semibold", | |
| getRankBadgeClass(rank) | |
| )} | |
| > | |
| {rank} | |
| </div> | |
| </TableCell> | |
| <TableCell className="px-4 whitespace-normal"> | |
| <div className="space-y-1"> | |
| <div className="flex items-center gap-1.5 font-semibold leading-tight"> | |
| {isResearchView && matchingResult && ( | |
| <button | |
| type="button" | |
| onClick={() => toggleExpandedRow(expandKey)} | |
| aria-label={isExpanded ? "Hide reproducibility" : "Show reproducibility"} | |
| aria-expanded={isExpanded} | |
| className="-ml-1 inline-flex h-5 w-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" | |
| > | |
| {isExpanded ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />} | |
| </button> | |
| )} | |
| <Link | |
| href={`/models/${getModelFamilyRouteId(row.model_info)}`} | |
| className="underline decoration-dotted underline-offset-4 hover:text-primary" | |
| > | |
| {row.model_info.name} | |
| </Link> | |
| <RowSignalsCompact | |
| annotations={getRowLevelAnnotations(row, visibleMetrics)} | |
| className="ml-1" | |
| /> | |
| </div> | |
| <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground"> | |
| {row.model_info.parameter_count && ( | |
| <Badge variant="secondary" className="font-normal"> | |
| {row.model_info.parameter_count} | |
| </Badge> | |
| )} | |
| {row.model_info.architecture && ( | |
| <Badge variant="outline" className="font-normal"> | |
| {row.model_info.architecture} | |
| </Badge> | |
| )} | |
| <span className="lg:hidden">{row.model_info.developer ?? "Unknown developer"}</span> | |
| </div> | |
| </div> | |
| </TableCell> | |
| <TableCell className="hidden lg:table-cell"> | |
| <div className="max-w-[180px] truncate text-sm text-muted-foreground"> | |
| {row.model_info.developer ?? "Unknown developer"} | |
| </div> | |
| </TableCell> | |
| <TableCell className="text-right tabular-nums text-sm font-semibold"> | |
| {getVisibleMetricCount(row)}/{visibleMetrics.length} | |
| </TableCell> | |
| {visibleMetrics.map((metric) => { | |
| const score = row.values[metric.column_key] | |
| const annotations = row.annotations_by_metric?.[metric.column_key] | |
| return ( | |
| <TableCell | |
| key={metric.column_key} | |
| className={cn( | |
| "text-right tabular-nums", | |
| !isNumericScore(score) && "text-muted-foreground" | |
| )} | |
| > | |
| <div>{isNumericScore(score) ? formatRawScore(score, metric.unit) : "—"}</div> | |
| <SignalsRowBadges annotations={annotations} variant="cell" /> | |
| </TableCell> | |
| ) | |
| })} | |
| <TableCell className="hidden xl:table-cell text-sm text-muted-foreground"> | |
| {formatDate(row.evaluation_timestamp)} | |
| </TableCell> | |
| </TableRow> | |
| {isResearchView && isExpanded && matchingResult && ( | |
| <TableRow className="hover:bg-transparent"> | |
| <TableCell | |
| colSpan={visibleMetrics.length + 5} | |
| className="bg-muted/10 px-4 py-5 sm:px-6" | |
| > | |
| <div className="space-y-3"> | |
| <ResearcherReproducibilityCard | |
| modelResult={matchingResult} | |
| benchmarkKey={summary.benchmark_leaf_key ?? summary.composite_benchmark_key} | |
| evalName={summary.evaluation_name} | |
| /> | |
| <div className="flex justify-end"> | |
| <FlagScoreButton | |
| modelName={matchingResult.model_info.name} | |
| modelId={matchingResult.model_info.id} | |
| benchmarkName={summary.evaluation_name} | |
| benchmarkId={summary.evaluation_id} | |
| score={formatRawScore(matchingResult.score, summary.metric_config.unit)} | |
| sourceUrl={matchingResult.source_metadata.source_url} | |
| sourceRecordUrl={matchingResult.source_record_url} | |
| /> | |
| </div> | |
| </div> | |
| </TableCell> | |
| </TableRow> | |
| )} | |
| </Fragment> | |
| )})} | |
| {filteredRows.length === 0 && ( | |
| <TableRow> | |
| <TableCell colSpan={visibleMetrics.length + 5} className="px-6 py-12 text-center text-sm text-muted-foreground"> | |
| No models match the selected parameter range. | |
| </TableCell> | |
| </TableRow> | |
| )} | |
| </TableBody> | |
| </Table> | |
| </div> | |
| {pagedRows.length < filteredRows.length && ( | |
| <div className="border-t px-6 py-4 text-center"> | |
| <Button variant="outline" onClick={() => setPage((current) => current + 1)}> | |
| Load more ({filteredRows.length - pagedRows.length} remaining) | |
| </Button> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| ) | |
| } | |
| function BenchmarkCardCollapsible({ | |
| card, | |
| isResearchView, | |
| defaultOpen = true, | |
| defaultRisksOpen = false, | |
| knownIssues = [], | |
| }: { | |
| card: BenchmarkCard | |
| isResearchView: boolean | |
| defaultOpen?: boolean | |
| defaultRisksOpen?: boolean | |
| knownIssues?: KnownIssue[] | |
| }) { | |
| const [open, setOpen] = useState(defaultOpen) | |
| return ( | |
| <Collapsible open={open} onOpenChange={setOpen}> | |
| <CollapsibleTrigger asChild> | |
| <button | |
| type="button" | |
| className="flex w-full items-center justify-between rounded-[1.5rem] border border-border/70 bg-muted/10 px-5 py-4 text-left transition-colors hover:bg-muted/20" | |
| > | |
| <div className="flex items-center gap-2"> | |
| <BookOpen className="h-4 w-4 text-muted-foreground" /> | |
| <span className="text-sm font-semibold">Benchmark card details</span> | |
| <span className="text-xs text-muted-foreground"> | |
| dataset, methodology, risks, resources | |
| </span> | |
| </div> | |
| {open ? ( | |
| <ChevronUp className="h-4 w-4 text-muted-foreground" /> | |
| ) : ( | |
| <ChevronDown className="h-4 w-4 text-muted-foreground" /> | |
| )} | |
| </button> | |
| </CollapsibleTrigger> | |
| <CollapsibleContent className="mt-2"> | |
| <BenchmarkCardPanel | |
| card={card} | |
| isResearchView={isResearchView} | |
| defaultRisksOpen={defaultRisksOpen} | |
| knownIssues={knownIssues} | |
| /> | |
| </CollapsibleContent> | |
| </Collapsible> | |
| ) | |
| } | |
| function DetailPanel({ | |
| title, | |
| subtitle, | |
| children, | |
| }: { | |
| title: string | |
| subtitle: string | |
| children: React.ReactNode | |
| }) { | |
| return ( | |
| <div className="min-w-0 rounded-2xl border bg-background/70 p-4"> | |
| <div className="mb-4"> | |
| <div className="font-semibold">{title}</div> | |
| <div className="text-sm text-muted-foreground">{subtitle}</div> | |
| </div> | |
| <div className="min-w-0 space-y-2.5">{children}</div> | |
| </div> | |
| ) | |
| } | |
| function MetaRow({ | |
| label, | |
| value, | |
| }: { | |
| label: string | |
| value: React.ReactNode | |
| }) { | |
| // Hide rows whose value is missing or a generic placeholder. This keeps | |
| // the detail panels focused on fields we actually have data for. | |
| if (value == null) return null | |
| if (typeof value === "string") { | |
| const normalized = value.trim().toLowerCase() | |
| if ( | |
| normalized === "" || | |
| normalized === "unknown" || | |
| normalized === "n/a" || | |
| normalized === "not recorded" || | |
| normalized === "not specified" || | |
| normalized === "not linked" | |
| ) { | |
| return null | |
| } | |
| } | |
| return ( | |
| <div | |
| className="text-sm" | |
| style={{ | |
| display: "grid", | |
| gridTemplateColumns: "8rem minmax(0, 1fr)", | |
| columnGap: "0.75rem", | |
| width: "100%", | |
| minWidth: 0, | |
| }} | |
| > | |
| <div className="text-muted-foreground">{label}</div> | |
| <div | |
| className="font-medium" | |
| style={{ | |
| minWidth: 0, | |
| maxWidth: "100%", | |
| overflowWrap: "anywhere", | |
| wordBreak: "break-word", | |
| }} | |
| > | |
| {value} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| function toStringArray(value: unknown): string[] { | |
| const result = new Set<string>() | |
| const visit = (candidate: unknown) => { | |
| if (!candidate) { | |
| return | |
| } | |
| if (Array.isArray(candidate)) { | |
| for (const item of candidate) { | |
| visit(item) | |
| } | |
| return | |
| } | |
| if (typeof candidate === "object") { | |
| for (const item of Object.values(candidate)) { | |
| visit(item) | |
| } | |
| return | |
| } | |
| if (typeof candidate !== "string") { | |
| return | |
| } | |
| const normalized = candidate.trim() | |
| if (!normalized || normalized === "Not specified") { | |
| return | |
| } | |
| for (const part of normalized.split(/[,;|]/)) { | |
| const token = part.trim() | |
| if (token && token !== "Not specified") { | |
| result.add(token) | |
| } | |
| } | |
| } | |
| visit(value) | |
| return Array.from(result) | |
| } | |
| function BenchmarkCardPanel({ | |
| card, | |
| isResearchView, | |
| defaultRisksOpen = false, | |
| knownIssues = [], | |
| }: { | |
| card: BenchmarkCard | |
| isResearchView: boolean | |
| defaultRisksOpen?: boolean | |
| knownIssues?: KnownIssue[] | |
| }) { | |
| const [risksOpen, setRisksOpen] = useState(defaultRisksOpen) | |
| const details = card.benchmark_details | |
| const purpose = card.purpose_and_intended_users | |
| const methodology = card.methodology | |
| const data = card.data | |
| const ethical = card.ethical_and_legal_considerations | |
| const risks = card.possible_risks ?? [] | |
| const flaggedFields = Object.entries(card.flagged_fields ?? {}) | |
| const missingFields = card.missing_fields ?? [] | |
| const domains = toStringArray(details.domains) | |
| const languages = toStringArray(details.languages) | |
| const resources = (details.resources ?? []).filter(Boolean) | |
| const tasks = toStringArray(purpose.tasks) | |
| const audience = toStringArray(purpose.audience) | |
| const license = ethical.data_licensing ?? "" | |
| const shortLicense = license && license !== "Not specified" ? license : null | |
| return ( | |
| <Card className="overflow-hidden"> | |
| <CardHeader className="border-b bg-muted/10"> | |
| <div className="flex flex-wrap items-center gap-3"> | |
| <BookOpen className="h-5 w-5 text-primary" /> | |
| <CardTitle className="text-xl">Benchmark Card</CardTitle> | |
| {shortLicense && ( | |
| <Badge variant="outline" className="font-normal"> | |
| {shortLicense} | |
| </Badge> | |
| )} | |
| {(flaggedFields.length > 0 || missingFields.length > 0) && ( | |
| <Badge className="bg-amber-500 text-amber-950 hover:bg-amber-500"> | |
| <AlertTriangle className="mr-1 h-3 w-3" /> | |
| {flaggedFields.length} flagged · {missingFields.length} missing | |
| </Badge> | |
| )} | |
| </div> | |
| <CardDescription> | |
| Structured metadata about this benchmark: what it measures, how it was built, and known limitations. | |
| {card.card_info?.llm && ( | |
| <span className="ml-1 text-muted-foreground/70"> | |
| Card generated by {card.card_info.llm}. | |
| </span> | |
| )} | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-6 p-5 sm:p-6"> | |
| {knownIssues.length > 0 && <KnownIssuesPanel issues={knownIssues} variant="full" />} | |
| {/* Overview + domains */} | |
| <div className="space-y-3"> | |
| <p className="text-sm leading-6 text-muted-foreground">{details.overview}</p> | |
| <div className="flex flex-wrap gap-2"> | |
| {domains.map((d) => ( | |
| <span | |
| key={d} | |
| className="inline-flex items-center gap-1 rounded-full border border-border/60 bg-muted/30 px-2.5 py-1 text-xs font-medium capitalize" | |
| > | |
| <Tag className="h-3 w-3 shrink-0 text-muted-foreground" /> | |
| {d} | |
| </span> | |
| ))} | |
| {languages.map((l) => ( | |
| <span | |
| key={l} | |
| className="inline-flex items-center gap-1 rounded-full border border-sky-200/70 bg-sky-50/60 px-2.5 py-1 text-xs font-medium dark:border-sky-900/40 dark:bg-sky-950/20" | |
| > | |
| <Globe className="h-3 w-3 shrink-0 text-sky-600" /> | |
| {l} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3"> | |
| {/* Goal */} | |
| <div className="rounded-[1.25rem] border border-border/70 bg-muted/10 p-4"> | |
| <div className="mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| <Scale className="h-3.5 w-3.5" /> Goal | |
| </div> | |
| <p className="text-sm leading-5 text-foreground">{purpose.goal}</p> | |
| </div> | |
| {/* Metric interpretation */} | |
| <div className="rounded-[1.25rem] border border-border/70 bg-muted/10 p-4"> | |
| <div className="mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| <BarChart3 className="h-3.5 w-3.5" /> Score interpretation | |
| </div> | |
| <p className="text-sm leading-5 text-foreground">{methodology.interpretation}</p> | |
| </div> | |
| {/* Limitations */} | |
| <div className="rounded-[1.25rem] border border-amber-200/60 bg-amber-50/60 p-4 dark:border-amber-900/40 dark:bg-amber-950/15"> | |
| <div className="mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-700 dark:text-amber-300"> | |
| <AlertTriangle className="h-3.5 w-3.5" /> Limitations | |
| </div> | |
| <p className="text-sm leading-5 text-amber-900/90 dark:text-amber-100/90">{purpose.limitations}</p> | |
| </div> | |
| </div> | |
| {(methodology.methods?.length > 0 || | |
| (methodology.calculation && methodology.calculation !== "Not specified") || | |
| (methodology.validation && methodology.validation !== "Not specified")) && ( | |
| <div className="rounded-[1.25rem] border border-border/70 bg-muted/10 p-4"> | |
| <div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| How tasks were sourced and scored | |
| </div> | |
| <div className="grid gap-4 lg:grid-cols-2"> | |
| {methodology.methods?.length > 0 && ( | |
| <div> | |
| <div className="mb-1.5 text-xs font-semibold text-foreground/80"> | |
| Task setup | |
| </div> | |
| <ol className="list-decimal space-y-1.5 pl-4 text-sm leading-5 text-muted-foreground"> | |
| {methodology.methods.map((m, i) => ( | |
| <li key={i}>{m}</li> | |
| ))} | |
| </ol> | |
| </div> | |
| )} | |
| <div className="space-y-3"> | |
| {methodology.calculation && methodology.calculation !== "Not specified" && ( | |
| <div> | |
| <div className="mb-1.5 text-xs font-semibold text-foreground/80"> | |
| Score calculation | |
| </div> | |
| <p className="text-sm leading-5 text-muted-foreground"> | |
| {methodology.calculation} | |
| </p> | |
| </div> | |
| )} | |
| {methodology.validation && methodology.validation !== "Not specified" && ( | |
| <div> | |
| <div className="mb-1.5 text-xs font-semibold text-foreground/80"> | |
| Validation | |
| </div> | |
| <p className="text-sm leading-5 text-muted-foreground"> | |
| {methodology.validation} | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Research-only: methodology + dataset details */} | |
| {isResearchView && ( | |
| <div className="grid gap-4 sm:grid-cols-2"> | |
| <div className="rounded-[1.25rem] border border-border/70 bg-muted/10 p-4"> | |
| <div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| Dataset | |
| </div> | |
| <dl className="space-y-2 text-sm"> | |
| <div className="flex gap-2"> | |
| <dt className="w-20 shrink-0 text-muted-foreground">Size</dt> | |
| <dd className="font-medium">{data.size}</dd> | |
| </div> | |
| <div className="flex gap-2"> | |
| <dt className="w-20 shrink-0 text-muted-foreground">Format</dt> | |
| <dd className="font-medium capitalize">{data.format}</dd> | |
| </div> | |
| <div className="flex gap-2"> | |
| <dt className="w-20 shrink-0 text-muted-foreground">Source</dt> | |
| <dd className="font-medium">{data.source}</dd> | |
| </div> | |
| </dl> | |
| </div> | |
| <div className="rounded-[1.25rem] border border-border/70 bg-muted/10 p-4"> | |
| <div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| Methodology | |
| </div> | |
| <dl className="space-y-2 text-sm"> | |
| {methodology.metrics.length > 0 && ( | |
| <div className="flex gap-2"> | |
| <dt className="w-20 shrink-0 text-muted-foreground">Metrics</dt> | |
| <dd className="font-medium">{methodology.metrics.join(", ")}</dd> | |
| </div> | |
| )} | |
| {tasks.length > 0 && ( | |
| <div className="flex gap-2"> | |
| <dt className="w-20 shrink-0 text-muted-foreground">Tasks</dt> | |
| <dd className="font-medium">{tasks.join(", ")}</dd> | |
| </div> | |
| )} | |
| {audience.length > 0 && ( | |
| <div className="flex gap-2"> | |
| <dt className="w-20 shrink-0 text-muted-foreground">Audience</dt> | |
| <dd className="font-medium">{audience.join("; ")}</dd> | |
| </div> | |
| )} | |
| </dl> | |
| </div> | |
| </div> | |
| )} | |
| {/* Generic IBM-style AI risks. These are boilerplate (per audit | |
| feedback: "least useful feature for policy users"), so in policy | |
| mode we hide them entirely — the curated known-issues panel above | |
| carries the benchmark-specific concerns. Researchers still get the | |
| full collapsible list. */} | |
| {risks.length > 0 && isResearchView && ( | |
| <Collapsible open={risksOpen} onOpenChange={setRisksOpen}> | |
| <CollapsibleTrigger asChild> | |
| <button | |
| type="button" | |
| className="flex w-full items-center justify-between rounded-[1.25rem] border border-border/70 bg-muted/10 p-4 text-left transition-colors hover:bg-muted/20" | |
| > | |
| <div className="flex items-center gap-2"> | |
| <Shield className="h-4 w-4 text-muted-foreground" /> | |
| <span className="text-sm font-semibold"> | |
| Risk considerations ({risks.length}) | |
| </span> | |
| </div> | |
| {risksOpen ? ( | |
| <ChevronUp className="h-4 w-4 text-muted-foreground" /> | |
| ) : ( | |
| <ChevronDown className="h-4 w-4 text-muted-foreground" /> | |
| )} | |
| </button> | |
| </CollapsibleTrigger> | |
| <CollapsibleContent> | |
| <div className="mt-2 grid gap-3 sm:grid-cols-2"> | |
| {risks.map((risk, i) => ( | |
| <div | |
| key={i} | |
| className="rounded-[1.25rem] border border-border/60 bg-background p-4" | |
| > | |
| <div className="mb-1.5 flex items-start justify-between gap-2"> | |
| <span className="text-sm font-semibold">{risk.category}</span> | |
| {risk.url && ( | |
| <a | |
| href={risk.url} | |
| target="_blank" | |
| rel="noreferrer" | |
| onClick={(e) => e.stopPropagation()} | |
| className="shrink-0 text-muted-foreground hover:text-primary" | |
| > | |
| <ExternalLink className="h-3.5 w-3.5" /> | |
| </a> | |
| )} | |
| </div> | |
| {risk.description?.[0] && ( | |
| <p className="text-xs leading-5 text-muted-foreground line-clamp-3"> | |
| {risk.description[0]} | |
| </p> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| </CollapsibleContent> | |
| </Collapsible> | |
| )} | |
| {/* Compliance / ethical notes (policy view emphasis) */} | |
| {!isResearchView && ( | |
| <div className="rounded-[1.25rem] border border-border/70 bg-muted/10 p-4"> | |
| <div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| Ethical & legal | |
| </div> | |
| <dl className="grid gap-x-6 gap-y-2 text-sm sm:grid-cols-2"> | |
| {shortLicense && ( | |
| <div className="flex gap-2"> | |
| <dt className="w-28 shrink-0 text-muted-foreground">License</dt> | |
| <dd className="font-medium">{license}</dd> | |
| </div> | |
| )} | |
| {ethical.compliance_with_regulations && ethical.compliance_with_regulations !== "Not specified" && ( | |
| <div className="flex gap-2"> | |
| <dt className="w-28 shrink-0 text-muted-foreground">Compliance</dt> | |
| <dd className="font-medium">{ethical.compliance_with_regulations}</dd> | |
| </div> | |
| )} | |
| {ethical.privacy_and_anonymity && ethical.privacy_and_anonymity !== "Not specified" && ( | |
| <div className="col-span-full flex gap-2"> | |
| <dt className="w-28 shrink-0 text-muted-foreground">Privacy</dt> | |
| <dd className="font-medium">{ethical.privacy_and_anonymity}</dd> | |
| </div> | |
| )} | |
| </dl> | |
| </div> | |
| )} | |
| {/* Flagged / missing fields warning */} | |
| {(flaggedFields.length > 0 || missingFields.length > 0) && isResearchView && ( | |
| <div className="rounded-[1.25rem] border border-amber-200/60 bg-amber-50/50 p-4 dark:border-amber-900/40 dark:bg-amber-950/15"> | |
| <div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-700 dark:text-amber-300"> | |
| Card quality notes | |
| </div> | |
| {flaggedFields.length > 0 && ( | |
| <ul className="space-y-1 text-xs text-amber-900/80 dark:text-amber-100/80"> | |
| {flaggedFields.map(([field, note]) => ( | |
| <li key={field}> | |
| <span className="font-semibold">{field}:</span> {note} | |
| </li> | |
| ))} | |
| </ul> | |
| )} | |
| {missingFields.length > 0 && ( | |
| <p className="mt-1 text-xs text-amber-900/70 dark:text-amber-100/70"> | |
| Missing: {missingFields.join(", ")} | |
| </p> | |
| )} | |
| </div> | |
| )} | |
| {/* External resources */} | |
| {resources.length > 0 && ( | |
| <div> | |
| <div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground"> | |
| Resources | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| {resources.map((url) => ( | |
| <a | |
| key={url} | |
| href={url} | |
| target="_blank" | |
| rel="noreferrer" | |
| onClick={(e) => e.stopPropagation()} | |
| className="inline-flex items-center gap-1 rounded-full border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-muted-foreground hover:text-primary hover:border-primary/40 transition-colors" | |
| > | |
| <FileText className="h-3 w-3 shrink-0" /> | |
| {url.replace(/^https?:\/\//, "").replace(/\/.+/, "")} | |
| <ExternalLink className="h-3 w-3 shrink-0" /> | |
| </a> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| ) | |
| } | |