diff --git a/.project-docs/30-worklog/tasks/20260818-video-confirm-media-9b7c.md b/.project-docs/30-worklog/tasks/20260818-video-confirm-media-9b7c.md new file mode 100644 index 0000000..49890d3 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260818-video-confirm-media-9b7c.md @@ -0,0 +1,47 @@ +# Task: Fix video intent rendering as image confirmation + +## Identity + +- Task ID: 20260818-video-confirm-media-9b7c +- Mode: Feature +- Branch: codex/20260818-video-confirm-media-9b7c-video-confirm-media +- Worktree: D:\Datas\OthersProjects\makelore-video-confirm-media-9b7c +- Base commit: 11b19832a35477d2136c6ea953dd9c408fd84816 +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Normalize the server's public generation-option payload for the Electron/Main-to-Renderer shared design contract. +- Support both canonical client options (`value`/`disabled`) and the current server configuration shape (`id` or `seconds`/`enabled`) without changing the Renderer API. +- Add a regression proving a video Quote retains its medium and exposes selectable 2/6 second durations. + +## Intent And Constraints + +- The raw trace proves this run reached `medium=video` and `bailian_video_direct_v1`; the UI failure was caused by option-shape loss, not by a server image decision. +- Preserve existing image and legacy conversation compatibility. +- Do not expose provider credentials or change the HTTP API; normalization belongs at the Electron adapter boundary. + +## Outcome + +- Added a typed adapter normalizer that maps `value`/`id`/`seconds` to the shared `value` field and maps `enabled=false` to `disabled=true` (while retaining canonical payloads). +- The Renderer now receives video durations as numeric selectable options, so the duration control and video confirmation validation can work with the live server response. +- Added a regression test using the server-shaped video options and asserting the mapped video medium, model, and durations. + +## Verification + +- `pnpm exec vitest run tests/unit/works-square-design-workspace.test.ts` — 32 passed. +- `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 electron/image-workspace/works-square-workspace.ts tests/unit/works-square-design-workspace.test.ts` — passed. +- `pnpm run build:vite` — passed (existing chunk-size and dynamic-import warnings only). +- `git diff --check` — passed (only the repository's CRLF conversion warnings). + +## Follow-ups + +- Ship this client commit with the server intent-guard commit; run the packaged desktop smoke test against a live video Quote. +- A future API contract cleanup may make the server publish canonical option names directly, but the adapter normalizer is the compatibility boundary for now. + +## Promotion Candidates + +- None recorded. diff --git a/electron/image-workspace/works-square-workspace.ts b/electron/image-workspace/works-square-workspace.ts index 24878e3..5006832 100644 --- a/electron/image-workspace/works-square-workspace.ts +++ b/electron/image-workspace/works-square-workspace.ts @@ -45,6 +45,17 @@ type ServerBrief = { missing_decision: string | null; }; +type ServerGenerationOption = { + value?: string | number; + id?: string | number; + seconds?: number; + label?: string; + default?: boolean; + disabled?: boolean; + enabled?: boolean; + multiplier?: number; +}; + type ServerQuote = { quote_id: string; status: DesignGenerationQuote['status']; @@ -60,10 +71,10 @@ type ServerQuote = { duration_seconds: number | null; }; generation_options: { - models: Array>; - resolutions: Array>; - durations: Array>; - aspect_ratios: Array>; + models: ServerGenerationOption[]; + resolutions: ServerGenerationOption[]; + durations: ServerGenerationOption[]; + aspect_ratios: ServerGenerationOption[]; }; pricing: { schema: string; @@ -281,6 +292,22 @@ function mapBrief(brief: ServerBrief): DesignBrief { }; } +function mapGenerationOptions( + options: ServerGenerationOption[] | undefined, +): DesignGenerationOption[] { + return (options ?? []).flatMap((option) => { + const value = option.value ?? option.id ?? option.seconds; + if (typeof value !== 'string' && typeof value !== 'number') return []; + return [{ + value: value as TValue, + label: option.label ?? String(value), + default: option.default ?? false, + disabled: option.disabled ?? option.enabled === false, + multiplier: option.multiplier ?? 1, + }]; + }); +} + function mapQuote(quote: ServerQuote | null): DesignGenerationQuote | null { if (!quote) return null; return { @@ -298,10 +325,10 @@ function mapQuote(quote: ServerQuote | null): DesignGenerationQuote | null { durationSeconds: quote.generation_parameters.duration_seconds, }, generationOptions: { - models: quote.generation_options.models, - resolutions: quote.generation_options.resolutions, - durations: quote.generation_options.durations, - aspectRatios: quote.generation_options.aspect_ratios, + models: mapGenerationOptions(quote.generation_options.models), + resolutions: mapGenerationOptions(quote.generation_options.resolutions), + durations: mapGenerationOptions(quote.generation_options.durations), + aspectRatios: mapGenerationOptions(quote.generation_options.aspect_ratios), }, pricing: { schema: quote.pricing.schema, diff --git a/tests/unit/works-square-design-workspace.test.ts b/tests/unit/works-square-design-workspace.test.ts index b0de8aa..d62cd32 100644 --- a/tests/unit/works-square-design-workspace.test.ts +++ b/tests/unit/works-square-design-workspace.test.ts @@ -592,6 +592,53 @@ describe('Works Square AI design adapter', () => { ); }); + it('normalizes the server video option shape so the duration picker stays available', async () => { + const videoConversation = { + ...serverConversation, + brief: { ...serverConversation.brief, medium: 'video' }, + messages: serverConversation.messages.map((message) => ({ + ...message, + generation_quote: { + ...message.generation_quote!, + medium: 'video', + generation_parameters: { + ...message.generation_quote!.generation_parameters, + model: 'wan2.7-i2v-2026-04-25', + resolution: '720P', + aspect_ratio: '4:5', + duration_seconds: 4, + }, + generation_options: { + models: [{ id: 'wan2.7-i2v-2026-04-25', label: '万相视频', enabled: true, multiplier: 1 }], + resolutions: [{ value: '720P', label: '720P', enabled: true, multiplier: 1 }], + durations: [ + { seconds: 2, label: '2 秒', enabled: true, multiplier: 1 }, + { seconds: 6, label: '6 秒', enabled: true, multiplier: 1 }, + ], + aspect_ratios: [{ value: '4:5', label: '4:5', default: true, enabled: true, multiplier: 1 }], + }, + }, + })), + }; + const fetchMock = vi.fn().mockResolvedValueOnce(jsonResponse(videoConversation)); + const adapter = new WorksSquareDesignWorkspace({ + apiBaseUrl: 'https://square.example', + fetchImpl: fetchMock, + }); + + const mapped = await adapter.getConversation('workspace-one', 'conversation-one'); + const quote = mapped.messages[0].generationQuote!; + + expect(mapped.brief.medium).toBe('video'); + expect(quote.medium).toBe('video'); + expect(quote.generationOptions.models[0]).toMatchObject({ + value: 'wan2.7-i2v-2026-04-25', + disabled: false, + }); + expect(quote.generationOptions.durations.map((option) => option.value)).toEqual([2, 6]); + expect(quote.generationOptions.durations.every((option) => !option.disabled)).toBe(true); + }); + it('uploads a local reference image as multipart data and maps the returned Asset', async () => { const fetchMock = vi.fn().mockResolvedValueOnce(jsonResponse({ asset_id: 'asset-uploaded',