forked from 77media/video-flow
713 lines
28 KiB
TypeScript
713 lines
28 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 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 } 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 { getFirstFrame } from '@/utils/tools';
|
||
// 临时禁用视频编辑功能
|
||
// import { EditPoint as EditPointType } from './work-flow/video-edit/types';
|
||
import { AIEditingIframeButton } from './work-flow/ai-editing-iframe';
|
||
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();
|
||
// 通过全局事件桥接 H5ProgressToast(Provider 在本组件 JSX 中,逻辑层无法直接使用 hook)
|
||
const emitToastShow = useCallback((params: { title?: string; progress?: number }) => {
|
||
window.dispatchEvent(new CustomEvent('h5Toast:show', { detail: params }));
|
||
}, []);
|
||
const emitToastUpdate = useCallback((params: { title?: string; progress?: number }) => {
|
||
window.dispatchEvent(new CustomEvent('h5Toast:update', { detail: params }));
|
||
}, []);
|
||
const emitToastHide = useCallback(() => {
|
||
window.dispatchEvent(new CustomEvent('h5Toast:hide'));
|
||
}, []);
|
||
useEffect(() => {
|
||
console.log("init-WorkFlow");
|
||
return () => {
|
||
console.log("unmount-WorkFlow");
|
||
// 不在卸载时强制隐藏,避免严格模式下二次卸载导致刚显示就被关闭
|
||
};
|
||
}, [emitToastHide]);
|
||
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 [selectedView, setSelectedView] = React.useState<'final' | 'video' | null>(null);
|
||
const [isFinalBarOpen, setIsFinalBarOpen] = React.useState(true);
|
||
|
||
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 editingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||
const editingProgressIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||
const editingProgressStartRef = useRef<number>(0);
|
||
|
||
const searchParams = useSearchParams();
|
||
const episodeId = searchParams.get('episodeId') || '';
|
||
|
||
const userId = JSON.parse(localStorage.getItem("currentUser") || '{}').id || NaN;
|
||
|
||
SaveEditUseCase.setProjectId(episodeId);
|
||
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);
|
||
|
||
// 显示进度提示并启动超时定时器
|
||
emitToastShow({ title: 'Performing intelligent editing...', progress: 0 });
|
||
// 启动自动推进到 90% 的进度(8分钟)
|
||
if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current);
|
||
editingProgressStartRef.current = Date.now();
|
||
const totalMs = 8 * 60 * 1000;
|
||
editingProgressIntervalRef.current = setInterval(() => {
|
||
const elapsed = Date.now() - editingProgressStartRef.current;
|
||
const pct = Math.min(90, Math.max(0, Math.floor((elapsed / totalMs) * 90)));
|
||
emitToastUpdate({ progress: pct });
|
||
}, 250);
|
||
if (editingTimeoutRef.current) clearTimeout(editingTimeoutRef.current);
|
||
editingTimeoutRef.current = setTimeout(() => {
|
||
console.log('❌ Editing timeout - retrying...');
|
||
localStorage.removeItem(`isLoaded_plan_${episodeId}`);
|
||
if (editingProgressIntervalRef.current) {
|
||
clearInterval(editingProgressIntervalRef.current);
|
||
editingProgressIntervalRef.current = null;
|
||
}
|
||
emitToastHide();
|
||
setTimeout(() => {
|
||
emitToastShow({ title: 'Retry intelligent editing...', progress: 0 });
|
||
// 重试阶段自动推进(5分钟到 90%)
|
||
if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current);
|
||
editingProgressStartRef.current = Date.now();
|
||
const retryTotalMs = 5 * 60 * 1000;
|
||
editingProgressIntervalRef.current = setInterval(() => {
|
||
const elapsed = Date.now() - editingProgressStartRef.current;
|
||
const pct = Math.min(90, Math.max(0, Math.floor((elapsed / retryTotalMs) * 90)));
|
||
emitToastUpdate({ progress: pct });
|
||
}, 250);
|
||
if (editingTimeoutRef.current) clearTimeout(editingTimeoutRef.current);
|
||
editingTimeoutRef.current = setTimeout(() => {
|
||
console.log('Editing retry failed');
|
||
localStorage.removeItem(`isLoaded_plan_${episodeId}`);
|
||
setTimeout(() => {
|
||
setEditingStatus('error');
|
||
setIsEditingInProgress(false);
|
||
isEditingInProgressRef.current = false;
|
||
if (editingProgressIntervalRef.current) {
|
||
clearInterval(editingProgressIntervalRef.current);
|
||
editingProgressIntervalRef.current = null;
|
||
}
|
||
emitToastHide();
|
||
}, 5000);
|
||
}, 5 * 60 * 1000);
|
||
}, 200);
|
||
}, 8 * 60 * 1000);
|
||
}, [episodeId, emitToastHide, emitToastShow, emitToastUpdate]); // 移除 isEditingInProgress 依赖
|
||
|
||
/** 处理导出失败 */
|
||
const handleExportFailed = useCallback(() => {
|
||
console.log('Export failed, setting error status');
|
||
setEditingStatus('error');
|
||
// setIsEditingInProgress(false); // 已移除该状态变量
|
||
isEditingInProgressRef.current = false;
|
||
if (editingTimeoutRef.current) {
|
||
clearTimeout(editingTimeoutRef.current);
|
||
editingTimeoutRef.current = null;
|
||
}
|
||
if (editingProgressIntervalRef.current) {
|
||
clearInterval(editingProgressIntervalRef.current);
|
||
editingProgressIntervalRef.current = null;
|
||
}
|
||
emitToastHide();
|
||
}, [emitToastHide]);
|
||
|
||
// 使用自定义 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(() => {
|
||
if (taskObject?.final?.url && selectedView === null) {
|
||
setSelectedView('final');
|
||
}
|
||
}, [taskObject?.final?.url, selectedView]);
|
||
|
||
// 监听粗剪是否完成
|
||
useEffect(() => {
|
||
console.log('🎬 final video useEffect triggered:', {
|
||
finalUrl: taskObject.final.url,
|
||
isHandleEdit
|
||
});
|
||
|
||
if (taskObject.final.url && isHandleEdit) {
|
||
console.log('🎉 显示编辑完成通知');
|
||
// 完成:推进到 100 并清理超时计时器
|
||
if (editingTimeoutRef.current) {
|
||
clearTimeout(editingTimeoutRef.current);
|
||
editingTimeoutRef.current = null;
|
||
}
|
||
if (editingProgressIntervalRef.current) {
|
||
clearInterval(editingProgressIntervalRef.current);
|
||
editingProgressIntervalRef.current = null;
|
||
}
|
||
emitToastUpdate({ title: 'Editing successful', progress: 100 });
|
||
console.log('Editing successful');
|
||
localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true');
|
||
setEditingStatus('success');
|
||
setIsEditingInProgress(false);
|
||
isEditingInProgressRef.current = false;
|
||
setTimeout(() => {
|
||
emitToastHide();
|
||
}, 3000);
|
||
}
|
||
}, [taskObject.final, isHandleEdit, episodeId, emitToastHide, emitToastUpdate]);
|
||
|
||
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);
|
||
// 收到显式进度时停止自动推进,防止倒退
|
||
if (editingProgressIntervalRef.current) {
|
||
clearInterval(editingProgressIntervalRef.current);
|
||
editingProgressIntervalRef.current = null;
|
||
}
|
||
emitToastUpdate({ title: message, progress });
|
||
}, [emitToastUpdate]);
|
||
*/
|
||
|
||
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}
|
||
/>
|
||
) : (
|
||
<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 ${!isDesktop ? '!flex' : ''}`}>
|
||
<div
|
||
className={`videoContainer-qteKNi ${!isDesktop ? '!w-full flex-1 items-center' : ''}`}
|
||
ref={containerRef}
|
||
>
|
||
{isDesktop ? (
|
||
<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}>
|
||
{/* 左侧最终视频缩略图栏(仅桌面) */}
|
||
{taskObject?.final?.url && (
|
||
<div
|
||
className="absolute -left-36 top-0 z-[50]"
|
||
data-alt="final-sidebar"
|
||
>
|
||
<div className={`flex items-start`}>
|
||
{isFinalBarOpen && (
|
||
<div
|
||
className="w-28 max-h-[60vh] overflow-y-auto rounded-lg backdrop-blur-lg bg-black/30 border border-white/20 shadow-xl p-2 mr-2"
|
||
data-alt="final-thumbnails"
|
||
>
|
||
{/* 预留历史列表,目前仅展示当前最终视频 */}
|
||
<button
|
||
type="button"
|
||
onClick={() => setSelectedView('final')}
|
||
className={`block w-full overflow-hidden rounded-md border ${selectedView === 'final' ? 'border-blue-500' : 'border-white/20'}`}
|
||
data-alt="final-thumb-item"
|
||
aria-label="Select final video"
|
||
>
|
||
<img
|
||
src={getFirstFrame(taskObject.final.url)}
|
||
alt="final"
|
||
className="w-full h-auto object-cover"
|
||
/>
|
||
<div className="text-xs text-white/80 text-center py-1">Final</div>
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
<MediaViewer
|
||
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 || editingStatus !== 'idle'}
|
||
onGotoCut={generateEditPlan}
|
||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||
/>
|
||
</div>
|
||
) : (
|
||
<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);
|
||
}}
|
||
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
|
||
onGotoCut={generateEditPlan}
|
||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||
onSelectView={(view) => setSelectedView(view)}
|
||
// 临时禁用视频编辑功能: enableVideoEdit={true} onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
|
||
/>
|
||
)}
|
||
</div>
|
||
{taskObject.currentStage !== 'script' && (
|
||
<div className={`h-[123px] ${!isDesktop ? '!w-full' : 'w-[calc((100vh-6rem-200px)/9*16)]'}`}>
|
||
<ThumbnailGrid
|
||
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
|
||
taskObject={taskObject}
|
||
currentSketchIndex={currentSketchIndex}
|
||
onSketchSelect={(index) => {
|
||
setSelectedView('video');
|
||
setCurrentSketchIndex(index);
|
||
}}
|
||
onRetryVideo={handleRetryVideo}
|
||
className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[40%]')}
|
||
selectedView={selectedView}
|
||
/>
|
||
</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>
|
||
</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;
|
||
};
|