feat: Add tree editor validation UI (Workstream A complete)

Implements comprehensive validation feedback system for tree editor:

Task A.1 - Circular Reference Detection:
- Added detectCircularRefs() function in treeEditorStore
- Detects loops in both decision options and action next_node_id chains
- Prevents infinite navigation paths

Task A.2 - ValidationSummary Component:
- Created collapsible panel showing error/warning count
- Click error to select problematic node
- Color-coded: red for errors, yellow for warnings
- Icon indicators (AlertCircle, AlertTriangle)

Task A.3 - TreeEditorPage Integration:
- Added ValidationSummary component display
- Save button disabled when errors exist
- Warnings are informational only (don't block save)
- Added manual "Validate" button in toolbar
- Imported CheckCircle2 icon for validate button

Task A.4 - Visual Node Error Indicators:
- Added error/warning badges on problem nodes
- Tooltip on hover showing specific error messages
- Red ring for errors, yellow ring for warnings
- Shows count of errors/warnings per node

All tasks from implementation plan completed.
Build tested successfully.

Related: Issue #1

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-02-03 19:01:27 -05:00
parent 4378ec4b20
commit f93c8d84df
4 changed files with 237 additions and 12 deletions

View File

@@ -623,6 +623,44 @@ export const useTreeEditorStore = create<TreeEditorState>()(
validateNode(state.treeStructure)
// Check for circular references in next_node_id chains
const detectCircularRefs = (startId: string, visited: Set<string> = new Set()): boolean => {
if (visited.has(startId)) return true
visited.add(startId)
const node = findNodeInTree(startId, state.treeStructure)
if (!node) return false
// Check options
if (node.options) {
for (const opt of node.options) {
if (opt.next_node_id && detectCircularRefs(opt.next_node_id, new Set(visited))) {
errors.push({
nodeId: node.id,
message: `Circular reference detected: "${opt.label}" creates a loop`,
severity: 'error'
})
return true
}
}
}
// Check next_node_id
if (node.next_node_id && detectCircularRefs(node.next_node_id, new Set(visited))) {
errors.push({
nodeId: node.id,
message: `Circular reference detected in node "${node.title || node.id}"`,
severity: 'error'
})
return true
}
return false
}
// Run from root
detectCircularRefs('root')
// Check for at least one solution
if (!hasSolution) {
errors.push({