SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
26.4 KB · 480 lines tsx
Raw Blame History
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