feat: unified prompt UI - Phase 3 complete (Issue #28)
Frontend Consolidation: - UnifiedPromptModal: Single editor for base + pipeline prompts - Type selector (base/pipeline) - Base: Template editor with placeholders - Pipeline: Dynamic stage editor - Add/remove stages with drag/drop - Inline or reference prompts per stage - Output key + format per prompt - AdminPromptsPage redesign: - Removed tab switcher (prompts/pipelines) - Added type filter (All/Base/Pipeline) - Type badge in table - Stage count column - Icon-based actions (Edit/Copy/Delete) - Category filter retained Changes: - Completely rewrote AdminPromptsPage (495 → 446 lines) - Single modal for all prompt types - Mobile-ready layout - Simplified state management Next: Phase 4 - Cleanup deprecated endpoints + docs
This commit is contained in:
parent
7be7266477
commit
31e2c24a8a
549
frontend/src/components/UnifiedPromptModal.jsx
Normal file
549
frontend/src/components/UnifiedPromptModal.jsx
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { api } from '../utils/api'
|
||||
import { X, Plus, Trash2, MoveUp, MoveDown } from 'lucide-react'
|
||||
|
||||
/**
|
||||
* Unified Prompt Editor Modal (Issue #28 Phase 3)
|
||||
*
|
||||
* Supports both prompt types:
|
||||
* - Base: Single reusable template
|
||||
* - Pipeline: Multi-stage workflow with dynamic stages
|
||||
*/
|
||||
export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
|
||||
const [name, setName] = useState('')
|
||||
const [slug, setSlug] = useState('')
|
||||
const [displayName, setDisplayName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [type, setType] = useState('pipeline') // 'base' or 'pipeline'
|
||||
const [category, setCategory] = useState('ganzheitlich')
|
||||
const [active, setActive] = useState(true)
|
||||
const [sortOrder, setSortOrder] = useState(0)
|
||||
|
||||
// Base prompt fields
|
||||
const [template, setTemplate] = useState('')
|
||||
const [outputFormat, setOutputFormat] = useState('text')
|
||||
|
||||
// Pipeline prompt fields
|
||||
const [stages, setStages] = useState([
|
||||
{
|
||||
stage: 1,
|
||||
prompts: []
|
||||
}
|
||||
])
|
||||
|
||||
// Available prompts for reference selection
|
||||
const [availablePrompts, setAvailablePrompts] = useState([])
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadAvailablePrompts()
|
||||
|
||||
if (prompt) {
|
||||
// Edit mode
|
||||
setName(prompt.name || '')
|
||||
setSlug(prompt.slug || '')
|
||||
setDisplayName(prompt.display_name || '')
|
||||
setDescription(prompt.description || '')
|
||||
setType(prompt.type || 'pipeline')
|
||||
setCategory(prompt.category || 'ganzheitlich')
|
||||
setActive(prompt.active ?? true)
|
||||
setSortOrder(prompt.sort_order || 0)
|
||||
setTemplate(prompt.template || '')
|
||||
setOutputFormat(prompt.output_format || 'text')
|
||||
|
||||
// Parse stages if editing pipeline
|
||||
if (prompt.type === 'pipeline' && prompt.stages) {
|
||||
try {
|
||||
const parsedStages = typeof prompt.stages === 'string'
|
||||
? JSON.parse(prompt.stages)
|
||||
: prompt.stages
|
||||
setStages(parsedStages.length > 0 ? parsedStages : [{ stage: 1, prompts: [] }])
|
||||
} catch (e) {
|
||||
console.error('Failed to parse stages:', e)
|
||||
setStages([{ stage: 1, prompts: [] }])
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [prompt])
|
||||
|
||||
const loadAvailablePrompts = async () => {
|
||||
try {
|
||||
const prompts = await api.listAdminPrompts()
|
||||
setAvailablePrompts(prompts)
|
||||
} catch (e) {
|
||||
setError('Fehler beim Laden der Prompts: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const addStage = () => {
|
||||
const nextStageNum = Math.max(...stages.map(s => s.stage), 0) + 1
|
||||
setStages([...stages, { stage: nextStageNum, prompts: [] }])
|
||||
}
|
||||
|
||||
const removeStage = (stageNum) => {
|
||||
if (stages.length === 1) {
|
||||
setError('Mindestens eine Stage erforderlich')
|
||||
return
|
||||
}
|
||||
setStages(stages.filter(s => s.stage !== stageNum))
|
||||
}
|
||||
|
||||
const moveStage = (stageNum, direction) => {
|
||||
const idx = stages.findIndex(s => s.stage === stageNum)
|
||||
if (idx === -1) return
|
||||
|
||||
const newStages = [...stages]
|
||||
if (direction === 'up' && idx > 0) {
|
||||
[newStages[idx], newStages[idx - 1]] = [newStages[idx - 1], newStages[idx]]
|
||||
} else if (direction === 'down' && idx < newStages.length - 1) {
|
||||
[newStages[idx], newStages[idx + 1]] = [newStages[idx + 1], newStages[idx]]
|
||||
}
|
||||
|
||||
// Renumber stages
|
||||
newStages.forEach((s, i) => s.stage = i + 1)
|
||||
setStages(newStages)
|
||||
}
|
||||
|
||||
const addPromptToStage = (stageNum) => {
|
||||
setStages(stages.map(s => {
|
||||
if (s.stage === stageNum) {
|
||||
return {
|
||||
...s,
|
||||
prompts: [...s.prompts, {
|
||||
source: 'inline',
|
||||
template: '',
|
||||
output_key: `output_${Date.now()}`,
|
||||
output_format: 'text'
|
||||
}]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}))
|
||||
}
|
||||
|
||||
const removePromptFromStage = (stageNum, promptIdx) => {
|
||||
setStages(stages.map(s => {
|
||||
if (s.stage === stageNum) {
|
||||
return {
|
||||
...s,
|
||||
prompts: s.prompts.filter((_, i) => i !== promptIdx)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}))
|
||||
}
|
||||
|
||||
const updateStagePrompt = (stageNum, promptIdx, field, value) => {
|
||||
setStages(stages.map(s => {
|
||||
if (s.stage === stageNum) {
|
||||
const newPrompts = [...s.prompts]
|
||||
newPrompts[promptIdx] = { ...newPrompts[promptIdx], [field]: value }
|
||||
return { ...s, prompts: newPrompts }
|
||||
}
|
||||
return s
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
// Validation
|
||||
if (!name.trim() || !slug.trim()) {
|
||||
setError('Name und Slug sind Pflichtfelder')
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'base' && !template.trim()) {
|
||||
setError('Basis-Prompts benötigen ein Template')
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'pipeline' && stages.length === 0) {
|
||||
setError('Pipeline-Prompts benötigen mindestens eine Stage')
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'pipeline') {
|
||||
// Validate all stages have at least one prompt
|
||||
const emptyStages = stages.filter(s => s.prompts.length === 0)
|
||||
if (emptyStages.length > 0) {
|
||||
setError(`Stage ${emptyStages[0].stage} hat keine Prompts`)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate all prompts have required fields
|
||||
for (const stage of stages) {
|
||||
for (const p of stage.prompts) {
|
||||
if (!p.output_key) {
|
||||
setError(`Stage ${stage.stage}: Output-Key fehlt`)
|
||||
return
|
||||
}
|
||||
if (p.source === 'inline' && !p.template) {
|
||||
setError(`Stage ${stage.stage}: Inline-Prompt ohne Template`)
|
||||
return
|
||||
}
|
||||
if (p.source === 'reference' && !p.slug) {
|
||||
setError(`Stage ${stage.stage}: Referenz-Prompt ohne Slug`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const data = {
|
||||
name,
|
||||
slug,
|
||||
display_name: displayName,
|
||||
description,
|
||||
type,
|
||||
category,
|
||||
active,
|
||||
sort_order: sortOrder,
|
||||
output_format: outputFormat
|
||||
}
|
||||
|
||||
if (type === 'base') {
|
||||
data.template = template
|
||||
data.stages = null
|
||||
} else {
|
||||
data.template = null
|
||||
data.stages = stages
|
||||
}
|
||||
|
||||
if (prompt?.id) {
|
||||
await api.updateUnifiedPrompt(prompt.id, data)
|
||||
} else {
|
||||
await api.createUnifiedPrompt(data)
|
||||
}
|
||||
|
||||
onSave()
|
||||
} catch (e) {
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
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: 1000, 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 }}>
|
||||
{prompt ? 'Prompt bearbeiten' : 'Neuer Prompt'}
|
||||
</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 style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<div>
|
||||
<label className="form-label">Name *</label>
|
||||
<input
|
||||
className="form-input"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="Interner Name"
|
||||
style={{ width: '100%', textAlign: 'left' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="form-label">Slug *</label>
|
||||
<input
|
||||
className="form-input"
|
||||
value={slug}
|
||||
onChange={e => setSlug(e.target.value)}
|
||||
placeholder="technischer_name"
|
||||
style={{ width: '100%', textAlign: 'left' }}
|
||||
disabled={!!prompt}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="form-label">Anzeigename</label>
|
||||
<input
|
||||
className="form-input"
|
||||
value={displayName}
|
||||
onChange={e => setDisplayName(e.target.value)}
|
||||
placeholder="Name für Benutzer (optional)"
|
||||
style={{ width: '100%', textAlign: 'left' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="form-label">Beschreibung</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
rows={2}
|
||||
placeholder="Kurze Beschreibung des Prompts"
|
||||
style={{ width: '100%', textAlign: 'left', resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
|
||||
<div>
|
||||
<label className="form-label">Typ *</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={type}
|
||||
onChange={e => setType(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
disabled={!!prompt}
|
||||
>
|
||||
<option value="base">Basis-Prompt</option>
|
||||
<option value="pipeline">Pipeline</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="form-label">Kategorie</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={category}
|
||||
onChange={e => setCategory(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<option value="ganzheitlich">Ganzheitlich</option>
|
||||
<option value="körper">Körper</option>
|
||||
<option value="ernährung">Ernährung</option>
|
||||
<option value="training">Training</option>
|
||||
<option value="schlaf">Schlaf</option>
|
||||
<option value="pipeline">Pipeline</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="form-label">Output-Format</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={outputFormat}
|
||||
onChange={e => setOutputFormat(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<option value="text">Text</option>
|
||||
<option value="json">JSON</option>
|
||||
</select>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<label className="form-label" style={{ margin: 0 }}>Sortierung:</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
value={sortOrder}
|
||||
onChange={e => setSortOrder(parseInt(e.target.value) || 0)}
|
||||
style={{ width: 80, padding: '4px 8px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Type-specific editor */}
|
||||
{type === 'base' && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 12 }}>Template</h3>
|
||||
<textarea
|
||||
className="form-input"
|
||||
value={template}
|
||||
onChange={e => setTemplate(e.target.value)}
|
||||
rows={12}
|
||||
placeholder="Prompt-Template mit {{placeholders}}..."
|
||||
style={{ width: '100%', textAlign: 'left', resize: 'vertical', fontFamily: 'monospace', fontSize: 12 }}
|
||||
/>
|
||||
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 4 }}>
|
||||
Verwende {{'{{'}}platzhalter{{'}}'}} für dynamische Werte (z.B. {{'{{'}}weight_data{{'}}'}}, {{'{{'}}nutrition_data{{'}}'}})
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === 'pipeline' && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>Stages ({stages.length})</h3>
|
||||
<button className="btn" onClick={addStage} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Plus size={16} /> Stage hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{stages.map((stage, sIdx) => (
|
||||
<div key={stage.stage} style={{
|
||||
background: 'var(--surface)', padding: 16, borderRadius: 8,
|
||||
border: '1px solid var(--border)', marginBottom: 12
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||
<h4 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>Stage {stage.stage}</h4>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{sIdx > 0 && (
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => moveStage(stage.stage, 'up')}
|
||||
style={{ padding: '4px 8px' }}
|
||||
>
|
||||
<MoveUp size={14} />
|
||||
</button>
|
||||
)}
|
||||
{sIdx < stages.length - 1 && (
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => moveStage(stage.stage, 'down')}
|
||||
style={{ padding: '4px 8px' }}
|
||||
>
|
||||
<MoveDown size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => removeStage(stage.stage)}
|
||||
style={{ padding: '4px 8px', color: 'var(--danger)' }}
|
||||
disabled={stages.length === 1}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prompts in this stage */}
|
||||
{stage.prompts.map((p, pIdx) => (
|
||||
<div key={pIdx} style={{
|
||||
background: 'var(--bg)', padding: 12, borderRadius: 6, marginBottom: 8,
|
||||
border: '1px solid var(--border)'
|
||||
}}>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr 120px auto', gap: 8, alignItems: 'center' }}>
|
||||
<select
|
||||
className="form-select"
|
||||
value={p.source || 'inline'}
|
||||
onChange={e => updateStagePrompt(stage.stage, pIdx, 'source', e.target.value)}
|
||||
style={{ fontSize: 12 }}
|
||||
>
|
||||
<option value="inline">Inline</option>
|
||||
<option value="reference">Referenz</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
className="form-input"
|
||||
value={p.output_key || ''}
|
||||
onChange={e => updateStagePrompt(stage.stage, pIdx, 'output_key', e.target.value)}
|
||||
placeholder="output_key"
|
||||
style={{ fontSize: 12, textAlign: 'left' }}
|
||||
/>
|
||||
|
||||
<select
|
||||
className="form-select"
|
||||
value={p.output_format || 'text'}
|
||||
onChange={e => updateStagePrompt(stage.stage, pIdx, 'output_format', e.target.value)}
|
||||
style={{ fontSize: 12 }}
|
||||
>
|
||||
<option value="text">Text</option>
|
||||
<option value="json">JSON</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={() => removePromptFromStage(stage.stage, pIdx)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4 }}
|
||||
>
|
||||
<Trash2 size={16} color="var(--danger)" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{p.source === 'reference' ? (
|
||||
<select
|
||||
className="form-select"
|
||||
value={p.slug || ''}
|
||||
onChange={e => updateStagePrompt(stage.stage, pIdx, 'slug', e.target.value)}
|
||||
style={{ fontSize: 12 }}
|
||||
>
|
||||
<option value="">-- Prompt wählen --</option>
|
||||
{availablePrompts
|
||||
.filter(ap => ap.type === 'base' || !ap.type)
|
||||
.map(ap => (
|
||||
<option key={ap.slug} value={ap.slug}>
|
||||
{ap.display_name || ap.name} ({ap.slug})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<textarea
|
||||
className="form-input"
|
||||
value={p.template || ''}
|
||||
onChange={e => updateStagePrompt(stage.stage, pIdx, 'template', e.target.value)}
|
||||
rows={3}
|
||||
placeholder="Inline-Template mit {{placeholders}}..."
|
||||
style={{ fontSize: 12, textAlign: 'left', resize: 'vertical', fontFamily: 'monospace' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => addPromptToStage(stage.stage)}
|
||||
style={{ fontSize: 12, display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
<Plus size={14} /> Prompt hinzufügen
|
||||
</button>
|
||||
</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,27 +1,23 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { api } from '../utils/api'
|
||||
import PromptEditModal from '../components/PromptEditModal'
|
||||
import PipelineConfigModal from '../components/PipelineConfigModal'
|
||||
import { Star, Trash2, Edit, Copy } from 'lucide-react'
|
||||
import UnifiedPromptModal from '../components/UnifiedPromptModal'
|
||||
import { Star, Trash2, Edit, Copy, Filter } from 'lucide-react'
|
||||
|
||||
/**
|
||||
* Admin Prompts Page - Unified System (Issue #28 Phase 3)
|
||||
*
|
||||
* Manages both base and pipeline-type prompts in one interface.
|
||||
*/
|
||||
export default function AdminPromptsPage() {
|
||||
const [activeTab, setActiveTab] = useState('prompts') // 'prompts' | 'pipelines'
|
||||
|
||||
// Prompts state
|
||||
const [prompts, setPrompts] = useState([])
|
||||
const [filteredPrompts, setFilteredPrompts] = useState([])
|
||||
const [typeFilter, setTypeFilter] = useState('all') // 'all' | 'base' | 'pipeline'
|
||||
const [category, setCategory] = useState('all')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [editingPrompt, setEditingPrompt] = useState(null)
|
||||
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 = [
|
||||
{ id: 'all', label: 'Alle Kategorien' },
|
||||
{ id: 'körper', label: 'Körper' },
|
||||
|
|
@ -30,21 +26,31 @@ export default function AdminPromptsPage() {
|
|||
{ id: 'schlaf', label: 'Schlaf' },
|
||||
{ id: 'vitalwerte', label: 'Vitalwerte' },
|
||||
{ id: 'ziele', label: 'Ziele' },
|
||||
{ id: 'ganzheitlich', label: 'Ganzheitlich' }
|
||||
{ id: 'ganzheitlich', label: 'Ganzheitlich' },
|
||||
{ id: 'pipeline', label: 'Pipeline' }
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
loadPrompts()
|
||||
loadPipelineConfigs()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (category === 'all') {
|
||||
setFilteredPrompts(prompts)
|
||||
} else {
|
||||
setFilteredPrompts(prompts.filter(p => p.category === category))
|
||||
let filtered = prompts
|
||||
|
||||
// Filter by type
|
||||
if (typeFilter === 'base') {
|
||||
filtered = filtered.filter(p => p.type === 'base')
|
||||
} else if (typeFilter === 'pipeline') {
|
||||
filtered = filtered.filter(p => p.type === 'pipeline')
|
||||
}
|
||||
}, [category, prompts])
|
||||
|
||||
// Filter by category
|
||||
if (category !== 'all') {
|
||||
filtered = filtered.filter(p => p.category === category)
|
||||
}
|
||||
|
||||
setFilteredPrompts(filtered)
|
||||
}, [typeFilter, category, prompts])
|
||||
|
||||
const loadPrompts = async () => {
|
||||
try {
|
||||
|
|
@ -61,7 +67,7 @@ export default function AdminPromptsPage() {
|
|||
|
||||
const handleToggleActive = async (prompt) => {
|
||||
try {
|
||||
await api.updatePrompt(prompt.id, { active: !prompt.active })
|
||||
await api.updateUnifiedPrompt(prompt.id, { active: !prompt.active })
|
||||
await loadPrompts()
|
||||
} catch (e) {
|
||||
alert('Fehler: ' + e.message)
|
||||
|
|
@ -88,408 +94,345 @@ export default function AdminPromptsPage() {
|
|||
}
|
||||
}
|
||||
|
||||
const handleMoveUp = async (prompt) => {
|
||||
const idx = filteredPrompts.findIndex(p => p.id === prompt.id)
|
||||
if (idx === 0) return // Already at top
|
||||
|
||||
const above = filteredPrompts[idx - 1]
|
||||
const newOrder = filteredPrompts.map(p => p.id)
|
||||
newOrder[idx] = above.id
|
||||
newOrder[idx - 1] = prompt.id
|
||||
|
||||
try {
|
||||
await api.reorderPrompts(newOrder)
|
||||
await loadPrompts()
|
||||
} catch (e) {
|
||||
alert('Fehler: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMoveDown = async (prompt) => {
|
||||
const idx = filteredPrompts.findIndex(p => p.id === prompt.id)
|
||||
if (idx === filteredPrompts.length - 1) return // Already at bottom
|
||||
|
||||
const below = filteredPrompts[idx + 1]
|
||||
const newOrder = filteredPrompts.map(p => p.id)
|
||||
newOrder[idx] = below.id
|
||||
newOrder[idx + 1] = prompt.id
|
||||
|
||||
try {
|
||||
await api.reorderPrompts(newOrder)
|
||||
await loadPrompts()
|
||||
} catch (e) {
|
||||
alert('Fehler: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSavePrompt = async () => {
|
||||
await loadPrompts()
|
||||
const handleSave = async () => {
|
||||
setEditingPrompt(null)
|
||||
setShowNewPrompt(false)
|
||||
await loadPrompts()
|
||||
}
|
||||
|
||||
// Pipeline Config handlers
|
||||
const loadPipelineConfigs = async () => {
|
||||
const getStageCount = (prompt) => {
|
||||
if (prompt.type !== 'pipeline' || !prompt.stages) return 0
|
||||
try {
|
||||
setPipelineLoading(true)
|
||||
const data = await api.listPipelineConfigs()
|
||||
setPipelineConfigs(data)
|
||||
const stages = typeof prompt.stages === 'string'
|
||||
? JSON.parse(prompt.stages)
|
||||
: prompt.stages
|
||||
return stages.length
|
||||
} catch (e) {
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setPipelineLoading(false)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
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 getTypeLabel = (type) => {
|
||||
if (type === 'base') return 'Basis'
|
||||
if (type === 'pipeline') return 'Pipeline'
|
||||
return type || 'Pipeline' // Default for old prompts
|
||||
}
|
||||
|
||||
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) {
|
||||
return (
|
||||
<div className="page">
|
||||
<div style={{textAlign:'center', padding:40}}>
|
||||
<div className="spinner"/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
const getTypeColor = (type) => {
|
||||
if (type === 'base') return 'var(--accent)'
|
||||
if (type === 'pipeline') return '#6366f1'
|
||||
return 'var(--text3)'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:24}}>
|
||||
<h1 className="page-title">KI-Prompts & Pipelines</h1>
|
||||
<div style={{
|
||||
padding: 20,
|
||||
maxWidth: 1400,
|
||||
margin: '0 auto',
|
||||
paddingBottom: 80
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 24
|
||||
}}>
|
||||
<h1 style={{ margin: 0, fontSize: 24, fontWeight: 600 }}>
|
||||
KI-Prompts ({filteredPrompts.length})
|
||||
</h1>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => activeTab === 'prompts' ? setShowNewPrompt(true) : setShowNewPipeline(true)}
|
||||
onClick={() => setShowNewPrompt(true)}
|
||||
>
|
||||
{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})
|
||||
+ Neuer Prompt
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{padding:12, background:'#fee', color:'#c00', borderRadius:8, marginBottom:16}}>
|
||||
<div style={{
|
||||
padding: 16,
|
||||
background: '#fee',
|
||||
color: '#c00',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prompts Tab */}
|
||||
{activeTab === 'prompts' && (
|
||||
<>
|
||||
{/* Category Filter */}
|
||||
<div style={{marginBottom:24}}>
|
||||
<label style={{fontSize:13, fontWeight:600, marginBottom:8, display:'block'}}>
|
||||
Kategorie filtern:
|
||||
</label>
|
||||
{/* Filters */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
marginBottom: 24,
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Filter size={16} color="var(--text3)" />
|
||||
<span style={{ fontSize: 13, color: 'var(--text3)' }}>Typ:</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<button
|
||||
className={typeFilter === 'all' ? 'btn btn-primary' : 'btn'}
|
||||
onClick={() => setTypeFilter('all')}
|
||||
style={{ fontSize: 13, padding: '6px 12px' }}
|
||||
>
|
||||
Alle ({prompts.length})
|
||||
</button>
|
||||
<button
|
||||
className={typeFilter === 'base' ? 'btn btn-primary' : 'btn'}
|
||||
onClick={() => setTypeFilter('base')}
|
||||
style={{ fontSize: 13, padding: '6px 12px' }}
|
||||
>
|
||||
Basis-Prompts ({prompts.filter(p => p.type === 'base').length})
|
||||
</button>
|
||||
<button
|
||||
className={typeFilter === 'pipeline' ? 'btn btn-primary' : 'btn'}
|
||||
onClick={() => setTypeFilter('pipeline')}
|
||||
style={{ fontSize: 13, padding: '6px 12px' }}
|
||||
>
|
||||
Pipelines ({prompts.filter(p => p.type === 'pipeline' || !p.type).length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
width: 1,
|
||||
height: 24,
|
||||
background: 'var(--border)',
|
||||
margin: '0 8px'
|
||||
}} />
|
||||
|
||||
<select
|
||||
className="form-select"
|
||||
value={category}
|
||||
onChange={e => setCategory(e.target.value)}
|
||||
style={{maxWidth:300}}
|
||||
style={{ fontSize: 13, padding: '6px 12px' }}
|
||||
>
|
||||
{categories.map(cat => (
|
||||
<option key={cat.id} value={cat.id}>{cat.label}</option>
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{cat.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Prompts Table */}
|
||||
<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)'}}>
|
||||
Titel
|
||||
</th>
|
||||
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>
|
||||
Kategorie
|
||||
</th>
|
||||
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>
|
||||
Aktiv
|
||||
</th>
|
||||
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>
|
||||
Reihenfolge
|
||||
</th>
|
||||
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>
|
||||
Aktionen
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredPrompts.map((prompt, idx) => (
|
||||
<tr key={prompt.id} style={{borderBottom:'1px solid var(--border)'}}>
|
||||
<td style={{padding:'12px 8px'}}>
|
||||
<div style={{fontWeight:500, fontSize:14}}>{prompt.name}</div>
|
||||
{prompt.description && (
|
||||
<div style={{fontSize:11, color:'var(--text3)', marginTop:2}}>
|
||||
{prompt.description}
|
||||
</div>
|
||||
)}
|
||||
<div style={{fontSize:10, color:'var(--text3)', marginTop:4}}>
|
||||
{prompt.slug}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{padding:'12px 8px'}}>
|
||||
<span style={{
|
||||
padding:'4px 8px', borderRadius:4, fontSize:11, fontWeight:500,
|
||||
background:'var(--surface2)', color:'var(--text2)'
|
||||
}}>
|
||||
{prompt.category || 'ganzheitlich'}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{padding:'12px 8px'}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={prompt.active}
|
||||
onChange={() => handleToggleActive(prompt)}
|
||||
style={{cursor:'pointer'}}
|
||||
/>
|
||||
</td>
|
||||
<td style={{padding:'12px 8px'}}>
|
||||
<div style={{display:'flex', gap:4}}>
|
||||
<button
|
||||
onClick={() => handleMoveUp(prompt)}
|
||||
disabled={idx === 0}
|
||||
style={{
|
||||
padding:'4px 8px', fontSize:12, cursor: idx === 0 ? 'not-allowed' : 'pointer',
|
||||
opacity: idx === 0 ? 0.5 : 1
|
||||
}}
|
||||
title="Nach oben"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleMoveDown(prompt)}
|
||||
disabled={idx === filteredPrompts.length - 1}
|
||||
style={{
|
||||
padding:'4px 8px', fontSize:12,
|
||||
cursor: idx === filteredPrompts.length - 1 ? 'not-allowed' : 'pointer',
|
||||
opacity: idx === filteredPrompts.length - 1 ? 0.5 : 1
|
||||
}}
|
||||
title="Nach unten"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{padding:'12px 8px'}}>
|
||||
<div style={{display:'flex', gap:6}}>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => setEditingPrompt(prompt)}
|
||||
style={{fontSize:12, padding:'6px 12px'}}
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => handleDuplicate(prompt)}
|
||||
style={{fontSize:12, padding:'6px 12px'}}
|
||||
>
|
||||
Duplizieren
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => handleDelete(prompt)}
|
||||
style={{fontSize:12, padding:'6px 12px', color:'#D85A30'}}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: 40, color: 'var(--text3)' }}>
|
||||
Lädt...
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
background: 'var(--surface)',
|
||||
borderRadius: 12,
|
||||
border: '1px solid var(--border)',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{
|
||||
background: 'var(--surface2)',
|
||||
borderBottom: '1px solid var(--border)'
|
||||
}}>
|
||||
<th style={{
|
||||
padding: 12,
|
||||
textAlign: 'left',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: 'var(--text3)',
|
||||
width: 80
|
||||
}}>
|
||||
Typ
|
||||
</th>
|
||||
<th style={{
|
||||
padding: 12,
|
||||
textAlign: 'left',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: 'var(--text3)'
|
||||
}}>
|
||||
Name
|
||||
</th>
|
||||
<th style={{
|
||||
padding: 12,
|
||||
textAlign: 'left',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: 'var(--text3)',
|
||||
width: 120
|
||||
}}>
|
||||
Kategorie
|
||||
</th>
|
||||
<th style={{
|
||||
padding: 12,
|
||||
textAlign: 'center',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: 'var(--text3)',
|
||||
width: 100
|
||||
}}>
|
||||
Stages
|
||||
</th>
|
||||
<th style={{
|
||||
padding: 12,
|
||||
textAlign: 'center',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: 'var(--text3)',
|
||||
width: 80
|
||||
}}>
|
||||
Status
|
||||
</th>
|
||||
<th style={{
|
||||
padding: 12,
|
||||
textAlign: 'right',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: 'var(--text3)',
|
||||
width: 120
|
||||
}}>
|
||||
Aktionen
|
||||
</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{filteredPrompts.length === 0 && (
|
||||
<div style={{padding:40, textAlign:'center', color:'var(--text3)'}}>
|
||||
Keine Prompts in dieser Kategorie
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredPrompts.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="6" style={{
|
||||
padding: 40,
|
||||
textAlign: 'center',
|
||||
color: 'var(--text3)'
|
||||
}}>
|
||||
Keine Prompts gefunden
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredPrompts.map(prompt => (
|
||||
<tr
|
||||
key={prompt.id}
|
||||
style={{
|
||||
borderBottom: '1px solid var(--border)',
|
||||
opacity: prompt.active ? 1 : 0.5
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: 12 }}>
|
||||
<div style={{
|
||||
display: 'inline-block',
|
||||
padding: '2px 8px',
|
||||
background: getTypeColor(prompt.type) + '20',
|
||||
color: getTypeColor(prompt.type),
|
||||
borderRadius: 6,
|
||||
fontSize: 11,
|
||||
fontWeight: 600
|
||||
}}>
|
||||
{getTypeLabel(prompt.type)}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: 12 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500 }}>
|
||||
{prompt.display_name || prompt.name}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}>
|
||||
{prompt.slug}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: 12, fontSize: 13 }}>
|
||||
{prompt.category || 'ganzheitlich'}
|
||||
</td>
|
||||
<td style={{ padding: 12, textAlign: 'center', fontSize: 13 }}>
|
||||
{prompt.type === 'pipeline' ? (
|
||||
<span style={{
|
||||
background: 'var(--surface2)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: 4,
|
||||
fontSize: 11
|
||||
}}>
|
||||
{getStageCount(prompt)} Stages
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ color: 'var(--text3)', fontSize: 11 }}>—</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: 12, textAlign: 'center' }}>
|
||||
<label style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={prompt.active}
|
||||
onChange={() => handleToggleActive(prompt)}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
<td style={{ padding: 12 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
justifyContent: 'flex-end'
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setEditingPrompt(prompt)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 4
|
||||
}}
|
||||
title="Bearbeiten"
|
||||
>
|
||||
<Edit size={16} color="var(--accent)" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDuplicate(prompt)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 4
|
||||
}}
|
||||
title="Duplizieren"
|
||||
>
|
||||
<Copy size={16} color="var(--text3)" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(prompt)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 4
|
||||
}}
|
||||
title="Löschen"
|
||||
>
|
||||
<Trash2 size={16} color="var(--danger)" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Modals */}
|
||||
{/* Unified Prompt Modal */}
|
||||
{(editingPrompt || showNewPrompt) && (
|
||||
<PromptEditModal
|
||||
<UnifiedPromptModal
|
||||
prompt={editingPrompt}
|
||||
onSave={handleSavePrompt}
|
||||
onSave={handleSave}
|
||||
onClose={() => {
|
||||
setEditingPrompt(null)
|
||||
setShowNewPrompt(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(editingPipeline || showNewPipeline) && (
|
||||
<PipelineConfigModal
|
||||
config={editingPipeline}
|
||||
onSave={handleSavePipeline}
|
||||
onClose={() => {
|
||||
setEditingPipeline(null)
|
||||
setShowNewPipeline(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user