video-flow-b/components/pages/work-flow.tsx
2025-06-28 10:09:28 +08:00

985 lines
36 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, { useEffect, useState, useRef, useMemo, useCallback } from "react";
import { Play, ChevronUp, Loader2, Edit3, FileText, Pause } from "lucide-react";
import "./style/work-flow.css";
import LiquidGlass from '@/plugins/liquid-glass';
import { Skeleton } from "@/components/ui/skeleton";
import { AISuggestionBar } from "@/components/ai-suggestion-bar";
import { motion, AnimatePresence } from "framer-motion";
import { debounce } from "lodash";
import { GlassIconButton } from "@/components/ui/glass-icon-button";
import { EditModal } from "@/components/ui/edit-modal";
const MOCK_SKETCH_URLS = [
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
];
const MOCK_SKETCH_SCRIPT = [
'script-123',
'script-123',
'script-123',
'script-123',
];
const MOCK_VIDEO_URLS = [
'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750384661_d8e30b79-828e-48cd-9025-ab62a996717c_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750320040_4b47996e-7c70-490e-8433-80c7df990fdd_text_to_video_0.mp4',
];
const MOCK_SKETCH_COUNT = 8;
export default function WorkFlow() {
const [taskObject, setTaskObject] = useState<any>(null);
const [projectObject, setProjectObject] = useState<any>(null);
const [taskSketch, setTaskSketch] = useState<any[]>([]);
const [sketchCount, setSketchCount] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = useState(true);
const [isAIBarVisible, setIsAIBarVisible] = useState(true);
const [currentStep, setCurrentStep] = useState('0');
const [currentSketchIndex, setCurrentSketchIndex] = useState(0);
const [isGeneratingSketch, setIsGeneratingSketch] = useState(false);
const thumbnailsRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [startX, setStartX] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const [showControls, setShowControls] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [taskVideos, setTaskVideos] = useState<any[]>([]);
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
const mainVideoRef = useRef<HTMLVideoElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const playTimerRef = useRef<NodeJS.Timeout | null>(null);
const [isVideoPlaying, setIsVideoPlaying] = useState(true);
const videoPlayTimerRef = useRef<NodeJS.Timeout | null>(null);
const [currentLoadingText, setCurrentLoadingText] = useState('加载中...');
// 模拟 AI 建议
const mockSuggestions = [
"优化场景转场效果",
"调整画面构图",
"改进角色动作设计",
"增加环境氛围",
"调整镜头语言"
];
useEffect(() => {
const taskId = localStorage.getItem("taskId") || "taskId-123";
getTaskDetail(taskId).then(async (data) => {
setTaskObject(data);
setIsLoading(false);
setCurrentStep('1');
// 只在任务详情加载完成后获取分镜草图
await getTaskSketch(taskId);
await new Promise(resolve => setTimeout(resolve, 2000));
// 首先修改 taskObject 下的 taskStatus 为 '2'
setTaskObject((prev: any) => ({
...prev,
taskStatus: '2'
}));
setCurrentStep('2');
// 获取分镜草图后,开始绘制角色
await getTaskRole(taskId);
await new Promise(resolve => setTimeout(resolve, 2000));
// 首先修改 taskObject 下的 taskStatus 为 '3'
setTaskObject((prev: any) => ({
...prev,
taskStatus: '3'
}));
setCurrentStep('3');
// 获取绘制角色后,开始获取分镜视频
await getTaskVideo(taskId);
});
}, []);
// 监听当前选中索引变化,自动滚动到对应位置
useEffect(() => {
if (thumbnailsRef.current && taskSketch.length > 0) {
const container = thumbnailsRef.current;
const thumbnailWidth = container.offsetWidth / 4; // 每个缩略图宽度(包含间距)
const scrollPosition = currentSketchIndex * thumbnailWidth;
container.scrollTo({
left: scrollPosition,
behavior: 'smooth'
});
}
}, [currentSketchIndex, taskSketch.length]);
// 处理鼠标/触摸拖动事件
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
setIsDragging(true);
setStartX(e.pageX - thumbnailsRef.current!.offsetLeft);
setScrollLeft(thumbnailsRef.current!.scrollLeft);
};
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!isDragging) return;
e.preventDefault();
const x = e.pageX - thumbnailsRef.current!.offsetLeft;
const walk = (x - startX) * 2;
thumbnailsRef.current!.scrollLeft = scrollLeft - walk;
};
const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
setIsDragging(false);
if (!isDragging) return;
const container = thumbnailsRef.current!;
const thumbnailWidth = container.offsetWidth / 4;
const currentScroll = container.scrollLeft;
const nearestIndex = Math.round(currentScroll / thumbnailWidth);
// 只有在拖动距离较小时才触发选中
const x = e.pageX - container.offsetLeft;
const walk = Math.abs(x - startX);
if (walk < 10) {
return; // 如果拖动距离太小,保持原有的点击选中逻辑
}
setCurrentSketchIndex(Math.min(Math.max(0, nearestIndex), taskSketch.length - 1));
};
// 模拟接口请求 获取任务详情
const getTaskDetail = async (taskId: string) => {
// const response = await fetch(`/api/task/${taskId}`);
// const data = await response.json();
// mock data
const data = {
projectId: 'projectId-123',
projectName: "Project 1",
taskId: taskId,
taskName: "Task 1",
taskDescription: "Task 1 Description",
taskStatus: "1", // '1' 绘制分镜、'2' 绘制角色、'3' 生成分镜视频、'4' 视频后期制作、'5' 最终成品
taskProgress: 0,
mode: 'auto', // 托管模式、人工干预模式
resolution: '1080p', // 1080p、2160p
taskCreatedAt: new Date().toISOString(),
taskUpdatedAt: new Date().toISOString(),
};
return data;
}
// 模拟接口请求 每次获取一个分镜草图 轮询获取
const getTaskSketch = async (taskId: string) => {
// 避免重复调用
if (isGeneratingSketch || taskSketch.length > 0) return;
setIsGeneratingSketch(true);
setTaskSketch([]);
// 模拟分批获取分镜草图
for (let i = 0; i < MOCK_SKETCH_COUNT; i++) {
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
const newSketch = {
id: `sketch-${i}`,
url: MOCK_SKETCH_URLS[i % MOCK_SKETCH_URLS.length],
script: MOCK_SKETCH_SCRIPT[i % MOCK_SKETCH_SCRIPT.length],
status: 'done'
};
setTaskSketch(prev => {
// 避免重复添加相同id的sketch
if (prev.find(sketch => sketch.id === newSketch.id)) {
return prev;
}
return [...prev, newSketch];
});
setCurrentSketchIndex(i);
setSketchCount(i + 1);
}
setIsGeneratingSketch(false);
}
// 模拟接口请求 每次获取一个角色 轮询获取
const getTaskRole = async (taskId: string) => {
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
}
// 模拟接口请求 每次获取一个分镜视频 轮询获取
const getTaskVideo = async (taskId: string) => {
setIsGeneratingVideo(true);
setTaskVideos([]);
// 模拟分批获取分镜视频
for (let i = 0; i < MOCK_SKETCH_COUNT; i++) {
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
const newVideo = {
id: `video-${i}`,
url: MOCK_VIDEO_URLS[i % MOCK_VIDEO_URLS.length],
script: MOCK_SKETCH_SCRIPT[i % MOCK_SKETCH_SCRIPT.length],
status: 'done'
};
setTaskVideos(prev => {
// 避免重复添加相同id的video
if (prev.find(video => video.id === newVideo.id)) {
return prev;
}
return [...prev, newVideo];
});
setCurrentSketchIndex(i);
}
setIsGeneratingVideo(false);
};
const handleSuggestionClick = (suggestion: string) => {
console.log('Selected suggestion:', suggestion);
};
const handleSubmit = (text: string) => {
console.log('Submitted text:', text);
};
// 缓存渲染的缩略图列表
const renderedSketches = useMemo(() =>
taskSketch.map((sketch, index) => (
<motion.div
key={sketch.id}
className={`relative aspect-video rounded-lg overflow-hidden
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
onClick={() => !isDragging && setCurrentSketchIndex(index)}
initial={false}
animate={{
scale: currentSketchIndex === index ? 1.05 : 1,
rotateY: currentSketchIndex === index ? 5 : 0,
rotateX: currentSketchIndex === index ? -5 : 0,
translateZ: currentSketchIndex === index ? '20px' : '0px',
transition: {
type: "spring",
stiffness: 300,
damping: 20
}
}}
style={{
transformStyle: 'preserve-3d',
perspective: '1000px'
}}
>
<motion.img
className="w-full h-full object-cover"
src={sketch.url}
alt={`缩略图 ${index + 1}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90"> {index + 1}</span>
</div>
</motion.div>
)), [taskSketch, currentSketchIndex, isDragging]
);
// 缓存渲染的视频缩略图列表
const renderedVideos = useMemo(() =>
taskVideos.map((video, index) => (
<motion.div
key={video.id}
className={`relative aspect-video rounded-lg overflow-hidden
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
onClick={() => !isDragging && setCurrentSketchIndex(index)}
initial={false}
animate={{
scale: currentSketchIndex === index ? 1.05 : 1,
rotateY: currentSketchIndex === index ? 5 : 0,
rotateX: currentSketchIndex === index ? -5 : 0,
translateZ: currentSketchIndex === index ? '20px' : '0px',
transition: {
type: "spring",
stiffness: 300,
damping: 20
}
}}
style={{
transformStyle: 'preserve-3d',
perspective: '1000px'
}}
>
<video
className="w-full h-full object-cover"
src={video.url}
muted
playsInline
loop
poster={taskSketch[index]?.url}
/>
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90"> {index + 1}</span>
</div>
</motion.div>
)), [taskVideos, currentSketchIndex, isDragging, taskSketch]
);
// 处理播放/暂停
const togglePlay = useCallback(() => {
setIsPlaying(prev => !prev);
}, []);
// 自动播放逻辑
useEffect(() => {
if (isPlaying && taskSketch.length > 0) {
playTimerRef.current = setInterval(() => {
setCurrentSketchIndex(prev => {
const nextIndex = (prev + 1) % taskSketch.length;
return nextIndex;
});
}, 2000); // 每2秒切换一次
} else if (playTimerRef.current) {
clearInterval(playTimerRef.current);
}
return () => {
if (playTimerRef.current) {
clearInterval(playTimerRef.current);
}
};
}, [isPlaying, taskSketch.length]);
// 当切换到视频模式时,停止播放
useEffect(() => {
if (currentStep === '3') {
setIsPlaying(false);
}
}, [currentStep]);
// 处理视频播放/暂停
const toggleVideoPlay = useCallback(() => {
setIsVideoPlaying(prev => !prev);
}, []);
// 视频自动播放逻辑
useEffect(() => {
if (isVideoPlaying && taskVideos.length > 0) {
// 确保当前视频开始播放
if (mainVideoRef.current) {
mainVideoRef.current.play();
}
} else {
// 暂停当前视频
if (mainVideoRef.current) {
mainVideoRef.current.pause();
}
// 清除定时器
if (videoPlayTimerRef.current) {
clearInterval(videoPlayTimerRef.current);
}
}
return () => {
if (videoPlayTimerRef.current) {
clearInterval(videoPlayTimerRef.current);
}
};
}, [isVideoPlaying, taskVideos.length]);
// 当切换视频时重置视频播放
useEffect(() => {
if (mainVideoRef.current) {
mainVideoRef.current.currentTime = 0;
if (isVideoPlaying) {
mainVideoRef.current.play();
}
}
}, [currentSketchIndex, isVideoPlaying]);
// 当切换到分镜草图模式时,停止视频播放
useEffect(() => {
if (currentStep !== '3') {
setIsVideoPlaying(false);
}
}, [currentStep]);
// 更新加载文字
useEffect(() => {
if (isLoading) {
setCurrentLoadingText('正在加载任务信息...');
return;
}
if (currentStep === '1') {
if (isGeneratingSketch) {
setCurrentLoadingText(`正在生成分镜草图 ${sketchCount + 1}/${MOCK_SKETCH_COUNT}...`);
} else {
setCurrentLoadingText('分镜草图生成完成');
}
} else if (currentStep === '2') {
setCurrentLoadingText('正在绘制角色...');
} else if (currentStep === '3') {
if (isGeneratingVideo) {
setCurrentLoadingText(`正在生成分镜视频 ${taskVideos.length + 1}/${taskSketch.length}...`);
} else {
setCurrentLoadingText('分镜视频生成完成');
}
}
}, [isLoading, currentStep, isGeneratingSketch, sketchCount, isGeneratingVideo, taskVideos.length, taskSketch.length]);
const renderSketchContent = () => {
if (!taskObject) {
return (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-black/40 via-black/20 to-black/40 backdrop-blur-sm rounded-lg overflow-hidden">
<motion.div
className="flex flex-col items-center gap-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<motion.div
animate={{
scale: [1, 1.2, 1],
rotate: [0, 360],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
<Loader2 className="w-8 h-8 text-blue-500" />
</motion.div>
<motion.p
className="text-sm text-white/70"
animate={{
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
...
</motion.p>
</motion.div>
</div>
);
}
if (currentStep === '3') {
return (
<div
className="relative w-full h-full"
onMouseEnter={() => setShowControls(true)}
onMouseLeave={() => setShowControls(false)}
>
{taskVideos[currentSketchIndex] ? (
<motion.div className="relative w-full h-full">
<motion.video
ref={mainVideoRef}
key={taskVideos[currentSketchIndex].url}
className="w-full h-full rounded-lg object-cover object-center"
src={taskVideos[currentSketchIndex].url}
autoPlay={isVideoPlaying}
muted
loop={false}
playsInline
poster={taskSketch[currentSketchIndex]?.url}
initial={{ opacity: 0, scale: 0.95 }}
animate={{
opacity: 1,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 25
}
}}
onEnded={() => {
if (isVideoPlaying) {
// 当前视频播放完成后,自动切换到下一个
setCurrentSketchIndex(prev => (prev + 1) % taskVideos.length);
}
}}
/>
{/* 播放进度指示器 */}
<AnimatePresence>
{isVideoPlaying && (
<motion.div
className="absolute bottom-0 left-0 right-0 h-1 bg-blue-500/20"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
exit={{ scaleX: 0 }}
transition={{
duration: mainVideoRef.current?.duration || 6,
repeat: Infinity
}}
style={{ transformOrigin: "left" }}
/>
)}
</AnimatePresence>
</motion.div>
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-500/20 via-purple-500/10 to-pink-500/20 backdrop-blur-sm rounded-lg overflow-hidden">
<motion.div
className="flex flex-col items-center gap-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full opacity-20 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear"
}}
/>
<motion.div
animate={{
rotate: [0, 360],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
<Loader2 className="w-8 h-8 text-blue-500 relative z-10" />
</motion.div>
</div>
<motion.p
className="text-sm text-white/70"
animate={{
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
{taskVideos.length + 1}/{taskSketch.length}
</motion.p>
</motion.div>
</div>
)}
{/* 操作按钮组 */}
<AnimatePresence>
{showControls && (
<>
{/* 顶部按钮组 */}
<motion.div
className="absolute top-4 right-4 flex gap-2"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<GlassIconButton
icon={Edit3}
tooltip="编辑分镜"
onClick={() => setIsEditModalOpen(true)}
/>
{/* <GlassIconButton
icon={FileText}
tooltip="显示脚本"
onClick={() => console.log('显示脚本')}
/> */}
</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 }}
>
<GlassIconButton
icon={isVideoPlaying ? Pause : Play}
tooltip={isVideoPlaying ? "暂停播放" : "自动播放"}
onClick={toggleVideoPlay}
size="sm"
/>
</motion.div>
</motion.div>
</AnimatePresence>
</div>
);
}
return (
<div
className="relative w-full h-full"
onMouseEnter={() => setShowControls(true)}
onMouseLeave={() => setShowControls(false)}
>
{taskSketch[currentSketchIndex] ? (
<motion.img
key={currentSketchIndex}
src={taskSketch[currentSketchIndex].url}
alt={`分镜草图 ${currentSketchIndex + 1}`}
className="w-full h-full rounded-lg"
initial={{ opacity: 0, scale: 0.95 }}
animate={{
opacity: 1,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 25
}
}}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-500/20 via-purple-500/10 to-pink-500/20 backdrop-blur-sm rounded-lg overflow-hidden">
<motion.div
className="flex flex-col items-center gap-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full opacity-20 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear"
}}
/>
<motion.div
animate={{
rotate: [0, 360],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
<Loader2 className="w-8 h-8 text-blue-500 relative z-10" />
</motion.div>
</div>
<motion.p
className="text-sm text-white/70"
animate={{
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
{sketchCount + 1}/{MOCK_SKETCH_COUNT}
</motion.p>
</motion.div>
</div>
)}
{/* 操作按钮组 */}
<AnimatePresence>
{showControls && (
<>
{/* 顶部按钮组 */}
<motion.div
className="absolute top-4 right-4 flex gap-2"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<GlassIconButton
icon={Edit3}
tooltip="编辑分镜"
onClick={() => setIsEditModalOpen(true)}
/>
{/* <GlassIconButton
icon={FileText}
tooltip="显示脚本"
onClick={() => console.log('显示脚本')}
/> */}
</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 }}
>
<GlassIconButton
icon={isPlaying ? Pause : Play}
tooltip={isPlaying ? "暂停播放" : "自动播放"}
onClick={togglePlay}
size="sm"
/>
</motion.div>
</motion.div>
</AnimatePresence>
{/* 播放进度指示器 */}
<AnimatePresence>
{isPlaying && (
<motion.div
className="absolute bottom-0 left-0 right-0 h-1 bg-blue-500/20"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
exit={{ scaleX: 0 }}
transition={{ duration: 2, repeat: Infinity }}
style={{ transformOrigin: "left" }}
/>
)}
</AnimatePresence>
</div>
);
};
return (
<div className="w-full h-full overflow-hidden">
<div className="flex h-full flex-col p-6 justify-center items-center pt-0">
<div className="container-H2sRZG">
<div className="splashContainer-otuV_A">
<div className="content-vPGYx8">
<div className="info-UUGkPJ">
{isLoading ? (
<>
<Skeleton className="h-8 w-64 mb-2" />
<Skeleton className="h-4 w-96" />
</>
) : (
<>
<div className="title-JtMejk">{taskObject?.projectName}{taskObject?.taskName}</div>
{/* 实时反馈当前 currentLoadingText */}
<motion.div
className="flex items-center gap-2 justify-center"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<motion.div
className="w-1.5 h-1.5 rounded-full bg-blue-500"
animate={{
scale: [1, 1.5, 1],
opacity: [1, 0.5, 1]
}}
transition={{
duration: 1,
repeat: Infinity,
repeatDelay: 0.2
}}
/>
<motion.p
className="normalS400 subtitle-had8uE text-blue-500/80"
key={currentLoadingText}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.3 }}
>
{currentLoadingText}
</motion.p>
<motion.div
className="w-1.5 h-1.5 rounded-full bg-blue-500"
animate={{
scale: [1, 1.5, 1],
opacity: [1, 0.5, 1]
}}
transition={{
duration: 1,
repeat: Infinity,
repeatDelay: 0.2,
delay: 0.3
}}
/>
<motion.div
className="w-1.5 h-1.5 rounded-full bg-blue-500"
animate={{
scale: [1, 1.5, 1],
opacity: [1, 0.5, 1]
}}
transition={{
duration: 1,
repeat: Infinity,
repeatDelay: 0.2,
delay: 0.6
}}
/>
</motion.div>
</>
)}
</div>
</div>
<div className="media-Ocdu1O">
<div className="videoContainer-qteKNi" ref={containerRef}>
{isLoading ? (
<Skeleton className="w-full aspect-video rounded-lg" />
) : (
<>
<div className="heroVideo-FIzuK1" style={{ aspectRatio: "16 / 9" }}>
{renderSketchContent()}
</div>
</>
)}
</div>
<div className="imageGrid-ymZV9z hidden-scrollbar">
{isLoading ? (
<>
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
</>
) : (
<div
ref={thumbnailsRef}
className="w-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hidden-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={() => setIsDragging(false)}
>
{currentStep === '3' ? (
<>
{renderedVideos}
{isGeneratingVideo && taskVideos.length < taskSketch.length && (
<motion.div
className="relative aspect-video rounded-lg overflow-hidden bg-gradient-to-br from-blue-500/20 via-purple-500/10 to-pink-500/20 backdrop-blur-sm"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full opacity-20 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear"
}}
/>
<motion.div
animate={{
rotate: [0, 360],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
<Loader2 className="w-6 h-6 text-blue-500 relative z-10" />
</motion.div>
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90"> {taskVideos.length + 1}</span>
</div>
</motion.div>
)}
</>
) : (
<>
{renderedSketches}
{isGeneratingSketch && sketchCount < MOCK_SKETCH_COUNT && (
<motion.div
className="relative aspect-video rounded-lg overflow-hidden bg-gradient-to-br from-blue-500/20 via-purple-500/10 to-pink-500/20 backdrop-blur-sm"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full opacity-20 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear"
}}
/>
<motion.div
animate={{
rotate: [0, 360],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
>
<Loader2 className="w-6 h-6 text-blue-500 relative z-10" />
</motion.div>
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90"> {sketchCount + 1}</span>
</div>
</motion.div>
)}
</>
)}
</div>
)}
</div>
</div>
</div>
</div>
</div>
{/* AI 建议栏 */}
<AISuggestionBar
suggestions={mockSuggestions}
onSuggestionClick={handleSuggestionClick}
onSubmit={handleSubmit}
placeholder="请输入你的想法,或点击预设词条获取 AI 建议..."
/>
<EditModal
isOpen={isEditModalOpen}
onClose={() => setIsEditModalOpen(false)}
taskStatus={taskObject?.taskStatus || '1'}
taskSketch={taskSketch}
currentSketchIndex={currentSketchIndex}
onSketchSelect={setCurrentSketchIndex}
/>
</div>
)
}