diff --git a/packages/app/cypress/component/header.cy.tsx b/packages/app/cypress/component/header.cy.tsx index 18df15a..bc0302f 100644 --- a/packages/app/cypress/component/header.cy.tsx +++ b/packages/app/cypress/component/header.cy.tsx @@ -28,7 +28,7 @@ describe('Header', () => { it('shows Dashboard nav link', () => { cy.get('[data-testid="nav-link-dashboard"]').should('be.visible'); - cy.get('[data-testid="nav-link-dashboard"]').should('have.attr', 'href', '/'); + cy.get('[data-testid="nav-link-dashboard"]').should('have.attr', 'href', '/inference'); }); it('shows Media nav link', () => { @@ -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/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..b1d49a1 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())); }, @@ -18,8 +18,8 @@ describe('Favorite Presets', () => { cy.get('[data-testid="favorites-panel"]').should('be.visible'); }); - it('shows all 6 preset cards', () => { - cy.get('[data-testid^="favorite-preset-"]').should('have.length', 6); + it('shows all 9 preset cards', () => { + cy.get('[data-testid^="favorite-preset-"]').should('have.length', 9); }); it('each preset card has a title and description', () => { 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..d54483c 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,16 +23,16 @@ 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'); + cy.contains('404').should('not.exist'); + cy.contains('Not Found').should('not.exist'); }); }); @@ -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..a09155d 100644 --- a/packages/app/cypress/e2e/url-params.cy.ts +++ b/packages/app/cypress/e2e/url-params.cy.ts @@ -11,7 +11,7 @@ describe('URL Parameter Persistence', () => { }); it('page loads without error with unknown params', () => { - cy.visit('/?unknown_param=test'); + cy.visit('/inference?unknown_param=test'); cy.get('[data-testid="inference-chart-display"]').should('exist'); }); @@ -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/app/globals.css b/packages/app/src/app/globals.css index 8ab14ee..f727332 100644 --- a/packages/app/src/app/globals.css +++ b/packages/app/src/app/globals.css @@ -353,3 +353,18 @@ .star-button-glow { animation: star-glow 2s ease-in-out infinite; } + +/* CTA button glow animation - brand colored */ +@keyframes cta-glow { + 0%, + 100% { + box-shadow: none; + } + 50% { + box-shadow: 0 0 20px 6px oklch(65% 0.18 250 / 60%); + } +} + +.cta-button-glow { + animation: cta-glow 2s ease-in-out infinite; +} diff --git a/packages/app/src/components/favorites/FavoritePresetsDropdown.tsx b/packages/app/src/components/favorites/FavoritePresetsDropdown.tsx index f7e33f3..7870886 100644 --- a/packages/app/src/components/favorites/FavoritePresetsDropdown.tsx +++ b/packages/app/src/components/favorites/FavoritePresetsDropdown.tsx @@ -190,6 +190,25 @@ export default function FavoritePresetsDropdown() { [activePresetId, applyPreset, clearPreset], ); + // Auto-apply preset from URL param (e.g., /inference?g_preset=gb200-vs-b200) + const urlPresetAppliedRef = useRef(false); + useEffect(() => { + if (urlPresetAppliedRef.current) return; + 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) { + urlPresetAppliedRef.current = true; + applyPreset(preset); + track('preset_applied_from_url', { preset_id: presetId }); + // Strip g_preset from URL to keep it clean + params.delete('g_preset'); + const qs = params.toString(); + window.history.replaceState(null, '', window.location.pathname + (qs ? `?${qs}` : '')); + } + }, [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..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'; @@ -12,7 +13,7 @@ import { GitHubStars } from './GithubStars'; const NAV_LINKS = [ { - href: '/', + href: '/inference', label: 'Dashboard', testId: 'nav-link-dashboard', event: 'header_dashboard_clicked', @@ -28,105 +29,142 @@ 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); } -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} + + ))} +
+ )} +
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..ee5fdbe --- /dev/null +++ b/packages/app/src/components/landing/landing-page.tsx @@ -0,0 +1,97 @@ +'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 trusted by Operators of Trillion Dollar + GigaWatt Scale Token Factories +

+

+ Independent, vendor-neutral benchmarks comparing AI inference performance across GPUs + and frameworks. +

+ + {/* 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 {