import type { Metadata } from 'next'; import Link from 'next/link'; import { ChangeRow } from '@/components/changes/change-row'; import { Bars } from '@/components/charts/charts'; import { ChipRow, type ChipItem } from '@/components/timeline/chip-row'; import { EntityBadge } from '@/components/ui/badges'; import { Container, Note, PageHeader } from '@/components/ui/section'; import { EmptyState, Unavailable } from '@/components/ui/unavailable'; import { api, safe } from '@/lib/api'; import { fmtInt, fmtMonth, fmtYear } from '@/lib/format'; import { CATEGORY_LABELS, categoryLabel, routes, SITE_NAME } from '@/lib/site'; export const revalidate = 600; type SP = Record; const LIMIT = 400; function current(sp: SP) { const out: { entity?: string; year?: string; category?: string } = {}; if (sp.entity) out.entity = sp.entity; if (sp.year && /^\d{4}$/.test(sp.year)) out.year = sp.year; if (sp.category) out.category = sp.category; return out; } export async function generateMetadata({ searchParams }: { searchParams: Promise }): Promise { const cur = current(await searchParams); const bits: string[] = []; if (cur.entity) bits.push(cur.entity); if (cur.category) bits.push(categoryLabel(cur.category).toLowerCase()); if (cur.year) bits.push(cur.year); const title = bits.length ? `Timeline — ${bits.join(' · ')}` : 'Timeline — the AI ecosystem month by month'; return { title, description: 'Change events grouped by month: releases, price moves, deprecations and benchmark results across the whole atlas or for one entity, each linked to its source.', alternates: { canonical: routes.timeline(cur) }, openGraph: { title: `${title} | ${SITE_NAME}` }, robots: cur.entity ? { index: false, follow: true } : undefined, }; } export default async function TimelinePage({ searchParams }: { searchParams: Promise }) { const sp = await searchParams; const cur = current(sp); const [res, cats, stats, entity] = await Promise.all([safe(api.timeline({ ...cur, limit: LIMIT })), safe(api.changesCategories(365)), safe(api.stats()), cur.entity ? safe(api.entity(cur.entity)) : Promise.resolve(null)]); const months = res?.items ?? []; const shown = months.reduce((n, m) => n + m.events.length, 0); const total = res?.total ?? shown; // Years: current UTC year back to the earliest year the atlas knows (first entity or earliest month returned). const thisYear = new Date().getUTCFullYear(); 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); const earliest = earliestCandidates.length ? Math.min(...earliestCandidates) : thisYear; const years: string[] = []; for (let y = thisYear; y >= earliest; y--) years.push(String(y)); // Categories: live counts over the last year, summed per category; fall back to the vocabulary when unavailable. const catCounts = new Map(); for (const it of cats?.items ?? []) catCounts.set(it.category, (catCounts.get(it.category) ?? 0) + (Number(it.count) || 0)); const catKeys = catCounts.size ? [...catCounts.entries()].sort((a, b) => b[1] - a[1]).map(([k]) => k) : Object.keys(CATEGORY_LABELS); if (cur.category && !catKeys.includes(cur.category)) catKeys.push(cur.category); const yearChips: ChipItem[] = [{ href: routes.timeline({ ...cur, year: undefined }), label: 'All years', active: !cur.year }, ...years.map((y) => ({ href: routes.timeline({ ...cur, year: y }), label: y, active: cur.year === y }))]; const catChips: ChipItem[] = [{ href: routes.timeline({ ...cur, category: undefined }), label: 'All categories', active: !cur.category }, ...catKeys.map((c) => ({ href: routes.timeline({ ...cur, category: c }), label: categoryLabel(c), count: catCounts.size ? catCounts.get(c) ?? 0 : undefined, active: cur.category === c }))]; // Density strip, oldest → newest (the list below is newest first). const density = [...months].reverse().map((m) => ({ label: fmtMonth(m.month), value: m.count ?? m.events.length })); return ( Timeline ) : ( 'Timeline' ) } title={ entity ? ( <> Timeline of {entity.name} ) : cur.entity ? ( <>Timeline of {cur.entity} ) : ( 'The ecosystem, month by month' ) } lede={entity ? <>Every recorded change for this {entity.entity_type.replace(/_/g, ' ')}{entity.organization ? ` by ${entity.organization.name}` : ''}, newest first. Back to the entity page → : 'Events grouped by month, newest first. Every entry links to the entity and to the source that stated the change.'} aside={res ?

{fmtInt(shown)}{total > shown ? ` of ${fmtInt(total)}` : ''} events

: undefined} >

Year

Category {cats ? · events in the last 365 days : null}

{cur.entity && (

Filtered to one entity. Show the whole atlas

)}
{!res ? ( ) : months.length === 0 ? ( Try another year or category, or open the live feed. ) : ( <> {density.length > 1 && (

Events per month · {fmtInt(density.length)} months shown

)}
{months.map((m) => (

{fmtMonth(m.month)} {fmtInt(m.count ?? m.events.length)} {(m.count ?? m.events.length) > m.events.length && {fmtInt(m.events.length)} shown}

    {m.events.map((e) => ( ))}
))} Showing up to {fmtInt(LIMIT)} events{total > shown ? ` of ${fmtInt(total)} matching` : ''}. Narrow by year or category, or use the changes feed for cursor-paged history. Times are UTC.
)}
); }