From 821939744adc978602548127b3c91ea7699e424a Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Feb 2026 21:02:00 -0500 Subject: [PATCH] feat: add FlowCanvasNode compact card for React Flow canvas Co-Authored-By: Claude Opus 4.6 --- .../components/tree-editor/FlowCanvasNode.tsx | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 frontend/src/components/tree-editor/FlowCanvasNode.tsx diff --git a/frontend/src/components/tree-editor/FlowCanvasNode.tsx b/frontend/src/components/tree-editor/FlowCanvasNode.tsx new file mode 100644 index 00000000..096591dd --- /dev/null +++ b/frontend/src/components/tree-editor/FlowCanvasNode.tsx @@ -0,0 +1,154 @@ +import { memo } from 'react' +import { Handle, Position, type NodeProps } from '@xyflow/react' +import { HelpCircle, Zap, CheckCircle, ChevronDown, ChevronRight, AlertCircle } from 'lucide-react' +import { cn } from '@/lib/utils' +import type { TreeStructure, NodeType } from '@/types' + +const NODE_TYPE_CONFIG: Record, { + icon: typeof HelpCircle + label: string + borderClass: string + badgeClass: string + minimapColor: string +}> = { + decision: { + icon: HelpCircle, + label: 'Decision', + borderClass: 'border-l-4 border-l-blue-500', + badgeClass: 'bg-blue-500/20 text-blue-400', + minimapColor: '#3b82f6', + }, + action: { + icon: Zap, + label: 'Action', + borderClass: 'border-l-4 border-l-yellow-500', + badgeClass: 'bg-yellow-500/20 text-yellow-400', + minimapColor: '#eab308', + }, + solution: { + icon: CheckCircle, + label: 'Solution', + borderClass: 'border-l-4 border-l-green-500', + badgeClass: 'bg-green-500/20 text-green-400', + minimapColor: '#22c55e', + }, +} + +export interface FlowCanvasNodeData { + node: TreeStructure + hasChildren: boolean + isCollapsed: boolean + hasValidationErrors: boolean + isNew: boolean + onToggleCollapse: (nodeId: string) => void +} + +function FlowCanvasNodeComponent({ data, selected }: NodeProps) { + const { node, hasChildren, isCollapsed, hasValidationErrors, isNew, onToggleCollapse } = data as unknown as FlowCanvasNodeData + const nodeType = node.type as Exclude + const config = NODE_TYPE_CONFIG[nodeType] ?? NODE_TYPE_CONFIG.decision + const Icon = config.icon + + const title = node.type === 'decision' + ? (node.question || 'Untitled Decision') + : (node.title || `Untitled ${config.label}`) + + const optionCount = node.options?.length ?? 0 + + return ( + <> + {/* Target handle at top */} + + +
+ {/* Header */} +
+ {/* Type badge */} + + + + + {/* Title */} + + {title} + + + {/* Badges */} + {isNew && ( + + New + + )} + {hasValidationErrors && ( + + )} +
+ + {/* Decision options preview */} + {node.type === 'decision' && optionCount > 0 && ( +
+
+ {optionCount} option{optionCount !== 1 ? 's' : ''} +
+
+ {node.options!.slice(0, 3).map((opt, i) => ( +
+ {String.fromCharCode(65 + i)}{' '} + {opt.label || '(empty)'} +
+ ))} + {optionCount > 3 && ( +
+{optionCount - 3} more
+ )} +
+
+ )} + + {/* Description preview for action/solution */} + {(node.type === 'action' || node.type === 'solution') && node.description && ( +
+
{node.description}
+
+ )} + + {/* Collapse chevron */} + {hasChildren && ( +
+ +
+ )} +
+ + {/* Source handle at bottom */} + + + ) +} + +export const FlowCanvasNode = memo(FlowCanvasNodeComponent) +export { NODE_TYPE_CONFIG }