From 5e7ef718e036118ca8e9664594d9b3cf73a7c249 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 25 Mar 2026 06:44:22 +0100 Subject: [PATCH] fix: placeholder picker improvements + insight display names (Issue #28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - get_placeholder_catalog(): grouped placeholders with descriptions - Returns {category: [{key, description, example}]} format - Categories: Profil, Körper, Ernährung, Training, Schlaf, Vitalwerte, Zeitraum Frontend - Placeholder Picker: - Grouped by category with visual separation - Search/filter across keys and descriptions - Hover effects for better UX - Insert at cursor position (not at end) - Shows: key + description + example value - 'Keine Platzhalter gefunden' message when filtered Frontend - Insight Display Names: - InsightCard receives prompts array - Finds matching prompt by scope/slug - Shows prompt.display_name instead of hardcoded SLUG_LABELS - History tab also shows display_name in group headers - Fallback chain: display_name → SLUG_LABELS → scope User-facing improvements: ✓ Platzhalter zeigen echte Daten statt Zahlen ✓ Durchsuchbar + filterbar ✓ Einfügen an Cursor-Position ✓ Insights zeigen custom Namen (z.B. '🍽️ Meine Ernährung') Co-Authored-By: Claude Opus 4.6 --- backend/placeholder_resolver.py | 76 ++++++++++ backend/routers/prompts.py | 9 +- frontend/src/components/PromptEditModal.jsx | 150 +++++++++++++++----- frontend/src/pages/Analysis.jsx | 16 ++- 4 files changed, 208 insertions(+), 43 deletions(-) diff --git a/backend/placeholder_resolver.py b/backend/placeholder_resolver.py index 731d6b8..6d4d409 100644 --- a/backend/placeholder_resolver.py +++ b/backend/placeholder_resolver.py @@ -306,3 +306,79 @@ def get_placeholder_example_values(profile_id: str) -> Dict[str, str]: examples[placeholder] = f"[Fehler: {str(e)}]" return examples + + +def get_placeholder_catalog(profile_id: str) -> Dict[str, List[Dict[str, str]]]: + """ + Get grouped placeholder catalog with descriptions and example values. + + Args: + profile_id: User profile ID + + Returns: + Dict mapping category to list of {key, description, example} + """ + # Placeholder definitions with descriptions + placeholders = { + 'Profil': [ + ('name', 'Name des Nutzers'), + ('age', 'Alter in Jahren'), + ('height', 'Körpergröße in cm'), + ('geschlecht', 'Geschlecht'), + ], + 'Körper': [ + ('weight_aktuell', 'Aktuelles Gewicht in kg'), + ('weight_trend', 'Gewichtstrend (7d/30d)'), + ('kf_aktuell', 'Aktueller Körperfettanteil in %'), + ('bmi', 'Body Mass Index'), + ], + 'Ernährung': [ + ('kcal_avg', 'Durchschn. Kalorien (30d)'), + ('protein_avg', 'Durchschn. Protein in g (30d)'), + ('carb_avg', 'Durchschn. Kohlenhydrate in g (30d)'), + ('fat_avg', 'Durchschn. Fett in g (30d)'), + ], + 'Training': [ + ('activity_summary', 'Aktivitäts-Zusammenfassung (7d)'), + ('trainingstyp_verteilung', 'Verteilung nach Trainingstypen'), + ], + 'Schlaf & Erholung': [ + ('sleep_avg_duration', 'Durchschn. Schlafdauer (7d)'), + ('sleep_avg_quality', 'Durchschn. Schlafqualität (7d)'), + ('rest_days_count', 'Anzahl Ruhetage (30d)'), + ], + 'Vitalwerte': [ + ('vitals_avg_hr', 'Durchschn. Ruhepuls (7d)'), + ('vitals_avg_hrv', 'Durchschn. HRV (7d)'), + ('vitals_vo2_max', 'Aktueller VO2 Max'), + ], + 'Zeitraum': [ + ('datum_heute', 'Heutiges Datum'), + ('zeitraum_7d', '7-Tage-Zeitraum'), + ('zeitraum_30d', '30-Tage-Zeitraum'), + ], + } + + catalog = {} + + for category, items in placeholders.items(): + catalog[category] = [] + for key, description in items: + placeholder = f'{{{{{key}}}}}' + # Get example value if resolver exists + resolver = PLACEHOLDER_MAP.get(placeholder) + if resolver: + try: + example = resolver(profile_id) + except Exception: + example = '[Nicht verfügbar]' + else: + example = '[Nicht implementiert]' + + catalog[category].append({ + 'key': key, + 'description': description, + 'example': str(example) + }) + + return catalog diff --git a/backend/routers/prompts.py b/backend/routers/prompts.py index 7cf1b4e..9d5340c 100644 --- a/backend/routers/prompts.py +++ b/backend/routers/prompts.py @@ -17,7 +17,8 @@ from placeholder_resolver import ( resolve_placeholders, get_unknown_placeholders, get_placeholder_example_values, - get_available_placeholders + get_available_placeholders, + get_placeholder_catalog ) # Environment variables @@ -204,13 +205,13 @@ def preview_prompt(data: dict, session: dict=Depends(require_auth)): @router.get("/placeholders") def list_placeholders(session: dict=Depends(require_auth)): """ - Get list of available placeholders with example values. + Get grouped catalog of available placeholders with descriptions and examples. Returns: - Dict mapping placeholder to example value using current user's data + Dict mapping category to list of {key, description, example} """ profile_id = session['profile_id'] - return get_placeholder_example_values(profile_id) + return get_placeholder_catalog(profile_id) # ── KI-Assisted Prompt Engineering ─────────────────────────────────────────── diff --git a/frontend/src/components/PromptEditModal.jsx b/frontend/src/components/PromptEditModal.jsx index f9bc58f..b9684da 100644 --- a/frontend/src/components/PromptEditModal.jsx +++ b/frontend/src/components/PromptEditModal.jsx @@ -15,7 +15,9 @@ export default function PromptEditModal({ prompt, onSave, onClose }) { const [unknownPlaceholders, setUnknownPlaceholders] = useState([]) const [showGenerator, setShowGenerator] = useState(false) const [showPlaceholders, setShowPlaceholders] = useState(false) - const [placeholders, setPlaceholders] = useState([]) + 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) @@ -146,22 +148,36 @@ 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) + setPlaceholders(data) setShowPlaceholders(true) + setPlaceholderFilter('') } catch (e) { alert('Fehler beim Laden der Platzhalter: ' + e.message) } } const insertPlaceholder = (key) => { - setTemplate(prev => prev + ` {{${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) } @@ -281,6 +297,7 @@ export default function PromptEditModal({ prompt, onSave, onClose }) {