forked from 77media/video-flow
356 lines
13 KiB
TypeScript
356 lines
13 KiB
TypeScript
"use client"
|
||
import React, { useRef, useEffect, useCallback } from "react";
|
||
import "./style/work-flow.css";
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
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 { AlertCircle, RefreshCw, Pause, Play, ChevronLast, ChevronsLeft, Bot, BriefcaseBusiness, Scissors } from "lucide-react";
|
||
import { motion } from "framer-motion";
|
||
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';
|
||
|
||
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 [aiEditingInProgress, setAiEditingInProgress] = React.useState(false);
|
||
const [isHovered, setIsHovered] = React.useState(false);
|
||
const [aiEditingResult, setAiEditingResult] = React.useState<any>(null);
|
||
const aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise<void> }>(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);
|
||
// 处理编辑计划生成完成的回调
|
||
const handleEditPlanGenerated = useCallback(() => {
|
||
console.log('✨ 编辑计划生成完成,开始AI剪辑');
|
||
setIsHandleEdit(true);
|
||
aiEditingButtonRef.current?.handleAIEditing();
|
||
editingNotificationKey.current = `editing-${Date.now()}`;
|
||
showEditingNotification({
|
||
description: 'Performing intelligent editing...',
|
||
successDescription: 'Editing successful',
|
||
timeoutDescription: 'Editing failed, please try again',
|
||
timeout: 5 * 60 * 1000,
|
||
key: editingNotificationKey.current,
|
||
onFail: () => {
|
||
console.log('Editing failed');
|
||
// 清缓存 生成计划 视频重新分析
|
||
localStorage.removeItem(`isLoaded_plan_${episodeId}`);
|
||
// 3秒后关闭通知
|
||
setTimeout(() => {
|
||
if (editingNotificationKey.current) {
|
||
notification.destroy(editingNotificationKey.current);
|
||
}
|
||
}, 3000);
|
||
}
|
||
});
|
||
}, []);
|
||
|
||
// 使用自定义 hooks 管理状态
|
||
const {
|
||
taskObject,
|
||
scriptData,
|
||
isLoading,
|
||
currentSketchIndex,
|
||
currentLoadingText,
|
||
dataLoadError,
|
||
setCurrentSketchIndex,
|
||
retryLoadData,
|
||
isPauseWorkFlow,
|
||
mode,
|
||
setIsPauseWorkFlow,
|
||
setAnyAttribute,
|
||
applyScript,
|
||
fallbackToStep,
|
||
originalText,
|
||
showGotoCutButton,
|
||
generateEditPlan,
|
||
handleRetryVideo
|
||
} = useWorkflowData({
|
||
onEditPlanGenerated: handleEditPlanGenerated
|
||
});
|
||
|
||
const {
|
||
isVideoPlaying,
|
||
toggleVideoPlay,
|
||
} = usePlaybackControls(taskObject.videos.data, taskObject.currentStage);
|
||
|
||
useEffect(() => {
|
||
console.log('changedIndex_work-flow', currentSketchIndex, taskObject);
|
||
}, [currentSketchIndex, taskObject]);
|
||
|
||
// 监听粗剪是否完成,如果完成 更新 showEditingNotification 的状态 为完成,延时 3s 并关闭
|
||
useEffect(() => {
|
||
if (taskObject.final.url && editingNotificationKey.current && isHandleEdit) {
|
||
// 更新通知状态为完成
|
||
showEditingNotification({
|
||
description: 'Performing intelligent editing...',
|
||
successDescription: 'Editing successful',
|
||
timeoutDescription: 'Editing failed, please try again',
|
||
timeout: 5 * 60 * 1000,
|
||
key: editingNotificationKey.current,
|
||
onComplete: () => {
|
||
console.log('编辑完成');
|
||
localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true');
|
||
// 3秒后关闭通知
|
||
setTimeout(() => {
|
||
if (editingNotificationKey.current) {
|
||
notification.destroy(editingNotificationKey.current);
|
||
}
|
||
}, 3000);
|
||
},
|
||
});
|
||
}
|
||
}, [taskObject.final, isHandleEdit]);
|
||
|
||
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);
|
||
}, []);
|
||
|
||
// 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}
|
||
onGotoCut={generateEditPlan}
|
||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="media-Ocdu1O rounded-lg">
|
||
<div
|
||
className="videoContainer-qteKNi"
|
||
ref={containerRef}
|
||
>
|
||
{isLoading ? (
|
||
<Skeleton className="w-full aspect-video rounded-lg" />
|
||
) : (
|
||
<div className={`relative heroVideo-FIzuK1 ${['final_video', '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}
|
||
onGotoCut={generateEditPlan}
|
||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
{taskObject.currentStage !== 'final_video' && 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剪辑按钮 - 当可以跳转剪辑时显示 */}
|
||
{
|
||
showGotoCutButton && (
|
||
<div className="fixed right-[2rem] top-[8rem] z-[49]">
|
||
<Tooltip title="AI智能剪辑" placement="left">
|
||
<AIEditingIframeButton
|
||
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>
|
||
)
|
||
}
|
||
|
||
{/* 智能对话按钮 */}
|
||
<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;
|