From cdbdfb600177fbc4026272597c866d769f216f89 Mon Sep 17 00:00:00 2001 From: cattan0710-tech Date: Tue, 10 Nov 2026 20:45:08 +0800 Subject: [PATCH 1/4] feat: add sorting to model detail recipe lists --- i18n/en/code.json | 6 ++ src/features/catalog/CatalogPage.tsx | 44 +------------ src/features/catalog/CatalogSort.tsx | 69 ++++++++++++++++++++ src/features/catalog/DetailPage.tsx | 35 +++++++++- src/features/catalog/query.ts | 17 +++-- src/features/catalog/routes.ts | 3 +- src/features/catalog/styles.module.css | 2 + tests/e2e/model-recipe-sort.spec.ts | 90 ++++++++++++++++++++++++++ tests/unit/model-recipe-sort.test.ts | 50 ++++++++++++++ 9 files changed, 266 insertions(+), 50 deletions(-) create mode 100644 src/features/catalog/CatalogSort.tsx create mode 100644 tests/e2e/model-recipe-sort.spec.ts create mode 100644 tests/unit/model-recipe-sort.test.ts diff --git a/i18n/en/code.json b/i18n/en/code.json index 31e8f83..984d9ef 100644 --- a/i18n/en/code.json +++ b/i18n/en/code.json @@ -1,4 +1,10 @@ { + "catalog.recipeNameAsc": { + "message": "Recipe name A–Z" + }, + "catalog.recipeNameDesc": { + "message": "Recipe name Z–A" + }, "catalog.loadingDetails": { "message": "Loading details…", "description": "正在加载详情…" diff --git a/src/features/catalog/CatalogPage.tsx b/src/features/catalog/CatalogPage.tsx index 1c7c261..62bcf7d 100644 --- a/src/features/catalog/CatalogPage.tsx +++ b/src/features/catalog/CatalogPage.tsx @@ -36,6 +36,7 @@ import { reportHref, } from './routes'; import CatalogSelect from './CatalogSelect'; +import CatalogSort from './CatalogSort'; import ModelCard, { ModelRecipeList, ModelTuning } from './ModelCard'; import styles from './styles.module.css'; @@ -316,49 +317,10 @@ export default function CatalogPage() { })} - update({ sort: value })} - options={[ - { - value: 'improvement-desc', - label: - q.view === 'models' - ? translate({ - id: 'site.f7e6d40ff9f5', - message: '按调优配方最佳提升排序', - }) - : translate({ - id: 'site.26ba23cf9e57', - message: '按吞吐量提升排序', - }), - }, - { - value: 'name-asc', - label: translate({ - id: 'site.e273b1d2eca6', - message: '按模型名称 A–Z', - }), - }, - { - value: 'name-desc', - label: translate({ - id: 'site.1322e7738e4a', - message: '按模型名称 Z–A', - }), - }, - { - value: 'updated-desc', - label: translate({ - id: 'site.6592019eaa05', - message: '按配方发布日期', - }), - }, - ]} + context={q.view === 'models' ? 'models' : 'recipes'} />
diff --git a/src/features/catalog/CatalogSort.tsx b/src/features/catalog/CatalogSort.tsx new file mode 100644 index 0000000..2d0cbc1 --- /dev/null +++ b/src/features/catalog/CatalogSort.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { translate } from '@docusaurus/Translate'; +import CatalogSelect from './CatalogSelect'; + +export default function CatalogSort({ + value, + onValueChange, + context = 'recipes', +}: { + value: string; + onValueChange: (value: string) => void; + context?: 'models' | 'recipes' | 'model-recipes'; +}) { + return ( + + ); +} diff --git a/src/features/catalog/DetailPage.tsx b/src/features/catalog/DetailPage.tsx index 9960d50..ad16de1 100644 --- a/src/features/catalog/DetailPage.tsx +++ b/src/features/catalog/DetailPage.tsx @@ -35,6 +35,8 @@ import ReportPanel from './ReportPanel'; import { availableReportTunings } from './report'; import CatalogLink from './CatalogLink'; import CatalogSelect from './CatalogSelect'; +import CatalogSort from './CatalogSort'; +import { compare, initialQuery, parseSort } from './query'; import RecipeOverview from './RecipeOverview'; import styles from './styles.module.css'; @@ -81,6 +83,7 @@ function DetailContent({ kind }: { kind: 'model' | 'recipe' }) { params.get('dataVersion') ?? navigation.catalogDataVersion; const state = useCatalog(dataVersion), c = state.catalog; + const sort = parseSort(params.get('sort')); const recipeParams = new URLSearchParams(location.search); recipeParams.delete('returnTo'); if (recipeParams.get('dataVersion') === state.currentCommit) @@ -90,7 +93,11 @@ function DetailContent({ kind }: { kind: 'model' | 'recipe' }) { !browser || !modelId ? null : kind === 'model' - ? modelRoute(modelId, params.get('dataVersion') ?? '') + ? modelRoute( + modelId, + params.get('dataVersion') ?? '', + sort === initialQuery.sort ? '' : sort, + ) : recipeId ? recipeRoute(modelId, recipeId, recipeParams.toString()) : null; @@ -129,6 +136,10 @@ function DetailContent({ kind }: { kind: 'model' | 'recipe' }) { [c, modelId], ); const recipe = recipes.find((r) => r.variant.id === recipeId); + const sortedRecipes = useMemo( + () => [...recipes].sort((a, b) => compare(a, b, sort)), + [recipes, sort], + ); const [revisions, setRevisions] = useState(null), [historyError, setHistoryError] = useState(''), [text, setText] = useState(''), @@ -230,6 +241,19 @@ function DetailContent({ kind }: { kind: 'model' | 'recipe' }) { modelNavigationState(dataVersion, returnTo), ); } + function chooseSort(value: string) { + if (value === sort) return; + history.push( + siteHref( + modelRoute( + modelId, + params.get('dataVersion') ?? '', + value === initialQuery.sort ? '' : value, + ), + ) + location.hash, + modelNavigationState(dataVersion, returnTo), + ); + } function chooseTab(tab: RecipeTab) { if (tab !== activeTab) history.push( @@ -487,9 +511,16 @@ function DetailContent({ kind }: { kind: 'model' | 'recipe' }) { {recipes.length} + + +
- {recipes.map((r) => ( + {sortedRecipes.map((r) => ( { + const source = parseCatalog(structuredClone(baseline)); + const model = source.models.find((item) => item.name === 'glm5.3')!; + const template = model.variants[0]; + model.variants = [ + { ...template, id: 'alpha', releaseDate: '2026-11-10' }, + { ...template, id: 'bravo', releaseDate: '2026-10-01' }, + { ...template, id: 'charlie', releaseDate: undefined }, + { ...template, id: 'delta', releaseDate: '2026-11-01' }, + ]; + model.tuning = [ + { optimized: 'alpha', uplift: 10, workloads: [], baseline: '' }, + { optimized: 'bravo', uplift: 50, workloads: [], baseline: '' }, + { optimized: 'delta', uplift: 50, workloads: [], baseline: '' }, + ]; + await page.route( + 'https://modelsphere.github.io/model-catalog/**', + (route) => + route.request().url().endsWith('index-v2.json') + ? route.fulfill({ json: source }) + : route.abort(), + ); + await page.setViewportSize({ width, height: 1000 }); + const returnTo = '/models?q=glm5.3&layout=table'; + await page.goto( + `/models/detail?modelId=${model.name}&dataVersion=${source.commit}&returnTo=${encodeURIComponent(returnTo)}`, + ); + const section = page.getByRole('region', { name: '可用部署配方 · 4' }); + const sort = section.getByRole('combobox', { name: '排序方式' }); + async function expectOrder(ids: string[]) { + await expect + .poll(() => + section + .locator('[data-recipe-id]') + .evaluateAll((cards) => + cards.map((card) => card.getAttribute('data-recipe-id')), + ), + ) + .toEqual(ids.map((id) => `${model.name}/${id}`)); + } + async function choose(label: string) { + await sort.click(); + await page.getByRole('option', { name: label, exact: true }).click(); + } + + await expect(sort).toHaveText('按吞吐量提升排序'); + await expectOrder(['bravo', 'delta', 'alpha', 'charlie']); + await choose('按配方名称 A–Z'); + await expectOrder(['alpha', 'bravo', 'charlie', 'delta']); + await choose('按配方名称 Z–A'); + await expectOrder(['delta', 'charlie', 'bravo', 'alpha']); + await choose('按配方发布日期'); + await expectOrder(['alpha', 'delta', 'bravo', 'charlie']); + await expect(page).toHaveURL( + (url) => + url.pathname === '/model' && + url.searchParams.get('sort') === 'updated-desc' && + url.searchParams.get('dataVersion') === source.commit, + ); + await page.reload(); + await expect(sort).toHaveText('按配方发布日期'); + await expectOrder(['alpha', 'delta', 'bravo', 'charlie']); + await page.goBack(); + await expect(sort).toHaveText('按配方名称 Z–A'); + await expectOrder(['delta', 'charlie', 'bravo', 'alpha']); + await page.goForward(); + await expect(sort).toHaveText('按配方发布日期'); + await choose('按吞吐量提升排序'); + await expectOrder(['bravo', 'delta', 'alpha', 'charlie']); + await expect(page).toHaveURL((url) => !url.searchParams.has('sort')); + await expect( + page.getByRole('navigation', { name: '面包屑' }).getByRole('link', { + name: '模型与调优', + exact: true, + }), + ).toHaveAttribute('href', returnTo); + expect( + await page.evaluate( + () => document.documentElement.scrollWidth <= window.innerWidth, + ), + ).toBe(true); + }); +} diff --git a/tests/unit/model-recipe-sort.test.ts b/tests/unit/model-recipe-sort.test.ts new file mode 100644 index 0000000..2265113 --- /dev/null +++ b/tests/unit/model-recipe-sort.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { baseline, recipesOf } from '../../src/features/catalog/data'; +import { compare, parseSort } from '../../src/features/catalog/query'; + +describe('model recipe sorting', () => { + const template = recipesOf(baseline)[0]; + const recipes = [ + { id: 'charlie', gain: null, date: undefined }, + { id: 'alpha', gain: 0, date: '2026-11-10' }, + { id: 'delta', gain: 50, date: '2026-11-01' }, + { id: 'bravo', gain: 50, date: '2026-10-01' }, + ].map(({ id, gain, date }) => ({ + ...template, + key: `${template.model.name}/${id}`, + variant: { ...template.variant, id, releaseDate: date }, + quotedGain: gain, + })); + const order = (sort: string) => + [...recipes].sort((a, b) => compare(a, b, sort)).map((r) => r.variant.id); + + it('reverses recipe names within the same model', () => { + expect(order('name-asc')).toEqual(['alpha', 'bravo', 'charlie', 'delta']); + expect(order('name-desc')).toEqual(['delta', 'charlie', 'bravo', 'alpha']); + }); + + it('puts missing gains last, keeps zero gains and resolves ties consistently', () => { + expect(order('improvement-desc')).toEqual([ + 'bravo', + 'delta', + 'alpha', + 'charlie', + ]); + }); + + it('orders newest dates first and puts missing dates last', () => { + expect(order('updated-desc')).toEqual([ + 'alpha', + 'delta', + 'bravo', + 'charlie', + ]); + }); + + it('uses the default for missing or unsupported URL values', () => { + expect(parseSort(null)).toBe('improvement-desc'); + expect(parseSort('unsupported')).toBe('improvement-desc'); + for (const value of ['name-asc', 'name-desc', 'updated-desc']) + expect(parseSort(value)).toBe(value); + }); +}); From 486317391170e1944ea19cc3b3159233dc8de542 Mon Sep 17 00:00:00 2001 From: cattan0710-tech Date: Tue, 10 Nov 2026 21:00:16 +0800 Subject: [PATCH 2/4] fix: clarify newest-first recipe release date sorting --- i18n/en/code.json | 4 ++-- src/features/catalog/CatalogSort.tsx | 2 +- tests/e2e/model-recipe-sort.spec.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/i18n/en/code.json b/i18n/en/code.json index 984d9ef..5497c6b 100644 --- a/i18n/en/code.json +++ b/i18n/en/code.json @@ -1682,8 +1682,8 @@ "description": "按模型名称 Z–A" }, "site.6592019eaa05": { - "message": "Recipe release date", - "description": "按配方发布日期" + "message": "Recipe release date (newest first)", + "description": "按配方发布日期(由新到旧)" }, "site.7904ff565aaa": { "message": "Card view", diff --git a/src/features/catalog/CatalogSort.tsx b/src/features/catalog/CatalogSort.tsx index 2d0cbc1..699f370 100644 --- a/src/features/catalog/CatalogSort.tsx +++ b/src/features/catalog/CatalogSort.tsx @@ -60,7 +60,7 @@ export default function CatalogSort({ value: 'updated-desc', label: translate({ id: 'site.6592019eaa05', - message: '按配方发布日期', + message: '按配方发布日期(由新到旧)', }), }, ]} diff --git a/tests/e2e/model-recipe-sort.spec.ts b/tests/e2e/model-recipe-sort.spec.ts index 0bb1d1d..7dcf4b0 100644 --- a/tests/e2e/model-recipe-sort.spec.ts +++ b/tests/e2e/model-recipe-sort.spec.ts @@ -56,7 +56,7 @@ for (const width of [390, 1440]) { await expectOrder(['alpha', 'bravo', 'charlie', 'delta']); await choose('按配方名称 Z–A'); await expectOrder(['delta', 'charlie', 'bravo', 'alpha']); - await choose('按配方发布日期'); + await choose('按配方发布日期(由新到旧)'); await expectOrder(['alpha', 'delta', 'bravo', 'charlie']); await expect(page).toHaveURL( (url) => @@ -65,13 +65,13 @@ for (const width of [390, 1440]) { url.searchParams.get('dataVersion') === source.commit, ); await page.reload(); - await expect(sort).toHaveText('按配方发布日期'); + await expect(sort).toHaveText('按配方发布日期(由新到旧)'); await expectOrder(['alpha', 'delta', 'bravo', 'charlie']); await page.goBack(); await expect(sort).toHaveText('按配方名称 Z–A'); await expectOrder(['delta', 'charlie', 'bravo', 'alpha']); await page.goForward(); - await expect(sort).toHaveText('按配方发布日期'); + await expect(sort).toHaveText('按配方发布日期(由新到旧)'); await choose('按吞吐量提升排序'); await expectOrder(['bravo', 'delta', 'alpha', 'charlie']); await expect(page).toHaveURL((url) => !url.searchParams.has('sort')); From 4da7d669847996949b2c934b570c23695522ff11 Mon Sep 17 00:00:00 2001 From: cattan0710-tech Date: Tue, 10 Nov 2026 21:06:54 +0800 Subject: [PATCH 3/4] fix: simplify newest release sorting label --- i18n/en/code.json | 4 ++-- src/features/catalog/CatalogSort.tsx | 2 +- tests/e2e/model-recipe-sort.spec.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/i18n/en/code.json b/i18n/en/code.json index 5497c6b..d9d184d 100644 --- a/i18n/en/code.json +++ b/i18n/en/code.json @@ -1682,8 +1682,8 @@ "description": "按模型名称 Z–A" }, "site.6592019eaa05": { - "message": "Recipe release date (newest first)", - "description": "按配方发布日期(由新到旧)" + "message": "Newest first", + "description": "按最新发布排序" }, "site.7904ff565aaa": { "message": "Card view", diff --git a/src/features/catalog/CatalogSort.tsx b/src/features/catalog/CatalogSort.tsx index 699f370..446bae6 100644 --- a/src/features/catalog/CatalogSort.tsx +++ b/src/features/catalog/CatalogSort.tsx @@ -60,7 +60,7 @@ export default function CatalogSort({ value: 'updated-desc', label: translate({ id: 'site.6592019eaa05', - message: '按配方发布日期(由新到旧)', + message: '按最新发布排序', }), }, ]} diff --git a/tests/e2e/model-recipe-sort.spec.ts b/tests/e2e/model-recipe-sort.spec.ts index 7dcf4b0..27004c0 100644 --- a/tests/e2e/model-recipe-sort.spec.ts +++ b/tests/e2e/model-recipe-sort.spec.ts @@ -56,7 +56,7 @@ for (const width of [390, 1440]) { await expectOrder(['alpha', 'bravo', 'charlie', 'delta']); await choose('按配方名称 Z–A'); await expectOrder(['delta', 'charlie', 'bravo', 'alpha']); - await choose('按配方发布日期(由新到旧)'); + await choose('按最新发布排序'); await expectOrder(['alpha', 'delta', 'bravo', 'charlie']); await expect(page).toHaveURL( (url) => @@ -65,13 +65,13 @@ for (const width of [390, 1440]) { url.searchParams.get('dataVersion') === source.commit, ); await page.reload(); - await expect(sort).toHaveText('按配方发布日期(由新到旧)'); + await expect(sort).toHaveText('按最新发布排序'); await expectOrder(['alpha', 'delta', 'bravo', 'charlie']); await page.goBack(); await expect(sort).toHaveText('按配方名称 Z–A'); await expectOrder(['delta', 'charlie', 'bravo', 'alpha']); await page.goForward(); - await expect(sort).toHaveText('按配方发布日期(由新到旧)'); + await expect(sort).toHaveText('按最新发布排序'); await choose('按吞吐量提升排序'); await expectOrder(['bravo', 'delta', 'alpha', 'charlie']); await expect(page).toHaveURL((url) => !url.searchParams.has('sort')); From d6516fa331078d2761e41de81f3a3567d9bd587f Mon Sep 17 00:00:00 2001 From: cattan0710-tech Date: Tue, 10 Nov 2026 21:23:34 +0800 Subject: [PATCH 4/4] test: fix recipe sort fixtures and stabilize report retry coverage --- tests/e2e/catalog.spec.ts | 12 ++++++++---- tests/e2e/model-recipe-sort.spec.ts | 30 ++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/tests/e2e/catalog.spec.ts b/tests/e2e/catalog.spec.ts index f32f15e..61b7a67 100644 --- a/tests/e2e/catalog.spec.ts +++ b/tests/e2e/catalog.spec.ts @@ -1202,11 +1202,13 @@ test('original report retries a failed request without requiring structured JSON page, }) => { let attempts = 0; + let available = false; + // Keep the report unavailable until Retry, including any redirect remounts. await page.route('**/mimo-v2.5-h100-report.html', (route) => { attempts++; - return attempts === 1 - ? route.fulfill({ status: 503, body: 'Unavailable' }) - : route.fulfill({ body: report, contentType: 'text/html' }); + return available + ? route.fulfill({ body: report, contentType: 'text/html' }) + : route.fulfill({ status: 503, body: 'Unavailable' }); }); await page.goto( `/models/recipe?modelId=mimo-v2.5&recipeId=${variant.id}&tab=performance`, @@ -1218,12 +1220,14 @@ test('original report retries a failed request without requiring structured JSON await expect( page.getByRole('button', { name: '全屏观看', exact: true }), ).toBeDisabled(); + const attemptsBeforeRetry = attempts; + available = true; await page.getByRole('button', { name: '重试', exact: true }).click(); await expect(reportBody(page)).toContainText('119.9%'); await expect( page.getByRole('button', { name: '全屏观看', exact: true }), ).toBeEnabled(); - expect(attempts).toBe(2); + expect(attempts).toBe(attemptsBeforeRetry + 1); }); test('history opens the exact configuration and keeps reports bound to their measured version', async ({ diff --git a/tests/e2e/model-recipe-sort.spec.ts b/tests/e2e/model-recipe-sort.spec.ts index 27004c0..1322f30 100644 --- a/tests/e2e/model-recipe-sort.spec.ts +++ b/tests/e2e/model-recipe-sort.spec.ts @@ -14,17 +14,37 @@ for (const width of [390, 1440]) { { ...template, id: 'bravo', releaseDate: '2026-10-01' }, { ...template, id: 'charlie', releaseDate: undefined }, { ...template, id: 'delta', releaseDate: '2026-11-01' }, - ]; + ].map((variant) => ({ + ...variant, + path: `models/${model.name}/${variant.id}.yaml`, + history: `models/${model.name}/${variant.id}.history.json`, + })); model.tuning = [ - { optimized: 'alpha', uplift: 10, workloads: [], baseline: '' }, - { optimized: 'bravo', uplift: 50, workloads: [], baseline: '' }, - { optimized: 'delta', uplift: 50, workloads: [], baseline: '' }, + { + optimized: `alpha@${template.version}`, + uplift: 10, + workloads: [], + baseline: '', + }, + { + optimized: `bravo@${template.version}`, + uplift: 50, + workloads: [], + baseline: '', + }, + { + optimized: `delta@${template.version}`, + uplift: 50, + workloads: [], + baseline: '', + }, ]; + const catalog = parseCatalog(source); await page.route( 'https://modelsphere.github.io/model-catalog/**', (route) => route.request().url().endsWith('index-v2.json') - ? route.fulfill({ json: source }) + ? route.fulfill({ json: catalog }) : route.abort(), ); await page.setViewportSize({ width, height: 1000 });