From 63cca8f8ced41c4bd53aa6950c85e2b55c162cf4 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Fri, 14 Aug 2026 13:28:05 -0700 Subject: [PATCH 1/2] feat(overview): merge the matrix footer into one bar and dim stale data in flight --- packages/app/cypress/e2e/overview.cy.ts | 10 ++- .../overview/overview-navigation.tsx | 14 ++- .../src/components/overview/overview-page.tsx | 88 ++++++++++++------- .../overview/overview-presentation.tsx | 18 +++- .../overview/overview-scorecard.tsx | 70 ++++++++------- 5 files changed, 124 insertions(+), 76 deletions(-) diff --git a/packages/app/cypress/e2e/overview.cy.ts b/packages/app/cypress/e2e/overview.cy.ts index a304a200..9ec7d4f9 100644 --- a/packages/app/cypress/e2e/overview.cy.ts +++ b/packages/app/cypress/e2e/overview.cy.ts @@ -255,10 +255,12 @@ describe('Overview page', () => { cy.viewport(1280, 900); cy.visit('/zh/overview?models=all'); - cy.get('[data-testid="overview-model-scope-toggle"]').should( - 'contain.text', - '隐藏已弃用与维护模式模型', - ); + // The chip carries the short label; the full sentence stays on the + // accessible name and hover title. + cy.get('[data-testid="overview-model-scope-toggle"]') + .should('contain.text', '隐藏停用模型') + .find('[data-overview-model-scope="default"]') + .should('have.attr', 'aria-label', '隐藏已弃用与维护模式模型'); desktopModel('gpt-oss-120b') .find('[data-testid="overview-model-category-badge"]') .should('contain.text', '已弃用'); diff --git a/packages/app/src/components/overview/overview-navigation.tsx b/packages/app/src/components/overview/overview-navigation.tsx index 12c904ef..839bb42b 100644 --- a/packages/app/src/components/overview/overview-navigation.tsx +++ b/packages/app/src/components/overview/overview-navigation.tsx @@ -18,6 +18,8 @@ import { mergeOverviewControlHref, type OverviewSearchKey } from '@/lib/overview interface OverviewNavigationValue { data: OverviewPageData; + /** True while `data` still shows the previous selection during a fetch. */ + pending: boolean; prefetch: (targetHref: string, keys: readonly OverviewSearchKey[]) => void; resolve: (targetHref: string, keys: readonly OverviewSearchKey[]) => string; push: (targetHref: string, keys: readonly OverviewSearchKey[]) => void; @@ -36,6 +38,7 @@ export function OverviewNavigationProvider({ }) { const router = useRouter(); const [data, setData] = useState(initialData); + const [pending, setPending] = useState(false); const [pendingHref, setPendingHref] = useState(initialHref); const pendingHrefRef = useRef(initialHref); const committedHrefRef = useRef(initialHref); @@ -47,8 +50,8 @@ export function OverviewNavigationProvider({ const cached = dataCacheRef.current.get(href); if (cached !== undefined) return Promise.resolve(cached); - const pending = requestCacheRef.current.get(href); - if (pending !== undefined) return pending; + const inFlight = requestCacheRef.current.get(href); + if (inFlight !== undefined) return inFlight; const url = new URL(href, window.location.origin); const request = fetch(`/api/v1/overview${url.search}`, { @@ -71,6 +74,7 @@ export function OverviewNavigationProvider({ const navigationId = ++navigationIdRef.current; pendingHrefRef.current = href; setPendingHref(href); + setPending(true); if (updateHistory) { History.prototype.pushState.call(window.history, window.history.state, '', href); notifyClientSearchChange(href); @@ -84,9 +88,11 @@ export function OverviewNavigationProvider({ History.prototype.replaceState.call(window.history, window.history.state, '', href); } setData(nextData); + setPending(false); }) .catch(() => { if (navigationId !== navigationIdRef.current) return; + setPending(false); if (updateHistory) { History.prototype.replaceState.call( window.history, @@ -111,6 +117,7 @@ export function OverviewNavigationProvider({ pendingHrefRef.current = initialHref; setPendingHref(initialHref); setData(initialData); + setPending(false); }, [initialData, initialHref]); useEffect(() => { @@ -135,6 +142,7 @@ export function OverviewNavigationProvider({ const value = useMemo( () => ({ data, + pending, resolve, prefetch: (targetHref, keys) => { const href = mergeOverviewControlHref(pendingHrefRef.current, targetHref, keys); @@ -145,7 +153,7 @@ export function OverviewNavigationProvider({ commit(href, true); }, }), - [commit, data, load, resolve], + [commit, data, load, pending, resolve], ); return ( diff --git a/packages/app/src/components/overview/overview-page.tsx b/packages/app/src/components/overview/overview-page.tsx index 5abe3747..78f95561 100644 --- a/packages/app/src/components/overview/overview-page.tsx +++ b/packages/app/src/components/overview/overview-page.tsx @@ -180,10 +180,18 @@ function OverviewControlRow({ locale }: { locale: OverviewLocale }) { ); if (!presenting) { + // Same three-column skeleton as the presenting toolbar below: the tabs + // keep the matrix centre and Present anchors the right edge as an action, + // instead of trailing the tabs and reading as a third view. return ( -
- {views} - +
+
+
+ {views} +
+
+ +
); } @@ -257,7 +265,7 @@ function OverviewControlRow({ locale }: { locale: OverviewLocale }) { /** The half of the page that goes fullscreen: the view tabs and the matrix. */ function OverviewMatrixSection({ locale }: { locale: OverviewLocale }) { - const { data } = useOverviewNavigation(); + const { data, pending } = useOverviewNavigation(); const { presenting } = useOverviewPresentation(); const strings = OVERVIEW_STRINGS[locale]; const formatters = overviewFormatters(locale); @@ -269,7 +277,14 @@ function OverviewMatrixSection({ locale }: { locale: OverviewLocale }) { {/* 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. */} - + {/* The 150ms delay keeps cache hits and fast responses from flickering; + only a fetch still in flight past it dims the stale matrix. */} + )} {presenting ? null : ( - <> + /* One footer bar instead of three stacked link rows: the notes keep + the left edge, the scope chips keep the right, and the card ends + on a single rule. */ +
- {data.comparisonMode === 'history' ? ( - + {data.comparisonMode === 'history' ? ( + + ) : ( + + )} + - ) : ( - - )} - - +
+
)} diff --git a/packages/app/src/components/overview/overview-presentation.tsx b/packages/app/src/components/overview/overview-presentation.tsx index eb795a3b..b0dc2a35 100644 --- a/packages/app/src/components/overview/overview-presentation.tsx +++ b/packages/app/src/components/overview/overview-presentation.tsx @@ -210,8 +210,24 @@ export function OverviewPresentToggle({ strings }: { strings: OverviewStrings }) aria-pressed={presenting} aria-label={presenting ? strings.presentExitAria : strings.presentEnterAria} title={strings.presentShortcutHint} - className="inline-flex min-h-11 items-center rounded-md border border-border/60 px-3 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" + className="inline-flex min-h-11 items-center gap-x-1.5 rounded-md border border-border/60 px-3 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" > + {presenting ? strings.presentExit : strings.presentEnter} ); diff --git a/packages/app/src/components/overview/overview-scorecard.tsx b/packages/app/src/components/overview/overview-scorecard.tsx index 8cd366ad..181deb7f 100644 --- a/packages/app/src/components/overview/overview-scorecard.tsx +++ b/packages/app/src/components/overview/overview-scorecard.tsx @@ -1103,15 +1103,12 @@ export function OverviewComparisonSwitcher({ } /** - * Where a scope toggle is drawn. `section` is the underlined sentence beneath - * the matrix; `toolbar` is the button that replaces it while presenting, where - * the sentence does not fit. + * Where a scope toggle is drawn. `section` is the chip in the footer bar + * beneath the matrix; `toolbar` is the same chip riding the deck toolbar while + * presenting, where the count badge would be noise at projection size. */ type OverviewScopeToggleVariant = 'section' | 'toolbar'; -const SCOPE_SENTENCE_CLASS = - 'inline-flex min-h-11 items-center text-muted-foreground underline decoration-dotted underline-offset-4 transition-colors hover:text-foreground hover:decoration-solid'; - /** * Matches Exit, so the right end of the deck toolbar reads as one row of * actions. Deliberately not filled-when-engaged: these labels name the click @@ -1120,6 +1117,19 @@ const SCOPE_SENTENCE_CLASS = const SCOPE_CHIP_CLASS = 'inline-flex min-h-11 items-center whitespace-nowrap rounded-md border border-border/60 px-3 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground'; +/** The count the chip's short label elides; the full sentence stays on the + * accessible name, so the badge is presentation only. */ +function ScopeChipCount({ count }: { count: number }) { + return ( + + ); +} + export function OverviewModelScopeToggle({ modelScope, tier, @@ -1169,15 +1179,11 @@ export function OverviewModelScopeToggle({ )} analytics={{ control: 'models', value: target }} searchKeys={['models', 'rows', 'hwrows']} - aria-label={variant === 'toolbar' ? sentence : undefined} - title={variant === 'toolbar' ? sentence : undefined} - className={variant === 'toolbar' ? SCOPE_CHIP_CLASS : SCOPE_SENTENCE_CLASS} + aria-label={sentence} + title={sentence} + className={SCOPE_CHIP_CLASS} > - {variant === 'toolbar' - ? modelScope === 'all' - ? strings.modelScopeChipHide - : strings.modelScopeChipShow - : sentence} + {modelScope === 'all' ? strings.modelScopeChipHide : strings.modelScopeChipShow} ); if (variant === 'toolbar') return link; @@ -1185,7 +1191,7 @@ export function OverviewModelScopeToggle({ @@ -1238,15 +1244,12 @@ export function OverviewRowScopeToggle({ )} analytics={{ control: 'rows', value: target }} searchKeys={['rows']} - aria-label={variant === 'toolbar' ? sentence : undefined} - title={variant === 'toolbar' ? sentence : undefined} - className={variant === 'toolbar' ? SCOPE_CHIP_CLASS : SCOPE_SENTENCE_CLASS} + aria-label={sentence} + title={sentence} + className={SCOPE_CHIP_CLASS} > - {variant === 'toolbar' - ? rowScope === 'all' - ? strings.rowScopeChipHide - : strings.rowScopeChipShow - : sentence} + {rowScope === 'all' ? strings.rowScopeChipHide : strings.rowScopeChipShow} + {variant === 'section' ? : null} ); if (variant === 'toolbar') return link; @@ -1254,7 +1257,7 @@ export function OverviewRowScopeToggle({ @@ -1306,15 +1309,14 @@ export function OverviewHardwareRowScopeToggle({ )} analytics={{ control: 'hwrows', value: target }} searchKeys={['hwrows']} - aria-label={variant === 'toolbar' ? sentence : undefined} - title={variant === 'toolbar' ? sentence : undefined} - className={variant === 'toolbar' ? SCOPE_CHIP_CLASS : SCOPE_SENTENCE_CLASS} + aria-label={sentence} + title={sentence} + className={SCOPE_CHIP_CLASS} > - {variant === 'toolbar' - ? hardwareRowScope === 'all' - ? strings.hardwareRowScopeChipHide - : strings.hardwareRowScopeChipShow - : sentence} + {hardwareRowScope === 'all' + ? strings.hardwareRowScopeChipHide + : strings.hardwareRowScopeChipShow} + {variant === 'section' ? : null} ); if (variant === 'toolbar') return link; @@ -1322,7 +1324,7 @@ export function OverviewHardwareRowScopeToggle({ @@ -1342,7 +1344,7 @@ export function OverviewMethodology({ return (
{comparisonMode === 'history' ?

{strings.historyCaption}

: null}

From 0385d4b866b51d34f259d9b4286fc647f4d49abf Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Fri, 14 Aug 2026 14:40:37 -0700 Subject: [PATCH 2/2] test(overview): cover the pending lifecycle across success and failure --- .../overview/overview-navigation.test.tsx | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/overview/overview-navigation.test.tsx b/packages/app/src/components/overview/overview-navigation.test.tsx index 3cb1f318..9374022a 100644 --- a/packages/app/src/components/overview/overview-navigation.test.tsx +++ b/packages/app/src/components/overview/overview-navigation.test.tsx @@ -41,7 +41,12 @@ function pageData(tier: OverviewTier): OverviewPageData { function Probe() { const navigation = useOverviewNavigation(); selectTier = () => navigation.push('/overview?tier=75', ['tier']); - return {navigation.data.tier}; + return ( + <> + {navigation.data.tier} + {String(navigation.pending)} + + ); } function renderProvider(data: OverviewPageData, href: string) { @@ -98,4 +103,59 @@ describe('OverviewNavigationProvider', () => { expect(container.querySelector('[data-testid="tier"]')?.textContent).toBe('100'); }); + + it('reports pending only while a selector response is in flight', async () => { + let resolveSelectorRequest: ((response: Response) => void) | undefined; + vi.stubGlobal( + 'fetch', + vi.fn( + () => + new Promise((resolve) => { + resolveSelectorRequest = resolve; + }), + ), + ); + + renderProvider(pageData(50), '/overview'); + const pending = () => container.querySelector('[data-testid="pending"]')?.textContent; + expect(pending()).toBe('false'); + + act(() => selectTier?.()); + expect(pending()).toBe('true'); + + await act(async () => { + resolveSelectorRequest?.(Response.json(pageData(75))); + await Promise.resolve(); + }); + + expect(pending()).toBe('false'); + expect(container.querySelector('[data-testid="tier"]')?.textContent).toBe('75'); + }); + + it('clears pending when the selector request fails', async () => { + let rejectSelectorRequest: ((reason: Error) => void) | undefined; + vi.stubGlobal( + 'fetch', + vi.fn( + () => + new Promise((_resolve, reject) => { + rejectSelectorRequest = reject; + }), + ), + ); + + renderProvider(pageData(50), '/overview'); + act(() => selectTier?.()); + expect(container.querySelector('[data-testid="pending"]')?.textContent).toBe('true'); + + await act(async () => { + rejectSelectorRequest?.(new Error('offline')); + await Promise.resolve(); + }); + + // The failed selection falls back to a router navigation; the matrix must + // not be left permanently dimmed over the data it still shows. + expect(container.querySelector('[data-testid="pending"]')?.textContent).toBe('false'); + expect(container.querySelector('[data-testid="tier"]')?.textContent).toBe('50'); + }); });