From adb5dcea8801592b5d87bd14a3ce3829f887ed8b Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 26 Mar 2026 12:59:52 +0100 Subject: [PATCH] feat: category grouping in value table (Issue #47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FEATURE: Gruppierung nach Kategorien - Wertetabelle jetzt nach Modulen/Kategorien gruppiert - Bessere Übersicht und Zuordnung der Werte BACKEND: Category Metadata - Für normale Platzhalter: Kategorie aus Catalog (Profil, Körper, Ernährung, etc.) - Für extrahierte Werte: "Stage X - [Output Name]" - Für Rohdaten: "Stage X - Rohdaten" - Fallback: "Sonstiges" FRONTEND: Grouped Display - sortedCategories: Sortierung (Normal → Stage Outputs → Rohdaten) - Section Headers: Grauer Hintergrund mit Kategorie-Name - React.Fragment für Gruppierung SORTIERUNG: 1. Normale Kategorien (Profil, Körper, Ernährung, Training, etc.) 2. Stage Outputs (Stage 1 - Body, Stage 1 - Nutrition, etc.) 3. Rohdaten (Stage 1 - Rohdaten, Stage 2 - Rohdaten) 4. Innerhalb: Alphabetisch BEISPIEL: ┌────────────────────────────────────────────┐ │ PROFIL │ ├────────────────────────────────────────────┤ │ name │ Lars │ Name des Nutzers │ │ age │ 55 │ Alter in Jahren │ ├────────────────────────────────────────────┤ │ KÖRPER │ ├────────────────────────────────────────────┤ │ weight_... │ 85.2 kg │ Aktuelles Gewicht │ │ bmi │ 26.6 │ Body Mass Index │ ├────────────────────────────────────────────┤ │ ERNÄHRUNG │ ├────────────────────────────────────────────┤ │ kcal_avg │ 1427... │ Durchschn. Kalorien│ │ protein... │ 106g... │ Durchschn. Protein │ ├────────────────────────────────────────────┤ │ STAGE 1 - BODY │ ├────────────────────────────────────────────┤ │ ↳ bmi │ 26.6 │ Aus Stage 1 (body) │ │ ↳ trend │ sinkend │ Aus Stage 1 (body) │ ├────────────────────────────────────────────┤ │ STAGE 1 - NUTRITION │ ├────────────────────────────────────────────┤ │ ↳ kcal_... │ 1427 │ Aus Stage 1 (nutr.)│ └────────────────────────────────────────────┘ Experten-Modus zusätzlich: ├────────────────────────────────────────────┤ │ STAGE 1 - ROHDATEN │ ├────────────────────────────────────────────┤ │ 🔬 stage...│ {"bmi"..│ Rohdaten Stage 1 │ └────────────────────────────────────────────┘ version: 9.10.0 (feature) module: prompts 2.5.0, insights 1.8.0 --- backend/routers/prompts.py | 30 ++++++++++++++----- frontend/src/pages/Analysis.jsx | 52 ++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/backend/routers/prompts.py b/backend/routers/prompts.py index 35b0f59..77cd2b7 100644 --- a/backend/routers/prompts.py +++ b/backend/routers/prompts.py @@ -840,17 +840,20 @@ async def execute_unified_prompt( # Get full untruncated value value = cleaned_values.get(key, result['debug']['resolved_placeholders'].get(key, '')) - # Find description in catalog + # Find description and category in catalog desc = None - for cat_items in catalog.values(): + category = 'Sonstiges' + for cat_name, cat_items in catalog.items(): matching = [item for item in cat_items if item['key'] == key] if matching: desc = matching[0].get('description', '') + category = cat_name break metadata['placeholders'][key] = { 'value': value, - 'description': desc or '' + 'description': desc or '', + 'category': category } elif result['type'] == 'pipeline': @@ -886,10 +889,15 @@ async def execute_unified_prompt( # Add extracted values from stage outputs (individual fields) for field_key, field_data in extracted_values.items(): if field_key not in metadata['placeholders']: + # Determine category for extracted values + output_name = field_data['source_output'].replace('stage1_', '').replace('_', ' ').title() + category = f"Stage {field_data['source_stage']} - {output_name}" + metadata['placeholders'][field_key] = { 'value': field_data['value'], 'description': f"Aus Stage {field_data['source_stage']} ({field_data['source_output']})", - 'is_extracted': True # Mark as extracted for filtering + 'is_extracted': True, # Mark as extracted for filtering + 'category': category } # Collect all resolved placeholders from prompts (input placeholders) @@ -909,15 +917,20 @@ async def execute_unified_prompt( # For stage output placeholders (raw JSON), add special description if key.startswith('stage_'): - desc = f"Rohdaten Stage {key.split('_')[1]} (Basis-Analyse JSON)" + stage_parts = key.split('_') + stage_num = stage_parts[1] if len(stage_parts) > 1 else '?' + desc = f"Rohdaten Stage {stage_num} (Basis-Analyse JSON)" + category = f"Stage {stage_num} - Rohdaten" is_stage_raw = True else: - # Find description in catalog + # Find description and category in catalog desc = None - for cat_items in catalog.values(): + category = 'Sonstiges' + for cat_name, cat_items in catalog.items(): matching = [item for item in cat_items if item['key'] == key] if matching: desc = matching[0].get('description', '') + category = cat_name break desc = desc or '' is_stage_raw = False @@ -925,7 +938,8 @@ async def execute_unified_prompt( metadata['placeholders'][key] = { 'value': value if isinstance(value, str) else json.dumps(value, ensure_ascii=False), 'description': desc, - 'is_stage_raw': is_stage_raw # Mark raw stage outputs for expert mode + 'is_stage_raw': is_stage_raw, # Mark raw stage outputs for expert mode + 'category': category } # Save to database with metadata diff --git a/frontend/src/pages/Analysis.jsx b/frontend/src/pages/Analysis.jsx index cb9021c..61a232b 100644 --- a/frontend/src/pages/Analysis.jsx +++ b/frontend/src/pages/Analysis.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import React, { useState, useEffect } from 'react' import { Brain, Trash2, ChevronDown, ChevronUp } from 'lucide-react' import { api } from '../utils/api' import { useAuth } from '../context/AuthContext' @@ -51,6 +51,33 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) { const placeholderCount = Object.keys(placeholders).length const hiddenCount = Object.keys(allPlaceholders).length - placeholderCount + // Group placeholders by category + const groupedPlaceholders = Object.entries(placeholders).reduce((acc, [key, data]) => { + const category = data.category || 'Sonstiges' + if (!acc[category]) acc[category] = [] + acc[category].push([key, data]) + return acc + }, {}) + + // Sort categories: Regular categories first, then Stage outputs, then Rohdaten + const sortedCategories = Object.keys(groupedPlaceholders).sort((a, b) => { + const aIsStage = a.startsWith('Stage') + const bIsStage = b.startsWith('Stage') + const aIsRohdaten = a.includes('Rohdaten') + const bIsRohdaten = b.includes('Rohdaten') + + // Rohdaten last + if (aIsRohdaten && !bIsRohdaten) return 1 + if (!aIsRohdaten && bIsRohdaten) return -1 + + // Stage outputs after regular categories + if (!aIsStage && bIsStage) return -1 + if (aIsStage && !bIsStage) return 1 + + // Otherwise alphabetical + return a.localeCompare(b) + }) + return (
- {Object.entries(placeholders).map(([key, data]) => { + {sortedCategories.map(category => ( + + {/* Category Header */} + + + {category} + + + {/* Category Values */} + {groupedPlaceholders[category].map(([key, data]) => { const isExtracted = data.is_extracted const isStageRaw = data.is_stage_raw @@ -194,8 +236,10 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) { {data.description || '—'} - ) - })} + ) + })} + + ))}