HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import { ExternalLink } from 'lucide-react';2import { ScrollX } from '@/components/models/scroll-x';3import Link from 'next/link';4import { Evidence } from '@/components/evidence/evidence';5import { ComparabilityBadge, ConfigChipEl, TrustBadge } from '@/components/models/badges';6import { configChipsOf, fmtScoreUnit, scoreDelta } from '@/components/models/shared';7import { Estimated } from '@/components/ui/badges';8import { DataTable, Td, Th } from '@/components/ui/data-table';9import { EntityLink } from '@/components/ui/entity';10import { Hint } from '@/components/ui/hint';11import { SourceCell } from '@/components/ui/provenance';12import { Note } from '@/components/ui/section';13import { EmptyState } from '@/components/ui/unavailable';14import { cn } from '@/lib/cn';15import { DASH, fmtAgo, fmtDate, fmtGb, fmtInt, fmtTokens, fmtUsdPerM, fmtValue, hostOf, num } from '@/lib/format';16import { propertyLabel, routes } from '@/lib/site';17import type { BenchmarkListItem, Deployment, HardwareFitRow, ModelDetail, ProvenanceEntry, VersionHistoryItem } from '@/lib/types';1819/* ------------------------------------------------------------------------------------------------------ identity panel */2021/** Canonical model · official checkpoints · third-party artifacts (by kind) · provider deployments · API aliases · folded variants. */22export function IdentityPanel({ d }: { d: ModelDetail }) {23 const id = d.identity;24 const kinds = d.artifacts?.items ?? [];25 if (!id) return <p className="text-sm text-ink-3">Identity block not returned by the API for this entity.</p>;26 // The definition hint sits at the far right of the value cell (right-aligned bubble): in the narrow label column the 18rem bubble would widen the page.27 const Row = ({ k, hint, children }: { k: string; hint?: string; children: React.ReactNode }) => (28 <div>29 <dt>{k}</dt>30 <dd className="flex items-start justify-between gap-2 text-ink">31 <span className="min-w-0 flex-1">{children}</span>32 {hint && <Hint text={hint} align="right" className="-my-1 shrink-0" />}33 </dd>34 </div>35 );36 return (37 <dl className="kv" data-identity-panel>38 <Row k="Canonical model" hint="One row per real model release. Artifacts (checkpoints, quantisations, conversions) and folded evaluation variants point here.">39 <span className="font-medium">{id.canonical_model ? 'Yes' : 'No'}</span>40 {d.identity_confidence && <span className="ml-2 text-xs text-ink-3">identity confidence: {d.identity_confidence}</span>}41 </Row>42 <Row k="Official checkpoints" hint={id.note ?? 'hf_repo identifiers carried by the model itself.'}>43 {id.official_checkpoints.length ? (44 <ul className="space-y-0.5">45 {id.official_checkpoints.map((c) => (46 <li key={c}>47 <a href={`https://huggingface.co/${c}`} target="_blank" rel="noopener noreferrer" className="mono inline-flex items-center gap-1 text-[13px] text-ink hover:text-accent">48 {c} <ExternalLink className="size-3" aria-hidden />49 </a>50 </li>51 ))}52 </ul>53 ) : (54 <span className="text-ink-3">None recorded{d.openness?.category === 'proprietary' ? ' — closed weights' : ''}</span>55 )}56 </Row>57 <Row k="Artifacts" hint="Separate entities (checkpoint · quantization · conversion · packaging) pointing to this model through canonical_id.">58 {kinds.length ? (59 <span className="flex flex-wrap gap-x-3 gap-y-0.5">60 {kinds.map((k) => (61 <a key={k.kind} href="#versions-artifacts" className="tnum text-ink-2 hover:text-accent">62 {fmtInt(k.count)} {k.kind}63 {k.count === 1 ? '' : 's'}64 </a>65 ))}66 <span className="text-xs text-ink-3">67 {fmtInt(id.official_artifacts)} official · {fmtInt(id.third_party_artifacts)} third-party68 </span>69 </span>70 ) : (71 <span className="text-ink-3">None recorded</span>72 )}73 </Row>74 <Row k="Provider deployments">75 {id.provider_deployments ? (76 <a href="#providers-pricing" className="tnum hover:text-accent">77 {fmtInt(id.provider_deployments)}78 </a>79 ) : (80 <span className="text-ink-3">None recorded</span>81 )}82 </Row>83 <Row k="API aliases" hint="Identifiers under which providers and evaluators refer to this model.">84 {id.api_aliases.length ? <span className="mono flex flex-wrap gap-x-2 gap-y-0.5 text-[12px] text-ink-2">{id.api_aliases.map((a) => <span key={a}>{a}</span>)}</span> : <span className="text-ink-3">None</span>}85 </Row>86 <Row k="Folded evaluation variants" hint="Effort / thinking variants (…-high, …-non-reasoning) are result configurations of this model, not separate models. Their old URLs redirect here.">87 <span className="tnum">{fmtInt(id.folded_variants)}</span>88 </Row>89 </dl>90 );91}9293/* ------------------------------------------------------------------------------------------------------ deployments */9495export function DeploymentsTable({ deployments, modelSlug }: { deployments: Deployment[]; modelSlug: string }) {96 if (!deployments.length) return <EmptyState title="No provider deployment recorded">Deployments appear when a provider's public pricing or model listing is crawled.</EmptyState>;97 const rows = [...deployments].sort((a, b) => (num(a.prices.output) ?? Infinity) - (num(b.prices.output) ?? Infinity) || (num(a.prices.input) ?? Infinity) - (num(b.prices.input) ?? Infinity));98 const has = (k: keyof Deployment['prices']) => rows.some((r) => num(r.prices[k]) !== null);99 const showCached = has('cached_input');100 const showWrite = has('cache_write');101 const showBatch = has('batch_input') || has('batch_output');102 const showNative = rows.some((r) => Object.keys(r.prices.native_units ?? {}).length > 0);103 const showImage = has('per_image') || has('per_request');104 const delisted = rows.filter((r) => r.status === 'delisted').length;105 return (106 <>107 <ScrollX><DataTable caption="Provider deployments, cheapest output first" compact>108 <thead>109 <tr>110 <Th>Provider</Th>111 <Th num>Context</Th>112 <Th num>Input / 1M</Th>113 {showCached && <Th num>Cached in</Th>}114 {showWrite && <Th num>Cache write</Th>}115 <Th num>Output / 1M</Th>116 {showBatch && <Th num>Batch in / out</Th>}117 {showImage && <Th num>Per image / request</Th>}118 {showNative && <Th>Native units</Th>}119 <Th>Status</Th>120 <Th>Observed</Th>121 <Th>Source</Th>122 </tr>123 </thead>124 <tbody>125 {rows.map((r, i) => {126 const native = Object.entries(r.prices.native_units ?? {});127 const evidence = (k: string, v: unknown, display: string) => (128 <Evidence slug={modelSlug} property={`price.${k}.${r.provider.slug}`} value={v} display={display} unit="USD / 1M tokens" label={`${propertyLabel(k)} · ${r.provider.name}`} fallback={{ source_id: null, source_name: hostOf(r.source_url) ?? r.provider.name, url: r.source_url, observed_at: r.observed_at, tier: r.tier, confidence: 'high', extractor: 'deterministic', unit: 'USD / 1M tokens' }} entity={{ name: r.model.name, entity_type: 'model' }}>129 {display}130 </Evidence>131 );132 return (133 <tr key={r.id} className={r.status === 'delisted' ? 'opacity-60' : undefined}>134 <Td primary>135 <span className="flex flex-wrap items-center gap-x-2">136 <EntityLink e={r.provider} />137 {i === 0 && r.status === 'active' && num(r.prices.output) !== null && <span className="text-[10px] font-medium uppercase tracking-wide text-accent-2">cheapest output</span>}138 </span>139 {r.provider_model_id && <span className="mono block text-[11px] text-ink-3">{r.provider_model_id}</span>}140 </Td>141 <Td num label="Context" className="tnum text-ink-2">142 {num(r.context_length) === null ? DASH : fmtTokens(r.context_length)}143 {num(r.max_output_tokens) !== null && <span className="block text-[11px] text-ink-3">out {fmtTokens(r.max_output_tokens)}</span>}144 </Td>145 <Td num label="Input / 1M" className="tnum font-medium text-accent-2">146 {num(r.prices.input) === null ? DASH : evidence('input_per_mtok', r.prices.input, fmtUsdPerM(r.prices.input))}147 </Td>148 {showCached && (149 <Td num label="Cached in" className="tnum text-ink-2">150 {fmtUsdPerM(r.prices.cached_input)}151 </Td>152 )}153 {showWrite && (154 <Td num label="Cache write" className="tnum text-ink-2">155 {fmtUsdPerM(r.prices.cache_write)}156 </Td>157 )}158 <Td num label="Output / 1M" className="tnum font-medium text-accent-2">159 {num(r.prices.output) === null ? DASH : evidence('output_per_mtok', r.prices.output, fmtUsdPerM(r.prices.output))}160 </Td>161 {showBatch && (162 <Td num label="Batch in / out" className="tnum text-ink-2">163 {num(r.prices.batch_input) === null && num(r.prices.batch_output) === null ? DASH : `${fmtUsdPerM(r.prices.batch_input)} / ${fmtUsdPerM(r.prices.batch_output)}`}164 </Td>165 )}166 {showImage && (167 <Td num label="Per image / request" className="tnum text-ink-2">168 {num(r.prices.per_image) === null && num(r.prices.per_request) === null ? DASH : `${num(r.prices.per_image) === null ? DASH : `$${fmtValue(num(r.prices.per_image))}`} / ${num(r.prices.per_request) === null ? DASH : `$${fmtValue(num(r.prices.per_request))}`}`}169 </Td>170 )}171 {showNative && (172 <Td label="Native units" className="text-xs text-ink-2">173 {native.length ? (174 <span className="flex flex-wrap gap-1" title={native.map(([k, v]) => `${k}=${String(v)}`).join('\n')}>175 {native.slice(0, 3).map(([k, v]) => (176 <span key={k} className="mono rounded-[3px] bg-surface-2 px-1 text-[10.5px]">177 {k}={typeof v === 'object' ? JSON.stringify(v) : String(v)}178 </span>179 ))}180 {native.length > 3 && <span className="text-ink-3">+{native.length - 3}</span>}181 </span>182 ) : (183 DASH184 )}185 </Td>186 )}187 <Td label="Status">188 <span className={cn('text-xs font-medium', r.status === 'active' ? 'text-positive' : 'text-warning')}>{r.status}</span>189 {r.valid_to && <span className="block text-[11px] text-ink-3">until {fmtDate(r.valid_to)}</span>}190 </Td>191 <Td label="Observed" className="text-xs text-ink-2" title={r.observed_at}>192 {fmtAgo(r.observed_at)}193 <span className="block text-[11px] text-ink-3">since {fmtDate(r.valid_from)}</span>194 </Td>195 <Td label="Source">196 <SourceCell url={r.source_url} tier={r.tier} />197 </Td>198 </tr>199 );200 })}201 </tbody>202 </DataTable></ScrollX>203 <Note className="mt-3">204 {rows[0]?.prices.unit ?? 'USD per 1M tokens'} as published by each provider; native units (per-request fees, flex/priority tiers) are kept verbatim. Rows are append-only — every price change is kept in the history below.205 {delisted > 0 && ` ${fmtInt(delisted)} delisted deployment${delisted === 1 ? '' : 's'} shown greyed.`} <Link href={routes.calculator()} className="link">Cost of a workload →</Link>206 </Note>207 </>208 );209}210211/* ------------------------------------------------------------------------------------------------------ version history */212213function fmtVersionValue(property: string, v: unknown): string {214 if (v === null || v === undefined) return 'unknown';215 if (/context_length|max_output_tokens/.test(property)) return `${fmtTokens(v)}`;216 return fmtValue(v, property);217}218219/** property → "128K → 200K → 1M" with dates; every hop is an evidence trigger (claim id, source, tier). */220export function VersionHistoryBlock({ items, d }: { items: VersionHistoryItem[]; d: ModelDetail }) {221 const shown = items.filter((v) => v.transitions.length > 0);222 if (!shown.length) return <p className="text-sm text-ink-3">No versioned property recorded yet.</p>;223 const sorted = [...shown].sort((a, b) => b.transitions.length - a.transitions.length || a.property.localeCompare(b.property));224 return (225 <div className="space-y-3" data-version-history>226 {sorted.map((v) => {227 const first = v.transitions[0];228 const chain = [first?.from ?? null, ...v.transitions.map((t) => t.to)];229 const changes = v.transitions.filter((t) => t.from !== null && t.from !== undefined).length;230 return (231 <div key={v.property} className="grid gap-1 border-b border-rule py-2 sm:grid-cols-[10rem_minmax(0,1fr)]">232 <p className="text-[13px] text-ink-3">233 {propertyLabel(v.property)}234 <span className="block text-[11px]">235 {changes ? `${fmtInt(changes)} change${changes === 1 ? '' : 's'}` : 'first observation only'}236 </span>237 </p>238 <div className="min-w-0">239 <p className="tnum flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm">240 {chain.map((val, i) => {241 const t = i === 0 ? null : v.transitions[i - 1];242 const isLast = i === chain.length - 1;243 const text = fmtVersionValue(v.property, val);244 if (!t) return val === null || val === undefined ? null : <span key={i} className="text-ink-3">{text}</span>;245 const fallback: ProvenanceEntry = { source_id: null, source_name: hostOf(t.source_url) ?? undefined, url: t.source_url, observed_at: t.valid_from, tier: t.tier, confidence: t.status === 'conflicting' ? 'conflicted' : 'high', extractor: 'deterministic' };246 return (247 <span key={t.claim_id} className="inline-flex items-center gap-1.5">248 {(i > 1 || (chain[0] !== null && chain[0] !== undefined)) && <span className="text-ink-3">→</span>}249 <Evidence slug={d.slug} property={v.property} value={t.to} display={text} fallback={fallback} entity={{ name: d.name, entity_type: d.entity_type }} className={cn(isLast ? 'font-semibold text-ink' : 'text-ink-2')}>250 {text}251 </Evidence>252 <span className="text-[11px] text-ink-3" title={t.valid_from}>253 {fmtDate(t.effective_at ?? t.valid_from)}254 </span>255 </span>256 );257 })}258 {v.transitions[v.transitions.length - 1]?.valid_to === null && <span className="text-[10px] font-medium uppercase tracking-wide text-positive">current</span>}259 </p>260 </div>261 </div>262 );263 })}264 <Note>Each hop is a claim: click a value for its source, tier and observation time. Nothing is overwritten — a new observation closes the previous claim.</Note>265 </div>266 );267}268269/* ------------------------------------------------------------------------------------------------------ benchmarks grouped */270271/** Benchmark → metric / comparability group → best current row (trust, comparability note, n results, vs leader, leaderboard link). */272export function ModelBenchmarksBlock({ d, leaders }: { d: ModelDetail; leaders: BenchmarkListItem[] | null }) {273 const b = d.benchmarks;274 if (!b || !b.items.length) return <EmptyState title="No benchmark results recorded">Results appear when a tier 1–3 source publishes them; we never copy scores without a source.</EmptyState>;275 const leaderMap = new Map<string, BenchmarkListItem>();276 for (const l of leaders ?? []) leaderMap.set(l.slug, l);277 const items = [...b.items].sort((x, y) => (x.category ?? '').localeCompare(y.category ?? '') || x.name.localeCompare(y.name));278 const rows: React.ReactNode[] = [];279 for (const bm of items) {280 const li = leaderMap.get(bm.slug);281 for (const m of bm.metrics) {282 for (const g of m.groups) {283 const isPrimary = li?.primary_group?.config_key === g.config_key && li?.primary_group?.metric === m.metric;284 const leader = isPrimary ? li?.leader : null;285 const leaderIsSelf = leader?.model.slug === d.slug;286 const delta = leader && !leaderIsSelf ? scoreDelta(g.best.score, leader.score, g.best.unit) : null;287 const chips = configChipsOf(g.best.config, null, 4);288 const conditions = chips.filter((c) => c.kind === 'condition');289 rows.push(290 <tr key={`${bm.slug}:${m.metric}:${g.config_key}`}>291 <Td primary>292 <Link href={routes.benchmark(bm.slug) + `?metric=${encodeURIComponent(m.metric)}&config_key=${encodeURIComponent(g.config_key)}`} className="text-ink hover:text-accent hover:underline">293 {bm.name}294 </Link>295 <span className="block text-[11px] text-ink-3">296 {bm.category ?? ''}297 {bm.category ? ' · ' : ''}298 {g.comparability_group}299 </span>300 </Td>301 <Td num label="Best score" className="tnum">302 <Evidence slug={d.slug} property={`benchmark.${bm.slug}.${m.metric}`} value={g.best.score} display={fmtScoreUnit(g.best.score, g.best.unit)} label={`${bm.name} · ${m.metric}`} fallback={{ source_id: null, source_name: hostOf(g.best.source_url) ?? undefined, url: g.best.source_url, observed_at: g.best.observed_at, tier: g.best.tier, confidence: 'high', extractor: 'deterministic', unit: g.best.unit ?? undefined }} entity={{ name: d.name, entity_type: d.entity_type }} className="font-semibold">303 {fmtScoreUnit(g.best.score, g.best.unit)}304 </Evidence>305 {!g.higher_is_better && <span className="block text-[10px] text-ink-3">lower is better</span>}306 </Td>307 <Td label="Trust">308 <TrustBadge level={g.best.trust_level} label={g.best.trust_label} />309 </Td>310 <Td label="Configuration">311 <span className="flex flex-wrap gap-1">312 {chips.length ? chips.map((c) => <ConfigChipEl key={c.key} k={c.key} v={c.value} kind={c.kind} />) : <span className="text-ink-3">{DASH}</span>}313 </span>314 {conditions.length > 0 && <span className="block text-[10px] text-ink-3">conditions differ across rows → partially comparable</span>}315 </Td>316 <Td num label="Results" className="tnum text-ink-2">317 {fmtInt(g.n_rows)}318 </Td>319 <Td label="vs leader" className="tnum whitespace-nowrap">320 {leaderIsSelf ? (321 <span className="text-xs font-medium text-positive">current leader</span>322 ) : leader ? (323 <span title={`Leader: ${leader.model.name} ${fmtScoreUnit(leader.score, leader.unit)}`}>324 <span className={cn('font-medium', delta?.startsWith('+') ? 'text-positive' : 'text-ink-2')}>{delta}</span>325 <span className="block text-[11px] text-ink-3">vs {leader.model.name}</span>326 </span>327 ) : (328 <span className="text-xs text-ink-3" title="Leader delta is shown only for the benchmark's primary comparability group">329 {isPrimary ? DASH : 'non-primary group'}330 </span>331 )}332 </Td>333 <Td label="Evaluated" className="tnum text-xs text-ink-2" title={g.best.evaluated_at ? undefined : `Observed ${fmtDate(g.best.observed_at)}; the source gave no evaluation date`}>334 {g.best.evaluated_at ? fmtDate(g.best.evaluated_at) : <span className="text-ink-3">obs. {fmtDate(g.best.observed_at)}</span>}335 </Td>336 <Td label="Source">337 <SourceCell url={g.best.source_url} tier={g.best.tier} />338 </Td>339 </tr>,340 );341 }342 }343 }344 return (345 <>346 <ScrollX><DataTable caption="Benchmark results grouped by comparability group" compact>347 <thead>348 <tr>349 <Th>Benchmark · group</Th>350 <Th num>Best score</Th>351 <Th>Trust</Th>352 <Th>Configuration</Th>353 <Th num>Results</Th>354 <Th>vs leader</Th>355 <Th>Evaluated</Th>356 <Th>Source</Th>357 </tr>358 </thead>359 <tbody>{rows}</tbody>360 </DataTable></ScrollX>361 <Note className="mt-3">362 {b.note ?? 'Current rows only, grouped by benchmark → canonical metric → comparability group.'} {fmtInt(b.total_rows)} current rows in total. “vs leader” compares with the current leader of the benchmark's primary group only; other groups are not directly comparable. <Link href="/methodology#benchmarks" className="link">Comparability rules →</Link>363 </Note>364 </>365 );366}367368/* ------------------------------------------------------------------------------------------------------ hardware fit */369370export function HardwareFitBlock({ rows, assumptions, modelSlug }: { rows: HardwareFitRow[]; assumptions?: string[]; modelSlug: string }) {371 if (!rows.length) return <EmptyState title="No hardware estimate available">Estimates need a parameter count; this model has none recorded from a source.</EmptyState>;372 const sorted = [...rows].sort((a, b) => Number(b.fits) - Number(a.fits) || (num(a.hardware.attributes?.memory_gb) ?? 0) - (num(b.hardware.attributes?.memory_gb) ?? 0));373 const fits = sorted.filter((r) => r.fits).length;374 return (375 <>376 <div className="mb-3 flex flex-wrap items-center gap-2">377 <Estimated />378 <p className="tnum text-sm text-ink-2">379 {fmtInt(fits)} of {fmtInt(sorted.length)} device × quantization combinations fit.380 </p>381 <Link href={`${routes.runLocally()}?model=${encodeURIComponent(modelSlug)}`} className="link text-sm">382 Run locally: your machine →383 </Link>384 </div>385 <ScrollX><DataTable caption="Estimated hardware fit" compact>386 <thead>387 <tr>388 <Th>Hardware</Th>389 <Th>Quantization</Th>390 <Th num>Device memory</Th>391 <Th num>Est. memory</Th>392 <Th>Fits</Th>393 </tr>394 </thead>395 <tbody>396 {sorted.map((r, i) => (397 <tr key={`${r.hardware.id}-${r.quantization}-${i}`}>398 <Td primary>399 <EntityLink e={r.hardware} />400 </Td>401 <Td label="Quantization" className="mono text-xs text-ink-2">402 {r.quantization}403 </Td>404 <Td num label="Device memory" className="tnum text-ink-2">405 {fmtGb(r.hardware.attributes?.memory_gb as never)}406 </Td>407 <Td num label="Est. memory" className="tnum">408 {fmtGb(r.estimated_memory_gb, 1)} <span className="text-[10px] uppercase tracking-wide text-warning">est.</span>409 </Td>410 <Td label="Fits" className={r.fits ? 'font-medium text-positive' : 'text-ink-3'}>411 {r.fits ? 'Yes' : 'No'}412 </Td>413 </tr>414 ))}415 </tbody>416 </DataTable></ScrollX>417 {assumptions && assumptions.length > 0 && (418 <details className="mt-3 text-xs text-ink-3">419 <summary className="cursor-pointer text-ink-2">Assumptions ({assumptions.length})</summary>420 <ul className="mt-1 list-disc space-y-0.5 pl-4">421 {assumptions.map((a) => (422 <li key={a}>{a}</li>423 ))}424 </ul>425 </details>426 )}427 </>428 );429}430431/* ------------------------------------------------------------------------------------------------------ artifacts */432433export function ArtifactsBlock({ d }: { d: ModelDetail }) {434 const groups = d.artifacts?.items ?? [];435 if (!groups.length) return <p className="text-sm text-ink-3">No artifact (checkpoint, quantisation, conversion or packaging) points to this model yet.</p>;436 return (437 <div className="space-y-4" data-artifacts-block>438 {groups.map((g) => (439 <div key={g.kind}>440 <p className="eyebrow mb-1">441 {g.kind}442 {g.count === 1 ? '' : 's'} <span className="tnum text-ink-3">{fmtInt(g.count)}</span>443 </p>444 <ul className="divide-y divide-rule border-y border-rule">445 {g.items.map((a) => {446 const at = a.attributes ?? {};447 return (448 <li key={a.id} className="grid grid-cols-[minmax(0,1fr)_auto] items-baseline gap-x-3 py-1.5 text-sm">449 <span className="min-w-0">450 <Link href={routes.artifact(a.slug)} className="text-ink hover:text-accent hover:underline">451 {a.name}452 </Link>453 <span className="block truncate text-[11px] text-ink-3">454 {a.organization?.name ?? ''}455 {typeof at.quant_format === 'string' ? ` · ${String(at.quant_format).toUpperCase()}` : ''}456 {Array.isArray(at.weights_dtype) && at.weights_dtype.length ? ` · ${(at.weights_dtype as string[]).join('/')}` : ''}457 {num(at['metric.downloads']) !== null ? ` · ${fmtInt(at['metric.downloads'])} downloads` : ''}458 </span>459 </span>460 <span className="tnum text-xs text-ink-2">{num(at.file_size_gb) !== null ? fmtGb(at.file_size_gb, 1) : DASH}</span>461 </li>462 );463 })}464 {g.count > g.items.length && <li className="py-1.5 text-xs text-ink-3">+{fmtInt(g.count - g.items.length)} more — see the graph.</li>}465 </ul>466 </div>467 ))}468 </div>469 );470}471472/* ------------------------------------------------------------------------------------------------------ comparability legend (reused) */473export function ComparabilityLegend() {474 return (475 <p className="flex flex-wrap items-center gap-2 text-[11px] text-ink-3">476 <ComparabilityBadge level="comparable" /> same task and conditions · <ComparabilityBadge level="partially-comparable" /> same task, conditions differ (effort, temperature, judge) · <ComparabilityBadge level="not-comparable" /> different variant or metric477 </p>478 );479}480