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%
7.8 KB · 116 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { ComparePicker } from '@/components/compare/compare-picker';4import type { TrayItem } from '@/components/compare/compare-store';5import { CompareTerminal } from '@/components/compare/compare-terminal';6import { EntityBadge } from '@/components/ui/badges';7import { Container, Note, PageHeader } from '@/components/ui/section';8import { EmptyState, Unavailable } from '@/components/ui/unavailable';9import { api, ApiError, apiD1, safe } from '@/lib/api';10import { cn } from '@/lib/cn';11import { fmtInt } from '@/lib/format';12import { routes, SITE_NAME, SITE_URL, typeLabel } from '@/lib/site';13import type { ComparePayload11 } from '@/lib/types';1415export const revalidate = 300;1617type SP = { ids?: string; mode?: string; diff_only?: string };18const MODES = [19  { id: 'models', label: 'Models', type: 'model' },20  { id: 'providers', label: 'Providers', type: 'provider' },21  { id: 'hardware', label: 'Hardware', type: 'hardware' },22  { id: 'companies', label: 'Companies', type: 'company' },23  { id: 'frameworks', label: 'Frameworks', type: 'framework' },24];2526function parseIds(raw: string | undefined): string[] {27  return [...new Set((raw ?? '').split(',').map((s) => s.trim()).filter(Boolean))].slice(0, 6);28}2930/** Resolve the comparison, distinguishing a semantic 400 (mixed types / unknown slug) from an outage. */31async function load(ids: string[], mode: string | undefined, diffOnly: boolean): Promise<{ res: ComparePayload11 | null; error: string | null }> {32  if (ids.length < 2) return { res: null, error: null };33  try {34    return { res: await apiD1.compare(ids, { diff_only: diffOnly, mode }), error: null };35  } catch (e) {36    if (e instanceof ApiError && (e.status === 400 || e.status === 404 || e.status === 422)) return { res: null, error: e.detail ?? 'These entities cannot be compared.' };37    return { res: null, error: null };38  }39}4041export async function generateMetadata({ searchParams }: { searchParams: Promise<SP> }): Promise<Metadata> {42  const sp = await searchParams;43  const ids = parseIds(sp.ids);44  const res = ids.length >= 2 ? await safe(apiD1.compare(ids, { mode: sp.mode })) : null;45  const canonical = routes.compare(ids.length >= 2 ? ids : undefined);46  if (!res) return { title: 'Compare — models, providers, hardware side by side', description: 'Compare 2–6 entities of one type on every recorded dimension — architecture, context, capabilities, comparable benchmarks, cheapest deployments, licences — with the source of each value.', alternates: { canonical } };47  const names = res.items.map((i) => i.entity.name);48  const title = `${names.join(' vs ')} — ${typeLabel(res.entity_type).toLowerCase()} comparison`;49  const description = `${names.join(', ')} compared on ${res.dimensions.length} recorded dimensions (comparable benchmark groups only) with the source of every value. ${SITE_NAME}.`.slice(0, 300);50  return { title, description, alternates: { canonical }, openGraph: { title: `${title} | ${SITE_NAME}`, description, url: `${SITE_URL}${canonical}`, type: 'website' } };51}5253export default async function ComparePage({ searchParams }: { searchParams: Promise<SP> }) {54  const sp = await searchParams;55  const ids = parseIds(sp.ids);56  const diffOnly = sp.diff_only === '1';57  const mode = MODES.some((m) => m.id === sp.mode) ? sp.mode : undefined;58  const [{ res, error }, examples] = await Promise.all([load(ids, mode, diffOnly), ids.length < 2 ? safe(api.models({ limit: 3, sort: 'quality' })) : Promise.resolve(null)]);59  const initial: TrayItem[] = res ? res.items.map((it) => ({ slug: it.entity.slug, name: it.entity.name, entity_type: it.entity.entity_type, organization: it.entity.organization?.name ?? null })) : [];60  const exampleHref = examples && examples.items.length >= 2 ? routes.compare(examples.items.slice(0, 3).map((m) => m.slug)) : null;61  const activeMode = res ? MODES.find((m) => m.type === res.entity_type || (m.type === 'company' && ['organization', 'lab', 'university'].includes(res.entity_type)) || (m.type === 'framework' && ['library', 'runtime'].includes(res.entity_type)))?.id ?? 'models' : mode ?? 'models';62  const ld = res ? { '@context': 'https://schema.org', '@type': 'Dataset', name: `${res.items.map((i) => i.entity.name).join(' vs ')} — comparison`, url: `${SITE_URL}${routes.compare(ids)}`, about: res.items.map((i) => ({ '@type': 'Thing', name: i.entity.name, url: `${SITE_URL}${routes.entity(i.entity)}` })) } : null;6364  return (65    <Container wide>66      {ld && <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }} />}67      <PageHeader68        eyebrow={69          res ? (70            <>71              <span>Compare</span>72              <EntityBadge type={res.entity_type} small />73            </>74          ) : (75            'Compare'76          )77        }78        title={res ? res.items.map((i) => i.entity.name).join(' vs ') : 'Compare side by side'}79        lede={res ? `${fmtInt(res.items.length)} ${typeLabel(res.entity_type, true).toLowerCase()} · ${fmtInt(res.dimensions.length)} recorded dimensions${diffOnly ? ' (differences only)' : ''}. Each cell is the value as stated by its source; benchmarks only where every entity sits in the same comparability group.` : 'Two to six entities of one type — models, providers, hardware, companies or frameworks. Each cell shows the recorded value, its source and tier; benchmarks are only compared inside the same comparability group.'}80      >81        <nav aria-label="Compare mode" className="no-scrollbar -mx-4 mt-5 flex gap-1 overflow-x-auto border-b border-rule px-4 md:mx-0 md:px-0" data-compare-modes>82          {MODES.map((m) => {83            const on = m.id === activeMode;84            const keep = res && m.id === activeMode ? `?ids=${ids.map(encodeURIComponent).join(',')}&mode=${m.id}` : `?mode=${m.id}`;85            return (86              <Link key={m.id} href={`/compare${keep}`} aria-current={on ? 'page' : undefined} className={cn('-mb-px flex h-10 shrink-0 items-center gap-1.5 border-b-2 px-3 text-sm whitespace-nowrap', on ? 'border-ink font-medium text-ink' : 'border-transparent text-ink-2 hover:text-ink')}>87                <EntityBadge type={m.type} small className={on ? '' : 'opacity-60'} /> {m.label}88              </Link>89            );90          })}91        </nav>92        <ComparePicker initial={initial} exampleHref={exampleHref} />93      </PageHeader>9495      <div className="space-y-8 pb-16">96        {ids.length < 2 ? (97          <EmptyState title={`Pick at least two ${MODES.find((m) => m.id === activeMode)?.label.toLowerCase() ?? 'entities'}`}>Search above, or press “Compare” on any listing or entity page — the selection is kept in this browser and in the URL, so it can be shared.</EmptyState>98        ) : error ? (99          <EmptyState title="These entities cannot be compared together">100            {error}. Comparisons work across entities of one type (all models, all providers…){mode ? ` — the “${MODES.find((m) => m.id === mode)?.label}” mode asserts that type` : ''}. Remove the odd one out in the tray above{mode ? ' or switch mode' : ''}.101          </EmptyState>102        ) : !res ? (103          <Unavailable what="Comparison" reason="The API did not answer. Try again in a moment." />104        ) : (105          <>106            <CompareTerminal res={res} diffOnly={diffOnly} />107            <Note>108              Share this comparison with its URL. Values are the atlas as of now; use each entity's change history for how a value moved{res.entity_type === 'model' && res.items.length === 2 ? <>, or the <Link href={`/models/${encodeURIComponent(res.items[0]!.entity.slug)}/diff/${encodeURIComponent(res.items[1]!.entity.slug)}`} className="link">pairwise diff</Link> with deltas</> : null}. <Link href={routes.methodology()} className="link">How AI Atlas records facts →</Link>109            </Note>110          </>111        )}112      </div>113    </Container>114  );115}116