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:
@@ -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) => {
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -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 })
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user