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 { 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<string, unknown>, metadata?: Record<string, unknown> | 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<AIFixProposal[] | null>(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<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') => {
|
||||
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 */}
|
||||
<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} />
|
||||
</div>
|
||||
|
||||
@@ -366,6 +459,15 @@ export function ProceduralEditorPage() {
|
||||
isLoading={editorAI.isLoading}
|
||||
suggestions={editorAI.suggestions}
|
||||
/>
|
||||
|
||||
{fixProposals && (
|
||||
<AIFixReviewModal
|
||||
fixes={fixProposals}
|
||||
onApply={handleApplyFix}
|
||||
onApplyAll={handleApplyAllFixes}
|
||||
onClose={handleCloseFixModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user