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} + /> + )} ) }