From 549c31431e1e0fcdd55bee3a0f66602cb7b35350 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 11 Apr 2026 15:27:14 +0200 Subject: [PATCH] feat: Validierungs-Panel mit Details und Click-to-Jump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #1 + #3: Validierung zeigt keine Details / Fehler-Lokalisierung NEU: ValidationPanel Component - Zeigt alle Fehler und Warnungen mit vollständigen Details - Gruppiert nach Severity (Fehler/Warnungen) - Aufklappbar pro Gruppe (collapsible) - Click-to-Jump: Fehler mit nodeId sind klickbar - Klick selektiert betroffene Node → Config-Panel öffnet sich - Schließbar, öffnet sich automatisch bei neuen Fehler/Warnungen Features: - Fixed position (bottom-right, über Canvas) - Farb-Kodierung: Rot (Errors) / Gelb (Warnings) - Hover-Effekt auf klickbare Items - Type-Labels (structure, isolation, etc.) - Scrollbar bei vielen Fehler/Warnungen - X-Button zum Schließen UX-Verbesserungen: - Kein Raten mehr: Zeigt WAS der Fehler ist - Kein Suchen mehr: Klick springt direkt zur Node - Übersichtlich auch bei vielen Fehler/Warnungen - Funktioniert in großen Workflows Technisch: - handleValidationNodeClick: setSelectedNodeId - Auto-show bei errors.length > 0 || warnings.length > 0 - State: showValidationPanel (closable) Co-Authored-By: Claude Opus 4.6 --- .../workflow/panels/ValidationPanel.jsx | 246 ++++++++++++++++++ frontend/src/pages/WorkflowEditorPage.jsx | 26 ++ 2 files changed, 272 insertions(+) create mode 100644 frontend/src/components/workflow/panels/ValidationPanel.jsx diff --git a/frontend/src/components/workflow/panels/ValidationPanel.jsx b/frontend/src/components/workflow/panels/ValidationPanel.jsx new file mode 100644 index 0000000..804a3e4 --- /dev/null +++ b/frontend/src/components/workflow/panels/ValidationPanel.jsx @@ -0,0 +1,246 @@ +import { useState } from 'react' +import { AlertCircle, AlertTriangle, ChevronDown, ChevronRight, X } from 'lucide-react' + +/** + * ValidationPanel - Zeigt Fehler und Warnungen mit Details + * + * Features: + * - Aufklappbar (collapsible) + * - Click-to-Jump zu betroffener Node + * - Gruppierung nach Severity + * - Klare visuelle Trennung + */ +export function ValidationPanel({ errors, warnings, onNodeClick, onClose }) { + const [isExpanded, setIsExpanded] = useState(true) + const [showErrors, setShowErrors] = useState(true) + const [showWarnings, setShowWarnings] = useState(true) + + const totalCount = errors.length + warnings.length + + if (totalCount === 0) return null + + const handleItemClick = (item) => { + if (item.nodeId && onNodeClick) { + onNodeClick(item.nodeId) + } + } + + return ( +
+ {/* Header */} +
setIsExpanded(!isExpanded)} + style={{ + padding: '12px 16px', + borderBottom: isExpanded ? '1px solid var(--border)' : 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + cursor: 'pointer', + userSelect: 'none', + background: errors.length > 0 ? 'rgba(216, 90, 48, 0.1)' : 'rgba(255, 193, 7, 0.1)' + }} + > +
+ {isExpanded ? : } + 0 ? 'var(--danger)' : '#f59e0b'} /> + + Validierung + + + {errors.length > 0 && `${errors.length} Fehler`} + {errors.length > 0 && warnings.length > 0 && ', '} + {warnings.length > 0 && `${warnings.length} Warnung${warnings.length > 1 ? 'en' : ''}`} + +
+ +
+ + {/* Content */} + {isExpanded && ( +
+ {/* Errors */} + {errors.length > 0 && ( +
+
setShowErrors(!showErrors)} + style={{ + padding: '8px 16px', + background: 'rgba(216, 90, 48, 0.05)', + display: 'flex', + alignItems: 'center', + gap: 8, + cursor: 'pointer', + userSelect: 'none', + borderBottom: '1px solid var(--border)' + }} + > + {showErrors ? : } + + + Fehler ({errors.length}) + +
+ {showErrors && ( +
+ {errors.map((error, idx) => ( +
handleItemClick(error)} + style={{ + padding: '10px 16px', + borderBottom: idx < errors.length - 1 ? '1px solid var(--border)' : 'none', + cursor: error.nodeId ? 'pointer' : 'default', + background: error.nodeId ? 'transparent' : 'transparent', + transition: 'background 0.15s', + }} + onMouseEnter={(e) => { + if (error.nodeId) e.currentTarget.style.background = 'rgba(216, 90, 48, 0.05)' + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'transparent' + }} + > +
+ {error.message} +
+ {error.nodeId && ( +
+ → Klicken um zu Node zu springen +
+ )} + {error.type && ( +
+ {error.type} +
+ )} +
+ ))} +
+ )} +
+ )} + + {/* Warnings */} + {warnings.length > 0 && ( +
+
setShowWarnings(!showWarnings)} + style={{ + padding: '8px 16px', + background: 'rgba(255, 193, 7, 0.05)', + display: 'flex', + alignItems: 'center', + gap: 8, + cursor: 'pointer', + userSelect: 'none', + borderBottom: '1px solid var(--border)' + }} + > + {showWarnings ? : } + + + Warnungen ({warnings.length}) + +
+ {showWarnings && ( +
+ {warnings.map((warning, idx) => ( +
handleItemClick(warning)} + style={{ + padding: '10px 16px', + borderBottom: idx < warnings.length - 1 ? '1px solid var(--border)' : 'none', + cursor: warning.nodeId ? 'pointer' : 'default', + background: 'transparent', + transition: 'background 0.15s', + }} + onMouseEnter={(e) => { + if (warning.nodeId) e.currentTarget.style.background = 'rgba(255, 193, 7, 0.05)' + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'transparent' + }} + > +
+ {warning.message} +
+ {warning.nodeId && ( +
+ → Klicken um zu Node zu springen +
+ )} + {warning.type && ( +
+ {warning.type} +
+ )} +
+ ))} +
+ )} +
+ )} +
+ )} +
+ ) +} diff --git a/frontend/src/pages/WorkflowEditorPage.jsx b/frontend/src/pages/WorkflowEditorPage.jsx index fb1f4a5..35392d8 100644 --- a/frontend/src/pages/WorkflowEditorPage.jsx +++ b/frontend/src/pages/WorkflowEditorPage.jsx @@ -19,6 +19,7 @@ import { PlaceholderPicker } from '../components/workflow/panels/PlaceholderPick import { WorkflowExecutePanel } from '../components/workflow/panels/WorkflowExecutePanel' import { WorkflowResultViewer } from '../components/workflow/panels/WorkflowResultViewer' import { InlineTemplateEditor } from '../components/workflow/panels/InlineTemplateEditor' +import { ValidationPanel } from '../components/workflow/panels/ValidationPanel' import { Toast } from '../components/Toast' import { ConfirmDialog } from '../components/ConfirmDialog' import '../styles/workflowEditor.css' @@ -78,6 +79,7 @@ export default function WorkflowEditorPage() { const endNodeTextareaRef = useRef(null) const inlineTemplateTextareaRef = useRef(null) const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) + const [showValidationPanel, setShowValidationPanel] = useState(true) // Toast & Confirm Dialog const [toast, setToast] = useState(null) @@ -122,6 +124,11 @@ export default function WorkflowEditorPage() { const { errors, warnings } = validateWorkflowGraph(nodes, edges) setValidationErrors(errors) setValidationWarnings(warnings) + + // Re-show validation panel if there are new errors/warnings + if (errors.length > 0 || warnings.length > 0) { + setShowValidationPanel(true) + } }, [nodes, edges]) // Warn on browser back/refresh if unsaved changes @@ -380,6 +387,15 @@ export default function WorkflowEditorPage() { navigate('/admin/prompts') } + const handleValidationNodeClick = (nodeId) => { + // Select the node to show its config panel + setSelectedNodeId(nodeId) + + // TODO: Optional - scroll to node in canvas + // ReactFlow doesn't expose a direct scrollTo API, but we could use fitView + // or manual DOM manipulation if needed + } + const handlePlaceholderSelect = (placeholderString) => { if (!selectedNode) return @@ -870,6 +886,16 @@ export default function WorkflowEditorPage() { /> )} + {/* Validation Panel */} + {showValidationPanel && (validationErrors.length > 0 || validationWarnings.length > 0) && ( + setShowValidationPanel(false)} + /> + )} + {/* Toast Notification */} {toast && (