"use client"; import { Bot, Clapperboard, FileOutput, Film, LayoutPanelTop, ScanSearch, Sparkles, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { getVideoWorkflowNodeDefinition, getVideoWorkflowQuickConnectOutputs, } from "@/lib/video-workflow/catalog"; import type { CanvasNode, WorkflowNodeData } from "@/types/canvas"; interface WorkflowNodeProps { node: CanvasNode; isSelected: boolean; } 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 STATUS_LABELS = { pending: "待配置", generating: "处理中", completed: "已完成", failed: "异常", } as const; export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) { const data = node.data as WorkflowNodeData; const definition = getVideoWorkflowNodeDefinition(data.templateType); 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 accentColor = data.accentColor || definition?.accentColor || "#e2e8f0"; const title = data.title || definition?.label || "工作流节点"; const description = data.description || definition?.description || "等待补充节点配置。"; const previewUrl = String(metadata.previewMediaUrl || metadata.resultMediaUrl || "").trim(); const previewKind = String(metadata.previewMediaKind || metadata.resultMediaKind || "").trim() === "video" ? "video" : "image"; const storyboardShots = Array.isArray(metadata.storyboardShots) ? metadata.storyboardShots.slice(0, 4) : []; const analysisHighlights = Array.isArray(metadata.analysisHighlights) ? metadata.analysisHighlights.slice(0, 4) : []; const compareItems = Array.isArray(metadata.compareItems) ? metadata.compareItems.slice(0, 4) : []; const tableColumns = Array.isArray(metadata.tableColumns) ? metadata.tableColumns.slice(0, 4) : []; const tableRows = Array.isArray(metadata.tableRows) ? metadata.tableRows.slice(0, 4) : []; const characters = Array.isArray(metadata.characters) ? metadata.characters.slice(0, 3) : []; const scenes = Array.isArray(metadata.scenes) ? metadata.scenes.slice(0, 3) : []; const characterAssets = Array.isArray(metadata.characterAssets) ? metadata.characterAssets.slice(0, 3) : []; const sceneAssets = Array.isArray(metadata.sceneAssets) ? metadata.sceneAssets.slice(0, 3) : []; 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 agentResultGroups = Array.isArray(metadata.agentResultGroups) ? metadata.agentResultGroups.slice(0, 4) : []; const batchShotCount = Math.max( Number(metadata.batchShotCount || 0), storyboardShots.length ); const batchResultCount = Number(metadata.batchResultCount || 0); const batchFailedCount = Number(metadata.batchFailedCount || 0); const batchMode = String(metadata.batchMode || "").trim() === "gen-video" ? "视频批量" : "图片批量"; const accentGlow = toAlphaColor(accentColor, 0.18) || "rgba(59, 130, 246, 0.12)"; const accentLine = toAlphaColor(accentColor, 0.3) || "rgba(59, 130, 246, 0.2)"; const sectionClassName = "rounded-2xl border border-border/60 bg-background/74 p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.3)]"; const mutedKickerClassName = "text-[11px] uppercase tracking-[0.18em] text-muted-foreground/80"; const itemClassName = "rounded-xl border border-border/50 bg-background/88 px-3 py-2"; const badgeClassName = "border-border/60 bg-background/82 text-foreground/80"; return (
{groupMeta.label}

{title}

{description}

{STATUS_LABELS[data.status] || STATUS_LABELS.pending}
节点标识
{data.templateType}

{data.summary || "把这个节点接在上游素材之后,用来承接处理或输出。"}

{batchShotCount > 0 || batchResultCount > 0 ? (
批量执行
{batchShotCount > 0 ? ( 镜头 {batchShotCount} ) : null} {batchResultCount > 0 ? ( 结果 {batchResultCount} ) : null} {batchFailedCount > 0 ? ( 失败 {batchFailedCount} ) : null} {batchResultCount > 0 ? ( {batchMode} ) : null}
) : null} {previewUrl ? (
{previewKind === "video" ? (
) : null} {compareItems.length > 0 ? (
对比结果
{compareItems.map((item, index) => { const compareItem = (item || {}) as Record; const kind = String(compareItem.kind || "").trim() === "video" ? "video" : "image"; const url = String(compareItem.url || "").trim(); if (!url) { return null; } return (
{String(compareItem.label || `对比 ${index + 1}`)}
{String(compareItem.title || `素材 ${index + 1}`)}
{kind === "video" ? (
); })}
) : null} {storyboardShots.length > 0 ? (
镜头草案
{storyboardShots.map((item, index) => { const shot = (item || {}) as Record; return (
{String(shot.title || `镜头 ${index + 1}`)}
{String(shot.description || "")}
); })}
) : null} {tableColumns.length > 0 && tableRows.length > 0 ? (
结构化表格
{tableColumns.map((item, index) => { const column = (item || {}) as Record; return ( {String(column.label || column.key || `列 ${index + 1}`)} ); })}
{tableRows.map((item, index) => { const row = (item || {}) as Record; return (
{tableColumns.map((columnItem, columnIndex) => { const column = (columnItem || {}) as Record; const columnKey = String(column.key || `col-${columnIndex}`).trim(); const label = String(column.label || columnKey || `列 ${columnIndex + 1}`).trim(); const value = String(row[columnKey] || "").trim(); if (!value) { return null; } return (
0 ? "mt-2" : ""} >
{label}
{value}
); })}
); })}
) : null} {analysisHighlights.length > 0 ? (
分析摘要
{analysisHighlights.map((item, index) => (
{String(item || "")}
))}
) : null} {characters.length > 0 ? (
角色线索
{characters.map((item, index) => { const character = (item || {}) as Record; return (
{String(character.name || character.title || `角色 ${index + 1}`)}
{String(character.description || character.prompt || "")}
); })}
) : null} {scenes.length > 0 ? (
场景线索
{scenes.map((item, index) => { const scene = (item || {}) as Record; return (
{String(scene.name || scene.title || `场景 ${index + 1}`)}
{String(scene.description || scene.prompt || "")}
); })}
) : null} {characterAssets.length > 0 || sceneAssets.length > 0 ? (
资产沉淀
{[...characterAssets, ...sceneAssets].slice(0, 4).map((item, index) => { const asset = (item || {}) as Record; return (
{String(asset.name || asset.title || `资产 ${index + 1}`)}
{String(asset.description || asset.prompt || "")}
); })}
) : null} {agentPlan.length > 0 || deliverables.length > 0 ? (
代理计划
{agentPlan.map((item, index) => (
{String(item || "")}
))} {deliverables.map((item, index) => (
交付物: {String(item || "")}
))}
) : null} {linkedAgentProjectId ? (
Agent 工作台
已连接 {importedAgentResultCount > 0 ? ( 已回流 {importedAgentResultCount} ) : null} {pendingAgentResultCount > 0 ? ( 待回流 {pendingAgentResultCount} ) : null}
{linkedAgentProjectTitle || linkedAgentProjectId}
) : null} {agentResultGroups.length > 0 ? (
Agent 结果
{agentResultGroups.map((item, index) => { const group = (item || {}) as Record; const label = String(group.label || `结果组 ${index + 1}`).trim(); const count = Math.max(Number(group.count || 0), 0); const importedCount = Math.max(Number(group.importedCount || 0), 0); return (
{label}
{importedCount}/{count}
共 {count} 个结果,已回流 {importedCount} 个。
); })}
) : null}
下一步推荐
{quickConnectOutputs.length > 0 ? (
{quickConnectOutputs.slice(0, 4).map((item) => ( {item.label} ))}
) : (
当前节点通常作为流程终点或独立处理节点
)}
); }