Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/app/cypress/e2e/overview.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', '已弃用');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ function pageData(tier: OverviewTier): OverviewPageData {
function Probe() {
const navigation = useOverviewNavigation();
selectTier = () => navigation.push('/overview?tier=75', ['tier']);
return <output data-testid="tier">{navigation.data.tier}</output>;
return (
<>
<output data-testid="tier">{navigation.data.tier}</output>
<output data-testid="pending">{String(navigation.pending)}</output>
</>
);
}

function renderProvider(data: OverviewPageData, href: string) {
Expand Down Expand Up @@ -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<Response>((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<Response>((_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');
});
});
14 changes: 11 additions & 3 deletions packages/app/src/components/overview/overview-navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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}`, {
Expand All @@ -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);
Expand All @@ -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,
Expand All @@ -111,6 +117,7 @@ export function OverviewNavigationProvider({
pendingHrefRef.current = initialHref;
setPendingHref(initialHref);
setData(initialData);
setPending(false);
}, [initialData, initialHref]);

useEffect(() => {
Expand All @@ -135,6 +142,7 @@ export function OverviewNavigationProvider({
const value = useMemo<OverviewNavigationValue>(
() => ({
data,
pending,
resolve,
prefetch: (targetHref, keys) => {
const href = mergeOverviewControlHref(pendingHrefRef.current, targetHref, keys);
Expand All @@ -145,7 +153,7 @@ export function OverviewNavigationProvider({
commit(href, true);
},
}),
[commit, data, load, resolve],
[commit, data, load, pending, resolve],
);

return (
Expand Down
88 changes: 54 additions & 34 deletions packages/app/src/components/overview/overview-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1.5">
{views}
<OverviewPresentToggle strings={strings} />
<div className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1.5 md:grid md:grid-cols-[1fr_auto_1fr]">
<div className="hidden md:block" />
<div className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1.5 md:justify-self-center">
{views}
</div>
<div className="md:justify-self-end">
<OverviewPresentToggle strings={strings} />
</div>
</div>
);
}
Expand Down Expand Up @@ -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);
Expand All @@ -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. */}
<Card className="overflow-hidden p-0 md:p-0 xl:overflow-visible">
{/* The 150ms delay keeps cache hits and fast responses from flickering;
only a fetch still in flight past it dims the stale matrix. */}
<Card
data-pending={pending}
className={`overflow-hidden p-0 transition-opacity duration-200 md:p-0 xl:overflow-visible ${
pending ? 'opacity-60 delay-150' : 'opacity-100'
Comment thread
edwingao28 marked this conversation as resolved.
}`}
>
<DesktopOverviewMatrix
models={data.models}
locale={locale}
Expand All @@ -291,47 +306,52 @@ function OverviewMatrixSection({ locale }: { locale: OverviewLocale }) {
/>
)}
{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. */
<div className="flex flex-col gap-x-6 gap-y-2 border-t border-border/50 px-4 py-3 lg:flex-row lg:items-center lg:justify-between lg:px-6">
<OverviewMethodology
strings={strings}
comparisonMode={data.comparisonMode}
referenceHardware={data.referenceHardware}
/>
{data.comparisonMode === 'history' ? (
<OverviewRowScopeToggle
rowScope={data.rowScope}
unchangedRowCount={data.unchangedRowCount}
tier={data.tier}
engineScope={data.engineScope}
referenceHardware={data.referenceHardware}
<div className="flex shrink-0 flex-wrap items-center gap-x-2 gap-y-1">
{data.comparisonMode === 'history' ? (
<OverviewRowScopeToggle
rowScope={data.rowScope}
unchangedRowCount={data.unchangedRowCount}
tier={data.tier}
engineScope={data.engineScope}
referenceHardware={data.referenceHardware}
modelScope={data.modelScope}
locale={locale}
strings={strings}
/>
) : (
<OverviewHardwareRowScopeToggle
hardwareRowScope={data.hardwareRowScope}
emptyRowCount={data.emptyRowCount}
tier={data.tier}
engineScope={data.engineScope}
referenceHardware={data.referenceHardware}
modelScope={data.modelScope}
locale={locale}
strings={strings}
/>
)}
<OverviewModelScopeToggle
modelScope={data.modelScope}
locale={locale}
strings={strings}
/>
) : (
<OverviewHardwareRowScopeToggle
hardwareRowScope={data.hardwareRowScope}
emptyRowCount={data.emptyRowCount}
tier={data.tier}
engineScope={data.engineScope}
comparisonMode={data.comparisonMode}
referenceHardware={data.referenceHardware}
modelScope={data.modelScope}
rowScope={data.rowScope}
hardwareRowScope={data.hardwareRowScope}
locale={locale}
strings={strings}
/>
)}
<OverviewModelScopeToggle
modelScope={data.modelScope}
tier={data.tier}
engineScope={data.engineScope}
comparisonMode={data.comparisonMode}
referenceHardware={data.referenceHardware}
rowScope={data.rowScope}
hardwareRowScope={data.hardwareRowScope}
locale={locale}
strings={strings}
/>
</>
</div>
</div>
)}
</Card>
</>
Expand Down
18 changes: 17 additions & 1 deletion packages/app/src/components/overview/overview-presentation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
<svg
aria-hidden="true"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="size-3.5"
>
{presenting ? (
<path d="M6 2v4H2M10 2v4h4M6 14v-4H2M10 14v-4h4" />
) : (
<path d="M6 2H2v4M10 2h4v4M6 14H2v-4M10 14h4v-4" />
)}
</svg>
{presenting ? strings.presentExit : strings.presentEnter}
</button>
);
Expand Down
Loading