diff --git a/packages/app/src/app/compare-per-dollar/page.tsx b/packages/app/src/app/compare-per-dollar/page.tsx index 56ccc4fd..6bafb86d 100644 --- a/packages/app/src/app/compare-per-dollar/page.tsx +++ b/packages/app/src/app/compare-per-dollar/page.tsx @@ -1,12 +1,12 @@ import type { Metadata } from 'next'; -import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; +import { SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; -import { ComparePairCardLink } from '@/components/compare/compare-pair-card-link'; +import { CompareMatrixLegend, ComparePairMatrix } from '@/components/compare/compare-pair-matrix'; import { JsonLd } from '@/components/json-ld'; import { Card } from '@/components/ui/card'; import { getComparablePairsByModelSlug } from '@/lib/compare-availability'; -import { type ComparePair, COMPARE_MODEL_SLUGS, type CompareModelSlug } from '@/lib/compare-slug'; +import { COMPARE_MODEL_SLUGS } from '@/lib/compare-slug'; import { bucketComparePairsByVendor, formatModelList } from '@/lib/compare-ssr'; export const dynamic = 'force-dynamic'; @@ -31,42 +31,6 @@ export const metadata: Metadata = { }, }; -interface VendorGroup { - heading: string; - description: string; - pairs: { a: string; b: string; slug: string; label: string }[]; -} - -function groupPairsByVendorForModel( - model: CompareModelSlug, - comparablePairs: ComparePair[], -): VendorGroup[] { - const { cross, nvidia, amd } = bucketComparePairsByVendor(model.slug, comparablePairs); - const groups: VendorGroup[] = []; - if (cross.length > 0) { - groups.push({ - heading: 'NVIDIA vs AMD', - description: 'Cross-vendor cost-per-token comparisons across architecture generations.', - pairs: cross, - }); - } - if (nvidia.length > 0) { - groups.push({ - heading: 'NVIDIA vs NVIDIA', - description: 'Hopper and Blackwell generation cost-per-token comparisons.', - pairs: nvidia, - }); - } - if (amd.length > 0) { - groups.push({ - heading: 'AMD vs AMD', - description: 'CDNA 3 and CDNA 4 generation cost-per-token comparisons.', - pairs: amd, - }); - } - return groups; -} - const jsonLd = { '@context': 'https://schema.org', '@type': 'CollectionPage', @@ -78,9 +42,9 @@ const jsonLd = { export default async function ComparePerDollarIndexPage() { // Server-side filter (Neon availability): only show (model, pair) combos // where both GPUs have benchmark data for that model. Matches the /compare - // index's behavior — no empty-state cards in navigation. The page-level - // handler at /compare-per-dollar/[slug] still renders the empty-state for - // direct URL hits. + // index's behavior — no empty cells in navigation. The page-level handler at + // /compare-per-dollar/[slug] still renders the empty-state for direct URL + // hits. const comparablePairsByModel = await getComparablePairsByModelSlug(); const totalUrls = [...comparablePairsByModel.values()].reduce((s, p) => s + p.length, 0); const modelsWithPairs = COMPARE_MODEL_SLUGS.filter( @@ -101,12 +65,16 @@ export default async function ComparePerDollarIndexPage() { each page renders the cost-per-token chart and an interpolated dollars-per-million comparison table so you can pick the cheaper SKU at any target interactivity level.

+
+ +
{modelsWithPairs.map((model) => { const pairs = comparablePairsByModel.get(model.slug) ?? []; - const groups = groupPairsByVendorForModel(model, pairs); + const buckets = bucketComparePairsByVendor(model.slug, pairs); + const entries = [...buckets.nvidia, ...buckets.amd, ...buckets.cross]; return (
@@ -117,30 +85,7 @@ export default async function ComparePerDollarIndexPage() { benchmark data on {model.label}.

- {groups.map((group) => ( -
-
-

{group.heading}

-

{group.description}

-
-
- {group.pairs.map(({ slug, label, a, b }) => { - const aMeta = HW_REGISTRY[a]; - const bMeta = HW_REGISTRY[b]; - const archLine = `${aMeta?.arch ?? '—'} · ${bMeta?.arch ?? '—'}`; - return ( - - ); - })} -
-
- ))} +
); diff --git a/packages/app/src/app/compare/page.tsx b/packages/app/src/app/compare/page.tsx index 38baf3b9..1c90211a 100644 --- a/packages/app/src/app/compare/page.tsx +++ b/packages/app/src/app/compare/page.tsx @@ -1,13 +1,13 @@ import type { Metadata } from 'next'; import Link from 'next/link'; -import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; +import { SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; -import { ComparePairCardLink } from '@/components/compare/compare-pair-card-link'; +import { CompareMatrixLegend, ComparePairMatrix } from '@/components/compare/compare-pair-matrix'; import { JsonLd } from '@/components/json-ld'; import { Card } from '@/components/ui/card'; import { getComparablePairsByModelSlug } from '@/lib/compare-availability'; -import { type ComparePair, COMPARE_MODEL_SLUGS, type CompareModelSlug } from '@/lib/compare-slug'; +import { COMPARE_MODEL_SLUGS } from '@/lib/compare-slug'; import { bucketComparePairsByVendor, formatModelList } from '@/lib/compare-ssr'; export const dynamic = 'force-dynamic'; @@ -32,42 +32,6 @@ export const metadata: Metadata = { }, }; -interface VendorGroup { - heading: string; - description: string; - pairs: { a: string; b: string; slug: string; label: string }[]; -} - -function groupPairsByVendorForModel( - model: CompareModelSlug, - comparablePairs: ComparePair[], -): VendorGroup[] { - const { cross, nvidia, amd } = bucketComparePairsByVendor(model.slug, comparablePairs); - const groups: VendorGroup[] = []; - if (cross.length > 0) { - groups.push({ - heading: 'NVIDIA vs AMD', - description: 'Cross-vendor comparisons across architecture generations.', - pairs: cross, - }); - } - if (nvidia.length > 0) { - groups.push({ - heading: 'NVIDIA vs NVIDIA', - description: 'Hopper and Blackwell generation comparisons.', - pairs: nvidia, - }); - } - if (amd.length > 0) { - groups.push({ - heading: 'AMD vs AMD', - description: 'CDNA 3 and CDNA 4 generation comparisons.', - pairs: amd, - }); - } - return groups; -} - const jsonLd = { '@context': 'https://schema.org', '@type': 'CollectionPage', @@ -78,7 +42,7 @@ const jsonLd = { export default async function CompareIndexPage() { // Server-side filter: only show (model, pair) combinations where both GPUs - // have benchmark data for that model. Avoids cards that would link to an + // have benchmark data for that model. Avoids cells that would link to an // empty-state page. The page-level handler at /compare/[slug] still renders // the empty-state for direct URL hits, so this is purely a navigation // hygiene concern. @@ -99,6 +63,9 @@ export default async function CompareIndexPage() { {formatModelList(modelsWithPairs)}. Each page includes interactive charts for latency, throughput, and cost metrics, plus an interpolated comparison table.

+
+ +
{ const pairs = comparablePairsByModel.get(model.slug) ?? []; - const groups = groupPairsByVendorForModel(model, pairs); + const buckets = bucketComparePairsByVendor(model.slug, pairs); + const entries = [...buckets.nvidia, ...buckets.amd, ...buckets.cross]; return (
@@ -127,30 +95,7 @@ export default async function CompareIndexPage() { {model.label}.

- {groups.map((group) => ( -
-
-

{group.heading}

-

{group.description}

-
-
- {group.pairs.map(({ slug, label, a, b }) => { - const aMeta = HW_REGISTRY[a]; - const bMeta = HW_REGISTRY[b]; - const archLine = `${aMeta?.arch ?? '—'} · ${bMeta?.arch ?? '—'}`; - return ( - - ); - })} -
-
- ))} + ); diff --git a/packages/app/src/components/compare/compare-pair-card-link.tsx b/packages/app/src/components/compare/compare-pair-card-link.tsx deleted file mode 100644 index 0b105493..00000000 --- a/packages/app/src/components/compare/compare-pair-card-link.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client'; - -import { ArrowRight } from 'lucide-react'; - -import { track } from '@/lib/analytics'; - -interface ComparePairCardLinkProps { - href: string; - slug: string; - label: string; - archLine: string; -} - -export function ComparePairCardLink({ href, slug, label, archLine }: ComparePairCardLinkProps) { - return ( - { - if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return; - e.preventDefault(); - track('compare_index_pair_clicked', { slug, label }); - window.location.href = href; - }} - > -
-
-
-

- {label} -

-

{archLine}

-
- -
-
- ); -} diff --git a/packages/app/src/components/compare/compare-pair-matrix.tsx b/packages/app/src/components/compare/compare-pair-matrix.tsx new file mode 100644 index 00000000..68e32101 --- /dev/null +++ b/packages/app/src/components/compare/compare-pair-matrix.tsx @@ -0,0 +1,250 @@ +'use client'; + +import { Fragment } from 'react'; + +import { HW_REGISTRY } from '@semianalysisai/inferencex-constants'; + +import { track } from '@/lib/analytics'; + +const NVIDIA_COLOR = '#76b900'; +const AMD_COLOR = '#ed1c24'; + +const NVIDIA_BG = 'rgba(118, 185, 0, 0.10)'; +const NVIDIA_BG_STRONG = 'rgba(118, 185, 0, 0.18)'; +const NVIDIA_BORDER = 'rgba(118, 185, 0, 0.40)'; +const AMD_BG = 'rgba(237, 28, 36, 0.10)'; +const AMD_BG_STRONG = 'rgba(237, 28, 36, 0.18)'; +const AMD_BORDER = 'rgba(237, 28, 36, 0.40)'; + +type VendorKey = 'nvidia' | 'amd' | 'unknown'; + +interface MatrixEntry { + a: string; + b: string; + slug: string; + label: string; +} + +interface ComparePairMatrixProps { + pairs: MatrixEntry[]; + hrefPrefix: '/compare' | '/compare-per-dollar'; +} + +function vendorOf(gpu: string): VendorKey { + const v = HW_REGISTRY[gpu]?.vendor; + if (v === 'NVIDIA') return 'nvidia'; + if (v === 'AMD') return 'amd'; + return 'unknown'; +} + +function vendorRank(vendor: VendorKey): number { + if (vendor === 'nvidia') return 0; + if (vendor === 'amd') return 1; + return 2; +} + +function compareGpus(a: string, b: string): number { + const va = vendorRank(vendorOf(a)); + const vb = vendorRank(vendorOf(b)); + if (va !== vb) return va - vb; + const sa = HW_REGISTRY[a]?.sort ?? 999; + const sb = HW_REGISTRY[b]?.sort ?? 999; + return sa - sb; +} + +function pairKey(a: string, b: string): string { + return [a, b].toSorted().join('|'); +} + +/** Drop trailing "NVL72" so labels fit inside narrow cells without wrapping. */ +function shortHwLabel(gpu: string): string { + const label = HW_REGISTRY[gpu]?.label ?? gpu.toUpperCase(); + return label.replace(/\s+NVL72$/u, ''); +} + +function vendorTextColor(vendor: VendorKey): string { + if (vendor === 'nvidia') return NVIDIA_COLOR; + if (vendor === 'amd') return AMD_COLOR; + return 'currentColor'; +} + +export function ComparePairMatrix({ pairs, hrefPrefix }: ComparePairMatrixProps) { + const gpus = [...new Set(pairs.flatMap((p) => [p.a, p.b]))].toSorted(compareGpus); + const pairByKey = new Map(pairs.map((p) => [pairKey(p.a, p.b), p])); + + return ( +
+
+
+ {gpus.map((g) => ( + + ))} + {gpus.map((rowGpu) => ( + + + {gpus.map((colGpu) => { + if (rowGpu === colGpu) { + return ; + } + const entry = pairByKey.get(pairKey(rowGpu, colGpu)); + if (!entry) return ; + return ( + + ); + })} + + ))} +
+
+ ); +} + +function HeaderChip({ gpu, axis }: { gpu: string; axis: 'row' | 'col' }) { + const vendor = vendorOf(gpu); + const label = HW_REGISTRY[gpu]?.label ?? gpu.toUpperCase(); + const arch = HW_REGISTRY[gpu]?.arch; + const style = + vendor === 'nvidia' + ? { background: NVIDIA_BG_STRONG, color: NVIDIA_COLOR, borderColor: NVIDIA_BORDER } + : vendor === 'amd' + ? { background: AMD_BG_STRONG, color: AMD_COLOR, borderColor: AMD_BORDER } + : undefined; + return ( +
+ {label} +
+ ); +} + +function DiagonalCell({ gpu }: { gpu: string }) { + return ( +
+ — +
+ ); +} + +function EmptyCell() { + return
; +} + +function PairCell({ + rowGpu, + colGpu, + href, + slug, + label, +}: { + rowGpu: string; + colGpu: string; + href: string; + slug: string; + label: string; +}) { + const vRow = vendorOf(rowGpu); + const vCol = vendorOf(colGpu); + const sameVendor = vRow === vCol && vRow !== 'unknown'; + + let cellStyle: { background: string; borderColor: string }; + if (sameVendor && vRow === 'nvidia') { + cellStyle = { background: NVIDIA_BG, borderColor: NVIDIA_BORDER }; + } else if (sameVendor && vRow === 'amd') { + cellStyle = { background: AMD_BG, borderColor: AMD_BORDER }; + } else { + // Cross-vendor: 45° split, row vendor on top-left, col vendor on bottom-right. + const rowColor = vRow === 'amd' ? AMD_BG_STRONG : NVIDIA_BG_STRONG; + const colColor = vCol === 'amd' ? AMD_BG_STRONG : NVIDIA_BG_STRONG; + cellStyle = { + background: `linear-gradient(135deg, ${rowColor} 0%, ${rowColor} 50%, ${colColor} 50%, ${colColor} 100%)`, + borderColor: 'rgba(160, 160, 160, 0.35)', + }; + } + + return ( + { + if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return; + e.preventDefault(); + track('compare_index_matrix_clicked', { slug, label }); + window.location.href = href; + }} + className="group relative flex min-h-[48px] flex-col items-center justify-center gap-0 rounded-md border px-1.5 py-1 text-center leading-tight transition-all hover:scale-[1.04] hover:shadow-md hover:shadow-brand/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand/60" + style={cellStyle} + > + + {shortHwLabel(rowGpu)} + + vs + + {shortHwLabel(colGpu)} + + + ↗ + + + ); +} + +export function CompareMatrixLegend() { + return ( +
+ + + +
+ ); +} + +function LegendSwatch({ style, label }: { style: React.CSSProperties; label: string }) { + return ( + + + {label} + + ); +}