HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
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