Flexibles KI Prompt System #48

Merged
Lars merged 56 commits from develop into main 2026-03-26 14:49:48 +01:00
5 changed files with 133 additions and 11 deletions
Showing only changes of commit 0c4264de44 - Show all commits

View File

@ -0,0 +1,20 @@
-- Migration 018: Add display_name to ai_prompts for user-facing labels
ALTER TABLE ai_prompts ADD COLUMN IF NOT EXISTS display_name VARCHAR(100);
-- Migrate existing prompts from hardcoded SLUG_LABELS
UPDATE ai_prompts SET display_name = '🔍 Gesamtanalyse' WHERE slug = 'gesamt' AND display_name IS NULL;
UPDATE ai_prompts SET display_name = '🫧 Körperkomposition' WHERE slug = 'koerper' AND display_name IS NULL;
UPDATE ai_prompts SET display_name = '🍽️ Ernährung' WHERE slug = 'ernaehrung' AND display_name IS NULL;
UPDATE ai_prompts SET display_name = '🏋️ Aktivität' WHERE slug = 'aktivitaet' AND display_name IS NULL;
UPDATE ai_prompts SET display_name = '❤️ Gesundheitsindikatoren' WHERE slug = 'gesundheit' AND display_name IS NULL;
UPDATE ai_prompts SET display_name = '🎯 Zielfortschritt' WHERE slug = 'ziele' AND display_name IS NULL;
UPDATE ai_prompts SET display_name = '🔬 Mehrstufige Gesamtanalyse' WHERE slug = 'pipeline' AND display_name IS NULL;
UPDATE ai_prompts SET display_name = '🔬 Pipeline: Körper-Analyse (JSON)' WHERE slug = 'pipeline_body' AND display_name IS NULL;
UPDATE ai_prompts SET display_name = '🔬 Pipeline: Ernährungs-Analyse (JSON)' WHERE slug = 'pipeline_nutrition' AND display_name IS NULL;
UPDATE ai_prompts SET display_name = '🔬 Pipeline: Aktivitäts-Analyse (JSON)' WHERE slug = 'pipeline_activity' AND display_name IS NULL;
UPDATE ai_prompts SET display_name = '🔬 Pipeline: Synthese' WHERE slug = 'pipeline_synthesis' AND display_name IS NULL;
UPDATE ai_prompts SET display_name = '🔬 Pipeline: Zielabgleich' WHERE slug = 'pipeline_goals' AND display_name IS NULL;
-- Fallback: use name as display_name if still NULL
UPDATE ai_prompts SET display_name = name WHERE display_name IS NULL;

View File

@ -134,6 +134,7 @@ class AdminProfileUpdate(BaseModel):
class PromptCreate(BaseModel):
name: str
slug: str
display_name: Optional[str] = None
description: Optional[str] = None
template: str
category: str = 'ganzheitlich'
@ -143,6 +144,7 @@ class PromptCreate(BaseModel):
class PromptUpdate(BaseModel):
name: Optional[str] = None
display_name: Optional[str] = None
description: Optional[str] = None
template: Optional[str] = None
category: Optional[str] = None

View File

@ -61,9 +61,9 @@ def create_prompt(p: PromptCreate, session: dict=Depends(require_admin)):
prompt_id = str(uuid.uuid4())
cur.execute(
"""INSERT INTO ai_prompts (id, name, slug, description, template, category, active, sort_order, created, updated)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)""",
(prompt_id, p.name, p.slug, p.description, p.template, p.category, p.active, p.sort_order)
"""INSERT INTO ai_prompts (id, name, slug, display_name, description, template, category, active, sort_order, created, updated)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)""",
(prompt_id, p.name, p.slug, p.display_name or p.name, p.description, p.template, p.category, p.active, p.sort_order)
)
return {"id": prompt_id, "slug": p.slug}
@ -82,6 +82,9 @@ def update_prompt(prompt_id: str, p: PromptUpdate, session: dict=Depends(require
if p.name is not None:
updates.append('name=%s')
values.append(p.name)
if p.display_name is not None:
updates.append('display_name=%s')
values.append(p.display_name)
if p.description is not None:
updates.append('description=%s')
values.append(p.description)
@ -140,10 +143,12 @@ def duplicate_prompt(prompt_id: str, session: dict=Depends(require_admin)):
new_name = f"{original['name']} (Kopie)"
new_slug = f"{original['slug']}_copy_{uuid.uuid4().hex[:6]}"
new_display_name = f"{original.get('display_name') or original['name']} (Kopie)"
cur.execute(
"""INSERT INTO ai_prompts (id, name, slug, description, template, category, active, sort_order, created, updated)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)""",
(new_id, new_name, new_slug, original['description'], original['template'],
"""INSERT INTO ai_prompts (id, name, slug, display_name, description, template, category, active, sort_order, created, updated)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)""",
(new_id, new_name, new_slug, new_display_name, original['description'], original['template'],
original.get('category', 'ganzheitlich'), original['active'], original['sort_order'])
)

View File

@ -5,6 +5,7 @@ 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('')
@ -13,6 +14,8 @@ export default function PromptEditModal({ prompt, onSave, onClose }) {
const [preview, setPreview] = useState(null)
const [unknownPlaceholders, setUnknownPlaceholders] = useState([])
const [showGenerator, setShowGenerator] = useState(false)
const [showPlaceholders, setShowPlaceholders] = useState(false)
const [placeholders, setPlaceholders] = useState([])
const [optimization, setOptimization] = useState(null)
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
@ -31,6 +34,7 @@ export default function PromptEditModal({ prompt, onSave, onClose }) {
if (prompt) {
setName(prompt.name || '')
setSlug(prompt.slug || '')
setDisplayName(prompt.display_name || '')
setDescription(prompt.description || '')
setCategory(prompt.category || 'ganzheitlich')
setTemplate(prompt.template || '')
@ -92,6 +96,7 @@ export default function PromptEditModal({ prompt, onSave, onClose }) {
// Update existing
await api.updatePrompt(prompt.id, {
name,
display_name: displayName || null,
description,
category,
template,
@ -106,6 +111,7 @@ export default function PromptEditModal({ prompt, onSave, onClose }) {
await api.createPrompt({
name,
slug,
display_name: displayName || null,
description,
category,
template,
@ -137,6 +143,28 @@ export default function PromptEditModal({ prompt, onSave, onClose }) {
}
}
const loadPlaceholders = async () => {
try {
const data = await api.listPlaceholders()
// Flatten nested structure into simple list
const flatList = []
Object.entries(data).forEach(([category, items]) => {
Object.entries(items).forEach(([key, value]) => {
flatList.push({ key, value, category })
})
})
setPlaceholders(flatList)
setShowPlaceholders(true)
} catch (e) {
alert('Fehler beim Laden der Platzhalter: ' + e.message)
}
}
const insertPlaceholder = (key) => {
setTemplate(prev => prev + ` {{${key}}}`)
setShowPlaceholders(false)
}
return (
<div style={{
position:'fixed', inset:0, background:'rgba(0,0,0,0.5)',
@ -208,6 +236,22 @@ export default function PromptEditModal({ prompt, onSave, onClose }) {
</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
@ -244,9 +288,60 @@ export default function PromptEditModal({ prompt, onSave, onClose }) {
placeholder="Du bist ein Ernährungsexperte. Analysiere folgende Daten: {{nutrition_summary}}"
style={{width:'100%', textAlign:'left', fontFamily:'monospace', fontSize:12, resize:'vertical'}}
/>
<div style={{fontSize:11, color:'var(--text3)', marginTop:4}}>
Nutze Platzhalter wie {`{{weight_aktuell}}`}, {`{{protein_avg}}`}, etc.
<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 && placeholders.length > 0 && (
<div style={{
marginTop:8, padding:12, background:'var(--surface)',
border:'1px solid var(--border)', borderRadius:8,
maxHeight:300, overflow:'auto'
}}>
<div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:8}}>
<strong style={{fontSize:13}}>Verfügbare Platzhalter:</strong>
<button
onClick={() => setShowPlaceholders(false)}
style={{
background:'none', border:'none', cursor:'pointer',
fontSize:18, color:'var(--text3)'
}}
>
×
</button>
</div>
<div style={{display:'grid', gap:6}}>
{placeholders.map((p, i) => (
<button
key={i}
onClick={() => insertPlaceholder(p.key)}
style={{
textAlign:'left', padding:'6px 10px',
background:'var(--surface2)', border:'1px solid var(--border)',
borderRadius:6, cursor:'pointer', fontSize:12,
fontFamily:'monospace'
}}
>
<div style={{fontWeight:600, color:'var(--accent)'}}>{`{{${p.key}}}`}</div>
<div style={{fontSize:11, color:'var(--text3)', marginTop:2}}>
Beispiel: {p.value}
</div>
</button>
))}
</div>
</div>
)}
</div>
<div style={{display:'flex', alignItems:'center', gap:8}}>

View File

@ -31,7 +31,7 @@ function InsightCard({ ins, onDelete, defaultOpen=false }) {
onClick={()=>setOpen(o=>!o)}>
<div style={{flex:1}}>
<div style={{fontSize:13,fontWeight:600}}>
{SLUG_LABELS[ins.scope] || ins.scope}
{ins.display_name || SLUG_LABELS[ins.scope] || ins.scope}
</div>
<div style={{fontSize:11,color:'var(--text3)'}}>
{dayjs(ins.created).format('DD. MMMM YYYY, HH:mm')}
@ -310,7 +310,7 @@ export default function Analysis() {
<div style={{display:'flex',alignItems:'flex-start',gap:12}}>
<div style={{flex:1}}>
<div className="badge-container-right" style={{fontWeight:600,fontSize:15}}>
<span>{SLUG_LABELS[p.slug]||p.name}</span>
<span>{p.display_name || SLUG_LABELS[p.slug] || p.name}</span>
{aiUsage && <UsageBadge {...aiUsage} />}
</div>
{p.description && <div style={{fontSize:12,color:'var(--text3)',marginTop:2}}>{p.description}</div>}
@ -398,7 +398,7 @@ export default function Analysis() {
<div style={{display:'flex',alignItems:'center',gap:10}}>
<div style={{flex:1}}>
<div style={{fontWeight:600,fontSize:14,display:'flex',alignItems:'center',gap:8}}>
{SLUG_LABELS[p.slug]||p.name}
{p.display_name || SLUG_LABELS[p.slug] || p.name}
{!p.active && <span style={{fontSize:10,color:'#D85A30',
background:'#FCEBEB',padding:'2px 8px',borderRadius:4,fontWeight:600}}> Deaktiviert</span>}
</div>