diff --git a/frontend/src/components/UnifiedPromptModal.jsx b/frontend/src/components/UnifiedPromptModal.jsx
new file mode 100644
index 0000000..4211ed5
--- /dev/null
+++ b/frontend/src/components/UnifiedPromptModal.jsx
@@ -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 (
+
+
+
+
+ {prompt ? 'Prompt bearbeiten' : 'Neuer Prompt'}
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Basic Info */}
+
+
+
+
+
+ setDisplayName(e.target.value)}
+ placeholder="Name für Benutzer (optional)"
+ style={{ width: '100%', textAlign: 'left' }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Type-specific editor */}
+ {type === 'base' && (
+
+ )}
+
+ {type === 'pipeline' && (
+
+
+
Stages ({stages.length})
+
+
+
+ {stages.map((stage, sIdx) => (
+
+
+
Stage {stage.stage}
+
+ {sIdx > 0 && (
+
+ )}
+ {sIdx < stages.length - 1 && (
+
+ )}
+
+
+
+
+ {/* Prompts in this stage */}
+ {stage.prompts.map((p, pIdx) => (
+
+
+
+
+
+ updateStagePrompt(stage.stage, pIdx, 'output_key', e.target.value)}
+ placeholder="output_key"
+ style={{ fontSize: 12, textAlign: 'left' }}
+ />
+
+
+
+
+
+
+ {p.source === 'reference' ? (
+
+ ) : (
+
+
+ ))}
+
+
+
+ ))}
+
+ )}
+
+ {/* Actions */}
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/AdminPromptsPage.jsx b/frontend/src/pages/AdminPromptsPage.jsx
index 5f528c0..1427699 100644
--- a/frontend/src/pages/AdminPromptsPage.jsx
+++ b/frontend/src/pages/AdminPromptsPage.jsx
@@ -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 (
-
- )
+ const getTypeColor = (type) => {
+ if (type === 'base') return 'var(--accent)'
+ if (type === 'pipeline') return '#6366f1'
+ return 'var(--text3)'
}
return (
-
-
-
KI-Prompts & Pipelines
+
+
+
+ KI-Prompts ({filteredPrompts.length})
+
-
-
- {/* Tab Switcher */}
-
-
-
{error && (
-
+
{error}
)}
- {/* Prompts Tab */}
- {activeTab === 'prompts' && (
- <>
- {/* Category Filter */}
-
-
+ {/* Filters */}
+
+
+
+ Typ:
+
+
+
+
+
+
+
+
+
{/* Prompts Table */}
-
-
-
-
- |
- Titel
- |
-
- Kategorie
- |
-
- Aktiv
- |
-
- Reihenfolge
- |
-
- Aktionen
- |
-
-
-
- {filteredPrompts.map((prompt, idx) => (
-
- |
- {prompt.name}
- {prompt.description && (
-
- {prompt.description}
-
- )}
-
- {prompt.slug}
-
- |
-
-
- {prompt.category || 'ganzheitlich'}
-
- |
-
- handleToggleActive(prompt)}
- style={{cursor:'pointer'}}
- />
- |
-
-
-
-
-
- |
-
-
-
-
-
-
- |
+ {loading ? (
+
+ Lädt...
+
+ ) : (
+
+
+
+
+ |
+ Typ
+ |
+
+ Name
+ |
+
+ Kategorie
+ |
+
+ Stages
+ |
+
+ Status
+ |
+
+ Aktionen
+ |
- ))}
-
-
-
- {filteredPrompts.length === 0 && (
-
- Keine Prompts in dieser Kategorie
-
- )}
-
- >
- )}
-
- {/* Pipelines Tab */}
- {activeTab === 'pipelines' && (
- <>
- {pipelineLoading ? (
-
- ) : (
-
-
-
-
- | Name |
- Module |
- Stages |
- Status |
- Aktionen |
+
+
+ {filteredPrompts.length === 0 ? (
+
+ |
+ Keine Prompts gefunden
+ |
+
+ ) : (
+ filteredPrompts.map(prompt => (
+
+ |
+
+ {getTypeLabel(prompt.type)}
+
+ |
+
+
+
+ {prompt.display_name || prompt.name}
+
+
+ {prompt.slug}
+
+
+ |
+
+ {prompt.category || 'ganzheitlich'}
+ |
+
+ {prompt.type === 'pipeline' ? (
+
+ {getStageCount(prompt)} Stages
+
+ ) : (
+ —
+ )}
+ |
+
+
+ |
+
+
+
+
+
+
+ |
-
-
- {pipelineConfigs.map(config => {
- const activeModules = Object.entries(config.modules || {}).filter(([_, active]) => active)
-
- return (
-
- |
- {config.name}
- {config.is_default && (
-
- Standard
-
- )}
- {config.description && (
-
- {config.description}
-
- )}
- |
-
-
- {activeModules.map(([name]) => name).join(', ')}
-
-
- {activeModules.length} Module
-
- |
-
-
- S1: {config.stage1_prompts?.length || 0} Prompts
-
-
- S2: {config.stage2_prompt ? '✓' : '-'} | S3: {config.stage3_prompt ? '✓' : '-'}
-
- |
-
-
- {config.active ? 'Aktiv' : 'Inaktiv'}
-
- |
-
-
- {!config.is_default && (
-
- )}
-
-
-
- |
-
- )
- })}
-
-
-
- {pipelineConfigs.length === 0 && (
-
- Noch keine Pipeline-Konfigurationen vorhanden
-
+ ))
)}
-
- )}
- >
+
+
+
)}
- {/* Edit Modals */}
+ {/* Unified Prompt Modal */}
{(editingPrompt || showNewPrompt) && (
-
{
setEditingPrompt(null)
setShowNewPrompt(false)
}}
/>
)}
-
- {(editingPipeline || showNewPipeline) && (
- {
- setEditingPipeline(null)
- setShowNewPipeline(false)
- }}
- />
- )}
)
}