forked from 77media/video-flow
436 lines
17 KiB
TypeScript
436 lines
17 KiB
TypeScript
"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;
|
||
};
|