diff --git a/apps/web/e2e/core-generation.spec.ts b/apps/web/e2e/core-generation.spec.ts index 7cb74098e..ab9305421 100644 --- a/apps/web/e2e/core-generation.spec.ts +++ b/apps/web/e2e/core-generation.spec.ts @@ -5,6 +5,7 @@ const PASSWORD = 'SmokePass123!'; type WorkspaceResponse = { id?: string; workspace_id?: string }; type ProjectResponse = { id: string }; type LibraryResponse = { id: string }; +type AssetListResponse = { items: Array<{ name: string; status: string; mime_type?: string; file_type?: string }> }; type GenerationTaskResponse = { id: string; status: string; progress: number; result_count: number; error_message?: string | null }; type GeneratedVideoResponse = { id: string; name: string; file_url: string; file_size: number }; @@ -53,6 +54,39 @@ test.describe('Core generation and download flow', () => { expect(library.status(), await library.text()).toBe(200); const libraryData = (await library.json()) as LibraryResponse; + const upload = await request.post(`${apiBase}/upload`, { + headers, + multipart: { + workspace_id: workspaceId || '', + project_id: projectData.id, + library_id: libraryData.id, + file: { + name: 'e2e-generation-source.MOV', + mimeType: 'video/quicktime', + buffer: Buffer.from('playwright generation source mov'), + }, + }, + }); + expect(upload.status(), await upload.text()).toBe(200); + + await expect + .poll( + async () => { + const assets = await request.get(`${apiBase}/assets`, { + headers, + params: { library_id: libraryData.id }, + }); + if (!assets.ok()) { + return `http_${assets.status()}`; + } + const data = (await assets.json()) as AssetListResponse; + const asset = data.items.find((item) => item.name === 'e2e-generation-source.MOV'); + return asset ? `${asset.mime_type || asset.file_type || ''}:${asset.status}` : 'missing'; + }, + { timeout: 30_000, intervals: [1_000, 2_000, 3_000] } + ) + .toMatch(/^(video\/quicktime|video\/mp4|video)?:ready$/); + await page.goto('/login'); await page.getByPlaceholder('邮箱').fill(email); await page.getByPlaceholder('密码').fill(PASSWORD); @@ -64,6 +98,9 @@ test.describe('Core generation and download flow', () => { await page.locator('.ant-select-selector').first().click(); await page.getByText(`${libraryName} (video)`).click(); + await expect(page.getByText(/素材准备度:/)).toBeVisible({ timeout: 20_000 }); + await expect(page.getByRole('button', { name: '发起生成' })).toBeEnabled({ timeout: 20_000 }); + const createTaskResponsePromise = page.waitForResponse( (response) => response.url().includes('/api/v1/generation/tasks') && response.request().method() === 'POST', { timeout: 30_000 } diff --git a/apps/web/src/pages/workspace/ProjectAssets.tsx b/apps/web/src/pages/workspace/ProjectAssets.tsx index 640ab32c3..0903e3db8 100644 --- a/apps/web/src/pages/workspace/ProjectAssets.tsx +++ b/apps/web/src/pages/workspace/ProjectAssets.tsx @@ -100,6 +100,7 @@ const ProjectAssets: React.FC = () => { const [classificationSourceFilter, setClassificationSourceFilter] = useState('all'); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [batchMode, setBatchMode] = useState<'unclassified_only' | 'include_classified'>('unclassified_only'); + const [smartViewFilter, setSmartViewFilter] = useState('all'); const [batchProgress, setBatchProgress] = useState(defaultBatchProgress); const [batchJobIds, setBatchJobIds] = useState([]); const [batchFailedIds, setBatchFailedIds] = useState([]); @@ -281,6 +282,16 @@ const ProjectAssets: React.FC = () => { const filteredAssets = useMemo(() => { let items = assetsQuery.data || []; + if (smartViewFilter === 'recommended') { + items = items.filter((item) => item.status === 'ready' && item.mime_type?.startsWith('video')); + } else if (smartViewFilter === 'needs_attention') { + items = items.filter((item) => item.status !== 'ready' || (typeof item.quality_score === 'number' && item.quality_score < 60)); + } else if (smartViewFilter === 'unclassified') { + items = items.filter((item) => !item.metadata?.classification || item.classification_status === 'failed'); + } else if (smartViewFilter === 'voice') { + items = items.filter((item) => item.mime_type?.startsWith('audio')); + } + if (classificationFilter !== 'all') { if (classificationFilter === 'unclassified') { items = items.filter((item) => !item.metadata?.classification); @@ -299,8 +310,12 @@ const ProjectAssets: React.FC = () => { } } + if (smartViewFilter === 'recent') { + return items.slice(0, 10); + } + return items; - }, [assetsQuery.data, classificationFilter, classificationSourceFilter]); + }, [assetsQuery.data, classificationFilter, classificationSourceFilter, smartViewFilter]); const selectedAssets = useMemo( () => filteredAssets.filter((item) => selectedRowKeys.includes(item.id)), @@ -481,6 +496,19 @@ const ProjectAssets: React.FC = () => { + setBatchMode(value)} /> - +