feat: Pipeline-System Frontend - Admin UI (Issue #28, Phase 2 Part 1)
Implementiert Admin-UI für Pipeline-Konfigurationen: - Pipeline-Config Dialog mit Module-Auswahl - Stage-Konfiguration (Stage 1/2/3 Prompts) - Admin-UI: Zwei Tabs (Prompts + Pipeline-Configs) - CRUD-Operationen für Pipeline-Configs - API-Integration: Pipeline-Config Endpoints **Frontend:** - components/PipelineConfigModal.jsx (neu): Dialog für Pipeline-Konfiguration - Module-Auswahl mit Zeiträumen (7 Module) - Stage 1: Multi-Select für parallele Prompts - Stage 2: Synthese-Prompt Auswahl - Stage 3: Optional (Goals) - Validierung (mind. 1 Modul, mind. 1 Stage-1-Prompt, Stage-2 erforderlich) - pages/AdminPromptsPage.jsx (erweitert): Tab-Navigation - Tab 1: Prompts (bestehend) - Tab 2: Pipeline-Konfigurationen (neu) - Liste aller Configs mit Status (Aktiv, Standard) - Aktionen: Bearbeiten, Löschen, Als Standard setzen - Icons: Star, Edit, Trash2 - utils/api.js (erweitert): - listPipelineConfigs, createPipelineConfig, updatePipelineConfig - deletePipelineConfig, setDefaultPipelineConfig - executePipeline, resetPromptToDefault **Nächste Schritte:** - Pipeline-Auswahl in AnalysisPage (User-Seite) - Mobile-Responsive Design Issue #28 Progress: Frontend 2/3 (67%) | Design 0/3 | Testing 0/1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
053a9e18cf
commit
b23e361791
386
frontend/src/components/PipelineConfigModal.jsx
Normal file
386
frontend/src/components/PipelineConfigModal.jsx
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { api } from '../utils/api'
|
||||||
|
import { X } from 'lucide-react'
|
||||||
|
|
||||||
|
const MODULES = [
|
||||||
|
{ id: 'körper', label: 'Körper', defaultDays: 30 },
|
||||||
|
{ id: 'ernährung', label: 'Ernährung', defaultDays: 30 },
|
||||||
|
{ id: 'training', label: 'Training', defaultDays: 14 },
|
||||||
|
{ id: 'schlaf', label: 'Schlaf', defaultDays: 14 },
|
||||||
|
{ id: 'vitalwerte', label: 'Vitalwerte', defaultDays: 7 },
|
||||||
|
{ id: 'mentales', label: 'Mentales', defaultDays: 7 },
|
||||||
|
{ id: 'ziele', label: 'Ziele', defaultDays: null }, // No timeframe for goals
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function PipelineConfigModal({ config, onSave, onClose }) {
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [description, setDescription] = useState('')
|
||||||
|
const [isDefault, setIsDefault] = useState(false)
|
||||||
|
const [active, setActive] = useState(true)
|
||||||
|
|
||||||
|
// Modules state: {körper: true, ernährung: false, ...}
|
||||||
|
const [modules, setModules] = useState({})
|
||||||
|
|
||||||
|
// Timeframes state: {körper: 30, ernährung: 30, ...}
|
||||||
|
const [timeframes, setTimeframes] = useState({})
|
||||||
|
|
||||||
|
// Stage prompts
|
||||||
|
const [stage1Prompts, setStage1Prompts] = useState([])
|
||||||
|
const [stage2Prompt, setStage2Prompt] = useState('')
|
||||||
|
const [stage3Prompt, setStage3Prompt] = useState('')
|
||||||
|
|
||||||
|
// Available prompts (for dropdowns)
|
||||||
|
const [availablePrompts, setAvailablePrompts] = useState([])
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAvailablePrompts()
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
// Edit mode
|
||||||
|
setName(config.name || '')
|
||||||
|
setDescription(config.description || '')
|
||||||
|
setIsDefault(config.is_default || false)
|
||||||
|
setActive(config.active ?? true)
|
||||||
|
setModules(config.modules || {})
|
||||||
|
setTimeframes(config.timeframes || {})
|
||||||
|
setStage1Prompts(config.stage1_prompts || [])
|
||||||
|
setStage2Prompt(config.stage2_prompt || '')
|
||||||
|
setStage3Prompt(config.stage3_prompt || '')
|
||||||
|
} else {
|
||||||
|
// New mode - set defaults
|
||||||
|
const defaultModules = {}
|
||||||
|
const defaultTimeframes = {}
|
||||||
|
MODULES.forEach(m => {
|
||||||
|
defaultModules[m.id] = false
|
||||||
|
if (m.defaultDays) defaultTimeframes[m.id] = m.defaultDays
|
||||||
|
})
|
||||||
|
setModules(defaultModules)
|
||||||
|
setTimeframes(defaultTimeframes)
|
||||||
|
}
|
||||||
|
}, [config])
|
||||||
|
|
||||||
|
const loadAvailablePrompts = async () => {
|
||||||
|
try {
|
||||||
|
const prompts = await api.listAdminPrompts()
|
||||||
|
setAvailablePrompts(prompts)
|
||||||
|
} catch (e) {
|
||||||
|
setError('Fehler beim Laden der Prompts: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleModule = (moduleId) => {
|
||||||
|
setModules(prev => ({ ...prev, [moduleId]: !prev[moduleId] }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTimeframe = (moduleId, days) => {
|
||||||
|
setTimeframes(prev => ({ ...prev, [moduleId]: parseInt(days) || 0 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
// Validation
|
||||||
|
if (!name.trim()) {
|
||||||
|
setError('Bitte Namen eingeben')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeModules = Object.entries(modules).filter(([_, active]) => active).map(([id]) => id)
|
||||||
|
if (activeModules.length === 0) {
|
||||||
|
setError('Mindestens ein Modul muss aktiviert sein')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stage1Prompts.length === 0) {
|
||||||
|
setError('Mindestens ein Stage-1-Prompt erforderlich')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stage2Prompt) {
|
||||||
|
setError('Stage-2-Prompt (Synthese) erforderlich')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
is_default: isDefault,
|
||||||
|
active,
|
||||||
|
modules,
|
||||||
|
timeframes,
|
||||||
|
stage1_prompts: stage1Prompts,
|
||||||
|
stage2_prompt: stage2Prompt,
|
||||||
|
stage3_prompt: stage3Prompt || null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config?.id) {
|
||||||
|
await api.updatePipelineConfig(config.id, data)
|
||||||
|
} else {
|
||||||
|
await api.createPipelineConfig(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addStage1Prompt = (slug) => {
|
||||||
|
if (slug && !stage1Prompts.includes(slug)) {
|
||||||
|
setStage1Prompts(prev => [...prev, slug])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeStage1Prompt = (slug) => {
|
||||||
|
setStage1Prompts(prev => prev.filter(s => s !== slug))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter prompts: only pipeline-type prompts
|
||||||
|
const pipelinePrompts = availablePrompts.filter(p =>
|
||||||
|
p.slug && p.slug.startsWith('pipeline_') && p.slug !== 'pipeline_synthesis' && p.slug !== 'pipeline_goals'
|
||||||
|
)
|
||||||
|
|
||||||
|
const synthesisPrompts = availablePrompts.filter(p =>
|
||||||
|
p.slug && (p.slug === 'pipeline_synthesis' || p.slug.includes('synthesis'))
|
||||||
|
)
|
||||||
|
|
||||||
|
const goalsPrompts = availablePrompts.filter(p =>
|
||||||
|
p.slug && (p.slug === 'pipeline_goals' || p.slug.includes('goals') || p.slug.includes('ziele'))
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position:'fixed', inset:0, background:'rgba(0,0,0,0.5)',
|
||||||
|
display:'flex', alignItems:'center', justifyContent:'center',
|
||||||
|
zIndex:1000, padding:20, overflow:'auto'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background:'var(--bg)', borderRadius:12, maxWidth:900, width:'100%',
|
||||||
|
maxHeight:'90vh', overflow:'auto', padding:24
|
||||||
|
}}>
|
||||||
|
<div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:24}}>
|
||||||
|
<h2 style={{margin:0, fontSize:20, fontWeight:600}}>
|
||||||
|
{config ? 'Pipeline-Konfiguration bearbeiten' : 'Neue Pipeline-Konfiguration'}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{background:'none', border:'none', cursor:'pointer', padding:4}}
|
||||||
|
>
|
||||||
|
<X size={24} color="var(--text3)" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{
|
||||||
|
padding:12, background:'#fee', color:'#c00', borderRadius:8, marginBottom:16, fontSize:13
|
||||||
|
}}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Basic Info */}
|
||||||
|
<div style={{display:'grid', gap:16, marginBottom:24}}>
|
||||||
|
<div>
|
||||||
|
<label className="form-label" style={{display:'block', marginBottom:6}}>Name *</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
placeholder="z.B. Alltags-Check"
|
||||||
|
style={{width:'100%', textAlign:'left'}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="form-label" style={{display:'block', marginBottom:6}}>Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={description}
|
||||||
|
onChange={e => setDescription(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
placeholder="Kurze Beschreibung der Pipeline"
|
||||||
|
style={{width:'100%', textAlign:'left', resize:'vertical'}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{display:'flex', gap:16, alignItems:'center'}}>
|
||||||
|
<label style={{display:'flex', alignItems:'center', gap:6, cursor:'pointer'}}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={active}
|
||||||
|
onChange={e => setActive(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span style={{fontSize:13}}>Aktiv</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style={{display:'flex', alignItems:'center', gap:6, cursor:'pointer'}}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isDefault}
|
||||||
|
onChange={e => setIsDefault(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span style={{fontSize:13}}>Als Standard-Pipeline markieren</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Module Selection */}
|
||||||
|
<div style={{marginBottom:24}}>
|
||||||
|
<h3 style={{fontSize:16, fontWeight:600, marginBottom:12}}>Module & Zeiträume</h3>
|
||||||
|
<div style={{
|
||||||
|
background:'var(--surface)', padding:16, borderRadius:8,
|
||||||
|
border:'1px solid var(--border)'
|
||||||
|
}}>
|
||||||
|
{MODULES.map(module => (
|
||||||
|
<div key={module.id} style={{
|
||||||
|
display:'flex', alignItems:'center', gap:12,
|
||||||
|
padding:'8px 0', borderBottom:'1px solid var(--border)'
|
||||||
|
}}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={modules[module.id] || false}
|
||||||
|
onChange={() => toggleModule(module.id)}
|
||||||
|
id={`module-${module.id}`}
|
||||||
|
/>
|
||||||
|
<label htmlFor={`module-${module.id}`} style={{flex:1, fontSize:13, cursor:'pointer'}}>
|
||||||
|
{module.label}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{module.defaultDays && (
|
||||||
|
<div style={{display:'flex', alignItems:'center', gap:6}}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={timeframes[module.id] || module.defaultDays}
|
||||||
|
onChange={e => updateTimeframe(module.id, e.target.value)}
|
||||||
|
disabled={!modules[module.id]}
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
style={{
|
||||||
|
width:60, padding:'4px 8px', fontSize:12,
|
||||||
|
opacity: modules[module.id] ? 1 : 0.5
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{fontSize:11, color:'var(--text3)'}}>Tage</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stage Configuration */}
|
||||||
|
<div style={{marginBottom:24}}>
|
||||||
|
<h3 style={{fontSize:16, fontWeight:600, marginBottom:12}}>Pipeline-Stufen</h3>
|
||||||
|
|
||||||
|
{/* Stage 1 */}
|
||||||
|
<div style={{marginBottom:16}}>
|
||||||
|
<label className="form-label" style={{display:'block', marginBottom:6}}>
|
||||||
|
Stage 1: Parallel-Analysen *
|
||||||
|
</label>
|
||||||
|
<div style={{fontSize:11, color:'var(--text3)', marginBottom:8}}>
|
||||||
|
Diese Prompts laufen parallel und liefern JSON-Summaries für Stage 2
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
onChange={e => { addStage1Prompt(e.target.value); e.target.value = '' }}
|
||||||
|
style={{width:'100%', marginBottom:8}}
|
||||||
|
>
|
||||||
|
<option value="">+ Prompt hinzufügen</option>
|
||||||
|
{pipelinePrompts.map(p => (
|
||||||
|
<option key={p.slug} value={p.slug} disabled={stage1Prompts.includes(p.slug)}>
|
||||||
|
{p.display_name || p.name} ({p.slug})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{stage1Prompts.length > 0 && (
|
||||||
|
<div style={{display:'flex', flexWrap:'wrap', gap:6}}>
|
||||||
|
{stage1Prompts.map(slug => (
|
||||||
|
<div key={slug} style={{
|
||||||
|
padding:'4px 8px', background:'var(--surface2)', borderRadius:6,
|
||||||
|
fontSize:11, display:'flex', alignItems:'center', gap:6
|
||||||
|
}}>
|
||||||
|
<span>{slug}</span>
|
||||||
|
<X
|
||||||
|
size={14}
|
||||||
|
onClick={() => removeStage1Prompt(slug)}
|
||||||
|
style={{cursor:'pointer', color:'var(--danger)'}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stage 2 */}
|
||||||
|
<div style={{marginBottom:16}}>
|
||||||
|
<label className="form-label" style={{display:'block', marginBottom:6}}>
|
||||||
|
Stage 2: Synthese *
|
||||||
|
</label>
|
||||||
|
<div style={{fontSize:11, color:'var(--text3)', marginBottom:8}}>
|
||||||
|
Kombiniert alle Stage-1-Ergebnisse zu einer narrativen Analyse
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={stage2Prompt}
|
||||||
|
onChange={e => setStage2Prompt(e.target.value)}
|
||||||
|
style={{width:'100%'}}
|
||||||
|
>
|
||||||
|
<option value="">-- Bitte wählen --</option>
|
||||||
|
{synthesisPrompts.map(p => (
|
||||||
|
<option key={p.slug} value={p.slug}>
|
||||||
|
{p.display_name || p.name} ({p.slug})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stage 3 */}
|
||||||
|
<div>
|
||||||
|
<label className="form-label" style={{display:'block', marginBottom:6}}>
|
||||||
|
Stage 3: Optional (z.B. Zielabgleich)
|
||||||
|
</label>
|
||||||
|
<div style={{fontSize:11, color:'var(--text3)', marginBottom:8}}>
|
||||||
|
Optional: Zusätzliche Auswertung basierend auf Synthese
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={stage3Prompt}
|
||||||
|
onChange={e => setStage3Prompt(e.target.value)}
|
||||||
|
style={{width:'100%'}}
|
||||||
|
>
|
||||||
|
<option value="">-- Keine --</option>
|
||||||
|
{goalsPrompts.map(p => (
|
||||||
|
<option key={p.slug} value={p.slug}>
|
||||||
|
{p.display_name || p.name} ({p.slug})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div style={{display:'flex', gap:12, justifyContent:'flex-end', paddingTop:16, borderTop:'1px solid var(--border)'}}>
|
||||||
|
<button className="btn" onClick={onClose}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? 'Speichern...' : 'Speichern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { api } from '../utils/api'
|
import { api } from '../utils/api'
|
||||||
import PromptEditModal from '../components/PromptEditModal'
|
import PromptEditModal from '../components/PromptEditModal'
|
||||||
|
import PipelineConfigModal from '../components/PipelineConfigModal'
|
||||||
|
import { Star, Trash2, Edit, Copy } from 'lucide-react'
|
||||||
|
|
||||||
export default function AdminPromptsPage() {
|
export default function AdminPromptsPage() {
|
||||||
|
const [activeTab, setActiveTab] = useState('prompts') // 'prompts' | 'pipelines'
|
||||||
|
|
||||||
|
// Prompts state
|
||||||
const [prompts, setPrompts] = useState([])
|
const [prompts, setPrompts] = useState([])
|
||||||
const [filteredPrompts, setFilteredPrompts] = useState([])
|
const [filteredPrompts, setFilteredPrompts] = useState([])
|
||||||
const [category, setCategory] = useState('all')
|
const [category, setCategory] = useState('all')
|
||||||
|
|
@ -11,6 +16,12 @@ export default function AdminPromptsPage() {
|
||||||
const [editingPrompt, setEditingPrompt] = useState(null)
|
const [editingPrompt, setEditingPrompt] = useState(null)
|
||||||
const [showNewPrompt, setShowNewPrompt] = useState(false)
|
const [showNewPrompt, setShowNewPrompt] = useState(false)
|
||||||
|
|
||||||
|
// Pipeline configs state
|
||||||
|
const [pipelineConfigs, setPipelineConfigs] = useState([])
|
||||||
|
const [pipelineLoading, setPipelineLoading] = useState(false)
|
||||||
|
const [editingPipeline, setEditingPipeline] = useState(null)
|
||||||
|
const [showNewPipeline, setShowNewPipeline] = useState(false)
|
||||||
|
|
||||||
const categories = [
|
const categories = [
|
||||||
{ id: 'all', label: 'Alle Kategorien' },
|
{ id: 'all', label: 'Alle Kategorien' },
|
||||||
{ id: 'körper', label: 'Körper' },
|
{ id: 'körper', label: 'Körper' },
|
||||||
|
|
@ -24,6 +35,7 @@ export default function AdminPromptsPage() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadPrompts()
|
loadPrompts()
|
||||||
|
loadPipelineConfigs()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -116,6 +128,45 @@ export default function AdminPromptsPage() {
|
||||||
setShowNewPrompt(false)
|
setShowNewPrompt(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pipeline Config handlers
|
||||||
|
const loadPipelineConfigs = async () => {
|
||||||
|
try {
|
||||||
|
setPipelineLoading(true)
|
||||||
|
const data = await api.listPipelineConfigs()
|
||||||
|
setPipelineConfigs(data)
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
} finally {
|
||||||
|
setPipelineLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeletePipeline = async (config) => {
|
||||||
|
if (!confirm(`Pipeline-Config "${config.name}" wirklich löschen?`)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.deletePipelineConfig(config.id)
|
||||||
|
await loadPipelineConfigs()
|
||||||
|
} catch (e) {
|
||||||
|
alert('Fehler: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetDefaultPipeline = async (config) => {
|
||||||
|
try {
|
||||||
|
await api.setDefaultPipelineConfig(config.id)
|
||||||
|
await loadPipelineConfigs()
|
||||||
|
} catch (e) {
|
||||||
|
alert('Fehler: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSavePipeline = async () => {
|
||||||
|
await loadPipelineConfigs()
|
||||||
|
setEditingPipeline(null)
|
||||||
|
setShowNewPipeline(false)
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
|
|
@ -129,12 +180,38 @@ export default function AdminPromptsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:24}}>
|
<div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:24}}>
|
||||||
<h1 className="page-title">KI-Prompts Verwaltung</h1>
|
<h1 className="page-title">KI-Prompts & Pipelines</h1>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={() => setShowNewPrompt(true)}
|
onClick={() => activeTab === 'prompts' ? setShowNewPrompt(true) : setShowNewPipeline(true)}
|
||||||
>
|
>
|
||||||
+ Neuer Prompt
|
{activeTab === 'prompts' ? '+ Neuer Prompt' : '+ Neue Pipeline'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Switcher */}
|
||||||
|
<div style={{display:'flex', gap:12, marginBottom:24, borderBottom:'2px solid var(--border)'}}>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('prompts')}
|
||||||
|
style={{
|
||||||
|
padding:'12px 24px', background:'none', border:'none',
|
||||||
|
borderBottom: activeTab === 'prompts' ? '2px solid var(--accent)' : '2px solid transparent',
|
||||||
|
marginBottom:-2, cursor:'pointer', fontSize:14, fontWeight:activeTab === 'prompts' ? 600 : 400,
|
||||||
|
color: activeTab === 'prompts' ? 'var(--accent)' : 'var(--text2)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Prompts ({prompts.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('pipelines')}
|
||||||
|
style={{
|
||||||
|
padding:'12px 24px', background:'none', border:'none',
|
||||||
|
borderBottom: activeTab === 'pipelines' ? '2px solid var(--accent)' : '2px solid transparent',
|
||||||
|
marginBottom:-2, cursor:'pointer', fontSize:14, fontWeight:activeTab === 'pipelines' ? 600 : 400,
|
||||||
|
color: activeTab === 'pipelines' ? 'var(--accent)' : 'var(--text2)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Pipeline-Konfigurationen ({pipelineConfigs.length})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -144,6 +221,9 @@ export default function AdminPromptsPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Prompts Tab */}
|
||||||
|
{activeTab === 'prompts' && (
|
||||||
|
<>
|
||||||
{/* Category Filter */}
|
{/* Category Filter */}
|
||||||
<div style={{marginBottom:24}}>
|
<div style={{marginBottom:24}}>
|
||||||
<label style={{fontSize:13, fontWeight:600, marginBottom:8, display:'block'}}>
|
<label style={{fontSize:13, fontWeight:600, marginBottom:8, display:'block'}}>
|
||||||
|
|
@ -276,8 +356,119 @@ export default function AdminPromptsPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Edit Modal */}
|
{/* Pipelines Tab */}
|
||||||
|
{activeTab === 'pipelines' && (
|
||||||
|
<>
|
||||||
|
{pipelineLoading ? (
|
||||||
|
<div style={{textAlign:'center', padding:40}}>
|
||||||
|
<div className="spinner"/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="card">
|
||||||
|
<table style={{width:'100%', borderCollapse:'collapse'}}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{borderBottom:'2px solid var(--border)', textAlign:'left'}}>
|
||||||
|
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>Name</th>
|
||||||
|
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>Module</th>
|
||||||
|
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>Stages</th>
|
||||||
|
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>Status</th>
|
||||||
|
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{pipelineConfigs.map(config => {
|
||||||
|
const activeModules = Object.entries(config.modules || {}).filter(([_, active]) => active)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={config.id} style={{borderBottom:'1px solid var(--border)'}}>
|
||||||
|
<td style={{padding:'12px 8px'}}>
|
||||||
|
<div style={{fontWeight:500, fontSize:14}}>{config.name}</div>
|
||||||
|
{config.is_default && (
|
||||||
|
<span style={{
|
||||||
|
fontSize:10, padding:'2px 6px', background:'var(--accent)',
|
||||||
|
color:'white', borderRadius:3, marginTop:4, display:'inline-block'
|
||||||
|
}}>
|
||||||
|
Standard
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{config.description && (
|
||||||
|
<div style={{fontSize:11, color:'var(--text3)', marginTop:2}}>
|
||||||
|
{config.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td style={{padding:'12px 8px'}}>
|
||||||
|
<div style={{fontSize:11}}>
|
||||||
|
{activeModules.map(([name]) => name).join(', ')}
|
||||||
|
</div>
|
||||||
|
<div style={{fontSize:10, color:'var(--text3)', marginTop:2}}>
|
||||||
|
{activeModules.length} Module
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style={{padding:'12px 8px'}}>
|
||||||
|
<div style={{fontSize:11}}>
|
||||||
|
S1: {config.stage1_prompts?.length || 0} Prompts
|
||||||
|
</div>
|
||||||
|
<div style={{fontSize:10, color:'var(--text3)'}}>
|
||||||
|
S2: {config.stage2_prompt ? '✓' : '-'} | S3: {config.stage3_prompt ? '✓' : '-'}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style={{padding:'12px 8px'}}>
|
||||||
|
<span style={{
|
||||||
|
fontSize:11, padding:'3px 8px', borderRadius:4,
|
||||||
|
background: config.active ? 'var(--accent-light)' : 'var(--surface2)',
|
||||||
|
color: config.active ? 'var(--accent)' : 'var(--text3)'
|
||||||
|
}}>
|
||||||
|
{config.active ? 'Aktiv' : 'Inaktiv'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style={{padding:'12px 8px'}}>
|
||||||
|
<div style={{display:'flex', gap:6}}>
|
||||||
|
{!config.is_default && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleSetDefaultPipeline(config)}
|
||||||
|
style={{padding:'4px 8px', fontSize:11}}
|
||||||
|
title="Als Standard setzen"
|
||||||
|
>
|
||||||
|
<Star size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingPipeline(config)}
|
||||||
|
style={{padding:'4px 8px', fontSize:11}}
|
||||||
|
title="Bearbeiten"
|
||||||
|
>
|
||||||
|
<Edit size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeletePipeline(config)}
|
||||||
|
style={{padding:'4px 8px', fontSize:11, color:'var(--danger)'}}
|
||||||
|
title="Löschen"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{pipelineConfigs.length === 0 && (
|
||||||
|
<div style={{padding:40, textAlign:'center', color:'var(--text3)'}}>
|
||||||
|
Noch keine Pipeline-Konfigurationen vorhanden
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Edit Modals */}
|
||||||
{(editingPrompt || showNewPrompt) && (
|
{(editingPrompt || showNewPrompt) && (
|
||||||
<PromptEditModal
|
<PromptEditModal
|
||||||
prompt={editingPrompt}
|
prompt={editingPrompt}
|
||||||
|
|
@ -288,6 +479,17 @@ export default function AdminPromptsPage() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(editingPipeline || showNewPipeline) && (
|
||||||
|
<PipelineConfigModal
|
||||||
|
config={editingPipeline}
|
||||||
|
onSave={handleSavePipeline}
|
||||||
|
onClose={() => {
|
||||||
|
setEditingPipeline(null)
|
||||||
|
setShowNewPipeline(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -294,4 +294,15 @@ export const api = {
|
||||||
generatePrompt: (d) => req('/prompts/generate', json(d)),
|
generatePrompt: (d) => req('/prompts/generate', json(d)),
|
||||||
optimizePrompt: (id) => req(`/prompts/${id}/optimize`, json({})),
|
optimizePrompt: (id) => req(`/prompts/${id}/optimize`, json({})),
|
||||||
listPlaceholders: () => req('/prompts/placeholders'),
|
listPlaceholders: () => req('/prompts/placeholders'),
|
||||||
|
resetPromptToDefault: (id) => req(`/prompts/${id}/reset-to-default`, json({})),
|
||||||
|
|
||||||
|
// Pipeline Configs Management (Issue #28 Phase 2)
|
||||||
|
listPipelineConfigs: () => req('/prompts/pipeline-configs'),
|
||||||
|
createPipelineConfig: (d) => req('/prompts/pipeline-configs', json(d)),
|
||||||
|
updatePipelineConfig: (id,d) => req(`/prompts/pipeline-configs/${id}`, jput(d)),
|
||||||
|
deletePipelineConfig: (id) => req(`/prompts/pipeline-configs/${id}`, {method:'DELETE'}),
|
||||||
|
setDefaultPipelineConfig: (id) => req(`/prompts/pipeline-configs/${id}/set-default`, json({})),
|
||||||
|
|
||||||
|
// Pipeline Execution (Issue #28 Phase 2)
|
||||||
|
executePipeline: (configId=null) => req('/insights/pipeline' + (configId ? `?config_id=${configId}` : ''), json({})),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user