Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion web/src/components/ui/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import styles from './Select.module.scss';
export interface SelectOption {
value: string;
label: string;
triggerLabel?: string;
suffix?: ReactNode;
suffixAriaLabel?: string;
disabled?: boolean;
Expand Down Expand Up @@ -198,7 +199,7 @@ export function Select({
? selectedIndex
: firstEnabledIndex;
const selected = selectedIndex >= 0 ? options[selectedIndex] : undefined;
const displayText = selected?.label ?? placeholder ?? '';
const displayText = selected?.triggerLabel ?? selected?.label ?? placeholder ?? '';
const isPlaceholder = !selected && placeholder;

const commitSelection = useCallback(
Expand Down
66 changes: 45 additions & 21 deletions web/src/features/ranking/RankingPage.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -892,13 +892,12 @@

@mixin ranking-header-stacked {
.leaderboardHeader {
grid-template-columns: minmax(0, 1fr);
grid-template-columns: minmax(0, 1fr) auto;
grid-template-areas:
'title'
'toolbar'
'profile';
'title title'
'toolbar profile';
align-items: stretch;
column-gap: 0;
column-gap: 12px;
row-gap: 14px;
padding: 0;
}
Expand All @@ -921,36 +920,43 @@
}

.leaderboardHeaderActions {
justify-content: center;
justify-self: center;
align-self: center;
justify-content: flex-end;
justify-self: end;
max-width: 100%;
width: auto;
margin-right: 0;
}

.profileActionShell,
.profileActionShellActive {
max-width: 100%;
}

.toolbar {
display: grid;
grid-template-columns: minmax(0, 1fr);
display: flex;
justify-content: flex-start;
width: 100%;
max-width: 100%;
padding: 4px;
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-inline: contain;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;

&::-webkit-scrollbar {
display: none;
}
}

.periods {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
width: 100%;
}

.periodButton {
padding-inline: 6px;
grid-template-columns: repeat(4, max-content);
flex: 0 0 auto;
width: max-content;
margin-left: auto;
}

.metricControl {
width: 100%;
flex: 0 0 168px;
width: 168px;
margin-right: auto;
}
}

Expand All @@ -961,6 +967,24 @@
@include mobile {
@include ranking-header-stacked;

.profileActionShell {
max-width: 160px;
}

.profileActionShellActive {
flex: 0 0 42px;
width: 42px;
}

.profileActionShellActive :global(.main-action-button) {
width: 32px;
padding-inline: 5px;
}

.profileActionName {
display: none;
}

.profileModal :global(.modal-body) {
max-height: min(60dvh, calc(100dvh - 180px));
overflow: auto;
Expand Down
6 changes: 4 additions & 2 deletions web/src/features/ranking/RankingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ function LeaderboardCard({
const podium = rows.slice(0, 3);
const tableRows = rows;
const scoreExplanation = resolveScoreExplanation(board, metric, language);
const hasRankingProfile = status?.status === 'active' || status?.status === 'paused';
return (
<article className={`card ${styles.leaderboardCard}`.trim()} aria-busy={loading}>
<header className={styles.leaderboardHeader}>
Expand Down Expand Up @@ -644,12 +645,13 @@ function LeaderboardCard({
data-ranking-profile-action-shell
>
<MainActionButton
shellClassName={`${styles.profileActionShell} ${status?.status === 'active' || status?.status === 'paused' ? styles.profileActionShellActive : ''}`.trim()}
shellClassName={`${styles.profileActionShell} ${hasRankingProfile ? styles.profileActionShellActive : ''}`.trim()}
onClick={onOpenProfile}
disabled={statusLoading && !status}
aria-label={hasRankingProfile ? t('ranking.profile_action') : undefined}
Comment thread
Willxup marked this conversation as resolved.
Outdated
data-ranking-profile-action
>
{status?.status === 'active' || status?.status === 'paused' ? (
{hasRankingProfile ? (
<>
<RankingAvatar avatarID={status.avatar_id ?? 1} name={status.display_name ?? ''} className={styles.profileActionAvatar} decorative />
<span className={styles.profileActionName} data-ranking-profile-name>{status.display_name || t('ranking.profile_action')}</span>
Expand Down
27 changes: 16 additions & 11 deletions web/src/features/ranking/components/RankingToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ const PERIODS: ReadonlyArray<{ value: RankingPeriod; labelKey: string }> = [
{ value: 'previous_month', labelKey: 'ranking.period_previous_month' },
];

const METRICS: ReadonlyArray<{ value: RankingMetric; labelKey: string }> = [
{ value: 'overall', labelKey: 'ranking.metric_overall' },
{ value: 'total_tokens', labelKey: 'ranking.metric_total_tokens' },
{ value: 'request_count', labelKey: 'ranking.metric_request_count' },
{ value: 'cache_read_rate', labelKey: 'ranking.metric_cache_read_rate' },
{ value: 'ttft_average', labelKey: 'ranking.metric_ttft_average' },
{ value: 'latency_average', labelKey: 'ranking.metric_latency_average' },
{ value: 'peak_tpm', labelKey: 'ranking.metric_peak_tpm' },
{ value: 'peak_rpm', labelKey: 'ranking.metric_peak_rpm' },
// 触发器保持短名称,展开菜单保留完整指标语义。
const METRICS: ReadonlyArray<{ value: RankingMetric; labelKey: string; triggerLabelKey: string }> = [
{ value: 'overall', labelKey: 'ranking.metric_overall', triggerLabelKey: 'ranking.metric_overall' },
{ value: 'total_tokens', labelKey: 'ranking.metric_total_tokens', triggerLabelKey: 'ranking.metric_short_total_tokens' },
{ value: 'request_count', labelKey: 'ranking.metric_request_count', triggerLabelKey: 'ranking.metric_short_request_count' },
{ value: 'cache_read_rate', labelKey: 'ranking.metric_cache_read_rate', triggerLabelKey: 'ranking.metric_short_cache_read_rate' },
{ value: 'ttft_average', labelKey: 'ranking.metric_ttft_average', triggerLabelKey: 'ranking.metric_short_ttft_average' },
{ value: 'latency_average', labelKey: 'ranking.metric_latency_average', triggerLabelKey: 'ranking.metric_short_latency_average' },
{ value: 'peak_tpm', labelKey: 'ranking.metric_peak_tpm', triggerLabelKey: 'ranking.metric_short_peak_tpm' },
{ value: 'peak_rpm', labelKey: 'ranking.metric_peak_rpm', triggerLabelKey: 'ranking.metric_short_peak_rpm' },
];

export interface RankingToolbarProps {
Expand All @@ -37,7 +38,11 @@ export function RankingToolbar({
}: RankingToolbarProps) {
const { t } = useTranslation();
const metricOptions = useMemo(
() => METRICS.map((option) => ({ value: option.value, label: t(option.labelKey) })),
() => METRICS.map((option) => ({
value: option.value,
label: t(option.labelKey),
triggerLabel: t(option.triggerLabelKey),
})),
[t],
);

Expand All @@ -64,7 +69,7 @@ export function RankingToolbar({
</div>
<div className={styles.metricControl} data-ranking-metric>
<span className={styles.metricWidthSizer} aria-hidden="true" data-ranking-metric-sizer>
{metricOptions.map((option) => <span key={option.value}>{option.label}</span>)}
{metricOptions.map((option) => <span key={option.value}>{option.triggerLabel}</span>)}
</span>
<Select
value={metric}
Expand Down
22 changes: 15 additions & 7 deletions web/src/features/ranking/components/test/RankingToolbar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ globalThis.IS_REACT_ACT_ENVIRONMENT = true;

vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key === 'ranking.metric_ttft_average'
? 'This is the longest translated ranking metric'
: key,
t: (key: string) => {
if (key === 'ranking.metric_ttft_average') {
return 'This is the longest translated ranking metric';
}
if (key === 'ranking.metric_short_ttft_average') return 'TTFT';
return key;
},
}),
}));

Expand All @@ -35,7 +39,7 @@ describe('RankingToolbar', () => {
act(() => root.render(
<RankingToolbar
period="today"
metric="overall"
metric="ttft_average"
onPeriodChange={onPeriodChange}
onMetricChange={vi.fn()}
/>,
Expand All @@ -49,12 +53,16 @@ describe('RankingToolbar', () => {

const metricTrigger = container.querySelector<HTMLButtonElement>('[data-ranking-metric] button');
expect(metricTrigger).not.toBeNull();
expect(metricTrigger?.textContent).toContain('ranking.metric_overall');
expect(metricTrigger?.textContent).toContain('TTFT');
expect(metricTrigger?.textContent).not.toContain('This is the longest translated ranking metric');
const metricSizer = container.querySelector('[data-ranking-metric-sizer]');
expect(metricSizer?.querySelectorAll('span')).toHaveLength(8);
expect(metricSizer?.textContent).toContain('This is the longest translated ranking metric');
expect(metricSizer?.textContent).toContain('TTFT');
expect(metricSizer?.textContent).not.toContain('This is the longest translated ranking metric');
act(() => metricTrigger?.click());
expect(document.querySelector<HTMLElement>('[role="listbox"]')?.style.width).toBe('260px');
const listbox = document.querySelector<HTMLElement>('[role="listbox"]');
expect(listbox?.textContent).toContain('This is the longest translated ranking metric');
expect(listbox?.style.width).toBe('260px');
expect(periodGroup?.compareDocumentPosition(container.querySelector('[data-ranking-metric]') as Node) & Node.DOCUMENT_POSITION_FOLLOWING)
.toBeTruthy();
});
Expand Down
23 changes: 19 additions & 4 deletions web/src/features/ranking/test/RankingPage.styles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe('Ranking table context styles', () => {
expect(rule('.toolbar')).toContain('row-gap: 10px;');
});

it('keeps long titles on one line and reflows the header when the card narrows', () => {
it('keeps long titles on one line and reflows narrow-card filters into one scrollable row', () => {
const card = rule('.leaderboardCard:global(.card)');
const title = rule('.leaderboardTitle :global(.keeper-card-title)');
const stackedStart = styles.indexOf('@mixin ranking-header-stacked');
Expand All @@ -158,9 +158,22 @@ describe('Ranking table context styles', () => {
expect(title).toContain('white-space: nowrap;');
expect(title).toContain('overflow: hidden;');
expect(title).toContain('text-overflow: ellipsis;');
expect(stacked).toContain("grid-template-areas:\n 'title'\n 'toolbar'\n 'profile';");
expect(stacked).toMatch(
/\.leaderboardHeader\s*\{[\s\S]*?grid-template-columns:\s*minmax\(0, 1fr\) auto;[\s\S]*?'title title'[\s\S]*?'toolbar profile';/,
);
expect(stacked).toMatch(/\.leaderboardHeaderToolbar\s*\{[\s\S]*?justify-self:\s*stretch;/);
expect(stacked).toMatch(/\.toolbar\s*\{[\s\S]*?grid-template-columns:\s*minmax\(0, 1fr\);/);
expect(stacked).toMatch(
/\.leaderboardHeaderActions\s*\{[\s\S]*?justify-content:\s*flex-end;[\s\S]*?justify-self:\s*end;[\s\S]*?margin-right:\s*0;/,
);
expect(stacked).toMatch(
/\.toolbar\s*\{[\s\S]*?display:\s*flex;[\s\S]*?padding:\s*4px;[\s\S]*?overflow-x:\s*auto;[\s\S]*?scrollbar-width:\s*none;/,
);
expect(stacked).toMatch(
/\.periods\s*\{[\s\S]*?grid-template-columns:\s*repeat\(4, max-content\);[\s\S]*?width:\s*max-content;/,
);
expect(stacked).toMatch(
/\.metricControl\s*\{[\s\S]*?flex:\s*0 0 168px;[\s\S]*?width:\s*168px;/,
);
expect(container).toContain('@include ranking-header-stacked;');
});

Expand All @@ -182,12 +195,14 @@ describe('Ranking table context styles', () => {
expect(rule('.profileActionAvatar')).toContain('flex: 0 0 22px;');
});

it('stacks the header in scan order with compact time text and a centered profile entry on mobile', () => {
it('keeps only the active avatar visible on mobile while reusing the narrow-card header layout', () => {
const mobileStart = styles.indexOf('@include mobile');
const mobileEnd = styles.indexOf('@media (prefers-reduced-motion', mobileStart);
const mobile = styles.slice(mobileStart, mobileEnd);

expect(mobile).toContain('@include ranking-header-stacked;');
expect(mobile).toMatch(/\.profileActionShellActive\s*\{[\s\S]*?flex:\s*0 0 42px;[\s\S]*?width:\s*42px;/);
expect(mobile).toMatch(/\.profileActionName\s*\{[\s\S]*?display:\s*none;/);
});

it('gives loading, empty, and error content a tall centered viewport', () => {
Expand Down
10 changes: 10 additions & 0 deletions web/src/features/ranking/test/RankingPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ describe('RankingPage', () => {
expect(toolbar?.parentElement).toBe(header);
expect(profile?.parentElement).toBe(header);
expect(header?.children).toHaveLength(3);
expect(title?.compareDocumentPosition(toolbar as Node) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(toolbar?.compareDocumentPosition(profile as Node) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(toolbar?.querySelector('[data-ranking-toolbar]')).not.toBeNull();
expect(header?.querySelector('[data-ranking-periods]')).not.toBeNull();
expect(header?.querySelector('[data-ranking-metric]')).not.toBeNull();
Expand Down Expand Up @@ -226,6 +228,14 @@ describe('RankingPage', () => {
expect(dialog?.querySelectorAll('[data-ranking-avatar-option]')).toHaveLength(66);
});

it('keeps an accessible profile action name when mobile styling leaves only the active avatar visible', async () => {
await renderPage({ status: { status: 'active', display_name: 'Owner', avatar_id: 7 } });

const profileAction = container.querySelector<HTMLButtonElement>('[data-ranking-profile-action]');
expect(profileAction?.getAttribute('aria-label')).toBe('ranking.profile_action');
expect(profileAction?.querySelector('[data-ranking-profile-name]')?.textContent).toBe('Owner');
});

it('keeps a successful leaderboard visible when metadata loading fails', async () => {
const onRetryMetadata = vi.fn(async () => null);
await renderPage({
Expand Down