Flexibles KI Prompt System #48
20
backend/migrations/018_prompt_display_name.sql
Normal file
20
backend/migrations/018_prompt_display_name.sql
Normal 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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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}}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user