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%
22.3 KB · 387 lines tsx
Raw Blame History
1'use client';2import { LayoutList, Table2 } from 'lucide-react';3import Link from 'next/link';4import { useMemo, useState } from 'react';5import { Evidence } from '@/components/evidence/evidence';6import { ComparabilityBadge, OpennessChip, TrustBadge } from '@/components/models/badges';7import { fmtScoreUnit, opennessLabel } from '@/components/models/shared';8import { Chip, TierBadge } from '@/components/ui/badges';9import { EntityLink } from '@/components/ui/entity';10import { Hint } from '@/components/ui/hint';11import { Note } from '@/components/ui/section';12import { cn } from '@/lib/cn';13import { DASH, fmtAgo, fmtDate, fmtTokens, fmtUsdPerM, fmtValue, hostOf, num } from '@/lib/format';14import { routes } from '@/lib/site';15import type { CompareDimension11, ComparePayload11, ProvenanceEntry } from '@/lib/types';16import { CompareButton } from './compare-button';1718/*19  Compare 3.0 (client): row groups × sticky entity headers, "hide identical rows" (default on when ≥ 3 entities), use-case chips that20  reorder and emphasise relevant rows (never a winner), per-cell evidence, mobile horizontal scroll with sticky first column or stacked cards.21  Data is fetched server-side (`/compare`, `diff_only` is a URL parameter); this component only arranges it.22*/2324type Group = { id: string; label: string; test: (d: CompareDimension11) => boolean };25const GROUPS: Group[] = [26  { 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) },27  { 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) },28  { id: 'context', label: 'Context', test: (d) => /context_length|max_output_tokens|knowledge_cutoff|training_data_cutoff/.test(d.key) },29  { id: 'capabilities', label: 'Capabilities', test: (d) => /modalit|reasoning|tool_calling|structured_output|vision|audio|fine_tuning|features|languages|runtimes/.test(d.key) },30  { id: 'benchmarks', label: 'Benchmarks', test: (d) => d.key.startsWith('bench:') || d.source === 'results' },31  { id: 'pricing', label: 'Pricing', test: (d) => d.source === 'prices' || /per_mtok|provider_count|price/.test(d.key) },32  { id: 'license', label: 'License', test: (d) => /license|licence/.test(d.key) },33  { id: 'history', label: 'History', test: () => false },34];35const USE_CASES: { id: string; label: string; match: RegExp; groups: string[] }[] = [36  { 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'] },37  { id: 'reasoning', label: 'Reasoning', match: /gpqa|humanitys-last-exam|aime|math|arc-agi|livebench-reasoning|livebench-math|reasoning|intelligence-index/i, groups: ['benchmarks', 'capabilities'] },38  { id: 'agentic', label: 'Agentic', match: /tau|terminal-bench|agentic|tool_calling|max_output_tokens/i, groups: ['benchmarks', 'capabilities', 'context'] },39  { id: 'long_context', label: 'Long context', match: /context_length|max_output_tokens|knowledge_cutoff/i, groups: ['context'] },40  { id: 'vision', label: 'Vision', match: /mmmu|vision|modalit/i, groups: ['capabilities', 'benchmarks'] },41  { id: 'low_cost', label: 'Low-cost', match: /per_mtok|provider_count|active_parameter/i, groups: ['pricing', 'architecture'] },42  { id: 'local', label: 'Local', match: /parameter_count|openness|license|is_moe|memory/i, groups: ['architecture', 'license', 'overview'] },43  { id: 'embeddings', label: 'Embeddings', match: /mteb|embedding|dimension/i, groups: ['benchmarks', 'capabilities'] },44];4546function groupOf(d: CompareDimension11): string {47  return GROUPS.find((g) => g.test(d))?.id ?? 'overview';48}49const LOWER_BETTER = /per_mtok|tdp|price|latency/;5051function fmtCell(v: unknown, dim: CompareDimension11): string {52  if (v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0)) return 'Unavailable';53  switch (dim.kind) {54    case 'date':55      return fmtDate(String(v));56    case 'bool':57      return v ? 'Yes' : 'No';58    case 'list':59      return (Array.isArray(v) ? v : [v]).map((x) => (typeof x === 'object' && x ? JSON.stringify(x) : String(x))).join(', ');60    case 'number':61      if (/per_mtok/.test(dim.key)) return fmtUsdPerM(v);62      if (dim.key.startsWith('bench:')) return fmtScoreUnit(num(v), dim.unit ?? null);63      if (/context_length|max_output_tokens/.test(dim.key)) return `${fmtTokens(v)} tokens`;64      return fmtValue(v, dim.key);65    default:66      if (dim.key === 'openness') return opennessLabel(v);67      return typeof v === 'string' ? v : fmtValue(v, dim.key);68  }69}70const sameValue = (vals: unknown[]) => {71  const norm = vals.map((v) => JSON.stringify(v ?? null));72  return norm.every((n) => n === norm[0]);73};7475export function CompareTerminal({ res, diffOnly }: { res: ComparePayload11; diffOnly: boolean }) {76  const n = res.items.length;77  const [hideIdentical, setHideIdentical] = useState(n >= 3);78  const [useCase, setUseCase] = useState<string | null>(null);79  const [layout, setLayout] = useState<'table' | 'stack'>('table');80  const uc = USE_CASES.find((u) => u.id === useCase) ?? null;81  const isModel = res.entity_type === 'model';8283  const rows = useMemo(() => {84    const byGroup = new Map<string, CompareDimension11[]>();85    for (const d of res.dimensions) {86      const g = groupOf(d);87      byGroup.set(g, [...(byGroup.get(g) ?? []), d]);88    }89    // History rows synthesised from the entity summaries (first seen · release · last change).90    const historyDims: CompareDimension11[] = [91      { key: '_first_seen', label: 'First seen in AI Atlas', kind: 'date', source: 'entity' },92      { key: '_last_change', label: 'Last change', kind: 'date', source: 'entity' },93    ];94    byGroup.set('history', historyDims);95    let order = GROUPS.map((g) => g.id);96    if (uc) order = [...uc.groups, ...order.filter((g) => !uc.groups.includes(g))];97    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);98  }, [res.dimensions, uc]);99100  const valueOf = (d: CompareDimension11, it: ComparePayload11['items'][number]) => {101    if (d.key === '_first_seen') return it.entity.first_seen_at || null;102    if (d.key === '_last_change') return it.entity.updated_at || null;103    return it.values[d.key];104  };105  const provenanceOf = (d: CompareDimension11, it: ComparePayload11['items'][number]): ProvenanceEntry | null => {106    const p = it.provenance?.[d.key];107    if (p) return p;108    if (d.key.startsWith('bench:') && d.benchmark) {109      const r = (it.results ?? []).find((x) => x.benchmark.slug === d.benchmark && (!d.metric || x.metric === d.metric));110      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 };111    }112    if (d.source === 'prices') {113      const best = [...(it.prices ?? [])].sort((a, b) => (num(a.output_per_mtok) ?? Infinity) - (num(b.output_per_mtok) ?? Infinity))[0];114      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' };115    }116    return null;117  };118  const propertyOf = (d: CompareDimension11) => (d.key.startsWith('bench:') && d.benchmark ? `benchmark.${d.benchmark}.${d.metric ?? ''}` : d.key.replace(/^_/, ''));119120  let shownRows = 0;121  let hiddenRows = 0;122  const body = rows.map((g) => {123    const dims = g.dims.filter((d) => {124      const vals = res.items.map((it) => valueOf(d, it));125      const identical = sameValue(vals);126      if (hideIdentical && identical && g.id !== 'history') {127        hiddenRows++;128        return false;129      }130      return true;131    });132    shownRows += dims.length;133    return { ...g, dims };134  }).filter((g) => g.dims.length);135136  const cellFor = (d: CompareDimension11, it: ComparePayload11['items'][number], best: number | null, emph: boolean) => {137    const v = valueOf(d, it);138    const text = fmtCell(v, d);139    const p = provenanceOf(d, it);140    const isBest = best !== null && num(v) === best;141    const canOpen = v !== null && v !== undefined && v !== '' && d.source !== 'entity';142    return (143      <div className={cn('min-w-0', d.kind === 'number' && 'tnum')}>144        {canOpen ? (145          <Evidence slug={it.entity.slug} property={propertyOf(d)} value={v} display={text} unit={d.unit} fallback={p} entity={{ name: it.entity.name, entity_type: it.entity.entity_type }} className={cn('block text-left whitespace-normal', isBest && 'font-semibold text-ink', emph && 'text-accent')}>146            {d.key === 'openness' && typeof v === 'string' ? <OpennessChip openness={v} /> : d.kind === 'list' ? <span className="flex flex-wrap gap-1">{(Array.isArray(v) ? v : [v]).map((x, i) => <Chip key={i}>{String(x)}</Chip>)}</span> : text}147          </Evidence>148        ) : (149          <span className={cn(text === 'Unavailable' && 'text-ink-3')}>{text}</span>150        )}151        {p && (152          <span className="mt-0.5 hidden items-center gap-1 text-[10.5px] leading-4 text-ink-3 md:flex">153            <TierBadge tier={p.tier} /> <span className="max-w-[9rem] truncate">{p.source_name ?? hostOf(p.url) ?? 'source'}</span> · {fmtAgo(p.observed_at)}154          </span>155        )}156      </div>157    );158  };159  const bestOf = (d: CompareDimension11) => {160    if (d.kind !== 'number' || d.source === 'entity') return null;161    const nums = res.items.map((it) => num(valueOf(d, it))).filter((x): x is number => x !== null);162    if (nums.length < 2) return null;163    const lower = d.higher_is_better === false || (d.higher_is_better === undefined && LOWER_BETTER.test(d.key));164    return lower ? Math.min(...nums) : Math.max(...nums);165  };166  const rowLabel = (d: CompareDimension11) => {167    const comp = res.comparability?.[d.key];168    return (169      <>170        {d.key.startsWith('bench:') && d.benchmark ? (171          <Link href={routes.benchmark(d.benchmark) + (d.metric && d.config_key ? `?metric=${encodeURIComponent(d.metric)}&config_key=${encodeURIComponent(d.config_key)}` : '')} className="text-ink hover:text-accent hover:underline">172            {d.label.split(' · ')[0]}173          </Link>174        ) : (175          <span className="text-ink">{d.label}</span>176        )}177        {(d.unit || d.key.startsWith('bench:')) && <span className="block text-[10.5px] text-ink-3">{d.key.startsWith('bench:') ? d.label.split(' · ').slice(1).join(' · ') : d.unit}</span>}178        {comp && (179          <span className="mt-0.5 flex flex-wrap items-center gap-1">180            <ComparabilityBadge level={comp.level} reasons={comp.reasons} short />181            {comp.reasons?.length ? <Hint text={comp.reasons.join('; ')} /> : null}182            {[...new Set(Object.values(comp.trust ?? {}).map((t) => t.level))].map((lvl) => (183              <TrustBadge key={lvl} level={lvl} />184            ))}185          </span>186        )}187      </>188    );189  };190  const emphasised = (d: CompareDimension11) => !!uc && (uc.match.test(d.key) || uc.match.test(d.label));191  const ROW_TH = { whiteSpace: 'normal', textTransform: 'none', letterSpacing: 0, fontSize: '0.8125rem', fontWeight: 400 } as const;192  const wide = n <= 3 ? 'md:overflow-visible' : 'xl:overflow-visible';193  const stickyTop = n <= 3 ? 'md:sticky md:top-[var(--header-h)] md:z-20' : 'xl:sticky xl:top-[var(--header-h)] xl:z-20';194195  return (196    <div className="space-y-4" data-compare-terminal>197      {/* controls */}198      <div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">199        <label className="inline-flex min-h-9 items-center gap-2 text-ink-2">200          <input type="checkbox" checked={hideIdentical} onChange={(e) => setHideIdentical(e.target.checked)} className="size-4 accent-[var(--accent)]" data-hide-identical /> Hide identical rows201        </label>202        <Link href={`?ids=${res.items.map((i) => 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>203          Differences only {diffOnly ? '· on' : ''}204        </Link>205        <span className="ml-auto inline-flex items-center gap-1 lg:hidden">206          <button type="button" onClick={() => setLayout('table')} aria-pressed={layout === 'table'} className={cn('inline-flex size-9 items-center justify-center border', layout === 'table' ? 'border-ink text-ink' : 'border-rule text-ink-3')} aria-label="Table layout">207            <Table2 className="size-4" aria-hidden />208          </button>209          <button type="button" onClick={() => setLayout('stack')} aria-pressed={layout === 'stack'} className={cn('inline-flex size-9 items-center justify-center border', layout === 'stack' ? 'border-ink text-ink' : 'border-rule text-ink-3')} aria-label="Stacked cards layout">210            <LayoutList className="size-4" aria-hidden />211          </button>212        </span>213      </div>214      {isModel && (215        <div className="flex flex-wrap items-center gap-1.5" role="group" aria-label="Use case emphasis" data-use-cases>216          <span className="eyebrow mr-1">Emphasise</span>217          {USE_CASES.map((u) => (218            <button key={u.id} type="button" onClick={() => setUseCase(useCase === u.id ? null : u.id)} aria-pressed={useCase === u.id} className={cn('inline-flex h-8 items-center border px-2.5 text-xs whitespace-nowrap', useCase === u.id ? 'border-accent bg-accent-soft text-accent' : 'border-rule text-ink-2 hover:border-rule-strong hover:text-ink')}>219              {u.label}220            </button>221          ))}222          {uc && <span className="text-[11px] text-ink-3">Relevant rows first and highlighted — no score is combined, no winner is declared.</span>}223        </div>224      )}225226      {/* ------------------------------------------------------------------------------------------ table layout */}227      <div className={cn(layout === 'stack' && 'hidden lg:block')}>228        <div className={cn('table-scroll scrollbar-thin relative', wide)}>229          <table className="data-table compare-matrix compact" data-compare-table>230            <caption className="sr-only">Comparison of {n} entities</caption>231            <thead>232              <tr>233                <th scope="col" className={cn('sticky left-0 z-30 w-[9rem] min-w-[9rem] bg-canvas md:w-[13rem] md:min-w-[13rem]', stickyTop)} style={{ whiteSpace: 'normal' }}>234                  Dimension235                </th>236                {res.items.map((it) => (237                  <th key={it.entity.id} scope="col" className={cn('min-w-[10rem] bg-canvas align-top', stickyTop)} style={{ whiteSpace: 'normal' }}>238                    <span className="flex flex-col items-start gap-1 normal-case tracking-normal">239                      <EntityLink e={it.entity} className="text-sm font-semibold" />240                      {it.entity.organization && <span className="text-[11px] font-normal text-ink-3">{it.entity.organization.name}</span>}241                      <span className="flex items-center gap-1">242                        <CompareButton e={it.entity} size="sm" label="Add" />243                        {isModel && n === 2 && <Link href={`/models/${encodeURIComponent(res.items[0]!.entity.slug)}/diff/${encodeURIComponent(res.items[1]!.entity.slug)}`} className="inline-flex h-7 items-center border border-rule px-1.5 text-xs font-normal text-ink-2 hover:text-ink">Diff</Link>}244                      </span>245                    </span>246                  </th>247                ))}248              </tr>249            </thead>250            <tbody>251              {body.map((g) => (252                <GroupRows key={g.id} label={g.label} cols={n + 1}>253                  {g.dims.map((d) => {254                    const best = bestOf(d);255                    const emph = emphasised(d);256                    return (257                      <tr key={d.key} className={cn(emph && 'bg-accent-soft/25')} data-dim={d.key}>258                        <th scope="row" className="sticky left-0 z-10 bg-canvas text-left" style={ROW_TH}>259                          {rowLabel(d)}260                        </th>261                        {res.items.map((it) => (262                          <td key={it.entity.id} className="align-top" style={{ whiteSpace: 'normal' }}>263                            {cellFor(d, it, best, emph)}264                          </td>265                        ))}266                      </tr>267                    );268                  })}269                </GroupRows>270              ))}271              {isModel && <PricingRows res={res} n={n} />}272              {isModel && (273                <GroupRows label="Hardware fit" cols={n + 1}>274                  <tr>275                    <th scope="row" className="sticky left-0 z-10 bg-canvas text-left" style={ROW_TH}>276                      Estimated memory277                      <span className="block text-[10.5px] text-warning">estimated</span>278                    </th>279                    {res.items.map((it) => (280                      <td key={it.entity.id} className="align-top text-xs text-ink-3" style={{ whiteSpace: 'normal' }}>281                        {num(it.entity.attributes?.parameter_count) === null ? 'No parameter count — not estimable' : <Link href={`${routes.entity(it.entity)}#hardware-fit`} className="link">Per-device estimates →</Link>}282                      </td>283                    ))}284                  </tr>285                </GroupRows>286              )}287            </tbody>288          </table>289        </div>290      </div>291292      {/* ------------------------------------------------------------------------------------------ stacked layout (mobile) */}293      {layout === 'stack' && (294        <div className="space-y-6 lg:hidden" data-compare-stack>295          {res.items.map((it) => (296            <section key={it.entity.id} className="border-t border-rule pt-3">297              <h3 className="flex flex-wrap items-center gap-2 text-[15px] font-semibold">298                <EntityLink e={it.entity} /> {it.entity.organization && <span className="text-xs font-normal text-ink-3">{it.entity.organization.name}</span>}299              </h3>300              {body.map((g) => (301                <div key={g.id} className="mt-3">302                  <p className="eyebrow mb-1">{g.label}</p>303                  <dl className="kv [&>div]:grid-cols-[7.5rem_minmax(0,1fr)] [&>div]:py-1">304                    {g.dims.map((d) => (305                      <div key={d.key} className={cn(emphasised(d) && 'bg-accent-soft/25')}>306                        <dt className="text-[12px]">{d.label.split(' · ')[0]}</dt>307                        <dd className="text-[13px]">{cellFor(d, it, bestOf(d), emphasised(d))}</dd>308                      </div>309                    ))}310                  </dl>311                </div>312              ))}313            </section>314          ))}315        </div>316      )}317318      <Note>319        {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.320      </Note>321    </div>322  );323}324325function GroupRows({ label, cols, children }: { label: string; cols: number; children: React.ReactNode }) {326  return (327    <>328      <tr className="group-row">329        <th colSpan={cols} scope="colgroup" className="sticky left-0 !border-b-0 bg-canvas pt-4 text-left text-[11px] tracking-[0.1em] text-ink-3">330          {label}331        </th>332      </tr>333      {children}334    </>335  );336}337338/** Cheapest current deployment per provider × entity (from the compare payload's prices). */339function PricingRows({ res, n }: { res: ComparePayload11; n: number }) {340  const providers = new Map<string, { slug: string; name: string; entity_type: string }>();341  const per = res.items.map((it) => {342    const m = new Map<string, { input: number | null; output: number | null; observed: string; url: string | null; tier: number }>();343    for (const p of it.prices ?? []) {344      providers.set(p.provider.slug, p.provider);345      const cur = m.get(p.provider.slug) ?? { input: null, output: null, observed: p.observed_at, url: p.source_url, tier: p.tier };346      const i = num(p.input_per_mtok);347      const o = num(p.output_per_mtok);348      if (i !== null && (cur.input === null || i < cur.input)) cur.input = i;349      if (o !== null && (cur.output === null || o < cur.output)) cur.output = o;350      m.set(p.provider.slug, cur);351    }352    return m;353  });354  if (!providers.size) return null;355  const rows = [...providers.values()].sort((a, b) => a.name.localeCompare(b.name));356  return (357    <GroupRows label="Pricing · cheapest deployment per provider (USD / 1M in / out)" cols={n + 1}>358      {rows.map((prov) => (359        <tr key={prov.slug}>360          <th scope="row" className="sticky left-0 z-10 bg-canvas text-left" style={{ whiteSpace: 'normal', textTransform: 'none', letterSpacing: 0, fontSize: '0.8125rem', fontWeight: 400 }}>361            <EntityLink e={prov} />362          </th>363          {per.map((m, i) => {364            const b = m.get(prov.slug);365            return (366              <td key={res.items[i]!.entity.id} className="tnum align-top" style={{ whiteSpace: 'normal' }}>367                {b ? (368                  <>369                    <span className="text-accent-2">370                      {fmtUsdPerM(b.input)} <span className="text-ink-3">/</span> {fmtUsdPerM(b.output)}371                    </span>372                    <span className="mt-0.5 hidden items-center gap-1 text-[10.5px] text-ink-3 md:flex">373                      <TierBadge tier={b.tier} /> {b.url ? <a href={b.url} target="_blank" rel="noopener noreferrer" className="max-w-[9rem] truncate hover:text-accent">{hostOf(b.url)}</a> : null} · {fmtAgo(b.observed)}374                    </span>375                  </>376                ) : (377                  <span className="text-ink-3">{DASH}</span>378                )}379              </td>380            );381          })}382        </tr>383      ))}384    </GroupRows>385  );386}387