feat: add validation summary and Fix with AI to procedural editor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,10 +13,13 @@ import { TagInput } from '@/components/common/TagInput'
|
|||||||
import { Spinner } from '@/components/common/Spinner'
|
import { Spinner } from '@/components/common/Spinner'
|
||||||
import { EditorAIPanel } from '@/components/editor-ai/EditorAIPanel'
|
import { EditorAIPanel } from '@/components/editor-ai/EditorAIPanel'
|
||||||
import { ContextMenu } from '@/components/common/ContextMenu'
|
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 { 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, 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'
|
type SectionKey = 'details' | 'intake' | 'schedule'
|
||||||
|
|
||||||
@@ -51,6 +54,9 @@ export function ProceduralEditorPage() {
|
|||||||
} = useProceduralEditorStore()
|
} = useProceduralEditorStore()
|
||||||
|
|
||||||
const steps = useProceduralEditorStore(s => s.steps)
|
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<string, unknown>, metadata?: Record<string, unknown> | null) => {
|
const handleFlowUpdate = useCallback((workingTree: Record<string, unknown>, metadata?: Record<string, unknown> | null) => {
|
||||||
const stepsData = workingTree.steps as ProceduralStep[] | undefined
|
const stepsData = workingTree.steps as ProceduralStep[] | undefined
|
||||||
@@ -75,6 +81,9 @@ export function ProceduralEditorPage() {
|
|||||||
onFlowUpdate: handleFlowUpdate,
|
onFlowUpdate: handleFlowUpdate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [isFixing, setIsFixing] = useState(false)
|
||||||
|
const [fixProposals, setFixProposals] = useState<AIFixProposal[] | null>(null)
|
||||||
|
|
||||||
const isMaintenance = treeType === 'maintenance'
|
const isMaintenance = treeType === 'maintenance'
|
||||||
const flowLabel = isMaintenance ? 'Maintenance Flow' : 'Procedure'
|
const flowLabel = isMaintenance ? 'Maintenance Flow' : 'Procedure'
|
||||||
|
|
||||||
@@ -110,6 +119,10 @@ export function ProceduralEditorPage() {
|
|||||||
return () => { reset() }
|
return () => { reset() }
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
useProceduralEditorStore.getState().validate()
|
||||||
|
}, [steps, intakeForm])
|
||||||
|
|
||||||
const loadExistingTree = async (treeId: string) => {
|
const loadExistingTree = async (treeId: string) => {
|
||||||
try {
|
try {
|
||||||
const tree = await treesApi.get(treeId)
|
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<string, unknown>,
|
||||||
|
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<ProceduralStep>)
|
||||||
|
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<ProceduralStep>)
|
||||||
|
})
|
||||||
|
useProceduralEditorStore.getState().validate()
|
||||||
|
setFixProposals(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseFixModal = () => setFixProposals(null)
|
||||||
|
|
||||||
const handleSave = async (saveStatus?: 'draft' | 'published') => {
|
const handleSave = async (saveStatus?: 'draft' | 'published') => {
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
toast.error(`Please enter a name for the ${flowLabel.toLowerCase()}`)
|
toast.error(`Please enter a name for the ${flowLabel.toLowerCase()}`)
|
||||||
return
|
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)
|
setIsSaving(true)
|
||||||
try {
|
try {
|
||||||
const payload = getTreeForSave()
|
const payload = getTreeForSave()
|
||||||
@@ -319,6 +397,21 @@ export function ProceduralEditorPage() {
|
|||||||
|
|
||||||
{/* Step List */}
|
{/* Step List */}
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
||||||
|
{validationErrors.length > 0 && (
|
||||||
|
<div className="px-4 py-3">
|
||||||
|
<ValidationSummary
|
||||||
|
errors={validationErrors.map((e): ValidationError => ({
|
||||||
|
nodeId: e.stepId,
|
||||||
|
field: e.field,
|
||||||
|
message: e.message,
|
||||||
|
severity: e.severity,
|
||||||
|
}))}
|
||||||
|
onSelectNode={handleSelectStep}
|
||||||
|
onFixWithAI={handleFixWithAI}
|
||||||
|
isFixing={isFixing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<StepList onStepContextMenu={editorAI.openContextMenu} />
|
<StepList onStepContextMenu={editorAI.openContextMenu} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -366,6 +459,15 @@ export function ProceduralEditorPage() {
|
|||||||
isLoading={editorAI.isLoading}
|
isLoading={editorAI.isLoading}
|
||||||
suggestions={editorAI.suggestions}
|
suggestions={editorAI.suggestions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{fixProposals && (
|
||||||
|
<AIFixReviewModal
|
||||||
|
fixes={fixProposals}
|
||||||
|
onApply={handleApplyFix}
|
||||||
|
onApplyAll={handleApplyAllFixes}
|
||||||
|
onClose={handleCloseFixModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user