2025-10-06 17:49:37 +08:00

403 lines
14 KiB
TypeScript

'use client';
import React, { useState, useEffect, useMemo, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ScriptModal } from '@/components/ui/script-modal';
import {
CircleAlert,
Heart,
Camera,
Film,
Scissors,
Play,
Pause
} from 'lucide-react';
import { TaskObject } from '@/api/DTO/movieEdit';
import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { Tooltip } from 'antd';
interface TaskInfoProps {
taskObject: TaskObject;
currentLoadingText: string;
roles: any[];
isPauseWorkFlow: boolean;
showGotoCutButton: boolean;
onGotoCut?: () => void;
setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void;
}
const stageIconMap = {
0: {
icon: Heart,
color: '#6bf5f9'
},
1: {
icon: Camera,
color: '#88bafb'
},
2: {
icon: Film,
color: '#a285fd'
},
3: {
icon: Scissors,
color: '#c73dff'
}
}
const TAG_COLORS = ['#924eadcc', '#4c90a0', '#3b4a5a', '#957558'];
// const TAG_COLORS = ['#6bf5f9', '#92a6fc', '#ac71fd', '#c73dfe'];
// 阶段图标组件
const StageIcons = ({ currentStage, isExpanded, isPauseWorkFlow, setIsPauseWorkFlow }: { currentStage: number, isExpanded: boolean, isPauseWorkFlow: boolean, setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void }) => {
// 根据当前阶段重新排序图标
const orderedStages = useMemo(() => {
const stages = Object.entries(stageIconMap).map(([stage, data]) => ({
stage: parseInt(stage),
...data
}));
return stages;
}, [currentStage]);
return (
<Tooltip title={isPauseWorkFlow ? "Click to Play" : "Click to Pause"} placement="bottom">
<motion.div
className="relative flex items-center cursor-pointer"
onClick={() => setIsPauseWorkFlow(!isPauseWorkFlow)}
>
<AnimatePresence mode="popLayout">
{orderedStages.map((stage, index) => {
const isCurrentStage = stage.stage === currentStage;
const Icon = stage.icon;
// 只显示当前阶段或展开状态
if (!isExpanded && !isCurrentStage) return null;
return (
<motion.div
key={stage.stage}
className="relative"
initial={isExpanded ? {
opacity: 0,
x: -20,
scale: 0.5
} : {}}
animate={{
opacity: 1,
x: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 25,
delay: index * 0.1
}
}}
exit={{
opacity: 0,
x: 20,
scale: 0.5,
transition: { duration: 0.2 }
}}
style={{
marginLeft: index > 0 ? '8px' : '0px',
zIndex: isCurrentStage ? 2 : 1
}}
>
<motion.div
className={`relative rounded-full p-1 ${isCurrentStage ? 'bg-opacity-20 cursor-pointer' : 'bg-opacity-10'}`}
animate={(isCurrentStage && !isPauseWorkFlow) ? {
rotate: [0, 360],
scale: [1, 1.2, 1],
transition: {
rotate: { duration: 3, repeat: Infinity, ease: "linear" },
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
}
} : {}}
>
<Icon
className="w-5 h-5"
style={{ color: stage.color }}
/>
</motion.div>
</motion.div>
);
})}
</AnimatePresence>
</motion.div>
</Tooltip>
);
};
export function TaskInfo({
taskObject,
currentLoadingText,
roles,
isPauseWorkFlow,
showGotoCutButton,
onGotoCut,
setIsPauseWorkFlow
}: TaskInfoProps) {
const [isScriptModalOpen, setIsScriptModalOpen] = useState(false);
const [currentStage, setCurrentStage] = useState(0);
const [isStageIconsExpanded, setIsStageIconsExpanded] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const stageColor = useMemo(() => {
return stageIconMap[currentStage as keyof typeof stageIconMap].color;
}, [currentStage]);
// 监听 currentLoadingText
useEffect(() => {
// 清理之前的定时器
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
// 统一更新currentStage
if (currentLoadingText.includes('initializing...') || currentLoadingText.includes('script') || currentLoadingText.includes('character')) {
setCurrentStage(0);
} else if (currentLoadingText.includes('sketch') && !currentLoadingText.includes('shot sketch')) {
setCurrentStage(1);
} else if (!currentLoadingText.includes('Post-production') && (currentLoadingText.includes('shot sketch') || currentLoadingText.includes('video'))) {
setCurrentStage(2);
} else if (currentLoadingText.includes('Post-production')) {
setCurrentStage(3);
}
if (currentLoadingText.includes('Task completed')) {
console.log('Closing modal at completion');
setIsScriptModalOpen(false);
}
if (currentLoadingText.includes('Post-production') || currentLoadingText.includes('status')) {
console.log('isScriptModalOpen-Post-production', currentLoadingText, isScriptModalOpen);
if (isScriptModalOpen) {
setIsScriptModalOpen(false);
} else {
setIsScriptModalOpen(true);
timerRef.current = setTimeout(() => {
setIsScriptModalOpen(false);
}, 8000);
}
}
if (currentLoadingText.includes('script')) {
console.log('isScriptModalOpen-script', currentLoadingText, isScriptModalOpen);
setIsScriptModalOpen(true);
}
if (currentLoadingText.includes('initializing...')) {
console.log('isScriptModalOpen-initializing', currentLoadingText, isScriptModalOpen);
setIsScriptModalOpen(true);
}
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}
}, [currentLoadingText]);
// 使用 useMemo 缓存标签颜色映射
const tagColors = useMemo(() => {
if (!taskObject?.tags) return {};
return taskObject.tags.reduce((acc: Record<string, string>, tag: string) => {
acc[tag] = TAG_COLORS[Math.floor(Math.random() * TAG_COLORS.length)];
return acc;
}, {});
}, [taskObject?.tags]); // 只在 tags 改变时重新计算
return (
<>
<div className="title-JtMejk flex items-center justify-center gap-2">
{taskObject?.title ? (
<>
<span>
{taskObject?.title || currentLoadingText}
</span>
</>
) : currentLoadingText}
</div>
<ScriptModal
isOpen={isScriptModalOpen}
onClose={() => {
console.log('Modal manually closed');
setIsScriptModalOpen(false);
}}
currentStage={currentStage}
roles={roles}
currentLoadingText={currentLoadingText}
/>
{
currentLoadingText !== 'Task completed' && (
currentLoadingText.includes('failed') ? (
<motion.div className='flex items-center gap-2 justify-center'>
<CircleAlert className="w-4 h-4 text-red-500/80" />
<span className="normalS400 subtitle-had8uE text-transparent bg-clip-text">{currentLoadingText}</span>
</motion.div>
) : (
<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"
style={{ backgroundColor: stageColor }}
animate={!isPauseWorkFlow ? {
scale: [1, 1.5, 1],
opacity: [1, 0.5, 1],
transition: {
duration: 1,
repeat: Infinity,
repeatDelay: 0.2
}
} : {}}
/> */}
{/* 阶段图标 */}
<motion.div
className="flex items-center gap-2"
key={currentLoadingText}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0.5, x: 10 }}
transition={{ duration: 0.3 }}
onMouseEnter={() => setIsStageIconsExpanded(true)}
onMouseLeave={() => setIsStageIconsExpanded(false)}
>
<StageIcons currentStage={currentStage} isExpanded={isStageIconsExpanded} isPauseWorkFlow={isPauseWorkFlow} setIsPauseWorkFlow={setIsPauseWorkFlow}/>
<motion.div
className="relative"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.3 }}
>
{/* 背景发光效果 */}
<motion.div
className="absolute inset-0 text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-cyan-400 to-purple-400 blur-sm"
animate={!isPauseWorkFlow ? {
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
transition: {
duration: 2,
repeat: Infinity,
ease: "linear"
}
}: {}}
style={{
backgroundSize: "200% 200%",
}}
>
<span className="normalS400 subtitle-had8uE">{isPauseWorkFlow ? 'workflow paused' : currentLoadingText}</span>
</motion.div>
{/* 主文字 - 颜色填充动画 */}
<motion.div
className="relative z-10 cursor-pointer"
animate={!isPauseWorkFlow ? {
scale: [1, 1.02, 1],
transition: {
duration: 1.5,
repeat: Infinity,
ease: "easeInOut"
}
}: {}}
onClick={() => setIsScriptModalOpen(true)}
>
<motion.span
className="normalS400 subtitle-had8uE text-transparent bg-clip-text bg-gradient-to-r from-blue-600 via-cyan-500 to-purple-600"
animate={!isPauseWorkFlow ? {
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
transition: {
duration: 3,
repeat: Infinity,
ease: "linear"
}
}: {}}
style={{
backgroundSize: "300% 300%",
}}
>
{currentLoadingText}
</motion.span>
</motion.div>
{/* 动态光点效果 */}
{/* <motion.div
className="absolute left-0 top-1/2 transform -translate-y-1/2 w-2 h-2 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full blur-sm"
animate={!isPauseWorkFlow ? {
x: [0, 200, 0],
opacity: [0, 1, 0],
scale: [0.5, 1, 0.5],
transition: {
duration: 2.5,
repeat: Infinity,
ease: "easeInOut",
}
}: {}}
/> */}
{/* 文字底部装饰线 */}
<motion.div
className="absolute bottom-0 left-0 h-0.5"
style={{
background: `linear-gradient(to right, ${stageColor}, rgb(34 211 238), rgb(168 85 247))`,
}}
animate={!isPauseWorkFlow ? {
width: ["0%", "100%", "0%"],
transition: {
width: { duration: 2, repeat: Infinity, ease: "easeInOut" }
}
} : {}}
/>
</motion.div>
</motion.div>
{/* <motion.div
className="w-1.5 h-1.5 rounded-full"
style={{ backgroundColor: stageColor }}
animate={!isPauseWorkFlow ? {
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"
style={{ backgroundColor: stageColor }}
animate={!isPauseWorkFlow ? {
scale: [1, 1.5, 1],
opacity: [1, 0.5, 1],
transition: {
duration: 1,
repeat: Infinity,
repeatDelay: 0.2,
delay: 0.3
}
} : {}}
/> */}
{/* // 跳转剪辑按钮
{showGotoCutButton && (
<Tooltip placement="top" title='AI-powered editing platform'>
<GlassIconButton icon={Scissors} size='sm' onClick={onGotoCut} />
</Tooltip>
)} */}
</motion.div>
)
)
}
</>
);
}