fix(canvas): block invalid video duration confirmation

This commit is contained in:
2026-08-18 11:32:38 +08:00
parent 566f99f5ed
commit 11b19832a3
3 changed files with 39 additions and 1 deletions

View File

@@ -29,6 +29,8 @@
- 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.
- Final-review P1 follow-up: a video Quote with `null`, an out-of-range duration, a disabled duration, or a duration absent from the filtered server options now disables the confirmation button and is rejected again inside the shared `handleConfirm` path used by direct, quick-reply, and text confirmation.
- Only an explicit allowed selection followed by a successful server re-quote re-enables confirmation.
- Existing request seams were verified: Renderer sends `durationSeconds: 6`, and the Main cloud adapter sends `duration_seconds: 6` to Works Square.
## Verification
@@ -40,6 +42,9 @@
- `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.
- Final-review P1 red test: the legacy 10-second Quote initially left `确认并开始生成` enabled.
- Final-review P1 focused regression: legacy 10-second initial state disables confirmation and text confirmation sends no request; selecting 6 seconds, completing re-quote, and confirming sends 6 — passed.
- Post-P1 focused suite: 3 files / 92 tests passed; typecheck and scoped ESLint passed; Renderer/Main/Preload `build:vite` passed with the same existing warnings.
## Follow-ups

View File

@@ -268,6 +268,20 @@ type EditableGenerationParameters = Partial<Pick<
const MIN_VIDEO_DURATION_SECONDS = 2;
const MAX_VIDEO_DURATION_SECONDS = 6;
function hasSelectableVideoDuration(
quote: DesignGenerationQuote,
parameters: DesignGenerationParameters,
): boolean {
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
));
}
function quoteDraftFromQuote(quote: DesignGenerationQuote): GenerationQuoteDraft {
return {
quoteId: quote.quoteId,
@@ -740,6 +754,10 @@ function QuoteCard({
const selectedDuration = availableDurations.some(
(option) => option.value === draft.generationParameters.durationSeconds,
) ? String(draft.generationParameters.durationSeconds) : '';
const durationSelectionValid = hasSelectableVideoDuration(
quote,
draft.generationParameters,
);
const updateParameters = (next: EditableGenerationParameters) => {
onDraftChange({
...draft,
@@ -867,7 +885,7 @@ function QuoteCard({
size="sm"
className="h-8 rounded-full bg-brand px-3 text-xs font-semibold text-primary-foreground"
onClick={onConfirm}
disabled={busy || dirty || repricing}
disabled={busy || dirty || repricing || !durationSelectionValid}
>
{busy || repricing
? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
@@ -1153,6 +1171,7 @@ export function ImageCanvas() {
setQuoteDirty(true);
setRepricingQuoteId(quote.quoteId);
setQuoteUpdateError(null);
setActionError(null);
quoteDraftVersionRef.current += 1;
const version = quoteDraftVersionRef.current;
if (quoteRepriceTimerRef.current !== null) clearTimeout(quoteRepriceTimerRef.current);
@@ -1189,6 +1208,11 @@ export function ImageCanvas() {
setActionError('当前报价正在更新,请等待重新报价完成后再确认。');
return;
}
if (!quote || quote.quoteId !== quoteId
|| !hasSelectableVideoDuration(quote, draft.generationParameters)) {
setActionError('请选择 2 至 6 秒的视频时长,等待重新报价完成后再确认。');
return;
}
const requestedWorkspaceId = workspace.workspaceId;
const requestedConversationId = conversation.conversationId;
setConfirmingQuoteId(quoteId);

View File

@@ -544,6 +544,15 @@ describe('ImageCanvas Workspace-first design experience', () => {
expect(durationSelect).toHaveValue('');
expect(within(durationSelect).getAllByRole('option').map((option) => option.getAttribute('value')))
.toEqual(['', '2', '6']);
expect(screen.getByRole('button', { name: '确认并开始生成' })).toBeDisabled();
fireEvent.change(screen.getByLabelText('设计需求'), {
target: { value: '确认生成' },
});
fireEvent.click(screen.getByRole('button', { name: '发送给设计 Agent' }));
expect(confirmImageWorkspaceGenerationMock).not.toHaveBeenCalled();
expect(await screen.findByRole('alert')).toHaveTextContent('请选择 2 至 6 秒的视频时长');
fireEvent.change(durationSelect, { target: { value: '6' } });