From 53d7312f9413beb64c439f4b0786bb492a992ded Mon Sep 17 00:00:00 2001 From: functionstackx <47992694+functionstackx@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:51:56 -0400 Subject: [PATCH] feat(overview): add presentation and comparison controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a focused presentation view, customizable platform columns, and 1-week, 30-day, and 2-month development-speed windows with bilingual UI and shareable URLs. 中文:新增专注的演示视图、可自定义的平台列,以及 1 周、30 天和 2 个月的开发速度区间;中英文界面与可分享 URL 同步支持。 --- packages/app/cypress/e2e/overview.cy.ts | 64 +++- .../app/src/app/(dashboard)/overview/page.tsx | 4 + .../app/src/app/api/v1/overview/route.test.ts | 9 +- packages/app/src/app/api/v1/overview/route.ts | 4 + .../src/app/zh/(dashboard)/overview/page.tsx | 4 + .../overview/overview-hardware-select.tsx | 83 +++++ .../overview/overview-navigation.test.tsx | 2 + .../src/components/overview/overview-page.tsx | 99 +++++- .../overview/overview-scorecard.tsx | 294 ++++++++++++------ .../app/src/components/ui/multi-select.tsx | 3 + .../app/src/lib/overview-data.server.test.ts | 27 ++ packages/app/src/lib/overview-data.server.ts | 14 +- packages/app/src/lib/overview-data.test.ts | 26 ++ packages/app/src/lib/overview-data.ts | 48 ++- packages/app/src/lib/overview-links.test.ts | 18 ++ packages/app/src/lib/overview-links.ts | 45 ++- packages/app/src/lib/url-state.ts | 2 +- 17 files changed, 631 insertions(+), 115 deletions(-) create mode 100644 packages/app/src/components/overview/overview-hardware-select.tsx diff --git a/packages/app/cypress/e2e/overview.cy.ts b/packages/app/cypress/e2e/overview.cy.ts index 145190649..2047c1439 100644 --- a/packages/app/cypress/e2e/overview.cy.ts +++ b/packages/app/cypress/e2e/overview.cy.ts @@ -158,7 +158,7 @@ describe('Overview page', () => { cy.location('search', { timeout: 15_000 }).should('eq', '?tier=75&engine=all'); cy.window().its('__overviewNavigationSentinel').should('eq', 'preserved'); - cy.get('[data-overview-comparison="history"]').click(); + cy.get('[data-overview-comparison="30d"]').click(); cy.wait('@overviewJson'); cy.location('search', { timeout: 15_000 }).should('eq', '?tier=75&engine=all&compare=30d'); cy.window().its('__overviewNavigationSentinel').should('eq', 'preserved'); @@ -290,7 +290,7 @@ describe('Overview page', () => { cy.get('[data-testid="overview-engine-scope-switcher"]') .find('[data-overview-engine-scope="all"]') .should('have.attr', 'href', '/overview?engine=all&ref=b300'); - cy.get('[data-overview-comparison="history"]').should( + cy.get('[data-overview-comparison="30d"]').should( 'have.attr', 'href', '/overview?ref=b300&compare=30d', @@ -311,14 +311,14 @@ describe('Overview page', () => { cy.get('[data-testid="overview-methodology"]').should('contain.text', 'GB200 NVL72 baseline'); }); - it('switches between B200 and 30-day comparison without losing page state', () => { + it('switches among hardware and development-speed windows without losing page state', () => { cy.viewport(1280, 900); cy.visit('/overview'); cy.get('[data-testid="overview-page"]') .children('[data-testid="overview-comparison-switcher"]') .should('have.length', 1) - .and('have.class', 'justify-center'); + .and('have.class', 'items-center'); cy.get('[data-testid="overview-comparison-switcher"]') .should('have.attr', 'aria-label', 'Compare') .within(() => { @@ -331,15 +331,21 @@ describe('Overview page', () => { expect(style.borderBottomWidth).to.equal('2px'); expect(style.backgroundColor).to.match(/rgba\(0, 0, 0, 0\)|transparent/); }); - cy.get('[data-overview-comparison="history"]') + cy.get('[data-overview-comparison="7d"]') + .should('have.attr', 'href', '/overview?compare=7d') + .and('have.text', '1 week'); + cy.get('[data-overview-comparison="60d"]') + .should('have.attr', 'href', '/overview?compare=60d') + .and('have.text', '2 months'); + cy.get('[data-overview-comparison="30d"]') .should('have.attr', 'href', '/overview?compare=30d') - .and('have.text', '30-day change') + .and('have.text', '30 days') .click(); }); cy.location('search').should('eq', '?compare=30d'); cy.get('[data-testid="overview-comparison-switcher"]') - .find('[data-overview-comparison="history"]') + .find('[data-overview-comparison="30d"]') .should('have.attr', 'aria-current', 'true') .and('match', 'span'); cy.get('[data-testid="overview-desktop-matrix"] thead').should('contain.text', 'B200'); @@ -360,14 +366,54 @@ describe('Overview page', () => { .should('have.attr', 'aria-label', '对比方式') .within(() => { cy.get('[data-overview-comparison="hardware"]').should('have.text', '对比 B200'); - cy.get('[data-overview-comparison="history"]') + cy.get('[data-overview-comparison="30d"]') .should('have.attr', 'aria-current', 'true') - .and('have.text', '30 天变化'); + .and('have.text', '30 天'); }); cy.contains('当前成本及其相对 30–60 天前最近一次有效平台结果的变化。').should('exist'); cy.contains('缺少有效 30 天对比的平台仅显示当前成本。').should('exist'); }); + it('customizes platform columns and opens a focused presentation view', () => { + cy.viewport(1280, 900); + cy.visit('/overview?compare=60d'); + + cy.get('[data-testid="overview-hardware-select"]') + .should('have.attr', 'aria-label', 'Visible platform columns') + .click(); + cy.contains('[role="option"]', 'B300').click(); + + cy.location('search').should('eq', '?compare=60d&hw=b200%2Cmi355x%2Cgb200%2Cgb300'); + cy.get('[data-testid="overview-desktop-matrix"] thead th').then(($headers) => { + expect([...$headers].map((header) => header.textContent?.trim())).to.deep.equal([ + 'Model · Scenario', + 'B200', + 'MI355X', + 'GB200 NVL72', + 'GB300 NVL72', + ]); + }); + cy.get('[data-testid="overview-platform"][data-hardware="b300"]').should('not.exist'); + + cy.get('[data-testid="overview-presentation-toggle"]') + .should('have.text', 'Presentation view') + .click(); + cy.get('[data-testid="overview-page"]') + .should('have.attr', 'data-presentation', 'true') + .and('have.class', 'fixed'); + cy.get('[data-testid="overview-desktop-matrix"]').should('be.visible'); + cy.get('[data-testid="overview-mobile-list"]').should('not.be.visible'); + cy.get('body').should('have.css', 'overflow', 'hidden'); + cy.get('[data-testid="overview-presentation-toggle"]').should( + 'have.text', + 'Exit presentation view', + ); + + cy.get('body').type('{esc}'); + cy.get('[data-testid="overview-page"]').should('have.attr', 'data-presentation', 'false'); + cy.get('body').should('not.have.css', 'overflow', 'hidden'); + }); + it('compares each platform with its own validated result from 30–60 days earlier', () => { cy.viewport(1280, 900); cy.visit('/overview?compare=30d'); diff --git a/packages/app/src/app/(dashboard)/overview/page.tsx b/packages/app/src/app/(dashboard)/overview/page.tsx index a0c044466..44a3401a2 100644 --- a/packages/app/src/app/(dashboard)/overview/page.tsx +++ b/packages/app/src/app/(dashboard)/overview/page.tsx @@ -7,6 +7,8 @@ import { enAlternates } from '@/lib/i18n'; import { resolveOverviewComparisonMode, resolveOverviewEngineScope, + resolveOverviewHardware, + resolveOverviewHistoryDays, resolveOverviewModelScope, resolveOverviewReferenceHardware, resolveOverviewTier, @@ -47,6 +49,8 @@ export default async function OverviewPage({ searchParams }: Props) { resolveOverviewComparisonMode(sp.compare), resolveOverviewReferenceHardware(sp.ref), resolveOverviewModelScope(sp.models), + resolveOverviewHistoryDays(sp.compare), + resolveOverviewHardware(sp.hw), ); return ; } diff --git a/packages/app/src/app/api/v1/overview/route.test.ts b/packages/app/src/app/api/v1/overview/route.test.ts index f0415cf37..4ceb91743 100644 --- a/packages/app/src/app/api/v1/overview/route.test.ts +++ b/packages/app/src/app/api/v1/overview/route.test.ts @@ -37,12 +37,15 @@ describe('GET /api/v1/overview', () => { mockGetOverviewPageData.mockResolvedValueOnce(data); const response = await GET( - request('/api/v1/overview?tier=75&engine=all&compare=30d&ref=b300&models=all'), + request('/api/v1/overview?tier=75&engine=all&compare=60d&ref=b300&models=all&hw=b300,gb300'), ); expect(response.status).toBe(200); expect(await response.json()).toEqual(data); - expect(mockGetOverviewPageData).toHaveBeenCalledWith(75, 'all', 'history', 'b300', 'all'); + expect(mockGetOverviewPageData).toHaveBeenCalledWith(75, 'all', 'history', 'b300', 'all', 60, [ + 'b300', + 'gb300', + ]); expect(mockCachedJson).toHaveBeenCalledWith(data); }); @@ -59,6 +62,8 @@ describe('GET /api/v1/overview', () => { 'hardware', 'b200', 'default', + 30, + ['b200', 'mi355x', 'b300', 'gb200', 'gb300'], ); }); diff --git a/packages/app/src/app/api/v1/overview/route.ts b/packages/app/src/app/api/v1/overview/route.ts index ba6819160..c98b37aa6 100644 --- a/packages/app/src/app/api/v1/overview/route.ts +++ b/packages/app/src/app/api/v1/overview/route.ts @@ -4,6 +4,8 @@ import { cachedJson } from '@/lib/api-cache'; import { resolveOverviewComparisonMode, resolveOverviewEngineScope, + resolveOverviewHardware, + resolveOverviewHistoryDays, resolveOverviewModelScope, resolveOverviewReferenceHardware, resolveOverviewTier, @@ -22,6 +24,8 @@ export async function GET(request: NextRequest) { resolveOverviewComparisonMode(params.get('compare') ?? undefined), resolveOverviewReferenceHardware(params.get('ref') ?? undefined), resolveOverviewModelScope(params.get('models') ?? undefined), + resolveOverviewHistoryDays(params.get('compare') ?? undefined), + resolveOverviewHardware(params.get('hw') ?? undefined), ); return cachedJson(data); } catch (error) { diff --git a/packages/app/src/app/zh/(dashboard)/overview/page.tsx b/packages/app/src/app/zh/(dashboard)/overview/page.tsx index 524d15db1..1bb6bb0d2 100644 --- a/packages/app/src/app/zh/(dashboard)/overview/page.tsx +++ b/packages/app/src/app/zh/(dashboard)/overview/page.tsx @@ -7,6 +7,8 @@ import { ZH_OG_LOCALE, zhAlternates } from '@/lib/i18n'; import { resolveOverviewComparisonMode, resolveOverviewEngineScope, + resolveOverviewHardware, + resolveOverviewHistoryDays, resolveOverviewModelScope, resolveOverviewReferenceHardware, resolveOverviewTier, @@ -48,6 +50,8 @@ export default async function ZhOverviewPage({ searchParams }: Props) { resolveOverviewComparisonMode(sp.compare), resolveOverviewReferenceHardware(sp.ref), resolveOverviewModelScope(sp.models), + resolveOverviewHistoryDays(sp.compare), + resolveOverviewHardware(sp.hw), ); return ; } diff --git a/packages/app/src/components/overview/overview-hardware-select.tsx b/packages/app/src/components/overview/overview-hardware-select.tsx new file mode 100644 index 000000000..f1c0e0f15 --- /dev/null +++ b/packages/app/src/components/overview/overview-hardware-select.tsx @@ -0,0 +1,83 @@ +'use client'; + +import { MultiSelect } from '@/components/ui/multi-select'; +import { track } from '@/lib/analytics'; +import { + OVERVIEW_HARDWARE, + overviewHardwareLabel, + type OverviewComparisonMode, + type OverviewEngineScope, + type OverviewHardware, + type OverviewHistoryDays, + type OverviewModelScope, + type OverviewReferenceHardware, + type OverviewTier, +} from '@/lib/overview-data'; +import { overviewHref } from '@/lib/overview-links'; + +import { useOverviewNavigation } from './overview-navigation'; +import type { OverviewLocale } from './overview-scorecard'; + +export function OverviewHardwareSelect({ + locale, + tier, + engineScope, + comparisonMode, + referenceHardware, + modelScope, + historyDays, + value, + ariaLabel, +}: { + locale: OverviewLocale; + tier: OverviewTier; + engineScope: OverviewEngineScope; + comparisonMode: OverviewComparisonMode; + referenceHardware: OverviewReferenceHardware; + modelScope: OverviewModelScope; + historyDays: OverviewHistoryDays; + value: OverviewHardware[]; + ariaLabel: string; +}) { + const navigation = useOverviewNavigation(); + const options = OVERVIEW_HARDWARE.map((hardware) => ({ + value: hardware, + label: overviewHardwareLabel(hardware), + })); + + return ( +
+ { + const selected = OVERVIEW_HARDWARE.filter((hardware) => nextValues.includes(hardware)); + const href = overviewHref( + locale, + tier, + engineScope, + comparisonMode, + referenceHardware, + modelScope, + historyDays, + selected, + ); + track('overview_selector_changed', { + control: 'hardware', + value: selected.join(','), + }); + navigation.push(href, ['hw']); + }} + /> +
+ ); +} diff --git a/packages/app/src/components/overview/overview-navigation.test.tsx b/packages/app/src/components/overview/overview-navigation.test.tsx index e4b4e64c4..bf31738f0 100644 --- a/packages/app/src/components/overview/overview-navigation.test.tsx +++ b/packages/app/src/components/overview/overview-navigation.test.tsx @@ -30,6 +30,8 @@ function pageData(tier: OverviewTier): OverviewPageData { comparisonMode: 'hardware', referenceHardware: 'b200', modelScope: 'default', + historyDays: 30, + visibleHardware: ['b200', 'mi355x', 'b300', 'gb200', 'gb300'], historicalWindow: null, }; } diff --git a/packages/app/src/components/overview/overview-page.tsx b/packages/app/src/components/overview/overview-page.tsx index c5764c5dc..be0ecbd45 100644 --- a/packages/app/src/components/overview/overview-page.tsx +++ b/packages/app/src/components/overview/overview-page.tsx @@ -1,7 +1,12 @@ 'use client'; +import { Maximize2, Minimize2 } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import { ExternalLinkIcon } from '@/components/ui/external-link-icon'; +import { track } from '@/lib/analytics'; import type { OverviewPageData } from '@/lib/overview-data'; import { overviewHref } from '@/lib/overview-links'; @@ -18,6 +23,7 @@ import { type OverviewLocale, } from './overview-scorecard'; import { OverviewNavigationProvider, useOverviewNavigation } from './overview-navigation'; +import { OverviewHardwareSelect } from './overview-hardware-select'; /** The SemiAnalysis AI Cloud TCO model behind `HW_REGISTRY.costh`. */ const OVERVIEW_SOURCE_HREF = 'https://semianalysis.com/ai-cloud-tco-model/'; @@ -38,6 +44,8 @@ export function OverviewPageContent({ data, locale }: OverviewPageProps) { data.comparisonMode, data.referenceHardware, data.modelScope, + data.historyDays, + data.visibleHardware, )} > @@ -47,12 +55,35 @@ export function OverviewPageContent({ data, locale }: OverviewPageProps) { function OverviewPageBody({ locale }: { locale: OverviewLocale }) { const { data } = useOverviewNavigation(); + const [presentation, setPresentation] = useState(false); const strings = OVERVIEW_STRINGS[locale]; const formatters = overviewFormatters(locale); + useEffect(() => { + if (!presentation) return; + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setPresentation(false); + }; + document.addEventListener('keydown', onKeyDown); + return () => { + document.body.style.overflow = previousOverflow; + document.removeEventListener('keydown', onKeyDown); + }; + }, [presentation]); + return ( -
- +
+
{/* Two rows at every width: the title, then the metric it is measured in and where that measure comes from. */} @@ -105,6 +136,8 @@ function OverviewPageBody({ locale }: { locale: OverviewLocale }) { comparisonMode={data.comparisonMode} referenceHardware={data.referenceHardware} modelScope={data.modelScope} + historyDays={data.historyDays} + visibleHardware={data.visibleHardware} locale={locale} strings={strings} /> @@ -114,6 +147,8 @@ function OverviewPageBody({ locale }: { locale: OverviewLocale }) { comparisonMode={data.comparisonMode} referenceHardware={data.referenceHardware} modelScope={data.modelScope} + historyDays={data.historyDays} + visibleHardware={data.visibleHardware} locale={locale} strings={strings} /> @@ -127,10 +162,61 @@ function OverviewPageBody({ locale }: { locale: OverviewLocale }) { tier={data.tier} referenceHardware={data.referenceHardware} modelScope={data.modelScope} + historyDays={data.historyDays} + visibleHardware={data.visibleHardware} locale={locale} strings={strings} + hidden={presentation} /> +
+
+ {presentation ? ( +
+

{strings.title}

+

+ {data.comparisonMode === 'history' + ? `${strings.developmentSpeedLabel}: ${strings.comparisonOptions[data.historyDays]}` + : strings.hardwareComparisonLabel( + data.models[0]?.platforms.find( + (platform) => platform.hardware === data.referenceHardware, + )?.hardwareLabel ?? data.referenceHardware, + )} +

+
+ ) : null} +
+ {strings.hardwareColumnsLabel} + +
+
+ +
+ {/* Official-only summary; uploaded runs remain in the linked dashboard. */} {/* Clipped on phones for the rounded corners; visible from xl so the desktop matrix header can stick to the page as it scrolls. */} @@ -142,6 +228,9 @@ function OverviewPageBody({ locale }: { locale: OverviewLocale }) { strings={strings} comparisonMode={data.comparisonMode} referenceHardware={data.referenceHardware} + historyDays={data.historyDays} + visibleHardware={data.visibleHardware} + presentation={presentation} /> diff --git a/packages/app/src/components/overview/overview-scorecard.tsx b/packages/app/src/components/overview/overview-scorecard.tsx index a52afc8b5..22e292481 100644 --- a/packages/app/src/components/overview/overview-scorecard.tsx +++ b/packages/app/src/components/overview/overview-scorecard.tsx @@ -1,10 +1,13 @@ import { OVERVIEW_DEFAULT_REFERENCE_HARDWARE, OVERVIEW_HARDWARE, + OVERVIEW_HISTORY_WINDOWS, OVERVIEW_TIERS, overviewHardwareLabel, type OverviewComparisonMode, type OverviewEngineScope, + type OverviewHardware, + type OverviewHistoryDays, type OverviewHistoricalComparison, type OverviewModelScope, type OverviewModelSummary, @@ -48,14 +51,21 @@ export const OVERVIEW_STRINGS = { }, comparisonNavLabel: 'Compare', comparisonOptions: { - history: '30-day change', + 7: '1 week', + 30: '30 days', + 60: '2 months', }, + developmentSpeedLabel: 'Development speed', + hardwareColumnsLabel: 'Platforms', + hardwareColumnsAria: 'Visible platform columns', + presentationView: 'Presentation view', + exitPresentationView: 'Exit presentation view', hardwareComparisonLabel: (reference: string) => `vs ${reference}`, referenceSelectorAria: 'Reference hardware', caption: 'Cost per million total tokens from each platform’s best observed serving envelope for the scenario shown with each model.', - historyCaption: - 'Current cost and change versus the latest validated platform result 30–60 days earlier.', + historyCaption: (days: number) => + `Current cost and change versus the latest validated platform result ${days}–${days * 2} days earlier.`, modelHeader: 'Model · Scenario', scenarioLabels: { single_turn_8k1k: '8K/1K', @@ -92,7 +102,8 @@ export const OVERVIEW_STRINGS = { `${pct} ${cheaper ? 'cheaper' : 'more expensive'} than this platform’s ${baselineDate} result`, historicalEvenAria: (baselineDate: string) => `About the same cost as this platform’s ${baselineDate} result`, - historyCellStateLegend: 'Platforms without a valid 30-day comparison show current cost only.', + historyCellStateLegend: (days: number) => + `Platforms without a valid ${days}-day comparison show current cost only.`, referenceHeader: 'Reference', modelScopeNavLabel: 'Inactive models', modelScopeShow: 'Show deprecated & maintenance-mode models', @@ -119,12 +130,20 @@ export const OVERVIEW_STRINGS = { }, comparisonNavLabel: '对比方式', comparisonOptions: { - history: '30 天变化', + 7: '1 周', + 30: '30 天', + 60: '2 个月', }, + developmentSpeedLabel: '开发速度', + hardwareColumnsLabel: '平台', + hardwareColumnsAria: '可见平台列', + presentationView: '演示视图', + exitPresentationView: '退出演示视图', hardwareComparisonLabel: (reference: string) => `对比 ${reference}`, referenceSelectorAria: '基准硬件', caption: '按各模型标注的场景,基于各平台最佳观测服务包络线计算每百万总 token 成本。', - historyCaption: '当前成本及其相对 30–60 天前最近一次有效平台结果的变化。', + historyCaption: (days: number) => + `当前成本及其相对 ${days}–${days * 2} 天前最近一次有效平台结果的变化。`, modelHeader: '模型 · 场景', scenarioLabels: { single_turn_8k1k: '8K/1K', @@ -159,7 +178,7 @@ export const OVERVIEW_STRINGS = { historicalDeltaAria: (pct: string, cheaper: boolean, baselineDate: string) => `比该平台 ${baselineDate} 的结果${cheaper ? '便宜' : '昂贵'} ${pct}`, historicalEvenAria: (baselineDate: string) => `与该平台 ${baselineDate} 的结果成本基本持平`, - historyCellStateLegend: '缺少有效 30 天对比的平台仅显示当前成本。', + historyCellStateLegend: (days: number) => `缺少有效 ${days} 天对比的平台仅显示当前成本。`, referenceHeader: '基准', modelScopeNavLabel: '非活跃模型', modelScopeShow: '显示已弃用与维护模式模型', @@ -635,6 +654,9 @@ interface SurfaceProps { strings: OverviewStrings; comparisonMode: OverviewComparisonMode; referenceHardware: OverviewReferenceHardware; + historyDays: OverviewHistoryDays; + visibleHardware: OverviewHardware[]; + presentation?: boolean; } export function DesktopOverviewMatrix({ @@ -644,14 +666,20 @@ export function DesktopOverviewMatrix({ strings, comparisonMode, referenceHardware, + historyDays, + visibleHardware, + presentation = false, }: SurfaceProps) { - const platforms = models[0]?.platforms ?? []; + const visible = new Set(visibleHardware); + const platforms = (models[0]?.platforms ?? []).filter((platform) => + visible.has(platform.hardware as OverviewHardware), + ); const referenceLabel = overviewHardwareLabel(referenceHardware); return ( -
+
@@ -708,24 +736,26 @@ export function DesktopOverviewMatrix({ )} - {model.platforms.map((platform) => ( - - ))} + {model.platforms + .filter((platform) => visible.has(platform.hardware as OverviewHardware)) + .map((platform) => ( + + ))} ))} @@ -741,10 +771,16 @@ export function MobileOverviewList({ strings, comparisonMode, referenceHardware, + visibleHardware, + presentation = false, }: SurfaceProps) { const referenceLabel = overviewHardwareLabel(referenceHardware); + const visible = new Set(visibleHardware); return ( -
    +
      {models.map((model) => (
    • - {model.platforms.map((platform) => ( -
      - visible.has(platform.hardware as OverviewHardware)) + .map((platform) => ( +
      - {platform.hardwareLabel} - - -
      - ))} + + {platform.hardwareLabel} + + +
      + ))}
      {comparisonMode === 'history' ? null : (
- {comparisonMode === 'history' ? strings.historyCaption : strings.caption} + {comparisonMode === 'history' ? strings.historyCaption(historyDays) : strings.caption}
- - + +