video-flow-b/components/pages/work-flow.tsx
2025-09-23 15:09:01 +08:00

731 lines
28 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 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();
// 通过全局事件桥接 H5ProgressToastProvider 在本组件 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,
aspectRatio
} = useWorkflowData({
onEditPlanGenerated: handleEditPlanGenerated,
editingStatus: editingStatus,
onExportFailed: handleExportFailed
});
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 && 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-0 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"
/>
</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}
projectId={episodeId}
/>
)}
</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}
aspectRatio={aspectRatio}
/>
</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]"
>
{isMobile ? (
<GlassIconButton
icon={Bot}
size='md'
onClick={() => setIsSmartChatBoxOpen(true)}
className="backdrop-blur-lg"
/>
) : (
<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%'}
placement={isMobile ? 'bottom' : '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',
...(isMobile
? { borderTopLeftRadius: 10, borderTopRightRadius: 10 }
: { borderBottomLeftRadius: 10, borderTopLeftRadius: 10 }),
overflow: 'hidden',
}}
styles={{
body: {
backgroundColor: 'transparent',
padding: 0,
maxHeight: '100vh',
overflow: 'auto',
}
}}
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;
};