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%
12.5 KB · 191 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { Bars } from '@/components/charts/charts';4import { TerminalLayout } from '@/components/layout/terminal';5import { RailFilters } from '@/components/changes/rail-filters';6import { type ChipItem } from '@/components/timeline/chip-row';7import { occurredAt, TIMELINE_LANES } from '@/components/timeline/lanes';8import { TimelineWorkbench } from '@/components/timeline/timeline-workbench';9import { EntityBadge } from '@/components/ui/badges';10import { withParams } from '@/components/ui/pagination';11import { Container, Note, PageHeader } from '@/components/ui/section';12import { EmptyState, Unavailable } from '@/components/ui/unavailable';13import { api, apiD1, apiD3, safe } from '@/lib/api';14import { fmtInt, fmtMonth, fmtYear, num } from '@/lib/format';15import { CATEGORY_LABELS, categoryLabel, eventLabel, IMPORTANCE_LABELS, routes, SITE_NAME } from '@/lib/site';16import type { ChangeEvent } from '@/lib/types';1718export const revalidate = 600;1920type SP = Record<string, string | undefined>;21const LIMIT = 1000;22const KEYS = ['entity', 'year', 'category', 'type', 'importance_min', 'include_backfill', 'org', 'family'] as const;2324function current(sp: SP): Record<string, string | undefined> {25  const out: Record<string, string | undefined> = {};26  for (const k of KEYS) if (sp[k]) out[k] = sp[k];27  if (out.year && !/^\d{4}$/.test(out.year)) delete out.year;28  if (out.include_backfill !== '1') delete out.include_backfill;29  return out;30}31/** `org` / `family` are shortcuts for the entity-scoped timeline (an organization's timeline includes its models' events). */32function scopeEntity(cur: Record<string, string | undefined>): string | undefined {33  return cur.entity ?? cur.org ?? cur.family;34}3536export async function generateMetadata({ searchParams }: { searchParams: Promise<SP> }): Promise<Metadata> {37  const cur = current(await searchParams);38  const bits: string[] = [];39  const scope = scopeEntity(cur);40  if (scope) bits.push(scope);41  if (cur.category) bits.push(categoryLabel(cur.category).toLowerCase());42  if (cur.type) bits.push(eventLabel(cur.type).toLowerCase());43  if (cur.year) bits.push(cur.year);44  const title = bits.length ? `Timeline — ${bits.join(' · ')}` : 'Timeline — the AI ecosystem, lane by lane';45  return {46    title,47    description: 'Eight lanes — models, research, prices, benchmarks, providers, hardware, frameworks, companies — of source-attributed events keyed on when they occurred, brushable by range, with historical backfill on demand.',48    alternates: { canonical: withParams('/timeline', cur, {}) },49    openGraph: { title: `${title} | ${SITE_NAME}` },50    robots: scope ? { index: false, follow: true } : undefined,51  };52}5354export default async function TimelinePage({ searchParams }: { searchParams: Promise<SP> }) {55  const sp = await searchParams;56  const cur = current(sp);57  const scope = scopeEntity(cur);58  const backfill = cur.include_backfill === '1';59  const [res, cats, stats, entity, orgs, fams, meth] = await Promise.all([60    safe(apiD3.timeline({ entity: scope, year: cur.year, category: cur.category, importance_min: cur.importance_min, include_backfill: backfill ? 1 : undefined, limit: LIMIT })),61    safe(api.changesCategories(365)),62    safe(api.stats()),63    scope ? safe(api.entity(scope)) : Promise.resolve(null),64    safe(api.companies({ limit: 40, sort: 'models' })),65    safe(apiD1.families({ limit: 40, sort: 'models' })),66    safe(apiD3.methodology()),67  ]);68  const months = res?.items ?? [];69  const all: ChangeEvent[] = months.flatMap((m) => m.events);70  const events = cur.type ? all.filter((e) => e.event_type === cur.type) : all;71  const loaded = all.length;72  const capped = loaded >= LIMIT;73  const href = (patch: Record<string, string | number | undefined | null>) => withParams('/timeline', cur, patch);7475  // Years: current UTC year back to the earliest year the atlas knows.76  const thisYear = new Date().getUTCFullYear();77  const earliestCandidates = [stats?.first_entity_at, ...months.map((m) => m.month), cur.year].filter((v): v is string => !!v).map((v) => Number(fmtYear(v))).filter((n) => Number.isFinite(n) && n > 1990);78  const earliest = earliestCandidates.length ? Math.min(...earliestCandidates) : thisYear;79  const years: string[] = [];80  for (let y = thisYear; y >= Math.max(earliest, thisYear - 12); y--) years.push(String(y));8182  const catCounts = new Map<string, number>();83  for (const it of cats?.items ?? []) catCounts.set(it.category, (catCounts.get(it.category) ?? 0) + (num(it.count) ?? 0));84  const catKeys = catCounts.size ? [...catCounts.entries()].sort((a, b) => b[1] - a[1]).map(([k]) => k) : Object.keys(CATEGORY_LABELS);85  if (cur.category && !catKeys.includes(cur.category)) catKeys.push(cur.category);86  const typeCounts = new Map<string, number>();87  for (const e of all) typeCounts.set(e.event_type, (typeCounts.get(e.event_type) ?? 0) + 1);88  const typeKeys = [...typeCounts.entries()].sort((a, b) => b[1] - a[1]).map(([k]) => k);8990  const yearChips: ChipItem[] = [{ href: href({ year: undefined }), label: 'All years', active: !cur.year }, ...years.map((y) => ({ href: href({ year: y }), label: y, active: cur.year === y }))];91  const density = [...months].reverse().map((m) => ({ label: fmtMonth(m.month), value: m.count ?? m.events.length }));92  const activeCount = Object.keys(cur).length;9394  const filters = (95    <RailFilters96      action="/timeline"97      resetHref={routes.timeline()}98      testId="timeline"99      fields={[100        { kind: 'chips', label: 'Year', items: yearChips.map((c) => ({ href: c.href, label: c.label, active: c.active })) },101        { kind: 'select', name: 'category', label: 'Category (lane)', value: cur.category, any: 'All lanes', options: catKeys.map((c) => ({ value: c, label: `${categoryLabel(c)}${catCounts.size ? ` (${fmtInt(catCounts.get(c) ?? 0)})` : ''}` })) },102        { kind: 'select', name: 'type', label: 'Event type', value: cur.type, any: 'All types', options: [...typeKeys.map((t) => ({ value: t, label: `${eventLabel(t)} (${fmtInt(typeCounts.get(t) ?? 0)})` })), ...(cur.type && !typeKeys.includes(cur.type) ? [{ value: cur.type, label: `${eventLabel(cur.type)} (0 loaded)` }] : [])], note: 'Filters the loaded events (the API has no type filter on /timeline).' },103        { kind: 'select', name: 'org', label: 'Organization', value: cur.org, any: 'Whole atlas', remote: 'companies', options: (orgs?.items ?? []).filter((o) => o.slug === cur.org).map((o) => ({ value: o.slug, label: `${o.name} (${fmtInt(o.model_count)})` })) },104        { kind: 'select', name: 'family', label: 'Family', value: cur.family, any: 'Any family', remote: 'families', options: (fams?.items ?? []).filter((f) => f.slug === cur.family).map((f) => ({ value: f.slug, label: `${f.name} (${fmtInt(f.model_count)})` })), note: 'Organization and family scope the timeline to that entity (one at a time).' },105        { kind: 'select', name: 'importance_min', label: 'Min importance', value: cur.importance_min, options: [3, 2, 1].map((n) => ({ value: String(n), label: `${IMPORTANCE_LABELS[n]} (≥ ${n})` })) },106        { kind: 'checkbox', name: 'include_backfill', label: 'Include historical backfill', checked: backfill, hint: 'Backfill = history imported when a source is first crawled (release dates, past prices). Off by default so the timeline shows what AI Atlas observed happening; on, it shows the reconstructed history.' },107        ...(cur.year ? [{ kind: 'hidden' as const, name: 'year', value: cur.year }] : []),108        ...(cur.entity ? [{ kind: 'hidden' as const, name: 'entity', value: cur.entity }] : []),109      ]}110    />111  );112113  const inspector = (114    <div className="space-y-4 text-sm">115      <div>116        <p className="eyebrow mb-1.5">Lanes</p>117        <ul className="space-y-1">118          {TIMELINE_LANES.filter((l) => l.key !== 'other' || all.some((e) => l.categories.length === 0 && !TIMELINE_LANES.some((x) => x.categories.includes(e.category)))).map((l) => {119            const n = all.filter((e) => (TIMELINE_LANES.find((x) => x.categories.includes(e.category))?.key ?? 'other') === l.key).length;120            return (121              <li key={l.key} className="flex items-center justify-between gap-2">122                <span className="flex items-center gap-2">123                  <span className="inline-block size-2.5 rounded-full" style={{ background: l.color }} aria-hidden /> {l.label}124                </span>125                <span className="tnum text-xs text-ink-3">{fmtInt(n)}</span>126              </li>127            );128          })}129        </ul>130      </div>131      <div>132        <p className="eyebrow mb-1.5">Semantics</p>133        <dl className="kv [&>div]:py-1 text-xs">134          {Object.entries(meth?.event_semantics ?? {}).slice(0, 5).map(([k, v]) => (135            <div key={k}>136              <dt className="mono">{k}</dt>137              <dd className="text-ink-2">{v}</dd>138            </div>139          ))}140          {!meth?.event_semantics && <div><dd className="text-ink-3">Definitions unavailable.</dd></div>}141        </dl>142      </div>143      <p className="text-xs text-ink-3">144        Times are UTC. Per-entity history: open any entity's History tab. Compare two dates in <Link href={routes.diff()} className="link">Diff</Link>; see the atlas as of a date in the <Link href={routes.timeMachine()} className="link">Time machine</Link>.145      </p>146    </div>147  );148149  return (150    <>151      <Container wide>152        <PageHeader153          eyebrow={entity ? <><EntityBadge type={entity.entity_type} small /> Timeline</> : 'Timeline 2.0'}154          title={entity ? <>Timeline of <Link href={routes.entity(entity)} className="text-ink-2 hover:text-accent">{entity.name}</Link></> : scope ? <>Timeline of <span className="mono text-ink-2">{scope}</span></> : 'The ecosystem, lane by lane'}155          lede={entity ? <>Every recorded change for this {entity.entity_type.replace(/_/g, ' ')}{entity.organization ? ` by ${entity.organization.name}` : ''}{['company', 'organization', 'lab', 'university'].includes(entity.entity_type) ? ' and for the models it develops' : ''}, keyed on when it occurred. <Link href={routes.entity(entity)} className="link">Back to the entity →</Link></> : 'Models · Research · Prices · Benchmarks · Providers · Hardware · Frameworks · Companies. Dots are events sized by importance; drag on the lanes to zoom the list to a range. Everything links to its entity and to the source that stated it.'}156          aside={res ? <p className="tnum text-sm text-ink-3">{fmtInt(events.length)}{cur.type ? ` of ${fmtInt(loaded)} loaded` : ''} events{capped ? ` · first ${fmtInt(LIMIT)}` : ''}</p> : undefined}157          className="pb-3"158        />159      </Container>160      <div className="pb-16">161        <TerminalLayout filters={filters} inspector={inspector} filtersTitle="Filters" inspectorTitle="Legend" storageKey="aia-timeline-inspector" filterCount={activeCount}>162          {!res ? (163            <Unavailable what="Timeline" reason={scope ? 'The entity slug may not exist.' : undefined} />164          ) : events.length === 0 ? (165            <EmptyState title="No events for this selection">166              {backfill ? 'Try another year, lane or scope, or open ' : 'Nothing was observed live for this selection — turn on “include historical backfill” for reconstructed history, or open '}167              <Link href={routes.changes()} className="link">the live feed</Link>.168            </EmptyState>169          ) : (170            <>171              {density.length > 1 && (172                <section aria-label="Events per month" className="mb-6">173                  <p className="eyebrow mb-2">174                    Events per month <span className="tnum normal-case tracking-normal text-ink-3">· {fmtInt(density.length)} months loaded</span>175                  </p>176                  <Bars data={density} height={72} />177                </section>178              )}179              <TimelineWorkbench events={events} semantics={meth?.event_semantics ?? null} backfill={backfill} />180              <Note className="mt-4">181                Showing up to {fmtInt(LIMIT)} events{capped ? ' — the API cap; narrow by year, lane or scope to see everything' : ''}. Dates are <span className="mono">occurred_at</span> (effective date when a source states one, else observation) — hover a date for the observation time.{!backfill && ' Historical backfill is excluded; the checkbox in the rail includes it.'} Cursor-paged history: <Link href={routes.changes()} className="link">changes feed</Link>.182              </Note>183              {events.length > 0 && <p className="sr-only">Earliest loaded event: {occurredAt(events[events.length - 1]!)}</p>}184            </>185          )}186        </TerminalLayout>187      </div>188    </>189  );190}191