import type { Metadata } from 'next'; import Link from 'next/link'; import { ChangeRow, groupByDay } from '@/components/changes/change-row'; import { LoadMore } from '@/components/changes/load-more'; import { RailFilters } from '@/components/changes/rail-filters'; import { TerminalLayout } from '@/components/layout/terminal'; import { ActiveFilters } from '@/components/listing/filters'; import { withParams } from '@/components/ui/pagination'; import { Container, Note, PageHeader } from '@/components/ui/section'; import { EmptyState, Unavailable } from '@/components/ui/unavailable'; import { api, apiD3, safe } from '@/lib/api'; import { fmtDate, fmtInt, num } from '@/lib/format'; import { categoryLabel, eventLabel, IMPORTANCE_LABELS, routes, typeLabel } from '@/lib/site'; export const metadata: Metadata = { title: 'Changes — what changed in AI, as it happens', description: 'A live, source-attributed feed of changes in the AI ecosystem keyed on when they occurred: new models, price moves, context changes, deprecations, benchmark results, announcements. Filter by category, type, entity type and importance.', alternates: { canonical: '/changes' }, }; export const revalidate = 60; type SP = Record; const KEYS = ['category', 'type', 'entity_type', 'importance_min', 'since', 'until', 'q', 'include_backfill', 'date_field', 'entity'] as const; const LIMIT = 50; const ENTITY_TYPES = ['model', 'company', 'paper', 'provider', 'benchmark', 'hardware', 'framework', 'dataset', 'artifact', 'model_family']; export default async function ChangesPage({ searchParams }: { searchParams: Promise }) { const sp = await searchParams; const current: Record = {}; for (const k of KEYS) if (sp[k]) current[k] = sp[k]; if (current.include_backfill !== '1') delete current.include_backfill; if (current.date_field !== 'observed') delete current.date_field; const dateField: 'occurred' | 'observed' = current.date_field === 'observed' ? 'observed' : 'occurred'; const [page, cats, meth] = await Promise.all([safe(apiD3.changes({ ...current, limit: LIMIT })), safe(api.changesCategories(7)), safe(apiD3.methodology())]); const href = (patch: Record) => withParams('/changes', current, patch); const qs = new URLSearchParams(Object.entries(current).filter(([, v]) => v) as [string, string][]).toString(); const groups = groupByDay(page?.items ?? [], dateField); const cursor = page && page.items.length >= LIMIT ? page.next_before ?? (dateField === 'occurred' ? page.items[page.items.length - 1]?.occurred_at ?? page.items[page.items.length - 1]?.observed_at : page.items[page.items.length - 1]?.observed_at) ?? null : null; const catCounts = new Map(); const typeCounts = new Map(); for (const c of cats?.items ?? []) { catCounts.set(c.category, (catCounts.get(c.category) ?? 0) + (num(c.count) ?? 0)); typeCounts.set(c.event_type, (typeCounts.get(c.event_type) ?? 0) + (num(c.count) ?? 0)); } const catOptions = [...catCounts.entries()].sort((a, b) => b[1] - a[1]); const typeOptions = [...typeCounts.entries()].sort((a, b) => b[1] - a[1]); const today = new Date().toISOString().slice(0, 10); const activeCount = Object.keys(current).filter((k) => k !== 'date_field').length; const sem = meth?.event_semantics ?? {}; const filters = ( ({ value: v, label: `${categoryLabel(v)} (${fmtInt(n)})` })) }, { kind: 'select', name: 'type', label: 'Event type', value: current.type, options: typeOptions.map(([v, n]) => ({ value: v, label: `${eventLabel(v)} (${fmtInt(n)})` })) }, { kind: 'select', name: 'entity_type', label: 'Entity type', value: current.entity_type, options: ENTITY_TYPES.map((t) => ({ value: t, label: typeLabel(t, true) })) }, { kind: 'select', name: 'importance_min', label: 'Min importance', value: current.importance_min, options: [3, 2, 1].map((n) => ({ value: String(n), label: `${IMPORTANCE_LABELS[n]} (≥ ${n})` })) }, { kind: 'row', fields: [{ kind: 'date', name: 'since', label: 'Since', value: current.since, max: today }, { kind: 'date', name: 'until', label: 'Until', value: current.until, max: today }] }, { kind: 'text', name: 'q', label: 'Text in summary', value: current.q, placeholder: 'e.g. context' }, { kind: 'checkbox', name: 'include_backfill', label: 'Include historical backfill', checked: current.include_backfill === '1', hint: sem.is_backfill ?? 'History imported when a source is first crawled; never shown as “today” in feeds.' }, { kind: 'checkbox', name: 'date_field', value: 'observed', label: 'Order by observation time', checked: dateField === 'observed', hint: 'Default order is occurred_at (effective date when a source states it). Tick to order by when AI Atlas first saw each change (v1 behaviour).' }, ...(current.entity ? [{ kind: 'hidden' as const, name: 'entity', value: current.entity }] : []), ]} /> ); const inspector = (

Last 7 days by category

{catOptions.length === 0 ? (

No category counts available.

) : (
    {catOptions.slice(0, 10).map(([c, n]) => (
  • {categoryLabel(c)} {fmtInt(n)}
  • ))}
)}

Semantics

{(['occurred_at', 'observed_at', 'recorded_at', 'is_backfill', 'group_key'] as const).filter((k) => sem[k]).map((k) => (
{k}
{sem[k]}
))} {!Object.keys(sem).length &&
Definitions unavailable.
}

Today in AI → · Timeline · Diff two dates

); return ( <> Today in AI →} className="pb-3"> href(p)} className="mt-3" />
{!page ? ( ) : page.items.length === 0 ? ( The change engine only emits events when a source states a material change — try widening the filters{current.include_backfill ? '' : ', or include historical backfill'}. ) : ( <>

{fmtInt(page.total)} events{current.since ? ` since ${fmtDate(current.since)}` : ''}{current.until ? ` until ${fmtDate(current.until)}` : ''} · ordered by {dateField === 'occurred' ? 'occurrence' : 'observation'}{page.include_backfill ? ' · backfill included' : ''}

{groups.map((g) => (

{fmtDate(g.day)} {' '} {g.items.length}

    {g.items.map((e) => ( ))}
))} Feed keyed on {dateField}_at. Backfill = history imported when a source is first crawled{current.include_backfill ? ' (included)' : ' (excluded — tick the box in the rail to include it)'}. Times are UTC. )}
); }