feat: Issue #28 complete - unified prompt system (Phase 4)
Some checks failed
Deploy Development / deploy (push) Failing after 34s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 15s

Cleanup & Documentation:
- Removed deprecated components: PipelineConfigModal, PromptEditModal
- Updated CLAUDE.md with Issue #28 summary
- Kept old backend endpoints for backward-compatibility

Summary of all 4 phases:
 Phase 1: DB Migration (unified schema)
 Phase 2: Backend Executor (universal execution engine)
 Phase 3: Frontend UI (consolidated interface)
 Phase 4: Cleanup & Docs

Key improvements:
- Unlimited dynamic stages (no hardcoded limit)
- Multiple prompts per stage (parallel execution)
- Base prompts (reusable) + Pipeline prompts (workflows)
- Inline templates or references
- JSON output enforceable
- Cross-module correlations possible

Ready for testing on dev.mitai.jinkendo.de
This commit is contained in:
Lars 2026-03-25 15:33:47 +01:00
parent 31e2c24a8a
commit 2f3314cd36
3 changed files with 48 additions and 951 deletions

View File

@ -56,7 +56,7 @@ frontend/src/
└── technical/ # MEMBERSHIP_SYSTEM.md └── technical/ # MEMBERSHIP_SYSTEM.md
``` ```
## Aktuelle Version: v9c (komplett) 🚀 Production seit 21.03.2026 ## Aktuelle Version: v9d + Issue #28 🚀 Development seit 25.03.2026
### Implementiert ✅ ### Implementiert ✅
- Login (E-Mail + bcrypt), Auth-Middleware alle Endpoints, Rate Limiting - Login (E-Mail + bcrypt), Auth-Middleware alle Endpoints, Rate Limiting
@ -188,6 +188,53 @@ frontend/src/
📚 Details: `.claude/docs/technical/MEMBERSHIP_SYSTEM.md` · `.claude/docs/architecture/FEATURE_ENFORCEMENT.md` 📚 Details: `.claude/docs/technical/MEMBERSHIP_SYSTEM.md` · `.claude/docs/architecture/FEATURE_ENFORCEMENT.md`
### Issue #28: Unified Prompt System ✅ (Development 25.03.2026)
**AI-Prompts Flexibilisierung - Komplett überarbeitet:**
- ✅ **Unified Prompt System (4 Phasen):**
- **Phase 1:** DB-Migration - Schema erweitert
- `ai_prompts` um `type`, `stages`, `output_format`, `output_schema` erweitert
- Alle Prompts zu 1-stage Pipelines migriert
- Pipeline-Configs in `ai_prompts` konsolidiert
- **Phase 2:** Backend Executor
- `prompt_executor.py` - universeller Executor für base + pipeline
- Dynamische Placeholder-Auflösung (`{{stage_N_key}}`)
- JSON-Output-Validierung
- Multi-stage parallele Ausführung
- Reference (Basis-Prompts) + Inline (Templates) Support
- **Phase 3:** Frontend UI Consolidation
- `UnifiedPromptModal` - ein Editor für beide Typen
- `AdminPromptsPage` - Tab-Switcher entfernt, Type-Filter hinzugefügt
- Stage-Editor mit Add/Remove/Reorder
- Mobile-ready Design
- **Phase 4:** Cleanup & Docs
- Deprecated Komponenten entfernt (PipelineConfigModal, PromptEditModal)
- Old endpoints behalten für Backward-Compatibility
**Features:**
- Unbegrenzte dynamische Stages (keine 3-Stage Limitierung mehr)
- Mehrere Prompts pro Stage (parallel)
- Zwei Prompt-Typen: `base` (wiederverwendbar) + `pipeline` (Workflows)
- Inline-Templates oder Referenzen zu Basis-Prompts
- JSON-Output erzwingbar pro Prompt
- Cross-Module Korrelationen möglich
**Migrations:**
- Migration 020: Unified Prompt System Schema
**Backend Endpoints:**
- `POST /api/prompts/execute` - Universeller Executor
- `POST /api/prompts/unified` - Create unified prompt
- `PUT /api/prompts/unified/{id}` - Update unified prompt
**UI:**
- Admin → KI-Prompts: Type-Filter (Alle/Basis/Pipeline)
- Neuer Prompt-Editor mit dynamischem Stage-Builder
- Inline editing von Stages + Prompts
📚 Details: `.claude/docs/functional/AI_PROMPTS.md`
## Feature-Roadmap ## Feature-Roadmap
> 📋 **Detaillierte Roadmap:** `.claude/docs/ROADMAP.md` (Phasen 0-3, Timeline, Abhängigkeiten) > 📋 **Detaillierte Roadmap:** `.claude/docs/ROADMAP.md` (Phasen 0-3, Timeline, Abhängigkeiten)

View File

@ -1,386 +0,0 @@
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>
)
}

View File

@ -1,564 +0,0 @@
import { useState, useEffect } from 'react'
import { api } from '../utils/api'
import PromptGenerator from './PromptGenerator'
export default function PromptEditModal({ prompt, onSave, onClose }) {
const [name, setName] = useState('')
const [slug, setSlug] = useState('')
const [displayName, setDisplayName] = useState('')
const [description, setDescription] = useState('')
const [category, setCategory] = useState('ganzheitlich')
const [template, setTemplate] = useState('')
const [active, setActive] = useState(true)
const [preview, setPreview] = useState(null)
const [unknownPlaceholders, setUnknownPlaceholders] = useState([])
const [showGenerator, setShowGenerator] = useState(false)
const [showPlaceholders, setShowPlaceholders] = useState(false)
const [placeholders, setPlaceholders] = useState({})
const [placeholderFilter, setPlaceholderFilter] = useState('')
const [templateRef, setTemplateRef] = useState(null)
const [optimization, setOptimization] = useState(null)
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const categories = [
{ id: 'körper', label: 'Körper' },
{ id: 'ernährung', label: 'Ernährung' },
{ id: 'training', label: 'Training' },
{ id: 'schlaf', label: 'Schlaf' },
{ id: 'vitalwerte', label: 'Vitalwerte' },
{ id: 'ziele', label: 'Ziele' },
{ id: 'ganzheitlich', label: 'Ganzheitlich' }
]
useEffect(() => {
if (prompt) {
setName(prompt.name || '')
setSlug(prompt.slug || '')
setDisplayName(prompt.display_name || '')
setDescription(prompt.description || '')
setCategory(prompt.category || 'ganzheitlich')
setTemplate(prompt.template || '')
setActive(prompt.active ?? true)
}
}, [prompt])
const handlePreview = async () => {
try {
setLoading(true)
const result = await api.previewPrompt(template)
setPreview(result.resolved)
setUnknownPlaceholders(result.unknown_placeholders || [])
} catch (e) {
alert('Fehler bei Vorschau: ' + e.message)
} finally {
setLoading(false)
}
}
const handleOptimize = async () => {
if (!prompt?.id) {
alert('Prompt muss erst gespeichert werden bevor er optimiert werden kann')
return
}
try {
setLoading(true)
const result = await api.optimizePrompt(prompt.id)
setOptimization(result)
} catch (e) {
alert('Fehler bei Optimierung: ' + e.message)
} finally {
setLoading(false)
}
}
const handleApplyOptimized = () => {
if (optimization?.optimized_prompt) {
setTemplate(optimization.optimized_prompt)
setOptimization(null)
}
}
const handleSave = async () => {
if (!name.trim()) {
alert('Bitte Titel eingeben')
return
}
if (!template.trim()) {
alert('Bitte Template eingeben')
return
}
try {
setSaving(true)
if (prompt?.id) {
// Update existing
await api.updatePrompt(prompt.id, {
name,
display_name: displayName || null,
description,
category,
template,
active
})
} else {
// Create new
if (!slug.trim()) {
alert('Bitte Slug eingeben')
return
}
await api.createPrompt({
name,
slug,
display_name: displayName || null,
description,
category,
template,
active
})
}
onSave()
} catch (e) {
alert('Fehler beim Speichern: ' + e.message)
} finally {
setSaving(false)
}
}
const handleGeneratorResult = (generated) => {
setName(generated.suggested_title)
setCategory(generated.suggested_category)
setTemplate(generated.template)
setShowGenerator(false)
// Auto-generate slug if new prompt
if (!prompt?.id) {
const autoSlug = generated.suggested_title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
setSlug(autoSlug)
}
}
const loadPlaceholders = async () => {
try {
const data = await api.listPlaceholders()
setPlaceholders(data)
setShowPlaceholders(true)
setPlaceholderFilter('')
} catch (e) {
alert('Fehler beim Laden der Platzhalter: ' + e.message)
}
}
const insertPlaceholder = (key) => {
if (!templateRef) {
// Fallback: append at end
setTemplate(prev => prev + ` {{${key}}}`)
} else {
// Insert at cursor position
const start = templateRef.selectionStart
const end = templateRef.selectionEnd
const text = template
const before = text.substring(0, start)
const after = text.substring(end)
const inserted = `{{${key}}}`
setTemplate(before + inserted + after)
// Set cursor position after inserted placeholder
setTimeout(() => {
templateRef.selectionStart = templateRef.selectionEnd = start + inserted.length
templateRef.focus()
}, 0)
}
setShowPlaceholders(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
}}>
<div style={{
background:'var(--bg)', borderRadius:12, maxWidth:900, width:'100%',
maxHeight:'90vh', overflow:'auto', padding:24
}}>
<h2 style={{margin:'0 0 24px 0', fontSize:20, fontWeight:600}}>
{prompt ? 'Prompt bearbeiten' : 'Neuer Prompt'}
</h2>
{/* Action Buttons */}
<div style={{display:'flex', gap:8, marginBottom:24, flexWrap:'wrap'}}>
<button
className="btn"
onClick={() => setShowGenerator(true)}
style={{fontSize:13}}
>
🤖 Von KI erstellen lassen
</button>
{prompt?.id && (
<button
className="btn"
onClick={handleOptimize}
disabled={loading}
style={{fontSize:13}}
>
Von KI optimieren lassen
</button>
)}
<button
className="btn"
onClick={handlePreview}
disabled={loading || !template.trim()}
style={{fontSize:13}}
>
👁 Vorschau
</button>
</div>
{/* Form Fields */}
<div style={{display:'grid', gap:20, marginBottom:24}}>
<div>
<label className="form-label" style={{display:'block', marginBottom:6}}>Titel *</label>
<input
className="form-input"
value={name}
onChange={e => setName(e.target.value)}
placeholder="z.B. Protein-Analyse"
style={{width:'100%', textAlign:'left'}}
/>
</div>
{!prompt?.id && (
<div>
<label className="form-label" style={{display:'block', marginBottom:6}}>
Slug * (eindeutig, keine Leerzeichen)
</label>
<input
className="form-input"
value={slug}
onChange={e => setSlug(e.target.value)}
placeholder="z.B. protein_analyse"
style={{width:'100%', textAlign:'left'}}
/>
</div>
)}
<div>
<label className="form-label" style={{display:'block', marginBottom:6}}>
Anzeigename (in der Anwendung sichtbar)
</label>
<input
className="form-input"
value={displayName}
onChange={e => setDisplayName(e.target.value)}
placeholder="z.B. 🍽️ Protein-Analyse"
style={{width:'100%', textAlign:'left'}}
/>
<div style={{fontSize:11, color:'var(--text3)', marginTop:4}}>
Leer lassen = Titel wird verwendet
</div>
</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="Wofür ist dieser Prompt? (für Admin sichtbar)"
style={{width:'100%', textAlign:'left', resize:'vertical'}}
/>
</div>
<div>
<label className="form-label" style={{display:'block', marginBottom:6}}>Kategorie</label>
<select
className="form-select"
value={category}
onChange={e => setCategory(e.target.value)}
style={{width:'100%'}}
>
{categories.map(cat => (
<option key={cat.id} value={cat.id}>{cat.label}</option>
))}
</select>
</div>
<div>
<label className="form-label" style={{display:'block', marginBottom:6}}>Template *</label>
<textarea
ref={el => setTemplateRef(el)}
className="form-input"
value={template}
onChange={e => setTemplate(e.target.value)}
rows={15}
placeholder="Du bist ein Ernährungsexperte. Analysiere folgende Daten: {{nutrition_summary}}"
style={{width:'100%', textAlign:'left', fontFamily:'monospace', fontSize:12, resize:'vertical'}}
/>
<div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginTop:6}}>
<div style={{fontSize:11, color:'var(--text3)'}}>
Nutze Platzhalter wie {`{{weight_aktuell}}`}, {`{{protein_avg}}`}, etc.
</div>
<button
className="btn"
onClick={loadPlaceholders}
type="button"
style={{fontSize:12, padding:'4px 10px'}}
>
📋 Platzhalter einfügen
</button>
</div>
{/* Placeholders Dropdown */}
{showPlaceholders && Object.keys(placeholders).length > 0 && (
<div style={{
marginTop:8, padding:16, background:'var(--surface)',
border:'1px solid var(--border)', borderRadius:8,
maxHeight:400, overflow:'auto'
}}>
<div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:12}}>
<strong style={{fontSize:14}}>Platzhalter einfügen</strong>
<button
onClick={() => setShowPlaceholders(false)}
style={{
background:'none', border:'none', cursor:'pointer',
fontSize:20, color:'var(--text3)', padding:0
}}
>
×
</button>
</div>
{/* Search Filter */}
<input
type="text"
placeholder="Suchen..."
value={placeholderFilter}
onChange={e => setPlaceholderFilter(e.target.value)}
style={{
width:'100%', padding:'6px 10px', marginBottom:12,
background:'var(--surface2)', border:'1px solid var(--border)',
borderRadius:6, fontSize:12
}}
/>
{/* Grouped Placeholders */}
{Object.entries(placeholders).map(([category, items]) => {
// Filter items
const filteredItems = items.filter(p =>
placeholderFilter === '' ||
p.key.toLowerCase().includes(placeholderFilter.toLowerCase()) ||
p.description.toLowerCase().includes(placeholderFilter.toLowerCase())
)
if (filteredItems.length === 0) return null
return (
<div key={category} style={{marginBottom:16}}>
<div style={{
fontSize:11, fontWeight:600, color:'var(--text3)',
textTransform:'uppercase', letterSpacing:'0.5px',
marginBottom:6
}}>
{category}
</div>
<div style={{display:'grid', gap:6}}>
{filteredItems.map((p, i) => (
<button
key={i}
onClick={() => insertPlaceholder(p.key)}
style={{
textAlign:'left', padding:'8px 10px',
background:'var(--surface2)', border:'1px solid var(--border)',
borderRadius:6, cursor:'pointer',
transition:'all 0.15s'
}}
onMouseEnter={e => {
e.currentTarget.style.background = 'var(--accent-light)'
e.currentTarget.style.borderColor = 'var(--accent)'
}}
onMouseLeave={e => {
e.currentTarget.style.background = 'var(--surface2)'
e.currentTarget.style.borderColor = 'var(--border)'
}}
>
<div style={{
fontWeight:600, fontSize:12, fontFamily:'monospace',
color:'var(--accent)', marginBottom:2
}}>
{`{{${p.key}}}`}
</div>
<div style={{fontSize:11, color:'var(--text2)', marginBottom:2}}>
{p.description}
</div>
<div style={{fontSize:10, color:'var(--text3)', fontStyle:'italic'}}>
Beispiel: {p.example}
</div>
</button>
))}
</div>
</div>
)
})}
{/* No results */}
{placeholderFilter && Object.values(placeholders).every(items =>
items.filter(p =>
p.key.toLowerCase().includes(placeholderFilter.toLowerCase()) ||
p.description.toLowerCase().includes(placeholderFilter.toLowerCase())
).length === 0
) && (
<div style={{textAlign:'center', padding:20, color:'var(--text3)', fontSize:12}}>
Keine Platzhalter gefunden für "{placeholderFilter}"
</div>
)}
</div>
)}
</div>
<div style={{display:'flex', alignItems:'center', gap:8}}>
<input
type="checkbox"
checked={active}
onChange={e => setActive(e.target.checked)}
id="active-checkbox"
/>
<label htmlFor="active-checkbox" style={{margin:0, cursor:'pointer'}}>
Prompt ist aktiv (für Nutzer sichtbar)
</label>
</div>
</div>
{/* Unknown Placeholders Warning */}
{unknownPlaceholders.length > 0 && (
<div style={{
padding:12, background:'#fff3cd', border:'1px solid #ffc107',
borderRadius:8, marginBottom:16, fontSize:12
}}>
<strong> Unbekannte Platzhalter:</strong> {unknownPlaceholders.join(', ')}
</div>
)}
{/* Preview */}
{preview && (
<div style={{marginBottom:24}}>
<h3 style={{fontSize:14, fontWeight:600, marginBottom:8}}>
Vorschau (mit echten Daten):
</h3>
<pre style={{
background:'var(--surface2)', padding:16, borderRadius:8,
fontSize:12, lineHeight:1.6, whiteSpace:'pre-wrap',
maxHeight:300, overflow:'auto'
}}>
{preview}
</pre>
</div>
)}
{/* Optimization Results */}
{optimization && (
<div style={{
marginBottom:24, padding:16, background:'var(--surface)',
border:'1px solid var(--border)', borderRadius:8
}}>
<h3 style={{fontSize:16, fontWeight:600, marginBottom:12}}>
Optimierungsvorschläge
</h3>
<div style={{display:'grid', gap:12, fontSize:13}}>
<div>
<strong>Score:</strong> {optimization.score}/100
</div>
<div>
<strong> Stärken:</strong>
<ul style={{marginTop:4, paddingLeft:20}}>
{optimization.strengths?.map((s, i) => (
<li key={i}>{s}</li>
))}
</ul>
</div>
<div>
<strong> Verbesserungspotenzial:</strong>
<ul style={{marginTop:4, paddingLeft:20}}>
{optimization.weaknesses?.map((w, i) => (
<li key={i}>{w}</li>
))}
</ul>
</div>
<div>
<strong>Änderungen:</strong>
<p style={{marginTop:4}}>{optimization.changes_summary}</p>
</div>
{/* Side-by-side comparison */}
<div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:12}}>
<div>
<strong>Original:</strong>
<pre style={{
marginTop:4, padding:12, background:'var(--surface2)',
borderRadius:8, fontSize:11, maxHeight:200, overflow:'auto',
whiteSpace:'pre-wrap', wordBreak:'break-word'
}}>
{template}
</pre>
</div>
<div>
<strong>Optimiert:</strong>
<pre style={{
marginTop:4, padding:12, background:'#d4edda',
borderRadius:8, fontSize:11, maxHeight:200, overflow:'auto',
whiteSpace:'pre-wrap', wordBreak:'break-word'
}}>
{optimization.optimized_prompt}
</pre>
</div>
</div>
<button
className="btn btn-primary"
onClick={handleApplyOptimized}
>
Optimierte Version übernehmen
</button>
</div>
</div>
)}
{/* Actions */}
<div style={{display:'flex', gap:12, justifyContent:'flex-end'}}>
<button className="btn" onClick={onClose}>
Abbrechen
</button>
<button
className="btn btn-primary"
onClick={handleSave}
disabled={saving}
>
{saving ? 'Speichern...' : 'Speichern'}
</button>
</div>
{/* Generator Modal */}
{showGenerator && (
<PromptGenerator
onGenerated={handleGeneratorResult}
onClose={() => setShowGenerator(false)}
/>
)}
</div>
</div>
)
}