import type { Metadata } from 'next'; import Link from 'next/link'; import { ChangeRow } from '@/components/changes/change-row'; import { EntityRow } from '@/components/ui/entity'; import { Container, Note, PageHeader, Section, Stat, StatGrid } from '@/components/ui/section'; import { EmptyState, Unavailable } from '@/components/ui/unavailable'; import { api, ApiError, safe } from '@/lib/api'; import { fmtDate, fmtInt, num } from '@/lib/format'; import { routes } from '@/lib/site'; import type { ChangeEvent, DiffPayload, EntitySummary } from '@/lib/types'; type SP = { a?: string; b?: string; scope?: string; scope_custom?: string }; const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/; function todayUtc(): string { return new Date().toISOString().slice(0, 10); } function daysBefore(iso: string, days: number): string { const d = new Date(`${iso}T00:00:00Z`); d.setUTCDate(d.getUTCDate() - days); return d.toISOString().slice(0, 10); } function resolve(sp: SP): { a: string; b: string; scope: string; valid: boolean } { const b = sp.b && ISO_DAY.test(sp.b) ? sp.b : todayUtc(); const a = sp.a && ISO_DAY.test(sp.a) ? sp.a : daysBefore(b, 7); const scope = (sp.scope_custom?.trim() || sp.scope || 'all').trim() || 'all'; const valid = (!sp.a || ISO_DAY.test(sp.a)) && (!sp.b || ISO_DAY.test(sp.b)) && a < b; return { a, b, scope, valid }; } export async function generateMetadata({ searchParams }: { searchParams: Promise }): Promise { const { a, b, scope } = resolve(await searchParams); const title = `What changed between ${fmtDate(a)} and ${fmtDate(b)}${scope !== 'all' ? ` (${scope})` : ''}`; return { title, description: `New, gone and changed entities, prices and benchmark results in the AI ecosystem between ${a} and ${b}, straight from the AI Atlas change log.`, alternates: { canonical: routes.diff({ a, b, scope }) }, robots: { index: false } }; } function describeScope(s: DiffPayload['scope'], fallback: string): string { if (!s) return fallback === 'all' ? 'the whole atlas' : fallback; if (typeof s === 'string') return s; const kind = typeof s.kind === 'string' ? s.kind : fallback; const parts = Object.entries(s) .filter(([k, v]) => k !== 'kind' && v !== null && v !== undefined && v !== '') .map(([k, v]) => `${k} ${typeof v === 'object' ? JSON.stringify(v) : String(v)}`); if (kind === 'all') return 'the whole atlas'; return parts.length ? `${kind} · ${parts.join(' · ')}` : kind; } function Capped({ shown, total, what }: { shown: number; total: number | null; what: string }) { if (total === null || total <= shown) return null; return Showing the first {fmtInt(shown)} of {fmtInt(total)} {what}. Narrow the scope or the dates to see everything.; } function EventsSection({ id, title, items, total, empty }: { id: string; title: string; items: ChangeEvent[]; total: number | null; empty: string }) { return (
{title} {fmtInt(total ?? items.length)}}> {items.length === 0 ? ( ) : ( <>
    {items.map((e) => ( ))}
)}
); } function EntitiesSection({ id, title, items, total, empty }: { id: string; title: string; items: EntitySummary[]; total: number | null; empty: string }) { return (
{title} {fmtInt(total ?? items.length)}}> {items.length === 0 ? ( ) : ( <>
    {items.map((e) => ( ))}
)}
); } export default async function DiffPage({ searchParams }: { searchParams: Promise }) { const sp = await searchParams; const { a, b, scope, valid } = resolve(sp); const orgs = await safe(api.companies({ limit: 30, sort: 'models' })); let payload: DiffPayload | null = null; let error: string | null = null; if (!valid) error = 'Dates must be YYYY-MM-DD and the first date must come before the second.'; else { try { payload = await api.diff(a, b, scope); } catch (e) { error = e instanceof ApiError && e.status === 400 ? e.detail ?? 'The API rejected these parameters.' : null; } } const cls = 'h-11 w-full border border-rule bg-surface px-2.5 text-sm text-ink focus:border-accent focus:outline-none'; const orgOptions = (orgs?.items ?? []).map((o) => ({ value: `org:${o.slug}`, label: `${o.name} (${fmtInt(o.model_count)} models)` })); const knownScopes = new Set(['all', 'models', ...orgOptions.map((o) => o.value)]); const custom = knownScopes.has(scope) ? '' : scope; const c = payload?.counts ?? {}; const cnt = (k: string): number | null => num(c[k]); return ( What changed between {fmtDate(a)} and {fmtDate(b)}} lede="Two dates, one scope: the entities that appeared or disappeared, and every property, price and benchmark change recorded in between. Built from the change log — nothing is inferred." aside={payload ?

Scope: {describeScope(payload.scope, scope)}

: undefined}>
Reset
Custom scope accepts org:<slug> or family:<name>; when filled it overrides the select. Quick links: yesterday → today · last 7 days, models · last 30 days
{error ? ( ) : !payload ? ( ) : ( <> Price rows opened {fmtInt(cnt('price_rows_opened'))} · closed {fmtInt(cnt('price_rows_closed'))}. Counts are totals over the period; the lists below are capped by the API. “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. Per-entity history: open any entity's History tab. Methodology → )}
); }