"use client" import React, { useRef, useEffect, useCallback } from "react"; import "./style/work-flow.css"; import { EditModal } from "@/components/ui/edit-modal"; import { TaskInfo } from "./work-flow/task-info"; import H5TaskInfo from "./work-flow/H5TaskInfo"; import H5MediaViewer from "./work-flow/H5MediaViewer"; import { MediaViewer } from "./work-flow/media-viewer"; import { ThumbnailGrid } from "./work-flow/thumbnail-grid"; import { useWorkflowData } from "./work-flow/use-workflow-data"; import { usePlaybackControls } from "./work-flow/use-playback-controls"; import { Bot, TestTube, MessageCircle } from "lucide-react"; import { GlassIconButton } from '@/components/ui/glass-icon-button'; import { SaveEditUseCase } from "@/app/service/usecase/SaveEditUseCase"; import { useSearchParams } from "next/navigation"; import SmartChatBox from "@/components/SmartChatBox/SmartChatBox"; import { Drawer, Tooltip, notification } from 'antd'; import { showEditingNotification } from "@/components/pages/work-flow/editing-notification"; // import { AIEditingIframeButton } from './work-flow/ai-editing-iframe'; import { exportVideoWithRetry } from '@/utils/export-service'; import { getFirstFrame } from '@/utils/tools'; import { EditPoint as EditPointType } from './work-flow/video-edit/types'; import { AIEditingIframeButton } from './work-flow/ai-editing-iframe'; import { useDeviceType } from '@/hooks/useDeviceType'; import { H5ProgressToastProvider, useH5ProgressToast } from '@/components/ui/h5-progress-toast'; const WorkFlow = React.memo(function WorkFlow() { const { isMobile, isTablet, isDesktop } = useDeviceType(); // 通过全局事件桥接 H5ProgressToast(Provider 在本组件 JSX 中,逻辑层无法直接使用 hook) const emitToastShow = useCallback((params: { title?: string; progress?: number }) => { window.dispatchEvent(new CustomEvent('h5Toast:show', { detail: params })); }, []); const emitToastUpdate = useCallback((params: { title?: string; progress?: number }) => { window.dispatchEvent(new CustomEvent('h5Toast:update', { detail: params })); }, []); const emitToastHide = useCallback(() => { window.dispatchEvent(new CustomEvent('h5Toast:hide')); }, []); useEffect(() => { console.log("init-WorkFlow"); return () => { console.log("unmount-WorkFlow"); // 不在卸载时强制隐藏,避免严格模式下二次卸载导致刚显示就被关闭 }; }, [emitToastHide]); const containerRef = useRef(null); const [isEditModalOpen, setIsEditModalOpen] = React.useState(false); const [activeEditTab, setActiveEditTab] = React.useState('1'); const [isSmartChatBoxOpen, setIsSmartChatBoxOpen] = React.useState(true); const [chatTip, setChatTip] = React.useState(null); const [hasUnread, setHasUnread] = React.useState(false); const [previewVideoUrl, setPreviewVideoUrl] = React.useState(null); const [previewVideoId, setPreviewVideoId] = React.useState(null); const [isFocusChatInput, setIsFocusChatInput] = React.useState(false); const [selectedView, setSelectedView] = React.useState<'final' | 'video' | null>(null); const [isFinalBarOpen, setIsFinalBarOpen] = React.useState(true); const [aiEditingResult, setAiEditingResult] = React.useState(null); // const aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise }>(null); const [editingStatus, setEditingStatus] = React.useState<'initial' | 'idle' | 'success' | 'error'>('initial'); // const [iframeAiEditingKey, setIframeAiEditingKey] = React.useState(`iframe-ai-editing-${Date.now()}`); const [isEditingInProgress, setIsEditingInProgress] = React.useState(false); const isEditingInProgressRef = useRef(false); // 导出进度状态 const [exportProgress, setExportProgress] = React.useState<{ status: 'processing' | 'completed' | 'failed'; percentage: number; message: string; stage?: string; taskId?: string; } | null>(null); const editingTimeoutRef = useRef(null); const editingProgressIntervalRef = useRef(null); const editingProgressStartRef = useRef(0); const searchParams = useSearchParams(); const episodeId = searchParams.get('episodeId') || ''; const userId = JSON.parse(localStorage.getItem("currentUser") || '{}').id || NaN; SaveEditUseCase.setProjectId(episodeId); const [isHandleEdit, setIsHandleEdit] = React.useState(false); // 使用 ref 存储 handleTestExport 避免循环依赖 const handleTestExportRef = useRef<(() => Promise) | null>(null); // 导出进度回调处理 const handleExportProgress = useCallback((progressData: { status: 'processing' | 'completed' | 'failed'; percentage: number; message: string; stage?: string; taskId?: string; }) => { console.log('📊 导出进度更新:', progressData); setExportProgress(progressData); // 根据状态显示不同的通知 - 已注释 /* if (progressData.status === 'processing') { notification.info({ message: '导出进度', description: `${progressData.message} (${progressData.percentage}%)`, placement: 'topRight', duration: 2, key: 'export-progress' }); } else if (progressData.status === 'completed') { notification.success({ message: '导出成功', description: progressData.message, placement: 'topRight', duration: 5, key: 'export-progress' }); } else if (progressData.status === 'failed') { notification.error({ message: '导出失败', description: progressData.message, placement: 'topRight', duration: 8, key: 'export-progress' }); } */ }, []); // 处理编辑计划生成完成的回调 const handleEditPlanGenerated = useCallback(() => { console.log('✨ 编辑计划生成完成,开始AI剪辑'); setIsHandleEdit(true); setEditingStatus('idle'); // setIsEditingInProgress(true); // 已移除该状态变量 isEditingInProgressRef.current = true; // 改为调用测试剪辑计划导出按钮方法 // aiEditingButtonRef.current?.handleAIEditing(); // 使用 ref 调用避免循环依赖 setTimeout(() => { handleTestExportRef.current?.(); }, 0); // 显示进度提示并启动超时定时器 emitToastShow({ title: 'Performing intelligent editing...', progress: 0 }); // 启动自动推进到 90% 的进度(8分钟) if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current); editingProgressStartRef.current = Date.now(); const totalMs = 8 * 60 * 1000; editingProgressIntervalRef.current = setInterval(() => { const elapsed = Date.now() - editingProgressStartRef.current; const pct = Math.min(90, Math.max(0, Math.floor((elapsed / totalMs) * 90))); emitToastUpdate({ progress: pct }); }, 250); if (editingTimeoutRef.current) clearTimeout(editingTimeoutRef.current); editingTimeoutRef.current = setTimeout(() => { console.log('❌ Editing timeout - retrying...'); localStorage.removeItem(`isLoaded_plan_${episodeId}`); if (editingProgressIntervalRef.current) { clearInterval(editingProgressIntervalRef.current); editingProgressIntervalRef.current = null; } emitToastHide(); setTimeout(() => { emitToastShow({ title: 'Retry intelligent editing...', progress: 0 }); // 重试阶段自动推进(5分钟到 90%) if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current); editingProgressStartRef.current = Date.now(); const retryTotalMs = 5 * 60 * 1000; editingProgressIntervalRef.current = setInterval(() => { const elapsed = Date.now() - editingProgressStartRef.current; const pct = Math.min(90, Math.max(0, Math.floor((elapsed / retryTotalMs) * 90))); emitToastUpdate({ progress: pct }); }, 250); if (editingTimeoutRef.current) clearTimeout(editingTimeoutRef.current); editingTimeoutRef.current = setTimeout(() => { console.log('Editing retry failed'); localStorage.removeItem(`isLoaded_plan_${episodeId}`); setTimeout(() => { setEditingStatus('error'); setIsEditingInProgress(false); isEditingInProgressRef.current = false; if (editingProgressIntervalRef.current) { clearInterval(editingProgressIntervalRef.current); editingProgressIntervalRef.current = null; } emitToastHide(); }, 5000); }, 5 * 60 * 1000); }, 200); }, 8 * 60 * 1000); }, [episodeId, emitToastHide, emitToastShow, emitToastUpdate]); // 移除 isEditingInProgress 依赖 /** 处理导出失败 */ const handleExportFailed = useCallback(() => { console.log('Export failed, setting error status'); setEditingStatus('error'); // setIsEditingInProgress(false); // 已移除该状态变量 isEditingInProgressRef.current = false; if (editingTimeoutRef.current) { clearTimeout(editingTimeoutRef.current); editingTimeoutRef.current = null; } if (editingProgressIntervalRef.current) { clearInterval(editingProgressIntervalRef.current); editingProgressIntervalRef.current = null; } emitToastHide(); }, [emitToastHide]); // 使用自定义 hooks 管理状态 const { taskObject, scriptData, isLoading, currentSketchIndex, currentLoadingText, setCurrentSketchIndex, isPauseWorkFlow, mode, setIsPauseWorkFlow, setAnyAttribute, applyScript, fallbackToStep, originalText, showGotoCutButton, generateEditPlan, handleRetryVideo, isShowAutoEditing, aspectRatio } = useWorkflowData({ onEditPlanGenerated: handleEditPlanGenerated, editingStatus: editingStatus, onExportFailed: handleExportFailed }); const { isVideoPlaying, toggleVideoPlay, } = usePlaybackControls(taskObject.videos.data, taskObject.currentStage); useEffect(() => { if (isMobile) { setIsSmartChatBoxOpen(false); } }, [isMobile]); useEffect(() => { console.log('changedIndex_work-flow', currentSketchIndex, taskObject); }, [currentSketchIndex, taskObject]); // 当最终视频出现时,默认选中最终视频 useEffect(() => { if (taskObject?.final?.url && selectedView === null) { setSelectedView('final'); } }, [taskObject?.final?.url, selectedView]); // 监听粗剪是否完成 useEffect(() => { console.log('🎬 final video useEffect triggered:', { finalUrl: taskObject.final.url, isHandleEdit }); if (taskObject.final.url && isHandleEdit) { console.log('🎉 显示编辑完成通知'); // 完成:推进到 100 并清理超时计时器 if (editingTimeoutRef.current) { clearTimeout(editingTimeoutRef.current); editingTimeoutRef.current = null; } if (editingProgressIntervalRef.current) { clearInterval(editingProgressIntervalRef.current); editingProgressIntervalRef.current = null; } emitToastUpdate({ title: 'Editing successful', progress: 100 }); console.log('Editing successful'); localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true'); setEditingStatus('success'); setIsEditingInProgress(false); isEditingInProgressRef.current = false; setTimeout(() => { emitToastHide(); }, 3000); } }, [taskObject.final, isHandleEdit, episodeId, emitToastHide, emitToastUpdate]); const handleEditModalOpen = useCallback((tab: string) => { setActiveEditTab(tab); setIsEditModalOpen(true); }, []); // AI剪辑回调函数 const handleAIEditingComplete = useCallback((finalVideoUrl: string) => { console.log('🎉 AI剪辑完成,最终视频URL:', finalVideoUrl); // 更新任务对象的最终视频状态 setAnyAttribute('final', { url: finalVideoUrl, note: 'ai_edited' }); // 切换到最终视频阶段 setAnyAttribute('currentStage', 'final_video'); // setAiEditingInProgress(false); // 已移除该状态变量 }, [setAnyAttribute]); const handleAIEditingError = useCallback((error: string) => { console.error('❌ AI剪辑失败:', error); // 这里可以显示错误提示 // setAiEditingInProgress(false); // 已移除该状态变量 }, []); // 视频编辑描述提交处理函数 const handleVideoEditDescriptionSubmit = useCallback((editPoint: EditPointType, description: string) => { console.log('🎬 视频编辑描述提交:', { editPoint, description }); // 构造编辑消息发送到SmartChatBox const editMessage = `📝 Video Edit Request 🎯 **Position**: ${Math.round(editPoint.position.x)}%, ${Math.round(editPoint.position.y)}% ⏰ **Timestamp**: ${Math.floor(editPoint.timestamp)}s 🎬 **Video**: Shot ${currentSketchIndex + 1} **Edit Description:** ${description} Please process this video editing request.`; // 如果SmartChatBox开启,自动发送消息 if (isSmartChatBoxOpen) { // 这里可以通过SmartChatBox的API发送消息 // 或者通过全局状态管理来处理 console.log('📤 发送编辑请求到聊天框:', editMessage); } // 显示成功通知 notification.success({ message: 'Edit Request Submitted', description: `Your edit request for timestamp ${Math.floor(editPoint.timestamp)}s has been submitted successfully.`, duration: 3 }); }, [currentSketchIndex, isSmartChatBoxOpen]); // 测试导出接口的处理函数(使用封装的导出服务) const handleTestExport = useCallback(async () => { console.log('🧪 开始测试导出接口...'); console.log('📊 当前taskObject状态:', { currentStage: taskObject.currentStage, videosCount: taskObject.videos?.data?.length || 0, completedVideos: taskObject.videos?.data?.filter(v => v.video_status === 1).length || 0 }); try { // 使用封装的导出服务,传递进度回调 const result = await exportVideoWithRetry(episodeId, taskObject, handleExportProgress); console.log('🎉 导出服务完成,结果:', result); return result; } catch (error) { console.error('❌ 导出服务失败:', error); throw error; } }, [episodeId, taskObject, handleExportProgress]); // 将 handleTestExport 赋值给 ref React.useEffect(() => { handleTestExportRef.current = handleTestExport; }, [handleTestExport]); // iframe智能剪辑回调函数 - 已注释 /* const handleIframeAIEditingComplete = useCallback((result: any) => { console.log('🎉 iframe AI剪辑完成,结果:', result); // 保存剪辑结果 setAiEditingResult(result); // 更新任务对象的最终视频状态 setAnyAttribute('final', { url: result.videoUrl, note: 'ai_edited_iframe' }); // 切换到最终视频阶段 setAnyAttribute('currentStage', 'final_video'); // setAiEditingInProgress(false); // 已移除该状态变量 }, [setAnyAttribute]); */ /* const handleIframeAIEditingError = useCallback((error: string) => { console.error('❌ iframe AI剪辑失败:', error); // setAiEditingInProgress(false); // 已移除该状态变量 }, []); */ /* const handleIframeAIEditingProgress = useCallback((progress: number, message: string) => { console.log(`📊 AI剪辑进度: ${progress}% - ${message}`); setAiEditingInProgress(true); // 收到显式进度时停止自动推进,防止倒退 if (editingProgressIntervalRef.current) { clearInterval(editingProgressIntervalRef.current); editingProgressIntervalRef.current = null; } emitToastUpdate({ title: message, progress }); }, [emitToastUpdate]); */ return (
{isMobile || isTablet ? ( ) : ( )}
{isDesktop ? (
{/* 左侧最终视频缩略图栏(仅桌面) */} {taskObject?.final?.url && (
{isFinalBarOpen && (
{/* 预留历史列表,目前仅展示当前最终视频 */}
)}
)} setIsSmartChatBoxOpen(true)} setVideoPreview={(url, id) => { setPreviewVideoUrl(url); setPreviewVideoId(id); }} showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'} onGotoCut={generateEditPlan} isSmartChatBoxOpen={isSmartChatBoxOpen} onRetryVideo={(video_id) => handleRetryVideo(video_id)} />
) : ( setIsSmartChatBoxOpen(true)} setVideoPreview={(url, id) => { setPreviewVideoUrl(url); setPreviewVideoId(id); }} showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'} onGotoCut={generateEditPlan} isSmartChatBoxOpen={isSmartChatBoxOpen} onRetryVideo={(video_id) => handleRetryVideo(video_id)} onSelectView={(view) => setSelectedView(view)} enableVideoEdit={true} onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit} projectId={episodeId} /> )}
{taskObject.currentStage !== 'script' && (
{ setSelectedView('video'); setCurrentSketchIndex(index); }} onRetryVideo={handleRetryVideo} className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[33%]')} cols={isDesktop ? '20%' : isTablet ? '25%' : '33%'} selectedView={selectedView} aspectRatio={aspectRatio} isMobile={isMobile} />
)}
{/* AI剪辑按钮 - 已注释,不加载iframe */} {/* { isShowAutoEditing && (
) } */} {/* 导出进度显示 - 已注释 */} {/* {exportProgress && exportProgress.status === 'processing' && (
导出进度: {exportProgress.percentage}%
{exportProgress.message} {exportProgress.stage && ` (${exportProgress.stage})`}
)} */} {/* 测试导出接口按钮 - 隐藏显示(仍可通过逻辑调用) */}
{/* 智能对话按钮 */}
{isMobile ? (
{(!isSmartChatBoxOpen && chatTip) && (
{chatTip}
)} {/* 红点徽标 */} {(!isSmartChatBoxOpen && hasUnread) && ( )} { setIsSmartChatBoxOpen(true); setChatTip(null); setHasUnread(false); }} className="backdrop-blur-lg bg-custom-purple/80 border-transparent hover:opacity-90" />
) : ( setIsSmartChatBoxOpen(true)} className="backdrop-blur-lg" /> )}
{/* 智能对话弹窗 */} setIsSmartChatBoxOpen(false)} > { if (!isSmartChatBoxOpen && snippet) { setChatTip(snippet); setHasUnread(true); // 3秒后自动消失 setTimeout(() => setChatTip(null), 3000); } }} onClearPreview={() => { setPreviewVideoUrl(null); setPreviewVideoId(null); }} aiEditingResult={aiEditingResult} /> { SaveEditUseCase.clearData(); setIsEditModalOpen(false) }} taskObject={taskObject} currentSketchIndex={currentSketchIndex} roles={taskObject.roles.data} setIsPauseWorkFlow={setIsPauseWorkFlow} isPauseWorkFlow={isPauseWorkFlow} fallbackToStep={fallbackToStep} originalText={originalText} />
) }); export default WorkFlow; // 桥接组件:监听全局事件并驱动 H5ProgressToast const H5ToastBridge: React.FC = () => { const { show, update, hide } = useH5ProgressToast(); useEffect(() => { const onShow = (e: Event) => { const detail = (e as CustomEvent).detail || {}; show(detail); }; const onUpdate = (e: Event) => { const detail = (e as CustomEvent).detail || {}; update(detail); }; const onHide = () => hide(); window.addEventListener('h5Toast:show', onShow as EventListener); window.addEventListener('h5Toast:update', onUpdate as EventListener); window.addEventListener('h5Toast:hide', onHide as EventListener); return () => { window.removeEventListener('h5Toast:show', onShow as EventListener); window.removeEventListener('h5Toast:update', onUpdate as EventListener); window.removeEventListener('h5Toast:hide', onHide as EventListener); }; }, [show, update, hide]); return null; };