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 { useState, useEffect } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { api } from '../utils/api'
|
import { api } from '../utils/api'
|
||||||
import UnifiedPromptModal from '../components/UnifiedPromptModal'
|
import UnifiedPromptModal from '../components/UnifiedPromptModal'
|
||||||
import { Star, Trash2, Edit, Copy, Filter, ArrowDownToLine } from 'lucide-react'
|
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.
|
* Manages both base and pipeline-type prompts in one interface.
|
||||||
*/
|
*/
|
||||||
export default function AdminPromptsPage() {
|
export default function AdminPromptsPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
const [prompts, setPrompts] = useState([])
|
const [prompts, setPrompts] = useState([])
|
||||||
const [filteredPrompts, setFilteredPrompts] = 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 [category, setCategory] = useState('all')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
|
|
@ -44,6 +46,8 @@ export default function AdminPromptsPage() {
|
||||||
filtered = filtered.filter(p => p.type === 'base')
|
filtered = filtered.filter(p => p.type === 'base')
|
||||||
} else if (typeFilter === 'pipeline') {
|
} else if (typeFilter === 'pipeline') {
|
||||||
filtered = filtered.filter(p => p.type === 'pipeline')
|
filtered = filtered.filter(p => p.type === 'pipeline')
|
||||||
|
} else if (typeFilter === 'workflow') {
|
||||||
|
filtered = filtered.filter(p => p.type === 'workflow')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by category
|
// Filter by category
|
||||||
|
|
@ -256,6 +260,13 @@ export default function AdminPromptsPage() {
|
||||||
>
|
>
|
||||||
+ Neuer Prompt
|
+ Neuer Prompt
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => navigate('/workflow-editor/new')}
|
||||||
|
style={{ marginLeft: 8 }}
|
||||||
|
>
|
||||||
|
🔀 Neuer Workflow
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -329,6 +340,13 @@ export default function AdminPromptsPage() {
|
||||||
>
|
>
|
||||||
Pipelines ({prompts.filter(p => p.type === 'pipeline' || !p.type).length})
|
Pipelines ({prompts.filter(p => p.type === 'pipeline' || !p.type).length})
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,22 @@ export default function WorkflowEditorPage() {
|
||||||
const [validationWarnings, setValidationWarnings] = useState([])
|
const [validationWarnings, setValidationWarnings] = useState([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
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
|
// Load workflow wenn ID vorhanden
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -237,10 +253,17 @@ export default function WorkflowEditorPage() {
|
||||||
Neu
|
Neu
|
||||||
</button>
|
</button>
|
||||||
<button className="btn-secondary" onClick={handleValidate}>
|
<button className="btn-secondary" onClick={handleValidate}>
|
||||||
Validieren
|
Validieren {validationErrors.length > 0 ? `(${validationErrors.length} ⚠️)` : ''}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn-primary" onClick={handleSave} disabled={loading}>
|
<button
|
||||||
{loading ? 'Speichern...' : 'Speichern'}
|
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>
|
</button>
|
||||||
{currentPrompt && (
|
{currentPrompt && (
|
||||||
<button className="btn-secondary" onClick={handleDelete} disabled={loading}>
|
<button className="btn-secondary" onClick={handleDelete} disabled={loading}>
|
||||||
|
|
@ -250,8 +273,13 @@ export default function WorkflowEditorPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div style={{ padding: '12px', background: 'var(--danger)', color: 'white' }}>
|
<div style={{ padding: '12px', background: 'var(--danger)', color: 'white', borderRadius: '4px', marginBottom: '8px' }}>
|
||||||
❌ {error}
|
❌ {error}
|
||||||
|
{validationErrors.length > 0 && (
|
||||||
|
<div style={{ marginTop: 8, fontSize: 12 }}>
|
||||||
|
Tipp: Behebe die Validierungsfehler unten, um speichern zu können.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -316,21 +344,86 @@ export default function WorkflowEditorPage() {
|
||||||
{/* Config Panel */}
|
{/* Config Panel */}
|
||||||
{selectedNode && (
|
{selectedNode && (
|
||||||
<div className="workflow-config-panel">
|
<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 */}
|
{/* Basis-Konfiguration */}
|
||||||
<div className="config-section">
|
<div className="config-section">
|
||||||
<label>Label</label>
|
<label>Node-Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={selectedNode.data.label || ''}
|
value={selectedNode.data.label || ''}
|
||||||
onChange={(e) => handleNodeUpdate(selectedNode.id, { label: e.target.value })}
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Type-spezifische Konfiguration */}
|
{/* Type-spezifische Konfiguration */}
|
||||||
{selectedNode.type === 'analysis' && (
|
{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} />
|
<QuestionAugmentationPanel node={selectedNode} onChange={handleNodeUpdate} />
|
||||||
<FallbackConfig node={selectedNode} edges={edges} onChange={handleNodeUpdate} />
|
<FallbackConfig node={selectedNode} edges={edges} onChange={handleNodeUpdate} />
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user