From 0c4264de443fa97ae34cad28124de42c6e3e6170 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 25 Mar 2026 06:31:25 +0100 Subject: [PATCH] feat: display_name + placeholder picker for prompts (Issue #28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration 018: - Add display_name column to ai_prompts - Migrate existing prompts from hardcoded SLUG_LABELS - Fallback: name if display_name is NULL Backend: - PromptCreate/Update models with display_name field - create/update/duplicate endpoints handle display_name - Fallback: use name if display_name not provided Frontend: - PromptEditModal: display_name input field - Placeholder picker: button + dropdown with all placeholders - Shows example values, inserts {{placeholder}} on click - Analysis.jsx: use display_name instead of SLUG_LABELS User-facing changes: - Prompts now show custom display names (e.g. '🍽️ Ernährung') - Admin can edit display names instead of hardcoded labels - Template editor has 'Platzhalter einfügen' button - No more hardcoded SLUG_LABELS in frontend Co-Authored-By: Claude Opus 4.6 --- .../migrations/018_prompt_display_name.sql | 20 ++++ backend/models.py | 2 + backend/routers/prompts.py | 17 ++-- frontend/src/components/PromptEditModal.jsx | 99 ++++++++++++++++++- frontend/src/pages/Analysis.jsx | 6 +- 5 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 backend/migrations/018_prompt_display_name.sql diff --git a/backend/migrations/018_prompt_display_name.sql b/backend/migrations/018_prompt_display_name.sql new file mode 100644 index 0000000..045ccc0 --- /dev/null +++ b/backend/migrations/018_prompt_display_name.sql @@ -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; diff --git a/backend/models.py b/backend/models.py index 9bd224e..8b2d846 100644 --- a/backend/models.py +++ b/backend/models.py @@ -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 diff --git a/backend/routers/prompts.py b/backend/routers/prompts.py index bee25f5..7cf1b4e 100644 --- a/backend/routers/prompts.py +++ b/backend/routers/prompts.py @@ -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']) ) diff --git a/frontend/src/components/PromptEditModal.jsx b/frontend/src/components/PromptEditModal.jsx index b1f4a64..f9bc58f 100644 --- a/frontend/src/components/PromptEditModal.jsx +++ b/frontend/src/components/PromptEditModal.jsx @@ -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 (
)} +
+ + setDisplayName(e.target.value)} + placeholder="z.B. 🍽️ Protein-Analyse" + style={{width:'100%', textAlign:'left'}} + /> +
+ Leer lassen = Titel wird verwendet +
+
+