diff --git a/.project-docs/30-worklog/tasks/20260818-video-duration-server-config-7b4e.md b/.project-docs/30-worklog/tasks/20260818-video-duration-server-config-7b4e.md new file mode 100644 index 0000000..5056b58 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260818-video-duration-server-config-7b4e.md @@ -0,0 +1,67 @@ +# Task: Load AI Design video duration options from server + +## Identity + +- Task ID: 20260818-video-duration-server-config-7b4e +- Mode: Feature +- Branch: main +- Worktree: D:\Datas\OthersProjects\makelore +- Base commit: 86ffeac066ca03cdaacb6f12eec11dc1e258432a +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Update the AI Design video confirmation card to use the active Quote's + server-provided `generationOptions.durations` for both rendering and + validation, removing the client-side 2–6 second range coupling. +- Add a regression proving an enabled duration outside the former local range + is rendered, can be selected, re-quoted, and submitted through the existing + confirmation flow. +- Do not change the API payload or provider contract; the server Quote remains + the authority for available and disabled durations. + +## Intent And Constraints + +- Preserve disabled options and the existing “must re-quote after changing a + parameter” behavior. +- Keep legacy/invalid quotes fail-closed when no enabled server duration is + selected; use a server-driven validation message rather than naming a local + range. +- Keep the local preview adapter unchanged; this change targets the cloud + Makelore adapter and the shared Quote confirmation UI. + +## Outcome + +- `ImageCanvas` now renders enabled video durations directly from the active + server Quote's `generationOptions.durations`. +- Confirmation validation now accepts only a non-null duration that matches an + enabled server option; the previous client-side 2–6 second range check was + removed so future server configuration changes do not require a client + release. +- The confirmation error is server-driven and no longer names a local range. +- Added a regression using server-provided `7` and `15` second options that + verifies the options are shown, the initial invalid selection is blocked, + `15` survives re-quoting, and the final confirmation receives `15`. + +## Verification + +- Red test: the new regression initially received only `['', '2', '6']`, + proving the old client-side range filter was still active. +- `pnpm exec vitest run tests/unit/image-canvas-page.test.tsx tests/unit/works-square-design-workspace.test.ts` — 79 passed. +- `pnpm run typecheck` — passed. +- `pnpm exec eslint src/pages/ImageCanvas/index.tsx tests/unit/image-canvas-page.test.tsx` — passed. +- `pnpm run build:vite` — passed for Renderer, Electron Main, and Preload; + only existing dynamic-import and chunk-size warnings were reported. +- `git diff --check` — passed; Git reported only the repository's LF-to-CRLF + checkout warnings. + +## Follow-ups + +- The server must continue publishing a valid enabled duration list in each + video Quote; an empty or all-disabled list intentionally keeps confirmation + unavailable. + +## Promotion Candidates + +- None recorded. diff --git a/src/pages/ImageCanvas/index.tsx b/src/pages/ImageCanvas/index.tsx index 5c9dd77..76cd49b 100644 --- a/src/pages/ImageCanvas/index.tsx +++ b/src/pages/ImageCanvas/index.tsx @@ -265,9 +265,6 @@ type EditableGenerationParameters = Partial>; -const MIN_VIDEO_DURATION_SECONDS = 2; -const MAX_VIDEO_DURATION_SECONDS = 6; - function hasSelectableVideoDuration( quote: DesignGenerationQuote, parameters: DesignGenerationParameters, @@ -275,8 +272,6 @@ function hasSelectableVideoDuration( if (quote.medium !== 'video') return true; const durationSeconds = parameters.durationSeconds; return durationSeconds !== null - && durationSeconds >= MIN_VIDEO_DURATION_SECONDS - && durationSeconds <= MAX_VIDEO_DURATION_SECONDS && quote.generationOptions.durations.some((option) => ( !option.disabled && option.value === durationSeconds )); @@ -745,11 +740,7 @@ function QuoteCard({ onConfirm: () => void; }) { const availableResolutions = quote.generationOptions.resolutions.filter((option) => !option.disabled); - const availableDurations = quote.generationOptions.durations.filter((option) => ( - !option.disabled - && option.value >= MIN_VIDEO_DURATION_SECONDS - && option.value <= MAX_VIDEO_DURATION_SECONDS - )); + const availableDurations = quote.generationOptions.durations.filter((option) => !option.disabled); const availableAspectRatios = quote.generationOptions.aspectRatios.filter((option) => !option.disabled); const selectedDuration = availableDurations.some( (option) => option.value === draft.generationParameters.durationSeconds, @@ -1210,7 +1201,7 @@ export function ImageCanvas() { } if (!quote || quote.quoteId !== quoteId || !hasSelectableVideoDuration(quote, draft.generationParameters)) { - setActionError('请选择 2 至 6 秒的视频时长,等待重新报价完成后再确认。'); + setActionError('请选择服务端提供的视频时长,等待重新报价完成后再确认。'); return; } const requestedWorkspaceId = workspace.workspaceId; diff --git a/tests/unit/image-canvas-page.test.tsx b/tests/unit/image-canvas-page.test.tsx index 086ae82..ebfd762 100644 --- a/tests/unit/image-canvas-page.test.tsx +++ b/tests/unit/image-canvas-page.test.tsx @@ -510,7 +510,7 @@ describe('ImageCanvas Workspace-first design experience', () => { expect(screen.getByRole('button', { name: '确认并开始生成' })).toBeEnabled(); }); - it('limits legacy video durations to 2-6 seconds and preserves 6 seconds through confirmation', async () => { + it('uses server-provided video durations and preserves the selected value through confirmation', async () => { const videoConversation = conversationFixture(); const videoQuote = videoConversation.messages[1].generationQuote!; videoConversation.brief.medium = 'video'; @@ -543,7 +543,7 @@ describe('ImageCanvas Workspace-first design experience', () => { const durationSelect = await screen.findByTestId('design-quote-duration'); expect(durationSelect).toHaveValue(''); expect(within(durationSelect).getAllByRole('option').map((option) => option.getAttribute('value'))) - .toEqual(['', '2', '6']); + .toEqual(['', '2', '6', '7', '15']); expect(screen.getByRole('button', { name: '确认并开始生成' })).toBeDisabled(); fireEvent.change(screen.getByLabelText('设计需求'), { @@ -552,9 +552,9 @@ describe('ImageCanvas Workspace-first design experience', () => { fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' })); expect(confirmImageWorkspaceGenerationMock).not.toHaveBeenCalled(); - expect(await screen.findByRole('alert')).toHaveTextContent('请选择 2 至 6 秒的视频时长'); + expect(await screen.findByRole('alert')).toHaveTextContent('请选择服务端提供的视频时长'); - fireEvent.change(durationSelect, { target: { value: '6' } }); + fireEvent.change(durationSelect, { target: { value: '15' } }); await waitFor(() => expect(updateImageWorkspaceGenerationQuoteMock).toHaveBeenCalledWith( 'workspace-cloud', @@ -562,10 +562,10 @@ describe('ImageCanvas Workspace-first design experience', () => { videoQuote.finalPrompt, { ...videoQuote.generationParameters, - durationSeconds: 6, + durationSeconds: 15, }, )); - await waitFor(() => expect(screen.getByTestId('design-quote-duration')).toHaveValue('6')); + await waitFor(() => expect(screen.getByTestId('design-quote-duration')).toHaveValue('15')); fireEvent.click(screen.getByRole('button', { name: '确认并开始生成' })); await waitFor(() => expect(confirmImageWorkspaceGenerationMock).toHaveBeenCalledWith( @@ -577,7 +577,7 @@ describe('ImageCanvas Workspace-first design experience', () => { videoQuote.finalPrompt, { ...videoQuote.generationParameters, - durationSeconds: 6, + durationSeconds: 15, }, )); });