'use client'; import { LayoutList, Table2 } from 'lucide-react'; import Link from 'next/link'; import { useMemo, useState } from 'react'; import { Evidence } from '@/components/evidence/evidence'; import { ComparabilityBadge, OpennessChip, TrustBadge } from '@/components/models/badges'; import { fmtScoreUnit, opennessLabel } from '@/components/models/shared'; import { Chip, TierBadge } from '@/components/ui/badges'; import { EntityLink } from '@/components/ui/entity'; import { Hint } from '@/components/ui/hint'; import { Note } from '@/components/ui/section'; import { cn } from '@/lib/cn'; import { DASH, fmtAgo, fmtDate, fmtTokens, fmtUsdPerM, fmtValue, hostOf, num } from '@/lib/format'; import { routes } from '@/lib/site'; import type { CompareDimension11, ComparePayload11, ProvenanceEntry } from '@/lib/types'; import { CompareButton } from './compare-button'; /* Compare 3.0 (client): row groups × sticky entity headers, "hide identical rows" (default on when ≥ 3 entities), use-case chips that reorder and emphasise relevant rows (never a winner), per-cell evidence, mobile horizontal scroll with sticky first column or stacked cards. Data is fetched server-side (`/compare`, `diff_only` is a URL parameter); this component only arranges it. */ type Group = { id: string; label: string; test: (d: CompareDimension11) => boolean }; const GROUPS: Group[] = [ { id: 'overview', label: 'Overview', test: (d) => ['release_date', 'status', 'family', 'version', 'openness', 'organization', 'kind', 'website', 'pricing_url', 'model_count', 'category', 'manufacturer', 'latest_version', 'language'].includes(d.key) }, { id: 'architecture', label: 'Architecture', test: (d) => /parameter_count|architecture|is_moe|num_experts|tokenizer|memory_gb|memory_type|bandwidth|tdp|tflops|compute/.test(d.key) }, { id: 'context', label: 'Context', test: (d) => /context_length|max_output_tokens|knowledge_cutoff|training_data_cutoff/.test(d.key) }, { id: 'capabilities', label: 'Capabilities', test: (d) => /modalit|reasoning|tool_calling|structured_output|vision|audio|fine_tuning|features|languages|runtimes/.test(d.key) }, { id: 'benchmarks', label: 'Benchmarks', test: (d) => d.key.startsWith('bench:') || d.source === 'results' }, { id: 'pricing', label: 'Pricing', test: (d) => d.source === 'prices' || /per_mtok|provider_count|price/.test(d.key) }, { id: 'license', label: 'License', test: (d) => /license|licence/.test(d.key) }, { id: 'history', label: 'History', test: () => false }, ]; const USE_CASES: { id: string; label: string; match: RegExp; groups: string[] }[] = [ { id: 'coding', label: 'Coding', match: /swe-bench|aider|livebench-coding|livebench-agentic|scicode|humaneval|livecodebench|terminal-bench|tool_calling|structured_output/i, groups: ['benchmarks', 'capabilities'] }, { id: 'reasoning', label: 'Reasoning', match: /gpqa|humanitys-last-exam|aime|math|arc-agi|livebench-reasoning|livebench-math|reasoning|intelligence-index/i, groups: ['benchmarks', 'capabilities'] }, { id: 'agentic', label: 'Agentic', match: /tau|terminal-bench|agentic|tool_calling|max_output_tokens/i, groups: ['benchmarks', 'capabilities', 'context'] }, { id: 'long_context', label: 'Long context', match: /context_length|max_output_tokens|knowledge_cutoff/i, groups: ['context'] }, { id: 'vision', label: 'Vision', match: /mmmu|vision|modalit/i, groups: ['capabilities', 'benchmarks'] }, { id: 'low_cost', label: 'Low-cost', match: /per_mtok|provider_count|active_parameter/i, groups: ['pricing', 'architecture'] }, { id: 'local', label: 'Local', match: /parameter_count|openness|license|is_moe|memory/i, groups: ['architecture', 'license', 'overview'] }, { id: 'embeddings', label: 'Embeddings', match: /mteb|embedding|dimension/i, groups: ['benchmarks', 'capabilities'] }, ]; function groupOf(d: CompareDimension11): string { return GROUPS.find((g) => g.test(d))?.id ?? 'overview'; } const LOWER_BETTER = /per_mtok|tdp|price|latency/; function fmtCell(v: unknown, dim: CompareDimension11): string { if (v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0)) return 'Unavailable'; switch (dim.kind) { case 'date': return fmtDate(String(v)); case 'bool': return v ? 'Yes' : 'No'; case 'list': return (Array.isArray(v) ? v : [v]).map((x) => (typeof x === 'object' && x ? JSON.stringify(x) : String(x))).join(', '); case 'number': if (/per_mtok/.test(dim.key)) return fmtUsdPerM(v); if (dim.key.startsWith('bench:')) return fmtScoreUnit(num(v), dim.unit ?? null); if (/context_length|max_output_tokens/.test(dim.key)) return `${fmtTokens(v)} tokens`; return fmtValue(v, dim.key); default: if (dim.key === 'openness') return opennessLabel(v); return typeof v === 'string' ? v : fmtValue(v, dim.key); } } const sameValue = (vals: unknown[]) => { const norm = vals.map((v) => JSON.stringify(v ?? null)); return norm.every((n) => n === norm[0]); }; export function CompareTerminal({ res, diffOnly }: { res: ComparePayload11; diffOnly: boolean }) { const n = res.items.length; const [hideIdentical, setHideIdentical] = useState(n >= 3); const [useCase, setUseCase] = useState(null); const [layout, setLayout] = useState<'table' | 'stack'>('table'); const uc = USE_CASES.find((u) => u.id === useCase) ?? null; const isModel = res.entity_type === 'model'; const rows = useMemo(() => { const byGroup = new Map(); for (const d of res.dimensions) { const g = groupOf(d); byGroup.set(g, [...(byGroup.get(g) ?? []), d]); } // History rows synthesised from the entity summaries (first seen · release · last change). const historyDims: CompareDimension11[] = [ { key: '_first_seen', label: 'First seen in AI Atlas', kind: 'date', source: 'entity' }, { key: '_last_change', label: 'Last change', kind: 'date', source: 'entity' }, ]; byGroup.set('history', historyDims); let order = GROUPS.map((g) => g.id); if (uc) order = [...uc.groups, ...order.filter((g) => !uc.groups.includes(g))]; return order.map((id) => ({ id, label: GROUPS.find((g) => g.id === id)?.label ?? id, dims: (byGroup.get(id) ?? []).slice().sort((a, b) => (uc ? Number(uc.match.test(b.key) || uc.match.test(b.label)) - Number(uc.match.test(a.key) || uc.match.test(a.label)) : 0)) })).filter((g) => g.dims.length); }, [res.dimensions, uc]); const valueOf = (d: CompareDimension11, it: ComparePayload11['items'][number]) => { if (d.key === '_first_seen') return it.entity.first_seen_at || null; if (d.key === '_last_change') return it.entity.updated_at || null; return it.values[d.key]; }; const provenanceOf = (d: CompareDimension11, it: ComparePayload11['items'][number]): ProvenanceEntry | null => { const p = it.provenance?.[d.key]; if (p) return p; if (d.key.startsWith('bench:') && d.benchmark) { const r = (it.results ?? []).find((x) => x.benchmark.slug === d.benchmark && (!d.metric || x.metric === d.metric)); if (r) return { source_id: null, source_name: hostOf(r.source_url) ?? undefined, url: r.source_url, observed_at: r.observed_at, tier: r.tier, confidence: r.confidence, extractor: 'deterministic', unit: r.unit ?? undefined }; } if (d.source === 'prices') { const best = [...(it.prices ?? [])].sort((a, b) => (num(a.output_per_mtok) ?? Infinity) - (num(b.output_per_mtok) ?? Infinity))[0]; if (best) return { source_id: null, source_name: hostOf(best.source_url) ?? best.provider.name, url: best.source_url, observed_at: best.observed_at, tier: best.tier, confidence: 'high', extractor: 'deterministic', unit: 'USD / 1M tokens' }; } return null; }; const propertyOf = (d: CompareDimension11) => (d.key.startsWith('bench:') && d.benchmark ? `benchmark.${d.benchmark}.${d.metric ?? ''}` : d.key.replace(/^_/, '')); let shownRows = 0; let hiddenRows = 0; const body = rows.map((g) => { const dims = g.dims.filter((d) => { const vals = res.items.map((it) => valueOf(d, it)); const identical = sameValue(vals); if (hideIdentical && identical && g.id !== 'history') { hiddenRows++; return false; } return true; }); shownRows += dims.length; return { ...g, dims }; }).filter((g) => g.dims.length); const cellFor = (d: CompareDimension11, it: ComparePayload11['items'][number], best: number | null, emph: boolean) => { const v = valueOf(d, it); const text = fmtCell(v, d); const p = provenanceOf(d, it); const isBest = best !== null && num(v) === best; const canOpen = v !== null && v !== undefined && v !== '' && d.source !== 'entity'; return (
{canOpen ? ( {d.key === 'openness' && typeof v === 'string' ? : d.kind === 'list' ? {(Array.isArray(v) ? v : [v]).map((x, i) => {String(x)})} : text} ) : ( {text} )} {p && ( {p.source_name ?? hostOf(p.url) ?? 'source'} · {fmtAgo(p.observed_at)} )}
); }; const bestOf = (d: CompareDimension11) => { if (d.kind !== 'number' || d.source === 'entity') return null; const nums = res.items.map((it) => num(valueOf(d, it))).filter((x): x is number => x !== null); if (nums.length < 2) return null; const lower = d.higher_is_better === false || (d.higher_is_better === undefined && LOWER_BETTER.test(d.key)); return lower ? Math.min(...nums) : Math.max(...nums); }; const rowLabel = (d: CompareDimension11) => { const comp = res.comparability?.[d.key]; return ( <> {d.key.startsWith('bench:') && d.benchmark ? ( {d.label.split(' · ')[0]} ) : ( {d.label} )} {(d.unit || d.key.startsWith('bench:')) && {d.key.startsWith('bench:') ? d.label.split(' · ').slice(1).join(' · ') : d.unit}} {comp && ( {comp.reasons?.length ? : null} {[...new Set(Object.values(comp.trust ?? {}).map((t) => t.level))].map((lvl) => ( ))} )} ); }; const emphasised = (d: CompareDimension11) => !!uc && (uc.match.test(d.key) || uc.match.test(d.label)); const ROW_TH = { whiteSpace: 'normal', textTransform: 'none', letterSpacing: 0, fontSize: '0.8125rem', fontWeight: 400 } as const; const wide = n <= 3 ? 'md:overflow-visible' : 'xl:overflow-visible'; const stickyTop = n <= 3 ? 'md:sticky md:top-[var(--header-h)] md:z-20' : 'xl:sticky xl:top-[var(--header-h)] xl:z-20'; return (
{/* controls */}
encodeURIComponent(i.entity.slug)).join(',')}${diffOnly ? '' : '&diff_only=1'}${res.entity_type !== 'model' ? `&mode=${res.entity_type}s` : ''}`} className={cn('inline-flex h-8 items-center border px-2.5 text-xs', diffOnly ? 'border-ink bg-ink text-canvas' : 'border-rule text-ink-2 hover:text-ink')} aria-pressed={diffOnly} data-diff-only> Differences only {diffOnly ? '· on' : ''}
{isModel && (
Emphasise {USE_CASES.map((u) => ( ))} {uc && Relevant rows first and highlighted — no score is combined, no winner is declared.}
)} {/* ------------------------------------------------------------------------------------------ table layout */}
{res.items.map((it) => ( ))} {body.map((g) => ( {g.dims.map((d) => { const best = bestOf(d); const emph = emphasised(d); return ( {res.items.map((it) => ( ))} ); })} ))} {isModel && } {isModel && ( {res.items.map((it) => ( ))} )}
Comparison of {n} entities
Dimension {it.entity.organization && {it.entity.organization.name}} {isModel && n === 2 && Diff}
{rowLabel(d)} {cellFor(d, it, best, emph)}
Estimated memory estimated {num(it.entity.attributes?.parameter_count) === null ? 'No parameter count — not estimable' : Per-device estimates →}
{/* ------------------------------------------------------------------------------------------ stacked layout (mobile) */} {layout === 'stack' && (
{res.items.map((it) => (

{it.entity.organization && {it.entity.organization.name}}

{body.map((g) => (

{g.label}

{g.dims.map((d) => (
{d.label.split(' · ')[0]}
{cellFor(d, it, bestOf(d), emphasised(d))}
))}
))}
))}
)} {shownRows} rows shown{hiddenRows ? `, ${hiddenRows} identical hidden` : ''}. Bold = best number in a row (highest; lowest for prices) — a reading aid, not a verdict. Benchmarks appear only when every entity has a current result in the same comparability group. {res.note ?? ''} Click any value for its evidence.
); } function GroupRows({ label, cols, children }: { label: string; cols: number; children: React.ReactNode }) { return ( <> {label} {children} ); } /** Cheapest current deployment per provider × entity (from the compare payload's prices). */ function PricingRows({ res, n }: { res: ComparePayload11; n: number }) { const providers = new Map(); const per = res.items.map((it) => { const m = new Map(); for (const p of it.prices ?? []) { providers.set(p.provider.slug, p.provider); const cur = m.get(p.provider.slug) ?? { input: null, output: null, observed: p.observed_at, url: p.source_url, tier: p.tier }; const i = num(p.input_per_mtok); const o = num(p.output_per_mtok); if (i !== null && (cur.input === null || i < cur.input)) cur.input = i; if (o !== null && (cur.output === null || o < cur.output)) cur.output = o; m.set(p.provider.slug, cur); } return m; }); if (!providers.size) return null; const rows = [...providers.values()].sort((a, b) => a.name.localeCompare(b.name)); return ( {rows.map((prov) => ( {per.map((m, i) => { const b = m.get(prov.slug); return ( {b ? ( <> {fmtUsdPerM(b.input)} / {fmtUsdPerM(b.output)} {b.url ? {hostOf(b.url)} : null} · {fmtAgo(b.observed)} ) : ( {DASH} )} ); })} ))} ); }