"use client"; import { Bot, Clapperboard, FileOutput, Film, LayoutPanelTop, ScanSearch, Sparkles, Trash2, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { CanvasToolbarIcon } from "@/components/canvas/CanvasToolbarIcon"; import { useCanvasStore } from "@/store/canvas"; import { getCanvasToolbarItem, type CanvasToolbarConfig, } from "@/lib/canvas-toolbar-config"; import { cn } from "@/lib/utils"; import { getNodeContentScale, getScaledNodeContentStyle, } from "@/lib/canvas/node-content-scale"; import { getVideoWorkflowNodeDefinition, getVideoWorkflowQuickConnectOutputs, } from "@/lib/video-workflow/catalog"; import type { CanvasNode, WorkflowNodeData } from "@/types/canvas"; interface WorkflowNodeProps { node: CanvasNode; isSelected: boolean; canvasToolbarConfig?: CanvasToolbarConfig; } function toAlphaColor(color: string | undefined, alpha: number) { const normalized = String(color || "").trim(); if (!normalized.startsWith("#")) return undefined; const hex = normalized.slice(1); if (hex.length !== 3 && hex.length !== 6) return undefined; const full = hex.length === 3 ? hex.split("").map((char) => char + char).join("") : hex; const int = Number.parseInt(full, 16); if (Number.isNaN(int)) return undefined; const red = (int >> 16) & 255; const green = (int >> 8) & 255; const blue = int & 255; return `rgba(${red}, ${green}, ${blue}, ${alpha})`; } const GROUP_META = { input: { label: "输入", icon: LayoutPanelTop, }, analysis: { label: "分析", icon: ScanSearch, }, storyboard: { label: "分镜", icon: Film, }, generation: { label: "生成", icon: Sparkles, }, character: { label: "角色场景", icon: Clapperboard, }, tool: { label: "工具", icon: Bot, }, output: { label: "输出", icon: FileOutput, }, } as const; const GROUP_STYLE_MAP = { input: { surface: "#f4f7ff", border: "#dfe6ff", icon: "#e5ebff", }, analysis: { surface: "#eefbf6", border: "#d6f1e6", icon: "#def6eb", }, storyboard: { surface: "#fff7e1", border: "#f7e6b3", icon: "#ffefbf", }, generation: { surface: "#fff1f4", border: "#f5d5dd", icon: "#ffe2e9", }, character: { surface: "#f4efff", border: "#e4dbff", icon: "#ece5ff", }, tool: { surface: "#f5f7fb", border: "#e4e8f0", icon: "#edf1f7", }, output: { surface: "#eef6ff", border: "#d8e8ff", icon: "#deecff", }, } as const; const STATUS_LABELS = { pending: "待配置", generating: "处理中", completed: "已完成", failed: "异常", } as const; const STATUS_CLASS_MAP = { pending: "border-slate-200 bg-white text-foreground/70 dark:border-zinc-800 dark:bg-zinc-950", generating: "border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900/40 dark:bg-blue-950/40 dark:text-blue-200", completed: "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/40 dark:bg-emerald-950/40 dark:text-emerald-200", failed: "border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-900/40 dark:bg-rose-950/40 dark:text-rose-200", } as const; export function WorkflowNode({ node, isSelected, canvasToolbarConfig, }: WorkflowNodeProps) { const deleteNode = useCanvasStore((state) => state.deleteNode); const data = node.data as WorkflowNodeData; const workflowDeleteItem = getCanvasToolbarItem( canvasToolbarConfig, "workflowDelete" ); const definition = getVideoWorkflowNodeDefinition(data.templateType); const contentScale = getNodeContentScale( node.size, definition?.size || { width: 300, height: 180, } ); const scaledContentStyle = getScaledNodeContentStyle(contentScale); const metadata = (data.metadata || {}) as Record; const quickConnectOutputs = data.templateType && definition ? getVideoWorkflowQuickConnectOutputs(definition.type) : []; const resolvedGroup = (data.group || definition?.group || "tool") as keyof typeof GROUP_META; const groupMeta = GROUP_META[resolvedGroup] || GROUP_META.tool; const GroupIcon = groupMeta.icon; const groupStyle = GROUP_STYLE_MAP[resolvedGroup] || GROUP_STYLE_MAP.tool; const accentColor = data.accentColor || definition?.accentColor || "#e2e8f0"; const accentBarColor = toAlphaColor(accentColor, 0.92) || accentColor; const title = data.title || definition?.label || "工作流节点"; const statusLabel = STATUS_LABELS[data.status] || STATUS_LABELS.pending; const statusClassName = STATUS_CLASS_MAP[data.status] || STATUS_CLASS_MAP.pending; const previewUrl = String(metadata.previewMediaUrl || metadata.resultMediaUrl || "").trim(); const previewKind = String(metadata.previewMediaKind || metadata.resultMediaKind || "").trim() === "video" ? "video" : "image"; const compareItems = Array.isArray(metadata.compareItems) ? metadata.compareItems.slice(0, 4) : []; const storyboardShots = Array.isArray(metadata.storyboardShots) ? metadata.storyboardShots.slice(0, 4) : []; const analysisHighlights = Array.isArray(metadata.analysisHighlights) ? metadata.analysisHighlights.slice(0, 4) : []; const tableRows = Array.isArray(metadata.tableRows) ? metadata.tableRows.slice(0, 4) : []; const characters = Array.isArray(metadata.characters) ? metadata.characters.slice(0, 4) : []; const scenes = Array.isArray(metadata.scenes) ? metadata.scenes.slice(0, 4) : []; const characterAssets = Array.isArray(metadata.characterAssets) ? metadata.characterAssets.slice(0, 4) : []; const sceneAssets = Array.isArray(metadata.sceneAssets) ? metadata.sceneAssets.slice(0, 4) : []; const agentPlan = Array.isArray(metadata.agentPlan) ? metadata.agentPlan.slice(0, 4) : []; const deliverables = Array.isArray(metadata.deliverables) ? metadata.deliverables.slice(0, 4) : []; const linkedAgentProjectId = String(metadata.linkedAgentProjectId || "").trim(); const linkedAgentProjectTitle = String(metadata.linkedAgentProjectTitle || "").trim(); const importedAgentResultCount = Math.max(Number(metadata.importedAgentResultCount || 0), 0); const pendingAgentResultCount = Math.max(Number(metadata.pendingAgentResultCount || 0), 0); const batchShotCount = Math.max( Number(metadata.batchShotCount || 0), storyboardShots.length ); const batchResultCount = Math.max(Number(metadata.batchResultCount || 0), 0); const batchFailedCount = Math.max(Number(metadata.batchFailedCount || 0), 0); const comparePreviewItems = compareItems .map((item, index) => { const compareItem = (item || {}) as Record; const url = String(compareItem.url || "").trim(); if (!url) return null; return { id: `${data.templateType}-compare-${index}`, url, kind: String(compareItem.kind || "").trim() === "video" ? "video" : "image", label: String(compareItem.label || compareItem.title || `对比 ${index + 1}`).trim(), }; }) .filter(Boolean) as Array<{ id: string; url: string; kind: "image" | "video"; label: string }>; const compactMetrics: string[] = []; if (batchShotCount > 0) compactMetrics.push(`镜头 ${batchShotCount}`); if (batchResultCount > 0) compactMetrics.push(`结果 ${batchResultCount}`); if (analysisHighlights.length > 0) compactMetrics.push(`分析 ${analysisHighlights.length}`); if (tableRows.length > 0) compactMetrics.push(`表格 ${tableRows.length}`); if (characters.length > 0) compactMetrics.push(`角色 ${characters.length}`); if (scenes.length > 0) compactMetrics.push(`场景 ${scenes.length}`); if (characterAssets.length > 0) compactMetrics.push(`角色资产 ${characterAssets.length}`); if (sceneAssets.length > 0) compactMetrics.push(`场景资产 ${sceneAssets.length}`); if (linkedAgentProjectId) compactMetrics.push("Agent"); if (importedAgentResultCount > 0) compactMetrics.push(`已回流 ${importedAgentResultCount}`); if (pendingAgentResultCount > 0) compactMetrics.push(`待回流 ${pendingAgentResultCount}`); if (batchFailedCount > 0) compactMetrics.push(`失败 ${batchFailedCount}`); const primaryTextCandidates = [ String(data.summary || "").trim(), String(analysisHighlights[0] || "").trim(), String((storyboardShots[0] as Record)?.title || "").trim(), String((storyboardShots[0] as Record)?.description || "").trim(), String((characters[0] as Record)?.description || "").trim(), String((scenes[0] as Record)?.description || "").trim(), String(agentPlan[0] || "").trim(), String(deliverables[0] || "").trim(), quickConnectOutputs.length > 0 ? `可接 ${quickConnectOutputs.map((item) => item.label).join(" / ")}` : "", String(definition?.description || data.description || "").trim(), ].filter(Boolean); const primaryText = primaryTextCandidates[0] || ""; const compactDetailLines = [ storyboardShots[0] ? `分镜:${String((storyboardShots[0] as Record)?.title || "镜头草案").trim()}` : "", characters[0] ? `角色:${String((characters[0] as Record)?.name || (characters[0] as Record)?.title || "角色线索").trim()}` : "", scenes[0] ? `场景:${String((scenes[0] as Record)?.name || (scenes[0] as Record)?.title || "场景线索").trim()}` : "", tableRows[0] ? "已整理结构化表格" : "", linkedAgentProjectId ? `Agent:${linkedAgentProjectTitle || linkedAgentProjectId}` : "", deliverables[0] ? `交付:${String(deliverables[0]).trim()}` : "", ] .filter(Boolean) .slice(0, 2); return (
{groupMeta.label} {statusLabel}

{title}

{primaryText ? (

{primaryText}

) : null}
{isSelected ? ( ) : null}
{previewUrl ? (
{previewKind === "video" ? (
) : comparePreviewItems.length > 0 ? (
{comparePreviewItems.slice(0, 4).map((item) => (
{item.kind === "video" ? (
))}
) : null} {compactMetrics.length > 0 ? (
{compactMetrics.slice(0, 6).map((metric) => ( {metric} ))}
) : null} {compactDetailLines.length > 0 ? (
{compactDetailLines.map((line) => (
{line}
))}
) : null}
{quickConnectOutputs.length > 0 ? ( quickConnectOutputs.slice(0, 4).map((item) => ( {item.label} )) ) : ( 结束节点 )}
); }