fix(canvas): use server video duration options
This commit is contained in:
1 parent
86ffeac066
commit
31332f6d76
3 files changed
+76
-18
No files matched your search
@@ -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.
|
||||||
@@ -265,9 +265,6 @@ type EditableGenerationParameters = Partial<Pick<
|
|||||||
'resolution' | 'aspectRatio' | 'durationSeconds'
|
'resolution' | 'aspectRatio' | 'durationSeconds'
|
||||||
>>;
|
>>;
|
||||||
|
|
||||||
const MIN_VIDEO_DURATION_SECONDS = 2;
|
|
||||||
const MAX_VIDEO_DURATION_SECONDS = 6;
|
|
||||||
|
|
||||||
function hasSelectableVideoDuration(
|
function hasSelectableVideoDuration(
|
||||||
quote: DesignGenerationQuote,
|
quote: DesignGenerationQuote,
|
||||||
parameters: DesignGenerationParameters,
|
parameters: DesignGenerationParameters,
|
||||||
@@ -275,8 +272,6 @@ function hasSelectableVideoDuration(
|
|||||||
if (quote.medium !== 'video') return true;
|
if (quote.medium !== 'video') return true;
|
||||||
const durationSeconds = parameters.durationSeconds;
|
const durationSeconds = parameters.durationSeconds;
|
||||||
return durationSeconds !== null
|
return durationSeconds !== null
|
||||||
&& durationSeconds >= MIN_VIDEO_DURATION_SECONDS
|
|
||||||
&& durationSeconds <= MAX_VIDEO_DURATION_SECONDS
|
|
||||||
&& quote.generationOptions.durations.some((option) => (
|
&& quote.generationOptions.durations.some((option) => (
|
||||||
!option.disabled && option.value === durationSeconds
|
!option.disabled && option.value === durationSeconds
|
||||||
));
|
));
|
||||||
@@ -745,11 +740,7 @@ function QuoteCard({
|
|||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
}) {
|
}) {
|
||||||
const availableResolutions = quote.generationOptions.resolutions.filter((option) => !option.disabled);
|
const availableResolutions = quote.generationOptions.resolutions.filter((option) => !option.disabled);
|
||||||
const availableDurations = quote.generationOptions.durations.filter((option) => (
|
const availableDurations = quote.generationOptions.durations.filter((option) => !option.disabled);
|
||||||
!option.disabled
|
|
||||||
&& option.value >= MIN_VIDEO_DURATION_SECONDS
|
|
||||||
&& option.value <= MAX_VIDEO_DURATION_SECONDS
|
|
||||||
));
|
|
||||||
const availableAspectRatios = quote.generationOptions.aspectRatios.filter((option) => !option.disabled);
|
const availableAspectRatios = quote.generationOptions.aspectRatios.filter((option) => !option.disabled);
|
||||||
const selectedDuration = availableDurations.some(
|
const selectedDuration = availableDurations.some(
|
||||||
(option) => option.value === draft.generationParameters.durationSeconds,
|
(option) => option.value === draft.generationParameters.durationSeconds,
|
||||||
@@ -1210,7 +1201,7 @@ export function ImageCanvas() {
|
|||||||
}
|
}
|
||||||
if (!quote || quote.quoteId !== quoteId
|
if (!quote || quote.quoteId !== quoteId
|
||||||
|| !hasSelectableVideoDuration(quote, draft.generationParameters)) {
|
|| !hasSelectableVideoDuration(quote, draft.generationParameters)) {
|
||||||
setActionError('请选择 2 至 6 秒的视频时长,等待重新报价完成后再确认。');
|
setActionError('请选择服务端提供的视频时长,等待重新报价完成后再确认。');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const requestedWorkspaceId = workspace.workspaceId;
|
const requestedWorkspaceId = workspace.workspaceId;
|
||||||
|
|||||||
@@ -510,7 +510,7 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
|||||||
expect(screen.getByRole('button', { name: '确认并开始生成' })).toBeEnabled();
|
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 videoConversation = conversationFixture();
|
||||||
const videoQuote = videoConversation.messages[1].generationQuote!;
|
const videoQuote = videoConversation.messages[1].generationQuote!;
|
||||||
videoConversation.brief.medium = 'video';
|
videoConversation.brief.medium = 'video';
|
||||||
@@ -543,7 +543,7 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
|||||||
const durationSelect = await screen.findByTestId('design-quote-duration');
|
const durationSelect = await screen.findByTestId('design-quote-duration');
|
||||||
expect(durationSelect).toHaveValue('');
|
expect(durationSelect).toHaveValue('');
|
||||||
expect(within(durationSelect).getAllByRole('option').map((option) => option.getAttribute('value')))
|
expect(within(durationSelect).getAllByRole('option').map((option) => option.getAttribute('value')))
|
||||||
.toEqual(['', '2', '6']);
|
.toEqual(['', '2', '6', '7', '15']);
|
||||||
expect(screen.getByRole('button', { name: '确认并开始生成' })).toBeDisabled();
|
expect(screen.getByRole('button', { name: '确认并开始生成' })).toBeDisabled();
|
||||||
|
|
||||||
fireEvent.change(screen.getByLabelText('设计需求'), {
|
fireEvent.change(screen.getByLabelText('设计需求'), {
|
||||||
@@ -552,9 +552,9 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));
|
fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));
|
||||||
|
|
||||||
expect(confirmImageWorkspaceGenerationMock).not.toHaveBeenCalled();
|
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(
|
await waitFor(() => expect(updateImageWorkspaceGenerationQuoteMock).toHaveBeenCalledWith(
|
||||||
'workspace-cloud',
|
'workspace-cloud',
|
||||||
@@ -562,10 +562,10 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
|||||||
videoQuote.finalPrompt,
|
videoQuote.finalPrompt,
|
||||||
{
|
{
|
||||||
...videoQuote.generationParameters,
|
...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: '确认并开始生成' }));
|
fireEvent.click(screen.getByRole('button', { name: '确认并开始生成' }));
|
||||||
|
|
||||||
await waitFor(() => expect(confirmImageWorkspaceGenerationMock).toHaveBeenCalledWith(
|
await waitFor(() => expect(confirmImageWorkspaceGenerationMock).toHaveBeenCalledWith(
|
||||||
@@ -577,7 +577,7 @@ describe('ImageCanvas Workspace-first design experience', () => {
|
|||||||
videoQuote.finalPrompt,
|
videoQuote.finalPrompt,
|
||||||
{
|
{
|
||||||
...videoQuote.generationParameters,
|
...videoQuote.generationParameters,
|
||||||
durationSeconds: 6,
|
durationSeconds: 15,
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user