diff --git a/frontend/src/components/ConfirmDialog.jsx b/frontend/src/components/ConfirmDialog.jsx
new file mode 100644
index 0000000..221f99a
--- /dev/null
+++ b/frontend/src/components/ConfirmDialog.jsx
@@ -0,0 +1,106 @@
+/**
+ * ConfirmDialog Component
+ *
+ * Modal confirmation dialog
+ *
+ * Props:
+ * - message: string
+ * - onConfirm: callback when confirmed
+ * - onCancel: callback when cancelled
+ * - confirmText: string (default: 'OK')
+ * - cancelText: string (default: 'Abbrechen')
+ * - type: 'warning' | 'danger' | 'info' (default: 'warning')
+ */
+export function ConfirmDialog({
+ message,
+ onConfirm,
+ onCancel,
+ confirmText = 'OK',
+ cancelText = 'Abbrechen',
+ type = 'warning'
+}) {
+ const colors = {
+ warning: '#FFC107',
+ danger: 'var(--danger)',
+ info: 'var(--accent)'
+ }
+
+ return (
+
+
e.stopPropagation()}
+ >
+
+ {message}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/Toast.jsx b/frontend/src/components/Toast.jsx
new file mode 100644
index 0000000..284a9bc
--- /dev/null
+++ b/frontend/src/components/Toast.jsx
@@ -0,0 +1,95 @@
+import { useEffect } from 'react'
+
+/**
+ * Toast Notification Component
+ *
+ * Auto-closing notification that appears at the top of the screen
+ *
+ * Props:
+ * - message: string
+ * - type: 'success' | 'error' | 'warning' | 'info'
+ * - duration: number (ms, default 3000)
+ * - onClose: callback when toast closes
+ */
+export function Toast({ message, type = 'info', duration = 3000, onClose }) {
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (onClose) onClose()
+ }, duration)
+
+ return () => clearTimeout(timer)
+ }, [duration, onClose])
+
+ const styles = {
+ success: {
+ background: '#4CAF50',
+ color: 'white',
+ icon: '✅'
+ },
+ error: {
+ background: 'var(--danger)',
+ color: 'white',
+ icon: '❌'
+ },
+ warning: {
+ background: '#FFC107',
+ color: '#856404',
+ icon: '⚠️'
+ },
+ info: {
+ background: 'var(--accent)',
+ color: 'white',
+ icon: 'ℹ️'
+ }
+ }
+
+ const style = styles[type] || styles.info
+
+ return (
+
+ {style.icon}
+ {message}
+
+ )
+}
+
+// Add animation CSS if not already in global styles
+const styleElement = document.createElement('style')
+styleElement.textContent = `
+ @keyframes slideDown {
+ from {
+ opacity: 0;
+ transform: translateX(-50%) translateY(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+ }
+`
+if (!document.querySelector('style[data-toast-styles]')) {
+ styleElement.setAttribute('data-toast-styles', 'true')
+ document.head.appendChild(styleElement)
+}
diff --git a/frontend/src/pages/WorkflowEditorPage.jsx b/frontend/src/pages/WorkflowEditorPage.jsx
index 0aa4433..c85f23b 100644
--- a/frontend/src/pages/WorkflowEditorPage.jsx
+++ b/frontend/src/pages/WorkflowEditorPage.jsx
@@ -19,6 +19,8 @@ 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 { Toast } from '../components/Toast'
+import { ConfirmDialog } from '../components/ConfirmDialog'
import '../styles/workflowEditor.css'
// Node-Type Mapping
@@ -55,6 +57,10 @@ export default function WorkflowEditorPage() {
const endNodeTextareaRef = useRef(null)
const inlineTemplateTextareaRef = useRef(null)
+ // Toast & Confirm Dialog
+ const [toast, setToast] = useState(null)
+ const [confirmDialog, setConfirmDialog] = useState(null)
+
// Load available basis prompts for Analysis nodes
useEffect(() => {
async function loadPrompts() {
@@ -155,7 +161,7 @@ export default function WorkflowEditorPage() {
description: workflowDescription,
graph_data
})
- alert('Workflow gespeichert!')
+ setToast({ message: '✅ Workflow gespeichert!', type: 'success' })
} else {
// Create new
console.log('✨ Creating new workflow')
@@ -167,7 +173,7 @@ export default function WorkflowEditorPage() {
})
console.log('✅ Workflow created:', result)
setCurrentPrompt({ id: result.id, slug: result.slug, name: workflowName })
- alert('Workflow erstellt!')
+ setToast({ message: '✅ Workflow erstellt!', type: 'success' })
console.log('🚀 Navigating to:', `/workflow-editor/${result.id}`)
navigate(`/workflow-editor/${result.id}`)
}
@@ -462,6 +468,33 @@ export default function WorkflowEditorPage() {
+ {/* Inline Validation Display */}
+ {(validationErrors.length > 0 || validationWarnings.length > 0) && (
+ 0 ? '#ffebee' : '#fff3cd',
+ border: `1px solid ${validationErrors.length > 0 ? 'var(--danger)' : '#FFC107'}`,
+ borderRadius: '8px',
+ padding: '12px',
+ marginBottom: '16px'
+ }}>
+
0 ? 'var(--danger)' : '#856404' }}>
+ {validationErrors.length > 0 ? `❌ ${validationErrors.length} Fehler` : `⚠️ ${validationWarnings.length} Warnungen`}
+
+ {validationErrors.map((err, i) => (
+
err.nodeId && setSelectedNodeId(err.nodeId)}>
+ • {err.message}
+
+ ))}
+ {validationWarnings.map((warn, i) => (
+
warn.nodeId && setSelectedNodeId(warn.nodeId)}>
+ • {warn.message}
+
+ ))}
+
+ )}
+
{/* Basis-Konfiguration */}
@@ -624,36 +657,7 @@ export default function WorkflowEditorPage() {
)}
- {/* Validation Panel */}
- {(validationErrors.length > 0 || validationWarnings.length > 0) && (
-
- {validationErrors.map((err, i) => (
-
{
- if (err.nodeId) {
- setSelectedNodeId(err.nodeId)
- }
- }}>
- ❌ {err.message}
-
- ))}
-
- {validationWarnings.map((warn, i) => (
-
{
- if (warn.nodeId) {
- setSelectedNodeId(warn.nodeId)
- }
- }}>
- ⚠️ {warn.message}
-
- ))}
-
- {validationErrors.length === 0 && validationWarnings.length > 0 && (
-
- ✅ Workflow ist valide ({validationWarnings.length} Warnungen)
-
- )}
-
- )}
+ {/* Validation Panel - REMOVED (moved to config panel header) */}
{/* Execution Result Viewer */}
{executionResult && (
@@ -672,6 +676,28 @@ export default function WorkflowEditorPage() {
onClose={() => setShowPlaceholderPicker(false)}
/>
)}
+
+ {/* Toast Notification */}
+ {toast && (
+ setToast(null)}
+ />
+ )}
+
+ {/* Confirm Dialog */}
+ {confirmDialog && (
+ setConfirmDialog(null)}
+ confirmText={confirmDialog.confirmText}
+ cancelText={confirmDialog.cancelText}
+ type={confirmDialog.type}
+ />
+ )}
)
}