From e19c74060376ddfe3dd7fd48126ffa04ffd238c2 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Feb 2026 01:33:08 -0500 Subject: [PATCH] feat: auto-create answer stubs on decision save, render AnswerStubCard Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/tree-editor/TreeCanvas.tsx | 74 +++++++++++++++---- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/tree-editor/TreeCanvas.tsx b/frontend/src/components/tree-editor/TreeCanvas.tsx index f268db79..b8f7bedf 100644 --- a/frontend/src/components/tree-editor/TreeCanvas.tsx +++ b/frontend/src/components/tree-editor/TreeCanvas.tsx @@ -2,6 +2,7 @@ import { useState, useCallback, useRef, useEffect } from 'react' import { HelpCircle, Zap, CheckCircle, Plus, X } from 'lucide-react' import { useTreeEditorStore, findNodeInTree } from '@/store/treeEditorStore' import { TreeCanvasNode } from './TreeCanvasNode' +import { AnswerStubCard } from './AnswerStubCard' import type { TreeStructure, NodeType } from '@/types' import { cn } from '@/lib/utils' @@ -203,6 +204,28 @@ export function TreeCanvas() { (nodeId: string, updates: Partial) => { updateNode(nodeId, updates) + // For decision nodes: create answer stubs for any option without a next_node_id + if (updates.options) { + const options = updates.options + const stubsToCreate: Array<{ opt: typeof options[number]; stubId: string }> = [] + + options.forEach((opt) => { + if (!opt.next_node_id && opt.label.trim()) { + const stubId = addNode(nodeId, 'answer') + updateNode(stubId, { title: opt.label }) + stubsToCreate.push({ opt, stubId }) + } + }) + + if (stubsToCreate.length > 0) { + const updatedOptions = options.map((o) => { + const stub = stubsToCreate.find((s) => s.opt.id === o.id) + return stub ? { ...o, next_node_id: stub.stubId } : o + }) + updateNode(nodeId, { options: updatedOptions }) + } + } + // Resolve pending link for new nodes const link = pendingLinks.get(nodeId) if (link) { @@ -278,6 +301,16 @@ export function TreeCanvas() { [duplicateNode] ) + // ── Convert answer stub to a real node type ── + const handleSelectAnswerType = useCallback( + (nodeId: string, type: 'decision' | 'action' | 'solution') => { + updateNode(nodeId, { type }) + setExpandedNodeId(nodeId) + selectNode(nodeId) + }, + [updateNode, selectNode] + ) + // ── Add node flow ── const handleAddNodeSelect = useCallback( (type: NodeType, parentId: string, optionId?: string) => { @@ -464,22 +497,30 @@ export function TreeCanvas() { )} - {/* The node card itself */} - handleToggleExpand(node.id)} - onSave={handleSave} - onCancelNew={handleCancelNew} - onDelete={handleDelete} - onDuplicate={handleDuplicate} - onDragStart={handleDragStart} - onDragOver={(e) => handleDragOver(e, parentId, index)} - onDrop={(e) => handleDrop(e, parentId, index)} - /> + {/* The node card — answer stubs get their own component */} + {node.type === 'answer' ? ( + + ) : ( + handleToggleExpand(node.id)} + onSave={handleSave} + onCancelNew={handleCancelNew} + onDelete={handleDelete} + onDuplicate={handleDuplicate} + onDragStart={handleDragStart} + onDragOver={(e) => handleDragOver(e, parentId, index)} + onDrop={(e) => handleDrop(e, parentId, index)} + /> + )} {/* Unlinked option add buttons (decision nodes with unlinked options) */} {!isExpanded && unlinkedOptions.length > 0 && ( @@ -586,6 +627,7 @@ export function TreeCanvas() { handleCancelNew, handleDelete, handleDuplicate, + handleSelectAnswerType, handleDragStart, handleDragOver, handleDrop,