'use client'; import * as React from 'react'; import { Handle, Position, type NodeProps } from '@xyflow/react'; import { cn } from '../../lib/utils'; import { StatusBadge } from '../badge/StatusBadge'; import { getCategoryToken } from './theme/categories'; import { getCategoryIcon } from './node-icons'; import { NODE_STATUS_BADGE } from './exec-status'; import type { NodeStatus, WfNode } from './types'; /** * Data payload carried on an xyflow node rendered by {@link WorkflowNode}. * The serializer stores the canonical {@link WfNode} under `node`; the canvas * layers presentation extras (category, status overlay, port labels, rename * callback) on top. */ export interface WorkflowNodeData { /** Canonical graph node. */ node: WfNode; /** Category token key (resolved via theme/categories). */ category?: string; /** Runtime status overlay (defaults to "idle" in the editor). */ status?: NodeStatus; /** Number of inputs (default 1). */ inputCount?: number; /** Number of outputs (default 1). */ outputCount?: number; /** Labels for each output port (e.g. ["true", "false"]). */ outputLabels?: string[]; /** Labels for each input port. */ inputLabels?: string[]; /** Called when the node is renamed inline. */ onRename?: (nodeId: string, name: string) => void; [key: string]: unknown; } /** Build evenly-spaced top offsets (as %) for `count` handles. */ function handleOffsets(count: number): string[] { if (count <= 1) return ['50%']; return Array.from({ length: count }, (_, i) => `${((i + 1) / (count + 1)) * 100}%` ); } /** * Custom xyflow node for the workflow editor: category accent color + icon, * an editable title, labelled input/output ports (handles), a runtime status * badge, and disabled styling. Renaming is inline (double-click the title) * and surfaced via `data.onRename`. */ export function WorkflowNode({ id, data, selected }: NodeProps) { const d = data as WorkflowNodeData; const node = d.node; const token = getCategoryToken(d.category ?? deriveCategory(node.type)); const Icon = getCategoryIcon(token.icon); const status: NodeStatus = d.status ?? 'idle'; const disabled = Boolean(node.disabled); const inputCount = d.inputCount ?? 1; const outputCount = d.outputCount ?? 1; const inputOffsets = handleOffsets(inputCount); const outputOffsets = handleOffsets(outputCount); const [editing, setEditing] = React.useState(false); const [draft, setDraft] = React.useState(node.name); React.useEffect(() => { setDraft(node.name); }, [node.name]); const commit = () => { setEditing(false); const next = draft.trim(); if (next && next !== node.name) { d.onRename?.(id, next); } else { setDraft(node.name); } }; return (