SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
11.2 KB · 183 lines tsx
Raw Blame History
1import type { AiEvidence } from "@dci/core";2import type { Metadata } from "next";3import Link from "next/link";4import { Breadcrumbs } from "@/components/entity/breadcrumbs";5import { SectionNav } from "@/components/entity/section-nav";6import { SourcesFooter } from "@/components/entity/sources-footer";7import { EventList } from "@/components/event-list";8import { AiFacilitiesTable } from "@/components/intelligence/ai/ai-facilities-table";9import { ApiNote, CoverageBar, HonestEmpty, MwFigure, PillLink, PublishedOnlyNote, ShareList } from "@/components/intelligence/shared";10import { ProjectList } from "@/components/project-list";11import { AiMark } from "@/components/ui/badge";12import { Page, PageHeader, Section } from "@/components/ui/card";13import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";14import { ErrorState, OfflineNotice } from "@/components/ui/states";15import { api } from "@/lib/api";16import { fmtInt, fmtMw, fmtPct } from "@/lib/format";17import { AI_EVIDENCE_LABEL, STATUS_LABEL, statusVar } from "@/lib/labels";18import { routes } from "@/lib/routes";19import { pageMetadata } from "@/lib/seo";2021export const revalidate = 120;2223export const metadata: Metadata = pageMetadata({24  title: "AI infrastructure",25  description: "AI / HPC data centers and projects with their evidence level (confirmed, likely, associated), the project pipeline by stage, top operators, markets and countries — published figures only.",26  path: routes.ai(),27});2829const LEVELS: Array<{ level: AiEvidence; rule: string; counted: boolean }> = [30  { level: "confirmed", rule: "A source explicitly describes the site as AI / HPC / GPU / accelerated-computing infrastructure.", counted: true },31  { level: "likely", rule: "AI-ready, high-density, liquid- or direct-to-chip cooling or GPU signals in the source.", counted: true },32  { level: "associated", rule: "Only an AI tenant, customer or company mention — reported separately, NOT counted as AI infrastructure.", counted: false },33  { level: "unknown", rule: "No signal in any indexed source. A single keyword never confirms a site.", counted: false },34];3536export default async function AiInfrastructurePage() {37  const r = await api.aiIndex();38  if (!r.ok) {39    return (40      <Page className="py-6">41        <Breadcrumbs items={[{ name: "AI infrastructure", href: routes.ai() }]} className="mb-4" />42        <OfflineNotice detail={r.error} className="mb-4" />43        <ErrorState />44      </Page>45    );46  }47  const d = r.data;48  const s = d.stats;49  const stages = [...d.pipelineByStage].sort((a, b) => b.count - a.count);50  const stageMax = Math.max(1, ...stages.map((x) => x.count));51  const sections = [52    { id: "evidence", label: "Evidence levels" },53    { id: "pipeline", label: "Project pipeline", count: stages.length },54    { id: "top", label: "Top operators & places" },55    { id: "announcements", label: "Announcements", count: d.recentAnnouncements.length },56    { id: "projects", label: "Recent projects", count: d.recentProjects.length },57    { id: "facilities", label: "Facilities", count: d.facilities.length },58  ];5960  return (61    <Page>62      <Breadcrumbs items={[{ name: "AI infrastructure", href: routes.ai() }]} className="pt-3" />63      <PageHeader64        eyebrow="Intelligence"65        title="AI infrastructure"66        description="Data centers and projects with evidence of AI / HPC use, ranked and counted only when a source says so. Counts and MW cover confirmed + likely sites; associated mentions are reported separately."67        aside={<PillLink href={routes.map("layer=ai")}>Open the AI layer on the map →</PillLink>}68      />6970      <MetricRow cols={5}>71        <MetricCell>72          <Metric value={fmtInt(s.facilities)} label="AI facilities" note={`${fmtInt(s.confirmed)} confirmed · ${fmtInt(s.likely)} likely`} />73        </MetricCell>74        <MetricCell>75          <Metric value={fmtInt(s.projects)} label="AI projects" note="live project records" />76        </MetricCell>77        <MetricCell>78          <Metric value={<MwFigure mw={s.plannedMw} estimate />} label="Planned MW" note={s.plannedMw === null ? "no published figure" : "projects, published ≈ figures"} />79        </MetricCell>80        <MetricCell>81          <Metric value={<MwFigure mw={s.constructionMw} />} label="Construction MW" note={s.constructionMw === null ? "no published figure" : "under construction"} />82        </MetricCell>83        <MetricCell>84          <Metric value={fmtPct(s.mwCoverage)} label="MW coverage" note="share of AI records with a figure" />85        </MetricCell>86      </MetricRow>87      <MetricRow cols={3} className="border-t-0 [&>div]:border-t-0">88        <MetricCell>89          <Metric size="sm" value={fmtInt(s.countries)} label="Countries" />90        </MetricCell>91        <MetricCell>92          <Metric size="sm" value={fmtInt(s.operators)} label="Operators" />93        </MetricCell>94        <MetricCell>95          <Metric size="sm" value={fmtInt(s.associated)} label="Associated mentions" note="reported separately, not counted" />96        </MetricCell>97      </MetricRow>9899      <SectionNav items={sections} className="mt-4" />100101      <div className="flex flex-col gap-8 pt-6">102        <Section id="evidence" title="Evidence levels" description="How a site earns the AI mark. Only confirmed and likely sites are counted above." className="scroll-mt-28">103          <ul className="divide-y divide-hair">104            {LEVELS.map((l) => (105              <li key={l.level} className="grid grid-cols-[auto_minmax(0,1fr)] items-start gap-x-3 py-2.5 sm:grid-cols-[140px_minmax(0,1fr)_120px]">106                <div className="pt-0.5">{l.level === "unknown" ? <span className="text-[11.5px] uppercase tracking-[0.06em] text-ink-4">Unknown</span> : <AiMark evidence={l.level} size="sm" />}</div>107                <p className="text-[13px] text-ink-2">108                  <span className="mr-1 font-medium text-ink">{AI_EVIDENCE_LABEL[l.level].replace("AI · ", "").replace(/^./, (c) => c.toUpperCase())}.</span>109                  {l.rule}110                </p>111                <span className={`col-start-2 text-[11.5px] sm:col-start-3 sm:text-right ${l.counted ? "text-ok" : "text-ink-4"}`}>{l.counted ? "counted" : "not counted"}</span>112              </li>113            ))}114          </ul>115          <ApiNote note={d.evidenceNote} label="From the index" className="mt-3" />116        </Section>117118        <Section id="pipeline" title="Project pipeline by stage" description="AI project records by lifecycle stage; MW are planned, published figures (≈) and are never added to operational capacity." className="scroll-mt-28">119          {stages.length === 0 ? (120            <HonestEmpty title="No AI project in the pipeline" detail="projects appear when an announcement with AI evidence is ingested" />121          ) : (122            <ol className="divide-y divide-hair">123              {stages.map((st) => (124                <li key={st.status} className="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-x-3 py-2 text-[13px]">125                  <div className="flex min-w-0 items-center gap-2">126                    <span aria-hidden className="inline-block size-2 shrink-0 rounded-full" style={{ background: statusVar(st.status) }} />127                    <span className="truncate text-ink">{STATUS_LABEL[st.status]}</span>128                    <span className="hidden h-[4px] min-w-[60px] flex-1 bg-surface-3 sm:block" aria-hidden>129                      <span className="block h-full" style={{ width: `${(st.count / stageMax) * 100}%`, background: statusVar(st.status) }} />130                    </span>131                  </div>132                  <span className="figure text-ink-2">133                    {fmtInt(st.count)} <span className="text-ink-4">{st.count === 1 ? "project" : "projects"}</span>134                  </span>135                  <span className="figure w-[96px] text-right text-ink-2">{st.mw === null ? <span className="text-ink-4">MW —</span> : <>≈ {fmtMw(st.mw)}</>}</span>136                </li>137              ))}138            </ol>139          )}140          <PublishedOnlyNote className="mt-2" />141        </Section>142143        <Section id="top" title="Top operators, markets and countries" description="Ranked by confirmed + likely AI facilities and projects; planned MW shown when published." className="scroll-mt-28">144          <div className="grid gap-6 lg:grid-cols-3">145            <TopColumn title="Operators" rows={d.topOperators.map((o) => ({ key: o.id, label: o.name, href: routes.operator(o.slug), facilities: o.facilities, projects: o.projects, plannedMw: o.plannedMw }))} />146            <TopColumn title="Markets" rows={d.topMetros.map((m) => ({ key: m.id, label: m.name, href: routes.market(m.slug), facilities: m.facilities, projects: m.projects, plannedMw: m.plannedMw, iso2: m.countryIso2 }))} />147            <TopColumn title="Countries" rows={d.topCountries.map((c) => ({ key: c.iso2, label: c.name, href: routes.country(c.slug), facilities: c.facilities, projects: c.projects, plannedMw: c.plannedMw, iso2: c.iso2 }))} />148          </div>149        </Section>150151        <Section id="announcements" title="New announcements" description="Events with AI evidence, newest first, each linked to its source." className="scroll-mt-28" aside={<Link href={routes.live("ai=1")} className="text-[12.5px] text-ink-3 hover:text-ink">Live feed (AI) →</Link>}>152          <EventList events={d.recentAnnouncements} dense emptyTitle="No AI announcement detected yet" emptyDescription="Announcements appear here when a source describes new AI / HPC capacity." />153        </Section>154155        <Section id="projects" title="Recent projects" description="Latest AI project records." className="scroll-mt-28" aside={<Link href={routes.projects("ai=1")} className="text-[12.5px] text-ink-3 hover:text-ink">All AI projects →</Link>}>156          <ProjectList items={d.recentProjects} emptyTitle="No AI project yet" />157        </Section>158159        <Section id="facilities" title="Facilities" description="Every facility flagged AI, with the evidence level per row." className="scroll-mt-28" aside={<span className="inline-flex items-center gap-2 text-[12px] text-ink-3"><span>MW coverage</span><CoverageBar value={s.mwCoverage} width="w-[110px]" /></span>}>160          <AiFacilitiesTable items={d.facilities} />161        </Section>162      </div>163164      <SourcesFooter sources={r.sources} note="AI status is never inferred from a single keyword; each evidence level is backed by the sentence that supports it. MW figures are published, site-scoped and containment-aware." />165    </Page>166  );167}168169function TopColumn({ title, rows }: { title: string; rows: Array<{ key: string; label: string; href: string; facilities: number; projects: number; plannedMw: number | null; iso2?: string | null }> }) {170  const total = rows.reduce((a, x) => a + x.facilities + x.projects, 0);171  return (172    <div>173      <div className="label-caps mb-2">{title}</div>174      {rows.length === 0 ? (175        <HonestEmpty title={`No ${title.toLowerCase()} yet`} />176      ) : (177        <ShareList unit="records" items={rows.map((x) => ({ key: x.key, label: x.label, href: x.href, count: x.facilities + x.projects, share: total ? (x.facilities + x.projects) / total : 0, mw: x.plannedMw, iso2: x.iso2 ?? null }))} />178      )}179      <p className="mt-1.5 text-[11px] text-ink-4">records = facilities + projects · MW = planned, published</p>180    </div>181  );182}183