fix: Phase 5 - Critical UX bugs in Workflow Editor
Behebt 5 kritische Bugs die Editor unbenutzbar machten:
BUG-01: Config Panel - Close Button hinzugefügt (×)
- User war im Config Panel "gefangen"
- Jetzt: Click × zum Deselektieren
BUG-02: Save UX - Validierungs-Feedback verbessert
- Speichern-Button zeigt Lock-Icon (🔒) bei Fehlern
- Tooltip erklärt warum Speichern blockiert ist
- Error-Message mit Hinweis auf Validierung
BUG-03: Analysis Node - Prompt-Auswahl implementiert
- Dropdown zum Auswählen von Basis-Prompts
- Lädt verfügbare Prompts via API
- Zeigt gewählten Prompt-Namen an
BUG-04: Label-Input - UX verbessert
- Header zeigt "Node-Konfiguration" (nicht Label)
- Input hat Placeholder und Hilfetext
- "Änderungen automatisch übernommen" Hinweis
BUG-05: Admin Page - "Neuer Workflow" Button
- Button neben "+ Neuer Prompt"
- Navigiert zu /workflow-editor/new
- Workflow-Filter im Type-Filter hinzugefügt
Tested: Manuell durch User (alle Bugs bestätigt gefixt)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dc59596f01
commit
e3ef18674a
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { api } from '../utils/api'
|
||||
import UnifiedPromptModal from '../components/UnifiedPromptModal'
|
||||
import { Star, Trash2, Edit, Copy, Filter, ArrowDownToLine } from 'lucide-react'
|
||||
|
|
@ -9,9 +10,10 @@ import { Star, Trash2, Edit, Copy, Filter, ArrowDownToLine } from 'lucide-react'
|
|||
* Manages both base and pipeline-type prompts in one interface.
|
||||
*/
|
||||
export default function AdminPromptsPage() {
|
||||
const navigate = useNavigate()
|
||||
const [prompts, setPrompts] = useState([])
|
||||
const [filteredPrompts, setFilteredPrompts] = useState([])
|
||||
const [typeFilter, setTypeFilter] = useState('all') // 'all' | 'base' | 'pipeline'
|
||||
const [typeFilter, setTypeFilter] = useState('all') // 'all' | 'base' | 'pipeline' | 'workflow'
|
||||
const [category, setCategory] = useState('all')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
|
|
@ -44,6 +46,8 @@ export default function AdminPromptsPage() {
|
|||
filtered = filtered.filter(p => p.type === 'base')
|
||||
} else if (typeFilter === 'pipeline') {
|
||||
filtered = filtered.filter(p => p.type === 'pipeline')
|
||||
} else if (typeFilter === 'workflow') {
|
||||
filtered = filtered.filter(p => p.type === 'workflow')
|
||||
}
|
||||
|
||||
// Filter by category
|
||||
|
|
@ -256,6 +260,13 @@ export default function AdminPromptsPage() {
|
|||
>
|
||||
+ Neuer Prompt
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => navigate('/workflow-editor/new')}
|
||||
style={{ marginLeft: 8 }}
|
||||
>
|
||||
🔀 Neuer Workflow
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -329,6 +340,13 @@ export default function AdminPromptsPage() {
|
|||
>
|
||||
Pipelines ({prompts.filter(p => p.type === 'pipeline' || !p.type).length})
|
||||
</button>
|
||||
<button
|
||||
className={typeFilter === 'workflow' ? 'btn btn-primary' : 'btn'}
|
||||
onClick={() => setTypeFilter('workflow')}
|
||||
style={{ fontSize: 13, padding: '6px 12px' }}
|
||||
>
|
||||
🔀 Workflows ({prompts.filter(p => p.type === 'workflow').length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
|
|
|
|||
|
|
@ -42,6 +42,22 @@ export default function WorkflowEditorPage() {
|
|||
const [validationWarnings, setValidationWarnings] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [availablePrompts, setAvailablePrompts] = useState([])
|
||||
|
||||
// Load available basis prompts for Analysis nodes
|
||||
useEffect(() => {
|
||||
async function loadPrompts() {
|
||||
try {
|
||||
const prompts = await api.listAdminPrompts()
|
||||
// Filter nur type='base' Prompts
|
||||
const basisPrompts = prompts.filter(p => p.type === 'base')
|
||||
setAvailablePrompts(basisPrompts)
|
||||
} catch (e) {
|
||||
console.error('Failed to load prompts:', e)
|
||||
}
|
||||
}
|
||||
loadPrompts()
|
||||
}, [])
|
||||
|
||||
// Load workflow wenn ID vorhanden
|
||||
useEffect(() => {
|
||||
|
|
@ -237,10 +253,17 @@ export default function WorkflowEditorPage() {
|
|||
Neu
|
||||
</button>
|
||||
<button className="btn-secondary" onClick={handleValidate}>
|
||||
Validieren
|
||||
Validieren {validationErrors.length > 0 ? `(${validationErrors.length} ⚠️)` : ''}
|
||||
</button>
|
||||
<button className="btn-primary" onClick={handleSave} disabled={loading}>
|
||||
{loading ? 'Speichern...' : 'Speichern'}
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
title={validationErrors.length > 0
|
||||
? `Speichern blockiert: ${validationErrors.length} Validierungsfehler`
|
||||
: 'Workflow in Datenbank speichern'}
|
||||
>
|
||||
{loading ? 'Speichern...' : validationErrors.length > 0 ? '🔒 Speichern' : '💾 Speichern'}
|
||||
</button>
|
||||
{currentPrompt && (
|
||||
<button className="btn-secondary" onClick={handleDelete} disabled={loading}>
|
||||
|
|
@ -250,8 +273,13 @@ export default function WorkflowEditorPage() {
|
|||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{ padding: '12px', background: 'var(--danger)', color: 'white' }}>
|
||||
<div style={{ padding: '12px', background: 'var(--danger)', color: 'white', borderRadius: '4px', marginBottom: '8px' }}>
|
||||
❌ {error}
|
||||
{validationErrors.length > 0 && (
|
||||
<div style={{ marginTop: 8, fontSize: 12 }}>
|
||||
Tipp: Behebe die Validierungsfehler unten, um speichern zu können.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -316,21 +344,86 @@ export default function WorkflowEditorPage() {
|
|||
{/* Config Panel */}
|
||||
{selectedNode && (
|
||||
<div className="workflow-config-panel">
|
||||
<h2 style={{ margin: '0 0 16px 0' }}>{selectedNode.data.label}</h2>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<h2 style={{ margin: 0 }}>Node-Konfiguration</h2>
|
||||
<button
|
||||
onClick={() => setSelectedNode(null)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: 24,
|
||||
cursor: 'pointer',
|
||||
color: 'var(--text3)',
|
||||
padding: 4,
|
||||
lineHeight: 1
|
||||
}}
|
||||
title="Schließen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Basis-Konfiguration */}
|
||||
<div className="config-section">
|
||||
<label>Label</label>
|
||||
<label>Node-Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedNode.data.label || ''}
|
||||
onChange={(e) => handleNodeUpdate(selectedNode.id, { label: e.target.value })}
|
||||
placeholder="z.B. Gewichtsanalyse"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface)',
|
||||
color: 'var(--text1)',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginTop: 4, fontSize: 11, color: 'var(--text3)' }}>
|
||||
Änderungen werden automatisch übernommen
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Type-spezifische Konfiguration */}
|
||||
{selectedNode.type === 'analysis' && (
|
||||
<>
|
||||
<div className="config-section">
|
||||
<label>KI-Prompt auswählen</label>
|
||||
<select
|
||||
value={selectedNode.data.prompt_id || ''}
|
||||
onChange={(e) => {
|
||||
const promptId = e.target.value
|
||||
const selectedPrompt = availablePrompts.find(p => p.id === parseInt(promptId))
|
||||
handleNodeUpdate(selectedNode.id, {
|
||||
prompt_id: promptId ? parseInt(promptId) : null,
|
||||
prompt_name: selectedPrompt?.name || null
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface)',
|
||||
color: 'var(--text1)'
|
||||
}}
|
||||
>
|
||||
<option value="">-- Basis-Prompt wählen --</option>
|
||||
{availablePrompts.map(prompt => (
|
||||
<option key={prompt.id} value={prompt.id}>
|
||||
{prompt.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedNode.data.prompt_name && (
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--text3)' }}>
|
||||
Gewählt: {selectedNode.data.prompt_name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<QuestionAugmentationPanel node={selectedNode} onChange={handleNodeUpdate} />
|
||||
<FallbackConfig node={selectedNode} edges={edges} onChange={handleNodeUpdate} />
|
||||
</>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user