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 && (