From 044a049be218ca03e765ecc7050cab103b09b933 Mon Sep 17 00:00:00 2001
From: Oseltamivir <58582368+Oseltamivir@users.noreply.github.com>
Date: Tue, 26 May 2026 19:45:44 -0700
Subject: [PATCH 1/2] feat(compare): replace vendor-grouped pair cards with a
per-model matrix
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Each model's index card now renders an N×N GPU matrix. Headers and cells
are tinted by vendor — NVIDIA green, AMD red, cross-vendor cells split
on the 135° diagonal — so users can scan vendor combos at a glance
instead of reading three separate "NVIDIA vs NVIDIA / AMD vs AMD /
cross" sections. Diagonal cells are inert; cells without benchmark data
render as faint outlines.
Applies to both /compare and /compare-per-dollar indices. Drops the
ComparePairCardLink component (no remaining callers).
---
.../app/src/app/compare-per-dollar/page.tsx | 79 +-----
packages/app/src/app/compare/page.tsx | 75 +-----
.../compare/compare-pair-card-link.tsx | 38 ---
.../compare/compare-pair-matrix.tsx | 225 ++++++++++++++++++
4 files changed, 247 insertions(+), 170 deletions(-)
delete mode 100644 packages/app/src/components/compare/compare-pair-card-link.tsx
create mode 100644 packages/app/src/components/compare/compare-pair-matrix.tsx
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..29e83baf
--- /dev/null
+++ b/packages/app/src/components/compare/compare-pair-matrix.tsx
@@ -0,0 +1,225 @@
+'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('|');
+}
+
+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 flex min-h-[44px] items-center justify-center rounded-md border 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}
+ >
+
+ ↗
+
+
+ );
+}
+
+export function CompareMatrixLegend() {
+ return (
+
+
+
+
+
+ );
+}
+
+function LegendSwatch({ style, label }: { style: React.CSSProperties; label: string }) {
+ return (
+
+
+ {label}
+
+ );
+}
From 25f3834947f26527e706ddca8575d861761b80f7 Mon Sep 17 00:00:00 2001
From: Oseltamivir <58582368+Oseltamivir@users.noreply.github.com>
Date: Tue, 26 May 2026 20:14:09 -0700
Subject: [PATCH 2/2] feat(compare): show stacked GPU labels inside each matrix
cell
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Empty colored rectangles felt visually weird. Each pair cell now shows
the two GPU short labels (NVL72 suffix dropped to fit) stacked with a
small "vs" separator, each tinted in its vendor's brand color. The
cell's vendor-tint background still carries the same×same / cross-vendor
signal; the in-cell labels add information density and make the matrix
readable without retracing the axes when scrolled.
---
.../compare/compare-pair-matrix.tsx | 39 +++++++++++++++----
1 file changed, 32 insertions(+), 7 deletions(-)
diff --git a/packages/app/src/components/compare/compare-pair-matrix.tsx b/packages/app/src/components/compare/compare-pair-matrix.tsx
index 29e83baf..68e32101 100644
--- a/packages/app/src/components/compare/compare-pair-matrix.tsx
+++ b/packages/app/src/components/compare/compare-pair-matrix.tsx
@@ -56,6 +56,18 @@ 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]));
@@ -65,7 +77,7 @@ export function ComparePairMatrix({ pairs, hrefPrefix }: ComparePairMatrixProps)
@@ -113,8 +125,8 @@ function HeaderChip({ gpu, axis }: { gpu: string; axis: 'row' | 'col' }) {
@@ -137,7 +149,7 @@ function DiagonalCell({ gpu }: { gpu: string }) {
}
function EmptyCell() {
- return
;
+ return
;
}
function PairCell({
@@ -183,10 +195,23 @@ function PairCell({
track('compare_index_matrix_clicked', { slug, label });
window.location.href = href;
}}
- className="group flex min-h-[44px] items-center justify-center rounded-md border 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"
+ 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)}
+
+
↗