SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
11.1 KB · 191 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { CompareButton } from '@/components/compare/compare-button';4import { CompareTrayBar } from '@/components/compare/compare-tray-bar';5import { fmtMemory, HW_KIND_LABELS, interconnect, precisions, releaseOf } from '@/components/hardware/hardware-bits';6import { BTN_GHOST, CTRL, Field, SortTh } from '@/components/intelligence/bits';7import { TerminalLayout } from '@/components/layout/terminal';8import { Chip } from '@/components/ui/badges';9import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';10import { EntityLink } from '@/components/ui/entity';11import { Pagination, withParams } from '@/components/ui/pagination';12import { Note, PageHeader } from '@/components/ui/section';13import { EmptyState, Unavailable } from '@/components/ui/unavailable';14import { intel, safe } from '@/lib/api';15import { fmtDate, fmtInt, num } from '@/lib/format';16import { routes, SITE_NAME, SITE_URL } from '@/lib/site';1718export const revalidate = 600;19type SP = Record<string, string | undefined>;20const LIMIT = 50;21const KEYS = ['q', 'kind', 'manufacturer', 'min_memory', 'sort', 'offset'] as const;22const SORTS = [23  { value: 'memory', label: 'Memory' },24  { value: 'name', label: 'Name' },25  { value: 'updated', label: 'Recently updated' },26];2728const TITLE = 'AI hardware — GPUs, accelerators, SoCs and systems: memory, bandwidth, TDP';29const DESC = 'Accelerators and devices for training and inference with the memory, bandwidth, TDP, precision support and interconnect figures published by their manufacturers — and, for each, which models are estimated to fit.';30export async function generateMetadata({ searchParams }: { searchParams: Promise<SP> }): Promise<Metadata> {31  const sp = await searchParams;32  const filtered = ['q', 'kind', 'manufacturer', 'min_memory', 'offset'].some((k) => sp[k]);33  return { title: TITLE, description: DESC, alternates: { canonical: routes.hardware() }, openGraph: { title: `${TITLE} | ${SITE_NAME}`, description: DESC, url: `${SITE_URL}${routes.hardware()}`, siteName: SITE_NAME }, robots: filtered ? { index: false, follow: true } : undefined };34}3536export default async function HardwarePage({ searchParams }: { searchParams: Promise<SP> }) {37  const sp = await searchParams;38  const cur: Record<string, string | undefined> = {};39  for (const k of KEYS) if (sp[k]) cur[k] = sp[k];40  const offset = Math.max(0, Number(cur.offset) || 0);41  const sort = SORTS.some((s) => s.value === cur.sort) ? (cur.sort as string) : 'memory';42  const page = await safe(intel.hardware({ q: cur.q, kind: cur.kind, manufacturer: cur.manufacturer, min_memory: num(cur.min_memory) ?? undefined, sort, limit: LIMIT, offset }));43  const href = (patch: Record<string, string | number | undefined | null>) => withParams('/hardware', cur, patch);44  const kinds = page?.facets?.kinds ?? [];45  const manufacturers = page?.facets?.manufacturers ?? [];46  const filterCount = ['q', 'kind', 'manufacturer', 'min_memory'].filter((k) => cur[k]).length;47  const items = page?.items ?? [];48  const withTdp = items.filter((e) => num(e.attributes?.tdp_watts) !== null).length;49  const withPrec = items.filter((e) => precisions(e.attributes ?? {}).length).length;5051  const filters = (52    <form action="/hardware" method="get" className="space-y-3" data-hardware-filters>53      <Field label="Name">54        <input name="q" defaultValue={cur.q ?? ''} placeholder="H100, M4 Max…" className={CTRL} />55      </Field>56      <Field label="Kind">57        <select name="kind" defaultValue={cur.kind ?? ''} className={CTRL}>58          <option value="">Any kind</option>59          {kinds.map((k) => (60            <option key={k.value} value={k.value}>61              {HW_KIND_LABELS[k.value] ?? k.value} ({fmtInt(k.count)})62            </option>63          ))}64          {cur.kind && !kinds.some((k) => k.value === cur.kind) && <option value={cur.kind}>{HW_KIND_LABELS[cur.kind] ?? cur.kind}</option>}65        </select>66      </Field>67      <Field label="Manufacturer">68        <select name="manufacturer" defaultValue={cur.manufacturer ?? ''} className={CTRL}>69          <option value="">Any manufacturer</option>70          {manufacturers.map((m) => (71            <option key={m.value} value={m.value}>72              {m.value} ({fmtInt(m.count)})73            </option>74          ))}75          {cur.manufacturer && !manufacturers.some((m) => m.value === cur.manufacturer) && <option value={cur.manufacturer}>{cur.manufacturer}</option>}76        </select>77      </Field>78      <Field label="Memory ≥ (GB)">79        <input name="min_memory" inputMode="numeric" defaultValue={cur.min_memory ?? ''} placeholder="e.g. 64" className={CTRL} />80      </Field>81      <Field label="Sort">82        <select name="sort" defaultValue={sort} className={CTRL}>83          {SORTS.map((s) => (84            <option key={s.value} value={s.value}>85              {s.label}86            </option>87          ))}88        </select>89      </Field>90      <div className="flex gap-2">91        <button type="submit" className="inline-flex h-11 flex-1 items-center justify-center bg-ink lg:h-10 px-3 text-sm font-medium text-canvas hover:opacity-90">92          Apply93        </button>94        <Link href="/hardware" className={BTN_GHOST}>95          Reset96        </Link>97      </div>98    </form>99  );100  const inspector = (101    <div className="space-y-3 text-xs leading-relaxed text-ink-2">102      <p>Figures are the manufacturer&apos;s published specifications (spec sheets, tier 2 when read from a reseller). A dash means no source has stated the value — TDP, precision support and interconnect are sparse in the current corpus ({fmtInt(withTdp)} / {fmtInt(withPrec)} of the {fmtInt(items.length)} rows shown carry TDP / precision).</p>103      <p>Memory listed as a range = several configurations (Apple silicon tiers); fit estimates use the largest unless you pick one.</p>104      <p>105        <Link href={routes.runLocally()} className="link">Run locally</Link> · <Link href="/hardware/frontier" className="link">Hardware frontier</Link> · <Link href="/methodology#estimates" className="link">estimate method</Link>106      </p>107    </div>108  );109110  return (111    <TerminalLayout filters={filters} inspector={inspector} filtersTitle="Filters" inspectorTitle="Reading" storageKey="aia-inspector-hardware" filterCount={filterCount}>112      <PageHeader eyebrow="Hardware" title="Hardware" lede="GPUs, accelerators, SoCs and systems with the memory and bandwidth figures their manufacturers publish. Every device links to what it is estimated to run." className="pt-4 md:pt-6" aside={113        <div className="flex flex-col items-start gap-2 md:items-end">114          {page && <p className="tnum text-sm text-ink-3">{fmtInt(page.total)} devices</p>}115          <span className="flex flex-wrap gap-2">116            <Link href="/hardware/frontier" className="inline-flex h-10 items-center border border-rule px-3 text-sm text-ink-2 hover:border-rule-strong hover:text-ink">117              Hardware frontier →118            </Link>119            <Link href={routes.runLocally()} className="inline-flex h-10 items-center border border-rule-strong px-3 text-sm font-medium text-ink hover:border-accent hover:text-accent">120              What fits my machine? →121            </Link>122          </span>123        </div>124      } />125      <div className="pb-16">126        {!page ? (127          <Unavailable what="Hardware" />128        ) : items.length === 0 && !offset ? (129          <EmptyState title={filterCount ? 'No hardware matches these filters' : 'No hardware recorded yet'}>{filterCount ? 'Remove a filter.' : 'Connectors: manufacturer spec pages.'}</EmptyState>130        ) : (131          <>132            <DataTable scroll compact>133              <thead>134                <tr>135                  <SortTh active={sort === 'name'} href={href({ sort: 'name', offset: undefined })}>Hardware</SortTh>136                  <Th>Kind</Th>137                  <SortTh active={sort === 'memory'} href={href({ sort: undefined, offset: undefined })} num dir="desc">Memory</SortTh>138                  <Th num>Bandwidth</Th>139                  <Th num>TDP</Th>140                  <Th>Precision</Th>141                  <Th>Interconnect</Th>142                  <Th>Release</Th>143                  <Th>Fit</Th>144                  <Th className="w-24" aria-label="Compare" />145                </tr>146              </thead>147              <tbody>148                {items.length === 0 && <EmptyRow cols={10} />}149                {items.map((e) => {150                  const a = e.attributes ?? {};151                  const prec = precisions(a);152                  const ic = interconnect(a);153                  const rel = releaseOf(e);154                  return (155                    <tr key={e.id} data-hardware-row>156                      <Td primary>157                        <EntityLink e={e} />158                        <span className="block text-xs text-ink-3">{typeof a.manufacturer === 'string' ? a.manufacturer : e.organization?.name ?? '—'}</span>159                      </Td>160                      <Td label="Kind">{typeof a.kind === 'string' ? <Chip>{HW_KIND_LABELS[a.kind] ?? a.kind}</Chip> : <span className="text-ink-3">—</span>}</Td>161                      <Td num label="Memory" className="tnum font-medium">{fmtMemory(a.memory_gb)}{typeof a.memory_type === 'string' && <span className="block text-[10px] font-normal text-ink-3">{a.memory_type}</span>}</Td>162                      <Td num label="Bandwidth" className="tnum text-ink-2">{num(a.memory_bandwidth_gbs) === null ? <span className="text-ink-3">—</span> : `${fmtInt(a.memory_bandwidth_gbs)} GB/s`}</Td>163                      <Td num label="TDP" className="tnum text-ink-2">{num(a.tdp_watts) === null ? <span className="text-ink-3">—</span> : `${fmtInt(a.tdp_watts)} W`}</Td>164                      <Td label="Precision" className="mono text-[11px] text-ink-2">{prec.length ? prec.join(' · ') : <span className="font-sans text-xs text-ink-3">—</span>}</Td>165                      <Td label="Interconnect" className="text-xs text-ink-2">{ic ?? <span className="text-ink-3">—</span>}</Td>166                      <Td label="Release" className="tnum text-ink-2 whitespace-nowrap">{rel ? fmtDate(rel) : <span className="text-ink-3">—</span>}</Td>167                      <Td label="Fit">168                        <Link href={`${routes.runLocally()}?hardware=${encodeURIComponent(e.slug)}`} className="link whitespace-nowrap text-xs">169                          What can it run? →170                        </Link>171                      </Td>172                      <Td className="text-right">173                        <CompareButton e={e} size="sm" />174                      </Td>175                    </tr>176                  );177                })}178              </tbody>179            </DataTable>180            <Pagination total={page.total} limit={LIMIT} offset={offset} makeHref={(o) => href({ offset: o || undefined })} className="mt-4" />181            <Note className="mt-3">182              Each device page lists the models estimated to fit its memory per quantization — labelled as estimates, method on <Link href="/methodology#estimates" className="link">/methodology</Link>. A dash = not stated by any source (never guessed).183            </Note>184          </>185        )}186      </div>187      <CompareTrayBar />188    </TerminalLayout>189  );190}191