video-flow-b/components/pages/work-flow.tsx
2025-10-14 15:00:03 +08:00

436 lines
17 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 H5TaskInfo from "./work-flow/H5TaskInfo";
import H5ProgressBar from "./work-flow/H5ProgressBar";
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 { EditPoint as EditPointType } from './work-flow/video-edit/types';
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();
const containerRef = useRef<HTMLDivElement>(null);
const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
const [activeEditTab, setActiveEditTab] = React.useState('1');
const [isSmartChatBoxOpen, setIsSmartChatBoxOpen] = React.useState(true);
const [chatTip, setChatTip] = React.useState<string | null>(null);
const [hasUnread, setHasUnread] = React.useState(false);
const [previewVideoUrl, setPreviewVideoUrl] = React.useState<string | null>(null);
const [previewVideoId, setPreviewVideoId] = React.useState<string | null>(null);
const [isFocusChatInput, setIsFocusChatInput] = React.useState(false);
const [selectedView, setSelectedView] = React.useState<'final' | 'video' | null>(null);
const [aiEditingResult, setAiEditingResult] = React.useState<any>(null);
const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || '';
const userId = JSON.parse(localStorage.getItem("currentUser") || '{}').id || NaN;
SaveEditUseCase.setProjectId(episodeId);
// 使用自定义 hooks 管理状态
const {
taskObject,
scriptData,
isLoading,
currentSketchIndex,
currentLoadingText,
setCurrentSketchIndex,
isPauseWorkFlow,
mode,
setIsPauseWorkFlow,
setAnyAttribute,
applyScript,
fallbackToStep,
originalText,
showGotoCutButton,
generateEditPlan,
handleRetryVideo,
aspectRatio
} = useWorkflowData({
});
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) {
setSelectedView('final');
}
}, [taskObject?.final?.url]);
const handleEditModalOpen = useCallback((tab: string) => {
setActiveEditTab(tab);
setIsEditModalOpen(true);
}, []);
// 视频编辑描述提交处理函数
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]);
return (
<H5ProgressToastProvider>
<H5ToastBridge />
<div className={`w-full overflow-hidden h-full ${isDesktop ? 'px-[1rem] pb-[1rem]' : ''}`}>
<div className="w-full h-full">
<div className="splashContainer-otuV_A">
<div className="content-vPGYx8">
<div className="info-UUGkPJ">
{isMobile || isTablet ? (
<>
<H5TaskInfo
title={taskObject.title}
current={currentSketchIndex + 1}
taskObject={taskObject}
selectedView={selectedView}
currentLoadingText={currentLoadingText}
/>
{taskObject.currentStage !== 'init' && taskObject.status !== 'COMPLETED' && (
<H5ProgressBar
taskObject={taskObject}
scriptData={scriptData}
currentLoadingText={currentLoadingText}
/>
)}
</>
) : (
<TaskInfo
taskObject={taskObject}
currentLoadingText={currentLoadingText}
roles={taskObject.roles.data}
isPauseWorkFlow={isPauseWorkFlow}
onGotoCut={generateEditPlan}
setIsPauseWorkFlow={setIsPauseWorkFlow}
/>
)}
</div>
</div>
<div className={`media-Ocdu1O rounded-lg ${!isDesktop ? '!flex' : ''}`}>
<div
className={`videoContainer-qteKNi ${!isDesktop ? '!w-full flex-1 items-center' : ''}`}
ref={containerRef}
>
{isDesktop ? (
<div className={`relative heroVideo-FIzuK1`}
style={{
height: taskObject.final.url ? 'calc(100vh - 8rem)' : 'calc(100vh - 12rem)'
}}
>
<MediaViewer
key={taskObject.currentStage+'_'+currentSketchIndex}
taskObject={taskObject}
scriptData={scriptData}
currentSketchIndex={currentSketchIndex}
isVideoPlaying={isVideoPlaying}
selectedView={selectedView}
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)}
aspectRatio={aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '16:9' : '9:16'}
placeholderWidth={aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `calc((100vh - ${taskObject.final.url ? '8rem' : '12rem'}) / 9 * 16)` : `calc((100vh - ${taskObject.final.url ? '8rem' : '12rem'}) / 16 * 9)`}
/>
{['scene', 'character', 'video', 'final_video'].includes(taskObject.currentStage) && (
<div className={`h-14 absolute bottom-[0.5rem] left-[50%] translate-x-[-50%] z-[21]`}
style={{
maxWidth: 'calc(100% - 6rem)'
}}>
<ThumbnailGrid
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
taskObject={taskObject}
currentSketchIndex={currentSketchIndex}
onSketchSelect={(index) => {
if (index === -1 && taskObject.final.url) {
// 点击最终视频
setSelectedView('final');
setCurrentSketchIndex(0);
} else {
// 点击普通视频
taskObject.final.url && 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}
/>
</div>
)}
</div>
) : (
<div className="relative w-full">
<H5MediaViewer
taskObject={taskObject}
scriptData={scriptData}
currentSketchIndex={currentSketchIndex}
selectedView={selectedView}
mode={mode}
setIsPauseWorkFlow={setIsPauseWorkFlow}
setAnyAttribute={setAnyAttribute}
isPauseWorkFlow={isPauseWorkFlow}
applyScript={applyScript}
setCurrentSketchIndex={setCurrentSketchIndex}
onOpenChat={() => setIsSmartChatBoxOpen(true)}
setVideoPreview={(url, id) => {
setPreviewVideoUrl(url);
setPreviewVideoId(id);
}}
isSmartChatBoxOpen={isSmartChatBoxOpen}
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
onSelectView={(view) => setSelectedView(view)}
enableVideoEdit={true}
onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
projectId={episodeId}
aspectRatio={aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '16:9' : '9:16'}
showProgress={taskObject.currentStage !== 'init' && taskObject.status !== 'COMPLETED'}
/>
{['scene', 'character', 'video', 'final_video'].includes(taskObject.currentStage) && (
<div className={`h-14 absolute left-[50%] translate-x-[-50%] z-[21] ${isMobile ? '' : 'bottom-[180px]'}`}
style={{
maxWidth: 'calc(100vw - 5.5rem)',
bottom: (aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE') ? '-2.5rem' : '0.5rem'
}}>
<ThumbnailGrid
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
taskObject={taskObject}
currentSketchIndex={currentSketchIndex}
onSketchSelect={(index) => {
if (index === -1) {
// 点击最终视频
setSelectedView('final');
setCurrentSketchIndex(0);
} else {
// 点击普通视频
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}
/>
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
{/* 智能对话按钮 */}
<div
className={`fixed right-[1rem] z-[49] ${isMobile ? 'bottom-[9rem]' : 'bottom-[10rem]'}`}
>
{isMobile ? (
<div className="relative">
{(!isSmartChatBoxOpen && chatTip) && (
<div className="absolute -top-8 right-0 bg-black/80 text-white text-xs px-2 py-1 rounded-md whitespace-nowrap bg-custom-blue/30">
{chatTip}
</div>
)}
{/* 红点徽标 */}
{(!isSmartChatBoxOpen && hasUnread) && (
<span className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border border-white" />
)}
<GlassIconButton
icon={MessageCircle}
size='md'
onClick={() => {
setIsSmartChatBoxOpen(true);
setChatTip(null);
setHasUnread(false);
}}
className="backdrop-blur-lg bg-custom-purple/80 border-transparent hover:bg-custom-purple/80"
/>
</div>
) : (
<Tooltip title="Open chat" placement="left">
<GlassIconButton
icon={Bot}
size='md'
onClick={() => setIsSmartChatBoxOpen(true)}
className="backdrop-blur-lg"
/>
</Tooltip>
)}
</div>
{/* 智能对话弹窗 */}
<Drawer
width={isMobile ? '100vw' : '25%'}
height={isMobile ? 'auto' : ''}
placement={isMobile ? 'bottom' : 'right'}
closable={false}
maskClosable={false}
open={isSmartChatBoxOpen}
getContainer={false}
autoFocus={false}
mask={false}
rootClassName="outline-none"
className="bg-transparent max-h-[100vh]"
style={{
backgroundColor: 'transparent',
...(isMobile
? { borderTopLeftRadius: 10, borderTopRightRadius: 10 }
: { borderBottomLeftRadius: 10, borderTopLeftRadius: 10 }),
overflow: 'hidden',
}}
styles={{
body: {
backgroundColor: 'transparent',
padding: 0,
maxHeight: isMobile ? 'calc(100vh - 5.5rem)' : 'calc(100vh - 4rem)',
overflow: 'hidden',
}
}}
onClose={() => setIsSmartChatBoxOpen(false)}
>
<SmartChatBox
isSmartChatBoxOpen={isSmartChatBoxOpen}
setIsSmartChatBoxOpen={setIsSmartChatBoxOpen}
projectId={episodeId}
userId={userId}
previewVideoUrl={previewVideoUrl}
previewVideoId={previewVideoId}
setIsFocusChatInput={setIsFocusChatInput}
onNewMessage={(snippet) => {
if (!isSmartChatBoxOpen && snippet) {
setChatTip(snippet);
setHasUnread(true);
// 5秒后自动消失
setTimeout(() => setChatTip(null), 5000);
}
}}
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>
</H5ProgressToastProvider>
)
});
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;
};