2025-10-14 23:38:42 +08:00

910 lines
34 KiB
TypeScript
Raw Permalink 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, useState, SetStateAction, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow, PictureInPicture2, PenTool } from 'lucide-react';
import { showDownloadOptionsModal } from './download-options-modal';
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
import { mockScriptData } from '@/components/script-renderer/mock';
import { Skeleton } from '@/components/ui/skeleton';
import ScriptLoading from './script-loading';
import { TaskObject } from '@/api/DTO/movieEdit';
import { Button, Tooltip } from 'antd';
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
import { post } from '@/api/request';
import { VideoEditOverlay } from './video-edit/VideoEditOverlay';
import { EditPoint as EditPointType } from './video-edit/types';
import { isVideoModificationEnabled } from '@/lib/server-config';
import { useSearchParams } from 'next/navigation';
import RenderLoading from './RenderLoading';
interface MediaViewerProps {
taskObject: TaskObject;
scriptData: any;
currentSketchIndex: number;
isVideoPlaying: boolean;
selectedView?: 'final' | 'video' | null;
onEditModalOpen: (tab: string) => void;
onToggleVideoPlay: () => void;
setIsPauseWorkFlow: (isPause: boolean) => void;
setAnyAttribute: any;
isPauseWorkFlow: boolean;
applyScript: any;
mode: string;
onOpenChat?: () => void;
setVideoPreview?: (url: string, id: string) => void;
showGotoCutButton?: boolean;
onGotoCut: () => void;
isSmartChatBoxOpen: boolean;
onRetryVideo?: (video_id: string) => void;
enableVideoEdit?: boolean;
onVideoEditDescriptionSubmit?: (editPoint: EditPointType, description: string) => void;
projectId?: string;
aspectRatio: string;
placeholderWidth: string;
}
export const MediaViewer = React.memo(function MediaViewer({
taskObject,
scriptData,
currentSketchIndex,
isVideoPlaying,
selectedView,
onEditModalOpen,
onToggleVideoPlay,
setIsPauseWorkFlow,
setAnyAttribute,
isPauseWorkFlow,
applyScript,
mode,
onOpenChat,
setVideoPreview,
showGotoCutButton,
onGotoCut,
isSmartChatBoxOpen,
onRetryVideo,
enableVideoEdit = true,
onVideoEditDescriptionSubmit,
projectId,
aspectRatio,
placeholderWidth
}: MediaViewerProps) {
const mainVideoRef = useRef<HTMLVideoElement>(null);
const finalVideoRef = useRef<HTMLVideoElement>(null);
const videoContentRef = useRef<HTMLDivElement>(null);
// 音量控制状态
const [isMuted, setIsMuted] = useState(false);
const [volume, setVolume] = useState(0.8);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
// 最终视频控制状态
const [isFinalVideoPlaying, setIsFinalVideoPlaying] = useState(true);
const [isFullscreen, setIsFullscreen] = useState(false);
const [finalVideoReady, setFinalVideoReady] = useState(false);
const [userHasInteracted, setUserHasInteracted] = useState(false);
const [toosBtnRight, setToodsBtnRight] = useState('1rem');
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
const [isLoadingDownloadAllVideosBtn, setIsLoadingDownloadAllVideosBtn] = useState(false);
const [isVideoEditMode, setIsVideoEditMode] = useState(false);
// 控制钢笔图标显示的状态 - 参考谷歌登录按钮的实现
const [showVideoModification, setShowVideoModification] = useState(false);
const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || '';
useEffect(() => {
if (isSmartChatBoxOpen) {
const videoContentWidth = videoContentRef.current?.clientWidth ?? 0;
const right = (window.innerWidth * 0.25) - ((window.innerWidth - videoContentWidth) / 2) + 32;
setToodsBtnRight(right + 'px');
} else {
setToodsBtnRight('1rem');
}
}, [isSmartChatBoxOpen])
// 检查视频修改功能是否启用 - 参考谷歌登录按钮的实现
useEffect(() => {
const checkVideoModificationStatus = async () => {
try {
console.log('🔍 MediaViewer开始检查视频修改功能状态...');
const enabled = await isVideoModificationEnabled();
console.log('📋 MediaViewer视频修改功能启用状态:', enabled);
setShowVideoModification(enabled);
console.log('📋 MediaViewer设置showVideoModification状态为:', enabled);
} catch (error) {
console.error("❌ MediaViewerFailed to check video modification status:", error);
setShowVideoModification(false); // 出错时默认不显示
}
};
checkVideoModificationStatus();
}, []); // 只在组件挂载时执行一次
// 调试:监控钢笔图标显示状态
useEffect(() => {
console.log('🔧 MediaViewer状态更新:', {
enableVideoEdit,
showVideoModification,
shouldShowPenIcon: enableVideoEdit && showVideoModification
});
}, [enableVideoEdit, showVideoModification]);
// 音量控制函数
const toggleMute = () => {
setUserHasInteracted(true);
setIsMuted(!isMuted);
if (mainVideoRef.current) {
mainVideoRef.current.muted = !isMuted;
}
if (finalVideoRef.current) {
finalVideoRef.current.muted = !isMuted;
}
};
const handleVolumeChange = (newVolume: number) => {
setUserHasInteracted(true);
setVolume(newVolume);
if (mainVideoRef.current) {
mainVideoRef.current.volume = newVolume;
}
if (finalVideoRef.current) {
finalVideoRef.current.volume = newVolume;
}
};
// 应用音量设置到视频元素
const applyVolumeSettings = (videoElement: HTMLVideoElement) => {
if (videoElement) {
videoElement.volume = volume;
videoElement.muted = isMuted;
}
};
useEffect(() => {
if (finalVideoRef.current && finalVideoReady) {
if (isFinalVideoPlaying) {
finalVideoRef.current.play().catch(error => {
console.log('最终视频自动播放被阻止:', error);
// 如果自动播放被阻止,将状态设置为暂停
setIsFinalVideoPlaying(false);
});
} else {
finalVideoRef.current.pause();
}
}
}, [isFinalVideoPlaying, finalVideoReady]);
// 最终视频播放控制
const toggleFinalVideoPlay = () => {
setUserHasInteracted(true);
setIsFinalVideoPlaying(!isFinalVideoPlaying);
};
// 处理最终视频加载完成
const handleFinalVideoLoaded = () => {
if (finalVideoRef.current) {
setFinalVideoReady(true);
applyVolumeSettings(finalVideoRef.current);
try {
setDuration(Number.isFinite(finalVideoRef.current.duration) ? finalVideoRef.current.duration : 0);
} catch {}
// 如果当前状态是应该播放的,尝试播放
if (isFinalVideoPlaying) {
finalVideoRef.current.play().catch(error => {
console.log('最终视频自动播放被阻止:', error);
setIsFinalVideoPlaying(false);
});
}
}
};
// 处理视频点击 - 首次交互时尝试播放
const handleVideoClick = () => {
if (!userHasInteracted && finalVideoRef.current && finalVideoReady) {
setUserHasInteracted(true);
if (isFinalVideoPlaying) {
finalVideoRef.current.play().catch(error => {
console.log('视频播放失败:', error);
});
}
}
};
// 使用 useMemo 缓存最终视频元素,避免重复创建和请求
const memoizedFinalVideoElement = useMemo(() => {
console.log('final', taskObject.final);
if (!taskObject.final?.url) return null;
return (
<video
ref={finalVideoRef}
className="w-full h-full object-contain rounded-lg"
style={{
aspectRatio: aspectRatio
}}
src={taskObject.final.url}
autoPlay={isFinalVideoPlaying}
loop
playsInline
preload="metadata"
poster={taskObject.final.snapshot_url || getFirstFrame(taskObject.final.url)}
onLoadedData={handleFinalVideoLoaded}
onPlay={() => setIsFinalVideoPlaying(true)}
onPause={() => setIsFinalVideoPlaying(false)}
onClick={handleVideoClick}
/>
);
}, [taskObject.final?.url, isFinalVideoPlaying, handleFinalVideoLoaded, handleVideoClick]);
// 包装编辑按钮点击事件
const handleEditClick = (tab: string, from?: string) => {
if (from === 'final') {
// 暂停视频播放
finalVideoRef.current?.pause();
}
// TODO 点击没有任何事件效果,页面没变化
setUserHasInteracted(true);
onEditModalOpen(tab);
};
// 全屏控制
const toggleFullscreen = () => {
setUserHasInteracted(true);
const target = activeVideoRef().current;
if (!document.fullscreenElement) {
if (target) {
target.requestFullscreen?.() ||
(target as any).webkitRequestFullscreen?.() ||
(target as any).msRequestFullscreen?.();
setIsFullscreen(true);
}
} else {
document.exitFullscreen?.() ||
(document as any).webkitExitFullscreen?.() ||
(document as any).msExitFullscreen?.();
setIsFullscreen(false);
}
};
// 视频播放控制
useEffect(() => {
if (mainVideoRef.current) {
applyVolumeSettings(mainVideoRef.current);
if (isVideoPlaying) {
mainVideoRef.current.play().catch(error => {
console.log('视频播放失败:', error);
});
} else {
mainVideoRef.current.pause();
}
try {
setDuration(Number.isFinite(mainVideoRef.current.duration) ? mainVideoRef.current.duration : 0);
} catch {}
}
}, [isVideoPlaying]);
// 当切换视频时重置视频播放
useEffect(() => {
if (mainVideoRef.current) {
applyVolumeSettings(mainVideoRef.current);
mainVideoRef.current.currentTime = 0;
if (isVideoPlaying) {
mainVideoRef.current.play().catch(error => {
console.log('视频播放失败:', error);
});
}
}
}, [currentSketchIndex]);
// 音量设置变化时应用到所有视频
useEffect(() => {
if (mainVideoRef.current) {
applyVolumeSettings(mainVideoRef.current);
}
if (finalVideoRef.current) {
applyVolumeSettings(finalVideoRef.current);
}
}, [volume, isMuted]);
// 绑定时间更新(只监听当前活跃的 video
useEffect(() => {
const activeRef = activeVideoRef();
const activeVideo = activeRef.current;
if (!activeVideo) return;
const onTimeUpdate = (e: Event) => {
const el = e.currentTarget as HTMLVideoElement;
// 只有当事件来源是当前活跃视频时才更新状态
if (el === activeVideo) {
setCurrentTime(el.currentTime || 0);
if (Number.isFinite(el.duration)) setDuration(el.duration);
}
};
activeVideo.addEventListener('timeupdate', onTimeUpdate);
activeVideo.addEventListener('loadedmetadata', onTimeUpdate);
return () => {
activeVideo.removeEventListener('timeupdate', onTimeUpdate);
activeVideo.removeEventListener('loadedmetadata', onTimeUpdate);
};
}, [selectedView, taskObject.currentStage]); // 依赖项包含影响activeVideoRef的状态
// 当切换视频源时重置进度状态
useEffect(() => {
setCurrentTime(0);
setDuration(0);
}, [selectedView, currentSketchIndex, taskObject.currentStage]);
const activeVideoRef = () => {
// 根据当前阶段选择活跃的 video 引用
const effectiveStage = (selectedView === 'final' && taskObject.final?.url)
? 'final_video'
: (!['script', 'character', 'scene'].includes(taskObject.currentStage) ? 'video' : taskObject.currentStage);
return effectiveStage === 'final_video' ? finalVideoRef : mainVideoRef;
};
const progressPercent = duration > 0 ? Math.min(100, Math.max(0, (currentTime / duration) * 100)) : 0;
const formatRemaining = (dur: number, cur: number) => {
const remain = Math.max(0, Math.round(dur - cur));
const m = Math.floor(remain / 60);
const s = remain % 60;
return `-${m}:${s.toString().padStart(2, '0')}`;
};
const seekTo = (pct: number) => {
const ref = activeVideoRef().current;
if (!ref || !Number.isFinite(ref.duration)) return;
const t = (pct / 100) * ref.duration;
ref.currentTime = t;
setCurrentTime(t);
};
const requestPip = async () => {
try {
const ref = activeVideoRef().current as any;
if (ref && typeof ref.requestPictureInPicture === 'function') {
await ref.requestPictureInPicture();
}
} catch {}
};
// 监听全屏状态变化
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
document.removeEventListener('msfullscreenchange', handleFullscreenChange);
};
}, []);
// 组件卸载时清理视频状态
useEffect(() => {
return () => {
// 清理最终视频状态
setFinalVideoReady(false);
if (finalVideoRef.current) {
finalVideoRef.current.pause();
}
};
}, []);
// 加载中
const renderLoading = () => {
return (
<div data-alt="generating-overlay" className="absolute inset-0 bg-black/40 flex items-center justify-center">
<div data-alt="generating-content" className="relative flex flex-col items-center gap-4">
{/* 渐变进度环 */}
<div className="relative w-24 h-24">
{/* 外层旋转渐变边框 */}
<div
className="absolute inset-0 rounded-full opacity-80 animate-spin"
style={{
background: 'conic-gradient(from 0deg, transparent 0%, #3b82f6 25%, #60a5fa 50%, #93c5fd 75%, transparent 100%)',
animation: 'spin 2s linear infinite'
}}
/>
{/* 内层遮罩(形成边框效果) */}
<div className="absolute inset-[2px] rounded-full bg-black/60 backdrop-blur-sm" />
</div>
{/* 文案 */}
<div className="text-white text-center">
<div className="text-sm font-medium" role="status" aria-live="polite" aria-busy="true">
Generating...
</div>
<div className="text-xs text-white/60 mt-1">Processing your request</div>
</div>
</div>
</div>
)
}
// 渲染底部通栏控制条与图2一致
const renderBottomControls = (
isFinal: boolean,
onPlayToggle: () => void,
playing: boolean
) => (
<div
className="absolute left-0 right-0 bottom-4 z-[21] px-2"
data-alt={isFinal ? 'final-controls' : 'video-controls'}
>
<div className="flex items-center justify-between">
{/* 播放/暂停 */}
<GlassIconButton icon={playing ? Pause : Play} onClick={onPlayToggle} size="sm" />
{/* 全屏 */}
<GlassIconButton className="group-hover:block hidden animate-in duration-300" icon={isFullscreen ? Minimize : Maximize} onClick={toggleFullscreen} size="sm" />
</div>
</div>
);
// 渲染最终成片
const renderFinalVideo = () => {
// 使用真实的final数据如果没有则使用默认值
return (
<div
className="relative w-full h-full rounded-lg overflow-hidden"
key={`render-video-${taskObject.final.url}`}
ref={videoContentRef}
style={{
minWidth: placeholderWidth
}}
>
<div className="relative w-full h-full group">
{/* 背景模糊的视频 */}
<motion.div
className="absolute inset-0 overflow-hidden"
initial={{ filter: "blur(0px)", scale: 1, opacity: 1 }}
animate={{ filter: "blur(20px)", scale: 1, opacity: 0.5 }}
transition={{ duration: 0.8, ease: "easeInOut" }}
>
<video
className="w-full h-full rounded-lg object-contain object-center"
style={{
aspectRatio: aspectRatio
}}
src={taskObject.final.url}
loop
playsInline
muted
poster={taskObject.final.snapshot_url || getFirstFrame(taskObject.final.url)}
preload="none"
/>
</motion.div>
{/* 最终成片视频 */}
<motion.div
initial={{ clipPath: "inset(0 50% 0 50%)", filter: "blur(10px)" }}
animate={{ clipPath: "inset(0 0% 0 0%)", filter: "blur(0px)" }}
transition={{
clipPath: { duration: 1.2, ease: [0.43, 0.13, 0.23, 0.96] },
filter: { duration: 0.6, delay: 0.3 }
}}
className="relative z-10 w-full h-full"
>
{memoizedFinalVideoElement}
</motion.div>
{/* 编辑和剪辑按钮 */}
<div className="absolute top-2 right-2 z-[21] flex items-center gap-2 transition-right duration-100"
style={{
right: aspectRatio === '16:9' ? toosBtnRight : ''
}}>
<Tooltip placement="top" title='Edit'>
<GlassIconButton
icon={Edit3}
size="sm"
onClick={() => handleEditClick('3', 'final')}
/>
</Tooltip>
{/* 下载按钮 */}
<Tooltip placement="top" title="Download">
<GlassIconButton
icon={Download}
size='sm'
onClick={() => {
const totalVideos = taskObject.videos.data.filter((video: any) => video.urls && video.urls.length > 0).length;
showDownloadOptionsModal({
currentVideoIndex: 0,
totalVideos: totalVideos + 1,
isCurrentVideoFailed: false,
isFinalStage: true,
projectId: episodeId || '',
onDownloadCurrent: async (withWatermark: boolean) => {
setIsLoadingDownloadBtn(true);
try {
const json: any = await post('/movie/download_video', {
project_id: episodeId,
watermark: withWatermark
});
const url = json?.data?.download_url as string | undefined;
if (url) await downloadVideo(url);
} finally {
setIsLoadingDownloadBtn(false);
}
},
onDownloadAll: () => {}
});
}}
/>
</Tooltip>
{showGotoCutButton && (
<Tooltip placement="top" title='Go to AI-powered editing platform'>
<GlassIconButton icon={Scissors} size='sm' onClick={onGotoCut} />
</Tooltip>
)}
</div>
{/* 底部通栏控制区域 */}
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.6 }}>
{renderBottomControls(true, toggleFinalVideoPlay, isFinalVideoPlaying)}
</motion.div>
</div>
</div>
);
};
// 渲染视频内容
const renderVideoContent = (onGotoCut: () => void) => {
if (!taskObject.videos.data[currentSketchIndex]) return null;
const urls = taskObject.videos.data[currentSketchIndex]?.urls ? taskObject.videos.data[currentSketchIndex]?.urls.join(',') : '';
return (
<div
className="relative w-full h-full rounded-lg group"
key={`render-video-${urls}`}
ref={videoContentRef}
style={{
minWidth: placeholderWidth
}}
>
{/* 背景模糊的图片 */}
{taskObject.videos.data[currentSketchIndex].video_status !== 1 && (
<div className="absolute inset-0 overflow-hidden z-20">
{/* 生成中 */}
{(taskObject.videos.data[currentSketchIndex].video_status === 0 || taskObject.videos.data[currentSketchIndex].video_status === 2) && (
<RenderLoading
loadingText={taskObject.videos.data[currentSketchIndex].video_status === 0 ? 'Generating video...' : 'Violation of security policy. Please modify your prompt and regenerate.'}
isFailed={taskObject.videos.data[currentSketchIndex].video_status === 2}
/>
)}
{/* 生成失败 */}
{/* {taskObject.videos.data[currentSketchIndex].video_status === 2 && (
<div className="absolute inset-0 bg-[#fcb0ba1a] flex flex-col items-center justify-center">
<img src={error_image.src} alt="error" className="w-12 h-12" />
<div className="text-white text-center">
<div className="text-sm font-medium" role="status" aria-live="polite" aria-busy="true">
Failed
</div>
<div className="text-xs text-white/60 mt-1">Violation of security policy. Please modify your prompt and regenerate.</div>
</div>
</div>
)} */}
</div>
)}
{/* 视频 多个 取第一个 */}
{ taskObject.videos.data[currentSketchIndex].urls && taskObject.videos.data[currentSketchIndex].urls.length > 0 && (
<>
<motion.div
initial={{ clipPath: "inset(0 100% 0 0)" }}
animate={{ clipPath: "inset(0 0% 0 0)" }}
transition={{ duration: 0.8, ease: [0.43, 0.13, 0.23, 0.96] }}
className="relative z-10 w-full h-full"
>
<video
ref={mainVideoRef}
key={taskObject.videos.data[currentSketchIndex].urls[0]}
className="w-full h-full rounded-lg object-contain object-center relative z-10"
style={{
aspectRatio: aspectRatio
}}
src={taskObject.videos.data[currentSketchIndex].urls[0]}
poster={taskObject.videos.data[currentSketchIndex].snapshot_url || getFirstFrame(taskObject.videos.data[currentSketchIndex].urls[0])}
preload="none"
autoPlay={isVideoPlaying}
loop={true}
playsInline
onEnded={() => {
if (isVideoPlaying) {
// 自动切换到下一个视频的逻辑在父组件处理
}
}}
/>
{/* 视频编辑覆盖层 */}
{enableVideoEdit && isVideoEditMode && (
<VideoEditOverlay
projectId={projectId || ''}
userId={JSON.parse(localStorage.getItem("currentUser") || '{}').id || 0}
currentVideo={{
id: taskObject.videos.data[currentSketchIndex].video_id,
url: taskObject.videos.data[currentSketchIndex].urls[0],
duration: 30 // 默认时长,可以从视频元素获取
}}
videoRef={mainVideoRef}
enabled={isVideoEditMode}
onDescriptionSubmit={onVideoEditDescriptionSubmit}
className="rounded-lg"
/>
)}
</motion.div>
</>
)}
{/* 操作按钮组 */}
{
taskObject.videos.data[currentSketchIndex].video_status !== 0 && (
<div className="absolute top-2 right-2 z-[21] flex items-center gap-2 transition-right duration-100" style={{
right: aspectRatio === '16:9' ? toosBtnRight : ''
}}>
{taskObject.videos.data[currentSketchIndex].video_status === 1 ? (
<>
{/* 视频编辑模式切换按钮 - 通过服务器配置控制显示 */}
{enableVideoEdit && showVideoModification && (
<Tooltip placement="top" title={isVideoEditMode ? "Exit edit mode" : "Enter edit mode"}>
<GlassIconButton
icon={PenTool}
size='sm'
onClick={() => {
console.log('🖊️ 钢笔图标被点击,切换编辑模式:', !isVideoEditMode);
setIsVideoEditMode(!isVideoEditMode);
}}
className={isVideoEditMode ? 'bg-blue-500/20 border-blue-500/50' : ''}
/>
</Tooltip>
)}
<Tooltip placement="top" title="Download">
<GlassIconButton
icon={Download}
size='sm'
onClick={() => {
const currentVideo = taskObject.videos.data[currentSketchIndex];
const totalVideos = taskObject.videos.data.filter((video: any) => video.urls && video.urls.length > 0).length;
const isCurrentVideoFailed = currentVideo.video_status === 2;
showDownloadOptionsModal({
currentVideoIndex: currentSketchIndex,
totalVideos: taskObject.final.url ? totalVideos + 1 : totalVideos,
isCurrentVideoFailed: isCurrentVideoFailed,
isFinalStage: false,
projectId: episodeId,
videoId: currentVideo?.video_id,
onDownloadCurrent: async (withWatermark: boolean) => {
if (!currentVideo?.video_id) return;
setIsLoadingDownloadBtn(true);
try {
const json: any = await post('/movie/download_video', {
project_id: episodeId,
video_id: currentVideo.video_id,
watermark: withWatermark
});
const url = json?.data?.download_url as string | undefined;
if (url) await downloadVideo(url);
} finally {
setIsLoadingDownloadBtn(false);
}
},
onDownloadAll: () => {}
});
}}
/>
</Tooltip>
</>
) : (
<>
<Tooltip placement="top" title="Regenerate video">
<GlassIconButton icon={RotateCcw} size='sm' onClick={() => {
const video = taskObject.videos.data[currentSketchIndex];
if (onRetryVideo && video?.video_id) {
onRetryVideo(video.video_id);
}
}} />
</Tooltip>
</>
)}
{/* 添加到chat去编辑 按钮 */}
<Tooltip placement="top" title="Edit video with chat">
<GlassIconButton icon={MessageCircleMore} size='sm' onClick={() => {
const currentVideo = taskObject.videos.data[currentSketchIndex];
if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0 && setVideoPreview) {
setVideoPreview(currentVideo.urls[0], currentVideo.video_id);
if (onOpenChat) onOpenChat();
}
}} />
</Tooltip>
{/* 跳转剪辑按钮 */}
{showGotoCutButton && (
<Tooltip placement="top" title='Go to AI-powered editing platform'>
<GlassIconButton icon={Scissors} size='sm' onClick={onGotoCut} />
</Tooltip>
)}
</div>
)
}
{/* 操作按钮组 */}
{/* <AnimatePresence>
<motion.div
className="absolute top-4 right-4 gap-2 z-[21] hidden group-hover:flex"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<GlassIconButton
icon={Edit3}
onClick={() => handleEditClick('3')}
/>
</motion.div>
</AnimatePresence> */}
{/* 底部通栏控制区域(仅生成成功时显示) */}
{ taskObject.videos.data[currentSketchIndex].video_status === 1 && (
<AnimatePresence>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }}>
{renderBottomControls(false, onToggleVideoPlay, isVideoPlaying)}
</motion.div>
</AnimatePresence>
)}
</div>
);
};
// 渲染分镜草图
const renderSketchContent = (currentSketch: any) => {
if (!currentSketch) return null;
return (
<div
className="relative w-full h-full rounded-lg group overflow-hidden"
key={`render-sketch-${currentSketch.url}`}
style={{
minWidth: placeholderWidth
}}
>
{/* 状态 */}
{(currentSketch.status === 0 || currentSketch.status === 2) && (
<RenderLoading
loadingText={currentSketch.status === 0 ? (currentSketch.type === 'role' ? 'Generating role...' : 'Generating scene...') : 'Generate failed'}
isFailed={currentSketch.status === 2}
/>
)}
{/* {currentSketch.status === 2 && (
<div className="absolute inset-0 bg-[#fcb0ba1a] flex items-center justify-center">
<img src={error_image.src} alt="error" className="w-12 h-12" />
</div>
)} */}
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
{currentSketch.status === 1 && (
<AnimatePresence mode="wait">
<motion.img
key={currentSketch.url}
src={currentSketch.url}
className="w-full h-full rounded-lg object-contain"
// 用 circle clip-path 实现“扩散”
initial={{ clipPath: "circle(0% at 50% 50%)" }}
animate={{ clipPath: "circle(150% at 50% 50%)" }}
exit={{ opacity: 0 }}
transition={{ duration: 1, ease: "easeInOut" }}
/>
</AnimatePresence>
)}
<div className="absolute top-0 left-0 right-0 p-2">
{/* 角色类型 */}
{currentSketch.type === 'role' && (
<div className="inline-flex items-center px-2 py-1 rounded-full bg-purple-500/20 backdrop-blur-sm">
<span className="text-xs text-purple-400">Role: {currentSketch.name}</span>
</div>
)}
</div>
{/* 操作按钮组 */}
{/* <AnimatePresence>
<motion.div
className="absolute top-4 right-4 gap-2 hidden group-hover:flex"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<GlassIconButton
icon={Edit3}
onClick={() => handleEditClick('1')}
/>
</motion.div>
</AnimatePresence> */}
{/* 底部播放按钮 */}
<AnimatePresence>
<motion.div
className="absolute bottom-4 left-4"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.2 }}
>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className="relative"
>
</motion.div>
</motion.div>
</AnimatePresence>
</div>
);
};
// 渲染剧本
const renderScriptContent = () => {
return (
<div className="relative h-full rounded-lg overflow-hidden p-2"
style={{
width: 'calc(100vw - 8rem)'
}}>
{scriptData ? (
<ScriptRenderer
data={scriptData}
setIsPauseWorkFlow={setIsPauseWorkFlow}
setAnyAttribute={setAnyAttribute}
isPauseWorkFlow={isPauseWorkFlow}
applyScript={applyScript}
mode={mode}
/>
) : (
<ScriptLoading isCompleted={!!scriptData} />
)}
</div>
);
};
// 计算生效阶段selectedView 优先于 taskObject.currentStage
const effectiveStage = (selectedView === 'final' && taskObject.final?.url)
? 'final_video'
: (!['script', 'character', 'scene'].includes(taskObject.currentStage) ? 'video' : taskObject.currentStage);
// 根据当前步骤渲染对应内容
if (effectiveStage === 'final_video') {
return renderFinalVideo();
}
if (effectiveStage === 'video') {
return renderVideoContent(onGotoCut);
}
if (effectiveStage === 'script') {
return renderScriptContent();
}
if (effectiveStage === 'scene' || effectiveStage === 'character') {
const allSketches = [...(taskObject.roles?.data || []), ...(taskObject.scenes?.data || [])];
const currentSketch = allSketches[currentSketchIndex];
return renderSketchContent(currentSketch);
}
return null;
});