SPB Git

spb/ai-atlas

Public
12.7 KB · 203 lines tsx
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
Files · 7456f4abc11ceb761f1f60861a5d96e6ddc93ef4
Menu

Theme