diff --git a/.project-docs/30-worklog/tasks/20260818-video-duration-client-a7c42f.md b/.project-docs/30-worklog/tasks/20260818-video-duration-client-a7c42f.md new file mode 100644 index 0000000..5a01280 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260818-video-duration-client-a7c42f.md @@ -0,0 +1,55 @@ +# Task: Fix AI design video duration selector + +## Identity + +- Task ID: 20260818-video-duration-client-a7c42f +- Mode: Feature +- Branch: main +- Worktree: D:\Datas\OthersProjects\makelore +- Base commit: 9e1e03f58312dcdff53f520be7247c9921ec288b +- Owner: codex-video-duration-client +- Status: Ready for integration + +## Scope + +- Audit and fix the AI Design video confirmation duration selector in `src/pages/ImageCanvas/index.tsx`. +- Defensively expose only enabled server-provided duration options from 2 through 6 seconds, including when an older server Quote still selects or advertises 7-15 seconds. +- Verify that selecting 6 seconds survives Quote repricing and is sent by both the Quote PATCH and final confirmation request. +- Add focused Renderer regression coverage in `tests/unit/image-canvas-page.test.tsx` and verify the existing Main-to-Works snake-case request mapping in `tests/unit/works-square-design-workspace.test.ts`. + +## Intent And Constraints + +- Own only `src/pages/ImageCanvas/index.tsx`, `tests/unit/image-canvas-page.test.tsx`, `tests/unit/works-square-design-workspace.test.ts`, and this task record. +- Preserve Conversation-owned Quote state, server-authoritative repricing, and existing request field naming across Renderer and Main. +- Do not modify the Works Square server, Electron Main routes/adapters, unrelated AI Design interactions, or files owned by peer tasks. +- Keep invalid legacy selected durations visibly unselected until the user chooses an allowed value; do not silently submit a replacement duration without a fresh server Quote. + +## Outcome + +- The confirmation card now filters enabled server-provided video durations to the inclusive 2-6 second client boundary. +- A legacy Quote whose selected duration is outside that boundary renders an explicit disabled `请选择时长` placeholder instead of visually selecting one allowed value while retaining a different hidden draft value. +- Selecting 6 seconds updates the controlled draft, survives the debounced server re-quote, and is used by final confirmation. +- Existing request seams were verified: Renderer sends `durationSeconds: 6`, and the Main cloud adapter sends `duration_seconds: 6` to Works Square. + +## Verification + +- Red test: the new legacy-duration regression initially failed because the native select visually resolved the unmatched controlled value `10` to option `2`. +- `corepack pnpm exec vitest run tests/unit/image-canvas-page.test.tsx -t "limits legacy video durations"` — passed after the fix (1 passed, 46 skipped). +- `corepack pnpm exec vitest run tests/unit/image-canvas-page.test.tsx tests/unit/image-workspace-api.test.ts tests/unit/works-square-design-workspace.test.ts` — passed (3 files, 92 tests). +- `corepack pnpm run typecheck` — passed. +- `corepack pnpm exec eslint src/pages/ImageCanvas/index.tsx tests/unit/image-canvas-page.test.tsx tests/unit/works-square-design-workspace.test.ts` — passed with no output. +- `corepack pnpm run build:vite` — passed for Renderer, Electron Main, and Preload; only the existing dynamic-import and chunk-size warnings were reported. +- `git diff --check` — passed; Git reported only the repository's LF-to-CRLF checkout warning. + +## Follow-ups + +- Production acceptance still requires a real server Quote containing the supported duration options; the client intentionally does not synthesize missing 2-6 second choices. + +## Promotion Candidates + +- Target: `.project-docs/40-domain/business-rules.md` during Integration Gate. + - Proposal: record that the AI Design client displays only enabled server-provided video duration options in the inclusive 2-6 second range; legacy selected values outside the range require an explicit allowed selection and fresh server Quote before confirmation. + - Evidence: focused controlled-select regression, Quote PATCH assertion, confirmation assertion, and Main-to-Works `duration_seconds: 6` contract test. + - Future impact: prevents older server configuration from re-exposing unsupported long video durations or silently submitting a hidden value different from the visible selection. + - Semantic conflicts: none; server pricing and option authority remain unchanged. + - Human confirmation required: no. diff --git a/src/pages/ImageCanvas/index.tsx b/src/pages/ImageCanvas/index.tsx index 67ba57a..721b641 100644 --- a/src/pages/ImageCanvas/index.tsx +++ b/src/pages/ImageCanvas/index.tsx @@ -265,6 +265,9 @@ type EditableGenerationParameters = Partial>; +const MIN_VIDEO_DURATION_SECONDS = 2; +const MAX_VIDEO_DURATION_SECONDS = 6; + function quoteDraftFromQuote(quote: DesignGenerationQuote): GenerationQuoteDraft { return { quoteId: quote.quoteId, @@ -728,8 +731,15 @@ function QuoteCard({ onConfirm: () => void; }) { const availableResolutions = quote.generationOptions.resolutions.filter((option) => !option.disabled); - const availableDurations = quote.generationOptions.durations.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 availableAspectRatios = quote.generationOptions.aspectRatios.filter((option) => !option.disabled); + const selectedDuration = availableDurations.some( + (option) => option.value === draft.generationParameters.durationSeconds, + ) ? String(draft.generationParameters.durationSeconds) : ''; const updateParameters = (next: EditableGenerationParameters) => { onDraftChange({ ...draft, @@ -819,9 +829,7 @@ function QuoteCard({ aria-label="视频时长" data-testid="design-quote-duration" className="mt-1.5 h-9 bg-background text-xs" - value={draft.generationParameters.durationSeconds === null - ? '' - : String(draft.generationParameters.durationSeconds)} + value={selectedDuration} disabled={busy} onChange={(event) => { const option = availableDurations.find( @@ -830,6 +838,9 @@ function QuoteCard({ if (option) updateParameters({ durationSeconds: option.value }); }} > + {selectedDuration === '' ? ( + + ) : null} {availableDurations.map((option) => (