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%
15.6 KB · 231 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { DataStrip, SectionNav } from '@/components/layout/terminal';4import { BreadcrumbLd } from '@/components/meta/breadcrumb-ld';5import { daysBefore, firstOfMonth, firstOfQuarter, ISO_DAY, monthsBefore, todayUtc } from '@/components/temporal/dates';6import { ContextChangesSection, EntitiesSection, EventsSection, LeadersSection, NewEntitiesSection, PriceChangesSection } from '@/components/temporal/diff-sections';7import { Container, Note, PageHeader } from '@/components/ui/section';8import { Unavailable } from '@/components/ui/unavailable';9import { api, ApiError, apiD1, apiD3, safe } from '@/lib/api';10import { fmtDate, fmtInt, num } from '@/lib/format';11import { routes, SITE_NAME, SITE_URL } from '@/lib/site';12import type { ChangeEvent, DiffPayload11, EntitySummary } from '@/lib/types';1314type SP = { a?: string; b?: string; scope?: string; scope_custom?: string; include_backfill?: string };15const LIMIT = 200;1617function resolve(sp: SP): { a: string; b: string; scope: string; valid: boolean; backfill: boolean } {18  const b = sp.b && ISO_DAY.test(sp.b) ? sp.b : todayUtc();19  const a = sp.a && ISO_DAY.test(sp.a) ? sp.a : daysBefore(b, 7);20  const scope = (sp.scope_custom?.trim() || sp.scope || 'all').trim() || 'all';21  const valid = (!sp.a || ISO_DAY.test(sp.a)) && (!sp.b || ISO_DAY.test(sp.b)) && a < b;22  return { a, b, scope, valid, backfill: sp.include_backfill === '1' };23}24function href(a: string, b: string, scope: string, backfill = false): string {25  const p = new URLSearchParams({ a, b });26  if (scope !== 'all') p.set('scope', scope);27  if (backfill) p.set('include_backfill', '1');28  return `/diff?${p.toString()}`;29}30function describeScope(s: DiffPayload11['scope'], fallback: string): string {31  if (!s) return fallback === 'all' ? 'the whole atlas' : fallback;32  if (typeof s === 'string') return s;33  const kind = typeof s.kind === 'string' ? s.kind : fallback;34  if (kind === 'all') return 'the whole atlas';35  const org = s.organization as { name?: string } | undefined;36  if (kind === 'org' && org?.name) return `organization · ${org.name}`;37  if (kind === 'family' && typeof s.family === 'string') return `family · ${s.family}`;38  if (kind === 'models') return 'models only';39  return kind;40}4142export async function generateMetadata({ searchParams }: { searchParams: Promise<SP> }): Promise<Metadata> {43  const { a, b, scope } = resolve(await searchParams);44  const title = `Diff the AI world — ${fmtDate(a)} → ${fmtDate(b)}${scope !== 'all' ? ` (${scope})` : ''}`;45  const description = `Every recorded change in the AI ecosystem between ${a} and ${b}: new and retired models, price and context changes, new benchmark leaders, papers, provider and hardware changes — from the change log, nothing inferred.`;46  const og = `${SITE_URL}/diff/og?a=${a}&b=${b}&scope=${encodeURIComponent(scope)}`;47  return { title, description, alternates: { canonical: href(a, b, scope) }, openGraph: { title: `${title} | ${SITE_NAME}`, description, url: `${SITE_URL}${href(a, b, scope)}`, images: [{ url: og, width: 1200, height: 630 }] }, twitter: { card: 'summary_large_image', images: [og] } };48}4950export default async function DiffPage({ searchParams }: { searchParams: Promise<SP> }) {51  const sp = await searchParams;52  const { a, b, scope, valid, backfill } = resolve(sp);53  const today = todayUtc();54  const [orgs, fams] = await Promise.all([safe(api.companies({ limit: 40, sort: 'models' })), safe(apiD1.families({ limit: 40, sort: 'models' }))]);55  let payload: DiffPayload11 | null = null;56  let error: string | null = null;57  if (!valid) error = 'Dates must be YYYY-MM-DD and the first date must come before the second.';58  else {59    try {60      payload = await apiD3.diff(a, b, scope, LIMIT, backfill);61    } catch (e) {62      error = e instanceof ApiError && e.status === 400 ? e.detail ?? 'The API rejected these parameters.' : null;63    }64  }65  const cls = 'h-11 w-full border border-rule bg-surface px-2.5 text-sm text-ink focus:border-accent focus:outline-none';66  const orgOptions = (orgs?.items ?? []).map((o) => ({ value: `org:${o.slug}`, label: `${o.name} (${fmtInt(o.model_count)} models)` }));67  const famOptions = (fams?.items ?? []).filter((f) => f.canonical).map((f) => ({ value: `family:${f.slug}`, label: `${f.name} (${fmtInt(f.model_count)})` }));68  const knownScopes = new Set(['all', 'models', ...orgOptions.map((o) => o.value), ...famOptions.map((o) => o.value)]);69  const custom = knownScopes.has(scope) ? '' : scope;70  const c = payload?.counts ?? {};71  const cnt = (k: string): number | null => num(c[k]);7273  // Split new entities by type (the API's new_entities is every type for scope=all).74  const newAll: EntitySummary[] = payload?.new_entities ?? [];75  const newModels = newAll.filter((e) => e.entity_type === 'model');76  const newPapers = newAll.filter((e) => e.entity_type === 'paper');77  const newOther = newAll.filter((e) => e.entity_type !== 'model' && e.entity_type !== 'paper');78  const newTotal = cnt('new_entities');79  const listCapped = newTotal !== null && newTotal > newAll.length;80  const retired: EntitySummary[] = (payload?.retired_models ?? []).map((r) => ('entity' in (r as ChangeEvent) && (r as ChangeEvent).entity ? ((r as ChangeEvent).entity as EntitySummary) : (r as EntitySummary))).filter((e) => e && e.slug);8182  const presets: { label: string; a: string; b: string }[] = [83    { label: 'Last 7 days', a: daysBefore(today, 7), b: today },84    { label: 'Since the 1st', a: firstOfMonth(today), b: today },85    { label: 'Last 30 days', a: monthsBefore(today, 1), b: today },86    { label: 'This quarter', a: firstOfQuarter(today), b: today },87    { label: 'Last 90 days', a: daysBefore(today, 90), b: today },88  ];89  const sections = payload90    ? [91        { id: 'new-models', label: 'New models' },92        { id: 'retired', label: 'Retired' },93        { id: 'prices', label: 'Prices' },94        { id: 'context', label: 'Context' },95        { id: 'leaders', label: 'Leaders' },96        { id: 'papers', label: 'Papers' },97        { id: 'providers', label: 'Providers' },98        { id: 'hardware', label: 'Hardware' },99        { id: 'properties', label: 'Properties' },100        { id: 'gone', label: 'Gone' },101      ]102    : [];103104  return (105    <Container wide>106      <BreadcrumbLd items={[{ name: SITE_NAME, href: '/' }, { name: 'Diff', href: '/diff' }, { name: `${a} → ${b}`, href: href(a, b, scope) }]} />107      <PageHeader108        eyebrow="Diff the AI world"109        title={110          <>111            What changed between <span className="tnum">{fmtDate(a)}</span> and <span className="tnum">{fmtDate(b)}</span>112          </>113        }114        lede="Two dates, one scope: new and retired models, price and context changes, new benchmark leaders, new papers, provider and hardware changes. Built from the change log keyed on when things occurred — nothing is inferred, and the URL is the report."115        aside={payload ? <p className="text-sm text-ink-3">Scope: <span className="text-ink-2">{describeScope(payload.scope, scope)}</span></p> : undefined}116      >117        <form action="/diff" method="get" className="mt-6 grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-6" data-diff-form>118          <label className="block min-w-0">119            <span className="eyebrow block pb-1">From (UTC)</span>120            <input type="date" name="a" defaultValue={a} max={today} className={cls} required />121          </label>122          <label className="block min-w-0">123            <span className="eyebrow block pb-1">To (UTC)</span>124            <input type="date" name="b" defaultValue={b} max={today} className={cls} required />125          </label>126          <label className="block min-w-0">127            <span className="eyebrow block pb-1">Scope</span>128            <select name="scope" defaultValue={custom ? '' : scope} className={cls}>129              <option value="all">Everything</option>130              <option value="models">Models only</option>131              {orgOptions.length > 0 && (132                <optgroup label="Organization">133                  {orgOptions.map((o) => (134                    <option key={o.value} value={o.value}>135                      {o.label}136                    </option>137                  ))}138                </optgroup>139              )}140              {famOptions.length > 0 && (141                <optgroup label="Family">142                  {famOptions.map((o) => (143                    <option key={o.value} value={o.value}>144                      {o.label}145                    </option>146                  ))}147                </optgroup>148              )}149              {custom && <option value="">Custom (below)</option>}150            </select>151          </label>152          <label className="block min-w-0">153            <span className="eyebrow block pb-1">Custom scope</span>154            <input name="scope_custom" defaultValue={custom} placeholder="org:<slug> · family:<slug>" className={cls} />155          </label>156          <label className="flex min-w-0 items-end">157            <span className="flex h-11 items-center gap-2 text-sm text-ink-2">158              <input type="checkbox" name="include_backfill" value="1" defaultChecked={backfill} className="size-4 accent-[var(--accent)]" /> include backfill159            </span>160          </label>161          <div className="flex items-end gap-2">162            <button type="submit" className="inline-flex h-11 flex-1 items-center justify-center bg-ink px-3 text-sm font-medium text-canvas hover:opacity-90">163              Compare dates164            </button>165            <Link href={routes.diff()} className="inline-flex h-11 items-center border border-rule px-3 text-sm text-ink-2 hover:text-ink">166              Reset167            </Link>168          </div>169        </form>170        <ul className="no-scrollbar -mx-4 mt-3 flex gap-1 overflow-x-auto px-4 md:mx-0 md:px-0" aria-label="Presets">171          {presets.map((p) => {172            const on = p.a === a && p.b === b;173            return (174              <li key={p.label} className="shrink-0">175                <Link href={href(p.a, p.b, scope, backfill)} className={`inline-flex h-9 items-center border px-2.5 text-sm ${on ? 'border-ink bg-ink text-canvas' : 'border-rule text-ink-2 hover:border-rule-strong hover:text-ink'}`} aria-current={on ? 'true' : undefined}>176                  {p.label}177                </Link>178              </li>179            );180          })}181        </ul>182      </PageHeader>183184      <div className="pb-16">185        {error ? (186          <Unavailable what="Diff" reason={error} />187        ) : !payload ? (188          <Unavailable what="Diff" reason="The change log could not be read right now." />189        ) : (190          <>191            <DataStrip192              dense193              items={[194                { label: 'New models', value: fmtInt(listCapped ? null : newModels.length), hint: listCapped ? `${fmtInt(newTotal)} new entities in total` : undefined, definition: 'Canonical models first seen between the two dates (artifacts excluded unless include=artifacts).', href: '#new-models' },195                { label: 'Retired models', value: fmtInt(cnt('retired_models') ?? retired.length), definition: 'Models whose status became retired/deprecated in the window.', href: '#retired' },196                { label: 'Price changes', value: fmtInt(cnt('price_changes') ?? payload.price_changes.length), definition: 'PRICE_CHANGED events that occurred in the window.', href: '#prices' },197                { label: 'Context changes', value: fmtInt(cnt('context_changes') ?? payload.context_changes?.length), definition: 'CONTEXT_CHANGED events in the window.', href: '#context' },198                { label: 'New leaders', value: fmtInt(cnt('new_benchmark_leaders') ?? payload.new_benchmark_leaders?.length), definition: 'Benchmarks whose primary-group leader differs between the two dates.', href: '#leaders' },199                { label: 'New papers', value: fmtInt(listCapped ? null : newPapers.length), definition: 'Papers first seen in the window (from the new-entities list).', href: '#papers' },200                { label: 'Provider changes', value: fmtInt(cnt('provider_changes') ?? payload.provider_changes?.length), definition: 'Listings and delistings by providers.', href: '#providers' },201                { label: 'Hardware changes', value: fmtInt(cnt('hardware_changes') ?? payload.hardware_changes?.length), definition: 'Hardware entities and spec changes.', href: '#hardware' },202              ]}203            />204            <Note className="mt-2">205              {fmtInt(cnt('events'))} events in the window · {fmtInt(cnt('claims_superseded'))} claims superseded · price rows opened {fmtInt(cnt('price_rows_opened'))} / closed {fmtInt(cnt('price_rows_closed'))} · entities at {fmtDate(a)}: {fmtInt(cnt('entities_at_a'))} → at {fmtDate(b)}: {fmtInt(cnt('entities_at_b'))}.206              {listCapped && <> The entity lists are capped at {fmtInt(LIMIT)} by the API — per-type counts below are “of the first {fmtInt(LIMIT)}”.</>}207              {payload.note && <> {payload.note}</>}208            </Note>209            <SectionNav items={sections} className="mt-4" />210211            <NewEntitiesSection id="new-models" title="New models" items={newModels} total={listCapped ? null : newModels.length} empty="No new canonical model between these dates" type="model" />212            <EntitiesSection id="retired" title="Retired models" items={retired} total={cnt('retired_models')} empty="No model was retired between these dates" />213            <PriceChangesSection id="prices" title="Price changes" items={payload.price_changes} total={cnt('price_changes')} empty="No price change between these dates" />214            <ContextChangesSection id="context" title="Context changes" items={payload.context_changes ?? []} total={cnt('context_changes')} empty="No context-window change between these dates" />215            <LeadersSection id="leaders" items={payload.new_benchmark_leaders ?? []} a={a} b={b} />216            <NewEntitiesSection id="papers" title="New papers" items={newPapers} total={listCapped ? null : newPapers.length} empty="No new paper between these dates" type="paper" />217            <PriceChangesSection id="providers" title="Provider changes" items={payload.provider_changes ?? []} total={cnt('provider_changes')} empty="No provider listing or delisting between these dates" />218            <EventsSection id="hardware" title="Hardware changes" items={payload.hardware_changes ?? []} total={cnt('hardware_changes')} empty="No hardware change between these dates" />219            <EventsSection id="properties" title="Other property changes" items={payload.property_changes} total={cnt('property_changes')} empty="No other property change between these dates" lede="Openness, licence, status, parameters, release-date and other material property changes (context changes are listed above)." />220            {newOther.length > 0 && <NewEntitiesSection id="new-other" title="Other new entities" items={newOther} total={listCapped ? null : newOther.length} empty="" type="other" />}221            <EntitiesSection id="gone" title="Gone entities" items={payload.gone_entities} total={cnt('gone_entities')} empty="No entity disappeared between these dates" />222            <Note className="mt-6">223              “Gone” means an entity present at the first date is no longer current at the second (merged or retired) — its record and history are kept. Events are keyed on <span className="mono">occurred_at</span>{backfill ? ' and include back-filled history' : '; back-filled history is excluded (tick “include backfill” to add it)'}. Per-entity history: any entity's History tab. <Link href={routes.timeMachine(a)} className="link">Atlas as of {fmtDate(a)}</Link> · <Link href={routes.methodology()} className="link">Methodology →</Link>224            </Note>225          </>226        )}227      </div>228    </Container>229  );230}231