1import { ArrowRight, Search } from 'lucide-react';2import type { Metadata } from 'next';3import Link from 'next/link';4import { ChangeRow } from '@/components/changes/change-row';5import { EntityBadge } from '@/components/ui/badges';6import { EntityLink } from '@/components/ui/entity';7import { LiveAgo } from '@/components/ui/live';8import { Container, Section, Stat, StatGrid } from '@/components/ui/section';9import { Unavailable } from '@/components/ui/unavailable';10import { api, safe } from '@/lib/api';11import { fmtDate, fmtInt, num } from '@/lib/format';12import { DESCRIPTION, EXAMPLE_QUERIES, routes, SITE_NAME, SITE_URL, TAGLINE, typeLabel } from '@/lib/site';13import type { ChangeEvent, EntitySummary } from '@/lib/types';1415export const metadata: Metadata = {16 title: { absolute: `${SITE_NAME} — ${TAGLINE}` },17 description: DESCRIPTION,18 alternates: { canonical: '/' },19};20export const revalidate = 60;2122const HOW = [23 { step: 'Connectors', text: 'First-party crawlers read official docs, pricing pages, model cards, papers, feeds and repositories directly — no third-party data APIs.' },24 { step: 'Archive', text: 'Every document is snapshotted and kept forever, content-addressed, so any fact can be traced back to the exact page that stated it.' },25 { step: 'Graph', text: 'Deterministic extractors (then a local LLM only where needed) turn snapshots into temporal claims and relations with source, tier and confidence.' },26 { step: 'History', text: 'Material changes become events: new models, price moves, context changes, deprecations. Nothing is overwritten; conflicts are flagged, never averaged.' },27];2829function pickFeed(daily: Awaited<ReturnType<typeof api.changesDaily>> | null, changes: ChangeEvent[] | null): { title: string; items: ChangeEvent[] }[] {30 const out: { title: string; items: ChangeEvent[] }[] = [];31 const all = changes ?? [];32 const by = (pred: (e: ChangeEvent) => boolean, title: string, n = 6) => {33 const items = all.filter(pred).slice(0, n);34 if (items.length) out.push({ title, items });35 };36 by((e) => e.event_type === 'NEW_MODEL', 'New models');37 by((e) => e.event_type === 'PRICE_CHANGED' || e.event_type === 'PROVIDER_LISTED', 'Prices');38 by((e) => e.event_type.startsWith('BENCHMARK'), 'Benchmarks');39 by((e) => e.event_type === 'ANNOUNCEMENT' || e.event_type === 'RELEASE' || e.event_type === 'VERSION_RELEASED', 'Announcements & releases');40 by((e) => ['DEPRECATION_ANNOUNCED', 'RETIREMENT_ANNOUNCED', 'STATUS_CHANGED', 'CONTEXT_CHANGED', 'LICENSE_CHANGED', 'OPENNESS_CHANGED'].includes(e.event_type), 'Status, context & licenses');41 if (!out.length && daily?.sections?.length) for (const s of daily.sections) if (s.items.length) out.push({ title: s.label, items: s.items.slice(0, 6) });42 if (!out.length && all.length) out.push({ title: 'Latest', items: all.slice(0, 12) });43 return out;44}4546export default async function HomePage() {47 const [stats, daily, changes, types, trending] = await Promise.all([safe(api.stats()), safe(api.changesDaily()), safe(api.changes({ importance_min: 2, limit: 40 })), safe(api.exploreTypes()), safe(api.trending(7, 12))]);48 const feed = pickFeed(daily, changes?.items ?? null);49 const today = daily?.date ?? new Date().toISOString().slice(0, 10);50 const ent = stats?.entities ?? {};51 const companies = (num(ent.company) ?? 0) + (num(ent.organization) ?? 0) + (num(ent.lab) ?? 0) || null;5253 return (54 <>55 {/* ------------------------------------------------------------------------------------------------ hero */}56 <div className="relative overflow-hidden border-b border-rule">57 <div className="grid-bg pointer-events-none absolute inset-0" aria-hidden />58 <Container className="relative py-12 md:py-20">59 <p className="eyebrow">The AI ecosystem, mapped · with provenance and history</p>60 <h1 className="display mt-3 max-w-4xl text-[38px] md:text-[64px]">Explore the entire AI ecosystem.</h1>61 <p className="mt-4 max-w-2xl text-base leading-relaxed text-ink-2 md:text-lg">62 Models, companies, research, providers and pricing, benchmarks, hardware, frameworks, datasets and tools — every fact linked to its source, every change kept.63 </p>64 <form action="/search" method="get" role="search" className="mt-7 flex max-w-2xl items-stretch border border-rule-strong bg-surface focus-within:border-accent">65 <label htmlFor="home-q" className="sr-only">Search</label>66 <span className="flex items-center pl-3 text-ink-3"><Search className="size-5" aria-hidden /></span>67 <input id="home-q" name="q" type="search" placeholder="Ask anything: “open models over 100B released in 2026”, “cheapest 1M context”…" className="h-12 min-w-0 flex-1 bg-transparent px-3 text-[16px] text-ink placeholder:text-ink-3 focus:outline-none md:h-14" autoComplete="off" />68 <button type="submit" className="flex items-center gap-1.5 bg-ink px-4 text-sm font-medium text-canvas hover:opacity-90 md:px-5">69 Search <ArrowRight className="hidden size-4 md:block" aria-hidden />70 </button>71 </form>72 <ul className="mt-3 flex flex-wrap gap-2">73 {EXAMPLE_QUERIES.map((q) => (74 <li key={q}>75 <Link href={routes.search(q)} className="inline-block border border-rule px-2.5 py-1 text-xs text-ink-2 hover:border-rule-strong hover:text-ink">76 {q}77 </Link>78 </li>79 ))}80 </ul>81 </Container>82 </div>8384 <Container>85 {/* ------------------------------------------------------------------------------------------------ counters */}86 <Section eyebrow="Live counters" title={<span className="flex flex-wrap items-center gap-x-3 gap-y-1">Inside the atlas {stats && <span className="flex items-center gap-1.5 text-xs font-normal text-ink-3"><span className="dot pulse" aria-hidden /> updated <LiveAgo at={stats.computed_at} /></span>}</span>} action={{ href: routes.methodology(), label: 'How counts are computed' }} hairline={false}>87 {stats ? (88 <StatGrid cols={8}>89 <Stat label="Models" value={fmtInt(ent.model)} href={routes.models()} />90 <Stat label="Companies" value={fmtInt(companies)} href={routes.companies()} />91 <Stat label="Papers" value={fmtInt(ent.paper)} href={routes.papers()} />92 <Stat label="Providers" value={fmtInt(ent.provider)} href={routes.providers()} />93 <Stat label="Benchmarks" value={fmtInt(ent.benchmark)} href={routes.benchmarks()} />94 <Stat label="Hardware" value={fmtInt(ent.hardware)} href={routes.hardware()} />95 <Stat label="Change events" value={fmtInt(stats.change_events)} delta={num(stats.change_events_24h) ? { value: `+${fmtInt(stats.change_events_24h)} · 24 h`, tone: 'neutral' } : undefined} href={routes.changes()} />96 <Stat label="Sources" value={fmtInt(stats.sources)} hint={`${fmtInt(stats.documents)} documents`} href={routes.sources()} />97 </StatGrid>98 ) : (99 <Unavailable what="Live counters" reason="The API did not answer. Counters are never cached or hardcoded." />100 )}101 </Section>102103 {/* ------------------------------------------------------------------------------------------------ today */}104 <Section eyebrow={`Today in AI · ${fmtDate(today)}`} title="What changed" action={{ href: routes.changesDay(today), label: 'Daily digest' }} lede={daily ? `${fmtInt(Object.values(daily.counts ?? {}).reduce<number>((n, v) => n + (num(v) ?? 0), 0))} events recorded today across ${Object.keys(daily.counts ?? {}).length} categories.` : undefined}>105 {feed.length ? (106 <div className="grid gap-x-10 gap-y-8 lg:grid-cols-2">107 {feed.map((f) => (108 <div key={f.title}>109 <p className="eyebrow mb-1">{f.title}</p>110 <ul className="border-t border-rule">111 {f.items.map((e) => (112 <ChangeRow key={e.id} e={e} dense />113 ))}114 </ul>115 </div>116 ))}117 </div>118 ) : changes ? (119 <p className="border-y border-rule py-8 text-center text-sm text-ink-3">No important events yet. The change engine only emits events when a source states a material change.</p>120 ) : (121 <Unavailable what="Change feed" />122 )}123 {daily?.new_models && daily.new_models.length > 0 && (124 <div className="mt-8">125 <p className="eyebrow mb-2">New models today <span className="tnum text-ink-3">{daily.new_models.length}</span></p>126 <ul className="flex flex-wrap gap-x-4 gap-y-1 text-sm">127 {daily.new_models.slice(0, 16).map((m) => (128 <li key={m.id}>129 <EntityLink e={m} className="font-medium" />130 {m.organization && <span className="text-ink-3"> · {m.organization.name}</span>}131 </li>132 ))}133 </ul>134 </div>135 )}136 <p className="mt-6 text-sm">137 <Link href={routes.changes()} className="link">All changes, filterable by category and importance →</Link>138 </p>139 </Section>140141 {/* ------------------------------------------------------------------------------------------------ explore */}142 <Section eyebrow="Explore" title="Every entity type, with live counts" action={{ href: routes.explore(), label: 'All types' }}>143 {types?.items?.length ? (144 <ul className="grid grid-cols-2 border-l border-t border-rule sm:grid-cols-3 lg:grid-cols-5">145 {types.items146 .filter((t) => (num(t.count) ?? 0) > 0)147 .sort((a, b) => (num(b.count) ?? 0) - (num(a.count) ?? 0))148 .slice(0, 15)149 .map((t) => (150 <li key={t.entity_type} className="border-b border-r border-rule">151 <Link href={routes.listing(t.entity_type)} className="block px-4 py-4 hover:bg-surface-2">152 <EntityBadge type={t.entity_type} small />153 <p className="tnum mt-2 text-2xl font-semibold tracking-tight">{fmtInt(t.count)}</p>154 <p className="text-sm text-ink-2">{t.label || typeLabel(t.entity_type, true)}</p>155 </Link>156 </li>157 ))}158 </ul>159 ) : (160 <Unavailable what="Entity types" />161 )}162 </Section>163164 {/* ------------------------------------------------------------------------------------------------ trending */}165 <Section eyebrow="Trending · 7 days" title="Most viewed" lede="Ranked by page views on AI Atlas, not by any external popularity signal.">166 {trending?.items?.length ? (167 <ol className="grid gap-x-8 sm:grid-cols-2 lg:grid-cols-3">168 {trending.items.slice(0, 12).map((t, i) => (169 <li key={t.id} className="flex items-baseline gap-3 border-b border-rule py-2 text-sm">170 <span className="tnum w-5 shrink-0 text-xs text-ink-3">{i + 1}</span>171 <EntityBadge type={t.entity_type} small />172 <EntityLink e={t as EntitySummary} className="min-w-0 flex-1 truncate font-medium" />173 <span className="tnum shrink-0 text-xs text-ink-3">{fmtInt(t.views)} views</span>174 </li>175 ))}176 </ol>177 ) : trending ? (178 <p className="text-sm text-ink-3">Not enough page views yet to rank entities.</p>179 ) : (180 <Unavailable what="Trending" compact />181 )}182 </Section>183184 {/* ------------------------------------------------------------------------------------------------ how */}185 <Section eyebrow="How AI Atlas works" title="Connectors → archive → graph → history" action={{ href: routes.methodology(), label: 'Methodology' }}>186 <ol className="grid gap-6 md:grid-cols-4">187 {HOW.map((h, i) => (188 <li key={h.step} className="border-t-2 border-ink pt-3">189 <p className="mono text-xs text-ink-3">0{i + 1}</p>190 <p className="mt-1 font-semibold">{h.step}</p>191 <p className="mt-1.5 text-sm leading-relaxed text-ink-2">{h.text}</p>192 </li>193 ))}194 </ol>195 <p className="mt-6 text-sm text-ink-2">196 Public API at <Link href={routes.developers()} className="link mono">{SITE_URL.replace(/^https?:\/\//, '')}/api/v1</Link> · sources listed on <Link href={routes.sources()} className="link">/sources</Link> · crawler policy on <Link href={routes.bot()} className="link">/bot</Link>.197 </p>198 </Section>199 </Container>200 </>201 );202}203