diff --git a/frontend/src/components/tree-editor/FlowCanvas.tsx b/frontend/src/components/tree-editor/FlowCanvas.tsx index 5a467c11..17e159ed 100644 --- a/frontend/src/components/tree-editor/FlowCanvas.tsx +++ b/frontend/src/components/tree-editor/FlowCanvas.tsx @@ -31,9 +31,10 @@ interface FlowCanvasProps { selectedNodeId: string | null onNodeSelect: (nodeId: string | null) => void onSelectAnswerType: (nodeId: string, type: 'decision' | 'action' | 'solution') => void + onNodeContextMenu?: (e: React.MouseEvent, nodeId: string) => void } -function FlowCanvasInner({ selectedNodeId, onNodeSelect, onSelectAnswerType }: FlowCanvasProps) { +function FlowCanvasInner({ selectedNodeId, onNodeSelect, onSelectAnswerType, onNodeContextMenu }: FlowCanvasProps) { const { fitView, setCenter } = useReactFlow() const { nodes: layoutNodes, edges: layoutEdges, collapsedNodeIds, toggleCollapse, onNodesMeasured } = useTreeLayout() const [minimapVisible, setMinimapVisible] = useState(true) @@ -46,7 +47,7 @@ function FlowCanvasInner({ selectedNodeId, onNodeSelect, onSelectAnswerType }: F return { ...n, selected: n.id === selectedNodeId, - data: { ...data, onToggleCollapse: toggleCollapse }, + data: { ...data, onToggleCollapse: toggleCollapse, onContextMenu: onNodeContextMenu }, } } if (n.type === 'answerStub') { @@ -59,7 +60,7 @@ function FlowCanvasInner({ selectedNodeId, onNodeSelect, onSelectAnswerType }: F } return n }) - }, [layoutNodes, selectedNodeId, toggleCollapse, onSelectAnswerType]) + }, [layoutNodes, selectedNodeId, toggleCollapse, onSelectAnswerType, onNodeContextMenu]) const [nodes, setNodes, onNodesChange] = useNodesState(nodesWithCallbacks) const [edges, setEdges, onEdgesChange] = useEdgesState(layoutEdges) diff --git a/frontend/src/components/tree-editor/FlowCanvasNode.tsx b/frontend/src/components/tree-editor/FlowCanvasNode.tsx index 096591dd..38800d12 100644 --- a/frontend/src/components/tree-editor/FlowCanvasNode.tsx +++ b/frontend/src/components/tree-editor/FlowCanvasNode.tsx @@ -41,10 +41,14 @@ export interface FlowCanvasNodeData { hasValidationErrors: boolean isNew: boolean onToggleCollapse: (nodeId: string) => void + onContextMenu?: (e: React.MouseEvent, nodeId: string) => void + onAcceptSuggestion?: (nodeId: string) => void + onDismissSuggestion?: (nodeId: string) => void } function FlowCanvasNodeComponent({ data, selected }: NodeProps) { - const { node, hasChildren, isCollapsed, hasValidationErrors, isNew, onToggleCollapse } = data as unknown as FlowCanvasNodeData + const { node, hasChildren, isCollapsed, hasValidationErrors, isNew, onToggleCollapse, onContextMenu, onAcceptSuggestion, onDismissSuggestion } = data as unknown as FlowCanvasNodeData + const isGhost = !!(node as unknown as Record)._suggestion const nodeType = node.type as Exclude const config = NODE_TYPE_CONFIG[nodeType] ?? NODE_TYPE_CONFIG.decision const Icon = config.icon @@ -61,10 +65,12 @@ function FlowCanvasNodeComponent({ data, selected }: NodeProps) {
onContextMenu?.(e, node.id)} className={cn( 'w-[280px] rounded-xl border border-border bg-card shadow-sm cursor-pointer transition-all', config.borderClass, - selected && 'ring-1 ring-primary shadow-md' + selected && 'ring-1 ring-primary shadow-md', + isGhost && 'border-dashed !border-primary/40 opacity-60' )} > {/* Header */} @@ -142,6 +148,30 @@ function FlowCanvasNodeComponent({ data, selected }: NodeProps) {
)} + + {/* Ghost node accept/dismiss overlay */} + {isGhost && ( +
+ + +
+ )} {/* Source handle at bottom */} diff --git a/frontend/src/components/tree-editor/TreeEditorLayout.tsx b/frontend/src/components/tree-editor/TreeEditorLayout.tsx index 94221c9d..be26846f 100644 --- a/frontend/src/components/tree-editor/TreeEditorLayout.tsx +++ b/frontend/src/components/tree-editor/TreeEditorLayout.tsx @@ -19,6 +19,7 @@ interface TreeEditorLayoutProps { editingNodeId: string | null onNodeSelect: (nodeId: string | null) => void onSelectAnswerType: (nodeId: string, type: 'decision' | 'action' | 'solution') => void + onNodeContextMenu?: (e: React.MouseEvent, nodeId: string) => void } export function TreeEditorLayout({ @@ -28,6 +29,7 @@ export function TreeEditorLayout({ editingNodeId, onNodeSelect, onSelectAnswerType, + onNodeContextMenu, }: TreeEditorLayoutProps) { const editorMode = useTreeEditorStore(s => s.editorMode) @@ -70,6 +72,7 @@ export function TreeEditorLayout({ selectedNodeId={editingNodeId} onNodeSelect={onNodeSelect} onSelectAnswerType={onSelectAnswerType} + onNodeContextMenu={onNodeContextMenu} /> diff --git a/frontend/src/pages/TreeEditorPage.tsx b/frontend/src/pages/TreeEditorPage.tsx index cc0a44fc..422413c9 100644 --- a/frontend/src/pages/TreeEditorPage.tsx +++ b/frontend/src/pages/TreeEditorPage.tsx @@ -1,7 +1,7 @@ -import { useEffect, useState, useCallback } from 'react' +import { useEffect, useState, useCallback, useRef } from 'react' import { useParams, useNavigate, useBlocker } from 'react-router-dom' import { useStore } from 'zustand' -import { Undo2, Redo2, Save, CheckCircle2, Monitor, FileText, Code2, LayoutList, BarChart3, Settings, Download } from 'lucide-react' +import { Undo2, Redo2, Save, CheckCircle2, Monitor, FileText, Code2, LayoutList, BarChart3, Settings, Download, Sparkles } from 'lucide-react' import { getMonacoEditor } from '@/components/tree-editor/code-mode' import { treesApi } from '@/api/trees' import { treeMarkdownApi } from '@/api/treeMarkdown' @@ -17,6 +17,10 @@ import { cn, safeGetItem } from '@/lib/utils' import { toast } from '@/lib/toast' import { FlowAnalyticsPanel } from '@/components/analytics/FlowAnalyticsPanel' import { ExportFlowModal } from '@/components/library/ExportFlowModal' +import { EditorAIPanel } from '@/components/editor-ai/EditorAIPanel' +import { ContextMenu } from '@/components/common/ContextMenu' +import { useEditorAI } from '@/hooks/useEditorAI' +import { findNodeInTree } from '@/store/treeEditorStore' /** Recursively check if any node in the tree has type 'answer' */ function hasAnswerNodes(node: TreeStructure): boolean { @@ -37,6 +41,7 @@ export function TreeEditorPage() { isSaving, validationErrors, editorMode, + treeStructure, initNewTree, loadTree, loadDraft, @@ -49,7 +54,9 @@ export function TreeEditorPage() { setSaving, selectNode, updateNode, + deleteNode, setEditorMode, + getAllNodeIds, } = useTreeEditorStore() // Access undo/redo from temporal store @@ -74,6 +81,22 @@ export function TreeEditorPage() { return () => window.removeEventListener('resize', check) }, []) + // AI Assist panel + const editorAI = useEditorAI({ + flowType: 'troubleshooting', + treeId: id, + }) + + const previousEditingNodeRef = useRef(null) + + const handleAIPanelClose = useCallback(() => { + editorAI.closePanel() + if (previousEditingNodeRef.current) { + setEditingNodeId(previousEditingNodeRef.current) + previousEditingNodeRef.current = null + } + }, [editorAI]) + // Calculate if there are blocking errors const hasBlockingErrors = validationErrors.some(e => e.severity === 'error') @@ -705,6 +728,31 @@ export function TreeEditorPage() { )} + {/* AI Assist toggle */} + +