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 */}
setName(e.target.value)} placeholder="Interner Name" style={{ width: '100%', textAlign: 'left' }} />
setSlug(e.target.value)} placeholder="technischer_name" style={{ width: '100%', textAlign: 'left' }} disabled={!!prompt} />
setDisplayName(e.target.value)} placeholder="Name für Benutzer (optional)" style={{ width: '100%', textAlign: 'left' }} />