web D2: intelligence surfaces — frontier, price terminal, calculator, run-locally, find-a-model, open, pulse, providers 2.0, hardware 2.0, typed listings
- /frontier flagship (SectionNav; latest majors, benchmark/price/context/open-weight/efficiency (Pareto scatter)/agentic/multimodal frontiers, movements ticker; API methodology per section; OG image)
- /prices Price History Terminal on TerminalLayout: rail filters (provider · org · family · modality · window · sort incl. cheapest_frontier, URL state), AI Price Index strip + InteractiveLineChart (5 medians, linear/log, sample sizes in tooltip, ≥2-snapshot honesty), distribution histogram, cheapest-frontier strips, movers with %, listings/delistings, offers table with lazy per-row price history (client fetch); OG image
- /calculator (client, same-origin /cost + /cost/context): every deployment side by side, cheapest bold ≠ verdict, price rows used + source; Context-cost tab with 128K/200K/1M/2M presets; URL state via replaceState
- /run-locally Local AI Explorer (rail form: platform · memory presets Apple/NVIDIA/AMD + custom · GPU count · quant · context · batch · use case · openness; ?hardware=<slug> mode via /hardware/{slug}/fit; fit breakdown, artifact rows with observed/estimated tags, multi-GPU note, Estimated everywhere)
- /find-a-model Model Finder (guided rail form → /find-a-model: why chips, observed dimensions, estimated fit, deployments in API mode, Compare/Watch; rules from the API; no single winner)
- /open Open Model Frontier (summary strip with /methodology openness definitions, rail filters, licence permission glyphs, ranks, providers, 64 GB@4bit / 128 GB@8bit estimates, openness explorer: category bar + licence-permission matrix; OG image)
- /pulse Ecosystem Pulse (DataStrip of counters with definitions, 7/30/90 windows, new benchmark leaders, price changes, ≥1M-context models, stats-history sparklines when ≥2 days; OG image)
- /providers 2.0 (TerminalLayout table: models · orgs · input/output distribution range bars on a shared log axis · Δ price 30 d · ± models 30 d · features) and dedicated /providers/[slug] (aggregate strip, deployments with expandable StepChart history, added/removed, price events, features & native price keys, organizations covered, attributes with evidence, JSON-LD, OG)
- /hardware 2.0 (rail: kind · manufacturer · memory ≥; precision/interconnect/TDP columns honest dashes), /hardware/frontier (memory & bandwidth step charts by release + per-manufacturer tables connected to /hardware/{slug}/fit counts) and dedicated /hardware/[slug] (KeyValue specs with evidence, fit table with quant/context/configuration chips, multi-unit note, JSON-LD, OG)
- Typed listings on a new IntelListing (per-page enrichment, honest empty states with connector names): /agents (new), /tools, /datasets (used-to-train + papers from relations, access), /frameworks (canonical kind, stars sparkline from claim history)
- Shared lib: D2 blocks in types.ts / api.ts (`intel`) / client-api.ts (`clientIntel`); reuses the D1 Deployment/Group/LeaderboardRow/LicenseInfo/Pareto shapes
- QA: qa/d2.mjs — 33 routes × 320…1920 × dark/light (status, console errors, overflow, Estimated label, price-index chart or note), flows (offer history, mobile filter sheet, calculator tabs + URL, provider history, run-locally submit, frontier scatter), OG images
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
37 changed files +4,974 −255
added
apps/web/qa/d2.mjs
+209 −0
@@ -0,0 +1,209 @@ | ||
| 1 | +/** | |
| 2 | + * D2 QA sweep — intelligence surfaces (frontier · prices · calculator · run-locally · find-a-model · open · pulse · providers · hardware · | |
| 3 | + * agents · tools · datasets · frameworks) at 320 · 360 · 375 · 390 · 430 · 768 · 1366 · 1440 · 1920, dark + light. | |
| 4 | + * Asserts: HTTP status (404 for unknown slugs), zero console errors, no horizontal overflow, an `Estimated` label on every fit page, | |
| 5 | + * the price-index chart or its honest note on /prices, then a few flows (offer history expansion, calculator result + context tab, | |
| 6 | + * provider history step chart, mobile filter sheet). Slugs are discovered live from the API. | |
| 7 | + * Run: node qa/d2.mjs [BASE_URL] [API_URL] (defaults http://localhost:8342, http://127.0.0.1:8332) | |
| 8 | + */ | |
| 9 | +import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs'; | |
| 10 | +import { mkdirSync } from 'node:fs'; | |
| 11 | + | |
| 12 | +const BASE = process.argv[2] ?? 'http://localhost:8342'; | |
| 13 | +const API = process.argv[3] ?? 'http://127.0.0.1:8332'; | |
| 14 | +const OUT = new URL('./screens/d2/', import.meta.url).pathname; | |
| 15 | +mkdirSync(OUT, { recursive: true }); | |
| 16 | +const QUICK = process.env.QUICK === '1'; | |
| 17 | +const WIDTHS = process.env.WIDTHS ? process.env.WIDTHS.split(',').map(Number) : QUICK ? [390, 1440] : [320, 360, 375, 390, 430, 768, 1366, 1440, 1920]; | |
| 18 | +const THEMES = ['dark', 'light']; | |
| 19 | + | |
| 20 | +const j = async (p) => (await fetch(`${API}/api/v1${p}`)).json(); | |
| 21 | +const providers = await j('/providers').catch(() => ({ items: [] })); | |
| 22 | +const topProvider = (providers.items ?? []).slice().sort((a, b) => Number(b.model_count) - Number(a.model_count))[0]?.slug ?? 'openrouter-api'; | |
| 23 | +const hw = await j('/hardware?limit=50&sort=memory').catch(() => ({ items: [] })); | |
| 24 | +const hwSlug = (hw.items ?? []).find((h) => h.slug === 'apple-m4-max')?.slug ?? hw.items?.[0]?.slug ?? 'apple-m4-max'; | |
| 25 | +const sug = await j('/search/suggest?q=claude').catch(() => ({ items: [] })); | |
| 26 | +const model = (sug.items ?? []).find((s) => s.entity_type === 'model')?.slug ?? 'claude-opus-5'; | |
| 27 | +console.log(`slugs: provider=${topProvider} hardware=${hwSlug} model=${model}`); | |
| 28 | + | |
| 29 | +/** [path, expectedStatus, checks[]] — checks: 'estimated' (an Estimated label exists), 'index' (price chart or honest note). */ | |
| 30 | +const PAGES = [ | |
| 31 | + ['/frontier', 200, []], | |
| 32 | + ['/prices', 200, ['index']], | |
| 33 | + ['/prices?sort=cheapest_frontier&days=30', 200, ['index']], | |
| 34 | + ['/calculator', 200, []], | |
| 35 | + [`/calculator?model=${model}&input_tokens=2000&output_tokens=800&requests_per_day=500&cached=20`, 200, []], | |
| 36 | + [`/calculator?model=${model}&tab=context&tokens=200000`, 200, []], | |
| 37 | + ['/run-locally', 200, ['estimated']], | |
| 38 | + ['/run-locally?memory_gb=64&quant=4bit', 200, ['estimated']], | |
| 39 | + ['/run-locally?memory_gb=24&gpu_count=2&quant=8bit&platform=nvidia&use_case=coding', 200, ['estimated']], | |
| 40 | + [`/run-locally?hardware=${hwSlug}`, 200, ['estimated']], | |
| 41 | + ['/find-a-model', 200, []], | |
| 42 | + ['/find-a-model?use_case=coding&deployment=local&memory_gb=64', 200, ['estimated']], | |
| 43 | + ['/find-a-model?use_case=chat&deployment=api&max_output_price=5', 200, []], | |
| 44 | + ['/open', 200, ['estimated']], | |
| 45 | + ['/open?license=Apache-2.0&min_params=7B&sort=params', 200, ['estimated']], | |
| 46 | + ['/pulse', 200, []], | |
| 47 | + ['/pulse?days=30', 200, []], | |
| 48 | + ['/providers', 200, []], | |
| 49 | + ['/providers?feature=batch&sort=median_input', 200, []], | |
| 50 | + [`/providers/${topProvider}`, 200, []], | |
| 51 | + ['/providers/does-not-exist', 404, []], | |
| 52 | + ['/hardware', 200, []], | |
| 53 | + ['/hardware?kind=gpu&min_memory=64', 200, []], | |
| 54 | + ['/hardware/frontier', 200, ['estimated']], | |
| 55 | + [`/hardware/${hwSlug}`, 200, ['estimated']], | |
| 56 | + [`/hardware/${hwSlug}?quant=8bit&context=32768`, 200, ['estimated']], | |
| 57 | + ['/hardware/does-not-exist', 404, []], | |
| 58 | + ['/agents', 200, []], | |
| 59 | + ['/tools', 200, []], | |
| 60 | + ['/datasets', 200, []], | |
| 61 | + ['/frameworks', 200, []], | |
| 62 | + ['/frameworks?sort=stars', 200, []], | |
| 63 | +]; | |
| 64 | +const OG = ['/frontier/opengraph-image', '/prices/opengraph-image', '/open/opengraph-image', '/pulse/opengraph-image', `/providers/${topProvider}/opengraph-image`, `/hardware/${hwSlug}/opengraph-image`]; | |
| 65 | + | |
| 66 | +// 404 favicon noise and 429 on the fire-and-forget view beacon (the API allows 1 view/s per IP; the sweep is much faster) are not page errors | |
| 67 | +const filterErrors = (errors, expected) => errors.filter((e) => !/favicon|Failed to load resource: the server responded with a status of (404|429)/.test(e)).filter((e) => !(expected === 404 && /Encountered a script tag while rendering React component/.test(e))); | |
| 68 | +const smallTargets = () => { | |
| 69 | + const root = document.querySelector('[role="dialog"]') ?? document; | |
| 70 | + return [...root.querySelectorAll('button, [role="button"], input:not([type=hidden]):not([type=radio]):not([type=checkbox]), select, a[href]')] | |
| 71 | + .filter((el) => { | |
| 72 | + const r = el.getBoundingClientRect(); | |
| 73 | + if (r.width === 0 || r.height === 0) return false; | |
| 74 | + const cs = getComputedStyle(el); | |
| 75 | + if (cs.visibility === 'hidden' || cs.display === 'none') return false; | |
| 76 | + if (el.closest('.sr-only, p, dd, .prose-atlas, td, th, .kv, [data-ticker], .data-table, footer, header, [data-section-nav], .hint, li')) return false; | |
| 77 | + if (el.tagName === 'A') { | |
| 78 | + const inNav = !!el.closest('nav, [role="menu"], [role="listbox"]'); | |
| 79 | + if (!inNav && el.textContent.trim().length > 0) return false; | |
| 80 | + } | |
| 81 | + return Math.min(r.width, r.height) < 44; | |
| 82 | + }) | |
| 83 | + .map((el) => `${el.tagName.toLowerCase()}.${[...el.classList].slice(0, 2).join('.')} "${(el.getAttribute('aria-label') || el.textContent || el.getAttribute('name') || '').trim().slice(0, 20)}" ${Math.round(el.getBoundingClientRect().width)}×${Math.round(el.getBoundingClientRect().height)}`); | |
| 84 | +}; | |
| 85 | +const slug = (p) => p.replace(/[^a-z0-9]+/gi, '_').replace(/^_|_$/g, '').slice(0, 80) || 'home'; | |
| 86 | + | |
| 87 | +const browser = await chromium.launch(); | |
| 88 | +let failures = 0; | |
| 89 | +let smallTotal = 0; | |
| 90 | + | |
| 91 | +for (const theme of THEMES) { | |
| 92 | + for (const width of WIDTHS) { | |
| 93 | + const mobile = width < 768; | |
| 94 | + const ctx = await browser.newContext({ viewport: { width, height: mobile ? 844 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: theme }); | |
| 95 | + await ctx.addInitScript((t) => localStorage.setItem('aia-theme', t), theme); | |
| 96 | + const page = await ctx.newPage(); | |
| 97 | + for (const [path, expected, checks] of PAGES) { | |
| 98 | + // full matrix in dark; light at the four reference widths to keep the sweep under control | |
| 99 | + if (theme === 'light' && ![390, 430, 768, 1440].includes(width) && !QUICK && !process.env.WIDTHS) continue; | |
| 100 | + const errors = []; | |
| 101 | + const onErr = (e) => errors.push(String(e)); | |
| 102 | + const onCon = (m) => { if (m.type() === 'error') errors.push(m.text()); }; | |
| 103 | + page.on('pageerror', onErr); | |
| 104 | + page.on('console', onCon); | |
| 105 | + const t0 = Date.now(); | |
| 106 | + const res = await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 90000 }).catch((e) => ({ status: () => `ERR ${e.message.slice(0, 40)}` })); | |
| 107 | + if (/calculator\?model/.test(path)) await page.waitForSelector('[data-cost-row], [data-context-row], [data-calculator] p', { timeout: 15000 }).catch(() => undefined); | |
| 108 | + await page.waitForTimeout(500); | |
| 109 | + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth).catch(() => -1); | |
| 110 | + const applied = await page.evaluate(() => document.documentElement.getAttribute('data-theme')).catch(() => null); | |
| 111 | + const est = checks.includes('estimated') ? await page.evaluate(() => !![...document.querySelectorAll('a,span')].find((el) => /^estimated$/i.test(el.textContent.trim()))).catch(() => false) : true; | |
| 112 | + const idx = checks.includes('index') ? await page.evaluate(() => !!document.querySelector('[data-price-index-chart], [data-price-index-note]')).catch(() => false) : true; | |
| 113 | + const small = mobile ? await page.evaluate(smallTargets).catch(() => []) : []; | |
| 114 | + smallTotal += small.length; | |
| 115 | + const status = res.status(); | |
| 116 | + const filtered = filterErrors(errors, expected); | |
| 117 | + const ok = status === expected && overflow <= 0 && filtered.length === 0 && applied === theme && est && idx; | |
| 118 | + if (!ok) failures++; | |
| 119 | + console.log(`${ok ? 'OK ' : 'FAIL'} ${theme.padEnd(5)} ${String(width).padStart(4)} ${status} ${String(Date.now() - t0).padStart(5)}ms overflow=${overflow} errors=${filtered.length}${checks.includes('estimated') ? ` estimated=${est}` : ''}${checks.includes('index') ? ` index=${idx}` : ''}${small.length ? ` small=${small.length}` : ''} ${path}${filtered.length ? ' :: ' + filtered[0].slice(0, 160) : ''}${small.length ? ' :: ' + small.slice(0, 2).join(' | ') : ''}`); | |
| 120 | + if (theme === 'dark' || width === 390 || width === 1440) await page.screenshot({ path: `${OUT}${theme}-${width}-${slug(path)}.png`, fullPage: false }).catch(() => undefined); | |
| 121 | + page.off('pageerror', onErr); | |
| 122 | + page.off('console', onCon); | |
| 123 | + } | |
| 124 | + await ctx.close(); | |
| 125 | + } | |
| 126 | +} | |
| 127 | + | |
| 128 | +// ---------------------------------------------------------------------------------------------------------------- flows | |
| 129 | +console.log('\n— flows —'); | |
| 130 | +{ | |
| 131 | + const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, colorScheme: 'dark' }); | |
| 132 | + await ctx.addInitScript(() => localStorage.setItem('aia-theme', 'dark')); | |
| 133 | + const page = await ctx.newPage(); | |
| 134 | + const errors = []; | |
| 135 | + page.on('pageerror', (e) => errors.push(String(e))); | |
| 136 | + page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); }); | |
| 137 | + const step = (ok, label) => { if (!ok) failures++; console.log(`${ok ? 'OK ' : 'FAIL'} flow ${label}`); }; | |
| 138 | + | |
| 139 | + // prices: expand the first offer row → history row appears (sparklines or the honest "no change" note) | |
| 140 | + await page.goto(`${BASE}/prices`, { waitUntil: 'networkidle' }); | |
| 141 | + await page.locator('[data-expand-history]').first().click(); | |
| 142 | + const hist = await page.waitForSelector('[data-offer-history]', { timeout: 15000 }).catch(() => null); | |
| 143 | + await page.waitForTimeout(1500); | |
| 144 | + const histText = hist ? await page.locator('[data-offer-history]').first().innerText() : ''; | |
| 145 | + step(!!hist && !/Loading/.test(histText), `prices: offer row expands to history (${histText.replace(/\s+/g, ' ').slice(0, 60)}…)`); | |
| 146 | + await page.screenshot({ path: `${OUT}flow-prices-history.png` }); | |
| 147 | + | |
| 148 | + // prices mobile: filter sheet opens with the rail form | |
| 149 | + await page.setViewportSize({ width: 390, height: 844 }); | |
| 150 | + await page.goto(`${BASE}/prices`, { waitUntil: 'networkidle' }); | |
| 151 | + await page.locator('[data-open-filters]').click(); | |
| 152 | + const sheetForm = await page.waitForSelector('[role="dialog"] [data-price-filters]', { timeout: 8000 }).catch(() => null); | |
| 153 | + step(!!sheetForm, 'prices mobile: filter sheet shows the rail form'); | |
| 154 | + await page.screenshot({ path: `${OUT}flow-prices-filter-sheet-390.png` }); | |
| 155 | + await page.keyboard.press('Escape'); | |
| 156 | + await page.setViewportSize({ width: 1440, height: 900 }); | |
| 157 | + | |
| 158 | + // calculator: URL-seeded model → cost rows; switch to Context tab → context rows; URL mirrors the tab | |
| 159 | + await page.goto(`${BASE}/calculator?model=${model}&input_tokens=2000&output_tokens=800`, { waitUntil: 'networkidle' }); | |
| 160 | + const costRows = await page.waitForSelector('[data-cost-row]', { timeout: 20000 }).catch(() => null); | |
| 161 | + step(!!costRows, 'calculator: cost rows render for the URL model'); | |
| 162 | + await page.screenshot({ path: `${OUT}flow-calculator-workload.png` }); | |
| 163 | + await page.locator('[data-calc-tab="context"]').click(); | |
| 164 | + const ctxRows = await page.waitForSelector('[data-context-row]', { timeout: 20000 }).catch(() => null); | |
| 165 | + await page.waitForURL(/tab=context/, { timeout: 10000 }).catch(() => undefined); | |
| 166 | + step(!!ctxRows && /tab=context/.test(page.url()), `calculator: context tab renders rows and URL carries tab=context (${page.url().split('?')[1]})`); | |
| 167 | + await page.screenshot({ path: `${OUT}flow-calculator-context.png` }); | |
| 168 | + | |
| 169 | + // provider page: expand a deployment → step chart or honest note | |
| 170 | + await page.goto(`${BASE}/providers/${topProvider}`, { waitUntil: 'networkidle' }); | |
| 171 | + await page.locator('[data-expand-history]').first().click(); | |
| 172 | + const ph = await page.waitForSelector('[data-offer-history]', { timeout: 15000 }).catch(() => null); | |
| 173 | + await page.waitForTimeout(1500); | |
| 174 | + step(!!ph, 'provider: deployment row expands to price history'); | |
| 175 | + await page.screenshot({ path: `${OUT}flow-provider-history.png` }); | |
| 176 | + | |
| 177 | + // run-locally: form submit round-trips through the URL and shows the Estimated banner | |
| 178 | + await page.goto(`${BASE}/run-locally`, { waitUntil: 'networkidle' }); | |
| 179 | + await page.selectOption('[data-run-locally-form] select[name="memory_gb"]', '128'); | |
| 180 | + await page.selectOption('[data-run-locally-form] select[name="quant"]', '8bit'); | |
| 181 | + await page.locator('[data-run-locally-form] button[type="submit"]').click(); | |
| 182 | + await page.waitForLoadState('networkidle'); | |
| 183 | + const banner = await page.$('[data-estimate-banner]'); | |
| 184 | + step(!!banner && /memory_gb=128/.test(page.url()) && /quant=8bit/.test(page.url()), `run-locally: submit → ${page.url().split('?')[1]} with estimate banner`); | |
| 185 | + await page.screenshot({ path: `${OUT}flow-run-locally-128-8bit.png` }); | |
| 186 | + | |
| 187 | + // frontier: section nav present and the efficiency scatter renders points | |
| 188 | + await page.goto(`${BASE}/frontier`, { waitUntil: 'networkidle' }); | |
| 189 | + const nav = await page.$('[data-section-nav]'); | |
| 190 | + const scatter = await page.$('[data-scatter] circle'); | |
| 191 | + step(!!nav && !!scatter, 'frontier: section nav + efficiency scatter points'); | |
| 192 | + | |
| 193 | + const filtered = filterErrors(errors, 200); | |
| 194 | + step(filtered.length === 0, `flows: zero console errors${filtered.length ? ' :: ' + filtered[0].slice(0, 160) : ''}`); | |
| 195 | + await ctx.close(); | |
| 196 | +} | |
| 197 | + | |
| 198 | +// ---------------------------------------------------------------------------------------------------------------- OG images | |
| 199 | +for (const p of OG) { | |
| 200 | + const r = await fetch(BASE + p).catch(() => null); | |
| 201 | + const ok = !!r && r.status === 200 && /image\/png/.test(r.headers.get('content-type') ?? ''); | |
| 202 | + if (!ok) failures++; | |
| 203 | + console.log(`${ok ? 'OK ' : 'FAIL'} og ${r?.status ?? 'ERR'} ${r?.headers.get('content-type') ?? ''} ${p}`); | |
| 204 | +} | |
| 205 | + | |
| 206 | +await browser.close(); | |
| 207 | +console.log(`\nsmall touch targets seen on mobile widths (informational): ${smallTotal}`); | |
| 208 | +console.log(failures ? `\n${failures} failure(s)` : '\nall checks OK'); | |
| 209 | +process.exit(failures ? 1 : 0); | |
added
apps/web/src/app/agents/page.tsx
+82 −0
@@ -0,0 +1,82 @@ | ||
| 1 | +import type { Metadata } from 'next'; | |
| 2 | +import Link from 'next/link'; | |
| 3 | +import { Dash, IntelListing, list, str } from '@/components/intelligence/listing-table'; | |
| 4 | +import { Chip, EntityBadge } from '@/components/ui/badges'; | |
| 5 | +import { EntityLink, QualityMark } from '@/components/ui/entity'; | |
| 6 | +import { api, safe } from '@/lib/api'; | |
| 7 | +import { fmtDate } from '@/lib/format'; | |
| 8 | +import { routes, SITE_NAME, SITE_URL } from '@/lib/site'; | |
| 9 | +import type { EntitySummary } from '@/lib/types'; | |
| 10 | + | |
| 11 | +export const metadata: Metadata = { title: 'AI agents — developer, kind, underlying models, platform, licence', description: 'Agents recorded in the atlas: developer, agent kind, the models they run on (relations), platform, repository, licence and release date — from official sources only.', alternates: { canonical: routes.agents() }, openGraph: { title: `AI agents | ${SITE_NAME}`, url: `${SITE_URL}${routes.agents()}`, siteName: SITE_NAME } }; | |
| 12 | +export const revalidate = 300; | |
| 13 | + | |
| 14 | +type Extra = { models: EntitySummary[]; total: number }; | |
| 15 | +const AGENT_KINDS: Record<string, string> = { coding: 'Coding agent', browser: 'Browser agent', computer_use: 'Computer use', research: 'Research agent', assistant: 'Assistant', workflow: 'Workflow', voice: 'Voice agent', mcp_server: 'MCP server' }; | |
| 16 | + | |
| 17 | +export default async function AgentsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) { | |
| 18 | + const sp = await searchParams; | |
| 19 | + return ( | |
| 20 | + <IntelListing<Extra> | |
| 21 | + title="Agents" | |
| 22 | + eyebrow="Agents & tools" | |
| 23 | + lede={<>Autonomous and semi-autonomous systems built on the models in the atlas: who develops them, what kind they are, which models they run on, where they run. Related software lives under <Link href={routes.frameworks()} className="link">Frameworks</Link>; developer tools under <Link href={routes.tools()} className="link">Tools</Link>.</>} | |
| 24 | + basePath="/agents" | |
| 25 | + searchParams={sp} | |
| 26 | + fetch={(q) => api.explore('agent', q)} | |
| 27 | + sorts={[ | |
| 28 | + { value: 'updated', label: 'Recently updated' }, | |
| 29 | + { value: 'release', label: 'Release date' }, | |
| 30 | + { value: 'stars', label: 'Stars' }, | |
| 31 | + { value: 'name', label: 'Name' }, | |
| 32 | + ]} | |
| 33 | + filters={sp.org ? [{ kind: 'hidden', name: 'org', value: sp.org }] : []} | |
| 34 | + connectors={['github (agent repositories)', 'official product pages', 'mcp registries']} | |
| 35 | + emptyHint={<>The atlas declares the <span className="mono">agent</span> entity type but no connector has produced one yet. <Link href={routes.frameworks()} className="link">Agent frameworks</Link> (LangChain, CrewAI, …) are listed under Frameworks.</>} | |
| 36 | + enrich={async (items) => { | |
| 37 | + const details = await Promise.all(items.map((e) => safe(api.entity(e.slug)))); | |
| 38 | + const m = new Map<string, Extra>(); | |
| 39 | + details.forEach((d, i) => { | |
| 40 | + if (!d) return; | |
| 41 | + const rel = (d.relations ?? []).filter((g) => g.direction === 'out' && ['uses', 'runs_on', 'derived_from', 'integrates'].includes(g.predicate)); | |
| 42 | + const models = rel.flatMap((g) => g.items.filter((x) => x.entity_type === 'model')); | |
| 43 | + m.set(items[i]!.slug, { models, total: rel.reduce((n, g) => n + g.total, 0) }); | |
| 44 | + }); | |
| 45 | + return m; | |
| 46 | + }} | |
| 47 | + columns={[ | |
| 48 | + { | |
| 49 | + key: 'name', | |
| 50 | + label: 'Agent', | |
| 51 | + primary: true, | |
| 52 | + render: (e) => ( | |
| 53 | + <> | |
| 54 | + <span className="flex flex-wrap items-center gap-x-2 gap-y-0.5"> | |
| 55 | + <EntityLink e={e} /> | |
| 56 | + {e.entity_type !== 'agent' && <EntityBadge type={e.entity_type} small />} | |
| 57 | + </span> | |
| 58 | + {e.description && <span className="block max-w-md truncate text-xs text-ink-3">{e.description}</span>} | |
| 59 | + </> | |
| 60 | + ), | |
| 61 | + }, | |
| 62 | + { key: 'developer', label: 'Developer', className: 'text-ink-2', render: (e) => (e.organization ? <Link href={routes.entity({ entity_type: 'company', slug: e.organization.slug })} className="hover:text-accent">{e.organization.name}</Link> : str(e.attributes?.developer) ?? <Dash />) }, | |
| 63 | + { key: 'kind', label: 'Agent kind', render: (e) => { const k = str(e.attributes?.agent_kind) ?? str(e.attributes?.kind) ?? str(e.attributes?.category); return k ? <Chip>{AGENT_KINDS[k] ?? k.replace(/_/g, ' ')}</Chip> : <Dash />; } }, | |
| 64 | + { | |
| 65 | + key: 'models', | |
| 66 | + label: 'Underlying models', | |
| 67 | + render: (e, x) => { | |
| 68 | + const fromAttr = list(e.attributes?.models ?? e.attributes?.underlying_models); | |
| 69 | + if (x?.models.length) return <span className="flex flex-wrap gap-1">{x.models.slice(0, 4).map((m) => <EntityLink key={m.id} e={m} className="text-xs" />)}{x.models.length > 4 && <span className="text-xs text-ink-3">+{x.models.length - 4}</span>}</span>; | |
| 70 | + return fromAttr.length ? <span className="text-xs text-ink-2">{fromAttr.join(', ')}</span> : <Dash />; | |
| 71 | + }, | |
| 72 | + }, | |
| 73 | + { key: 'platform', label: 'Platform', className: 'text-ink-2 text-xs', render: (e) => { const p = list(e.attributes?.platforms ?? e.attributes?.platform); return p.length ? p.join(' · ') : <Dash />; } }, | |
| 74 | + { key: 'repo', label: 'Repository', className: 'text-xs', render: (e) => { const u = str(e.attributes?.repository_url) ?? str(e.attributes?.official_url); return u ? <a href={u} target="_blank" rel="noopener noreferrer" className="text-ink-2 hover:text-accent">{u.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '').slice(0, 40)}</a> : <Dash />; } }, | |
| 75 | + { key: 'license', label: 'Licence', className: 'max-w-[10rem] truncate text-ink-2', render: (e) => str(e.attributes?.license) ?? <Dash /> }, | |
| 76 | + { key: 'release', label: 'Release', className: 'tnum text-ink-2 whitespace-nowrap', render: (e) => (str(e.attributes?.release_date) ? fmtDate(str(e.attributes.release_date)) : <Dash />) }, | |
| 77 | + { key: 'quality', label: 'Quality', num: true, render: (e) => <QualityMark q={e.quality?.score} /> }, | |
| 78 | + ]} | |
| 79 | + note="Underlying models come from graph relations (uses · runs_on · derived_from · integrates) on each agent, else from the agent's stated attributes; a dash means no source has stated it." | |
| 80 | + /> | |
| 81 | + ); | |
| 82 | +} | |
added
apps/web/src/app/calculator/page.tsx
+30 −0
@@ -0,0 +1,30 @@ | ||
| 1 | +import type { Metadata } from 'next'; | |
| 2 | +import Link from 'next/link'; | |
| 3 | +import { Suspense } from 'react'; | |
| 4 | +import { Calculator } from '@/components/intelligence/calculator'; | |
| 5 | +import { Container, Note, PageHeader } from '@/components/ui/section'; | |
| 6 | +import { routes, SITE_NAME, SITE_URL } from '@/lib/site'; | |
| 7 | + | |
| 8 | +type SP = Record<string, string | undefined>; | |
| 9 | +const TITLE = 'AI cost calculator — price a workload across every provider'; | |
| 10 | +const DESC = 'Pick a model, enter tokens per request, requests per day, cached share and batch: the calculator prices the workload per request, day, month and year for every provider that currently serves the model, from published prices only, plus the cost of filling a 128K, 200K, 1M or 2M-token context.'; | |
| 11 | + | |
| 12 | +export async function generateMetadata({ searchParams }: { searchParams: Promise<SP> }): Promise<Metadata> { | |
| 13 | + const sp = await searchParams; | |
| 14 | + const title = sp.model ? `Cost of ${sp.name ?? sp.model} per request, day, month and year — every provider` : TITLE; | |
| 15 | + return { title, description: DESC, alternates: { canonical: routes.calculator() }, openGraph: { title: `${title} | ${SITE_NAME}`, description: DESC, url: `${SITE_URL}${routes.calculator()}`, siteName: SITE_NAME }, robots: Object.keys(sp).length ? { index: false, follow: true } : undefined }; | |
| 16 | +} | |
| 17 | + | |
| 18 | +export default function CalculatorPage() { | |
| 19 | + return ( | |
| 20 | + <Container wide> | |
| 21 | + <PageHeader eyebrow="Cost calculator" title="What will this workload cost?" lede="Per request, per day, per month, per year — for every provider that currently serves the model, side by side, from the prices they publish. The second tab prices a fully populated context window. Inputs live in the URL, so a result is shareable." /> | |
| 22 | + <Suspense fallback={<p className="py-10 text-center text-sm text-ink-3" aria-busy="true">Loading calculator…</p>}> | |
| 23 | + <Calculator /> | |
| 24 | + </Suspense> | |
| 25 | + <Note className="mt-8 pb-16"> | |
| 26 | + Computation: <Link href="/developers" className="link">GET /cost</Link> and <Link href="/developers" className="link">GET /cost/context</Link> on current offers only — no rate limits, latency or quality are modelled. See also the <Link href={routes.prices()} className="link">price terminal</Link> and <Link href={routes.findAModel()} className="link">Find a model</Link>. | |
| 27 | + </Note> | |
| 28 | + </Container> | |
| 29 | + ); | |
| 30 | +} | |
modified
apps/web/src/app/datasets/page.tsx
+30 −33
@@ -1,22 +1,24 @@ | ||
| 1 | 1 | import type { Metadata } from 'next'; |
| 2 | 2 | import Link from 'next/link'; |
| 3 | −import { Dash, list, str, TypedListing } from '@/components/listing/typed-listing'; | |
| 3 | +import { Dash, IntelListing, list, str } from '@/components/intelligence/listing-table'; | |
| 4 | 4 | import { Chip } from '@/components/ui/badges'; |
| 5 | 5 | import { EntityLink, QualityMark } from '@/components/ui/entity'; |
| 6 | −import { api } from '@/lib/api'; | |
| 7 | −import { fmtAgo, fmtValue } from '@/lib/format'; | |
| 8 | −import { routes } from '@/lib/site'; | |
| 6 | +import { api, safe } from '@/lib/api'; | |
| 7 | +import { fmtInt, fmtValue } from '@/lib/format'; | |
| 8 | +import { routes, SITE_NAME, SITE_URL } from '@/lib/site'; | |
| 9 | 9 | |
| 10 | −export const metadata: Metadata = { title: 'AI datasets — modality, size, license', description: 'Training and evaluation datasets in the atlas with modality, size, license and publisher as stated by their hosts.', alternates: { canonical: '/datasets' } }; | |
| 10 | +export const metadata: Metadata = { title: 'AI datasets — publisher, modality, size, licence, access, models trained on them', description: 'Training and evaluation datasets in the atlas with publisher, modality, size, licence and access as stated by their hosts, plus how many models the graph records as trained on each and the papers that describe them.', alternates: { canonical: routes.datasets() }, openGraph: { title: `AI datasets | ${SITE_NAME}`, url: `${SITE_URL}${routes.datasets()}`, siteName: SITE_NAME } }; | |
| 11 | 11 | export const revalidate = 300; |
| 12 | 12 | |
| 13 | +type Extra = { trained: number; papers: number; access: string | null }; | |
| 14 | + | |
| 13 | 15 | export default async function DatasetsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) { |
| 14 | 16 | const sp = await searchParams; |
| 15 | 17 | return ( |
| 16 | − <TypedListing | |
| 18 | + <IntelListing<Extra> | |
| 17 | 19 | title="Datasets" |
| 18 | 20 | eyebrow="Datasets" |
| 19 | − lede="Training and evaluation datasets referenced by models and benchmarks. Modality, size and license are read from dataset cards and hosting pages." | |
| 21 | + lede="Training and evaluation datasets referenced by models and benchmarks. Modality, size, licence and access are read from dataset cards and hosting pages; the “used to train” count comes from the graph (uses_dataset relations)." | |
| 20 | 22 | basePath="/datasets" |
| 21 | 23 | searchParams={sp} |
| 22 | 24 | fetch={(q) => api.explore('dataset', q)} |
@@ -27,7 +29,20 @@ export default async function DatasetsPage({ searchParams }: { searchParams: Pro | ||
| 27 | 29 | { value: 'quality', label: 'Data quality' }, |
| 28 | 30 | ]} |
| 29 | 31 | filters={sp.org ? [{ kind: 'hidden', name: 'org', value: sp.org }] : []} |
| 30 | − emptyTitle="No datasets match" | |
| 32 | + connectors={['huggingface (dataset cards)', 'arxiv (papers naming datasets)', 'model cards']} | |
| 33 | + enrich={async (items) => { | |
| 34 | + const details = await Promise.all(items.map((e) => safe(api.entity(e.slug)))); | |
| 35 | + const m = new Map<string, Extra>(); | |
| 36 | + details.forEach((d, i) => { | |
| 37 | + if (!d) return; | |
| 38 | + const trained = (d.relations ?? []).filter((g) => g.predicate === 'uses_dataset' && g.direction === 'in').reduce((n, g) => n + (g.total || g.items.length), 0); | |
| 39 | + const papers = (d.papers?.length ?? 0) + (d.relations ?? []).filter((g) => g.predicate === 'described_by' && g.direction === 'out').reduce((n, g) => n + (g.total || g.items.length), 0); | |
| 40 | + const a = d.attributes ?? {}; | |
| 41 | + const access = typeof a.access === 'string' ? a.access : a.gated === true ? 'gated' : a.gated === false ? 'public' : null; | |
| 42 | + m.set(items[i]!.slug, { trained, papers, access }); | |
| 43 | + }); | |
| 44 | + return m; | |
| 45 | + }} | |
| 31 | 46 | columns={[ |
| 32 | 47 | { |
| 33 | 48 | key: 'name', |
@@ -40,41 +55,23 @@ export default async function DatasetsPage({ searchParams }: { searchParams: Pro | ||
| 40 | 55 | </> |
| 41 | 56 | ), |
| 42 | 57 | }, |
| 58 | + { key: 'publisher', label: 'Publisher', className: 'text-ink-2', render: (e) => (e.organization ? <Link href={routes.entity({ entity_type: 'company', slug: e.organization.slug })} className="hover:text-accent">{e.organization.name}</Link> : str(e.attributes?.publisher) ?? <Dash />) }, | |
| 43 | 59 | { |
| 44 | 60 | key: 'modality', |
| 45 | 61 | label: 'Modality', |
| 46 | 62 | render: (e) => { |
| 47 | 63 | const m = str(e.attributes?.modality) ? [str(e.attributes.modality) as string] : list(e.attributes?.modalities); |
| 48 | − return m.length ? ( | |
| 49 | − <span className="flex flex-wrap gap-1"> | |
| 50 | − {m.slice(0, 3).map((x) => ( | |
| 51 | − <Chip key={x}>{x}</Chip> | |
| 52 | − ))} | |
| 53 | − </span> | |
| 54 | − ) : ( | |
| 55 | − <Dash /> | |
| 56 | − ); | |
| 64 | + return m.length ? <span className="flex flex-wrap gap-1">{m.slice(0, 3).map((x) => <Chip key={x}>{x}</Chip>)}</span> : <Dash />; | |
| 57 | 65 | }, |
| 58 | 66 | }, |
| 59 | 67 | { key: 'size', label: 'Size', className: 'tnum text-ink-2', render: (e) => (e.attributes?.size ? fmtValue(e.attributes.size, 'size') : <Dash />) }, |
| 60 | − { key: 'license', label: 'License', className: 'max-w-[10rem] truncate text-ink-2', render: (e) => str(e.attributes?.license) ?? <Dash /> }, | |
| 61 | − { | |
| 62 | − key: 'publisher', | |
| 63 | − label: 'Publisher', | |
| 64 | − className: 'text-ink-2', | |
| 65 | − render: (e) => | |
| 66 | − e.organization ? ( | |
| 67 | − <Link href={routes.entity({ entity_type: 'company', slug: e.organization.slug })} className="hover:text-accent"> | |
| 68 | − {e.organization.name} | |
| 69 | − </Link> | |
| 70 | − ) : ( | |
| 71 | − str(e.attributes?.publisher) ?? <Dash /> | |
| 72 | − ), | |
| 73 | − }, | |
| 74 | − { key: 'updated', label: 'Updated', className: 'text-ink-2 whitespace-nowrap', render: (e) => <span title={e.updated_at}>{fmtAgo(e.updated_at)}</span> }, | |
| 68 | + { key: 'license', label: 'Licence', className: 'max-w-[10rem] truncate text-ink-2', render: (e) => str(e.attributes?.license) ?? <Dash /> }, | |
| 69 | + { key: 'access', label: 'Access', className: 'text-ink-2', render: (e, x) => (x?.access ? <Chip tone={x.access === 'gated' ? 'accent' : 'neutral'}>{x.access}</Chip> : str(e.attributes?.access) ?? <Dash />) }, | |
| 70 | + { key: 'trained', label: 'Used to train', num: true, className: 'tnum', render: (_e, x) => (x ? (x.trained ? fmtInt(x.trained) : <span className="text-ink-3">0</span>) : <Dash />) }, | |
| 71 | + { key: 'papers', label: 'Papers', num: true, className: 'tnum', render: (_e, x) => (x ? (x.papers ? fmtInt(x.papers) : <span className="text-ink-3">0</span>) : <Dash />) }, | |
| 75 | 72 | { key: 'quality', label: 'Quality', num: true, render: (e) => <QualityMark q={e.quality?.score} /> }, |
| 76 | 73 | ]} |
| 77 | − note="Many datasets are known only by reference (a model card names them) and carry no attributes yet — the dash means the sources have not stated it." | |
| 74 | + note="Many datasets are known only by reference (a model card names them) and carry no attributes yet — the dash means the sources have not stated it. “Used to train” counts inbound uses_dataset relations; “Papers” counts papers linked to the dataset." | |
| 78 | 75 | /> |
| 79 | 76 | ); |
| 80 | 77 | } |
added
apps/web/src/app/find-a-model/page.tsx
+227 −0
@@ -0,0 +1,227 @@ | ||
| 1 | +import type { Metadata } from 'next'; | |
| 2 | +import Link from 'next/link'; | |
| 3 | +import { CompareButton } from '@/components/compare/compare-button'; | |
| 4 | +import { CompareTrayBar } from '@/components/compare/compare-tray-bar'; | |
| 5 | +import { QUANTS } from '@/components/hardware/fit-form'; | |
| 6 | +import { FINDER_USE_CASES, FinderForm, type FinderInputs, MODALITIES } from '@/components/intelligence/finder-form'; | |
| 7 | +import { FitCell, Methodology, RankChips } from '@/components/intelligence/bits'; | |
| 8 | +import { TerminalLayout } from '@/components/layout/terminal'; | |
| 9 | +import { Chip, Estimated, OpennessBadge } from '@/components/ui/badges'; | |
| 10 | +import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table'; | |
| 11 | +import { EntityLink } from '@/components/ui/entity'; | |
| 12 | +import { Note, PageHeader } from '@/components/ui/section'; | |
| 13 | +import { EmptyState, Unavailable } from '@/components/ui/unavailable'; | |
| 14 | +import { WatchButton } from '@/components/watchlist/watch-button'; | |
| 15 | +import { ApiError, intel, safe } from '@/lib/api'; | |
| 16 | +import { fmtDate, fmtInt, fmtParams, fmtTokens, fmtUsdPerM, num } from '@/lib/format'; | |
| 17 | +import { routes, SITE_NAME, SITE_URL } from '@/lib/site'; | |
| 18 | +import type { FinderPayload } from '@/lib/types'; | |
| 19 | + | |
| 20 | +export const revalidate = 300; | |
| 21 | +type SP = Record<string, string | string[] | undefined>; | |
| 22 | +const one = (v: string | string[] | undefined) => (Array.isArray(v) ? v[0] : v); | |
| 23 | + | |
| 24 | +function parse(sp: SP): FinderInputs { | |
| 25 | + const custom = num(one(sp.memory_custom)); | |
| 26 | + const preset = num(one(sp.memory_gb)); | |
| 27 | + const mods = (Array.isArray(sp.mod) ? sp.mod : sp.mod ? [sp.mod] : one(sp.modalities)?.split(',') ?? []).filter((m) => MODALITIES.includes(m)); | |
| 28 | + const dep = one(sp.deployment); | |
| 29 | + const lic = one(sp.license); | |
| 30 | + return { | |
| 31 | + useCase: FINDER_USE_CASES.some((u) => u.value === one(sp.use_case)) ? (one(sp.use_case) as string) : '', | |
| 32 | + deployment: dep === 'local' || dep === 'api' ? dep : 'any', | |
| 33 | + memory: custom !== null && custom > 0 ? custom : preset !== null && preset > 0 ? preset : null, | |
| 34 | + quant: QUANTS.some((q) => q.value === one(sp.quant)) ? (one(sp.quant) as string) : '4bit', | |
| 35 | + contextMin: num(one(sp.context_min)), | |
| 36 | + license: lic === 'commercial' ? 'commercial' : 'any', | |
| 37 | + openness: ['open-source', 'open-weights', 'restricted-weights', 'proprietary'].includes(one(sp.openness) ?? '') ? (one(sp.openness) as string) : '', | |
| 38 | + maxIn: num(one(sp.max_input_price)), | |
| 39 | + maxOut: num(one(sp.max_output_price)), | |
| 40 | + modalities: mods, | |
| 41 | + }; | |
| 42 | +} | |
| 43 | +const active = (v: FinderInputs) => [v.useCase, v.deployment !== 'any', v.memory, v.contextMin, v.license !== 'any', v.openness, v.maxIn, v.maxOut, v.modalities.length].filter(Boolean).length; | |
| 44 | + | |
| 45 | +const TITLE = 'Find a model — guided search over observed dimensions'; | |
| 46 | +const DESC = 'Tell the atlas what you need — use case, local or API deployment, memory, licence, openness, context, price ceiling, modalities — and it lists the canonical models that satisfy the criteria, with the reasons, their observed dimensions, an estimated local fit and current deployments. Deterministic rules, no composite score, no single winner.'; | |
| 47 | +export async function generateMetadata({ searchParams }: { searchParams: Promise<SP> }): Promise<Metadata> { | |
| 48 | + const v = parse(await searchParams); | |
| 49 | + const n = active(v); | |
| 50 | + const title = n ? `Models for ${[FINDER_USE_CASES.find((u) => u.value === v.useCase)?.label, v.deployment !== 'any' ? v.deployment : null, v.memory ? `${v.memory} GB` : null].filter(Boolean).join(' · ') || 'your criteria'}` : TITLE; | |
| 51 | + return { title, description: DESC, alternates: { canonical: routes.findAModel() }, openGraph: { title: `${title} | ${SITE_NAME}`, description: DESC, url: `${SITE_URL}${routes.findAModel()}`, siteName: SITE_NAME }, robots: n ? { index: false, follow: true } : undefined }; | |
| 52 | +} | |
| 53 | + | |
| 54 | +export default async function FindAModelPage({ searchParams }: { searchParams: Promise<SP> }) { | |
| 55 | + const sp = await searchParams; | |
| 56 | + const v = parse(sp); | |
| 57 | + const n = active(v); | |
| 58 | + let res: FinderPayload | null = null; | |
| 59 | + let error: string | null = null; | |
| 60 | + if (n > 0) { | |
| 61 | + try { | |
| 62 | + res = await intel.findAModel({ use_case: v.useCase || undefined, deployment: v.deployment, memory_gb: v.memory ?? undefined, quant: v.deployment === 'local' || v.memory ? v.quant : undefined, context_min: v.contextMin ?? undefined, license: v.license, openness: v.openness || undefined, max_input_price: v.maxIn ?? undefined, max_output_price: v.maxOut ?? undefined, modalities: v.modalities.length ? v.modalities.join(',') : undefined, limit: 40 }); | |
| 63 | + } catch (e) { | |
| 64 | + error = e instanceof ApiError ? e.detail ?? e.message : 'API unreachable'; | |
| 65 | + } | |
| 66 | + } | |
| 67 | + const rules = res?.rules ?? (await safe(intel.methodology()))?.find_a_model ?? null; | |
| 68 | + const rulesObj = rules && typeof rules === 'object' ? (rules as Record<string, string>) : null; | |
| 69 | + const apiMode = v.deployment === 'api'; | |
| 70 | + const localMode = v.deployment === 'local' || v.memory !== null; | |
| 71 | + | |
| 72 | + const inspector = ( | |
| 73 | + <div className="space-y-3 text-xs leading-relaxed text-ink-2"> | |
| 74 | + <p className="font-medium text-ink">Rules (deterministic)</p> | |
| 75 | + {rulesObj ? ( | |
| 76 | + <dl className="space-y-1.5"> | |
| 77 | + {Object.entries(rulesObj).map(([k, r]) => ( | |
| 78 | + <div key={k}> | |
| 79 | + <dt className="mono text-[11px] text-ink-3">{k}</dt> | |
| 80 | + <dd>{r}</dd> | |
| 81 | + </div> | |
| 82 | + ))} | |
| 83 | + </dl> | |
| 84 | + ) : ( | |
| 85 | + <p className="text-ink-3">Rules unavailable from the API.</p> | |
| 86 | + )} | |
| 87 | + <p> | |
| 88 | + <Link href="/methodology" className="link">/methodology</Link> · <Link href="/developers" className="link">GET /find-a-model</Link> | |
| 89 | + </p> | |
| 90 | + </div> | |
| 91 | + ); | |
| 92 | + | |
| 93 | + return ( | |
| 94 | + <TerminalLayout filters={<FinderForm v={v} />} inspector={inspector} filtersTitle="What do you need?" inspectorTitle="Rules" storageKey="aia-inspector-finder" filterCount={n}> | |
| 95 | + <PageHeader eyebrow="Model finder" title="Find a model" lede="Describe the need; the atlas applies deterministic rules to observed attributes, current prices and current benchmark results and returns the models that satisfy them — sorted by how many criteria they meet, then best benchmark rank, then release date. No composite score, no single winner." className="pt-4 md:pt-6" /> | |
| 96 | + <div className="pb-16"> | |
| 97 | + {n === 0 ? ( | |
| 98 | + <EmptyState title="Pick at least one criterion"> | |
| 99 | + Examples: <Link href="/find-a-model?use_case=coding&deployment=local&memory_gb=64" className="link">coding · local · 64 GB</Link>, <Link href="/find-a-model?use_case=vision&deployment=api&max_output_price=5" className="link">vision · API · output ≤ $5</Link>, <Link href="/find-a-model?use_case=long_context&context_min=1000000&license=commercial" className="link">1M context · commercial licence</Link>. | |
| 100 | + </EmptyState> | |
| 101 | + ) : !res ? ( | |
| 102 | + <Unavailable what="Model finder" reason={error ?? undefined} /> | |
| 103 | + ) : ( | |
| 104 | + <> | |
| 105 | + <div className="border-y border-rule py-3"> | |
| 106 | + <p className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm"> | |
| 107 | + <span className="font-semibold text-ink"> | |
| 108 | + {fmtInt(res.total)} model{res.total === 1 ? '' : 's'} satisfy {res.matches[0]?.why.length ? 'at least one of' : ''} the criteria | |
| 109 | + </span> | |
| 110 | + <span className="text-ink-3">No single winner — these are the observed dimensions; the order is criteria met → best rank → release date.</span> | |
| 111 | + </p> | |
| 112 | + <p className="mt-1.5 flex flex-wrap gap-1.5"> | |
| 113 | + {Object.entries(res.filters_applied).map(([k, val]) => ( | |
| 114 | + <Chip key={k}> | |
| 115 | + <span className="text-ink-3">{k.replace(/_/g, ' ')}</span> {String(val)} | |
| 116 | + </Chip> | |
| 117 | + ))} | |
| 118 | + </p> | |
| 119 | + </div> | |
| 120 | + {res.matches.length === 0 ? ( | |
| 121 | + <EmptyState title="No model satisfies these criteria" className="mt-4"> | |
| 122 | + Relax a bound (price, memory, context) or drop a modality. Every rule is listed in the Rules pane. | |
| 123 | + </EmptyState> | |
| 124 | + ) : ( | |
| 125 | + <DataTable scroll compact className="mt-4"> | |
| 126 | + <thead> | |
| 127 | + <tr> | |
| 128 | + <Th>Model</Th> | |
| 129 | + <Th>Why</Th> | |
| 130 | + <Th>Observed dimensions</Th> | |
| 131 | + {localMode && ( | |
| 132 | + <Th> | |
| 133 | + Fit <Estimated className="ml-1 align-middle" /> | |
| 134 | + </Th> | |
| 135 | + )} | |
| 136 | + {apiMode && <Th>Deployments</Th>} | |
| 137 | + <Th>Ranks</Th> | |
| 138 | + <Th className="w-40" aria-label="Actions" /> | |
| 139 | + </tr> | |
| 140 | + </thead> | |
| 141 | + <tbody> | |
| 142 | + {res.matches.length === 0 && <EmptyRow cols={7} />} | |
| 143 | + {res.matches.map((m) => { | |
| 144 | + const o = m.observed ?? {}; | |
| 145 | + const mods = Array.isArray(o.modalities) ? (o.modalities as string[]) : []; | |
| 146 | + return ( | |
| 147 | + <tr key={m.model.id} data-finder-row> | |
| 148 | + <Td primary> | |
| 149 | + <span className="flex flex-wrap items-center gap-x-2 gap-y-0.5"> | |
| 150 | + <EntityLink e={m.model} /> | |
| 151 | + {typeof o.openness === 'string' && <OpennessBadge openness={o.openness} />} | |
| 152 | + </span> | |
| 153 | + {m.model.organization && <span className="block text-xs text-ink-3">{m.model.organization.name}</span>} | |
| 154 | + </Td> | |
| 155 | + <Td label="Why" wide> | |
| 156 | + <ul className="flex flex-wrap gap-1"> | |
| 157 | + {m.why.map((w) => ( | |
| 158 | + <li key={w} className={`rounded-[3px] px-1.5 py-[1px] text-[11px] leading-4 ${/estimate/i.test(w) ? 'border border-dashed border-warning/60 text-warning' : 'bg-positive-soft text-positive'}`}> | |
| 159 | + {w} | |
| 160 | + </li> | |
| 161 | + ))} | |
| 162 | + {m.why.length === 0 && <li className="text-xs text-ink-3">—</li>} | |
| 163 | + </ul> | |
| 164 | + </Td> | |
| 165 | + <Td label="Observed" className="tnum text-xs text-ink-2" wide> | |
| 166 | + {[ | |
| 167 | + num(o.parameter_count) !== null ? `${fmtParams(o.parameter_count)} params` : null, | |
| 168 | + num(o.context_length) !== null ? `${fmtTokens(o.context_length)} context` : null, | |
| 169 | + typeof o.license === 'string' ? o.license : null, | |
| 170 | + mods.length ? mods.join(' · ') : null, | |
| 171 | + o.reasoning === true ? 'reasoning' : null, | |
| 172 | + o.tool_calling === true ? 'tool calling' : null, | |
| 173 | + typeof o.release_date === 'string' ? `released ${fmtDate(o.release_date)}` : null, | |
| 174 | + num(o.cheapest_input_per_mtok) !== null ? `from ${fmtUsdPerM(o.cheapest_input_per_mtok)} in / ${fmtUsdPerM(o.cheapest_output_per_mtok)} out` : null, | |
| 175 | + num(o.providers) !== null ? `${fmtInt(o.providers)} provider${num(o.providers) === 1 ? '' : 's'}` : null, | |
| 176 | + ] | |
| 177 | + .filter(Boolean) | |
| 178 | + .join(' · ') || '—'} | |
| 179 | + </Td> | |
| 180 | + {localMode && ( | |
| 181 | + <Td label="Fit (estimated)"> | |
| 182 | + <FitCell fit={m.estimated_fit} /> | |
| 183 | + </Td> | |
| 184 | + )} | |
| 185 | + {apiMode && ( | |
| 186 | + <Td label="Deployments" className="text-xs"> | |
| 187 | + {m.deployments && m.deployments.length ? ( | |
| 188 | + <ul className="space-y-0.5"> | |
| 189 | + {m.deployments.slice(0, 4).map((d) => ( | |
| 190 | + <li key={d.id} className="tnum flex flex-wrap gap-x-2"> | |
| 191 | + <EntityLink e={d.provider} className="text-ink-2" /> | |
| 192 | + <span className="text-accent-2"> | |
| 193 | + {fmtUsdPerM(d.prices.input)} / {fmtUsdPerM(d.prices.output)} | |
| 194 | + </span> | |
| 195 | + </li> | |
| 196 | + ))} | |
| 197 | + {m.deployments.length > 4 && <li className="text-ink-3">+{m.deployments.length - 4} more</li>} | |
| 198 | + </ul> | |
| 199 | + ) : ( | |
| 200 | + <span className="text-ink-3">—</span> | |
| 201 | + )} | |
| 202 | + </Td> | |
| 203 | + )} | |
| 204 | + <Td label="Ranks"> | |
| 205 | + <RankChips ranks={o.benchmark_ranks ?? null} max={3} /> | |
| 206 | + </Td> | |
| 207 | + <Td className="text-right"> | |
| 208 | + <span className="inline-flex flex-wrap justify-end gap-1"> | |
| 209 | + <CompareButton e={m.model} size="sm" /> | |
| 210 | + <WatchButton e={m.model} size="sm" /> | |
| 211 | + </span> | |
| 212 | + </Td> | |
| 213 | + </tr> | |
| 214 | + ); | |
| 215 | + })} | |
| 216 | + </tbody> | |
| 217 | + </DataTable> | |
| 218 | + )} | |
| 219 | + <Note className="mt-3">Green chips = criteria satisfied by an observed fact; dashed amber = satisfied by an estimate (hardware fit). Ranks are positions inside each benchmark's primary comparability group.</Note> | |
| 220 | + <Methodology text={res.note} /> | |
| 221 | + </> | |
| 222 | + )} | |
| 223 | + </div> | |
| 224 | + <CompareTrayBar /> | |
| 225 | + </TerminalLayout> | |
| 226 | + ); | |
| 227 | +} | |
modified
apps/web/src/app/frameworks/page.tsx
+63 −28
@@ -1,22 +1,46 @@ | ||
| 1 | 1 | import type { Metadata } from 'next'; |
| 2 | 2 | import Link from 'next/link'; |
| 3 | −import { Dash, str, TypedListing } from '@/components/listing/typed-listing'; | |
| 4 | −import { EntityBadge } from '@/components/ui/badges'; | |
| 3 | +import { Sparkline } from '@/components/charts'; | |
| 4 | +import { Dash, IntelListing, str } from '@/components/intelligence/listing-table'; | |
| 5 | +import { Chip } from '@/components/ui/badges'; | |
| 5 | 6 | import { EntityLink, QualityMark } from '@/components/ui/entity'; |
| 6 | −import { api } from '@/lib/api'; | |
| 7 | +import { api, safe } from '@/lib/api'; | |
| 7 | 8 | import { fmtDate, fmtInt, num } from '@/lib/format'; |
| 8 | −import { routes } from '@/lib/site'; | |
| 9 | +import { routes, SITE_NAME, SITE_URL } from '@/lib/site'; | |
| 9 | 10 | |
| 10 | −export const metadata: Metadata = { title: 'AI frameworks, libraries and runtimes — versions, licenses, stars', description: 'Training, inference and agent frameworks with latest version, release date, license and repository stars read from PyPI, GitHub and release pages.', alternates: { canonical: '/frameworks' } }; | |
| 11 | +export const metadata: Metadata = { title: 'AI frameworks, inference engines, libraries and runtimes — versions, licences, stars', description: 'Training frameworks, inference engines, serving stacks, libraries, runtimes, agent frameworks, orchestration and eval harnesses with latest version, release date, licence and repository stars read from PyPI, GitHub and release pages.', alternates: { canonical: routes.frameworks() }, openGraph: { title: `AI frameworks | ${SITE_NAME}`, url: `${SITE_URL}${routes.frameworks()}`, siteName: SITE_NAME } }; | |
| 11 | 12 | export const revalidate = 300; |
| 12 | 13 | |
| 14 | +type Extra = { stars: number[] }; | |
| 15 | +/** Canonical kind vocabulary → label; entity_type is the fallback when no `kind` attribute is stated. */ | |
| 16 | +const KIND_LABELS: Record<string, string> = { | |
| 17 | + training_framework: 'Training framework', | |
| 18 | + training: 'Training framework', | |
| 19 | + inference_engine: 'Inference engine', | |
| 20 | + inference: 'Inference engine', | |
| 21 | + serving: 'Serving', | |
| 22 | + library: 'Library', | |
| 23 | + runtime: 'Runtime', | |
| 24 | + agent_framework: 'Agent framework', | |
| 25 | + agent: 'Agent framework', | |
| 26 | + orchestration: 'Orchestration', | |
| 27 | + eval_harness: 'Eval harness', | |
| 28 | + evaluation: 'Eval harness', | |
| 29 | + framework: 'Framework', | |
| 30 | +}; | |
| 31 | +const kindOf = (e: { entity_type: string; attributes: Record<string, unknown> }) => { | |
| 32 | + const k = str(e.attributes?.kind) ?? str(e.attributes?.category); | |
| 33 | + if (k) return KIND_LABELS[k.toLowerCase().replace(/[\s-]+/g, '_')] ?? k; | |
| 34 | + return KIND_LABELS[e.entity_type] ?? e.entity_type; | |
| 35 | +}; | |
| 36 | + | |
| 13 | 37 | export default async function FrameworksPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) { |
| 14 | 38 | const sp = await searchParams; |
| 15 | 39 | return ( |
| 16 | − <TypedListing | |
| 40 | + <IntelListing<Extra> | |
| 17 | 41 | title="Frameworks & runtimes" |
| 18 | 42 | eyebrow="Frameworks" |
| 19 | − lede="Libraries, inference engines and agent frameworks. Versions and stars are read from PyPI, GitHub and release pages — never estimated." | |
| 43 | + lede="Training frameworks, inference engines, serving stacks, libraries, runtimes, agent frameworks, orchestration and eval harnesses. Versions and stars are read from PyPI, GitHub and release pages — never estimated." | |
| 20 | 44 | basePath="/frameworks" |
| 21 | 45 | searchParams={sp} |
| 22 | 46 | fetch={(q) => api.explore('framework', q)} |
@@ -28,7 +52,17 @@ export default async function FrameworksPage({ searchParams }: { searchParams: P | ||
| 28 | 52 | { value: 'quality', label: 'Data quality' }, |
| 29 | 53 | ]} |
| 30 | 54 | filters={sp.org ? [{ kind: 'hidden', name: 'org', value: sp.org }] : []} |
| 55 | + connectors={['github (repositories)', 'pypi', 'release feeds']} | |
| 31 | 56 | compare |
| 57 | + enrich={async (items) => { | |
| 58 | + const hist = await Promise.all(items.map((e) => safe(api.entityHistory(e.slug, 'metric.stars')))); | |
| 59 | + const m = new Map<string, Extra>(); | |
| 60 | + hist.forEach((h, i) => { | |
| 61 | + const vals = (h?.items ?? []).slice().sort((a, b) => a.observed_at.localeCompare(b.observed_at)).map((c) => num(c.value)).filter((v): v is number => v !== null); | |
| 62 | + m.set(items[i]!.slug, { stars: vals }); | |
| 63 | + }); | |
| 64 | + return m; | |
| 65 | + }} | |
| 32 | 66 | columns={[ |
| 33 | 67 | { |
| 34 | 68 | key: 'name', |
@@ -36,35 +70,36 @@ export default async function FrameworksPage({ searchParams }: { searchParams: P | ||
| 36 | 70 | primary: true, |
| 37 | 71 | render: (e) => ( |
| 38 | 72 | <> |
| 39 | − <span className="flex flex-wrap items-center gap-x-2 gap-y-0.5"> | |
| 40 | − <EntityLink e={e} /> | |
| 41 | − {e.entity_type !== 'framework' && <EntityBadge type={e.entity_type} small />} | |
| 42 | − </span> | |
| 73 | + <EntityLink e={e} /> | |
| 43 | 74 | {e.description && <span className="block max-w-md truncate text-xs text-ink-3">{e.description}</span>} |
| 44 | 75 | </> |
| 45 | 76 | ), |
| 46 | 77 | }, |
| 47 | − { key: 'version', label: 'Latest version', className: 'mono text-xs text-ink', render: (e) => str(e.attributes?.latest_version) ?? <Dash /> }, | |
| 48 | − { key: 'release', label: 'Release date', className: 'tnum text-ink-2 whitespace-nowrap', render: (e) => (str(e.attributes?.latest_release_at) ? fmtDate(str(e.attributes.latest_release_at)) : <Dash />) }, | |
| 49 | − { key: 'license', label: 'License', className: 'max-w-[10rem] truncate text-ink-2', render: (e) => str(e.attributes?.license) ?? <Dash /> }, | |
| 50 | − { key: 'stars', label: 'Stars', num: true, className: 'tnum', render: (e) => (num(e.attributes?.['metric.stars']) === null ? <Dash /> : fmtInt(e.attributes['metric.stars'])) }, | |
| 51 | − { key: 'language', label: 'Language', className: 'text-ink-2', render: (e) => str(e.attributes?.language) ?? <Dash /> }, | |
| 78 | + { key: 'kind', label: 'Kind', render: (e) => <Chip>{kindOf(e)}</Chip> }, | |
| 79 | + { key: 'version', label: 'Latest release', className: 'mono text-xs text-ink', render: (e) => (str(e.attributes?.latest_version) ? <>{(str(e.attributes.latest_version) as string).slice(0, 24)}{str(e.attributes?.latest_release_at) && <span className="tnum block font-sans text-[11px] text-ink-3">{fmtDate(str(e.attributes.latest_release_at))}</span>}</> : <Dash />) }, | |
| 80 | + { key: 'license', label: 'Licence', className: 'max-w-[10rem] truncate text-ink-2', render: (e) => str(e.attributes?.license) ?? <Dash /> }, | |
| 52 | 81 | { |
| 53 | − key: 'org', | |
| 54 | − label: 'Organization', | |
| 55 | − className: 'text-ink-2', | |
| 56 | − render: (e) => | |
| 57 | − e.organization ? ( | |
| 58 | − <Link href={routes.entity({ entity_type: 'company', slug: e.organization.slug })} className="hover:text-accent"> | |
| 59 | − {e.organization.name} | |
| 60 | − </Link> | |
| 61 | − ) : ( | |
| 62 | − <Dash /> | |
| 63 | − ), | |
| 82 | + key: 'stars', | |
| 83 | + label: 'Stars', | |
| 84 | + num: true, | |
| 85 | + className: 'tnum', | |
| 86 | + render: (e, x) => { | |
| 87 | + const v = num(e.attributes?.['metric.stars']); | |
| 88 | + if (v === null) return <Dash />; | |
| 89 | + const distinct = new Set(x?.stars ?? []).size; | |
| 90 | + return ( | |
| 91 | + <span className="inline-flex items-center justify-end gap-2"> | |
| 92 | + {x && x.stars.length >= 2 && distinct >= 2 && <Sparkline values={x.stars} width={64} height={18} stroke="var(--type-framework)" title={`Stars history · ${x.stars.length} observations`} />} | |
| 93 | + <span>{fmtInt(v)}</span> | |
| 94 | + </span> | |
| 95 | + ); | |
| 96 | + }, | |
| 64 | 97 | }, |
| 98 | + { key: 'language', label: 'Language', className: 'text-ink-2', render: (e) => str(e.attributes?.language) ?? <Dash /> }, | |
| 99 | + { key: 'org', label: 'Organization', className: 'text-ink-2', render: (e) => (e.organization ? <Link href={routes.entity({ entity_type: 'company', slug: e.organization.slug })} className="hover:text-accent">{e.organization.name}</Link> : <Dash />) }, | |
| 65 | 100 | { key: 'quality', label: 'Quality', num: true, render: (e) => <QualityMark q={e.quality?.score} /> }, |
| 66 | 101 | ]} |
| 67 | − note="Stars are a GitHub metric observed at crawl time (see each framework's Sources tab for the snapshot). Versions come from PyPI or the project's release feed." | |
| 102 | + note="Stars are a GitHub metric observed at crawl time; the sparkline appears once the claim history holds two distinct values (see each framework's History tab). Kind falls back to the entity type (framework · library · runtime) when no source states a finer category." | |
| 68 | 103 | /> |
| 69 | 104 | ); |
| 70 | 105 | } |
added
apps/web/src/app/frontier/loading.tsx
+23 −0
@@ -0,0 +1,23 @@ | ||
| 1 | +import { Container } from '@/components/ui/section'; | |
| 2 | + | |
| 3 | +export default function Loading() { | |
| 4 | + return ( | |
| 5 | + <Container wide className="pb-16 pt-7 md:pt-10" aria-busy="true"> | |
| 6 | + <div className="h-3 w-64 animate-pulse bg-surface-2" /> | |
| 7 | + <div className="mt-3 h-9 w-48 animate-pulse bg-surface-2" /> | |
| 8 | + <div className="mt-6 flex gap-2 border-b border-rule pb-2"> | |
| 9 | + {Array.from({ length: 7 }).map((_, i) => ( | |
| 10 | + <div key={i} className="h-6 w-24 animate-pulse bg-surface-2" /> | |
| 11 | + ))} | |
| 12 | + </div> | |
| 13 | + <ul className="mt-8 border-t border-rule"> | |
| 14 | + {Array.from({ length: 10 }).map((_, i) => ( | |
| 15 | + <li key={i} className="border-b border-rule py-3"> | |
| 16 | + <div className="h-4 w-3/4 animate-pulse bg-surface-2" /> | |
| 17 | + </li> | |
| 18 | + ))} | |
| 19 | + </ul> | |
| 20 | + <p className="sr-only">Loading the frontier…</p> | |
| 21 | + </Container> | |
| 22 | + ); | |
| 23 | +} | |
added
apps/web/src/app/frontier/opengraph-image.tsx
+23 −0
@@ -0,0 +1,23 @@ | ||
| 1 | +import { ImageResponse } from 'next/og'; | |
| 2 | +import { Wallpaper } from '@/components/brand/og'; | |
| 3 | +import { intel, safe } from '@/lib/api'; | |
| 4 | +import { fmtInt, fmtScore } from '@/lib/format'; | |
| 5 | +import { SITE_NAME } from '@/lib/site'; | |
| 6 | + | |
| 7 | +export const runtime = 'nodejs'; | |
| 8 | +export const alt = `The AI frontier right now on ${SITE_NAME}`; | |
| 9 | +export const size = { width: 1200, height: 630 }; | |
| 10 | +export const contentType = 'image/png'; | |
| 11 | + | |
| 12 | +export default async function FrontierOgImage() { | |
| 13 | + const f = await safe(intel.frontier(3)); | |
| 14 | + const counters: [string, string][] = []; | |
| 15 | + if (f) { | |
| 16 | + counters.push(['Benchmarks tracked', fmtInt(f.benchmark_frontier?.length ?? 0)]); | |
| 17 | + counters.push(['Frontier models', fmtInt(f.price_frontier?.frontier_models)]); | |
| 18 | + const lead = f.benchmark_frontier?.[0]?.leader; | |
| 19 | + if (lead) counters.push([`Leads ${f.benchmark_frontier?.[0]?.benchmark.name ?? ''}`.slice(0, 28), `${lead.model.name} · ${fmtScore(lead.score)}`.slice(0, 30)]); | |
| 20 | + counters.push(['Pareto set', fmtInt(f.efficiency_frontier?.frontier?.length ?? 0)]); | |
| 21 | + } | |
| 22 | + return new ImageResponse(<Wallpaper eyebrow="What defines the AI frontier right now" title="Frontier" subtitle="Leaders per benchmark, cheapest frontier offers, largest contexts, open weights, quality vs price — observed dimensions, no composite score." counters={counters} footer="www.ai-atlas.co/frontier" markPx={240} />, { ...size }); | |
| 23 | +} | |
added
apps/web/src/app/frontier/page.tsx
+435 −0
@@ -0,0 +1,435 @@ | ||
| 1 | +import type { Metadata } from 'next'; | |
| 2 | +import Link from 'next/link'; | |
| 3 | +import type { ScatterPoint } from '@/components/charts'; | |
| 4 | +import { EfficiencyScatter } from '@/components/intelligence/client-charts'; | |
| 5 | +import { ChangeRow } from '@/components/changes/change-row'; | |
| 6 | +import { DistBars, Methodology, RankChips, TrustChip } from '@/components/intelligence/bits'; | |
| 7 | +import { SectionNav, Ticker, type TickerItem } from '@/components/layout/terminal'; | |
| 8 | +import { Chip, OpennessBadge } from '@/components/ui/badges'; | |
| 9 | +import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table'; | |
| 10 | +import { EntityLink } from '@/components/ui/entity'; | |
| 11 | +import { Container, Note, PageHeader, Section } from '@/components/ui/section'; | |
| 12 | +import { EmptyState, Unavailable } from '@/components/ui/unavailable'; | |
| 13 | +import { intel } from '@/lib/api'; | |
| 14 | +import { safe } from '@/lib/api'; | |
| 15 | +import { fmtDate, fmtDateTime, fmtInt, fmtParams, fmtScore, fmtTokens, fmtUsdPerM, num } from '@/lib/format'; | |
| 16 | +import { eventTone, routes, SITE_NAME, SITE_URL } from '@/lib/site'; | |
| 17 | +import { eventDate, type Deployment, type EntitySummary, type ModelRef } from '@/lib/types'; | |
| 18 | + | |
| 19 | +export const revalidate = 300; | |
| 20 | + | |
| 21 | +const TITLE = 'The AI frontier right now — leaders per benchmark, price, context and open weights'; | |
| 22 | +const DESC = 'What defines the AI frontier today: the latest major models, the leader and runner-up of every benchmark comparability group, the cheapest frontier offers, the largest context windows, the open-weight frontier, the quality-vs-price Pareto set, agentic and multimodal leaders and recent movements — observed dimensions only, no composite score.'; | |
| 23 | +export const metadata: Metadata = { | |
| 24 | + title: TITLE, | |
| 25 | + description: DESC, | |
| 26 | + alternates: { canonical: routes.frontier() }, | |
| 27 | + openGraph: { title: `${TITLE} | ${SITE_NAME}`, description: DESC, url: `${SITE_URL}${routes.frontier()}`, type: 'website', siteName: SITE_NAME }, | |
| 28 | + twitter: { card: 'summary_large_image', title: TITLE, description: DESC }, | |
| 29 | +}; | |
| 30 | + | |
| 31 | +const NAV = [ | |
| 32 | + { id: 'latest', label: 'Latest major models' }, | |
| 33 | + { id: 'benchmarks', label: 'Benchmark frontier' }, | |
| 34 | + { id: 'price', label: 'Price frontier' }, | |
| 35 | + { id: 'context', label: 'Context frontier' }, | |
| 36 | + { id: 'open', label: 'Open-weight frontier' }, | |
| 37 | + { id: 'efficiency', label: 'Efficiency frontier' }, | |
| 38 | + { id: 'agentic', label: 'Agentic' }, | |
| 39 | + { id: 'multimodal', label: 'Multimodal' }, | |
| 40 | + { id: 'movements', label: 'Recent movements' }, | |
| 41 | +]; | |
| 42 | + | |
| 43 | +const modelHref = (m: { slug: string; entity_type?: string }) => routes.entity({ entity_type: m.entity_type ?? 'model', slug: m.slug }); | |
| 44 | +const orgName = (m: ModelRef | EntitySummary) => m.organization?.name ?? null; | |
| 45 | + | |
| 46 | +function DeploymentStrip({ d, label }: { d: Deployment | null | undefined; label: string }) { | |
| 47 | + return ( | |
| 48 | + <div className="min-w-0 border-b border-rule py-3 md:border-b-0 md:border-r md:pr-6 last:md:border-r-0"> | |
| 49 | + <p className="eyebrow">{label}</p> | |
| 50 | + {d && d.model ? ( | |
| 51 | + <> | |
| 52 | + <p className="mt-1 flex flex-wrap items-baseline gap-x-2"> | |
| 53 | + <EntityLink e={d.model} className="text-[15px] font-medium" /> | |
| 54 | + {d.model.organization && <span className="text-xs text-ink-3">{d.model.organization.name}</span>} | |
| 55 | + </p> | |
| 56 | + <p className="tnum mt-1 text-[22px] font-semibold leading-none text-accent-2"> | |
| 57 | + {fmtUsdPerM(d.prices.output)} <span className="text-xs font-normal text-ink-3">output / 1M</span> | |
| 58 | + </p> | |
| 59 | + <p className="tnum mt-1 text-xs text-ink-3"> | |
| 60 | + input {fmtUsdPerM(d.prices.input)} · context {fmtTokens(d.context_length)} · via <EntityLink e={d.provider} className="text-ink-2" /> · observed {fmtDate(d.observed_at)} | |
| 61 | + </p> | |
| 62 | + </> | |
| 63 | + ) : ( | |
| 64 | + <p className="mt-1 text-sm text-ink-3">No frontier model has a current priced offer in the API response.</p> | |
| 65 | + )} | |
| 66 | + </div> | |
| 67 | + ); | |
| 68 | +} | |
| 69 | + | |
| 70 | +export default async function FrontierPage() { | |
| 71 | + const [f, idx] = await Promise.all([safe(intel.frontier(12)), safe(intel.priceIndex(30))]); | |
| 72 | + if (!f) { | |
| 73 | + return ( | |
| 74 | + <Container wide> | |
| 75 | + <PageHeader eyebrow="What defines the AI frontier right now" title="Frontier" lede="Observed leaders per dimension — benchmarks, price, context, open weights, efficiency — from the atlas. No composite score." /> | |
| 76 | + <Unavailable what="Frontier" reason="GET /frontier did not answer. Nothing here is cached or hardcoded." /> | |
| 77 | + </Container> | |
| 78 | + ); | |
| 79 | + } | |
| 80 | + const latest = f.latest_major_models ?? []; | |
| 81 | + const bench = f.benchmark_frontier ?? []; | |
| 82 | + const pf = f.price_frontier; | |
| 83 | + const ctx = f.context_frontier ?? []; | |
| 84 | + const open = f.open_weight_frontier; | |
| 85 | + const eff = f.efficiency_frontier; | |
| 86 | + const agentic = f.agentic_frontier ?? []; | |
| 87 | + const multi = f.multimodal_frontier ?? []; | |
| 88 | + const moves = f.recent_frontier_movements ?? []; | |
| 89 | + const compo = pf?.composition ?? f.price_frontier?.composition ?? null; | |
| 90 | + | |
| 91 | + // efficiency scatter | |
| 92 | + const points: ScatterPoint[] = (eff?.points ?? []) | |
| 93 | + .filter((p) => num(p.x) !== null && num(p.y) !== null) | |
| 94 | + .map((p) => ({ id: p.id, x: num(p.x) as number, y: num(p.y) as number, label: p.model.name, sub: `${p.model.organization ?? ''}${p.provider ? ` · via ${p.provider.name}` : ''} · rank ${p.rank}`, href: modelHref(p.model), color: p.pareto ? 'var(--accent-2)' : undefined })); | |
| 95 | + const frontierSet = new Set(eff?.frontier ?? []); | |
| 96 | + const frontierLine = points.filter((p) => frontierSet.has(p.id)).sort((a, b) => a.x - b.x); | |
| 97 | + const ticker: TickerItem[] = moves.slice(0, 24).map((e) => ({ id: e.id, tone: eventTone(e.event_type), href: e.entity ? routes.entity(e.entity) : routes.changes(), label: e.summary.length > 100 ? `${e.summary.slice(0, 99)}…` : e.summary, meta: num(e.percent_change) !== null ? `${(num(e.percent_change) as number) > 0 ? '+' : ''}${(num(e.percent_change) as number).toFixed(0)}%` : fmtDate(eventDate(e)) })); | |
| 98 | + const ld = { | |
| 99 | + '@context': 'https://schema.org', | |
| 100 | + '@type': 'Dataset', | |
| 101 | + name: 'AI Atlas Frontier', | |
| 102 | + description: DESC, | |
| 103 | + url: `${SITE_URL}${routes.frontier()}`, | |
| 104 | + dateModified: f.generated_at, | |
| 105 | + creator: { '@type': 'Organization', name: SITE_NAME, url: SITE_URL }, | |
| 106 | + isAccessibleForFree: true, | |
| 107 | + }; | |
| 108 | + | |
| 109 | + return ( | |
| 110 | + <Container wide> | |
| 111 | + <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }} /> | |
| 112 | + <PageHeader eyebrow="What defines the AI frontier right now" title="Frontier" lede="Who leads, on what, since when — one observed dimension at a time. Every leader comes from a benchmark comparability group, a published price or a sourced attribute; the atlas never sums them into a single score." aside={f.generated_at ? <p className="tnum text-sm text-ink-3" title={f.generated_at}>computed {fmtDateTime(f.generated_at)}</p> : undefined} /> | |
| 113 | + <SectionNav items={NAV} /> | |
| 114 | + {ticker.length > 0 && <Ticker items={ticker} className="mt-3" label="Movements" />} | |
| 115 | + | |
| 116 | + {/* ---------------------------------------------------------------------------------------------- latest */} | |
| 117 | + <Section id="latest" eyebrow="Latest major models" title="Most recent importance-3 model events" lede="New canonical model releases, newest first. Facts are the model's sourced attributes at crawl time." action={{ href: `${routes.changes()}?type=NEW_MODEL&importance_min=3`, label: 'All new models' }} hairline={false}> | |
| 118 | + {latest.length === 0 ? ( | |
| 119 | + <EmptyState title="No major model event returned">The API returned an empty list for latest_major_models.</EmptyState> | |
| 120 | + ) : ( | |
| 121 | + <DataTable caption="Latest major models"> | |
| 122 | + <thead> | |
| 123 | + <tr> | |
| 124 | + <Th>Model</Th> | |
| 125 | + <Th>Organization</Th> | |
| 126 | + <Th>Released</Th> | |
| 127 | + <Th>Key facts</Th> | |
| 128 | + <Th>Evidence</Th> | |
| 129 | + </tr> | |
| 130 | + </thead> | |
| 131 | + <tbody> | |
| 132 | + {latest.map((e) => { | |
| 133 | + const m = e.entity; | |
| 134 | + const a = m?.attributes ?? {}; | |
| 135 | + return ( | |
| 136 | + <tr key={e.id}> | |
| 137 | + <Td primary> | |
| 138 | + {m ? <EntityLink e={m} /> : <span>{e.summary}</span>} | |
| 139 | + {typeof a.openness === 'string' && <OpennessBadge openness={a.openness} className="ml-2" />} | |
| 140 | + </Td> | |
| 141 | + <Td label="Organization" className="text-ink-2">{m?.organization ? <Link href={routes.entity({ entity_type: 'company', slug: m.organization.slug })} className="hover:text-accent">{m.organization.name}</Link> : '—'}</Td> | |
| 142 | + <Td label="Released" className="tnum text-ink-2 whitespace-nowrap">{typeof a.release_date === 'string' ? fmtDate(a.release_date) : fmtDate(eventDate(e))}</Td> | |
| 143 | + <Td label="Key facts" className="tnum text-xs text-ink-2"> | |
| 144 | + {[num(a.parameter_count) !== null ? `${fmtParams(a.parameter_count)} params` : null, num(a.context_length) !== null ? `${fmtTokens(a.context_length)} context` : null, Array.isArray(a.modalities) && a.modalities.length ? (a.modalities as string[]).join(' · ') : null, typeof a.license === 'string' ? a.license : null].filter(Boolean).join(' · ') || '—'} | |
| 145 | + </Td> | |
| 146 | + <Td label="Evidence" className="text-xs"> | |
| 147 | + {e.source_url ? ( | |
| 148 | + <a href={e.source_url} target="_blank" rel="noopener noreferrer" className="text-ink-2 hover:text-accent"> | |
| 149 | + {new URL(e.source_url).hostname.replace(/^www\./, '')} | |
| 150 | + </a> | |
| 151 | + ) : ( | |
| 152 | + <span className="text-ink-3">—</span> | |
| 153 | + )} | |
| 154 | + {e.connector_name && <span className="mono ml-1.5 text-[10px] text-ink-3">{e.connector_name}</span>} | |
| 155 | + {e.is_backfill && <span className="ml-1.5 text-[10px] text-ink-3">(back-filled date)</span>} | |
| 156 | + </Td> | |
| 157 | + </tr> | |
| 158 | + ); | |
| 159 | + })} | |
| 160 | + </tbody> | |
| 161 | + </DataTable> | |
| 162 | + )} | |
| 163 | + </Section> | |
| 164 | + | |
| 165 | + {/* ------------------------------------------------------------------------------------------ benchmarks */} | |
| 166 | + <Section id="benchmarks" eyebrow="Benchmark frontier" title="Leader and runner-up per benchmark" lede="One row per benchmark with ≥ 20 current results: the primary comparability group, its leader, the second model and the gap between them, with the trust level of the leading row." action={{ href: routes.benchmarks(), label: 'All leaderboards' }}> | |
| 167 | + {bench.length === 0 ? ( | |
| 168 | + <EmptyState title="No benchmark group qualifies yet">The frontier lists groups with at least 20 current results.</EmptyState> | |
| 169 | + ) : ( | |
| 170 | + <DataTable caption="Benchmark frontier"> | |
| 171 | + <thead> | |
| 172 | + <tr> | |
| 173 | + <Th>Benchmark</Th> | |
| 174 | + <Th>Leader</Th> | |
| 175 | + <Th num>Score</Th> | |
| 176 | + <Th>Second</Th> | |
| 177 | + <Th num>Gap</Th> | |
| 178 | + <Th>Trust</Th> | |
| 179 | + </tr> | |
| 180 | + </thead> | |
| 181 | + <tbody> | |
| 182 | + {bench.map((b) => ( | |
| 183 | + <tr key={b.benchmark.id}> | |
| 184 | + <Td primary> | |
| 185 | + <Link href={routes.benchmark(b.benchmark.slug)} className="text-ink hover:text-accent hover:underline"> | |
| 186 | + {b.benchmark.name} | |
| 187 | + </Link> | |
| 188 | + {b.group && <span className="mono block text-[11px] text-ink-3">{b.group.label} · n={fmtInt(b.group.model_count)} models</span>} | |
| 189 | + </Td> | |
| 190 | + <Td label="Leader"> | |
| 191 | + {b.leader ? ( | |
| 192 | + <> | |
| 193 | + <Link href={modelHref(b.leader.model)} className="font-medium text-ink hover:text-accent hover:underline"> | |
| 194 | + {b.leader.model.name} | |
| 195 | + </Link> | |
| 196 | + {orgName(b.leader.model) && <span className="ml-1.5 text-xs text-ink-3">{orgName(b.leader.model)}</span>} | |
| 197 | + </> | |
| 198 | + ) : ( | |
| 199 | + '—' | |
| 200 | + )} | |
| 201 | + </Td> | |
| 202 | + <Td num label="Score" className="tnum font-medium">{b.leader ? `${fmtScore(b.leader.score)}${b.leader.unit ? ` ${b.leader.unit}` : ''}` : '—'}</Td> | |
| 203 | + <Td label="Second" className="text-ink-2"> | |
| 204 | + {b.second ? ( | |
| 205 | + <> | |
| 206 | + <Link href={modelHref(b.second.model)} className="hover:text-accent"> | |
| 207 | + {b.second.model.name} | |
| 208 | + </Link>{' '} | |
| 209 | + <span className="tnum text-xs text-ink-3">{fmtScore(b.second.score)}</span> | |
| 210 | + </> | |
| 211 | + ) : ( | |
| 212 | + '—' | |
| 213 | + )} | |
| 214 | + </Td> | |
| 215 | + <Td num label="Gap" className="tnum text-ink-2">{num(b.gap) === null ? '—' : fmtScore(b.gap)}</Td> | |
| 216 | + <Td label="Trust"> | |
| 217 | + <TrustChip level={b.leader?.trust_level} label={b.leader?.trust_label} /> | |
| 218 | + </Td> | |
| 219 | + </tr> | |
| 220 | + ))} | |
| 221 | + </tbody> | |
| 222 | + </DataTable> | |
| 223 | + )} | |
| 224 | + <Methodology text="Leader = best current row of the primary comparability group (canonical metric × task-defining configuration), one row per canonical model. Gap = leader − second in the metric's unit." /> | |
| 225 | + </Section> | |
| 226 | + | |
| 227 | + {/* ----------------------------------------------------------------------------------------------- price */} | |
| 228 | + <Section id="price" eyebrow="Price frontier" title="Cheapest frontier output" lede="Among frontier models, the cheapest current published output price — and the cheapest with a context window of at least 1M tokens — with the provider that publishes it." action={{ href: `${routes.prices()}?sort=cheapest_frontier`, label: 'Price terminal' }}> | |
| 229 | + <div className="grid grid-cols-[minmax(0,1fr)] gap-x-6 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.2fr)]"> | |
| 230 | + <DeploymentStrip d={pf?.cheapest_output} label="Cheapest frontier output" /> | |
| 231 | + <DeploymentStrip d={pf?.cheapest_output_1m_context} label="Cheapest with ≥ 1M context" /> | |
| 232 | + <div className="py-3 md:pl-6"> | |
| 233 | + <p className="eyebrow">Current offers by output price</p> | |
| 234 | + <DistBars d={idx?.distribution} className="mt-1" /> | |
| 235 | + </div> | |
| 236 | + </div> | |
| 237 | + <p className="tnum mt-2 text-xs text-ink-3"> | |
| 238 | + Frontier universe: {fmtInt(pf?.frontier_models)} canonical models | |
| 239 | + {compo && ( | |
| 240 | + <> | |
| 241 | + {' '} | |
| 242 | + ({fmtInt(compo.recent_by_active_orgs)} recent releases by active organizations · {fmtInt(compo.top10_on_a_benchmark)} top-10 on a benchmark | |
| 243 | + {compo.since ? ` · since ${fmtDate(compo.since)}` : ''}) | |
| 244 | + </> | |
| 245 | + )} | |
| 246 | + . | |
| 247 | + </p> | |
| 248 | + <Methodology text={idx?.frontier?.methodology ?? f.methodology} /> | |
| 249 | + </Section> | |
| 250 | + | |
| 251 | + {/* --------------------------------------------------------------------------------------------- context */} | |
| 252 | + <Section id="context" eyebrow="Context frontier" title="Largest context windows" lede="Distinct canonical models with the largest sourced context_length. A routing endpoint counts as a model only if its provider publishes it as one." action={{ href: `${routes.models()}?sort=context`, label: 'Models by context' }}> | |
| 253 | + {ctx.length === 0 ? ( | |
| 254 | + <EmptyState title="No context data returned" /> | |
| 255 | + ) : ( | |
| 256 | + <ol className="grid gap-x-6 md:grid-cols-2 xl:grid-cols-3"> | |
| 257 | + {ctx.map((c, i) => ( | |
| 258 | + <li key={c.model.id} className="flex items-baseline gap-3 border-b border-rule py-2.5"> | |
| 259 | + <span className="tnum w-5 shrink-0 text-xs text-ink-3">{i + 1}</span> | |
| 260 | + <span className="min-w-0 flex-1"> | |
| 261 | + <EntityLink e={c.model} className="font-medium" /> | |
| 262 | + {c.model.organization && <span className="ml-1.5 text-xs text-ink-3">{c.model.organization.name}</span>} | |
| 263 | + </span> | |
| 264 | + <span className="tnum shrink-0 text-[15px] font-semibold">{fmtTokens(c.context_length)}</span> | |
| 265 | + </li> | |
| 266 | + ))} | |
| 267 | + </ol> | |
| 268 | + )} | |
| 269 | + </Section> | |
| 270 | + | |
| 271 | + {/* ------------------------------------------------------------------------------------------------ open */} | |
| 272 | + <Section id="open" eyebrow="Open-weight frontier" title="Best-ranked downloadable models" lede="Observed dimensions only — best rank on any benchmark, parameters, context — for models whose weights can be downloaded. Sorted by best rank, then parameters." action={{ href: routes.open(), label: 'Open model frontier' }}> | |
| 273 | + {!open || open.items.length === 0 ? ( | |
| 274 | + <EmptyState title="No open-weight model with a current benchmark rank" /> | |
| 275 | + ) : ( | |
| 276 | + <DataTable caption="Open-weight frontier"> | |
| 277 | + <thead> | |
| 278 | + <tr> | |
| 279 | + <Th>Model</Th> | |
| 280 | + <Th num>Best rank</Th> | |
| 281 | + <Th>On</Th> | |
| 282 | + <Th num>Parameters</Th> | |
| 283 | + <Th num>Context</Th> | |
| 284 | + <Th>Other ranks</Th> | |
| 285 | + </tr> | |
| 286 | + </thead> | |
| 287 | + <tbody> | |
| 288 | + {open.items.map((it) => ( | |
| 289 | + <tr key={it.model.id}> | |
| 290 | + <Td primary> | |
| 291 | + <EntityLink e={it.model} /> | |
| 292 | + {it.model.organization && <span className="ml-2 text-xs text-ink-3">{it.model.organization.name}</span>} | |
| 293 | + {typeof it.model.attributes?.license === 'string' && <span className="block text-[11px] text-ink-3">{it.model.attributes.license as string}</span>} | |
| 294 | + </Td> | |
| 295 | + <Td num label="Best rank" className="tnum font-semibold">{num(it.best_rank) === null ? '—' : `#${fmtInt(it.best_rank)}`}</Td> | |
| 296 | + <Td label="On">{it.best_rank_on ? <Link href={routes.benchmark(it.best_rank_on)} className="text-ink-2 hover:text-accent">{it.best_rank_on}</Link> : '—'}</Td> | |
| 297 | + <Td num label="Parameters" className="tnum">{fmtParams(it.parameter_count)}</Td> | |
| 298 | + <Td num label="Context" className="tnum">{fmtTokens(it.context_length)}</Td> | |
| 299 | + <Td label="Other ranks"><RankChips ranks={it.ranks} max={3} /></Td> | |
| 300 | + </tr> | |
| 301 | + ))} | |
| 302 | + </tbody> | |
| 303 | + </DataTable> | |
| 304 | + )} | |
| 305 | + <Methodology text={`Dimensions: ${(open?.dimensions ?? []).join(', ') || 'best_rank, parameter_count, context_length'}. ${open?.note ?? ''}`} /> | |
| 306 | + </Section> | |
| 307 | + | |
| 308 | + {/* ------------------------------------------------------------------------------------------- efficiency */} | |
| 309 | + <Section id="efficiency" eyebrow="Efficiency frontier" title={eff?.quality?.benchmark ? <>Quality vs output price · {eff.quality.benchmark}</> : 'Quality vs output price'} lede={eff ? `y = ${eff.quality.group?.label ?? 'score'} in the primary comparability group; x = ${eff.x}. Amber points form the Pareto set (higher score, lower price); the rest are dimmed.` : undefined} action={eff?.quality?.benchmark ? { href: routes.benchmark(eff.quality.benchmark), label: 'Leaderboard' } : undefined}> | |
| 310 | + {points.length < 2 ? ( | |
| 311 | + <EmptyState title="Not enough priced results for a Pareto view">The efficiency frontier needs models with both a current benchmark result and a current output price.</EmptyState> | |
| 312 | + ) : ( | |
| 313 | + <> | |
| 314 | + <EfficiencyScatter points={points} frontier={frontierLine.map((p) => ({ x: p.x, y: p.y }))} highlight={[...frontierSet]} yLabel={eff?.quality.group?.label ?? 'score'} /> | |
| 315 | + <ul className="mt-3 flex flex-wrap gap-1.5"> | |
| 316 | + {frontierLine.map((p) => ( | |
| 317 | + <li key={p.id}> | |
| 318 | + <Link href={p.href ?? '#'} className="tnum inline-flex items-center gap-1.5 border border-rule px-2 py-1 text-xs text-ink-2 hover:border-rule-strong hover:text-ink"> | |
| 319 | + <span className="inline-block size-1.5 rounded-full bg-accent-2" aria-hidden /> {p.label} <span className="text-ink-3">{fmtScore(p.y)} · {fmtUsdPerM(p.x)}</span> | |
| 320 | + </Link> | |
| 321 | + </li> | |
| 322 | + ))} | |
| 323 | + </ul> | |
| 324 | + <p className="tnum mt-2 text-xs text-ink-3"> | |
| 325 | + {fmtInt(points.length)} models plotted · {fmtInt(frontierLine.length)} on the Pareto frontier · group n={fmtInt(eff?.quality.group?.model_count)} | |
| 326 | + </p> | |
| 327 | + </> | |
| 328 | + )} | |
| 329 | + <Methodology text="Pareto frontier maximises the score and minimises the cheapest current output price; exact ties are kept. Price = cheapest current offer across providers. A point's position is two observed facts, not a rating." /> | |
| 330 | + </Section> | |
| 331 | + | |
| 332 | + {/* ---------------------------------------------------------------------------------------------- agentic */} | |
| 333 | + <Section id="agentic" eyebrow="Agentic frontier" title="Leaders on agentic benchmarks" lede="Top rows of the primary comparability group of each agentic benchmark (tool use, terminal tasks, multi-turn agents)."> | |
| 334 | + {agentic.length === 0 ? ( | |
| 335 | + <EmptyState title="No agentic benchmark group returned" /> | |
| 336 | + ) : ( | |
| 337 | + <div className="grid gap-6 md:grid-cols-2"> | |
| 338 | + {agentic.map((g) => ( | |
| 339 | + <div key={g.benchmark.id} className="min-w-0"> | |
| 340 | + <p className="flex flex-wrap items-baseline gap-x-2"> | |
| 341 | + <Link href={routes.benchmark(g.benchmark.slug)} className="text-[15px] font-semibold text-ink hover:text-accent"> | |
| 342 | + {g.benchmark.name} | |
| 343 | + </Link> | |
| 344 | + {g.group && <span className="mono text-[11px] text-ink-3">{g.group.label} · n={fmtInt(g.group.model_count)}</span>} | |
| 345 | + </p> | |
| 346 | + <ol className="mt-1.5 border-t border-rule"> | |
| 347 | + {g.leaders.map((l) => ( | |
| 348 | + <li key={l.result_id} className="grid grid-cols-[1.5rem_minmax(0,1fr)_auto] items-baseline gap-2 border-b border-rule py-2 text-sm"> | |
| 349 | + <span className="tnum text-xs text-ink-3">{l.rank}</span> | |
| 350 | + <span className="min-w-0 truncate"> | |
| 351 | + <Link href={modelHref(l.model)} className="text-ink hover:text-accent hover:underline"> | |
| 352 | + {l.model.name} | |
| 353 | + </Link> | |
| 354 | + {orgName(l.model) && <span className="ml-1.5 text-xs text-ink-3">{orgName(l.model)}</span>} | |
| 355 | + </span> | |
| 356 | + <span className="tnum inline-flex items-center gap-2 text-right"> | |
| 357 | + <span className="font-medium">{fmtScore(l.score)}</span> | |
| 358 | + <TrustChip level={l.trust_level} label={l.trust_label} /> | |
| 359 | + </span> | |
| 360 | + </li> | |
| 361 | + ))} | |
| 362 | + </ol> | |
| 363 | + </div> | |
| 364 | + ))} | |
| 365 | + </div> | |
| 366 | + )} | |
| 367 | + </Section> | |
| 368 | + | |
| 369 | + {/* ------------------------------------------------------------------------------------------- multimodal */} | |
| 370 | + <Section id="multimodal" eyebrow="Multimodal frontier" title="Models with the most modalities among top-10 ranks" lede="Sourced modalities of models that hold a top-10 rank on at least one benchmark. Modality lists are attributes as published, not evaluations."> | |
| 371 | + {multi.length === 0 ? ( | |
| 372 | + <EmptyState title="No multimodal leader returned" /> | |
| 373 | + ) : ( | |
| 374 | + <DataTable caption="Multimodal frontier"> | |
| 375 | + <thead> | |
| 376 | + <tr> | |
| 377 | + <Th>Model</Th> | |
| 378 | + <Th>Modalities</Th> | |
| 379 | + <Th>Top-10 on</Th> | |
| 380 | + </tr> | |
| 381 | + </thead> | |
| 382 | + <tbody> | |
| 383 | + {multi.map((m) => ( | |
| 384 | + <tr key={m.model.id}> | |
| 385 | + <Td primary> | |
| 386 | + <Link href={modelHref(m.model)} className="font-medium text-ink hover:text-accent hover:underline"> | |
| 387 | + {m.model.name} | |
| 388 | + </Link> | |
| 389 | + {'organization' in m.model && m.model.organization && <span className="ml-2 text-xs text-ink-3">{m.model.organization.name}</span>} | |
| 390 | + </Td> | |
| 391 | + <Td label="Modalities"> | |
| 392 | + <span className="flex flex-wrap gap-1"> | |
| 393 | + {m.modalities.map((x) => ( | |
| 394 | + <Chip key={x}>{x}</Chip> | |
| 395 | + ))} | |
| 396 | + </span> | |
| 397 | + </Td> | |
| 398 | + <Td label="Top-10 on" className="text-xs"> | |
| 399 | + <span className="flex flex-wrap gap-1"> | |
| 400 | + {m.top10_on.slice(0, 6).map((b) => ( | |
| 401 | + <Link key={b} href={routes.benchmark(b)} className="rounded-[3px] bg-surface-2 px-1.5 py-[1px] text-[11px] text-ink-2 hover:text-accent"> | |
| 402 | + {b} | |
| 403 | + </Link> | |
| 404 | + ))} | |
| 405 | + {m.top10_on.length > 6 && <span className="text-[11px] text-ink-3">+{m.top10_on.length - 6}</span>} | |
| 406 | + </span> | |
| 407 | + </Td> | |
| 408 | + </tr> | |
| 409 | + ))} | |
| 410 | + </tbody> | |
| 411 | + </DataTable> | |
| 412 | + )} | |
| 413 | + </Section> | |
| 414 | + | |
| 415 | + {/* -------------------------------------------------------------------------------------------- movements */} | |
| 416 | + <Section id="movements" eyebrow="Recent frontier movements" title="Leadership changes and price moves ≥ 20 %" lede="Non-backfill benchmark events and price changes of at least 20 % over the last 30 days, newest first." action={{ href: routes.changes(), label: 'All changes' }}> | |
| 417 | + {moves.length === 0 ? ( | |
| 418 | + <EmptyState title="No frontier movement in the last 30 days"> | |
| 419 | + The atlas has not observed a leadership change or a ≥ 20 % price move that occurred in the window (historical backfill is excluded). The <Link href={routes.changes()} className="link">change feed</Link> lists everything else. | |
| 420 | + </EmptyState> | |
| 421 | + ) : ( | |
| 422 | + <ul className="border-t border-rule"> | |
| 423 | + {moves.map((e) => ( | |
| 424 | + <ChangeRow key={e.id} e={e} showDate live={false} /> | |
| 425 | + ))} | |
| 426 | + </ul> | |
| 427 | + )} | |
| 428 | + <Methodology text={f.methodology} /> | |
| 429 | + <Note className="mt-2"> | |
| 430 | + Composition and every threshold above are the API's (<Link href="/developers" className="link">GET /frontier</Link>); this page adds no ranking of its own. | |
| 431 | + </Note> | |
| 432 | + </Section> | |
| 433 | + </Container> | |
| 434 | + ); | |
| 435 | +} | |
added
apps/web/src/app/hardware/[slug]/opengraph-image.tsx
+23 −0
@@ -0,0 +1,23 @@ | ||
| 1 | +import { ImageResponse } from 'next/og'; | |
| 2 | +import { Fallback, Wallpaper } from '@/components/brand/og'; | |
| 3 | +import { fmtMemory } from '@/components/hardware/hardware-bits'; | |
| 4 | +import { api, intel, safe } from '@/lib/api'; | |
| 5 | +import { fmtInt, num } from '@/lib/format'; | |
| 6 | +import { SITE_NAME } from '@/lib/site'; | |
| 7 | + | |
| 8 | +export const runtime = 'nodejs'; | |
| 9 | +export const alt = `Hardware on ${SITE_NAME}`; | |
| 10 | +export const size = { width: 1200, height: 630 }; | |
| 11 | +export const contentType = 'image/png'; | |
| 12 | + | |
| 13 | +export default async function HardwareOgImage({ params }: { params: Promise<{ slug: string }> }) { | |
| 14 | + const { slug } = await params; | |
| 15 | + const [d, fit] = await Promise.all([safe(api.entityOfType('hardware', slug)), safe(intel.hardwareSlugFit(slug, { limit: 1 }))]); | |
| 16 | + if (!d || d.entity_type !== 'hardware') return new ImageResponse(<Fallback label="Hardware" />, { ...size }); | |
| 17 | + const a = d.attributes ?? {}; | |
| 18 | + const counters: [string, string][] = [['Memory', fmtMemory(a.memory_gb)]]; | |
| 19 | + if (num(a.memory_bandwidth_gbs) !== null) counters.push(['Bandwidth', `${fmtInt(a.memory_bandwidth_gbs)} GB/s`]); | |
| 20 | + if (num(a.tdp_watts) !== null) counters.push(['TDP', `${fmtInt(a.tdp_watts)} W`]); | |
| 21 | + if (fit) counters.push(['Fit @4bit (est.)', `${fmtInt(fit.counts.fits)} / ${fmtInt(fit.counts.evaluated)}`]); | |
| 22 | + return new ImageResponse(<Wallpaper eyebrow={typeof a.manufacturer === 'string' ? `Hardware · ${a.manufacturer}` : 'Hardware'} title={d.name} subtitle="Published specifications with sources, and the models estimated to fit at 4-bit, 8-bit and fp16." counters={counters} footer={`www.ai-atlas.co/hardware/${slug}`} markPx={220} />, { ...size }); | |
| 23 | +} | |
added
apps/web/src/app/hardware/[slug]/page.tsx
+229 −0
@@ -0,0 +1,229 @@ | ||
| 1 | +import { ExternalLink } from 'lucide-react'; | |
| 2 | +import type { Metadata } from 'next'; | |
| 3 | +import Link from 'next/link'; | |
| 4 | +import { permanentRedirect } from 'next/navigation'; | |
| 5 | +import { CompareButton } from '@/components/compare/compare-button'; | |
| 6 | +import { CompareTrayBar } from '@/components/compare/compare-tray-bar'; | |
| 7 | +import { RelationsBlock, SourcesTable, TimelineList } from '@/components/entity/blocks'; | |
| 8 | +import { buildMetadata, loadEntity } from '@/components/entity/load'; | |
| 9 | +import { QUANTS } from '@/components/hardware/fit-form'; | |
| 10 | +import { fmtMemory, HW_KIND_LABELS, memoryOptions } from '@/components/hardware/hardware-bits'; | |
| 11 | +import { EstimateBanner, FitBreakdownList, Methodology, SourceTag } from '@/components/intelligence/bits'; | |
| 12 | +import { ViewBeacon } from '@/components/layout/view-beacon'; | |
| 13 | +import { Chip, EntityBadge, Estimated, OpennessBadge, StatusBadge } from '@/components/ui/badges'; | |
| 14 | +import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table'; | |
| 15 | +import { EntityLink, QualityMark } from '@/components/ui/entity'; | |
| 16 | +import { KeyValue } from '@/components/ui/key-value'; | |
| 17 | +import { Container, Note, Section } from '@/components/ui/section'; | |
| 18 | +import { EmptyState, Unavailable } from '@/components/ui/unavailable'; | |
| 19 | +import { WatchButton } from '@/components/watchlist/watch-button'; | |
| 20 | +import { api, intel, safe } from '@/lib/api'; | |
| 21 | +import { fmtGb, fmtInt, fmtParams, fmtTokens, num } from '@/lib/format'; | |
| 22 | +import { PROSE_KEYS, routes, SITE_NAME, SITE_URL } from '@/lib/site'; | |
| 23 | + | |
| 24 | +export const revalidate = 600; | |
| 25 | +type Params = { params: Promise<{ slug: string }>; searchParams: Promise<Record<string, string | undefined>> }; | |
| 26 | +const CONTEXTS = [8192, 32768, 131072]; | |
| 27 | + | |
| 28 | +export async function generateMetadata({ params }: Params): Promise<Metadata> { | |
| 29 | + const { slug } = await params; | |
| 30 | + const d = await safe(api.entityOfType('hardware', slug)); | |
| 31 | + if (!d || d.entity_type !== 'hardware') return { title: 'Hardware', robots: { index: false } }; | |
| 32 | + const m = buildMetadata(d); | |
| 33 | + const a = d.attributes ?? {}; | |
| 34 | + return { ...m, title: `${d.name} — specs and which AI models fit (estimated)`, description: `${d.name}${typeof a.manufacturer === 'string' ? ` by ${a.manufacturer}` : ''}: ${fmtMemory(a.memory_gb)} memory${num(a.memory_bandwidth_gbs) !== null ? `, ${fmtInt(a.memory_bandwidth_gbs)} GB/s` : ''} as published, with sources — and the models estimated to fit at 4-bit, 8-bit and fp16. ${SITE_NAME}.` }; | |
| 35 | +} | |
| 36 | + | |
| 37 | +export default async function HardwarePage({ params, searchParams }: Params) { | |
| 38 | + const { slug } = await params; | |
| 39 | + const sp = await searchParams; | |
| 40 | + const d = await loadEntity('hardware', slug); | |
| 41 | + const canonical = routes.entity(d); | |
| 42 | + if (canonical !== `/hardware/${encodeURIComponent(slug)}`) permanentRedirect(canonical); | |
| 43 | + const a = d.attributes ?? {}; | |
| 44 | + const quant = QUANTS.some((q) => q.value === sp.quant) ? (sp.quant as string) : '4bit'; | |
| 45 | + const ctx = num(sp.context); | |
| 46 | + const context = ctx !== null && ctx > 0 ? Math.round(ctx) : 8192; | |
| 47 | + const memChoice = num(sp.memory_gb); | |
| 48 | + const options = memoryOptions(a.memory_gb); | |
| 49 | + const fit = await safe(intel.hardwareSlugFit(d.slug, { quant, context, memory_gb: memChoice ?? undefined, limit: 120 })); | |
| 50 | + const memoryUsed = num(fit?.inputs?.memory_gb) ?? memChoice ?? options.at(-1) ?? null; | |
| 51 | + const self = (patch: { quant?: string; context?: number; memory_gb?: number | null }) => { | |
| 52 | + const p = new URLSearchParams(); | |
| 53 | + const q = patch.quant ?? quant; | |
| 54 | + const c = patch.context ?? context; | |
| 55 | + const m = patch.memory_gb === undefined ? memChoice : patch.memory_gb; | |
| 56 | + if (q !== '4bit') p.set('quant', q); | |
| 57 | + if (c !== 8192) p.set('context', String(c)); | |
| 58 | + if (m !== null && m !== undefined) p.set('memory_gb', String(m)); | |
| 59 | + const s = p.toString(); | |
| 60 | + return `${canonical}${s ? `?${s}` : ''}#fit`; | |
| 61 | + }; | |
| 62 | + const specRows = Object.keys(a) | |
| 63 | + .filter((k) => !PROSE_KEYS.has(k)) | |
| 64 | + .map((k) => ({ key: k, raw: a[k] })); | |
| 65 | + const items = fit?.items ?? []; | |
| 66 | + const ld = { | |
| 67 | + '@context': 'https://schema.org', | |
| 68 | + '@type': 'Product', | |
| 69 | + name: d.name, | |
| 70 | + brand: typeof a.manufacturer === 'string' ? { '@type': 'Brand', name: a.manufacturer } : undefined, | |
| 71 | + category: typeof a.kind === 'string' ? HW_KIND_LABELS[a.kind] ?? a.kind : 'Hardware', | |
| 72 | + description: d.description ?? undefined, | |
| 73 | + url: `${SITE_URL}${canonical}`, | |
| 74 | + additionalProperty: [ | |
| 75 | + options.length ? { '@type': 'PropertyValue', name: 'Memory', value: fmtMemory(a.memory_gb) } : null, | |
| 76 | + num(a.memory_bandwidth_gbs) !== null ? { '@type': 'PropertyValue', name: 'Memory bandwidth', value: `${fmtInt(a.memory_bandwidth_gbs)} GB/s` } : null, | |
| 77 | + ].filter(Boolean), | |
| 78 | + }; | |
| 79 | + const chip = (on: boolean) => `inline-flex h-9 items-center border px-2.5 text-xs font-medium ${on ? 'border-ink bg-ink text-canvas' : 'border-rule text-ink-2 hover:border-rule-strong hover:text-ink'}`; | |
| 80 | + | |
| 81 | + return ( | |
| 82 | + <Container wide> | |
| 83 | + <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }} /> | |
| 84 | + <ViewBeacon path={canonical} /> | |
| 85 | + <header className="pb-5 pt-7 md:pt-10"> | |
| 86 | + <div className="flex flex-wrap items-center gap-2 text-sm text-ink-3"> | |
| 87 | + <Link href={routes.hardware()} className="hover:text-ink">Hardware</Link> <span aria-hidden>/</span> | |
| 88 | + <EntityBadge type={d.entity_type} /> | |
| 89 | + {typeof a.kind === 'string' && <Chip>{HW_KIND_LABELS[a.kind] ?? a.kind}</Chip>} | |
| 90 | + <StatusBadge status={d.status} /> | |
| 91 | + </div> | |
| 92 | + <div className="mt-2 flex flex-col gap-4 md:flex-row md:items-end md:justify-between"> | |
| 93 | + <div className="min-w-0"> | |
| 94 | + <h1 className="display text-[28px] md:text-[40px]">{d.name}</h1> | |
| 95 | + <p className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-ink-2"> | |
| 96 | + {typeof a.manufacturer === 'string' && <span>{a.manufacturer}</span>} | |
| 97 | + {d.organization && ( | |
| 98 | + <Link href={routes.entity({ entity_type: 'company', slug: d.organization.slug })} className="hover:text-accent"> | |
| 99 | + {d.organization.name} | |
| 100 | + </Link> | |
| 101 | + )} | |
| 102 | + {typeof a.spec_url === 'string' && ( | |
| 103 | + <a href={a.spec_url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 hover:text-accent"> | |
| 104 | + spec sheet <ExternalLink className="size-3" aria-hidden /> | |
| 105 | + </a> | |
| 106 | + )} | |
| 107 | + <QualityMark q={d.quality?.score} label /> | |
| 108 | + </p> | |
| 109 | + <dl className="tnum mt-3 flex flex-wrap gap-x-6 gap-y-1 text-sm"> | |
| 110 | + <div><dt className="inline text-ink-3">Memory </dt><dd className="inline font-medium text-ink">{fmtMemory(a.memory_gb)}</dd></div> | |
| 111 | + <div><dt className="inline text-ink-3">Bandwidth </dt><dd className="inline font-medium text-ink">{num(a.memory_bandwidth_gbs) === null ? '—' : `${fmtInt(a.memory_bandwidth_gbs)} GB/s`}</dd></div> | |
| 112 | + <div><dt className="inline text-ink-3">TDP </dt><dd className="inline font-medium text-ink">{num(a.tdp_watts) === null ? '—' : `${fmtInt(a.tdp_watts)} W`}</dd></div> | |
| 113 | + {Array.isArray(a.runtimes) && a.runtimes.length > 0 && <div><dt className="inline text-ink-3">Runtimes </dt><dd className="inline font-medium text-ink">{(a.runtimes as string[]).join(', ')}</dd></div>} | |
| 114 | + </dl> | |
| 115 | + {d.description && <p className="mt-3 max-w-2xl text-[15px] leading-relaxed text-ink-2">{d.description}</p>} | |
| 116 | + </div> | |
| 117 | + <div className="flex shrink-0 flex-wrap items-center gap-2"> | |
| 118 | + <CompareButton e={d} /> | |
| 119 | + <WatchButton e={d} /> | |
| 120 | + <Link href={`${routes.runLocally()}?hardware=${encodeURIComponent(d.slug)}`} className="inline-flex h-9 items-center border border-rule-strong px-2.5 text-sm font-medium text-ink hover:border-accent hover:text-accent"> | |
| 121 | + Run locally with this device → | |
| 122 | + </Link> | |
| 123 | + </div> | |
| 124 | + </div> | |
| 125 | + </header> | |
| 126 | + | |
| 127 | + <Section eyebrow="Specifications" title="As published" lede="Every value opens its evidence (source, tier, observed time)." hairline={false}> | |
| 128 | + <KeyValue rows={specRows} provenance={d.provenance} slug={d.slug} entity={{ name: d.name, entity_type: d.entity_type }} /> | |
| 129 | + </Section> | |
| 130 | + | |
| 131 | + {/* ------------------------------------------------------------------------------------------------- fit */} | |
| 132 | + <Section id="fit" eyebrow="What can this run?" title={<span className="inline-flex flex-wrap items-center gap-2">Models estimated to fit <Estimated /></span>} lede="Estimate from each model's parameter count, the chosen quantization and context — never a measurement." action={{ href: `${routes.runLocally()}?hardware=${encodeURIComponent(d.slug)}&quant=${quant}&context=${context}${memoryUsed ? `&memory_gb=${memoryUsed}` : ''}`, label: 'Open in Run locally' }}> | |
| 133 | + <div className="mb-4 flex flex-wrap items-center gap-x-6 gap-y-3 text-xs"> | |
| 134 | + <div className="flex flex-wrap items-center gap-1.5"> | |
| 135 | + <span className="eyebrow mr-1">Quantization</span> | |
| 136 | + {QUANTS.map((q) => ( | |
| 137 | + <Link key={q.value} href={self({ quant: q.value })} className={chip(quant === q.value)} aria-current={quant === q.value ? 'true' : undefined}> | |
| 138 | + {q.value} | |
| 139 | + </Link> | |
| 140 | + ))} | |
| 141 | + </div> | |
| 142 | + <div className="flex flex-wrap items-center gap-1.5"> | |
| 143 | + <span className="eyebrow mr-1">Context</span> | |
| 144 | + {CONTEXTS.map((c) => ( | |
| 145 | + <Link key={c} href={self({ context: c })} className={chip(context === c)} aria-current={context === c ? 'true' : undefined}> | |
| 146 | + {fmtTokens(c)} | |
| 147 | + </Link> | |
| 148 | + ))} | |
| 149 | + </div> | |
| 150 | + {options.length > 1 && ( | |
| 151 | + <div className="flex flex-wrap items-center gap-1.5"> | |
| 152 | + <span className="eyebrow mr-1">Configuration</span> | |
| 153 | + {options.map((m) => ( | |
| 154 | + <Link key={m} href={self({ memory_gb: m })} className={chip(memoryUsed === m)} aria-current={memoryUsed === m ? 'true' : undefined}> | |
| 155 | + {m} GB | |
| 156 | + </Link> | |
| 157 | + ))} | |
| 158 | + </div> | |
| 159 | + )} | |
| 160 | + </div> | |
| 161 | + {!fit ? ( | |
| 162 | + <Unavailable what="Fit estimate" reason={options.length ? undefined : 'This device has no sourced memory figure — nothing can be estimated.'} /> | |
| 163 | + ) : ( | |
| 164 | + <> | |
| 165 | + <EstimateBanner assumptions={fit.assumptions} note={fit.note ?? null} counts={fit.counts} /> | |
| 166 | + {options.length > 1 && <Note className="mt-2">This device ships in {options.length} memory configurations ({options.join(' / ')} GB). Estimates use {fmtGb(memoryUsed)} — pick another configuration above. Multiple units: use <Link href={`${routes.runLocally()}?hardware=${encodeURIComponent(d.slug)}&gpu_count=2`} className="link">Run locally</Link> (memories are summed; interconnect is not modelled).</Note>} | |
| 167 | + {fit.runtimes?.length > 0 && <p className="mt-2 text-xs text-ink-3">Runtimes recorded for this device: <span className="mono">{fit.runtimes.join(' · ')}</span></p>} | |
| 168 | + {items.length === 0 ? ( | |
| 169 | + <EmptyState title="No model evaluated" className="mt-4">Models without a sourced parameter count are not estimated.</EmptyState> | |
| 170 | + ) : ( | |
| 171 | + <DataTable scroll compact className="mt-4"> | |
| 172 | + <thead> | |
| 173 | + <tr> | |
| 174 | + <Th>Model</Th> | |
| 175 | + <Th num>Params</Th> | |
| 176 | + <Th num>Est. memory</Th> | |
| 177 | + <Th num>Headroom</Th> | |
| 178 | + <Th>Fits</Th> | |
| 179 | + <Th>Breakdown</Th> | |
| 180 | + <Th className="w-24" aria-label="Compare" /> | |
| 181 | + </tr> | |
| 182 | + </thead> | |
| 183 | + <tbody> | |
| 184 | + {items.length === 0 && <EmptyRow cols={7} />} | |
| 185 | + {items.map((r) => { | |
| 186 | + const openness = typeof r.model.attributes?.openness === 'string' ? r.model.attributes.openness : null; | |
| 187 | + const head = num(r.headroom_gb); | |
| 188 | + return ( | |
| 189 | + <tr key={r.model.id}> | |
| 190 | + <Td primary> | |
| 191 | + <span className="flex flex-wrap items-center gap-x-2 gap-y-0.5"> | |
| 192 | + <EntityLink e={r.model} /> | |
| 193 | + {openness && <OpennessBadge openness={openness} />} | |
| 194 | + </span> | |
| 195 | + {r.model.organization && <span className="block text-xs text-ink-3">{r.model.organization.name}</span>} | |
| 196 | + </Td> | |
| 197 | + <Td num label="Params" className="tnum">{fmtParams(r.parameter_count ?? r.model.attributes?.parameter_count)}</Td> | |
| 198 | + <Td num label="Est. memory" className="tnum">{fmtGb(r.estimated_memory_gb, 1)} <SourceTag source={r.breakdown?.weights_source ?? 'estimated'} /></Td> | |
| 199 | + <Td num label="Headroom" className={r.fits ? 'tnum text-positive' : 'tnum text-danger'}>{head === null ? '—' : `${head >= 0 ? '+' : '−'}${fmtGb(Math.abs(head), 1)}`}</Td> | |
| 200 | + <Td label="Fits" className={r.fits ? 'font-medium text-positive' : 'text-ink-3'}>{r.fits ? '✓ fits' : '✗ too large'}</Td> | |
| 201 | + <Td label="Breakdown" wide><FitBreakdownList fit={r} /></Td> | |
| 202 | + <Td className="text-right"><CompareButton e={r.model} size="sm" /></Td> | |
| 203 | + </tr> | |
| 204 | + ); | |
| 205 | + })} | |
| 206 | + </tbody> | |
| 207 | + </DataTable> | |
| 208 | + )} | |
| 209 | + {fit.items.length >= 120 && <Note className="mt-2">First 120 models (fitting first). The full list is in <Link href={`${routes.runLocally()}?hardware=${encodeURIComponent(d.slug)}&quant=${quant}&context=${context}`} className="link">Run locally</Link>.</Note>} | |
| 210 | + <Methodology text={fit.note ?? fit.assumptions[0]} /> | |
| 211 | + </> | |
| 212 | + )} | |
| 213 | + </Section> | |
| 214 | + | |
| 215 | + {d.relations?.length ? ( | |
| 216 | + <Section eyebrow="Relations" title="In the graph" action={{ href: routes.graph(d.slug), label: 'Explore graph' }}> | |
| 217 | + <RelationsBlock relations={d.relations} /> | |
| 218 | + </Section> | |
| 219 | + ) : null} | |
| 220 | + <Section eyebrow="Timeline" title="Events" action={{ href: routes.timeline({ entity: d.slug }), label: 'Full timeline' }}> | |
| 221 | + <TimelineList events={d.timeline ?? []} slug={d.slug} /> | |
| 222 | + </Section> | |
| 223 | + <Section eyebrow="Sources" title="Where these facts come from"> | |
| 224 | + <SourcesTable sources={d.sources ?? []} /> | |
| 225 | + </Section> | |
| 226 | + <CompareTrayBar /> | |
| 227 | + </Container> | |
| 228 | + ); | |
| 229 | +} | |
added
apps/web/src/app/hardware/frontier/page.tsx
+120 −0
Large diff (120 lines) is not loaded by default to keep the page fast.Load diff
modified
apps/web/src/app/hardware/page.tsx
+177 −32
Large diff (221 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/open/opengraph-image.tsx
+17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/open/page.tsx
+361 −0
Large diff (361 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/prices/opengraph-image.tsx
+24 −0
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
modified
apps/web/src/app/prices/page.tsx
+308 −97
Large diff (524 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/providers/[slug]/opengraph-image.tsx
+24 −0
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/providers/[slug]/page.tsx
+275 −0
Large diff (275 lines) is not loaded by default to keep the page fast.Load diff
modified
apps/web/src/app/providers/page.tsx
+149 −36
Large diff (220 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/pulse/opengraph-image.tsx
+17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/pulse/page.tsx
+250 −0
Large diff (250 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/run-locally/page.tsx
+246 −0
Large diff (246 lines) is not loaded by default to keep the page fast.Load diff
modified
apps/web/src/app/tools/page.tsx
+11 −23
Large diff (65 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/hardware/hardware-bits.tsx
+58 −0
Large diff (58 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/intelligence/bits.tsx
+295 −0
Large diff (295 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/intelligence/calculator.tsx
+390 −0
Large diff (390 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/intelligence/client-charts.tsx
+29 −0
Large diff (29 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/intelligence/expand-price-row.tsx
+97 −0
Large diff (97 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/intelligence/finder-form.tsx
+132 −0
Large diff (132 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/intelligence/listing-table.tsx
+155 −0
Large diff (155 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/intelligence/model-picker.tsx
+108 −0
Large diff (108 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/intelligence/price-index-chart.tsx
+81 −0
Large diff (81 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/intelligence/run-locally-form.tsx
+172 −0
Large diff (172 lines) is not loaded by default to keep the page fast.Load diff
modified
apps/web/src/components/prices/movers.tsx
+42 −6
Large diff (98 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/providers/provider-strip.tsx
+23 −0
Large diff (23 lines) is not loaded by default to keep the page fast.Load diff
modified
apps/web/src/lib/client-api.ts
+39 −0
Large diff (43 lines) is not loaded by default to keep the page fast.Load diff