studio/PipelineVisualizer.tsx

140 lines
6.6 KiB
TypeScript

import React from 'react';
import { GenerationStep } from '../types';
import { Database, Palette, FileText, Layers, Clapperboard, Check } from 'lucide-react';
interface PipelineVisualizerProps {
currentStep: GenerationStep;
onNavigate: (step: GenerationStep) => void;
isProcessing: boolean;
}
const PipelineVisualizer: React.FC<PipelineVisualizerProps> = ({ currentStep, onNavigate, isProcessing }) => {
const nodes = [
{ id: 'source', label: '來源選擇', icon: Database, steps: [GenerationStep.PROJECT_SELECTION], color: '#ec4899' },
{ id: 'style', label: '風格定義', icon: Palette, steps: [GenerationStep.STYLE_SELECTION], color: '#8b5cf6' },
{ id: 'blueprint', label: '結構藍圖', icon: FileText, steps: [GenerationStep.INPUT_PHASE, GenerationStep.STRUCTURE_PLANNING], color: '#6366f1' },
{ id: 'assets', label: '資產合成', icon: Layers, steps: [GenerationStep.ASSET_MANAGEMENT], color: '#06b6d4' },
{ id: 'prod', label: '分鏡製作', icon: Clapperboard, steps: [GenerationStep.PRODUCTION_DASHBOARD], color: '#10b981' },
];
const getNodeStatus = (nodeSteps: GenerationStep[]) => {
const stepOrder = Object.values(GenerationStep);
const firstNodeStepIndex = stepOrder.indexOf(nodeSteps[0]);
const currentStepIndex = stepOrder.indexOf(currentStep);
if (nodeSteps.includes(currentStep)) return 'active';
if (currentStepIndex > firstNodeStepIndex) return 'completed';
return 'pending';
};
const getTargetStep = (nodeSteps: GenerationStep[]) => {
return nodeSteps[0];
}
const nodeWidth = 40;
const gap = 120;
const startX = 50;
const y = 40; // Centered vertically in the available space of Header (excluding text)
return (
<div className="w-full h-full flex items-center justify-center select-none">
<div className="relative w-full max-w-3xl h-full">
<svg className="w-full h-full overflow-visible">
<defs>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
{/* Edges */}
{nodes.map((node, i) => {
if (i === nodes.length - 1) return null;
const status = getNodeStatus(node.steps);
const nextStatus = getNodeStatus(nodes[i+1].steps);
const isActiveFlow = status === 'active' && isProcessing;
const isCompletedFlow = nextStatus === 'active' || nextStatus === 'completed';
return (
<g key={`edge-${i}`}>
<line
x1={startX + (i * gap) + nodeWidth/2}
y1={y}
x2={startX + ((i + 1) * gap) - nodeWidth/2}
y2={y}
stroke={isCompletedFlow ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.05)"}
strokeWidth="2"
/>
{(isActiveFlow || isCompletedFlow) && (
<line
x1={startX + (i * gap) + nodeWidth/2}
y1={y}
x2={startX + ((i + 1) * gap) - nodeWidth/2}
y2={y}
stroke={node.color}
strokeWidth="2"
strokeDasharray="4 4"
className={isActiveFlow ? "animate-flow" : ""}
opacity={isActiveFlow ? 1 : 0.5}
/>
)}
</g>
);
})}
{/* Nodes */}
{nodes.map((node, i) => {
const status = getNodeStatus(node.steps);
const cx = startX + (i * gap);
const isClickable = status === 'completed' || status === 'active';
return (
<g
key={node.id}
onClick={() => isClickable && onNavigate(getTargetStep(node.steps))}
className={`transition-all duration-300 ${isClickable ? 'cursor-pointer hover:opacity-80' : 'opacity-40 cursor-not-allowed'}`}
>
{status === 'active' && (
<circle cx={cx} cy={y} r={nodeWidth / 1.5} fill={node.color} opacity="0.2" filter="url(#glow)" className="animate-pulse" />
)}
<circle
cx={cx}
cy={y}
r={nodeWidth / 2}
fill={status === 'active' ? node.color : (status === 'completed' ? '#1f2937' : '#000')}
stroke={status === 'active' ? '#fff' : (status === 'completed' ? node.color : '#374151')}
strokeWidth={status === 'active' ? 2 : 1}
/>
<foreignObject x={cx - 10} y={y - 10} width="20" height="20">
<div className="flex items-center justify-center w-full h-full text-white">
{status === 'completed' ? <Check className="w-4 h-4 text-emerald-400" /> : <node.icon className="w-4 h-4" />}
</div>
</foreignObject>
<text
x={cx}
y={y + 35}
textAnchor="middle"
fill={status === 'active' ? '#fff' : '#6b7280'}
fontSize="10"
fontWeight="bold"
letterSpacing="1"
className={status === 'active' ? 'text-glow' : ''}
>
{node.label}
</text>
</g>
)
})}
</svg>
</div>
</div>
);
};
export default PipelineVisualizer;