From cdd387dc565f3169a871ad6266fd355c88b2ffd0 Mon Sep 17 00:00:00 2001 From: functionstackx <47992694+functionstackx@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:04:30 -0400 Subject: [PATCH 1/2] feat: replace overview SLO buttons with slider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a six-stop accessible range control that preserves overview URL state, cached navigation, analytics, and English/Chinese value text. Update overview E2E coverage for slider selection and filter preservation. 中文:将 Overview 页的 SLO 按钮组替换为六档无障碍滑块,同时保留 URL 状态、缓存导航、埋点以及中英文数值说明;更新 E2E 测试以覆盖滑块选择与筛选条件保留。 --- packages/app/cypress/e2e/overview.cy.ts | 95 +++++++----- .../overview/overview-scorecard.tsx | 55 ++----- .../overview/overview-tier-slider.tsx | 139 ++++++++++++++++++ 3 files changed, 212 insertions(+), 77 deletions(-) create mode 100644 packages/app/src/components/overview/overview-tier-slider.tsx diff --git a/packages/app/cypress/e2e/overview.cy.ts b/packages/app/cypress/e2e/overview.cy.ts index 14519064..c0f429df 100644 --- a/packages/app/cypress/e2e/overview.cy.ts +++ b/packages/app/cypress/e2e/overview.cy.ts @@ -37,6 +37,22 @@ const SCOPE_LINE = `${SCOPE_METRIC} · ${SCOPE_DIRECTION} · ${SOURCE_NOTE}`; const SCOPE_METRIC_ZH = '超大规模云(hyperscaler)成本'; const SCOPE_DIRECTION_ZH = '↓ 越低越好'; const SCOPE_LINE_ZH = `${SCOPE_METRIC_ZH} · ${SCOPE_DIRECTION_ZH} · ${SOURCE_NOTE_ZH}`; +const OVERVIEW_TIERS = [30, 50, 75, 100, 150, 200] as const; + +function selectOverviewTier(tier: (typeof OVERVIEW_TIERS)[number]) { + const targetIndex = OVERVIEW_TIERS.indexOf(tier); + return cy.get('[data-testid="overview-tier-slider"]').then(([slider]) => { + const view = slider.ownerDocument.defaultView; + if (view === null) throw new Error('Slider has no window'); + const nativeValueSetter = Object.getOwnPropertyDescriptor( + view.HTMLInputElement.prototype, + 'value', + )?.set; + if (nativeValueSetter === undefined) throw new Error('Range input has no native value setter'); + nativeValueSetter.call(slider, String(targetIndex)); + slider.dispatchEvent(new view.Event('input', { bubbles: true })); + }); +} function expectNoHorizontalOverflow() { cy.document().then((doc) => { @@ -137,14 +153,14 @@ describe('Overview page', () => { 'preserved'; }); - cy.get('[data-testid="overview-tier-switcher"]').contains('a', '75').click(); + selectOverviewTier(75); cy.wait('@overviewJson'); cy.location('search', { timeout: 15_000 }).should('eq', '?tier=75'); cy.window().its('__overviewNavigationSentinel').should('eq', 'preserved'); - cy.get('[data-testid="overview-tier-switcher"]').contains('a', '50').click(); + selectOverviewTier(50); cy.location('search').should('eq', ''); - cy.get('[data-testid="overview-tier-switcher"]').contains('a', '75').click(); + selectOverviewTier(75); cy.location('search').should('eq', '?tier=75'); cy.then(() => { expect(jsonRequests, 'one request; both visited selections are cached').to.equal(1); @@ -179,7 +195,7 @@ describe('Overview page', () => { cy.viewport(1280, 900); cy.visit('/overview'); cy.intercept('GET', '**/api/v1/overview*').as('overviewJson'); - cy.get('[data-testid="overview-tier-switcher"]').contains('a', '75').click(); + selectOverviewTier(75); cy.wait('@overviewJson'); cy.location('search').should('eq', '?tier=75'); @@ -205,7 +221,7 @@ describe('Overview page', () => { cy.viewport(1280, 900); cy.visit('/overview'); - cy.get('[data-testid="overview-tier-switcher"]').contains('a', '75').click(); + selectOverviewTier(75); cy.get('[data-testid="overview-engine-scope-switcher"]') .find('[data-overview-engine-scope="all"]') .click(); @@ -240,7 +256,7 @@ describe('Overview page', () => { ); }); - cy.get('[data-testid="overview-tier-switcher"]').contains('a', '75').click(); + selectOverviewTier(75); cy.location('search').should('eq', '?tier=75&models=all'); cy.get('[data-testid="overview-desktop-model"][data-model="gpt-oss-120b"]').should('exist'); @@ -284,22 +300,21 @@ describe('Overview page', () => { platform('b200').find('[data-testid="overview-cost-delta"]').should('exist'); }); - cy.get('[data-testid="overview-tier-switcher"]') - .contains('a', '100') - .should('have.attr', 'href', '/overview?tier=100&ref=b300'); + selectOverviewTier(100); + cy.location('search').should('eq', '?tier=100&ref=b300'); cy.get('[data-testid="overview-engine-scope-switcher"]') .find('[data-overview-engine-scope="all"]') - .should('have.attr', 'href', '/overview?engine=all&ref=b300'); + .should('have.attr', 'href', '/overview?tier=100&engine=all&ref=b300'); cy.get('[data-overview-comparison="history"]').should( 'have.attr', 'href', - '/overview?ref=b300&compare=30d', + '/overview?tier=100&ref=b300&compare=30d', ); cy.get('[data-testid="language-toggle"]') - .should('have.attr', 'href', '/zh/overview?ref=b300') + .should('have.attr', 'href', '/zh/overview?tier=100&ref=b300') .click(); cy.location('pathname').should('eq', '/zh/overview'); - cy.location('search').should('eq', '?ref=b300'); + cy.location('search').should('eq', '?tier=100&ref=b300'); cy.get('[data-overview-comparison="hardware"]').should('contain.text', '对比 B300'); }); @@ -345,17 +360,16 @@ describe('Overview page', () => { cy.get('[data-testid="overview-desktop-matrix"] thead').should('contain.text', 'B200'); cy.get('[data-testid="overview-desktop-matrix"] thead').should('not.contain.text', 'Reference'); - cy.get('[data-testid="overview-tier-switcher"]') - .contains('a', '100') - .should('have.attr', 'href', '/overview?tier=100&compare=30d'); + selectOverviewTier(100); + cy.location('search').should('eq', '?tier=100&compare=30d'); cy.get('[data-testid="overview-engine-scope-switcher"]') .find('[data-overview-engine-scope="all"]') - .should('have.attr', 'href', '/overview?engine=all&compare=30d'); + .should('have.attr', 'href', '/overview?tier=100&engine=all&compare=30d'); cy.get('[data-testid="language-toggle"]') - .should('have.attr', 'href', '/zh/overview?compare=30d') + .should('have.attr', 'href', '/zh/overview?tier=100&compare=30d') .click(); cy.location('pathname').should('eq', '/zh/overview'); - cy.location('search').should('eq', '?compare=30d'); + cy.location('search').should('eq', '?tier=100&compare=30d'); cy.get('[data-testid="overview-comparison-switcher"]') .should('have.attr', 'aria-label', '对比方式') .within(() => { @@ -533,10 +547,7 @@ describe('Overview page', () => { ).click(); cy.location('search').should('eq', '?engine=all'); desktopModel('GLM-5.2').find('[data-testid="overview-pair-missing"]').should('have.length', 5); - cy.get('[data-testid="overview-tier-switcher"]') - .contains('a', '100') - .should('have.attr', 'href', '/overview?tier=100&engine=all') - .click(); + selectOverviewTier(100); cy.location('search').should('eq', '?tier=100&engine=all'); cy.get('[data-testid="overview-engine-scope-switcher"]') @@ -956,7 +967,7 @@ describe('Overview page', () => { .should('not.match', /∞\s*%/); }); - it('re-renders the whole matrix at the service level the URL names, via plain links', () => { + it('re-renders the whole matrix from the six-stop SLO slider', () => { cy.viewport(1280, 900); cy.visit('/overview'); @@ -966,22 +977,35 @@ describe('Overview page', () => { .and('contain.text', 'SLO'); cy.get('body').should('not.contain.text', 'Service level'); cy.get('[data-testid="overview-tier-switcher"]').within(() => { - cy.get('[aria-current="page"]').should('have.text', '50'); - // 30 / 75 / 100 / 150 / 200 link out; the active 50 is inert text. - cy.get('a').should('have.length', 5); - cy.contains('a', '30').should('have.attr', 'href', '/overview?tier=30'); - cy.contains('a', '150').should('have.attr', 'href', '/overview?tier=150'); - cy.contains('a', '200').should('have.attr', 'href', '/overview?tier=200'); - cy.contains('a', '100').should('have.attr', 'href', '/overview?tier=100').click(); + cy.get('a, button').should('not.exist'); + cy.get('[data-tier-option]').should('have.length', 6); + cy.get('[data-tier-option="50"]').should('have.attr', 'data-selected', 'true'); + cy.get('[data-testid="overview-tier-slider"]') + .should('have.attr', 'min', '0') + .and('have.attr', 'max', '5') + .and('have.attr', 'step', '1') + .and('have.value', '1') + .and('have.attr', 'data-tier', '50') + .and('have.attr', 'aria-valuetext', '50 tok/s/user'); }); + selectOverviewTier(100); + cy.location('search').should('eq', '?tier=100'); // The metric line never repeats the tier — the switcher states it. cy.get('[data-testid="overview-scope"]').should('have.text', SCOPE_LINE); cy.get('[data-testid="overview-tier-switcher"]').within(() => { - cy.get('[aria-current="page"]').should('have.text', '100'); - cy.contains('a', '50').should('have.attr', 'href', '/overview'); + cy.get('[data-tier-option="100"]').should('have.attr', 'data-selected', 'true'); + cy.get('[data-testid="overview-tier-slider"]') + .should('have.attr', 'type', 'range') + .should('have.value', '3') + .and('have.attr', 'data-tier', '100') + .and('have.attr', 'aria-valuetext', '100 tok/s/user'); }); + selectOverviewTier(75); + cy.location('search').should('eq', '?tier=75'); + selectOverviewTier(100); + cy.location('search').should('eq', '?tier=100'); desktopModel('Qwen-3.5-397B-A17B', SINGLE_TURN).within(() => { platform('b200').should('contain.text', '$0.124').and('contain.text', 'FP8'); @@ -1328,7 +1352,10 @@ describe('Overview page', () => { .and('contain.text', 'SLO'); cy.get('body').should('not.contain.text', '服务档位'); cy.get('[data-testid="overview-tier-switcher"]').within(() => { - cy.contains('a', '50').should('have.attr', 'href', '/zh/overview'); + cy.get('[data-testid="overview-tier-slider"]') + .should('have.attr', 'data-tier', '100') + .and('have.attr', 'aria-valuetext', '100 tok/s/用户'); + cy.get('[data-tier-option]').should('have.length', 6); }); }); }); diff --git a/packages/app/src/components/overview/overview-scorecard.tsx b/packages/app/src/components/overview/overview-scorecard.tsx index a52afc8b..42054358 100644 --- a/packages/app/src/components/overview/overview-scorecard.tsx +++ b/packages/app/src/components/overview/overview-scorecard.tsx @@ -1,7 +1,6 @@ import { OVERVIEW_DEFAULT_REFERENCE_HARDWARE, OVERVIEW_HARDWARE, - OVERVIEW_TIERS, overviewHardwareLabel, type OverviewComparisonMode, type OverviewEngineScope, @@ -18,13 +17,13 @@ import { detailHref, overviewEngineScopeHref, overviewHref, - overviewTierHref, } from '@/lib/overview-links'; import { OverviewDetailLink } from './overview-detail-link'; import { OverviewHistoryDetailLink } from './overview-history-detail-link'; import { OverviewNavLink } from './overview-nav-link'; import { OverviewReferenceSelect } from './overview-reference-select'; +import { OverviewTierSlider } from './overview-tier-slider'; export type OverviewLocale = 'en' | 'zh'; @@ -803,8 +802,7 @@ export function MobileOverviewList({ ); } -/** Every option remains a copyable server-rendered URL; ordinary clicks use a - * soft App Router transition and the displayed tier is never a self-link. */ +/** The six benchmarked service levels are exposed as discrete slider stops. */ export function OverviewTierSwitcher({ tier, engineScope, @@ -822,46 +820,17 @@ export function OverviewTierSwitcher({ locale: OverviewLocale; strings: OverviewStrings; }) { - const optionClass = 'inline-flex min-h-11 items-center px-3 tabular-nums'; return ( - + ); } diff --git a/packages/app/src/components/overview/overview-tier-slider.tsx b/packages/app/src/components/overview/overview-tier-slider.tsx new file mode 100644 index 00000000..0d7cdfde --- /dev/null +++ b/packages/app/src/components/overview/overview-tier-slider.tsx @@ -0,0 +1,139 @@ +'use client'; + +import { type ChangeEvent, useEffect, useRef, useState } from 'react'; + +import { track } from '@/lib/analytics'; +import { + OVERVIEW_TIERS, + type OverviewComparisonMode, + type OverviewEngineScope, + type OverviewModelScope, + type OverviewReferenceHardware, + type OverviewTier, +} from '@/lib/overview-data'; +import { overviewTierHref } from '@/lib/overview-links'; + +import { useOverviewNavigation } from './overview-navigation'; + +interface OverviewTierSliderProps { + tier: OverviewTier; + engineScope: OverviewEngineScope; + comparisonMode: OverviewComparisonMode; + referenceHardware: OverviewReferenceHardware; + modelScope: OverviewModelScope; + locale: 'en' | 'zh'; + label: string; + unit: string; +} + +function tierIndex(tier: OverviewTier): number { + return OVERVIEW_TIERS.indexOf(tier); +} + +export function OverviewTierSlider({ + tier, + engineScope, + comparisonMode, + referenceHardware, + modelScope, + locale, + label, + unit, +}: OverviewTierSliderProps) { + const navigation = useOverviewNavigation(); + const pointerActive = useRef(false); + const committedIndex = useRef(tierIndex(tier)); + const [selectedIndex, setSelectedIndex] = useState(() => tierIndex(tier)); + + useEffect(() => { + const index = tierIndex(tier); + committedIndex.current = index; + setSelectedIndex(index); + }, [tier]); + + const selectedTier = OVERVIEW_TIERS[selectedIndex]; + const progress = (selectedIndex / (OVERVIEW_TIERS.length - 1)) * 100; + + const commit = (index: number) => { + const nextTier = OVERVIEW_TIERS[index]; + if (index === committedIndex.current) return; + committedIndex.current = index; + + track('overview_selector_changed', { + control: 'tier', + value: String(nextTier), + }); + navigation.push( + overviewTierHref( + locale, + nextTier, + engineScope, + comparisonMode, + referenceHardware, + modelScope, + ), + ['tier'], + ); + }; + + const handleChange = (event: ChangeEvent) => { + const nextIndex = Number(event.currentTarget.value); + setSelectedIndex(nextIndex); + if (!pointerActive.current) commit(nextIndex); + }; + + return ( + + ); +} From e02d71a328cb1a39239865125ea10e53106810c2 Mon Sep 17 00:00:00 2001 From: functionstackx <47992694+functionstackx@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:14:19 -0400 Subject: [PATCH 2/2] feat: add stop ridges to overview SLO slider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render contrasting ridges at each of the six SLO stops and extend overview E2E coverage for filled, selected, and unfilled ridge states. 中文:为 Overview SLO 滑块的六个档位添加对比鲜明的刻度脊线,并扩展 E2E 测试,覆盖已填充、当前选中与未填充状态。 --- packages/app/cypress/e2e/overview.cy.ts | 7 ++ .../overview/overview-tier-slider.tsx | 86 +++++++++++++------ 2 files changed, 66 insertions(+), 27 deletions(-) diff --git a/packages/app/cypress/e2e/overview.cy.ts b/packages/app/cypress/e2e/overview.cy.ts index c0f429df..b0ccb786 100644 --- a/packages/app/cypress/e2e/overview.cy.ts +++ b/packages/app/cypress/e2e/overview.cy.ts @@ -980,6 +980,10 @@ describe('Overview page', () => { cy.get('a, button').should('not.exist'); cy.get('[data-tier-option]').should('have.length', 6); cy.get('[data-tier-option="50"]').should('have.attr', 'data-selected', 'true'); + cy.get('[data-testid="overview-tier-ridge"]').should('have.length', 6); + cy.get('[data-tier-ridge="30"]').should('have.attr', 'data-state', 'filled'); + cy.get('[data-tier-ridge="50"]').should('have.attr', 'data-state', 'selected'); + cy.get('[data-tier-ridge="75"]').should('have.attr', 'data-state', 'unfilled'); cy.get('[data-testid="overview-tier-slider"]') .should('have.attr', 'min', '0') .and('have.attr', 'max', '5') @@ -996,6 +1000,9 @@ describe('Overview page', () => { cy.get('[data-testid="overview-scope"]').should('have.text', SCOPE_LINE); cy.get('[data-testid="overview-tier-switcher"]').within(() => { cy.get('[data-tier-option="100"]').should('have.attr', 'data-selected', 'true'); + cy.get('[data-tier-ridge="75"]').should('have.attr', 'data-state', 'filled'); + cy.get('[data-tier-ridge="100"]').should('have.attr', 'data-state', 'selected'); + cy.get('[data-tier-ridge="150"]').should('have.attr', 'data-state', 'unfilled'); cy.get('[data-testid="overview-tier-slider"]') .should('have.attr', 'type', 'range') .should('have.value', '3') diff --git a/packages/app/src/components/overview/overview-tier-slider.tsx b/packages/app/src/components/overview/overview-tier-slider.tsx index 0d7cdfde..9b35d0ec 100644 --- a/packages/app/src/components/overview/overview-tier-slider.tsx +++ b/packages/app/src/components/overview/overview-tier-slider.tsx @@ -90,33 +90,65 @@ export function OverviewTierSlider({ > {label}
- { - pointerActive.current = true; - }} - onPointerUp={(event) => { - pointerActive.current = false; - commit(Number(event.currentTarget.value)); - }} - onPointerCancel={() => { - pointerActive.current = false; - setSelectedIndex(tierIndex(tier)); - }} - className="h-1.5 w-full cursor-pointer appearance-none rounded-full bg-border accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background [&::-moz-range-progress]:h-1.5 [&::-moz-range-progress]:rounded-full [&::-moz-range-progress]:bg-foreground [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-background [&::-moz-range-thumb]:bg-foreground [&::-moz-range-thumb]:shadow-sm [&::-moz-range-thumb]:active:cursor-grabbing [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-background [&::-webkit-slider-thumb]:bg-foreground [&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-thumb]:active:cursor-grabbing" - style={{ - background: `linear-gradient(to right, var(--foreground) 0%, var(--foreground) ${progress}%, var(--border) ${progress}%, var(--border) 100%)`, - }} - /> +
+ { + pointerActive.current = true; + }} + onPointerUp={(event) => { + pointerActive.current = false; + commit(Number(event.currentTarget.value)); + }} + onPointerCancel={() => { + pointerActive.current = false; + setSelectedIndex(tierIndex(tier)); + }} + className="absolute top-1/2 z-10 h-1.5 w-full -translate-y-1/2 cursor-pointer appearance-none rounded-full bg-border accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background [&::-moz-range-progress]:h-1.5 [&::-moz-range-progress]:rounded-full [&::-moz-range-progress]:bg-foreground [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-background [&::-moz-range-thumb]:bg-foreground [&::-moz-range-thumb]:shadow-sm [&::-moz-range-thumb]:active:cursor-grabbing [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-background [&::-webkit-slider-thumb]:bg-foreground [&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-thumb]:active:cursor-grabbing" + style={{ + background: `linear-gradient(to right, var(--foreground) 0%, var(--foreground) ${progress}%, var(--border) ${progress}%, var(--border) 100%)`, + }} + /> + +