fix(design): distinguish saved video settings from preparation needs
This commit is contained in:
@@ -169,7 +169,7 @@ export function YouthCreationCard({
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [medium, setMedium] = useState(values.intent.media ?? 'image');
|
||||
const [aspectRatio, setAspectRatio] = useState(values.output.aspect_ratio ?? '');
|
||||
const [duration, setDuration] = useState(values.video.total_duration_seconds ?? 6);
|
||||
const [duration, setDuration] = useState(values.video.total_duration_seconds);
|
||||
const [variantCount, setVariantCount] = useState(values.output.variant_count ?? 1);
|
||||
const promptRef = useRef<HTMLTextAreaElement>(null);
|
||||
const uploadInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -183,7 +183,7 @@ export function YouthCreationCard({
|
||||
useEffect(() => {
|
||||
setMedium(values.intent.media ?? 'image');
|
||||
setAspectRatio(values.output.aspect_ratio ?? '');
|
||||
setDuration(values.video.total_duration_seconds ?? 6);
|
||||
setDuration(values.video.total_duration_seconds);
|
||||
setVariantCount(values.output.variant_count ?? 1);
|
||||
}, [
|
||||
values.intent.media,
|
||||
@@ -205,6 +205,9 @@ export function YouthCreationCard({
|
||||
));
|
||||
const busy = localBusy || submitting || uploading;
|
||||
const blockers = quoteBlockers.filter((issue) => issue.severity === 'blocker');
|
||||
const inputBlockers = blockers.filter((issue) => medium !== 'video' || issue.code !== 'video_shots_required');
|
||||
const needsMotionPreparation = medium === 'video'
|
||||
&& (values.video.shots.length === 0 || blockers.some((issue) => issue.code === 'video_shots_required'));
|
||||
const options = generationOptions ?? UNAVAILABLE_GENERATION_OPTIONS;
|
||||
const activeOptions = options[medium];
|
||||
const supportedAspectRatios = activeOptions.supportedAspectRatios;
|
||||
@@ -228,7 +231,7 @@ export function YouthCreationCard({
|
||||
&& !unsupportedReferences
|
||||
&& unboundReferenceNumbers.length === 0
|
||||
&& variantCount <= activeOptions.maxOutputCount
|
||||
&& !(medium === 'video' && !supportedDurations.includes(duration));
|
||||
&& !(medium === 'video' && (duration === null || !supportedDurations.includes(duration)));
|
||||
const canAddReference = medium === 'video'
|
||||
? activeOptions.supportedReferenceRoles.includes('first_frame')
|
||||
&& (!selectedFirstFrameAsset
|
||||
@@ -313,7 +316,15 @@ export function YouthCreationCard({
|
||||
) => {
|
||||
if (busy) return;
|
||||
optimisticUpdate();
|
||||
void runOperations(operations);
|
||||
void runOperations(operations).then((updated) => {
|
||||
// The controls must reflect accepted values, including after a failed save.
|
||||
const current = useImageWorkspaceStore.getState().workspace;
|
||||
if (current?.workspace.workspaceId !== workspace.workspace.workspaceId) return;
|
||||
const accepted = updated?.form.specification.values ?? current.form.specification.values;
|
||||
setAspectRatio(accepted.output.aspect_ratio ?? '');
|
||||
setDuration(accepted.video.total_duration_seconds);
|
||||
setVariantCount(accepted.output.variant_count ?? 1);
|
||||
});
|
||||
};
|
||||
|
||||
const uploadReference = async (file: File) => {
|
||||
@@ -424,9 +435,10 @@ export function YouthCreationCard({
|
||||
if (saveState === 'repricing') return { text: '正在重新核价', icon: <Loader2 className="h-4 w-4 animate-spin" /> };
|
||||
if (saveState === 'error') return { text: '方案需要重试', icon: <RefreshCw className="h-4 w-4" /> };
|
||||
if (promptDirty) return { text: '提示词尚未保存', icon: <Sparkles className="h-4 w-4" /> };
|
||||
if (blockers.length > 0) return { text: '还需要补充一点信息', icon: <Sparkles className="h-4 w-4" /> };
|
||||
if (inputBlockers.length > 0 || (medium === 'video' && duration === null)) return { text: '还需要补充一点信息', icon: <Sparkles className="h-4 w-4" /> };
|
||||
if (needsMotionPreparation) return { text: '视频动作待整理', icon: <Sparkles className="h-4 w-4" /> };
|
||||
return { text: offeredQuote ? '方案已核价' : '方案已更新', icon: <Check className="h-4 w-4" /> };
|
||||
}, [blockers.length, offeredQuote, promptDirty, saveState]);
|
||||
}, [inputBlockers.length, medium, duration, needsMotionPreparation, offeredQuote, promptDirty, saveState]);
|
||||
|
||||
if (!hasActiveCreationPlan(workspace)) return null;
|
||||
|
||||
@@ -707,7 +719,7 @@ export function YouthCreationCard({
|
||||
void runOperations(operations, { prepare: true }).then((updated) => {
|
||||
if (!updated) {
|
||||
setMedium(values.intent.media ?? 'image');
|
||||
setDuration(values.video.total_duration_seconds ?? 6);
|
||||
setDuration(values.video.total_duration_seconds);
|
||||
}
|
||||
});
|
||||
}}
|
||||
@@ -751,7 +763,31 @@ export function YouthCreationCard({
|
||||
</Select>
|
||||
</label>
|
||||
|
||||
{medium === 'video' && (
|
||||
{medium === 'video' && duration === null && (
|
||||
<label className="flex items-center gap-2 text-xs font-medium text-foreground">
|
||||
时长
|
||||
<Select
|
||||
aria-label="时长"
|
||||
value=""
|
||||
disabled={busy || supportedDurations.length === 0}
|
||||
className="h-8 w-[110px] rounded-lg px-2 py-0 pr-7 text-xs"
|
||||
onChange={(event) => {
|
||||
const next = Number(event.currentTarget.value);
|
||||
changeParameter(
|
||||
[{ kind: 'set', path: 'video.total_duration_seconds', value: next }],
|
||||
() => setDuration(next),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="" disabled>请选择时长</option>
|
||||
{supportedDurations.map((seconds) => (
|
||||
<option key={seconds} value={seconds}>{seconds} 秒</option>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{medium === 'video' && duration !== null && (
|
||||
<Stepper
|
||||
label="时长"
|
||||
value={duration}
|
||||
@@ -786,9 +822,15 @@ export function YouthCreationCard({
|
||||
</p>
|
||||
)}
|
||||
|
||||
{blockers.length > 0 && (
|
||||
{needsMotionPreparation && (
|
||||
<p className="rounded-xl bg-surface-subtle px-3 py-2 text-xs leading-5 text-muted-foreground">
|
||||
{youthCompilationIssueMessage({ code: 'video_shots_required', message: '', severity: 'blocker' })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{inputBlockers.length > 0 && (
|
||||
<div className="rounded-xl border border-destructive/15 bg-destructive/[0.035] px-3 py-2 text-xs leading-5 text-destructive">
|
||||
{blockers.map((blocker) => (
|
||||
{inputBlockers.map((blocker) => (
|
||||
<p key={`${blocker.code}-${blocker.path ?? ''}`}>{youthCompilationIssueMessage(blocker)}</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ const COMPILATION_COPY: Readonly<Record<string, string>> = {
|
||||
image_direction_required: '再说一句你想画什么,或想把图片改成什么样。',
|
||||
video_duration_required: '先选择视频时长。',
|
||||
video_first_frame_required: '先选择视频从哪张图片开始。',
|
||||
video_shots_required: '至少写一个视频画面。',
|
||||
video_shots_required: '视频动作还没整理好。点击“准备制作方案”,AI 会根据你的描述继续整理。',
|
||||
video_shot_timing_required: '多个视频画面需要安排好先后时间,请让 AI 帮你整理。',
|
||||
video_timeline_must_start_at_zero: '第一个视频画面需要从开头开始。',
|
||||
video_timeline_gap: '视频画面之间有空缺,请让 AI 帮你重新安排。',
|
||||
|
||||
Reference in New Issue
Block a user