fix: wire AI-generated flow structures into editor stores

The useEditorAI hook was ignoring result.working_tree from AI responses,
so generated steps/trees never appeared in the editor. Now:
- useEditorAI calls onFlowUpdate when working_tree is present in response
- ProceduralEditorPage handles steps + intake form updates via replaceSteps
- TreeEditorPage handles tree structure updates via replaceTreeStructure
- Both stores have new bulk-replace methods for AI integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-07 01:56:24 -05:00
parent a9251a0ee3
commit 1d1ca3f294
5 changed files with 54 additions and 3 deletions

View File

@@ -12,9 +12,11 @@ interface UseEditorAIOptions {
treeId?: string | null treeId?: string | null
/** Returns the live flow structure from the editor for AI context */ /** Returns the live flow structure from the editor for AI context */
getFlowContext?: () => Record<string, unknown> | null getFlowContext?: () => Record<string, unknown> | null
/** Called when the AI response contains a working_tree update */
onFlowUpdate?: (workingTree: Record<string, unknown>, metadata?: Record<string, unknown> | null) => void
} }
export function useEditorAI({ flowType, treeId, getFlowContext }: UseEditorAIOptions) { export function useEditorAI({ flowType, treeId, getFlowContext, onFlowUpdate }: UseEditorAIOptions) {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [focalNodeId, setFocalNodeId] = useState<string | null>(null) const [focalNodeId, setFocalNodeId] = useState<string | null>(null)
const [contextMenu, setContextMenu] = useState<{ const [contextMenu, setContextMenu] = useState<{
@@ -108,6 +110,11 @@ export function useEditorAI({ flowType, treeId, getFlowContext }: UseEditorAIOpt
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}, },
]) ])
// Apply AI-generated flow structure to the editor
if (result.working_tree && onFlowUpdate) {
onFlowUpdate(result.working_tree, result.tree_metadata || null)
}
} catch { } catch {
setMessages((prev) => [ setMessages((prev) => [
...prev, ...prev,
@@ -121,7 +128,7 @@ export function useEditorAI({ flowType, treeId, getFlowContext }: UseEditorAIOpt
setIsLoading(false) setIsLoading(false)
pendingActionRef.current = 'open_chat' pendingActionRef.current = 'open_chat'
} }
}, [input, isLoading, ensureSession, focalNodeId, getFlowContext]) }, [input, isLoading, ensureSession, focalNodeId, getFlowContext, onFlowUpdate])
const triggerAction = useCallback( const triggerAction = useCallback(
(nodeId: string, actionType: AIActionType, prompt: string) => { (nodeId: string, actionType: AIActionType, prompt: string) => {

View File

@@ -15,7 +15,7 @@ import { ContextMenu } from '@/components/common/ContextMenu'
import { useEditorAI } from '@/hooks/useEditorAI' import { useEditorAI } from '@/hooks/useEditorAI'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast' import { toast } from '@/lib/toast'
import type { TreeType, MaintenanceSchedule, TargetList } from '@/types' import type { TreeType, MaintenanceSchedule, TargetList, ProceduralStep, IntakeFormField } from '@/types'
type SectionKey = 'details' | 'intake' | 'schedule' type SectionKey = 'details' | 'intake' | 'schedule'
@@ -46,10 +46,20 @@ export function ProceduralEditorPage() {
setIsSaving, setIsSaving,
markSaved, markSaved,
getTreeForSave, getTreeForSave,
replaceSteps,
} = useProceduralEditorStore() } = useProceduralEditorStore()
const steps = useProceduralEditorStore(s => s.steps) const steps = useProceduralEditorStore(s => s.steps)
const handleFlowUpdate = useCallback((workingTree: Record<string, unknown>, metadata?: Record<string, unknown> | null) => {
const stepsData = workingTree.steps as ProceduralStep[] | undefined
if (stepsData && Array.isArray(stepsData)) {
// Intake form may be in working_tree or in metadata
const intakeData = (workingTree.intake_form || metadata?.intake_form) as IntakeFormField[] | undefined
replaceSteps(stepsData, intakeData)
}
}, [replaceSteps])
const editorAI = useEditorAI({ const editorAI = useEditorAI({
flowType: 'procedural', flowType: 'procedural',
treeId: id, treeId: id,
@@ -61,6 +71,7 @@ export function ProceduralEditorPage() {
intake_form: intakeForm, intake_form: intakeForm,
} }
}, [steps, intakeForm, name, description]), }, [steps, intakeForm, name, description]),
onFlowUpdate: handleFlowUpdate,
}) })
const isMaintenance = treeType === 'maintenance' const isMaintenance = treeType === 'maintenance'

View File

@@ -58,6 +58,7 @@ export function TreeEditorPage() {
deleteNode, deleteNode,
setEditorMode, setEditorMode,
getAllNodeIds, getAllNodeIds,
replaceTreeStructure,
} = useTreeEditorStore() } = useTreeEditorStore()
// Access undo/redo from temporal store // Access undo/redo from temporal store
@@ -83,6 +84,13 @@ export function TreeEditorPage() {
}, []) }, [])
// AI Assist panel // AI Assist panel
const handleFlowUpdate = useCallback((workingTree: Record<string, unknown>) => {
// For troubleshooting flows, working_tree is the tree structure directly
if (workingTree.type && workingTree.id) {
replaceTreeStructure(workingTree as unknown as TreeStructure)
}
}, [replaceTreeStructure])
const editorAI = useEditorAI({ const editorAI = useEditorAI({
flowType: 'troubleshooting', flowType: 'troubleshooting',
treeId: id, treeId: id,
@@ -94,6 +102,7 @@ export function TreeEditorPage() {
tree_structure: treeStructure as unknown as Record<string, unknown>, tree_structure: treeStructure as unknown as Record<string, unknown>,
} }
}, [treeStructure, name, description]), }, [treeStructure, name, description]),
onFlowUpdate: handleFlowUpdate,
}) })
const previousEditingNodeRef = useRef<string | null>(null) const previousEditingNodeRef = useRef<string | null>(null)

View File

@@ -112,6 +112,9 @@ interface ProceduralEditorState {
updateField: (index: number, updates: Partial<IntakeFormField>) => void updateField: (index: number, updates: Partial<IntakeFormField>) => void
moveField: (fromIndex: number, toIndex: number) => void moveField: (fromIndex: number, toIndex: number) => void
// Actions - AI Integration
replaceSteps: (steps: ProceduralStep[], intakeForm?: IntakeFormField[]) => void
// Actions - Save // Actions - Save
setIsSaving: (saving: boolean) => void setIsSaving: (saving: boolean) => void
markSaved: () => void markSaved: () => void
@@ -341,6 +344,15 @@ export const useProceduralEditorStore = create<ProceduralEditorState>()(
}) })
}, },
// AI Integration
replaceSteps: (steps, intakeForm) => {
set((state) => {
state.steps = steps
if (intakeForm) state.intakeForm = intakeForm
state.isDirty = true
})
},
// Save // Save
setIsSaving: (saving) => set((state) => { state.isSaving = saving }), setIsSaving: (saving) => set((state) => { state.isSaving = saving }),
markSaved: () => set((state) => { state.isDirty = false }), markSaved: () => set((state) => { state.isDirty = false }),

View File

@@ -283,6 +283,9 @@ interface TreeEditorState {
syncMarkdownToTree: () => void syncMarkdownToTree: () => void
syncTreeToMarkdown: () => void syncTreeToMarkdown: () => void
// Actions - AI Integration
replaceTreeStructure: (structure: TreeStructure) => void
// Actions - State // Actions - State
setLoading: (loading: boolean) => void setLoading: (loading: boolean) => void
setSaving: (saving: boolean) => void setSaving: (saving: boolean) => void
@@ -1015,6 +1018,15 @@ export const useTreeEditorStore = create<TreeEditorState>()(
} }
}, },
// AI Integration
replaceTreeStructure: (structure: TreeStructure) => {
set((state) => {
state.treeStructure = structure
state.selectedNodeId = structure.id
state.isDirty = true
})
},
setLoading: (loading: boolean) => { setLoading: (loading: boolean) => {
set((state) => { state.isLoading = loading }) set((state) => { state.isLoading = loading })
}, },