feat: Workflow Editor UX improvements - validation and notifications
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Build Test / pytest-backend (push) Successful in 8s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s

**Problem 1: Validation footer covers mobile menu**
- Fixed bottom validation panel (z-index 1000) overlapped mobile nav
- Solution: Removed bottom panel, added inline validation in config panel header

**Problem 2: Alert dialogs for save success**
- alert() blocks UI and requires OK click for every save
- Solution: Toast notifications (auto-close after 3s, non-blocking)

**Problem 3: Validation shows only counts, not details**
- Footer showed "1 Error, 2 Warnings" without details
- Solution: Inline display shows all error/warning messages with click-to-navigate

**New Components:**
- Toast.jsx: Auto-closing notifications (success/error/warning/info)
- ConfirmDialog.jsx: Modal confirmation dialogs (for future save-on-close)

**Changes:**
- WorkflowEditorPage: Inline validation in config panel, toast state
- Removed fixed bottom .validation-panel (no mobile overlap)
- Toast for save success instead of alert()

**Still TODO (separate commit):**
- Save confirmation when closing/switching nodes with unsaved changes
- Dirty state tracking

Part 3: Inline Prompts - UX polish (validation + notifications)
This commit is contained in:
Lars 2026-04-11 10:48:28 +02:00
parent 8d89b23db1
commit 3541c416f9
3 changed files with 259 additions and 32 deletions

View File

@ -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 (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 20000
}}
onClick={onCancel}
>
<div
style={{
background: 'var(--surface)',
borderRadius: '12px',
padding: '24px',
maxWidth: '500px',
width: '90%',
boxShadow: '0 8px 24px rgba(0,0,0,0.3)'
}}
onClick={(e) => e.stopPropagation()}
>
<div
style={{
fontSize: '16px',
marginBottom: '24px',
color: 'var(--text1)',
lineHeight: '1.5'
}}
>
{message}
</div>
<div
style={{
display: 'flex',
gap: '12px',
justifyContent: 'flex-end'
}}
>
<button
onClick={onCancel}
style={{
padding: '8px 16px',
border: '1px solid var(--border)',
borderRadius: '6px',
background: 'var(--surface2)',
color: 'var(--text1)',
cursor: 'pointer',
fontSize: '14px'
}}
>
{cancelText}
</button>
<button
onClick={onConfirm}
style={{
padding: '8px 16px',
border: 'none',
borderRadius: '6px',
background: colors[type],
color: type === 'warning' ? '#000' : 'white',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 500
}}
>
{confirmText}
</button>
</div>
</div>
</div>
)
}

View File

@ -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 (
<div
style={{
position: 'fixed',
top: '80px',
left: '50%',
transform: 'translateX(-50%)',
background: style.background,
color: style.color,
padding: '12px 24px',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
zIndex: 10000,
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '14px',
fontWeight: 500,
minWidth: '300px',
maxWidth: '600px',
animation: 'slideDown 0.3s ease-out'
}}
onClick={onClose}
>
<span style={{ fontSize: '18px' }}>{style.icon}</span>
<span>{message}</span>
</div>
)
}
// 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)
}

View File

@ -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() {
</button>
</div>
{/* Inline Validation Display */}
{(validationErrors.length > 0 || validationWarnings.length > 0) && (
<div style={{
background: validationErrors.length > 0 ? '#ffebee' : '#fff3cd',
border: `1px solid ${validationErrors.length > 0 ? 'var(--danger)' : '#FFC107'}`,
borderRadius: '8px',
padding: '12px',
marginBottom: '16px'
}}>
<div style={{ fontWeight: 600, marginBottom: '8px', color: validationErrors.length > 0 ? 'var(--danger)' : '#856404' }}>
{validationErrors.length > 0 ? `${validationErrors.length} Fehler` : `⚠️ ${validationWarnings.length} Warnungen`}
</div>
{validationErrors.map((err, i) => (
<div key={`err-${i}`} style={{ fontSize: '13px', marginBottom: '4px', color: 'var(--danger)', cursor: err.nodeId ? 'pointer' : 'default' }}
onClick={() => err.nodeId && setSelectedNodeId(err.nodeId)}>
{err.message}
</div>
))}
{validationWarnings.map((warn, i) => (
<div key={`warn-${i}`} style={{ fontSize: '13px', marginBottom: '4px', color: '#856404', cursor: warn.nodeId ? 'pointer' : 'default' }}
onClick={() => warn.nodeId && setSelectedNodeId(warn.nodeId)}>
{warn.message}
</div>
))}
</div>
)}
{/* Basis-Konfiguration */}
<div className="config-section">
<label>Node-Name</label>
@ -624,36 +657,7 @@ export default function WorkflowEditorPage() {
)}
</div>
{/* Validation Panel */}
{(validationErrors.length > 0 || validationWarnings.length > 0) && (
<div className="validation-panel">
{validationErrors.map((err, i) => (
<div key={i} className="validation-error" onClick={() => {
if (err.nodeId) {
setSelectedNodeId(err.nodeId)
}
}}>
{err.message}
</div>
))}
{validationWarnings.map((warn, i) => (
<div key={i} className="validation-warning" onClick={() => {
if (warn.nodeId) {
setSelectedNodeId(warn.nodeId)
}
}}>
{warn.message}
</div>
))}
{validationErrors.length === 0 && validationWarnings.length > 0 && (
<div className="validation-success">
Workflow ist valide ({validationWarnings.length} Warnungen)
</div>
)}
</div>
)}
{/* 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 && (
<Toast
message={toast.message}
type={toast.type}
duration={toast.duration || 3000}
onClose={() => setToast(null)}
/>
)}
{/* Confirm Dialog */}
{confirmDialog && (
<ConfirmDialog
message={confirmDialog.message}
onConfirm={confirmDialog.onConfirm}
onCancel={() => setConfirmDialog(null)}
confirmText={confirmDialog.confirmText}
cancelText={confirmDialog.cancelText}
type={confirmDialog.type}
/>
)}
</div>
)
}