fix(canvas): block invalid video duration confirmation

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

No files matched your search

@@ -29,6 +29,8 @@
- The confirmation card now filters enabled server-provided video durations to the inclusive 2-6 second client boundary. - 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. - 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. - 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. - Existing request seams were verified: Renderer sends `durationSeconds: 6`, and the Main cloud adapter sends `duration_seconds: 6` to Works Square.
## Verification ## 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 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. - `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. - `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 ## Follow-ups
+25 -1
View File
@@ -268,6 +268,20 @@ type EditableGenerationParameters = Partial<Pick<
const MIN_VIDEO_DURATION_SECONDS = 2; const MIN_VIDEO_DURATION_SECONDS = 2;
const MAX_VIDEO_DURATION_SECONDS = 6; 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 { function quoteDraftFromQuote(quote: DesignGenerationQuote): GenerationQuoteDraft {
return { return {
quoteId: quote.quoteId, quoteId: quote.quoteId,
@@ -740,6 +754,10 @@ function QuoteCard({
const selectedDuration = availableDurations.some( const selectedDuration = availableDurations.some(
(option) => option.value === draft.generationParameters.durationSeconds, (option) => option.value === draft.generationParameters.durationSeconds,
) ? String(draft.generationParameters.durationSeconds) : ''; ) ? String(draft.generationParameters.durationSeconds) : '';
const durationSelectionValid = hasSelectableVideoDuration(
quote,
draft.generationParameters,
);
const updateParameters = (next: EditableGenerationParameters) => { const updateParameters = (next: EditableGenerationParameters) => {
onDraftChange({ onDraftChange({
...draft, ...draft,
@@ -867,7 +885,7 @@ function QuoteCard({
size="sm" size="sm"
className="h-8 rounded-full bg-brand px-3 text-xs font-semibold text-primary-foreground" className="h-8 rounded-full bg-brand px-3 text-xs font-semibold text-primary-foreground"
onClick={onConfirm} onClick={onConfirm}
disabled={busy || dirty || repricing} disabled={busy || dirty || repricing || !durationSelectionValid}
> >
{busy || repricing {busy || repricing
? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" /> ? <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
@@ -1153,6 +1171,7 @@ export function ImageCanvas() {
setQuoteDirty(true); setQuoteDirty(true);
setRepricingQuoteId(quote.quoteId); setRepricingQuoteId(quote.quoteId);
setQuoteUpdateError(null); setQuoteUpdateError(null);
setActionError(null);
quoteDraftVersionRef.current += 1; quoteDraftVersionRef.current += 1;
const version = quoteDraftVersionRef.current; const version = quoteDraftVersionRef.current;
if (quoteRepriceTimerRef.current !== null) clearTimeout(quoteRepriceTimerRef.current); if (quoteRepriceTimerRef.current !== null) clearTimeout(quoteRepriceTimerRef.current);
@@ -1189,6 +1208,11 @@ export function ImageCanvas() {
setActionError('当前报价正在更新,请等待重新报价完成后再确认。'); setActionError('当前报价正在更新,请等待重新报价完成后再确认。');
return; return;
} }
if (!quote || quote.quoteId !== quoteId
|| !hasSelectableVideoDuration(quote, draft.generationParameters)) {
setActionError('请选择 2 至 6 秒的视频时长,等待重新报价完成后再确认。');
return;
}
const requestedWorkspaceId = workspace.workspaceId; const requestedWorkspaceId = workspace.workspaceId;
const requestedConversationId = conversation.conversationId; const requestedConversationId = conversation.conversationId;
setConfirmingQuoteId(quoteId); setConfirmingQuoteId(quoteId);
+9
View File
@@ -544,6 +544,15 @@ describe('ImageCanvas Workspace-first design experience', () => {
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']);
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' } }); fireEvent.change(durationSelect, { target: { value: '6' } });