diff --git a/frontend/src/pages/ProceduralEditorPage.tsx b/frontend/src/pages/ProceduralEditorPage.tsx index b1c6a73e..511dfc0b 100644 --- a/frontend/src/pages/ProceduralEditorPage.tsx +++ b/frontend/src/pages/ProceduralEditorPage.tsx @@ -13,10 +13,13 @@ import { TagInput } from '@/components/common/TagInput' import { Spinner } from '@/components/common/Spinner' import { EditorAIPanel } from '@/components/editor-ai/EditorAIPanel' import { ContextMenu } from '@/components/common/ContextMenu' +import { ValidationSummary } from '@/components/tree-editor/ValidationSummary' +import { AIFixReviewModal } from '@/components/tree-editor/AIFixReviewModal' import { useEditorAI } from '@/hooks/useEditorAI' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' -import type { TreeType, MaintenanceSchedule, TargetList, ProceduralStep, IntakeFormField } from '@/types' +import type { TreeType, MaintenanceSchedule, TargetList, ProceduralStep, IntakeFormField, AIFixProposal } from '@/types' +import type { ValidationError } from '@/store/treeEditorStore' type SectionKey = 'details' | 'intake' | 'schedule' @@ -51,6 +54,9 @@ export function ProceduralEditorPage() { } = useProceduralEditorStore() const steps = useProceduralEditorStore(s => s.steps) + const validationErrors = useProceduralEditorStore((s) => s.validationErrors) + const setExpandedStepId = useProceduralEditorStore((s) => s.setExpandedStepId) + const updateStep = useProceduralEditorStore((s) => s.updateStep) const handleFlowUpdate = useCallback((workingTree: Record, metadata?: Record | null) => { const stepsData = workingTree.steps as ProceduralStep[] | undefined @@ -75,6 +81,9 @@ export function ProceduralEditorPage() { onFlowUpdate: handleFlowUpdate, }) + const [isFixing, setIsFixing] = useState(false) + const [fixProposals, setFixProposals] = useState(null) + const isMaintenance = treeType === 'maintenance' const flowLabel = isMaintenance ? 'Maintenance Flow' : 'Procedure' @@ -110,6 +119,10 @@ export function ProceduralEditorPage() { return () => { reset() } }, [id]) + useEffect(() => { + useProceduralEditorStore.getState().validate() + }, [steps, intakeForm]) + const loadExistingTree = async (treeId: string) => { try { const tree = await treesApi.get(treeId) @@ -125,12 +138,77 @@ export function ProceduralEditorPage() { } } + const handleSelectStep = useCallback((stepId: string) => { + // Guard against setExpandedStepId's toggle behavior — calling it with the same ID collapses the step + const currentExpanded = useProceduralEditorStore.getState().expandedStepId + if (currentExpanded !== stepId) { + setExpandedStepId(stepId) + } + // Small delay to let expanded state render before scrolling + setTimeout(() => { + const el = document.querySelector(`[data-step-id="${stepId}"]`) + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }) + }, 50) + }, [setExpandedStepId]) + + const handleFixWithAI = async () => { + const fixableErrors = validationErrors + .filter((e) => e.severity === 'error' && e.stepId) + .map((e) => ({ node_id: e.stepId!, message: e.message })) + + if (fixableErrors.length === 0) return + + setIsFixing(true) + try { + const result = await treesApi.fixTree({ + tree_structure: { steps } as unknown as Record, + tree_name: name, + tree_type: treeType as 'procedural' | 'maintenance', + validation_errors: fixableErrors, + }) + if (result.fixes.length > 0) { + setFixProposals(result.fixes) + } else { + toast.info('AI could not generate fixes for these errors') + } + } catch { + toast.error('Failed to generate AI fixes. Please try again.') + } finally { + setIsFixing(false) + } + } + + const handleApplyFix = (fix: AIFixProposal) => { + updateStep(fix.target_node_id, fix.fixed_node as Partial) + useProceduralEditorStore.getState().validate() + } + + // AIFixReviewModal.onApplyAll is () => void — no arguments + const handleApplyAllFixes = () => { + if (!fixProposals) return + fixProposals.forEach((fix) => { + updateStep(fix.target_node_id, fix.fixed_node as Partial) + }) + useProceduralEditorStore.getState().validate() + setFixProposals(null) + } + + const handleCloseFixModal = () => setFixProposals(null) + const handleSave = async (saveStatus?: 'draft' | 'published') => { if (!name.trim()) { toast.error(`Please enter a name for the ${flowLabel.toLowerCase()}`) return } + // Block publish if there are validation errors + const errors = useProceduralEditorStore.getState().validate() + const hasErrors = errors.some((e) => e.severity === 'error') + if (hasErrors && saveStatus === 'published') { + toast.error('Please fix all errors before publishing') + return + } + setIsSaving(true) try { const payload = getTreeForSave() @@ -319,6 +397,21 @@ export function ProceduralEditorPage() { {/* Step List */}
+ {validationErrors.length > 0 && ( +
+ ({ + nodeId: e.stepId, + field: e.field, + message: e.message, + severity: e.severity, + }))} + onSelectNode={handleSelectStep} + onFixWithAI={handleFixWithAI} + isFixing={isFixing} + /> +
+ )}
@@ -366,6 +459,15 @@ export function ProceduralEditorPage() { isLoading={editorAI.isLoading} suggestions={editorAI.suggestions} /> + + {fixProposals && ( + + )} ) }