video-flow-b/components/pages/work-flow.tsx
2025-09-05 21:06:39 +08:00

359 lines
13 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 { 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 [editingStatus, setEditingStatus] = React.useState<'initial' | 'idle' | 'success' | 'error'>('initial');
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);
setEditingStatus('idle');
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}`);
setEditingStatus('error');
// 3秒后关闭通知
setTimeout(() => {
if (editingNotificationKey.current) {
notification.destroy(editingNotificationKey.current);
}
}, 3000);
}
});
}, []);
// 使用自定义 hooks 管理状态
const {
taskObject,
scriptData,
isLoading,
currentSketchIndex,
currentLoadingText,
setCurrentSketchIndex,
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({
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');
// 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 || editingStatus !== 'idle'}
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 ${['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)}
/>
</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剪辑按钮 - 当可以跳转剪辑时显示 */}
{
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;