import { useState, useCallback, useEffect } from 'react' import { HelpCircle, Zap, CheckCircle, Play, Check, X, Copy, Trash2, GripVertical, AlertCircle, AlertTriangle, ChevronDown, ChevronRight, ChevronsDownUp, ChevronsUpDown, } from 'lucide-react' import { useTreeEditorStore } from '@/store/treeEditorStore' import { NodeFormDecision } from './NodeFormDecision' import { NodeFormAction } from './NodeFormAction' import { NodeFormResolution } from './NodeFormResolution' import type { TreeStructure } from '@/types' import { cn } from '@/lib/utils' interface TreeCanvasNodeProps { node: TreeStructure depth: number fromOption?: string isExpanded: boolean isNew: boolean hasChildren?: boolean isSubtreeCollapsed?: boolean onToggleExpand: () => void onToggleSubtreeCollapse?: () => void onSave: (nodeId: string, updates: Partial) => void onCancelNew: (nodeId: string) => void onDelete: (nodeId: string) => void onDuplicate: (nodeId: string) => void onDragStart: (e: React.DragEvent, nodeId: string) => void onDragOver: (e: React.DragEvent) => void onDrop: (e: React.DragEvent) => void } /** Clone a node without its children (for local draft state) */ function cloneNodeWithoutChildren(node: TreeStructure): TreeStructure { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { children, ...rest } = node return structuredClone(rest) as TreeStructure } const NODE_TYPE_CONFIG = { decision: { icon: HelpCircle, label: 'Decision', borderClass: 'border-l-4 border-l-blue-500', badgeClass: 'bg-blue-500/20 text-blue-400', }, action: { icon: Zap, label: 'Action', borderClass: 'border-l-4 border-l-yellow-500', badgeClass: 'bg-yellow-500/20 text-yellow-400', }, solution: { icon: CheckCircle, label: 'Solution', borderClass: 'border-l-4 border-l-green-500', badgeClass: 'bg-green-500/20 text-green-400', }, } as const export function TreeCanvasNode({ node, fromOption, isExpanded, isNew, hasChildren = false, isSubtreeCollapsed = false, onToggleExpand, onToggleSubtreeCollapse, onSave, onCancelNew, onDelete, onDuplicate, onDragStart, onDragOver, onDrop, }: TreeCanvasNodeProps) { const { validationErrors, selectedNodeId, selectNode } = useTreeEditorStore() const isRoot = node.id === 'root' const isSelected = selectedNodeId === node.id const nodeErrors = validationErrors.filter( (e) => e.nodeId === node.id && e.severity === 'error' ) const nodeWarnings = validationErrors.filter( (e) => e.nodeId === node.id && e.severity === 'warning' ) const hasError = nodeErrors.length > 0 const hasWarning = nodeWarnings.length > 0 // Local draft state for inline editing const [draft, setDraft] = useState(() => cloneNodeWithoutChildren(node) ) // Reset draft if node ID changes (e.g. navigating between nodes) const [lastNodeId, setLastNodeId] = useState(node.id) if (node.id !== lastNodeId) { setDraft(cloneNodeWithoutChildren(node)) setLastNodeId(node.id) } // Re-sync draft from store whenever the card is opened, so stale next_node_id // values (written back after stub creation) don't cause duplicate stubs on re-save useEffect(() => { if (isExpanded) { setDraft(cloneNodeWithoutChildren(node)) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isExpanded]) const handleDraftUpdate = useCallback((updates: Partial) => { setDraft((prev) => ({ ...prev, ...updates })) }, []) const handleSave = (e: React.MouseEvent) => { e.stopPropagation() // Strip children from draft before passing to onSave // eslint-disable-next-line @typescript-eslint/no-unused-vars const { children, ...draftWithoutChildren } = draft onSave(node.id, draftWithoutChildren) } const handleCancel = (e: React.MouseEvent) => { e.stopPropagation() if (isNew) { onCancelNew(node.id) } else { // Discard draft changes and collapse setDraft(cloneNodeWithoutChildren(node)) onToggleExpand() } } const handleCardClick = () => { selectNode(node.id) onToggleExpand() } const config = node.type in NODE_TYPE_CONFIG ? NODE_TYPE_CONFIG[node.type as keyof typeof NODE_TYPE_CONFIG] : NODE_TYPE_CONFIG.decision // fallback for 'answer' (rendered by AnswerStubCard) const TypeIcon = config.icon const getTitle = () => { if (node.type === 'decision') return node.question || 'Untitled Question' return node.title || `Untitled ${node.type}` } const getOptionsSummary = () => { if (node.type !== 'decision' || !node.options?.length) return null const count = node.options.length return `${count} option${count !== 1 ? 's' : ''}` } return (
{/* Card Header */}
{/* Drag handle (hide for root) */} {!isRoot && ( { e.stopPropagation() onDragStart(e, node.id) }} > )} {/* Node type badge */} {isRoot ? ( START ) : ( {config.label} )} {/* From-option label */} {fromOption && ( {fromOption} )} {/* Title text (compact mode) */} {!isExpanded && ( {getTitle()} )} {/* Options count badge */} {!isExpanded && getOptionsSummary() && ( {getOptionsSummary()} )} {/* Validation badges (compact mode) */} {!isExpanded && hasError && ( e.message).join('\n')} > {nodeErrors.length} )} {!isExpanded && !hasError && hasWarning && ( e.message).join('\n')} > {nodeWarnings.length} )} {/* Unsaved badge */} {!isExpanded && isNew && ( Unsaved )} {/* Subtree collapse toggle — only in compact mode when node has children */} {!isExpanded && hasChildren && onToggleSubtreeCollapse && ( )} {/* Expand/collapse chevron */} {!isExpanded ? ( ) : ( )} {/* Editing action buttons (expanded state) */} {isExpanded && (
{/* New badge */} {isNew && ( Unsaved )} {/* Duplicate (hide for root) */} {!isRoot && ( )} {/* Delete (hide for root) */} {!isRoot && ( )} {/* Cancel */} {/* Save */}
)}
{/* Expanded editing area */} {isExpanded && (
{/* Validation errors */} {(hasError || hasWarning) && (
{nodeErrors.map((error, i) => (
{error.message}
))} {!hasError && nodeWarnings.map((warning, i) => (
{warning.message}
))}
)} {/* Type-specific form — uses draft, not live node */} {draft.type === 'decision' && ( )} {draft.type === 'action' && ( )} {draft.type === 'solution' && ( )}
)}
) } export default TreeCanvasNode