Responsive Gui - partially Workflow #61

Merged
Lars merged 47 commits from develop into main 2026-04-05 11:27:44 +02:00
2 changed files with 118 additions and 7 deletions
Showing only changes of commit e3ef18674a - Show all commits

View File

@ -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={{

View File

@ -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} />
</>