Skip to content

Commit 044a049

Browse files
committed
feat(compare): replace vendor-grouped pair cards with a per-model matrix
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).
1 parent ae78b8a commit 044a049

4 files changed

Lines changed: 247 additions & 170 deletions

File tree

packages/app/src/app/compare-per-dollar/page.tsx

Lines changed: 12 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { Metadata } from 'next';
22

3-
import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants';
3+
import { SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants';
44

5-
import { ComparePairCardLink } from '@/components/compare/compare-pair-card-link';
5+
import { CompareMatrixLegend, ComparePairMatrix } from '@/components/compare/compare-pair-matrix';
66
import { JsonLd } from '@/components/json-ld';
77
import { Card } from '@/components/ui/card';
88
import { getComparablePairsByModelSlug } from '@/lib/compare-availability';
9-
import { type ComparePair, COMPARE_MODEL_SLUGS, type CompareModelSlug } from '@/lib/compare-slug';
9+
import { COMPARE_MODEL_SLUGS } from '@/lib/compare-slug';
1010
import { bucketComparePairsByVendor, formatModelList } from '@/lib/compare-ssr';
1111

1212
export const dynamic = 'force-dynamic';
@@ -31,42 +31,6 @@ export const metadata: Metadata = {
3131
},
3232
};
3333

34-
interface VendorGroup {
35-
heading: string;
36-
description: string;
37-
pairs: { a: string; b: string; slug: string; label: string }[];
38-
}
39-
40-
function groupPairsByVendorForModel(
41-
model: CompareModelSlug,
42-
comparablePairs: ComparePair[],
43-
): VendorGroup[] {
44-
const { cross, nvidia, amd } = bucketComparePairsByVendor(model.slug, comparablePairs);
45-
const groups: VendorGroup[] = [];
46-
if (cross.length > 0) {
47-
groups.push({
48-
heading: 'NVIDIA vs AMD',
49-
description: 'Cross-vendor cost-per-token comparisons across architecture generations.',
50-
pairs: cross,
51-
});
52-
}
53-
if (nvidia.length > 0) {
54-
groups.push({
55-
heading: 'NVIDIA vs NVIDIA',
56-
description: 'Hopper and Blackwell generation cost-per-token comparisons.',
57-
pairs: nvidia,
58-
});
59-
}
60-
if (amd.length > 0) {
61-
groups.push({
62-
heading: 'AMD vs AMD',
63-
description: 'CDNA 3 and CDNA 4 generation cost-per-token comparisons.',
64-
pairs: amd,
65-
});
66-
}
67-
return groups;
68-
}
69-
7034
const jsonLd = {
7135
'@context': 'https://schema.org',
7236
'@type': 'CollectionPage',
@@ -78,9 +42,9 @@ const jsonLd = {
7842
export default async function ComparePerDollarIndexPage() {
7943
// Server-side filter (Neon availability): only show (model, pair) combos
8044
// where both GPUs have benchmark data for that model. Matches the /compare
81-
// index's behavior — no empty-state cards in navigation. The page-level
82-
// handler at /compare-per-dollar/[slug] still renders the empty-state for
83-
// direct URL hits.
45+
// index's behavior — no empty cells in navigation. The page-level handler at
46+
// /compare-per-dollar/[slug] still renders the empty-state for direct URL
47+
// hits.
8448
const comparablePairsByModel = await getComparablePairsByModelSlug();
8549
const totalUrls = [...comparablePairsByModel.values()].reduce((s, p) => s + p.length, 0);
8650
const modelsWithPairs = COMPARE_MODEL_SLUGS.filter(
@@ -101,12 +65,16 @@ export default async function ComparePerDollarIndexPage() {
10165
each page renders the cost-per-token chart and an interpolated dollars-per-million
10266
comparison table so you can pick the cheaper SKU at any target interactivity level.
10367
</p>
68+
<div className="mt-5">
69+
<CompareMatrixLegend />
70+
</div>
10471
</Card>
10572
</section>
10673

10774
{modelsWithPairs.map((model) => {
10875
const pairs = comparablePairsByModel.get(model.slug) ?? [];
109-
const groups = groupPairsByVendorForModel(model, pairs);
76+
const buckets = bucketComparePairsByVendor(model.slug, pairs);
77+
const entries = [...buckets.nvidia, ...buckets.amd, ...buckets.cross];
11078
return (
11179
<section key={model.slug} id={model.slug}>
11280
<Card className="flex flex-col gap-4">
@@ -117,30 +85,7 @@ export default async function ComparePerDollarIndexPage() {
11785
benchmark data on {model.label}.
11886
</p>
11987
</div>
120-
{groups.map((group) => (
121-
<div key={`${model.slug}__${group.heading}`} className="flex flex-col gap-3">
122-
<div>
123-
<h3 className="text-base font-semibold">{group.heading}</h3>
124-
<p className="text-xs text-muted-foreground mt-1">{group.description}</p>
125-
</div>
126-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
127-
{group.pairs.map(({ slug, label, a, b }) => {
128-
const aMeta = HW_REGISTRY[a];
129-
const bMeta = HW_REGISTRY[b];
130-
const archLine = `${aMeta?.arch ?? '—'} · ${bMeta?.arch ?? '—'}`;
131-
return (
132-
<ComparePairCardLink
133-
key={slug}
134-
href={`/compare-per-dollar/${slug}`}
135-
slug={slug}
136-
label={label}
137-
archLine={archLine}
138-
/>
139-
);
140-
})}
141-
</div>
142-
</div>
143-
))}
88+
<ComparePairMatrix pairs={entries} hrefPrefix="/compare-per-dollar" />
14489
</Card>
14590
</section>
14691
);

packages/app/src/app/compare/page.tsx

Lines changed: 10 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { Metadata } from 'next';
22
import Link from 'next/link';
33

4-
import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants';
4+
import { SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants';
55

6-
import { ComparePairCardLink } from '@/components/compare/compare-pair-card-link';
6+
import { CompareMatrixLegend, ComparePairMatrix } from '@/components/compare/compare-pair-matrix';
77
import { JsonLd } from '@/components/json-ld';
88
import { Card } from '@/components/ui/card';
99
import { getComparablePairsByModelSlug } from '@/lib/compare-availability';
10-
import { type ComparePair, COMPARE_MODEL_SLUGS, type CompareModelSlug } from '@/lib/compare-slug';
10+
import { COMPARE_MODEL_SLUGS } from '@/lib/compare-slug';
1111
import { bucketComparePairsByVendor, formatModelList } from '@/lib/compare-ssr';
1212

1313
export const dynamic = 'force-dynamic';
@@ -32,42 +32,6 @@ export const metadata: Metadata = {
3232
},
3333
};
3434

35-
interface VendorGroup {
36-
heading: string;
37-
description: string;
38-
pairs: { a: string; b: string; slug: string; label: string }[];
39-
}
40-
41-
function groupPairsByVendorForModel(
42-
model: CompareModelSlug,
43-
comparablePairs: ComparePair[],
44-
): VendorGroup[] {
45-
const { cross, nvidia, amd } = bucketComparePairsByVendor(model.slug, comparablePairs);
46-
const groups: VendorGroup[] = [];
47-
if (cross.length > 0) {
48-
groups.push({
49-
heading: 'NVIDIA vs AMD',
50-
description: 'Cross-vendor comparisons across architecture generations.',
51-
pairs: cross,
52-
});
53-
}
54-
if (nvidia.length > 0) {
55-
groups.push({
56-
heading: 'NVIDIA vs NVIDIA',
57-
description: 'Hopper and Blackwell generation comparisons.',
58-
pairs: nvidia,
59-
});
60-
}
61-
if (amd.length > 0) {
62-
groups.push({
63-
heading: 'AMD vs AMD',
64-
description: 'CDNA 3 and CDNA 4 generation comparisons.',
65-
pairs: amd,
66-
});
67-
}
68-
return groups;
69-
}
70-
7135
const jsonLd = {
7236
'@context': 'https://schema.org',
7337
'@type': 'CollectionPage',
@@ -78,7 +42,7 @@ const jsonLd = {
7842

7943
export default async function CompareIndexPage() {
8044
// Server-side filter: only show (model, pair) combinations where both GPUs
81-
// have benchmark data for that model. Avoids cards that would link to an
45+
// have benchmark data for that model. Avoids cells that would link to an
8246
// empty-state page. The page-level handler at /compare/[slug] still renders
8347
// the empty-state for direct URL hits, so this is purely a navigation
8448
// hygiene concern.
@@ -99,6 +63,9 @@ export default async function CompareIndexPage() {
9963
{formatModelList(modelsWithPairs)}. Each page includes interactive charts for latency,
10064
throughput, and cost metrics, plus an interpolated comparison table.
10165
</p>
66+
<div className="mt-5">
67+
<CompareMatrixLegend />
68+
</div>
10269
<div className="mt-6">
10370
<Link
10471
data-testid="compare-index-per-dollar-link"
@@ -116,7 +83,8 @@ export default async function CompareIndexPage() {
11683

11784
{modelsWithPairs.map((model) => {
11885
const pairs = comparablePairsByModel.get(model.slug) ?? [];
119-
const groups = groupPairsByVendorForModel(model, pairs);
86+
const buckets = bucketComparePairsByVendor(model.slug, pairs);
87+
const entries = [...buckets.nvidia, ...buckets.amd, ...buckets.cross];
12088
return (
12189
<section key={model.slug} id={model.slug}>
12290
<Card className="flex flex-col gap-4">
@@ -127,30 +95,7 @@ export default async function CompareIndexPage() {
12795
{model.label}.
12896
</p>
12997
</div>
130-
{groups.map((group) => (
131-
<div key={`${model.slug}__${group.heading}`} className="flex flex-col gap-3">
132-
<div>
133-
<h3 className="text-base font-semibold">{group.heading}</h3>
134-
<p className="text-xs text-muted-foreground mt-1">{group.description}</p>
135-
</div>
136-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
137-
{group.pairs.map(({ slug, label, a, b }) => {
138-
const aMeta = HW_REGISTRY[a];
139-
const bMeta = HW_REGISTRY[b];
140-
const archLine = `${aMeta?.arch ?? '—'} · ${bMeta?.arch ?? '—'}`;
141-
return (
142-
<ComparePairCardLink
143-
key={slug}
144-
href={`/compare/${slug}`}
145-
slug={slug}
146-
label={label}
147-
archLine={archLine}
148-
/>
149-
);
150-
})}
151-
</div>
152-
</div>
153-
))}
98+
<ComparePairMatrix pairs={entries} hrefPrefix="/compare" />
15499
</Card>
155100
</section>
156101
);

packages/app/src/components/compare/compare-pair-card-link.tsx

Lines changed: 0 additions & 38 deletions
This file was deleted.

0 commit comments

Comments
 (0)