video-flow-b/components/pages/work-flow.tsx

582 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 { 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 } 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 { EditPoint as EditPointType } from './work-flow/video-edit/types';
const WorkFlow = React.memo(function WorkFlow() {
useEffect(() => {
console.log("init-WorkFlow");
return () => {
console.log("unmount-WorkFlow");
// 销毁编辑通知
if (editingNotificationKey.current) {
notification.destroy(editingNotificationKey.current);
}
};
}, []);
const containerRef = useRef<HTMLDivElement>(null);
const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
const [activeEditTab, setActiveEditTab] = React.useState('1');
const [isSmartChatBoxOpen, setIsSmartChatBoxOpen] = React.useState(true);
const [previewVideoUrl, setPreviewVideoUrl] = React.useState<string | null>(null);
const [previewVideoId, setPreviewVideoId] = React.useState<string | null>(null);
const [isFocusChatInput, setIsFocusChatInput] = React.useState(false);
const [aiEditingResult, setAiEditingResult] = React.useState<any>(null);
// const aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise<void> }>(null);
const [editingStatus, setEditingStatus] = React.useState<'initial' | 'idle' | 'success' | 'error'>('initial');
// const [iframeAiEditingKey, setIframeAiEditingKey] = React.useState<string>(`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 searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || '';
const userId = JSON.parse(localStorage.getItem("currentUser") || '{}').id || NaN;
SaveEditUseCase.setProjectId(episodeId);
let editingNotificationKey = useRef<string>(`editing-${Date.now()}`);
const [isHandleEdit, setIsHandleEdit] = React.useState(false);
// 使用 ref 存储 handleTestExport 避免循环依赖
const handleTestExportRef = useRef<(() => Promise<any>) | 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);
editingNotificationKey.current = `editing-${Date.now()}`;
showEditingNotification({
description: 'Performing intelligent editing...',
successDescription: 'Editing successful',
timeoutDescription: 'Editing failed. Please click the scissors button to go to the intelligent editing platform.',
timeout: 8 * 60 * 1000,
key: editingNotificationKey.current,
onFail: () => {
console.log('❌ onFail callback triggered - Editing failed, retrying...');
// 清缓存 生成计划 视频重新分析
localStorage.removeItem(`isLoaded_plan_${episodeId}`);
// 先销毁当前通知
if (editingNotificationKey.current) {
notification.destroy(editingNotificationKey.current);
}
// 重新生成 iframeAiEditingKey 触发重新渲染
// setIframeAiEditingKey(`iframe-ai-editing-${Date.now()}`);
// 延时200ms后显示重试通知确保之前的通知已销毁
setTimeout(() => {
editingNotificationKey.current = `editing-${Date.now()}`;
showEditingNotification({
description: 'Retry intelligent editing...',
successDescription: 'Editing successful',
timeoutDescription: 'Editing failed. Please click the scissors button to go to the intelligent editing platform.',
timeout: 5 * 60 * 1000, // 5分钟超时
key: editingNotificationKey.current,
onFail: () => {
console.log('Editing retry failed');
// 清缓存
localStorage.removeItem(`isLoaded_plan_${episodeId}`);
// 5秒后关闭通知并设置错误状态
setTimeout(() => {
setEditingStatus('error');
setIsEditingInProgress(false); // 重置编辑状态
isEditingInProgressRef.current = false; // 重置 ref
if (editingNotificationKey.current) {
notification.destroy(editingNotificationKey.current);
}
}, 5000);
}
});
}, 200);
}
});
}, [episodeId]); // handleTestExport 在内部调用,无需作为依赖
/** 处理导出失败 */
const handleExportFailed = useCallback(() => {
console.log('Export failed, setting error status');
setEditingStatus('error');
// setIsEditingInProgress(false); // 已移除该状态变量
isEditingInProgressRef.current = false;
// 销毁当前编辑通知
if (editingNotificationKey.current) {
notification.destroy(editingNotificationKey.current);
}
}, []);
// 使用自定义 hooks 管理状态
const {
taskObject,
scriptData,
isLoading,
currentSketchIndex,
currentLoadingText,
setCurrentSketchIndex,
isPauseWorkFlow,
mode,
setIsPauseWorkFlow,
setAnyAttribute,
applyScript,
fallbackToStep,
originalText,
showGotoCutButton,
generateEditPlan,
handleRetryVideo,
isShowAutoEditing
} = useWorkflowData({
onEditPlanGenerated: handleEditPlanGenerated,
editingStatus: editingStatus,
onExportFailed: handleExportFailed
});
const {
isVideoPlaying,
toggleVideoPlay,
} = usePlaybackControls(taskObject.videos.data, taskObject.currentStage);
useEffect(() => {
console.log('changedIndex_work-flow', currentSketchIndex, taskObject);
}, [currentSketchIndex, taskObject]);
// 监听粗剪是否完成
useEffect(() => {
console.log('🎬 final video useEffect triggered:', {
finalUrl: taskObject.final.url,
notificationKey: editingNotificationKey.current,
isHandleEdit
});
if (taskObject.final.url && editingNotificationKey.current && isHandleEdit) {
console.log('🎉 显示编辑完成通知');
// 更新通知状态为完成
showEditingNotification({
isCompleted: true,
description: 'Performing intelligent editing...',
successDescription: 'Editing successful',
timeoutDescription: 'Editing failed, please try again',
timeout: 5 * 60 * 1000,
key: editingNotificationKey.current,
onComplete: () => {
console.log('Editing successful');
localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true');
setEditingStatus('success');
setIsEditingInProgress(false); // 重置编辑状态
isEditingInProgressRef.current = false; // 重置 ref
// 3秒后关闭通知
setTimeout(() => {
if (editingNotificationKey.current) {
notification.destroy(editingNotificationKey.current);
}
}, 3000);
},
});
}
}, [taskObject.final, isHandleEdit, episodeId]);
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); // 已移除该状态变量
}, []);
*/
return (
<div className="w-full overflow-hidden h-full px-[1rem] pb-[1rem]">
<div className="w-full h-full">
<div className="splashContainer-otuV_A">
<div className="content-vPGYx8">
<div className="info-UUGkPJ">
<TaskInfo
taskObject={taskObject}
currentLoadingText={currentLoadingText}
roles={taskObject.roles.data}
isPauseWorkFlow={isPauseWorkFlow}
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
onGotoCut={generateEditPlan}
setIsPauseWorkFlow={setIsPauseWorkFlow}
/>
</div>
</div>
<div className="media-Ocdu1O rounded-lg">
<div
className="videoContainer-qteKNi"
ref={containerRef}
>
<div className={`relative heroVideo-FIzuK1 ${['script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={taskObject.currentStage+'_'+currentSketchIndex}>
<MediaViewer
taskObject={taskObject}
scriptData={scriptData}
currentSketchIndex={currentSketchIndex}
isVideoPlaying={isVideoPlaying}
onEditModalOpen={handleEditModalOpen}
onToggleVideoPlay={toggleVideoPlay}
setIsPauseWorkFlow={setIsPauseWorkFlow}
setAnyAttribute={setAnyAttribute}
isPauseWorkFlow={isPauseWorkFlow}
applyScript={applyScript}
mode={mode}
onOpenChat={() => setIsSmartChatBoxOpen(true)}
setVideoPreview={(url, id) => {
setPreviewVideoUrl(url);
setPreviewVideoId(id);
}}
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
onGotoCut={generateEditPlan}
isSmartChatBoxOpen={isSmartChatBoxOpen}
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
// 临时禁用视频编辑功能: enableVideoEdit={true} onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
/>
</div>
</div>
{taskObject.currentStage !== 'script' && (
<div className="h-[123px] w-[calc((100vh-6rem-200px)/9*16)]">
<ThumbnailGrid
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
taskObject={taskObject}
currentSketchIndex={currentSketchIndex}
onSketchSelect={setCurrentSketchIndex}
onRetryVideo={handleRetryVideo}
/>
</div>
)}
</div>
</div>
</div>
{/* AI剪辑按钮 - 已注释不加载iframe */}
{/*
{
isShowAutoEditing && (
<div className="fixed right-[2rem] top-[8rem] z-[49]">
<Tooltip title="AI智能剪辑" placement="left">
<AIEditingIframeButton
key={iframeAiEditingKey}
ref={aiEditingButtonRef}
projectId={episodeId}
token={localStorage.getItem("token") || ""}
userId={userId.toString()}
size="md"
onComplete={handleIframeAIEditingComplete}
onError={handleIframeAIEditingError}
onProgress={handleIframeAIEditingProgress}
autoStart={process.env.NODE_ENV !== 'development'}
/>
</Tooltip>
</div>
)
}
*/}
{/* 导出进度显示 - 已注释 */}
{/*
{exportProgress && exportProgress.status === 'processing' && (
<div className="fixed right-[1rem] bottom-[20rem] z-[49]">
<div className="backdrop-blur-lg bg-black/30 border border-white/20 rounded-lg p-4 max-w-xs">
<div className="text-white text-sm mb-2">
导出进度: {exportProgress.percentage}%
</div>
<div className="w-full bg-gray-700 rounded-full h-2 mb-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${exportProgress.percentage}%` }}
/>
</div>
<div className="text-gray-300 text-xs">
{exportProgress.message}
{exportProgress.stage && ` (${exportProgress.stage})`}
</div>
</div>
</div>
)}
*/}
{/* 测试导出接口按钮 - 隐藏显示(仍可通过逻辑调用) */}
<div
className="fixed right-[1rem] bottom-[16rem] z-[49]"
style={{ display: 'none' }}
>
<Tooltip title="测试剪辑计划导出接口" placement="left">
<GlassIconButton
icon={TestTube}
size='md'
onClick={handleTestExport}
className="backdrop-blur-lg"
/>
</Tooltip>
</div>
{/* 智能对话按钮 */}
<div
className="fixed right-[1rem] bottom-[10rem] z-[49]"
>
<Tooltip title="Open chat" placement="left">
<GlassIconButton
icon={Bot}
size='md'
onClick={() => setIsSmartChatBoxOpen(true)}
className="backdrop-blur-lg"
/>
</Tooltip>
</div>
{/* 智能对话弹窗 */}
<Drawer
width="25%"
placement="right"
closable={false}
maskClosable={false}
open={isSmartChatBoxOpen}
getContainer={false}
autoFocus={false}
mask={false}
zIndex={49}
rootClassName="outline-none"
className="backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl"
style={{
backgroundColor: 'transparent',
borderBottomLeftRadius: 10,
borderTopLeftRadius: 10,
overflow: 'hidden',
}}
styles={{
body: {
backgroundColor: 'transparent',
padding: 0,
},
}}
onClose={() => setIsSmartChatBoxOpen(false)}
>
<SmartChatBox
isSmartChatBoxOpen={isSmartChatBoxOpen}
setIsSmartChatBoxOpen={setIsSmartChatBoxOpen}
projectId={episodeId}
userId={userId}
previewVideoUrl={previewVideoUrl}
previewVideoId={previewVideoId}
setIsFocusChatInput={setIsFocusChatInput}
onClearPreview={() => {
setPreviewVideoUrl(null);
setPreviewVideoId(null);
}}
aiEditingResult={aiEditingResult}
/>
</Drawer>
<EditModal
isOpen={isEditModalOpen}
activeEditTab={activeEditTab}
onClose={() => {
SaveEditUseCase.clearData();
setIsEditModalOpen(false)
}}
taskObject={taskObject}
currentSketchIndex={currentSketchIndex}
roles={taskObject.roles.data}
setIsPauseWorkFlow={setIsPauseWorkFlow}
isPauseWorkFlow={isPauseWorkFlow}
fallbackToStep={fallbackToStep}
originalText={originalText}
/>
</div>
)
});
export default WorkFlow;