返回 VideoClaw
WorkflowPanel.tsx
根目录 / video-claw / video-claw / frontend / components / WorkflowPanel.tsx
1 'use client';
2
3 import React, { useState, useRef, useEffect, useCallback } from 'react';
4 import { useRouter, useSearchParams } from 'next/navigation';
5 import {
6 startProject,
7 executeStage,
8 parseStreamEvents,
9 getProjectStatus,
10 getProjectStatusFromDisk,
11 continueWorkflow,
12 stopProject,
13 intervene,
14 getArtifact,
15 fetchSessions,
16 updateModels,
17 deleteSession,
18 saveSelections,
19 } from '@/lib/workflowApi';
20 import TopBar, { STAGES, type ModelConfig } from './TopBar';
21 import HomePage, { type ProjectParams } from './HomePage';
22 import {
23 ScriptStage,
24 CharacterStage,
25 StoryboardStage,
26 ReferenceStage,
27 VideoStage,
28 PostProductionStage,
29 type StageState,
30 type StageStatus,
31 } from './stages';
32
33 const STAGE_ORDER: string[] = STAGES.map(s => s.id);
34
35 const STAGE_COMPONENTS: Record<string, React.ComponentType<any>> = {
36 script_generation: ScriptStage,
37 character_design: CharacterStage,
38 storyboard: StoryboardStage,
39 reference_generation: ReferenceStage,
40 video_generation: VideoStage,
41 post_production: PostProductionStage,
42 };
43
44 interface HistoryItem {
45 id: string;
46 idea: string;
47 style?: string;
48 date: string;
49 status: string;
50 stages?: Record<string, string>; // stageId => status 映射
51 }
52
53 function initStageStates(): Record<string, StageState> {
54 const states: Record<string, StageState> = {};
55 for (const s of STAGE_ORDER) {
56 states[s] = {
57 status: 'pending',
58 progress: 0,
59 progressMessage: '',
60 artifact: null,
61 error: null,
62 };
63 }
64 return states;
65 }
66
67 function hasDoneArtifactItems(artifact: any): boolean {
68 if (!artifact) return false;
69 return ['clips', 'images', 'scenes', 'characters', 'settings'].some(key => {
70 const items = artifact?.[key];
71 return Array.isArray(items) && items.some((item: any) => item?.status === 'done' || item?.selected);
72 });
73 }
74
75 function hasRunningArtifactItems(artifact: any): boolean {
76 if (!artifact) return false;
77 return ['clips', 'images', 'scenes', 'characters', 'settings'].some(key => {
78 const items = artifact?.[key];
79 return Array.isArray(items) && items.some((item: any) => item?.status === 'running');
80 });
81 }
82
83 function mergeAssetVersions(current?: string[], incoming?: string[]): string[] {
84 const merged: string[] = [];
85 [...(Array.isArray(current) ? current : []), ...(Array.isArray(incoming) ? incoming : [])].forEach(path => {
86 if (path && !merged.includes(path)) merged.push(path);
87 });
88 return merged;
89 }
90
91 function mergeAssetUpdateItem(item: any, assetUpdate: any): any {
92 const hasExistingSelection = Boolean(item?.selected);
93 const nextStatus = hasExistingSelection && ['done', 'failed'].includes(assetUpdate.status)
94 ? 'done'
95 : (assetUpdate.status ?? item.status);
96 return {
97 ...item,
98 status: nextStatus,
99 selected: hasExistingSelection
100 ? item.selected
101 : (assetUpdate.selected || item.selected),
102 versions: assetUpdate.versions
103 ? mergeAssetVersions(item.versions, assetUpdate.versions)
104 : item.versions,
105 rewrite_result: assetUpdate.rewrite_result ?? item.rewrite_result,
106 };
107 }
108
109 function getStageProgressSnapshot(status: any, stageId: string): Partial<StageState> {
110 const snapshot = status?.stage_progress?.[stageId];
111 if (!snapshot || typeof snapshot !== 'object') return {};
112 const progress = typeof snapshot.percent === 'number'
113 ? Math.max(0, Math.min(100, Math.round(snapshot.percent)))
114 : undefined;
115 const progressMessage = typeof snapshot.message === 'string' && snapshot.message.trim()
116 ? snapshot.message
117 : (typeof snapshot.step === 'string' ? snapshot.step : undefined);
118 return {
119 ...(typeof progress === 'number' ? { progress } : {}),
120 ...(progressMessage ? { progressMessage } : {}),
121 };
122 }
123
124 export default function WorkflowPanel() {
125 const router = useRouter();
126 const searchParams = useSearchParams();
127
128 const [sessionId, setSessionId] = useState<string | null>(null);
129 const [activeStage, setActiveStage] = useState<string | null>(null);
130 const [stageStates, setStageStates] = useState<Record<string, StageState>>(initStageStates());
131 const [isRunning, setIsRunning] = useState(false);
132 const [history, setHistory] = useState<HistoryItem[]>([]);
133 const [projectParams, setProjectParams] = useState<ProjectParams | null>(null);
134 const [autoMode, setAutoMode] = useState(false);
135 // 用于顶栏流程图状态判断
136 const [currentStageFromSession, setCurrentStageFromSession] = useState<string | null>(null);
137 const [completedStagesFromSession, setCompletedStagesFromSession] = useState<string[]>([]);
138
139 const abortRef = useRef<AbortController | null>(null);
140 const stoppedRef = useRef(false);
141 const autoModeRef = useRef(autoMode);
142 const pollRef = useRef<Set<string>>(new Set());
143
144 useEffect(() => {
145 autoModeRef.current = autoMode;
146 }, [autoMode]);
147
148 const handleAutoModeChange = useCallback((nextAutoMode: boolean) => {
149 autoModeRef.current = nextAutoMode;
150 setAutoMode(nextAutoMode);
151 }, []);
152
153 // 清理轮询
154 useEffect(() => {
155 return () => { pollRef.current.clear(); };
156 }, []);
157
158 // 轮询等待后端阶段完成。运行中优先读后端内存,后端重启后再回退到磁盘快照。
159 const pollForCompletion = useCallback(async (sid: string, stageId: string) => {
160 const key = `${sid}:${stageId}`;
161 pollRef.current.add(key);
162 for (let i = 0; i < 300; i++) { // 增加轮询次数(最多10分钟)
163 await new Promise(r => setTimeout(r, 2000));
164 if (!pollRef.current.has(key)) return;
165 try {
166 let status: any;
167 try {
168 status = await getProjectStatus(sid);
169 } catch {
170 status = await getProjectStatusFromDisk(sid);
171 }
172 if (!pollRef.current.has(key)) return;
173 setGlobalStatusMap(status.status || {});
174 const done = Object.keys(status.status || {}).filter(k => ["completed", "session_completed"].includes(status.status[k]));
175 const currentStageStatus = status.status?.[stageId] || 'idle';
176 const artifacts = status.artifacts || {};
177 const currentArtifact = artifacts[stageId];
178 const hasRunningItems = hasRunningArtifactItems(currentArtifact);
179 if (done.includes(stageId) && !hasRunningItems) {
180 const isWait = currentStageStatus === 'waiting' && status.current_stage === stageId;
181 let artifact = null;
182 try { artifact = (await getArtifact(sid, stageId)).artifact; } catch {}
183 updateStageState(stageId, {
184 status: isWait ? 'waiting' : 'completed',
185 progress: 100,
186 progressMessage: isWait ? '等待确认' : '已完成',
187 artifact,
188 });
189 pollRef.current.delete(key);
190 return;
191 }
192 if (currentStageStatus === 'error' && !hasRunningItems) {
193 updateStageState(stageId, {
194 status: 'error',
195 error: status.error || '执行出错',
196 progressMessage: '执行失败',
197 });
198 pollRef.current.delete(key);
199 return;
200 }
201 if (currentStageStatus === 'stopped' && !hasRunningItems) {
202 updateStageState(stageId, {
203 status: 'stopped',
204 error: '手动停止',
205 progressMessage: '已停止',
206 });
207 pollRef.current.delete(key);
208 return;
209 }
210 // 后台单卡重生成时,阶段状态可能仍是 waiting/completed;只要 item 还在 running 就继续轮询。
211 if (currentStageStatus !== 'running' && !hasRunningItems) {
212 updateStageState(stageId, {
213 status: currentStageStatus === 'waiting'
214 ? 'waiting'
215 : (done.includes(stageId) ? 'completed' : 'pending'),
216 progress: done.includes(stageId) ? 100 : undefined,
217 progressMessage: currentStageStatus === 'waiting'
218 ? '等待确认'
219 : (done.includes(stageId) ? '已完成' : undefined),
220 artifact: currentArtifact || null,
221 });
222 pollRef.current.delete(key);
223 return;
224 }
225 // 更新进度信息(优先使用后端持久化的 stage_progress 快照)
226 const update: Partial<StageState> = {
227 status: 'running',
228 ...getStageProgressSnapshot(status, stageId),
229 };
230
231 // 无论何种状态,只要有数据就更新 artifact
232 if (currentArtifact) {
233 update.artifact = currentArtifact;
234 }
235
236 if (!update.progressMessage && currentArtifact) {
237 // 检查是否有已生成的参考图/视频/分镜
238 const hasProgress = currentArtifact.scenes?.some((s: any) => s.versions?.length > 0) ||
239 currentArtifact.clips?.some((c: any) => c.versions?.length > 0) ||
240 currentArtifact.episodes?.some((e: any) => e.segments?.length > 0) ||
241 currentArtifact.characters?.length > 0 ||
242 currentArtifact.shots?.length > 0;
243 if (hasProgress) {
244 update.progressMessage = '执行中...(已生成部分资源)';
245 } else {
246 update.progressMessage = '执行中...';
247 }
248 } else if (!update.progressMessage) {
249 update.progressMessage = '执行中...';
250 }
251
252 if (Object.keys(update).length > 0) {
253 updateStageState(stageId, update);
254 }
255 } catch { /* retry */ }
256 }
257 pollRef.current.delete(key);
258 }, []);
259
260 // 检查 URL 参数,加载指定的 session 和阶段
261 useEffect(() => {
262 const sessionParam = searchParams.get('session');
263 const stageParam = searchParams.get('stage');
264 if (sessionParam) {
265 // 保存目标阶段,等会话加载完成后再设置
266 const targetStage = stageParam && STAGE_ORDER.includes(stageParam) ? stageParam : null;
267 handleResumeProject(sessionParam, targetStage);
268 }
269 }, [searchParams]);
270
271 // 页面加载时从后端获取历史记录
272 useEffect(() => {
273 fetchSessions()
274 .then(sessions => {
275 setHistory(
276 sessions.map((s: any) => ({
277 id: s.id,
278 idea: (s.title || s.idea || 'Untitled').slice(0, 60),
279 style: s.style || '',
280 date: s.date
281 ? new Date(s.date * 1000).toLocaleDateString('zh-CN')
282 : '',
283 status: Object.keys(s.status || {}).length > 0 ? 'partial' : 'new',
284 stages: s.status || {},
285 }))
286 );
287 })
288 .catch(() => {});
289 }, []);
290
291 const updateStageState = (stageId: string, update: Partial<StageState>) => {
292 setStageStates(prev => {
293 const current = prev[stageId];
294 const nextUpdate = { ...update };
295 if (
296 current?.status === 'running' &&
297 typeof update.progress === 'number' &&
298 update.progress < (current.progress || 0) &&
299 update.status !== 'completed' &&
300 update.status !== 'waiting'
301 ) {
302 nextUpdate.progress = current.progress;
303 }
304 return {
305 ...prev,
306 [stageId]: { ...current, ...nextUpdate },
307 };
308 });
309 };
310
311 // ── 停止执行 ──
312 const handleStop = async () => {
313 stoppedRef.current = true;
314 // 1. 断开前端 SSE 流
315 if (abortRef.current) {
316 abortRef.current.abort();
317 abortRef.current = null;
318 }
319 // 2. 通知后端停止
320 if (sessionId) {
321 try { await stopProject(sessionId); } catch { /* ignore */ }
322 }
323 setIsRunning(false);
324 // 3. 将所有 running 阶段标记为 stopped,保留已有的 artifact
325 setStageStates(prev => {
326 const next = { ...prev };
327 for (const s of STAGE_ORDER) {
328 if (next[s]?.status === 'running') {
329 // 如果已有 artifact 数据(如部分已生成的视频片段),保留为 waiting 状态以便用户操作
330 const hasArtifact = hasDoneArtifactItems(next[s]?.artifact);
331 if (hasArtifact) {
332 next[s] = { ...next[s], status: 'waiting', error: null, progressMessage: '已停止(保留已完成内容)' };
333 } else {
334 next[s] = { ...next[s], status: 'error', error: '已手动停止', progressMessage: '已停止' };
335 }
336 }
337 }
338 return next;
339 });
340 // 4. 尝试从后端获取最新 artifact(后端可能已保存部分结果)
341 if (sessionId) {
342 setTimeout(async () => {
343 for (const s of STAGE_ORDER) {
344 try {
345 const artResult = await getArtifact(sessionId, s);
346 if (artResult?.artifact) {
347 setStageStates(prev => {
348 const cur = prev[s];
349 if (!cur || cur.status === 'completed') return prev;
350 // 检查 artifact 是否含有效内容
351 const hasDone = hasDoneArtifactItems(artResult.artifact);
352 if (hasDone) {
353 return { ...prev, [s]: { ...cur, status: 'waiting', artifact: artResult.artifact, error: null, progressMessage: '已停止(保留已完成内容)' } };
354 }
355 return prev;
356 });
357 }
358 } catch { /* 该阶段无 artifact,跳过 */ }
359 }
360 }, 500);
361 }
362 };
363
364 // ── 执行单个阶段 ──
365 const runStage = async (sid: string, stageId: string, inputData: Record<string, any>) => {
366 if (stoppedRef.current) throw new Error('Stopped');
367 updateStageState(stageId, { status: 'running', progress: 0, progressMessage: '启动中...', error: null });
368 setActiveStage(stageId);
369
370 const controller = new AbortController();
371 abortRef.current = controller;
372
373 try {
374 const response = await executeStage(sid, stageId, inputData, controller.signal);
375
376 for await (const event of parseStreamEvents(response)) {
377 if (event.type === 'progress') {
378 updateStageState(stageId, {
379 progress: event.percent || 0,
380 progressMessage: event.message || '',
381 });
382 // 处理素材预览和逐个完成事件
383 if (event.data?.assets_preview) {
384 updateStageState(stageId, { artifact: event.data.assets_preview });
385 }
386 if (event.data?.asset_complete) {
387 const assetUpdate = event.data.asset_complete;
388 setStageStates(prev => {
389 const prevArt = prev[stageId]?.artifact;
390 if (!prevArt) return prev;
391 const key = assetUpdate.type as string;
392 const items = [...(prevArt[key] || [])];
393 const idx = items.findIndex((item: any) => item.id === assetUpdate.id);
394 if (idx >= 0) {
395 items[idx] = mergeAssetUpdateItem(items[idx], assetUpdate);
396 }
397 return {
398 ...prev,
399 [stageId]: { ...prev[stageId], artifact: { ...prevArt, [key]: items } },
400 };
401 });
402 }
403 // 处理剧本逐幕增量事件
404 if (event.data?.beat_sheet) {
405 setStageStates(prev => {
406 const prevArt = prev[stageId]?.artifact || {};
407 return {
408 ...prev,
409 [stageId]: {
410 ...prev[stageId],
411 artifact: { ...prevArt, phase: 'generating', beat_sheet: event.data.beat_sheet },
412 },
413 };
414 });
415 }
416 if (event.data?.act_complete) {
417 const actData = event.data.act_complete;
418 setStageStates(prev => {
419 const prevArt = prev[stageId]?.artifact || {};
420 const prevActs = prevArt.completed_acts || [];
421 return {
422 ...prev,
423 [stageId]: {
424 ...prev[stageId],
425 artifact: {
426 ...prevArt,
427 phase: 'generating',
428 completed_acts: [...prevActs, actData],
429 },
430 },
431 };
432 });
433 }
434 // 处理分镜逐场完成事件
435 if (event.data?.scene_shots_complete) {
436 const sceneData = event.data.scene_shots_complete;
437 setStageStates(prev => {
438 const prevArt = prev[stageId]?.artifact || {};
439 const prevShots = prevArt.shots || [];
440 return {
441 ...prev,
442 [stageId]: {
443 ...prev[stageId],
444 artifact: {
445 ...prevArt,
446 phase: 'generating',
447 shots: [...prevShots, ...sceneData.shots],
448 },
449 },
450 };
451 });
452 }
453 } else if (event.type === 'stage_complete') {
454 const newStatus: StageStatus = event.requires_intervention ? 'waiting' : 'completed';
455
456 let artifact = null;
457 try {
458 const artResult = await getArtifact(sid, stageId);
459 artifact = artResult.artifact;
460 } catch { /* ignore */ }
461
462 // 如果是后处理阶段,即使 artifact 为空也尝试从事件中读取预览数据
463 if (stageId === 'post_production' && !artifact && event.data?.assets_preview) {
464 artifact = event.data.assets_preview;
465 }
466
467 updateStageState(stageId, {
468 status: newStatus,
469 progress: 100,
470 progressMessage: newStatus === 'waiting' ? '等待确认' : '已完成',
471 artifact,
472 });
473 } else if (event.type === 'error') {
474 // 取消/停止类错误:尝试获取部分结果而不是直接报错
475 const isCancelError = /cancel|取消|停止/i.test(event.content || '');
476 if (isCancelError) {
477 let artifact = null;
478 try {
479 const artResult = await getArtifact(sid, stageId);
480 artifact = artResult?.artifact;
481 } catch { /* ignore */ }
482 // 如果有已完成的内容,显示为 waiting 状态
483 const hasDone = hasDoneArtifactItems(artifact);
484 if (hasDone) {
485 updateStageState(stageId, {
486 status: 'waiting',
487 progress: 100,
488 progressMessage: '已停止(保留已完成内容)',
489 artifact,
490 error: null,
491 });
492 return; // 不抛异常,让工作流正常停下
493 }
494 }
495 updateStageState(stageId, {
496 status: 'error',
497 error: event.content || 'Unknown error',
498 progressMessage: '执行失败',
499 });
500 throw new Error(event.content);
501 }
502 }
503 } catch (error: any) {
504 if (error.name !== 'AbortError') {
505 updateStageState(stageId, {
506 status: 'error',
507 error: error.message,
508 progressMessage: '执行失败',
509 });
510 }
511 throw error;
512 }
513 };
514
515 // ── 启动新项目 ──
516 const handleStartProject = async (params: ProjectParams, autoOverride?: boolean) => {
517 if (isRunning) return;
518 stoppedRef.current = false;
519 const useAutoMode = autoOverride !== undefined ? autoOverride : autoMode;
520 if (autoOverride !== undefined) handleAutoModeChange(autoOverride);
521 setIsRunning(true);
522 setStageStates(initStageStates());
523 setProjectParams(params);
524
525 try {
526 const result = await startProject({
527 idea: params.idea,
528 file_path: params.file_path, // 修复:将上传的文件路径传给后端
529 style: params.style,
530 video_ratio: params.video_ratio,
531 video_resolution: params.video_resolution,
532 llm_model: params.llm_model,
533 vlm_model: params.vlm_model,
534 image_t2i_model: params.image_t2i_model,
535 image_it2i_model: params.image_it2i_model,
536 video_generation_mode: params.video_generation_mode,
537 video_first_frame_model: params.video_first_frame_model,
538 video_start_end_model: params.video_start_end_model,
539 video_reference_model: params.video_reference_model,
540 video_model: params.video_model,
541 enable_concurrency: params.enable_concurrency,
542 web_search: params.web_search,
543 expand_idea: params.expand_idea,
544 episodes: params.episodes,
545 });
546 setSessionId(result.session_id);
547
548 // 添加到历史
549 setHistory(prev => [
550 {
551 id: result.session_id,
552 idea: params.idea.slice(0, 60),
553 style: params.style,
554 date: new Date().toLocaleDateString('zh-CN'),
555 status: 'running',
556 stages: {},
557 },
558 ...prev,
559 ]);
560
561 const inputData: Record<string, any> = {
562 idea: params.idea,
563 session_id: result.session_id,
564 style: params.style,
565 video_ratio: params.video_ratio,
566 video_resolution: params.video_resolution,
567 llm_model: params.llm_model,
568 vlm_model: params.vlm_model,
569 image_t2i_model: params.image_t2i_model,
570 image_it2i_model: params.image_it2i_model,
571 video_generation_mode: params.video_generation_mode,
572 video_first_frame_model: params.video_first_frame_model,
573 video_start_end_model: params.video_start_end_model,
574 video_reference_model: params.video_reference_model,
575 video_model: params.video_model,
576 scene_number: result.params?.scene_number,
577 expand_idea: params.expand_idea,
578 enable_concurrency: params.enable_concurrency,
579 web_search: params.web_search,
580 episodes: params.episodes,
581 auto_mode: useAutoMode,
582 };
583
584 for (const stageId of STAGE_ORDER) {
585 if (stoppedRef.current) break;
586 await runStage(result.session_id, stageId, inputData);
587
588 // 剧本生成完成后,用实际场景数更新 scene_number
589 if (stageId === 'script_generation') {
590 try {
591 const artResult = await getArtifact(result.session_id, 'script_generation');
592 if (artResult?.artifact?.scenes?.length) {
593 inputData.scene_number = artResult.artifact.scenes.length;
594 }
595 } catch { /* ignore */ }
596 }
597
598 const status = await getProjectStatus(result.session_id);
599 const stageStatus = status?.status?.[stageId];
600 setGlobalStatusMap(status.status || {});
601 // waiting: 等待用户介入(如选择角色/图片),不能自动 continue
602 // completed: 阶段完成,等待确认进入下一阶段
603 if (stageStatus === 'completed' || stageStatus === 'session_completed') {
604 if (autoModeRef.current) {
605 // 代理模式:自动确认并继续
606 await continueWorkflow(result.session_id);
607 updateStageState(stageId, { status: 'completed', progressMessage: '已自动确认' });
608 } else {
609 updateStageState(stageId, { status: 'completed' });
610 setActiveStage(stageId);
611 setIsRunning(false);
612 return;
613 }
614 } else if (stageStatus === 'waiting') {
615 // 用户需要介入,停止自动执行
616 updateStageState(stageId, { status: 'waiting' });
617 setActiveStage(stageId);
618 setIsRunning(false);
619 return;
620 }
621 }
622 } catch (error: any) {
623 if (!stoppedRef.current) console.error('Workflow error:', error);
624 } finally {
625 setIsRunning(false);
626 }
627 };
628
629 // ── 确认阶段并继续 ──
630 const handleConfirmStage = async (stageId: string) => {
631 if (!sessionId || isRunning) return;
632 setIsRunning(true);
633
634 try {
635 const result: any = await continueWorkflow(sessionId);
636 updateStageState(stageId, { status: 'completed', progressMessage: '已确认' });
637
638 // 更新顶栏状态
639 if (result.status_map) {
640 setGlobalStatusMap(result.status_map);
641 }
642 setCompletedStagesFromSession(prev => [...prev, stageId]);
643 if (result.next_stage) {
644 setCurrentStageFromSession(result.next_stage);
645 }
646
647 if (result.next_stage) {
648 const idx = STAGE_ORDER.indexOf(stageId);
649
650 // 构建完整的 inputData
651 const inputData: Record<string, any> = {
652 session_id: sessionId,
653 style: projectParams?.style,
654 llm_model: projectParams?.llm_model,
655 vlm_model: projectParams?.vlm_model,
656 image_t2i_model: projectParams?.image_t2i_model,
657 image_it2i_model: projectParams?.image_it2i_model,
658 video_generation_mode: projectParams?.video_generation_mode,
659 video_first_frame_model: projectParams?.video_first_frame_model,
660 video_start_end_model: projectParams?.video_start_end_model,
661 video_reference_model: projectParams?.video_reference_model,
662 video_model: projectParams?.video_model,
663 video_ratio: projectParams?.video_ratio,
664 video_resolution: projectParams?.video_resolution,
665 video_sound: 'on',
666 video_shot_type: 'multi',
667 };
668 // 从剧本产物获取实际场景数
669 try {
670 const scriptArt = await getArtifact(sessionId, 'script_generation');
671 if (scriptArt?.artifact?.scenes?.length) {
672 inputData.scene_number = scriptArt.artifact.scenes.length;
673 }
674 } catch { /* ignore */ }
675
676 // 从参考图产物获取用户选择的图片版本,传递给视频生成阶段
677 if (stageId === 'reference_generation') {
678 const refArt = stageStates['reference_generation']?.artifact;
679 if (refArt?.scenes) {
680 const selectedImages: Record<string, string> = {};
681 refArt.scenes.forEach((s: any) => {
682 if (s.id && s.selected) {
683 selectedImages[s.id] = s.selected;
684 }
685 });
686 inputData.selected_images = selectedImages;
687 }
688 }
689
690 // 从视频产物获取用户选择的视频版本,传递给后期制作阶段
691 if (stageId === 'video_generation') {
692 const vidArt = stageStates['video_generation']?.artifact;
693 if (vidArt?.clips) {
694 const selectedClips: Record<string, string> = {};
695 vidArt.clips.forEach((c: any) => {
696 if (c.id && c.selected) {
697 selectedClips[c.id] = c.selected;
698 }
699 });
700 inputData.selected_clips = selectedClips;
701 }
702 }
703
704 for (let i = idx + 1; i < STAGE_ORDER.length; i++) {
705 if (stoppedRef.current) break;
706 const nextStage = STAGE_ORDER[i];
707 await runStage(sessionId, nextStage, inputData);
708
709 const status = await getProjectStatus(sessionId);
710 setGlobalStatusMap(status.status || {});
711 const nStageStatus = status?.status?.[nextStage];
712 // waiting: 等待用户介入(如选择角色/图片),不能自动 continue
713 // completed: 阶段完成,等待确认进入下一阶段,可以自动 continue
714 if (nStageStatus === 'completed' || nStageStatus === 'session_completed') {
715 if (autoModeRef.current) {
716 await continueWorkflow(sessionId);
717 updateStageState(nextStage, { status: 'completed', progressMessage: '已自动确认' });
718 setCompletedStagesFromSession(prev => [...prev, nextStage]);
719 setCurrentStageFromSession(nextStage);
720 } else {
721 updateStageState(nextStage, { status: 'completed' });
722 setCompletedStagesFromSession(prev => [...prev, nextStage]);
723 setCurrentStageFromSession(nextStage);
724 setActiveStage(nextStage);
725 break;
726 }
727 } else if (nStageStatus === 'waiting') {
728 // 用户需要介入,停止自动执行
729 updateStageState(nextStage, { status: 'waiting' });
730 setCurrentStageFromSession(nextStage);
731 setActiveStage(nextStage);
732 break;
733 }
734 }
735 }
736 } catch (error: any) {
737 console.error('Continue error:', error);
738 } finally {
739 setIsRunning(false);
740 }
741 };
742
743 // ── 用户介入修改 ──
744 const handleIntervene = async (stageId: string, modifications: Record<string, any>) => {
745 if (!sessionId) return;
746 const isBackgroundItemRegeneration =
747 (stageId === 'character_design' && (
748 Array.isArray(modifications.regenerate_characters) ||
749 Array.isArray(modifications.regenerate_settings)
750 )) ||
751 (stageId === 'reference_generation' && Array.isArray(modifications.regenerate_scenes)) ||
752 (stageId === 'video_generation' && Array.isArray(modifications.regenerate_clips)) ||
753 (stageId === 'post_production' && Array.isArray(modifications.regenerate_episodes));
754
755 // 构建完整的 inputData 传给后端,确保比例等参数能传递
756 const inputData: Record<string, any> = {
757 ...modifications,
758 session_id: sessionId,
759 style: projectParams?.style,
760 video_ratio: projectParams?.video_ratio,
761 video_resolution: projectParams?.video_resolution,
762 llm_model: projectParams?.llm_model,
763 vlm_model: projectParams?.vlm_model,
764 image_t2i_model: projectParams?.image_t2i_model,
765 image_it2i_model: projectParams?.image_it2i_model,
766 video_generation_mode: projectParams?.video_generation_mode,
767 video_first_frame_model: projectParams?.video_first_frame_model,
768 video_start_end_model: projectParams?.video_start_end_model,
769 video_reference_model: projectParams?.video_reference_model,
770 video_model: projectParams?.video_model,
771 video_sound: 'on',
772 video_shot_type: 'multi',
773 };
774
775 // 设置 running 状态以便显示进度条(如 Logline 选择后生成剧本)
776 // 若选择了 Logline,将其保存到 artifact 以便 ScriptStage 在生成期间展示
777 // 若选择了模式,保留已选 Logline 并设置 generating 状态
778 const artifactPatch = modifications.selected_logline
779 ? { phase: 'generating', selected_logline: modifications.selected_logline }
780 : modifications.selected_mode
781 ? { phase: 'generating', selected_logline: stageStates[stageId]?.artifact?.selected_logline }
782 : undefined;
783 if (!isBackgroundItemRegeneration) {
784 setIsRunning(true);
785 updateStageState(stageId, { status: 'running', progress: 0, progressMessage: '处理中...', ...(artifactPatch ? { artifact: artifactPatch } : {}) });
786 } else if (artifactPatch) {
787 updateStageState(stageId, { artifact: artifactPatch });
788 }
789 try {
790 const response = await intervene(sessionId, stageId, inputData);
791 for await (const event of parseStreamEvents(response)) {
792 if (event.type === 'progress') {
793 if (!isBackgroundItemRegeneration) {
794 updateStageState(stageId, {
795 progress: event.percent || 0,
796 progressMessage: event.message || '',
797 });
798 }
799 // 处理 asset_complete 实时事件
800 if (event.data?.asset_complete) {
801 const assetUpdate = event.data.asset_complete;
802 setStageStates(prev => {
803 const prevArt = prev[stageId]?.artifact;
804 if (!prevArt) return prev;
805 const key = assetUpdate.type as string;
806 const items = [...(prevArt[key] || [])];
807 const idx = items.findIndex((item: any) => item.id === assetUpdate.id);
808 if (idx >= 0) {
809 items[idx] = mergeAssetUpdateItem(items[idx], assetUpdate);
810 }
811 return {
812 ...prev,
813 [stageId]: { ...prev[stageId], artifact: { ...prevArt, [key]: items } },
814 };
815 });
816 }
817 // 处理剧本逐幕增量事件 (介入模式)
818 if (event.data?.beat_sheet) {
819 setStageStates(prev => {
820 const prevArt = prev[stageId]?.artifact || {};
821 return {
822 ...prev,
823 [stageId]: {
824 ...prev[stageId],
825 artifact: { ...prevArt, phase: 'generating', beat_sheet: event.data.beat_sheet },
826 },
827 };
828 });
829 }
830 if (event.data?.act_complete) {
831 const actData = event.data.act_complete;
832 setStageStates(prev => {
833 const prevArt = prev[stageId]?.artifact || {};
834 const prevActs = prevArt.completed_acts || [];
835 return {
836 ...prev,
837 [stageId]: {
838 ...prev[stageId],
839 artifact: {
840 ...prevArt,
841 phase: 'generating',
842 completed_acts: [...prevActs, actData],
843 },
844 },
845 };
846 });
847 }
848 // 处理分镜逐场完成事件 (介入模式)
849 if (event.data?.scene_shots_complete) {
850 const sceneData = event.data.scene_shots_complete;
851 setStageStates(prev => {
852 const prevArt = prev[stageId]?.artifact || {};
853 const prevShots = prevArt.shots || [];
854 return {
855 ...prev,
856 [stageId]: {
857 ...prev[stageId],
858 artifact: {
859 ...prevArt,
860 phase: 'generating',
861 shots: [...prevShots, ...sceneData.shots],
862 },
863 },
864 };
865 });
866 }
867 } else if (event.type === 'stage_complete') {
868 const artResult = await getArtifact(sessionId, stageId);
869 if (isBackgroundItemRegeneration) {
870 updateStageState(stageId, { artifact: artResult.artifact });
871 } else {
872 updateStageState(stageId, { artifact: artResult.artifact, status: 'waiting' });
873 }
874 } else if (event.type === 'error') {
875 console.error('Intervention error:', event.content);
876 }
877 }
878 } catch (error: any) {
879 console.error('Intervention error:', error);
880 } finally {
881 if (!isBackgroundItemRegeneration) {
882 setIsRunning(false);
883 }
884 }
885 };
886
887 // ── 本地更新产物(不触发服务端调用) ──
888 const handleUpdateArtifact = (stageId: string, patch: Record<string, any>) => {
889 setStageStates(prev => ({
890 ...prev,
891 [stageId]: {
892 ...prev[stageId],
893 artifact: { ...(prev[stageId]?.artifact || {}), ...patch },
894 },
895 }));
896 };
897
898 // ── 保存用户选择到服务端 ──
899 const handleSaveSelections = async (stageId: string, selections: Record<string, any>): Promise<void> => {
900 if (!sessionId) return;
901 try {
902 // 构建更新: 把用户选择写回 artifact 中每个 item 的 selected 字段
903 const art = stageStates[stageId]?.artifact;
904 if (!art) return;
905
906 let patch: Record<string, any> = {};
907 if (stageId === 'script_generation') {
908 // 剧本阶段:直接保存整个 data 覆盖 artifact
909 patch = selections;
910 } else if (stageId === 'character_design') {
911 const { _editDescs, ...restSelections } = selections;
912 const chars = (art.characters || []).map((c: any) => ({
913 ...c,
914 selected: restSelections[c.id] || c.selected,
915 description: _editDescs?.characters?.[c.id] ?? c.description,
916 }));
917 const sets = (art.settings || []).map((s: any) => ({
918 ...s,
919 selected: restSelections[s.id] || s.selected,
920 description: _editDescs?.settings?.[s.id] ?? s.description,
921 }));
922 patch = { characters: chars, settings: sets };
923 } else if (stageId === 'storyboard') {
924 // 分镜阶段:保存 shots 数据(排除 original_shots,只保留 artifact 需要的字段)
925 const { original_shots, ...rest } = selections;
926 patch = { ...rest, user_modified: true };
927 } else if (stageId === 'reference_generation') {
928 const { _editDescs, ...restSelections } = selections;
929 const scenes = (art.scenes || []).map((s: any) => ({
930 ...s,
931 selected: restSelections[s.id] || s.selected,
932 description: _editDescs?.[s.id] ?? s.description,
933 }));
934 patch = { scenes };
935 } else if (stageId === 'video_generation') {
936 const { _editDescs, ...restSelections } = selections;
937 const clips = (art.clips || []).map((c: any) => ({
938 ...c,
939 selected: Object.prototype.hasOwnProperty.call(restSelections, c.id)
940 ? restSelections[c.id]
941 : c.selected,
942 description: _editDescs?.[c.id] ?? c.description,
943 }));
944 patch = { clips };
945 }
946
947 // 本地更新
948 handleUpdateArtifact(stageId, patch);
949
950 // 服务端持久化
951 await saveSelections(sessionId, stageId, patch);
952 // 保存成功后标记为已完成,顶栏显示对勾
953 updateStageState(stageId, { status: 'completed' });
954
955 // 如果是分镜阶段保存,刷���第3和第4阶段的 artifact
956 console.log('[handleSaveSelections] stageId:', stageId, 'patch:', patch);
957 if (stageId === 'storyboard') {
958 // 更新本地 artifact 以清除 is_new 标记
959 handleUpdateArtifact(stageId, patch);
960
961 // 获取第4阶段当前已有 artifact
962 const oldRefArtifact = stageStates['reference_generation']?.artifact;
963 const oldScenes = (oldRefArtifact?.scenes || []) as any[];
964 const oldScenesMap = new Map(oldScenes.map((s: any) => [s.id, s]));
965
966 // 使用 segments 构建第4阶段 scenes 和第5阶段 clips
967 const sourceSegments = patch.segments || [];
968 console.log('[handleSaveSelections] sourceSegments:', sourceSegments);
969
970 const newScenes = sourceSegments.map((seg: any, idx: number) => {
971 const seg_id = seg.segment_id;
972 const oldScene = oldScenesMap.get(seg_id) as any;
973 const isNew = seg.is_new;
974
975 const shots = seg.shots || [];
976 const desc_ref = shots.map((sh: any) => sh.visual_prompt || sh.plot || sh.content || '').join(' ').trim();
977
978 return {
979 id: seg_id,
980 name: `片段_${seg_id}`,
981 index: idx + 1,
982 description: desc_ref,
983 selected: isNew ? '' : (oldScene?.selected || ''),
984 versions: isNew ? [] : (oldScene?.versions || []),
985 status: isNew ? 'pending' : 'done',
986 };
987 });
988
989 // 更新第4阶段的 artifact
990 updateStageState('reference_generation', {
991 artifact: { session_id: sessionId, scenes: newScenes },
992 status: 'completed'
993 });
994
995 // 同步更新第5阶段(video_generation)
996 const oldVidArtifact = stageStates['video_generation']?.artifact;
997 const oldClips = (oldVidArtifact?.clips || []) as any[];
998 const oldClipsMap = new Map(oldClips.map((c: any) => [c.id, c]));
999
1000 const newClips = sourceSegments.map((seg: any, idx: number) => {
1001 const seg_id = seg.segment_id;
1002 const oldClip = oldClipsMap.get(seg_id) as any;
1003
1004 const shots = seg.shots || [];
1005 const desc_video = shots.map((sh: any) => sh.plot || sh.content || '').join(' ').trim();
1006 const total_dur = seg.total_duration || shots.reduce((acc: number, sh: any) => acc + (sh.duration || 0), 0) || 10;
1007
1008 return {
1009 id: seg_id,
1010 name: `片段_${seg_id}`,
1011 index: idx + 1,
1012 description: desc_video,
1013 duration: total_dur,
1014 selected: oldClip?.selected || '',
1015 versions: oldClip?.versions || [],
1016 status: 'pending',
1017 };
1018 });
1019
1020 // 更新第5阶段的 artifact
1021 updateStageState('video_generation', {
1022 artifact: { session_id: sessionId, clips: newClips },
1023 status: 'completed'
1024 });
1025 console.log('[handleSaveSelections] 第5阶段已更新');
1026 }
1027 } catch (error) {
1028 console.error('Save selections error:', error);
1029 throw error; // 抛出让 StageActions 捕获以恢复按钮状态
1030 }
1031 };
1032
1033 // ── 重新生成当前阶段 ──
1034 const handleRegenerate = async (stageId: string) => {
1035 if (!sessionId || isRunning) return;
1036 stoppedRef.current = false;
1037 setIsRunning(true);
1038
1039 // 清空当前阶段状态
1040 updateStageState(stageId, {
1041 status: 'running',
1042 progress: 0,
1043 progressMessage: '重新生成中...',
1044 artifact: null,
1045 error: null,
1046 });
1047
1048 // 将该阶段之后的所有阶段重置为 pending
1049 const idx = STAGE_ORDER.indexOf(stageId);
1050 for (let i = idx + 1; i < STAGE_ORDER.length; i++) {
1051 updateStageState(STAGE_ORDER[i], {
1052 status: 'pending',
1053 progress: 0,
1054 progressMessage: '',
1055 artifact: null,
1056 error: null,
1057 });
1058 }
1059
1060 try {
1061 const inputData: Record<string, any> = {
1062 session_id: sessionId,
1063 style: projectParams?.style,
1064 llm_model: projectParams?.llm_model,
1065 vlm_model: projectParams?.vlm_model,
1066 image_t2i_model: projectParams?.image_t2i_model,
1067 image_it2i_model: projectParams?.image_it2i_model,
1068 video_generation_mode: projectParams?.video_generation_mode,
1069 video_first_frame_model: projectParams?.video_first_frame_model,
1070 video_start_end_model: projectParams?.video_start_end_model,
1071 video_reference_model: projectParams?.video_reference_model,
1072 video_model: projectParams?.video_model,
1073 video_ratio: projectParams?.video_ratio,
1074 video_resolution: projectParams?.video_resolution,
1075 };
1076
1077 // 尝试获取场景数
1078 try {
1079 const scriptArt = await getArtifact(sessionId, 'script_generation');
1080 if (scriptArt?.artifact?.scenes?.length) {
1081 inputData.scene_number = scriptArt.artifact.scenes.length;
1082 }
1083 } catch { /* ignore */ }
1084
1085 await runStage(sessionId, stageId, inputData);
1086
1087 const status = await getProjectStatus(sessionId);
1088 if (status?.status?.[stageId] === 'waiting') {
1089 updateStageState(stageId, { status: 'waiting' });
1090 setActiveStage(stageId);
1091 }
1092 } catch (error: any) {
1093 if (!stoppedRef.current) console.error('Regenerate error:', error);
1094 } finally {
1095 setIsRunning(false);
1096 }
1097 };
1098
1099 // ── 恢复历史项目 ──
1100 const handleResumeProject = async (sid: string, targetStage: string | null = null) => {
1101 // 如果正在执行的就是同一个项目,直接恢复视图,不重置状态
1102 if (sid === sessionId) {
1103 const runningStage = STAGE_ORDER.find(s => stageStates[s]?.status === 'running');
1104 const waitingStage = STAGE_ORDER.find(s => stageStates[s]?.status === 'waiting');
1105 const lastCompleted = [...STAGE_ORDER].reverse().find(s => stageStates[s]?.status === 'completed');
1106 setActiveStage(targetStage || runningStage || waitingStage || lastCompleted || STAGE_ORDER[0]);
1107 return;
1108 }
1109
1110 setSessionId(sid);
1111 // 更新 URL 参数(包含阶段)
1112 const stageParam = targetStage ? `&stage=${targetStage}` : '';
1113 router.push(`/?session=${sid}${stageParam}`);
1114 try {
1115 let status: any;
1116 try {
1117 status = await getProjectStatus(sid);
1118 } catch {
1119 status = await getProjectStatusFromDisk(sid);
1120 }
1121 setGlobalStatusMap(status.status || {});
1122 const newStates = initStageStates();
1123 const stMap = status.status || {};
1124 const allStatusStages = Object.keys(stMap);
1125 const currentStage = status.current_stage;
1126
1127 // 保存到 state 供顶栏流程图使用
1128 setCurrentStageFromSession(currentStage || null);
1129 const completedStages = allStatusStages.filter(k => ["completed", "session_completed"].includes(stMap[k]));
1130 setCompletedStagesFromSession(completedStages);
1131
1132 for (const sName of allStatusStages) {
1133 const cStatus = stMap[sName];
1134 if (cStatus === 'pending' || cStatus === 'idle') {
1135 // 即使是 pending,如果 artifacts 中有数据,也尝试恢复数据(用于初始占位显示)
1136 if (status.artifacts?.[sName]) {
1137 newStates[sName].artifact = status.artifacts[sName];
1138 }
1139 continue;
1140 }
1141
1142 const isCompleted = ["completed", "session_completed"].includes(cStatus);
1143 const isError = cStatus === 'error';
1144 const isWaiting = cStatus === 'waiting';
1145 const isStopped = cStatus === 'stopped';
1146 const isRunningStatus = cStatus === 'running';
1147 const progressSnapshot = getStageProgressSnapshot(status, sName);
1148
1149 newStates[sName] = {
1150 status: isError ? 'error' : (isWaiting ? 'waiting' : (isStopped ? 'stopped' : (isCompleted ? 'completed' : 'running'))),
1151 progress: isCompleted ? 100 : (typeof progressSnapshot.progress === 'number' ? progressSnapshot.progress : 0),
1152 progressMessage: isError
1153 ? '执行失败'
1154 : (isWaiting
1155 ? '等待确认'
1156 : (isStopped
1157 ? '已停止'
1158 : (isCompleted
1159 ? '已完成'
1160 : (progressSnapshot.progressMessage || (isRunningStatus ? '执行中...' : '进行中'))))),
1161 artifact: status.artifacts?.[sName] || null,
1162 error: isError ? (status.error || '执行出错') : (isStopped ? '手动停止' : null),
1163 };
1164
1165 // 如果内存中没有,尝试异步拉取
1166 if (!newStates[sName].artifact) {
1167 try {
1168 const artResult = await getArtifact(sid, sName);
1169 newStates[sName].artifact = artResult.artifact;
1170 } catch { /* ignore */ }
1171 }
1172 }
1173
1174 // 恢复项目参数:会话级生成配置统一从 meta 中读取,兼容少量旧扁平字段。
1175 const meta = ((status as any).meta || {}) as any;
1176 const s = Object.keys(meta).length > 0 ? meta : (status as any);
1177 if (s.idea || s.user_textbox_input) {
1178 // Legacy session compatibility: old sessions only stored one video_model, so hydrate it as the first-frame model.
1179 const restoredFirstFrameVideoModel = s.video_first_frame_model || s.video_model || '';
1180 setProjectParams({
1181 idea: s.idea || s.user_textbox_input || '',
1182 style: s.style || '',
1183 video_ratio: s.video_ratio || '16:9',
1184 video_resolution: s.video_resolution || '720P',
1185 llm_model: s.llm_model || '',
1186 vlm_model: s.vlm_model || '',
1187 image_t2i_model: s.image_t2i_model || '',
1188 image_it2i_model: s.image_it2i_model || '',
1189 video_generation_mode: s.video_generation_mode || 'first_frame',
1190 video_first_frame_model: restoredFirstFrameVideoModel,
1191 video_start_end_model: s.video_start_end_model || 'wan2.7-i2v',
1192 video_reference_model: s.video_reference_model || 'wan2.7-r2v',
1193 video_model: s.video_model || '',
1194 expand_idea: s.expand_idea || false,
1195 enable_concurrency: s.enable_concurrency || false,
1196 web_search: s.web_search || false,
1197 });
1198 }
1199
1200 // 确定要显示的阶段(优先使用 URL 中的目标阶段)
1201 let finalStage = targetStage;
1202 if (!finalStage) {
1203 finalStage = currentStage || (completedStages.length > 0 ? completedStages[completedStages.length - 1] : STAGE_ORDER[0]);
1204 }
1205 setActiveStage(finalStage);
1206 setStageStates(newStates);
1207
1208 // 轮询正在执行的阶段;后台单卡重生成时阶段可能不是 running,但 item 会是 running。
1209 for (const stageId of STAGE_ORDER) {
1210 if (stMap[stageId] === 'running' || hasRunningArtifactItems(status.artifacts?.[stageId])) {
1211 pollForCompletion(sid, stageId);
1212 }
1213 }
1214 } catch {
1215 setActiveStage(STAGE_ORDER[0]);
1216 }
1217 };
1218
1219 // ── 返回首页 ──
1220 // 处理阶段点击,更新 URL
1221 const handleStageClick = (stage: string) => {
1222 setActiveStage(stage);
1223 if (sessionId) {
1224 router.push(`/?session=${sessionId}&stage=${stage}`);
1225 }
1226 };
1227
1228 const handleGoHome = () => {
1229 setActiveStage(null);
1230 router.push('/');
1231 };
1232
1233 // ── 删除历史记录 ──
1234 const handleDeleteSession = async (sid: string) => {
1235 await deleteSession(sid);
1236 setHistory(prev => prev.filter(h => h.id !== sid));
1237 };
1238
1239 // ── 模型配置变更处理 ──
1240 const handleModelConfigChange = (config: ModelConfig) => {
1241 setProjectParams(prev => prev ? { ...prev, ...config } : null);
1242 // 同步到后端会话元数据
1243 if (sessionId) {
1244 const payload: Record<string, any> = { ...config };
1245 updateModels(sessionId, payload).catch(console.error);
1246 }
1247 };
1248
1249 // 构建 modelConfig for TopBar
1250 const modelConfig: ModelConfig | undefined = projectParams
1251 ? {
1252 llm_model: projectParams.llm_model,
1253 vlm_model: projectParams.vlm_model,
1254 image_t2i_model: projectParams.image_t2i_model,
1255 image_it2i_model: projectParams.image_it2i_model,
1256 // Legacy session compatibility: projectParams may come from an old session with only video_model.
1257 video_generation_mode: projectParams.video_generation_mode || 'first_frame',
1258 video_first_frame_model: projectParams.video_first_frame_model || projectParams.video_model,
1259 video_start_end_model: projectParams.video_start_end_model || 'wan2.7-i2v',
1260 video_reference_model: projectParams.video_reference_model || 'wan2.7-r2v',
1261 video_model: projectParams.video_model,
1262 video_ratio: projectParams.video_ratio,
1263 video_resolution: projectParams.video_resolution || '720P',
1264 enable_concurrency: projectParams.enable_concurrency || false,
1265 // web_search: projectParams.web_search || false,
1266 // expand_idea: projectParams.expand_idea || false,
1267 }
1268 : undefined;
1269
1270 // ── 计算 stageStatuses for TopBar ──
1271 // 直接根据后端返回的 status 数据计算图标状态
1272 const [globalStatusMap, setGlobalStatusMap] = useState<Record<string, string>>({});
1273
1274 const stageStatuses: Record<string, StageStatus> = {};
1275
1276 for (let i = 0; i < STAGE_ORDER.length; i++) {
1277 const s = STAGE_ORDER[i];
1278 const backendStatus = globalStatusMap[s]; // get status from global mapping updated from backend
1279
1280 if (backendStatus === 'completed') {
1281 stageStatuses[s] = 'completed';
1282 } else if (backendStatus === 'waiting') {
1283 stageStatuses[s] = 'waiting';
1284 } else if (backendStatus === 'running') {
1285 stageStatuses[s] = 'running';
1286 } else if (backendStatus === 'error') {
1287 stageStatuses[s] = 'error';
1288 } else {
1289 stageStatuses[s] = 'pending';
1290 }
1291 }
1292
1293 // ── 计算项目状态 ──
1294 // 后端阶段状态: pending, running, waiting, completed, stopped, error
1295 // 前端 StageStatus: pending, running, waiting, completed, error
1296 const hasRunning = Object.values(stageStates).some(s => s.status === 'running');
1297 const hasWaiting = Object.values(stageStates).some(s => s.status === 'waiting');
1298 const hasError = Object.values(stageStates).some(s => s.status === 'error');
1299 const allCompleted = Object.values(stageStates).every(s => s.status === 'completed' || s.status === 'pending');
1300 const effectiveIsRunning = isRunning || hasRunning;
1301
1302 let computedStatus: string;
1303 if (hasRunning) computedStatus = 'running';
1304 else if (hasWaiting) computedStatus = 'waiting';
1305 else if (hasError) computedStatus = 'error';
1306 // Check if stopped in stageStates
1307 else if (Object.values(stageStates).some(s => s.status === 'stopped')) computedStatus = 'stopped';
1308 else if (allCompleted && stageStates[STAGE_ORDER[STAGE_ORDER.length - 1]]?.status === 'completed') {
1309 computedStatus = 'completed';
1310 } else if (allCompleted) {
1311 computedStatus = 'completed';
1312 } else {
1313 computedStatus = Object.values(stageStates).some(s => s.status === 'stopped') ? 'stopped' : 'pending';
1314 }
1315
1316 const projectStatus = sessionId ? computedStatus : undefined;
1317
1318 // ── 渲染阶段内容 ──
1319 const renderStageContent = () => {
1320 if (!activeStage) return null;
1321 const Component = STAGE_COMPONENTS[activeStage];
1322 if (!Component) return null;
1323
1324 const state = stageStates[activeStage];
1325
1326 // 判断后续阶段是否已执行过:如有,则隐藏"确认并继续"
1327 const idx = STAGE_ORDER.indexOf(activeStage);
1328 const hasSubsequentExecution = STAGE_ORDER.slice(idx + 1).some(
1329 s => stageStates[s]?.status && stageStates[s].status !== 'pending'
1330 );
1331 const showConfirm = !hasSubsequentExecution;
1332
1333 // 计算是否有待生成的项(阶段2、4、5)
1334 let hasPendingItems = false;
1335 if (activeStage === 'character_design') {
1336 const chars = state?.artifact?.characters || [];
1337 const sets = state?.artifact?.settings || [];
1338 hasPendingItems = chars.some((c: any) => !c.selected) || sets.some((s: any) => !s.selected);
1339 } else if (activeStage === 'reference_generation') {
1340 const scenes = state?.artifact?.scenes || [];
1341 hasPendingItems = scenes.some((s: any) => !s.selected);
1342 } else if (activeStage === 'video_generation') {
1343 const clips = state?.artifact?.clips || [];
1344 // 检查是否有未选中的 clips
1345 const hasUnselected = clips.some((c: any) => !c.selected);
1346 // 检查参考图阶段的 scenes 数量是否大于 video clips 数量
1347 // 如果是,说明有新分镜需要生成
1348 const refScenes = stageStates['reference_generation']?.artifact?.scenes || [];
1349 const hasNewShots = refScenes.length > clips.length;
1350 hasPendingItems = hasUnselected || hasNewShots;
1351 }
1352
1353 // 计算后续阶段是否已开始(阶段1、3)
1354 const hasNextStageStarted = hasSubsequentExecution;
1355
1356 // 传递第4阶段的 artifact 到第5阶段用于依赖检查
1357 const referenceArtifact = activeStage === 'video_generation'
1358 ? stageStates['reference_generation']?.artifact
1359 : undefined;
1360
1361 // 传递第1阶段的 artifact 到第4、5、6阶段用于获取剧集标题
1362 const scriptArtifact = (activeStage === 'reference_generation' || activeStage === 'video_generation' || activeStage === 'post_production')
1363 ? stageStates['script_generation']?.artifact
1364 : undefined;
1365
1366 return (
1367 <Component
1368 state={state}
1369 sessionId={sessionId || ''}
1370 onConfirm={() => handleConfirmStage(activeStage)}
1371 onIntervene={(mods: Record<string, any>) => handleIntervene(activeStage, mods)}
1372 onRegenerate={() => handleRegenerate(activeStage)}
1373 onUpdateArtifact={(patch: Record<string, any>) => handleUpdateArtifact(activeStage, patch)}
1374 onSaveSelections={(selections: Record<string, any>) => handleSaveSelections(activeStage, selections)}
1375 showConfirm={showConfirm}
1376 isRunning={effectiveIsRunning}
1377 hasPendingItems={hasPendingItems}
1378 hasNextStageStarted={hasNextStageStarted}
1379 referenceArtifact={referenceArtifact}
1380 scriptArtifact={scriptArtifact}
1381 artifacts={Object.keys(stageStates).reduce((acc: any, key) => {
1382 acc[key] = stageStates[key].artifact;
1383 return acc;
1384 }, {})}
1385 />
1386 );
1387 };
1388
1389 const showHome = activeStage === null;
1390
1391 return (
1392 <div className="flex h-screen w-full min-w-0 flex-col bg-gray-50/50">
1393 <TopBar
1394 activeStage={activeStage}
1395 stageStatuses={stageStatuses}
1396 onStageClick={handleStageClick}
1397 onHomeClick={handleGoHome}
1398 hasSession={sessionId !== null}
1399 isRunning={effectiveIsRunning}
1400 onStop={handleStop}
1401 autoMode={autoMode}
1402 onAutoModeChange={handleAutoModeChange}
1403 modelConfig={modelConfig}
1404 onModelConfigChange={handleModelConfigChange}
1405 projectStatus={projectStatus}
1406 />
1407
1408 <main className="flex-1 min-w-0 overflow-hidden">
1409 {showHome ? (
1410 <HomePage
1411 onStartProject={handleStartProject}
1412 onResumeProject={handleResumeProject}
1413 onDeleteSession={handleDeleteSession}
1414 history={history}
1415 />
1416 ) : (
1417 renderStageContent()
1418 )}
1419 </main>
1420 </div>
1421 );
1422 }
1423
1423 lines Plain Text