From d213a3d3889f11d46477c17bd5bb9b2e68e81436 Mon Sep 17 00:00:00 2001 From: functionstackx <47992694+functionstackx@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:51:31 -0400 Subject: [PATCH 01/12] feat: add curated landing page to reduce clicks-to-insight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users landing on inferencex.com were overwhelmed by 3 paragraphs of intro text, 6+ dropdown selectors, and 30+ chart lines. PostHog showed 10-30 clicks needed to find useful views. New landing page at / with: - Condensed hero + quote carousel - "Explore Full Dashboard" CTA → /inference - 6 curated preset cards (1-click to pre-configured views) - g_preset URL param auto-applies presets on dashboard load - Backward-compat redirect for existing share links with chart params Dashboard moves from / to /inference. Header updated accordingly. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/app/cypress/e2e/csv-export.cy.ts | 2 +- .../app/cypress/e2e/custom-user-values.cy.ts | 2 +- .../app/cypress/e2e/drill-down-trend.cy.ts | 2 +- .../app/cypress/e2e/favorite-presets.cy.ts | 2 +- packages/app/cypress/e2e/gpu-power.cy.ts | 4 +- packages/app/cypress/e2e/gpu-specs.cy.ts | 2 +- .../app/cypress/e2e/gradient-labels.cy.ts | 4 +- packages/app/cypress/e2e/high-contrast.cy.ts | 2 +- .../app/cypress/e2e/inference-chart.cy.ts | 2 +- .../app/cypress/e2e/model-architecture.cy.ts | 2 +- packages/app/cypress/e2e/performance.cy.ts | 4 +- packages/app/cypress/e2e/sanity.cy.ts | 8 +- packages/app/cypress/e2e/tabs.cy.ts | 4 +- .../cypress/e2e/throughput-calculator.cy.ts | 2 +- .../app/cypress/e2e/ttft-x-axis-toggle.cy.ts | 2 +- packages/app/cypress/e2e/url-params.cy.ts | 6 +- .../cypress/e2e/yaxis-metrics-render.cy.ts | 2 +- packages/app/src/app/[[...tab]]/page.tsx | 59 ++++++++++-- .../favorites/FavoritePresetsDropdown.tsx | 20 ++++ .../components/favorites/favorite-presets.ts | 58 +++++++++++ packages/app/src/components/header/header.tsx | 20 ++-- .../src/components/landing/landing-page.tsx | 96 +++++++++++++++++++ .../src/components/landing/preset-card.tsx | 40 ++++++++ packages/app/src/components/page-content.tsx | 2 - packages/app/src/lib/tab-meta.ts | 6 ++ 25 files changed, 309 insertions(+), 44 deletions(-) create mode 100644 packages/app/src/components/landing/landing-page.tsx create mode 100644 packages/app/src/components/landing/preset-card.tsx diff --git a/packages/app/cypress/e2e/csv-export.cy.ts b/packages/app/cypress/e2e/csv-export.cy.ts index 15c1f74..e13ad20 100644 --- a/packages/app/cypress/e2e/csv-export.cy.ts +++ b/packages/app/cypress/e2e/csv-export.cy.ts @@ -3,7 +3,7 @@ describe('CSV Export', () => { cy.window().then((win) => { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }); - cy.visit('/'); + cy.visit('/inference'); cy.get('[data-testid="chart-figure"]').should('exist'); }); diff --git a/packages/app/cypress/e2e/custom-user-values.cy.ts b/packages/app/cypress/e2e/custom-user-values.cy.ts index b47926d..e721d7f 100644 --- a/packages/app/cypress/e2e/custom-user-values.cy.ts +++ b/packages/app/cypress/e2e/custom-user-values.cy.ts @@ -3,7 +3,7 @@ describe('Custom User Values', () => { cy.window().then((win) => { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }); - cy.visit('/'); + cy.visit('/inference'); cy.get('[data-testid="model-selector"]').should('be.visible'); }); diff --git a/packages/app/cypress/e2e/drill-down-trend.cy.ts b/packages/app/cypress/e2e/drill-down-trend.cy.ts index 9649290..bd06ec7 100644 --- a/packages/app/cypress/e2e/drill-down-trend.cy.ts +++ b/packages/app/cypress/e2e/drill-down-trend.cy.ts @@ -8,7 +8,7 @@ describe('Drill-Down Trend Chart Modal', () => { cy.window().then((win) => { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }); - cy.visit('/'); + cy.visit('/inference'); // Wait for scatter graph to render with data points cy.get('[data-testid="scatter-graph"]') .first() diff --git a/packages/app/cypress/e2e/favorite-presets.cy.ts b/packages/app/cypress/e2e/favorite-presets.cy.ts index 4c1bf44..836ddcf 100644 --- a/packages/app/cypress/e2e/favorite-presets.cy.ts +++ b/packages/app/cypress/e2e/favorite-presets.cy.ts @@ -1,6 +1,6 @@ describe('Favorite Presets', () => { before(() => { - cy.visit('/', { + cy.visit('/inference', { onBeforeLoad(win) { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }, diff --git a/packages/app/cypress/e2e/gpu-power.cy.ts b/packages/app/cypress/e2e/gpu-power.cy.ts index a4ee52e..8fdc875 100644 --- a/packages/app/cypress/e2e/gpu-power.cy.ts +++ b/packages/app/cypress/e2e/gpu-power.cy.ts @@ -5,7 +5,7 @@ function unlockPowerX() { describe('PowerX', () => { beforeEach(() => { - cy.visit('/', { + cy.visit('/inference', { onBeforeLoad(win) { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }, @@ -32,7 +32,7 @@ describe('PowerX', () => { describe('(unlocked)', () => { beforeEach(() => { - cy.visit('/', { + cy.visit('/inference', { onBeforeLoad(win) { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); win.localStorage.setItem('inferencex-powerx-unlocked', '1'); diff --git a/packages/app/cypress/e2e/gpu-specs.cy.ts b/packages/app/cypress/e2e/gpu-specs.cy.ts index 9d78bd1..4f386ab 100644 --- a/packages/app/cypress/e2e/gpu-specs.cy.ts +++ b/packages/app/cypress/e2e/gpu-specs.cy.ts @@ -371,7 +371,7 @@ describe('GPU Specs Radar Chart View', () => { describe('GPU Specs Navigation', () => { it('tab switcher activates GPU Specs', () => { - cy.visit('/'); + cy.visit('/inference'); // Wait for tabs to be rendered and page to be interactive cy.get('[role="tablist"]').should('be.visible'); // Use force:true to handle potential pointer-events:none from modals/overlays diff --git a/packages/app/cypress/e2e/gradient-labels.cy.ts b/packages/app/cypress/e2e/gradient-labels.cy.ts index d41fec6..3ff8eb2 100644 --- a/packages/app/cypress/e2e/gradient-labels.cy.ts +++ b/packages/app/cypress/e2e/gradient-labels.cy.ts @@ -1,6 +1,6 @@ describe('Gradient Labels Toggle', () => { before(() => { - cy.visit('/', { + cy.visit('/inference', { onBeforeLoad(win) { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }, @@ -117,7 +117,7 @@ describe('Gradient Labels with non-default Y-axis metrics', () => { }; before(() => { - cy.visit('/', { + cy.visit('/inference', { onBeforeLoad(win) { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }, diff --git a/packages/app/cypress/e2e/high-contrast.cy.ts b/packages/app/cypress/e2e/high-contrast.cy.ts index 211f724..e64448a 100644 --- a/packages/app/cypress/e2e/high-contrast.cy.ts +++ b/packages/app/cypress/e2e/high-contrast.cy.ts @@ -3,7 +3,7 @@ describe('High Contrast Mode', () => { cy.window().then((win) => { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }); - cy.visit('/'); + cy.visit('/inference'); cy.get('[data-testid="scatter-graph"]').should('exist'); }); diff --git a/packages/app/cypress/e2e/inference-chart.cy.ts b/packages/app/cypress/e2e/inference-chart.cy.ts index e39cd83..93350d6 100644 --- a/packages/app/cypress/e2e/inference-chart.cy.ts +++ b/packages/app/cypress/e2e/inference-chart.cy.ts @@ -3,7 +3,7 @@ describe('Inference Chart', () => { cy.window().then((win) => { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }); - cy.visit('/'); + cy.visit('/inference'); }); it('renders the inference chart display wrapper', () => { diff --git a/packages/app/cypress/e2e/model-architecture.cy.ts b/packages/app/cypress/e2e/model-architecture.cy.ts index f143300..1d89d02 100644 --- a/packages/app/cypress/e2e/model-architecture.cy.ts +++ b/packages/app/cypress/e2e/model-architecture.cy.ts @@ -2,7 +2,7 @@ describe('Model Architecture Diagram', () => { before(() => { // Use desktop viewport to ensure all UI elements are visible cy.viewport(1280, 800); - cy.visit('/', { + cy.visit('/inference', { onBeforeLoad(win) { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }, diff --git a/packages/app/cypress/e2e/performance.cy.ts b/packages/app/cypress/e2e/performance.cy.ts index 99e89ac..2c9d57e 100644 --- a/packages/app/cypress/e2e/performance.cy.ts +++ b/packages/app/cypress/e2e/performance.cy.ts @@ -1,7 +1,7 @@ describe('Performance', () => { it('page loads quickly', () => { const startTime = Date.now(); - cy.visit('/'); + cy.visit('/inference'); cy.document().then(() => { const loadTime = Date.now() - startTime; const threshold = 5_000; @@ -18,7 +18,7 @@ describe('Performance', () => { }); it('no excessive layout shift issues', () => { - cy.visit('/'); + cy.visit('/inference'); cy.wait(5000); // Measure CLS using PerformanceObserver diff --git a/packages/app/cypress/e2e/sanity.cy.ts b/packages/app/cypress/e2e/sanity.cy.ts index bd83641..45ea8ca 100644 --- a/packages/app/cypress/e2e/sanity.cy.ts +++ b/packages/app/cypress/e2e/sanity.cy.ts @@ -3,7 +3,7 @@ describe('Page Load & Navigation', () => { before(() => { - cy.visit('/'); + cy.visit('/inference'); }); it('page loads with correct title', () => { @@ -23,13 +23,13 @@ describe('Page Load & Navigation', () => { }); // Re-visit to capture errors from a fresh load - cy.visit('/'); + cy.visit('/inference'); cy.get('[data-testid="scatter-graph"]').should('exist'); cy.wrap(errors).should('have.length', 0); }); it('page loads without 404 errors', () => { - cy.visit('/'); + cy.visit('/inference'); cy.get('[data-testid="scatter-graph"]').should('exist'); cy.get('h1').should('not.contain.text', '404'); cy.get('h1').should('not.contain.text', 'Not Found'); @@ -45,7 +45,7 @@ describe('Theme Toggle', () => { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); win.localStorage.setItem('theme', 'light'); }); - cy.visit('/'); + cy.visit('/inference'); cy.get('[data-testid="theme-toggle"]').click(); cy.get('html').should('have.class', 'dark'); cy.reload(); diff --git a/packages/app/cypress/e2e/tabs.cy.ts b/packages/app/cypress/e2e/tabs.cy.ts index 723f6e6..dbc0d15 100644 --- a/packages/app/cypress/e2e/tabs.cy.ts +++ b/packages/app/cypress/e2e/tabs.cy.ts @@ -3,7 +3,7 @@ describe('Chart Section Tabs — E2E', () => { cy.window().then((win) => { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }); - cy.visit('/'); + cy.visit('/inference'); }); it('updates the URL path when switching tabs', () => { @@ -28,7 +28,7 @@ describe('Chart Section Tabs — E2E', () => { it('shows mobile chart select dropdown on small viewport', () => { cy.viewport(375, 812); - cy.visit('/'); + cy.visit('/inference'); cy.get('[data-testid="mobile-chart-select"]').should('be.visible'); }); }); diff --git a/packages/app/cypress/e2e/throughput-calculator.cy.ts b/packages/app/cypress/e2e/throughput-calculator.cy.ts index b2d95d7..f961210 100644 --- a/packages/app/cypress/e2e/throughput-calculator.cy.ts +++ b/packages/app/cypress/e2e/throughput-calculator.cy.ts @@ -8,7 +8,7 @@ describe('TCO Calculator', () => { cy.window().then((win) => { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }); - cy.visit('/'); + cy.visit('/inference'); }); it('shows the TCO Calculator tab trigger', () => { diff --git a/packages/app/cypress/e2e/ttft-x-axis-toggle.cy.ts b/packages/app/cypress/e2e/ttft-x-axis-toggle.cy.ts index cfcfe69..e17a4af 100644 --- a/packages/app/cypress/e2e/ttft-x-axis-toggle.cy.ts +++ b/packages/app/cypress/e2e/ttft-x-axis-toggle.cy.ts @@ -3,7 +3,7 @@ describe('TTFT X-Axis Toggle (E2E chart)', () => { cy.window().then((win) => { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }); - cy.visit('/'); + cy.visit('/inference'); cy.get('[data-testid="chart-figure"]').should('have.length.at.least', 2); }); diff --git a/packages/app/cypress/e2e/url-params.cy.ts b/packages/app/cypress/e2e/url-params.cy.ts index ae1b71a..8e83425 100644 --- a/packages/app/cypress/e2e/url-params.cy.ts +++ b/packages/app/cypress/e2e/url-params.cy.ts @@ -39,7 +39,7 @@ describe('URL Parameter Persistence', () => { }); it('changing Y-axis metric via dropdown updates SVG axis label', () => { - cy.visit('/'); + cy.visit('/inference'); cy.get('[data-testid="scatter-graph"]') .first() @@ -59,7 +59,7 @@ describe('URL Parameter Persistence', () => { }); it('selecting a Y-axis metric updates the displayed value', () => { - cy.visit('/'); + cy.visit('/inference'); cy.get('[data-testid="yaxis-metric-selector"]').click(); cy.get('[role="option"]') .eq(1) @@ -73,7 +73,7 @@ describe('URL Parameter Persistence', () => { }); it('switching to energy metric updates SVG axis label to joules', () => { - cy.visit('/'); + cy.visit('/inference'); cy.get('[data-testid="scatter-graph"]').first().should('be.visible'); cy.get('[data-testid="yaxis-metric-selector"]').click(); diff --git a/packages/app/cypress/e2e/yaxis-metrics-render.cy.ts b/packages/app/cypress/e2e/yaxis-metrics-render.cy.ts index 8450f95..0fba82c 100644 --- a/packages/app/cypress/e2e/yaxis-metrics-render.cy.ts +++ b/packages/app/cypress/e2e/yaxis-metrics-render.cy.ts @@ -32,7 +32,7 @@ describe('Y-Axis Metrics All Render Data', () => { cy.window().then((win) => { win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); }); - cy.visit('/'); + cy.visit('/inference'); cy.get('[data-testid="scatter-graph"]') .first() .find('svg .dot-group') diff --git a/packages/app/src/app/[[...tab]]/page.tsx b/packages/app/src/app/[[...tab]]/page.tsx index acd78bf..69fed25 100644 --- a/packages/app/src/app/[[...tab]]/page.tsx +++ b/packages/app/src/app/[[...tab]]/page.tsx @@ -1,8 +1,9 @@ import type { Metadata } from 'next'; -import { notFound } from 'next/navigation'; +import { notFound, redirect } from 'next/navigation'; +import { LandingPage } from '@/components/landing/landing-page'; import { PageContent } from '@/components/page-content'; -import { TAB_META, VALID_TABS } from '@/lib/tab-meta'; +import { LANDING_META, TAB_META, VALID_TABS } from '@/lib/tab-meta'; import { SITE_URL } from '@semianalysisai/inferencex-constants'; export const dynamicParams = true; @@ -17,11 +18,30 @@ export async function generateMetadata({ params: Promise<{ tab?: string[] }>; }): Promise { const { tab } = await params; - const activeTab = tab?.[0] ?? 'inference'; + + // Landing page metadata + if (!tab || tab.length === 0) { + return { + title: LANDING_META.title, + description: LANDING_META.description, + alternates: { canonical: SITE_URL }, + openGraph: { + title: LANDING_META.title, + description: LANDING_META.description, + url: SITE_URL, + }, + twitter: { + title: LANDING_META.title, + description: LANDING_META.description, + }, + }; + } + + const activeTab = tab[0]; const meta = TAB_META[activeTab as keyof typeof TAB_META]; if (!meta) return {}; - const url = activeTab === 'inference' ? SITE_URL : `${SITE_URL}/${activeTab}`; + const url = `${SITE_URL}/${activeTab}`; return { title: meta.title, @@ -39,11 +59,36 @@ export async function generateMetadata({ }; } -export default async function Page({ params }: { params: Promise<{ tab?: string[] }> }) { +export default async function Page({ + params, + searchParams, +}: { + params: Promise<{ tab?: string[] }>; + searchParams: Promise>; +}) { const { tab } = await params; - const activeTab = tab?.[0] ?? 'inference'; - if (!VALID_TABS.includes(activeTab as (typeof VALID_TABS)[number]) || (tab && tab.length > 1)) { + // Landing page at / + if (!tab || tab.length === 0) { + // Backward compat: if / has chart query params, redirect to /inference with them + const search = await searchParams; + const hasChartParams = Object.keys(search).some( + (k) => k.startsWith('g_') || k.startsWith('i_') || k.startsWith('e_') || k.startsWith('r_'), + ); + if (hasChartParams) { + const qs = new URLSearchParams( + Object.entries(search).flatMap(([k, v]) => + Array.isArray(v) ? v.map((val) => [k, val]) : v != null ? [[k, v]] : [], + ), + ).toString(); + redirect(`/inference?${qs}`); + } + return ; + } + + const activeTab = tab[0]; + + if (!VALID_TABS.includes(activeTab as (typeof VALID_TABS)[number]) || tab.length > 1) { notFound(); } diff --git a/packages/app/src/components/favorites/FavoritePresetsDropdown.tsx b/packages/app/src/components/favorites/FavoritePresetsDropdown.tsx index f7e33f3..c1bcdbe 100644 --- a/packages/app/src/components/favorites/FavoritePresetsDropdown.tsx +++ b/packages/app/src/components/favorites/FavoritePresetsDropdown.tsx @@ -2,6 +2,7 @@ import { track } from '@/lib/analytics'; import { ChevronDown, Star } from 'lucide-react'; +import { useSearchParams } from 'next/navigation'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { sequenceToIslOsl } from '@semianalysisai/inferencex-constants'; @@ -190,6 +191,25 @@ export default function FavoritePresetsDropdown() { [activePresetId, applyPreset, clearPreset], ); + // Auto-apply preset from URL param (e.g., /inference?g_preset=gb200-vs-b200) + const searchParams = useSearchParams(); + const urlPresetAppliedRef = useRef(false); + useEffect(() => { + if (urlPresetAppliedRef.current) return; + const presetId = searchParams.get('g_preset'); + if (!presetId) return; + const preset = FAVORITE_PRESETS.find((p) => p.id === presetId); + if (preset) { + urlPresetAppliedRef.current = true; + applyPreset(preset); + track('preset_applied_from_url', { preset_id: presetId }); + // Strip g_preset from URL to keep it clean + const url = new URL(window.location.href); + url.searchParams.delete('g_preset'); + window.history.replaceState(null, '', url.pathname + url.search); + } + }, [searchParams, applyPreset]); + const activePreset = activePresetId ? FAVORITE_PRESETS.find((p) => p.id === activePresetId) : null; diff --git a/packages/app/src/components/favorites/favorite-presets.ts b/packages/app/src/components/favorites/favorite-presets.ts index b01477e..f60bf0b 100644 --- a/packages/app/src/components/favorites/favorite-presets.ts +++ b/packages/app/src/components/favorites/favorite-presets.ts @@ -219,4 +219,62 @@ export const FAVORITE_PRESETS: FavoritePreset[] = [ dateRangeMonths: 2, }, }, + // 7 — NVIDIA Generations + { + id: 'nvidia-generations', + title: 'H100 → H200 → B200 — NVIDIA Generations', + description: + 'Three generations of NVIDIA datacenter GPUs compared on DeepSeek R1 (1k/8k) at FP8.', + tags: ['DeepSeek', 'H100', 'H200', 'B200', 'FP8'], + category: 'comparison', + config: { + model: Model.DeepSeek_R1, + sequence: Sequence.OneK_EightK, + precisions: ['fp8'], + yAxisMetric: 'y_tpPerGpu', + hwFilter: ['h100', 'h200', 'b200'], + }, + }, + // 8 — GB300 vs GB200 + { + id: 'gb300-vs-gb200', + title: 'GB300 vs GB200 — NVL72 Comparison', + description: + 'GB300 NVL72 vs GB200 NVL72 disaggregated multi-node performance on DeepSeek R1 (1k/1k) at FP4.', + tags: ['DeepSeek', 'GB300', 'GB200', 'NVL72', 'FP4'], + category: 'comparison', + config: { + model: Model.DeepSeek_R1, + sequence: Sequence.OneK_OneK, + precisions: ['fp4'], + yAxisMetric: 'y_tpPerGpu', + hwFilter: ['gb300', 'gb200'], + }, + }, + // 9 — Cross-vendor: GB200 vs MI355X + { + id: 'gb200-vs-mi355x', + title: 'GB200 NVL72 vs MI355X — NVIDIA vs AMD', + description: + 'Cross-vendor flagship comparison on DeepSeek R1 (1k/8k) at FP8. Multi-node NVL72 vs single-node MI355X.', + tags: ['DeepSeek', 'GB200', 'MI355X', 'FP8', 'NVL72'], + category: 'comparison', + config: { + model: Model.DeepSeek_R1, + sequence: Sequence.OneK_EightK, + precisions: ['fp8'], + yAxisMetric: 'y_tpPerGpu', + hwFilter: ['gb200', 'mi355x'], + }, + }, +]; + +/** Curated presets shown on the landing page. */ +export const LANDING_PRESETS: FavoritePreset[] = [ + FAVORITE_PRESETS.find((p) => p.id === 'nvidia-generations')!, + FAVORITE_PRESETS.find((p) => p.id === 'amd-generations')!, + FAVORITE_PRESETS.find((p) => p.id === 'gb300-vs-gb200')!, + FAVORITE_PRESETS.find((p) => p.id === 'gb200-vs-mi355x')!, + FAVORITE_PRESETS.find((p) => p.id === 'gb200-vs-b200')!, + FAVORITE_PRESETS.find((p) => p.id === 'b200-vs-h200')!, ]; diff --git a/packages/app/src/components/header/header.tsx b/packages/app/src/components/header/header.tsx index 97644b1..bfb4b6d 100644 --- a/packages/app/src/components/header/header.tsx +++ b/packages/app/src/components/header/header.tsx @@ -12,7 +12,7 @@ import { GitHubStars } from './GithubStars'; const NAV_LINKS = [ { - href: '/', + href: '/inference', label: 'Dashboard', testId: 'nav-link-dashboard', event: 'header_dashboard_clicked', @@ -28,12 +28,12 @@ const NAV_LINKS = [ ] as const; function isActive(pathname: string, href: string): boolean { - if (href === '/') + if (href === '/inference') return ( - pathname === '/' || - (!pathname.startsWith('/media') && - !pathname.startsWith('/quotes') && - !pathname.startsWith('/blog')) + pathname !== '/' && + !pathname.startsWith('/media') && + !pathname.startsWith('/quotes') && + !pathname.startsWith('/blog') ); return pathname.startsWith(href); } @@ -72,9 +72,11 @@ export const Header = () => {
-

- InferenceX -

+ +

+ InferenceX +

+

(formerly InferenceMAX)

diff --git a/packages/app/src/components/landing/landing-page.tsx b/packages/app/src/components/landing/landing-page.tsx new file mode 100644 index 0000000..0c1e7eb --- /dev/null +++ b/packages/app/src/components/landing/landing-page.tsx @@ -0,0 +1,96 @@ +'use client'; + +import Link from 'next/link'; +import { ArrowRight } from 'lucide-react'; + +import { LANDING_PRESETS } from '@/components/favorites/favorite-presets'; +import { PresetCard } from '@/components/landing/preset-card'; +import { QuoteCarousel } from '@/components/quote-carousel'; +import { QUOTES } from '@/components/quotes/quotes-data'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { track } from '@/lib/analytics'; + +const LANDING_QUOTES = QUOTES.filter((q) => + [ + 'OpenAI', + 'Microsoft', + 'Together AI', + 'vLLM', + 'GPU Mode', + 'PyTorch Foundation', + 'Oracle', + 'CoreWeave', + 'Nebius', + 'Crusoe', + 'TensorWave', + 'SGLang', + 'WEKA', + 'Stanford', + 'Core42', + 'Meta Superintelligence Labs', + 'Hugging Face', + 'UC Berkeley', + ].includes(q.org), +); + +export function LandingPage() { + return ( +
+
+ {/* Hero */} +
+ +

+ Open Source Continuous Inference Benchmark +

+

+ Independent, vendor-neutral benchmarks comparing AI inference performance across GPUs + and frameworks. Trusted by operators of trillion-dollar-scale token factories. +

+ + {/* Quote Carousel */} +
+ +
+
+
+ + {/* CTA */} +
+ +
+ + {/* Curated Views */} +
+

Popular Comparisons

+
+ {LANDING_PRESETS.map((preset) => ( + + ))} +
+
+
+
+ ); +} diff --git a/packages/app/src/components/landing/preset-card.tsx b/packages/app/src/components/landing/preset-card.tsx new file mode 100644 index 0000000..fbda43b --- /dev/null +++ b/packages/app/src/components/landing/preset-card.tsx @@ -0,0 +1,40 @@ +'use client'; + +import Link from 'next/link'; +import { ArrowRight } from 'lucide-react'; + +import type { FavoritePreset } from '@/components/favorites/favorite-presets'; +import { Badge } from '@/components/ui/badge'; +import { track } from '@/lib/analytics'; + +export function PresetCard({ preset }: { preset: FavoritePreset }) { + return ( + + track('landing_preset_clicked', { + preset_id: preset.id, + preset_title: preset.title, + }) + } + className="group flex flex-col rounded-xl border border-border bg-card/90 p-4 md:p-5 transition-all duration-200 hover:border-brand/50 hover:bg-card" + > +

{preset.title}

+

+ {preset.description} +

+
+ {preset.tags.map((tag) => ( + + {tag} + + ))} +
+
+ View comparison + +
+ + ); +} diff --git a/packages/app/src/components/page-content.tsx b/packages/app/src/components/page-content.tsx index a11992a..0ec6b19 100644 --- a/packages/app/src/components/page-content.tsx +++ b/packages/app/src/components/page-content.tsx @@ -3,7 +3,6 @@ import { ChartTabs } from '@/components/chart-tabs'; import { ExportNudge } from '@/components/export-nudge'; import { GitHubStarModal } from '@/components/github-star-modal'; -import { IntroSection } from '@/components/intro-section'; import { StarNudge } from '@/components/star-nudge'; import { UnofficialRunProvider } from '@/components/unofficial-run-provider'; @@ -16,7 +15,6 @@ export function PageContent({ initialTab = 'inference' }: { initialTab?: string
-
diff --git a/packages/app/src/lib/tab-meta.ts b/packages/app/src/lib/tab-meta.ts index 5120898..42ae529 100644 --- a/packages/app/src/lib/tab-meta.ts +++ b/packages/app/src/lib/tab-meta.ts @@ -50,6 +50,12 @@ export const TAB_META: Record = }, }; +export const LANDING_META = { + title: `${SITE_NAME} — Open Source AI Inference Benchmark by ${AUTHOR_NAME}`, + description: + 'Compare AI inference performance across NVIDIA and AMD GPUs. Open-source benchmark covering GB300, B200, MI355X, and more.', +}; + const TITLE_SUFFIX = `${SITE_NAME} by ${AUTHOR_NAME}`; export function isValidTab(value: string): value is TabKey { From 0508916e8b29517497232bff6162905ffbaca4a5 Mon Sep 17 00:00:00 2001 From: functionstackx <47992694+functionstackx@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:53:50 -0400 Subject: [PATCH 02/12] fix: replace useSearchParams with window.location.search useSearchParams() requires a Suspense boundary during static generation, causing the build to fail. Since g_preset is only read once on mount, direct window.location.search access works and avoids the SSG bailout. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../favorites/FavoritePresetsDropdown.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/app/src/components/favorites/FavoritePresetsDropdown.tsx b/packages/app/src/components/favorites/FavoritePresetsDropdown.tsx index c1bcdbe..7870886 100644 --- a/packages/app/src/components/favorites/FavoritePresetsDropdown.tsx +++ b/packages/app/src/components/favorites/FavoritePresetsDropdown.tsx @@ -2,7 +2,6 @@ import { track } from '@/lib/analytics'; import { ChevronDown, Star } from 'lucide-react'; -import { useSearchParams } from 'next/navigation'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { sequenceToIslOsl } from '@semianalysisai/inferencex-constants'; @@ -192,11 +191,11 @@ export default function FavoritePresetsDropdown() { ); // Auto-apply preset from URL param (e.g., /inference?g_preset=gb200-vs-b200) - const searchParams = useSearchParams(); const urlPresetAppliedRef = useRef(false); useEffect(() => { if (urlPresetAppliedRef.current) return; - const presetId = searchParams.get('g_preset'); + const params = new URLSearchParams(window.location.search); + const presetId = params.get('g_preset'); if (!presetId) return; const preset = FAVORITE_PRESETS.find((p) => p.id === presetId); if (preset) { @@ -204,11 +203,11 @@ export default function FavoritePresetsDropdown() { applyPreset(preset); track('preset_applied_from_url', { preset_id: presetId }); // Strip g_preset from URL to keep it clean - const url = new URL(window.location.href); - url.searchParams.delete('g_preset'); - window.history.replaceState(null, '', url.pathname + url.search); + params.delete('g_preset'); + const qs = params.toString(); + window.history.replaceState(null, '', window.location.pathname + (qs ? `?${qs}` : '')); } - }, [searchParams, applyPreset]); + }, [applyPreset]); const activePreset = activePresetId ? FAVORITE_PRESETS.find((p) => p.id === activePresetId) From f82bcba95efc5f0c2141a4cc5bf0769588e453fb Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:11:16 -0500 Subject: [PATCH 03/12] feat: clean header navbar with hamburger menu on mobile --- packages/app/cypress/component/header.cy.tsx | 11 +- packages/app/src/components/header/header.tsx | 190 +++++++++++------- 2 files changed, 119 insertions(+), 82 deletions(-) diff --git a/packages/app/cypress/component/header.cy.tsx b/packages/app/cypress/component/header.cy.tsx index 18df15a..ff8510e 100644 --- a/packages/app/cypress/component/header.cy.tsx +++ b/packages/app/cypress/component/header.cy.tsx @@ -52,11 +52,12 @@ describe('Header', () => { cy.get('[data-testid="theme-toggle"]').should('be.visible'); }); - it('shows mobile nav links on small viewports', () => { + it('shows mobile hamburger menu on small viewports', () => { cy.viewport(375, 812); - cy.get('[data-testid="mobile-nav"]').should('be.visible'); - cy.get('[data-testid="mobile-nav"]').contains('Dashboard').should('be.visible'); - cy.get('[data-testid="mobile-nav"]').contains('Media').should('be.visible'); - cy.get('[data-testid="mobile-nav"]').contains('Supporters').should('be.visible'); + cy.get('[data-testid="mobile-menu-toggle"]').should('be.visible'); + cy.get('[data-testid="mobile-menu-toggle"]').click(); + cy.contains('Dashboard').should('be.visible'); + cy.contains('Media').should('be.visible'); + cy.contains('Supporters').should('be.visible'); }); }); diff --git a/packages/app/src/components/header/header.tsx b/packages/app/src/components/header/header.tsx index bfb4b6d..2e9bff0 100644 --- a/packages/app/src/components/header/header.tsx +++ b/packages/app/src/components/header/header.tsx @@ -3,6 +3,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { track } from '@/lib/analytics'; import { ModeToggle } from '@/components/ui/mode-toggle'; @@ -38,97 +39,132 @@ function isActive(pathname: string, href: string): boolean { return pathname.startsWith(href); } -const baseClasses = - 'items-center px-3 py-1.5 rounded-md border transition-colors text-sm font-medium'; -const inactiveClasses = - 'border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800'; -const activeClasses = 'bg-brand/10 border-brand/50 text-brand'; - export const Header = () => { const pathname = usePathname() ?? '/'; + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const menuRef = useRef(null); + + // Close menu on route change + useEffect(() => { + setMobileMenuOpen(false); + }, [pathname]); + + // Close menu on click outside + useEffect(() => { + if (!mobileMenuOpen) return; + const handler = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setMobileMenuOpen(false); + } + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, [mobileMenuOpen]); + + const toggleMenu = useCallback(() => { + setMobileMenuOpen((prev) => !prev); + track('header_mobile_menu_toggled'); + }, []); return ( -
-
-
-
-
- -

- InferenceX -

- -

- (formerly InferenceMAX) -

-
- By - - SemiAnalysis logo - -
-
-
- {NAV_LINKS.map(({ href, label, testId, event }) => ( - track(event)} - > - {label} - - ))} - - -
-
-
- {NAV_LINKS.map(({ href, label, event }) => ( +
+
+
+ {/* Brand */} + + InferenceX + + by + SemiAnalysis logo + + + + {/* Desktop nav */} + + + {/* Right side */} +
+ + + + {/* Mobile hamburger */} +
+ + {mobileMenuOpen && ( +
+ {NAV_LINKS.map(({ href, label, event }) => ( + track(event)} + > + {label} + + ))} +
+ )} +
From 917d756dc29917c2142fc64daa9d6c805050ee9d Mon Sep 17 00:00:00 2001 From: functionstackx <47992694+functionstackx@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:58:49 -0400 Subject: [PATCH 04/12] feat: add tagline to header, remove from landing page body Move "Open Source Continuous Inference Benchmark trusted by Operators of Trillion Dollar GigaWatt Scale Token Factories" into the header (visible on desktop). Remove the duplicate "Trusted by..." sentence from the landing page description. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/app/src/components/header/header.tsx | 4 ++++ packages/app/src/components/landing/landing-page.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/header/header.tsx b/packages/app/src/components/header/header.tsx index 2e9bff0..956c152 100644 --- a/packages/app/src/components/header/header.tsx +++ b/packages/app/src/components/header/header.tsx @@ -73,6 +73,10 @@ export const Header = () => { {/* Brand */} InferenceX + + Open Source Continuous Inference Benchmark trusted by Operators of Trillion Dollar + GigaWatt Scale Token Factories + by

Independent, vendor-neutral benchmarks comparing AI inference performance across GPUs - and frameworks. Trusted by operators of trillion-dollar-scale token factories. + and frameworks.

{/* Quote Carousel */} From f41c50f36dcaccfbd69cc49a4921614a22b6f3a3 Mon Sep 17 00:00:00 2001 From: functionstackx <47992694+functionstackx@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:59:29 -0400 Subject: [PATCH 05/12] fix: reduce padding between landing page sections Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/app/src/components/landing/landing-page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/landing/landing-page.tsx b/packages/app/src/components/landing/landing-page.tsx index 10b226a..f110c53 100644 --- a/packages/app/src/components/landing/landing-page.tsx +++ b/packages/app/src/components/landing/landing-page.tsx @@ -37,7 +37,7 @@ const LANDING_QUOTES = QUOTES.filter((q) => export function LandingPage() { return (
-
+
{/* Hero */}
From f0c83a463541a8f05f82d5f6d6fcfab3560803b2 Mon Sep 17 00:00:00 2001 From: functionstackx <47992694+functionstackx@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:01:10 -0400 Subject: [PATCH 06/12] fix: move tagline to landing page h2, remove from navbar header The full tagline belongs in the landing page

, not the navbar. Reverts the navbar tagline addition from the previous commit. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/app/src/components/header/header.tsx | 4 ---- packages/app/src/components/landing/landing-page.tsx | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/header/header.tsx b/packages/app/src/components/header/header.tsx index 956c152..2e9bff0 100644 --- a/packages/app/src/components/header/header.tsx +++ b/packages/app/src/components/header/header.tsx @@ -73,10 +73,6 @@ export const Header = () => { {/* Brand */} InferenceX - - Open Source Continuous Inference Benchmark trusted by Operators of Trillion Dollar - GigaWatt Scale Token Factories - by

- Open Source Continuous Inference Benchmark + Open Source Continuous Inference Benchmark trusted by Operators of Trillion Dollar + GigaWatt Scale Token Factories

Independent, vendor-neutral benchmarks comparing AI inference performance across GPUs From 1a6cecd98295815507370dc026293cc180bb66a1 Mon Sep 17 00:00:00 2001 From: functionstackx <47992694+functionstackx@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:02:51 -0400 Subject: [PATCH 07/12] fix: use brand color for dashboard CTA button Uses bg-brand so the button is blue in light mode and matches the brand color in dark mode. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/app/src/components/landing/landing-page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/landing/landing-page.tsx b/packages/app/src/components/landing/landing-page.tsx index 5217dd6..9e16cf1 100644 --- a/packages/app/src/components/landing/landing-page.tsx +++ b/packages/app/src/components/landing/landing-page.tsx @@ -72,7 +72,7 @@ export function LandingPage() {