feat: category grouping in value table (Issue #47)
All checks were successful
Deploy Development / deploy (push) Successful in 52s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

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
This commit is contained in:
Lars 2026-03-26 12:59:52 +01:00
parent da803da816
commit adb5dcea88
2 changed files with 70 additions and 12 deletions

View File

@ -840,17 +840,20 @@ async def execute_unified_prompt(
# Get full untruncated value # Get full untruncated value
value = cleaned_values.get(key, result['debug']['resolved_placeholders'].get(key, '')) value = cleaned_values.get(key, result['debug']['resolved_placeholders'].get(key, ''))
# Find description in catalog # Find description and category in catalog
desc = None 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] matching = [item for item in cat_items if item['key'] == key]
if matching: if matching:
desc = matching[0].get('description', '') desc = matching[0].get('description', '')
category = cat_name
break break
metadata['placeholders'][key] = { metadata['placeholders'][key] = {
'value': value, 'value': value,
'description': desc or '' 'description': desc or '',
'category': category
} }
elif result['type'] == 'pipeline': elif result['type'] == 'pipeline':
@ -886,10 +889,15 @@ async def execute_unified_prompt(
# Add extracted values from stage outputs (individual fields) # Add extracted values from stage outputs (individual fields)
for field_key, field_data in extracted_values.items(): for field_key, field_data in extracted_values.items():
if field_key not in metadata['placeholders']: 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] = { metadata['placeholders'][field_key] = {
'value': field_data['value'], 'value': field_data['value'],
'description': f"Aus Stage {field_data['source_stage']} ({field_data['source_output']})", '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) # 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 # For stage output placeholders (raw JSON), add special description
if key.startswith('stage_'): 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 is_stage_raw = True
else: else:
# Find description in catalog # Find description and category in catalog
desc = None 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] matching = [item for item in cat_items if item['key'] == key]
if matching: if matching:
desc = matching[0].get('description', '') desc = matching[0].get('description', '')
category = cat_name
break break
desc = desc or '' desc = desc or ''
is_stage_raw = False is_stage_raw = False
@ -925,7 +938,8 @@ async def execute_unified_prompt(
metadata['placeholders'][key] = { metadata['placeholders'][key] = {
'value': value if isinstance(value, str) else json.dumps(value, ensure_ascii=False), 'value': value if isinstance(value, str) else json.dumps(value, ensure_ascii=False),
'description': desc, '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 # Save to database with metadata

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { Brain, Trash2, ChevronDown, ChevronUp } from 'lucide-react' import { Brain, Trash2, ChevronDown, ChevronUp } from 'lucide-react'
import { api } from '../utils/api' import { api } from '../utils/api'
import { useAuth } from '../context/AuthContext' import { useAuth } from '../context/AuthContext'
@ -51,6 +51,33 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
const placeholderCount = Object.keys(placeholders).length const placeholderCount = Object.keys(placeholders).length
const hiddenCount = Object.keys(allPlaceholders).length - placeholderCount 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 ( return (
<div className="card section-gap" style={{borderLeft:`3px solid var(--accent)`}}> <div className="card section-gap" style={{borderLeft:`3px solid var(--accent)`}}>
<div style={{display:'flex',alignItems:'center',gap:8,marginBottom:open?12:0,cursor:'pointer'}} <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:open?12:0,cursor:'pointer'}}
@ -152,7 +179,22 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{Object.entries(placeholders).map(([key, data]) => { {sortedCategories.map(category => (
<React.Fragment key={category}>
{/* Category Header */}
<tr style={{ background: 'var(--surface2)', borderTop: '2px solid var(--border)' }}>
<td colSpan="3" style={{
padding: '8px',
fontWeight: 600,
fontSize: 11,
color: 'var(--text2)',
letterSpacing: '0.5px'
}}>
{category}
</td>
</tr>
{/* Category Values */}
{groupedPlaceholders[category].map(([key, data]) => {
const isExtracted = data.is_extracted const isExtracted = data.is_extracted
const isStageRaw = data.is_stage_raw const isStageRaw = data.is_stage_raw
@ -194,8 +236,10 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
{data.description || '—'} {data.description || '—'}
</td> </td>
</tr> </tr>
) )
})} })}
</React.Fragment>
))}
</tbody> </tbody>
</table> </table>
</div> </div>