mitai-jinkendo/frontend/src/pages/Analysis.jsx

724 lines
31 KiB
React
Raw Normal View History

2026-04-05 09:04:08 +02:00
import React, { useState, useEffect, useMemo } from 'react'
import { Brain, Trash2, ChevronDown, ChevronUp } from 'lucide-react'
import { Link } from 'react-router-dom'
import { api } from '../utils/api'
import { getWorkflowDisplayContent } from '../utils/workflowDisplay'
import {
ANALYSIS_CATEGORY_ORDER,
ANALYSIS_CATEGORY_LABELS,
} from '../config/analysisCategories'
import { useAuth } from '../context/AuthContext'
import Markdown from '../utils/Markdown'
import UsageBadge from '../components/UsageBadge'
import WorkflowDebugPanel from '../components/WorkflowDebugPanel'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
dayjs.locale('de')
// Legacy fallback labels (display_name takes precedence)
const SLUG_LABELS = {
pipeline: '🔬 Mehrstufige Gesamtanalyse'
}
2026-04-05 09:04:08 +02:00
function sortAnalysisCategoryKeys(keys) {
return [...keys].sort((a, b) => {
const na = String(a).toLowerCase()
const nb = String(b).toLowerCase()
const ia = ANALYSIS_CATEGORY_ORDER.indexOf(na)
const ib = ANALYSIS_CATEGORY_ORDER.indexOf(nb)
if (ia === -1 && ib === -1) return String(a).localeCompare(String(b), 'de')
if (ia === -1) return 1
if (ib === -1) return -1
return ia - ib
})
}
function analysisCategoryLabel(key) {
const k = String(key).toLowerCase()
return ANALYSIS_CATEGORY_LABELS[k] || String(key)
}
/** Analyse-Angebote: klassische Pipelines + graphbasierte KI-Workflows (`type === 'workflow'`) */
function isAnalysisOfferPrompt(p) {
return p.active && (p.type === 'pipeline' || p.type === 'workflow')
}
2026-04-05 09:04:08 +02:00
/** Pipeline-Prompts nach `category` gruppieren (Backend-Feld), innerhalb Gruppe nach sort_order */
function buildPipelineGroups(pipelinePrompts) {
const m = new Map()
for (const p of pipelinePrompts) {
const raw =
p.category != null && String(p.category).trim() !== ''
? String(p.category).trim()
: 'ganzheitlich'
if (!m.has(raw)) m.set(raw, [])
m.get(raw).push(p)
}
for (const arr of m.values()) {
arr.sort((a, b) => (Number(a.sort_order) || 0) - (Number(b.sort_order) || 0))
}
return sortAnalysisCategoryKeys([...m.keys()]).map(cat => ({
categoryKey: cat,
label: analysisCategoryLabel(cat),
prompts: m.get(cat),
}))
}
function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
const [open, setOpen] = useState(defaultOpen)
feat: untruncated values + smart base prompt display (Issue #47) FEATURE: Volle Werte (nicht abgeschnitten) - Backend holt ungekürzten Werte direkt von placeholder_resolver - get_placeholder_example_values() statt debug.resolved_placeholders - Debug bleibt gekürzt (100 chars), Metadata ungekürzt FEATURE: Smart Display für Basis-Prompts - Basis-Prompts mit JSON-Output: Nur Wertetabelle anzeigen - JSON-Output in Collapsible "Technische Daten" verschoben - Wertetabelle auto-expanded bei Basis-Prompts - Pipeline + Text-Prompts: Wie bisher (Content + Wertetabelle) UI: Bessere Wertetabelle - Werte: word-break + max-width (400px) → kein Overflow - Alle Spalten: verticalAlign top für bessere Lesbarkeit - Platzhalter: nowrap (keine Umbrüche) BEISPIEL: ┌─────────────────────────────────────────┐ │ ℹ️ Basis-Prompt Rohdaten │ │ [Technische Daten anzeigen ▼] │ │ │ │ 📊 Verwendete Werte (8) ▼ ← expanded │ │ ┌──────────────────────────────────────┐│ │ │ Platzhalter │ Vollständiger Wert... ││ │ │ kcal_avg │ 1427 kcal/Tag (Ø 30...││ ← ungekürzt │ └──────────────────────────────────────┘│ └─────────────────────────────────────────┘ version: 9.7.0 (feature) module: prompts 2.2.0, insights 1.5.0
2026-03-26 12:37:52 +01:00
// Parse metadata early to determine showOnlyValues
const metadataRaw = ins.metadata ? (typeof ins.metadata === 'string' ? JSON.parse(ins.metadata) : ins.metadata) : null
const isBasePrompt = metadataRaw?.prompt_type === 'base'
const isJsonOutput = ins.content && (ins.content.trim().startsWith('{') || ins.content.trim().startsWith('['))
const placeholdersRaw = metadataRaw?.placeholders || {}
const showOnlyValues = isBasePrompt && isJsonOutput && Object.keys(placeholdersRaw).length > 0
const [showValues, setShowValues] = useState(showOnlyValues) // Auto-expand for base prompts with JSON
feat: expert mode + stage outputs in value table (Issue #47) FEATURE: Experten-Modus 🔬 - Toggle-Button in Wertetabelle - Normal: Nur gefüllte Werte anzeigen - Experten: Alle Platzhalter inkl. leere/technische - Anzeige: "(+X ausgeblendet)" wenn Werte gefiltert - Button-Style: Accent wenn aktiv FILTER: Leere Werte ausblenden (Normal-Modus) - Filtert: '', 'nicht verfügbar', '[Nicht verfügbar]' - Zeigt nur relevante Nutzer-Daten - Experten-Modus zeigt alles FEATURE: Stage-Outputs in Wertetabelle ✅ ROOT CAUSE: stage_N_key Platzhalter hatten keine Werte - Stage-Outputs (z.B. stage_1_body) sind Basis-Analysen-Ergebnisse - Wurden nicht in cleaned_values gefunden (nur statische Platzhalter) FIX: - Collect stage outputs aus result.debug.stages[].output - Store als stage_N_key dict - Lookup: erst stage_outputs, dann cleaned_values - Description: "Output aus Stage X (Basis-Analyse)" - JSON-Werte automatisch serialisiert BEISPIEL Pipeline-Wertetabelle: ┌──────────────────────────────────────────────┐ │ 📊 Verwendete Werte (8) (+3 ausgeblendet) 🔬│ │ ┌──────────────────────────────────────────┐ │ │ │ weight_aktuell │ 85.2 kg │ Gewicht │ │ │ │ stage_1_body │ {"bmi":...│ Output...│ │ ← Stage output! │ │ stage_1_nutr... │ {"kcal"...│ Output...│ │ │ └──────────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ AKTIVIERUNG Experten-Modus: 1. Analyse öffnen 2. "📊 Verwendete Werte" aufklappen 3. Button "🔬 Experten-Modus" klicken 4. Zeigt alle Platzhalter (auch leere stage outputs) version: 9.8.0 (feature) module: prompts 2.3.0, insights 1.6.0
2026-03-26 12:44:28 +01:00
const [expertMode, setExpertMode] = useState(false) // Show empty/technical placeholders
// Find matching prompt to get display_name
const prompt = prompts.find(p => p.slug === ins.scope)
const displayName =
prompt?.display_name || prompt?.name || SLUG_LABELS[ins.scope] || ins.scope
feat: untruncated values + smart base prompt display (Issue #47) FEATURE: Volle Werte (nicht abgeschnitten) - Backend holt ungekürzten Werte direkt von placeholder_resolver - get_placeholder_example_values() statt debug.resolved_placeholders - Debug bleibt gekürzt (100 chars), Metadata ungekürzt FEATURE: Smart Display für Basis-Prompts - Basis-Prompts mit JSON-Output: Nur Wertetabelle anzeigen - JSON-Output in Collapsible "Technische Daten" verschoben - Wertetabelle auto-expanded bei Basis-Prompts - Pipeline + Text-Prompts: Wie bisher (Content + Wertetabelle) UI: Bessere Wertetabelle - Werte: word-break + max-width (400px) → kein Overflow - Alle Spalten: verticalAlign top für bessere Lesbarkeit - Platzhalter: nowrap (keine Umbrüche) BEISPIEL: ┌─────────────────────────────────────────┐ │ ℹ️ Basis-Prompt Rohdaten │ │ [Technische Daten anzeigen ▼] │ │ │ │ 📊 Verwendete Werte (8) ▼ ← expanded │ │ ┌──────────────────────────────────────┐│ │ │ Platzhalter │ Vollständiger Wert... ││ │ │ kcal_avg │ 1427 kcal/Tag (Ø 30...││ ← ungekürzt │ └──────────────────────────────────────┘│ └─────────────────────────────────────────┘ version: 9.7.0 (feature) module: prompts 2.2.0, insights 1.5.0
2026-03-26 12:37:52 +01:00
// Use already-parsed metadata
const metadata = metadataRaw
feat: expert mode + stage outputs in value table (Issue #47) FEATURE: Experten-Modus 🔬 - Toggle-Button in Wertetabelle - Normal: Nur gefüllte Werte anzeigen - Experten: Alle Platzhalter inkl. leere/technische - Anzeige: "(+X ausgeblendet)" wenn Werte gefiltert - Button-Style: Accent wenn aktiv FILTER: Leere Werte ausblenden (Normal-Modus) - Filtert: '', 'nicht verfügbar', '[Nicht verfügbar]' - Zeigt nur relevante Nutzer-Daten - Experten-Modus zeigt alles FEATURE: Stage-Outputs in Wertetabelle ✅ ROOT CAUSE: stage_N_key Platzhalter hatten keine Werte - Stage-Outputs (z.B. stage_1_body) sind Basis-Analysen-Ergebnisse - Wurden nicht in cleaned_values gefunden (nur statische Platzhalter) FIX: - Collect stage outputs aus result.debug.stages[].output - Store als stage_N_key dict - Lookup: erst stage_outputs, dann cleaned_values - Description: "Output aus Stage X (Basis-Analyse)" - JSON-Werte automatisch serialisiert BEISPIEL Pipeline-Wertetabelle: ┌──────────────────────────────────────────────┐ │ 📊 Verwendete Werte (8) (+3 ausgeblendet) 🔬│ │ ┌──────────────────────────────────────────┐ │ │ │ weight_aktuell │ 85.2 kg │ Gewicht │ │ │ │ stage_1_body │ {"bmi":...│ Output...│ │ ← Stage output! │ │ stage_1_nutr... │ {"kcal"...│ Output...│ │ │ └──────────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ AKTIVIERUNG Experten-Modus: 1. Analyse öffnen 2. "📊 Verwendete Werte" aufklappen 3. Button "🔬 Experten-Modus" klicken 4. Zeigt alle Platzhalter (auch leere stage outputs) version: 9.8.0 (feature) module: prompts 2.3.0, insights 1.6.0
2026-03-26 12:44:28 +01:00
const allPlaceholders = placeholdersRaw
feat: extract individual values from stage outputs (Issue #47) FEATURE: Basis-Analysen Einzelwerte Vorher: stage_1_body → {"bmi": 26.6, "weight": "85.2kg"} (1 Zeile) Jetzt: bmi → 26.6 (eigene Zeile) weight → 85.2kg (eigene Zeile) BACKEND: JSON-Extraktion - Stage outputs (JSON) → extract individual fields - extracted_values dict sammelt alle Einzelwerte - Deduplizierung: Gleiche Keys nur einmal - Flags: - is_extracted: true → Wert aus Stage-Output extrahiert - is_stage_raw: true → Rohdaten (JSON) nur Experten-Modus BEISPIEL Stage 1 Output: { "stage_1_body": { "bmi": 26.6, "weight": "85.2 kg", "trend": "sinkend" } } → Metadata: { "bmi": { value: "26.6", description: "Aus Stage 1 (stage_1_body)", is_extracted: true }, "weight": { value: "85.2 kg", description: "Aus Stage 1 (stage_1_body)", is_extracted: true }, "stage_1_body": { value: "{\"bmi\": 26.6, ...}", description: "Rohdaten Stage 1 (Basis-Analyse JSON)", is_stage_raw: true } } FRONTEND: Smart Filtering Normal-Modus: - Zeigt: Einzelwerte (bmi, weight, trend) - Versteckt: Rohdaten (stage_1_body JSON) - Filter: is_stage_raw === false Experten-Modus: - Zeigt: Alles (Einzelwerte + Rohdaten) - Rohdaten: Grauer Hintergrund + 🔬 Icon VISUAL Indicators: ↳ bmi → Extrahierter Wert (grün) weight → Normaler Platzhalter (accent) 🔬 stage_1_* → Rohdaten JSON (grau, klein, nur Experten) ERGEBNIS: ┌──────────────────────────────────────────┐ │ 📊 Verwendete Werte (8) (+2 ausgeblendet)│ │ ┌────────────────────────────────────────┐│ │ │ weight_aktuell │ 85.2 kg │ Gewicht ││ ← Normal │ │ ↳ bmi │ 26.6 │ Aus St..││ ← Extrahiert │ │ ↳ trend │ sinkend │ Aus St..││ ← Extrahiert │ └────────────────────────────────────────┘│ └──────────────────────────────────────────┘ Experten-Modus zusätzlich: │ 🔬 stage_1_body │ {"bmi":...│ Rohdaten││ ← JSON version: 9.9.0 (feature) module: prompts 2.4.0, insights 1.7.0
2026-03-26 12:55:53 +01:00
// Filter placeholders: In normal mode, hide empty values and raw stage outputs
feat: expert mode + stage outputs in value table (Issue #47) FEATURE: Experten-Modus 🔬 - Toggle-Button in Wertetabelle - Normal: Nur gefüllte Werte anzeigen - Experten: Alle Platzhalter inkl. leere/technische - Anzeige: "(+X ausgeblendet)" wenn Werte gefiltert - Button-Style: Accent wenn aktiv FILTER: Leere Werte ausblenden (Normal-Modus) - Filtert: '', 'nicht verfügbar', '[Nicht verfügbar]' - Zeigt nur relevante Nutzer-Daten - Experten-Modus zeigt alles FEATURE: Stage-Outputs in Wertetabelle ✅ ROOT CAUSE: stage_N_key Platzhalter hatten keine Werte - Stage-Outputs (z.B. stage_1_body) sind Basis-Analysen-Ergebnisse - Wurden nicht in cleaned_values gefunden (nur statische Platzhalter) FIX: - Collect stage outputs aus result.debug.stages[].output - Store als stage_N_key dict - Lookup: erst stage_outputs, dann cleaned_values - Description: "Output aus Stage X (Basis-Analyse)" - JSON-Werte automatisch serialisiert BEISPIEL Pipeline-Wertetabelle: ┌──────────────────────────────────────────────┐ │ 📊 Verwendete Werte (8) (+3 ausgeblendet) 🔬│ │ ┌──────────────────────────────────────────┐ │ │ │ weight_aktuell │ 85.2 kg │ Gewicht │ │ │ │ stage_1_body │ {"bmi":...│ Output...│ │ ← Stage output! │ │ stage_1_nutr... │ {"kcal"...│ Output...│ │ │ └──────────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ AKTIVIERUNG Experten-Modus: 1. Analyse öffnen 2. "📊 Verwendete Werte" aufklappen 3. Button "🔬 Experten-Modus" klicken 4. Zeigt alle Platzhalter (auch leere stage outputs) version: 9.8.0 (feature) module: prompts 2.3.0, insights 1.6.0
2026-03-26 12:44:28 +01:00
const placeholders = expertMode
? allPlaceholders
: Object.fromEntries(
Object.entries(allPlaceholders).filter(([key, data]) => {
feat: extract individual values from stage outputs (Issue #47) FEATURE: Basis-Analysen Einzelwerte Vorher: stage_1_body → {"bmi": 26.6, "weight": "85.2kg"} (1 Zeile) Jetzt: bmi → 26.6 (eigene Zeile) weight → 85.2kg (eigene Zeile) BACKEND: JSON-Extraktion - Stage outputs (JSON) → extract individual fields - extracted_values dict sammelt alle Einzelwerte - Deduplizierung: Gleiche Keys nur einmal - Flags: - is_extracted: true → Wert aus Stage-Output extrahiert - is_stage_raw: true → Rohdaten (JSON) nur Experten-Modus BEISPIEL Stage 1 Output: { "stage_1_body": { "bmi": 26.6, "weight": "85.2 kg", "trend": "sinkend" } } → Metadata: { "bmi": { value: "26.6", description: "Aus Stage 1 (stage_1_body)", is_extracted: true }, "weight": { value: "85.2 kg", description: "Aus Stage 1 (stage_1_body)", is_extracted: true }, "stage_1_body": { value: "{\"bmi\": 26.6, ...}", description: "Rohdaten Stage 1 (Basis-Analyse JSON)", is_stage_raw: true } } FRONTEND: Smart Filtering Normal-Modus: - Zeigt: Einzelwerte (bmi, weight, trend) - Versteckt: Rohdaten (stage_1_body JSON) - Filter: is_stage_raw === false Experten-Modus: - Zeigt: Alles (Einzelwerte + Rohdaten) - Rohdaten: Grauer Hintergrund + 🔬 Icon VISUAL Indicators: ↳ bmi → Extrahierter Wert (grün) weight → Normaler Platzhalter (accent) 🔬 stage_1_* → Rohdaten JSON (grau, klein, nur Experten) ERGEBNIS: ┌──────────────────────────────────────────┐ │ 📊 Verwendete Werte (8) (+2 ausgeblendet)│ │ ┌────────────────────────────────────────┐│ │ │ weight_aktuell │ 85.2 kg │ Gewicht ││ ← Normal │ │ ↳ bmi │ 26.6 │ Aus St..││ ← Extrahiert │ │ ↳ trend │ sinkend │ Aus St..││ ← Extrahiert │ └────────────────────────────────────────┘│ └──────────────────────────────────────────┘ Experten-Modus zusätzlich: │ 🔬 stage_1_body │ {"bmi":...│ Rohdaten││ ← JSON version: 9.9.0 (feature) module: prompts 2.4.0, insights 1.7.0
2026-03-26 12:55:53 +01:00
// Hide raw stage outputs (JSON) in normal mode
if (data.is_stage_raw) return false
// Hide empty values
feat: expert mode + stage outputs in value table (Issue #47) FEATURE: Experten-Modus 🔬 - Toggle-Button in Wertetabelle - Normal: Nur gefüllte Werte anzeigen - Experten: Alle Platzhalter inkl. leere/technische - Anzeige: "(+X ausgeblendet)" wenn Werte gefiltert - Button-Style: Accent wenn aktiv FILTER: Leere Werte ausblenden (Normal-Modus) - Filtert: '', 'nicht verfügbar', '[Nicht verfügbar]' - Zeigt nur relevante Nutzer-Daten - Experten-Modus zeigt alles FEATURE: Stage-Outputs in Wertetabelle ✅ ROOT CAUSE: stage_N_key Platzhalter hatten keine Werte - Stage-Outputs (z.B. stage_1_body) sind Basis-Analysen-Ergebnisse - Wurden nicht in cleaned_values gefunden (nur statische Platzhalter) FIX: - Collect stage outputs aus result.debug.stages[].output - Store als stage_N_key dict - Lookup: erst stage_outputs, dann cleaned_values - Description: "Output aus Stage X (Basis-Analyse)" - JSON-Werte automatisch serialisiert BEISPIEL Pipeline-Wertetabelle: ┌──────────────────────────────────────────────┐ │ 📊 Verwendete Werte (8) (+3 ausgeblendet) 🔬│ │ ┌──────────────────────────────────────────┐ │ │ │ weight_aktuell │ 85.2 kg │ Gewicht │ │ │ │ stage_1_body │ {"bmi":...│ Output...│ │ ← Stage output! │ │ stage_1_nutr... │ {"kcal"...│ Output...│ │ │ └──────────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ AKTIVIERUNG Experten-Modus: 1. Analyse öffnen 2. "📊 Verwendete Werte" aufklappen 3. Button "🔬 Experten-Modus" klicken 4. Zeigt alle Platzhalter (auch leere stage outputs) version: 9.8.0 (feature) module: prompts 2.3.0, insights 1.6.0
2026-03-26 12:44:28 +01:00
const val = data.value || ''
return val.trim() !== '' && val !== 'nicht verfügbar' && val !== '[Nicht verfügbar]'
})
)
const placeholderCount = Object.keys(placeholders).length
feat: expert mode + stage outputs in value table (Issue #47) FEATURE: Experten-Modus 🔬 - Toggle-Button in Wertetabelle - Normal: Nur gefüllte Werte anzeigen - Experten: Alle Platzhalter inkl. leere/technische - Anzeige: "(+X ausgeblendet)" wenn Werte gefiltert - Button-Style: Accent wenn aktiv FILTER: Leere Werte ausblenden (Normal-Modus) - Filtert: '', 'nicht verfügbar', '[Nicht verfügbar]' - Zeigt nur relevante Nutzer-Daten - Experten-Modus zeigt alles FEATURE: Stage-Outputs in Wertetabelle ✅ ROOT CAUSE: stage_N_key Platzhalter hatten keine Werte - Stage-Outputs (z.B. stage_1_body) sind Basis-Analysen-Ergebnisse - Wurden nicht in cleaned_values gefunden (nur statische Platzhalter) FIX: - Collect stage outputs aus result.debug.stages[].output - Store als stage_N_key dict - Lookup: erst stage_outputs, dann cleaned_values - Description: "Output aus Stage X (Basis-Analyse)" - JSON-Werte automatisch serialisiert BEISPIEL Pipeline-Wertetabelle: ┌──────────────────────────────────────────────┐ │ 📊 Verwendete Werte (8) (+3 ausgeblendet) 🔬│ │ ┌──────────────────────────────────────────┐ │ │ │ weight_aktuell │ 85.2 kg │ Gewicht │ │ │ │ stage_1_body │ {"bmi":...│ Output...│ │ ← Stage output! │ │ stage_1_nutr... │ {"kcal"...│ Output...│ │ │ └──────────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ AKTIVIERUNG Experten-Modus: 1. Analyse öffnen 2. "📊 Verwendete Werte" aufklappen 3. Button "🔬 Experten-Modus" klicken 4. Zeigt alle Platzhalter (auch leere stage outputs) version: 9.8.0 (feature) module: prompts 2.3.0, insights 1.6.0
2026-03-26 12:44:28 +01:00
const hiddenCount = Object.keys(allPlaceholders).length - placeholderCount
feat: category grouping in value table (Issue #47) 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
2026-03-26 12:59:52 +01:00
// 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 (
<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'}}
onClick={()=>setOpen(o=>!o)}>
<div style={{flex:1}}>
<div style={{fontSize:13,fontWeight:600}}>
{displayName}
</div>
<div style={{fontSize:11,color:'var(--text3)'}}>
{dayjs(ins.created).format('DD. MMMM YYYY, HH:mm')}
</div>
</div>
<button style={{background:'none',border:'none',cursor:'pointer',color:'var(--text3)',padding:4}}
onClick={e=>{e.stopPropagation();onDelete(ins.id)}}>
<Trash2 size={13}/>
</button>
{open ? <ChevronUp size={16} color="var(--text3)"/> : <ChevronDown size={16} color="var(--text3)"/>}
</div>
{open && (
<>
feat: untruncated values + smart base prompt display (Issue #47) FEATURE: Volle Werte (nicht abgeschnitten) - Backend holt ungekürzten Werte direkt von placeholder_resolver - get_placeholder_example_values() statt debug.resolved_placeholders - Debug bleibt gekürzt (100 chars), Metadata ungekürzt FEATURE: Smart Display für Basis-Prompts - Basis-Prompts mit JSON-Output: Nur Wertetabelle anzeigen - JSON-Output in Collapsible "Technische Daten" verschoben - Wertetabelle auto-expanded bei Basis-Prompts - Pipeline + Text-Prompts: Wie bisher (Content + Wertetabelle) UI: Bessere Wertetabelle - Werte: word-break + max-width (400px) → kein Overflow - Alle Spalten: verticalAlign top für bessere Lesbarkeit - Platzhalter: nowrap (keine Umbrüche) BEISPIEL: ┌─────────────────────────────────────────┐ │ ℹ️ Basis-Prompt Rohdaten │ │ [Technische Daten anzeigen ▼] │ │ │ │ 📊 Verwendete Werte (8) ▼ ← expanded │ │ ┌──────────────────────────────────────┐│ │ │ Platzhalter │ Vollständiger Wert... ││ │ │ kcal_avg │ 1427 kcal/Tag (Ø 30...││ ← ungekürzt │ └──────────────────────────────────────┘│ └─────────────────────────────────────────┘ version: 9.7.0 (feature) module: prompts 2.2.0, insights 1.5.0
2026-03-26 12:37:52 +01:00
{/* For base prompts with JSON: Only show value table */}
{showOnlyValues && (
<div style={{ padding: '12px 16px', background: 'var(--surface)', borderRadius: 8, marginBottom: 12 }}>
<div style={{ fontSize: 11, color: 'var(--text3)', marginBottom: 8 }}>
ℹ️ Basis-Prompt Rohdaten (JSON-Struktur für technische Nutzung)
</div>
<details style={{ fontSize: 11, color: 'var(--text3)' }}>
<summary style={{ cursor: 'pointer' }}>Technische Daten anzeigen</summary>
<pre style={{
marginTop: 8,
padding: 8,
background: 'var(--bg)',
borderRadius: 4,
overflow: 'auto',
fontSize: 10,
fontFamily: 'monospace'
}}>
{ins.content}
</pre>
</details>
</div>
)}
{/* For other prompts: Show full content */}
{!showOnlyValues && <Markdown text={ins.content}/>}
{/* Value Table */}
{placeholderCount > 0 && (
<div style={{ marginTop: 16, borderTop: '1px solid var(--border)', paddingTop: 12 }}>
feat: expert mode + stage outputs in value table (Issue #47) FEATURE: Experten-Modus 🔬 - Toggle-Button in Wertetabelle - Normal: Nur gefüllte Werte anzeigen - Experten: Alle Platzhalter inkl. leere/technische - Anzeige: "(+X ausgeblendet)" wenn Werte gefiltert - Button-Style: Accent wenn aktiv FILTER: Leere Werte ausblenden (Normal-Modus) - Filtert: '', 'nicht verfügbar', '[Nicht verfügbar]' - Zeigt nur relevante Nutzer-Daten - Experten-Modus zeigt alles FEATURE: Stage-Outputs in Wertetabelle ✅ ROOT CAUSE: stage_N_key Platzhalter hatten keine Werte - Stage-Outputs (z.B. stage_1_body) sind Basis-Analysen-Ergebnisse - Wurden nicht in cleaned_values gefunden (nur statische Platzhalter) FIX: - Collect stage outputs aus result.debug.stages[].output - Store als stage_N_key dict - Lookup: erst stage_outputs, dann cleaned_values - Description: "Output aus Stage X (Basis-Analyse)" - JSON-Werte automatisch serialisiert BEISPIEL Pipeline-Wertetabelle: ┌──────────────────────────────────────────────┐ │ 📊 Verwendete Werte (8) (+3 ausgeblendet) 🔬│ │ ┌──────────────────────────────────────────┐ │ │ │ weight_aktuell │ 85.2 kg │ Gewicht │ │ │ │ stage_1_body │ {"bmi":...│ Output...│ │ ← Stage output! │ │ stage_1_nutr... │ {"kcal"...│ Output...│ │ │ └──────────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ AKTIVIERUNG Experten-Modus: 1. Analyse öffnen 2. "📊 Verwendete Werte" aufklappen 3. Button "🔬 Experten-Modus" klicken 4. Zeigt alle Platzhalter (auch leere stage outputs) version: 9.8.0 (feature) module: prompts 2.3.0, insights 1.6.0
2026-03-26 12:44:28 +01:00
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div
onClick={() => setShowValues(!showValues)}
style={{
cursor: 'pointer',
fontSize: 12,
color: 'var(--text2)',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: 6
}}
>
{showValues ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
📊 Verwendete Werte ({placeholderCount})
{hiddenCount > 0 && !expertMode && (
<span style={{ fontSize: 10, color: 'var(--text3)', fontWeight: 400 }}>
(+{hiddenCount} ausgeblendet)
</span>
)}
</div>
{showValues && Object.keys(allPlaceholders).length > 0 && (
<button
onClick={(e) => {
e.stopPropagation()
setExpertMode(!expertMode)
}}
className="btn"
style={{
fontSize: 10,
padding: '4px 8px',
background: expertMode ? 'var(--accent)' : 'var(--surface)',
color: expertMode ? 'white' : 'var(--text2)'
}}
>
🔬 Experten-Modus
</button>
)}
</div>
{showValues && (
<div style={{ marginTop: 12, fontSize: 11 }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--border)', textAlign: 'left' }}>
<th style={{ padding: '6px 8px', color: 'var(--text3)', fontWeight: 600 }}>Platzhalter</th>
<th style={{ padding: '6px 8px', color: 'var(--text3)', fontWeight: 600 }}>Wert</th>
<th style={{ padding: '6px 8px', color: 'var(--text3)', fontWeight: 600 }}>Beschreibung</th>
</tr>
</thead>
<tbody>
feat: category grouping in value table (Issue #47) 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
2026-03-26 12:59:52 +01:00
{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]) => {
feat: extract individual values from stage outputs (Issue #47) FEATURE: Basis-Analysen Einzelwerte Vorher: stage_1_body → {"bmi": 26.6, "weight": "85.2kg"} (1 Zeile) Jetzt: bmi → 26.6 (eigene Zeile) weight → 85.2kg (eigene Zeile) BACKEND: JSON-Extraktion - Stage outputs (JSON) → extract individual fields - extracted_values dict sammelt alle Einzelwerte - Deduplizierung: Gleiche Keys nur einmal - Flags: - is_extracted: true → Wert aus Stage-Output extrahiert - is_stage_raw: true → Rohdaten (JSON) nur Experten-Modus BEISPIEL Stage 1 Output: { "stage_1_body": { "bmi": 26.6, "weight": "85.2 kg", "trend": "sinkend" } } → Metadata: { "bmi": { value: "26.6", description: "Aus Stage 1 (stage_1_body)", is_extracted: true }, "weight": { value: "85.2 kg", description: "Aus Stage 1 (stage_1_body)", is_extracted: true }, "stage_1_body": { value: "{\"bmi\": 26.6, ...}", description: "Rohdaten Stage 1 (Basis-Analyse JSON)", is_stage_raw: true } } FRONTEND: Smart Filtering Normal-Modus: - Zeigt: Einzelwerte (bmi, weight, trend) - Versteckt: Rohdaten (stage_1_body JSON) - Filter: is_stage_raw === false Experten-Modus: - Zeigt: Alles (Einzelwerte + Rohdaten) - Rohdaten: Grauer Hintergrund + 🔬 Icon VISUAL Indicators: ↳ bmi → Extrahierter Wert (grün) weight → Normaler Platzhalter (accent) 🔬 stage_1_* → Rohdaten JSON (grau, klein, nur Experten) ERGEBNIS: ┌──────────────────────────────────────────┐ │ 📊 Verwendete Werte (8) (+2 ausgeblendet)│ │ ┌────────────────────────────────────────┐│ │ │ weight_aktuell │ 85.2 kg │ Gewicht ││ ← Normal │ │ ↳ bmi │ 26.6 │ Aus St..││ ← Extrahiert │ │ ↳ trend │ sinkend │ Aus St..││ ← Extrahiert │ └────────────────────────────────────────┘│ └──────────────────────────────────────────┘ Experten-Modus zusätzlich: │ 🔬 stage_1_body │ {"bmi":...│ Rohdaten││ ← JSON version: 9.9.0 (feature) module: prompts 2.4.0, insights 1.7.0
2026-03-26 12:55:53 +01:00
const isExtracted = data.is_extracted
const isStageRaw = data.is_stage_raw
return (
<tr key={key} style={{
borderBottom: '1px solid var(--border)',
background: isStageRaw && expertMode ? 'var(--surface)' : 'transparent'
feat: untruncated values + smart base prompt display (Issue #47) FEATURE: Volle Werte (nicht abgeschnitten) - Backend holt ungekürzten Werte direkt von placeholder_resolver - get_placeholder_example_values() statt debug.resolved_placeholders - Debug bleibt gekürzt (100 chars), Metadata ungekürzt FEATURE: Smart Display für Basis-Prompts - Basis-Prompts mit JSON-Output: Nur Wertetabelle anzeigen - JSON-Output in Collapsible "Technische Daten" verschoben - Wertetabelle auto-expanded bei Basis-Prompts - Pipeline + Text-Prompts: Wie bisher (Content + Wertetabelle) UI: Bessere Wertetabelle - Werte: word-break + max-width (400px) → kein Overflow - Alle Spalten: verticalAlign top für bessere Lesbarkeit - Platzhalter: nowrap (keine Umbrüche) BEISPIEL: ┌─────────────────────────────────────────┐ │ ℹ️ Basis-Prompt Rohdaten │ │ [Technische Daten anzeigen ▼] │ │ │ │ 📊 Verwendete Werte (8) ▼ ← expanded │ │ ┌──────────────────────────────────────┐│ │ │ Platzhalter │ Vollständiger Wert... ││ │ │ kcal_avg │ 1427 kcal/Tag (Ø 30...││ ← ungekürzt │ └──────────────────────────────────────┘│ └─────────────────────────────────────────┘ version: 9.7.0 (feature) module: prompts 2.2.0, insights 1.5.0
2026-03-26 12:37:52 +01:00
}}>
feat: extract individual values from stage outputs (Issue #47) FEATURE: Basis-Analysen Einzelwerte Vorher: stage_1_body → {"bmi": 26.6, "weight": "85.2kg"} (1 Zeile) Jetzt: bmi → 26.6 (eigene Zeile) weight → 85.2kg (eigene Zeile) BACKEND: JSON-Extraktion - Stage outputs (JSON) → extract individual fields - extracted_values dict sammelt alle Einzelwerte - Deduplizierung: Gleiche Keys nur einmal - Flags: - is_extracted: true → Wert aus Stage-Output extrahiert - is_stage_raw: true → Rohdaten (JSON) nur Experten-Modus BEISPIEL Stage 1 Output: { "stage_1_body": { "bmi": 26.6, "weight": "85.2 kg", "trend": "sinkend" } } → Metadata: { "bmi": { value: "26.6", description: "Aus Stage 1 (stage_1_body)", is_extracted: true }, "weight": { value: "85.2 kg", description: "Aus Stage 1 (stage_1_body)", is_extracted: true }, "stage_1_body": { value: "{\"bmi\": 26.6, ...}", description: "Rohdaten Stage 1 (Basis-Analyse JSON)", is_stage_raw: true } } FRONTEND: Smart Filtering Normal-Modus: - Zeigt: Einzelwerte (bmi, weight, trend) - Versteckt: Rohdaten (stage_1_body JSON) - Filter: is_stage_raw === false Experten-Modus: - Zeigt: Alles (Einzelwerte + Rohdaten) - Rohdaten: Grauer Hintergrund + 🔬 Icon VISUAL Indicators: ↳ bmi → Extrahierter Wert (grün) weight → Normaler Platzhalter (accent) 🔬 stage_1_* → Rohdaten JSON (grau, klein, nur Experten) ERGEBNIS: ┌──────────────────────────────────────────┐ │ 📊 Verwendete Werte (8) (+2 ausgeblendet)│ │ ┌────────────────────────────────────────┐│ │ │ weight_aktuell │ 85.2 kg │ Gewicht ││ ← Normal │ │ ↳ bmi │ 26.6 │ Aus St..││ ← Extrahiert │ │ ↳ trend │ sinkend │ Aus St..││ ← Extrahiert │ └────────────────────────────────────────┘│ └──────────────────────────────────────────┘ Experten-Modus zusätzlich: │ 🔬 stage_1_body │ {"bmi":...│ Rohdaten││ ← JSON version: 9.9.0 (feature) module: prompts 2.4.0, insights 1.7.0
2026-03-26 12:55:53 +01:00
<td style={{
padding: '6px 8px',
fontFamily: 'monospace',
color: isStageRaw ? 'var(--text3)' : (isExtracted ? '#6B8E23' : 'var(--accent)'),
whiteSpace: 'nowrap',
verticalAlign: 'top',
fontSize: isStageRaw ? 10 : 11
}}>
{isExtracted && '↳ '}
{isStageRaw && '🔬 '}
{key}
</td>
<td style={{
padding: '6px 8px',
fontFamily: 'monospace',
wordBreak: 'break-word',
maxWidth: '400px',
verticalAlign: 'top',
fontSize: isStageRaw ? 9 : 11,
color: isStageRaw ? 'var(--text3)' : 'var(--text1)'
}}>
{isStageRaw ? (
<details style={{ cursor: 'pointer' }}>
<summary style={{
fontWeight: 600,
color: 'var(--accent)',
fontSize: 10,
marginBottom: '4px'
}}>
JSON anzeigen ▼
</summary>
<pre style={{
background: 'var(--surface2)',
padding: '8px',
borderRadius: '4px',
fontSize: 9,
overflow: 'auto',
maxHeight: '300px',
margin: 0
}}>
{data.value}
</pre>
</details>
) : data.value}
feat: extract individual values from stage outputs (Issue #47) FEATURE: Basis-Analysen Einzelwerte Vorher: stage_1_body → {"bmi": 26.6, "weight": "85.2kg"} (1 Zeile) Jetzt: bmi → 26.6 (eigene Zeile) weight → 85.2kg (eigene Zeile) BACKEND: JSON-Extraktion - Stage outputs (JSON) → extract individual fields - extracted_values dict sammelt alle Einzelwerte - Deduplizierung: Gleiche Keys nur einmal - Flags: - is_extracted: true → Wert aus Stage-Output extrahiert - is_stage_raw: true → Rohdaten (JSON) nur Experten-Modus BEISPIEL Stage 1 Output: { "stage_1_body": { "bmi": 26.6, "weight": "85.2 kg", "trend": "sinkend" } } → Metadata: { "bmi": { value: "26.6", description: "Aus Stage 1 (stage_1_body)", is_extracted: true }, "weight": { value: "85.2 kg", description: "Aus Stage 1 (stage_1_body)", is_extracted: true }, "stage_1_body": { value: "{\"bmi\": 26.6, ...}", description: "Rohdaten Stage 1 (Basis-Analyse JSON)", is_stage_raw: true } } FRONTEND: Smart Filtering Normal-Modus: - Zeigt: Einzelwerte (bmi, weight, trend) - Versteckt: Rohdaten (stage_1_body JSON) - Filter: is_stage_raw === false Experten-Modus: - Zeigt: Alles (Einzelwerte + Rohdaten) - Rohdaten: Grauer Hintergrund + 🔬 Icon VISUAL Indicators: ↳ bmi → Extrahierter Wert (grün) weight → Normaler Platzhalter (accent) 🔬 stage_1_* → Rohdaten JSON (grau, klein, nur Experten) ERGEBNIS: ┌──────────────────────────────────────────┐ │ 📊 Verwendete Werte (8) (+2 ausgeblendet)│ │ ┌────────────────────────────────────────┐│ │ │ weight_aktuell │ 85.2 kg │ Gewicht ││ ← Normal │ │ ↳ bmi │ 26.6 │ Aus St..││ ← Extrahiert │ │ ↳ trend │ sinkend │ Aus St..││ ← Extrahiert │ └────────────────────────────────────────┘│ └──────────────────────────────────────────┘ Experten-Modus zusätzlich: │ 🔬 stage_1_body │ {"bmi":...│ Rohdaten││ ← JSON version: 9.9.0 (feature) module: prompts 2.4.0, insights 1.7.0
2026-03-26 12:55:53 +01:00
</td>
<td style={{
padding: '6px 8px',
color: 'var(--text3)',
fontSize: 10,
verticalAlign: 'top',
fontStyle: isExtracted ? 'italic' : 'normal'
}}>
{data.description || '—'}
</td>
</tr>
feat: category grouping in value table (Issue #47) 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
2026-03-26 12:59:52 +01:00
)
})}
</React.Fragment>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</>
)}
</div>
)
}
export default function Analysis() {
const { canUseAI } = useAuth()
const [prompts, setPrompts] = useState([])
const [allInsights, setAllInsights] = useState([])
const [loading, setLoading] = useState(null)
const [error, setError] = useState(null)
const [tab, setTab] = useState('run')
const [newResult, setNewResult] = useState(null)
const [aiUsage, setAiUsage] = useState(null)
/** Kategorie-Schlüssel aus `buildPipelineGroups` (Navigation); Detail = alle Pipelines dieser Kategorie */
const [activeCategoryKey, setActiveCategoryKey] = useState(null)
const [historyScopePick, setHistoryScopePick] = useState(null)
// NEW: Progress tracking for SSE workflows
const [progress, setProgress] = useState(null) // { total_nodes, completed_nodes, current_node_label }
const loadAll = async () => {
const [p, i] = await Promise.all([
api.listPrompts(),
api.listInsights()
])
setPrompts(Array.isArray(p)?p:[])
setAllInsights(Array.isArray(i)?i:[])
}
useEffect(()=>{
loadAll()
// Load feature usage for badges
api.getFeatureUsage().then(features => {
const aiFeature = features.find(f => f.feature_id === 'ai_calls')
setAiUsage(aiFeature)
}).catch(err => console.error('Failed to load usage:', err))
},[])
useEffect(() => {
const list = prompts.filter(isAnalysisOfferPrompt)
setActiveCategoryKey(prev => {
if (!list.length) return null
2026-04-05 09:04:08 +02:00
const groups = buildPipelineGroups(list)
const keys = groups.map(g => g.categoryKey)
if (prev && keys.includes(prev)) return prev
return groups[0]?.categoryKey ?? null
})
}, [prompts])
useEffect(() => {
if (!newResult?.scope) return
const list = prompts.filter(isAnalysisOfferPrompt)
const groups = buildPipelineGroups(list)
const g = groups.find(gg => gg.prompts.some(p => p.slug === newResult.scope))
if (g) setActiveCategoryKey(g.categoryKey)
}, [newResult?.scope, prompts])
const runPrompt = async (slug) => {
setLoading(slug); setError(null); setNewResult(null); setProgress(null)
try {
// Use SSE-based executor for long-running workflows
const result = await api.executeUnifiedPromptStream(slug, null, null, true, true, (event) => {
// Progress callback: update UI in real-time
if (event.type === 'execution_started') {
setProgress({ total_nodes: 0, completed_nodes: 0, current_node_label: 'Starte...' })
} else if (event.type === 'node_complete') {
setProgress({
total_nodes: event.total_nodes || 0,
completed_nodes: event.completed_nodes || 0,
current_node_label: event.node_label || `Node ${event.node_id}`
})
}
})
// Transform result to match old format for InsightCard
let content = ''
if (result.type === 'pipeline') {
// For pipeline, extract final output
const finalOutput = result.output || {}
if (typeof finalOutput === 'object' && Object.keys(finalOutput).length === 1) {
content = Object.values(finalOutput)[0]
} else {
content = JSON.stringify(finalOutput, null, 2)
}
} else if (result.type === 'workflow') {
content = getWorkflowDisplayContent(result.aggregated_result)
} else {
// For base prompts, use output directly
content = typeof result.output === 'string' ? result.output : JSON.stringify(result.output, null, 2)
}
// Build metadata from debug info (same logic as backend)
let metadata = null
if (result.debug && result.debug.resolved_placeholders) {
const placeholders = {}
const resolved = result.debug.resolved_placeholders
// For pipeline, collect from all stages (Workflow: kein gleiches debug-Schema)
if (result.type === 'pipeline' && result.debug.stages) {
for (const stage of result.debug.stages) {
for (const promptDebug of (stage.prompts || [])) {
const stageResolved = promptDebug.resolved_placeholders || promptDebug.ref_debug?.resolved_placeholders || {}
for (const [key, value] of Object.entries(stageResolved)) {
if (!placeholders[key]) {
placeholders[key] = { value, description: '' }
}
}
}
}
} else {
// For base prompts
for (const [key, value] of Object.entries(resolved)) {
placeholders[key] = { value, description: '' }
}
}
if (Object.keys(placeholders).length > 0) {
metadata = { prompt_type: result.type, placeholders }
}
}
setNewResult({
scope: slug,
content,
metadata,
node_states: result.node_states, // For workflow debug panel
result_type: result.type,
aggregated_result: result.aggregated_result
})
await loadAll()
setTab('run')
} catch(e) {
setError('Fehler: ' + e.message)
} finally {
setLoading(null)
setProgress(null) // Clear progress
}
}
const deleteInsight = async (id) => {
if (!confirm('Analyse löschen?')) return
try {
await api.deleteInsight(id)
if (newResult?.id === id) setNewResult(null)
await loadAll()
} catch (e) {
setError('Löschen fehlgeschlagen: ' + e.message)
}
}
// Group insights by scope for history view
const grouped = {}
allInsights.forEach(ins => {
const key = ins.scope || 'sonstige'
grouped[key] = grouped[key] || []
grouped[key].push(ins)
})
// Aktive Pipeline- + Workflow-Prompts (nach DB-Kategorie gruppiert)
2026-04-05 09:04:08 +02:00
const { pipelinePrompts, pipelineGroups } = useMemo(() => {
const list = prompts.filter(isAnalysisOfferPrompt)
2026-04-05 09:04:08 +02:00
return { pipelinePrompts: list, pipelineGroups: buildPipelineGroups(list) }
}, [prompts])
const historyScopeKeys = Object.keys(grouped).sort((a, b) => a.localeCompare(b))
const activeHistoryScope =
historyScopeKeys.length === 0
? null
: historyScopeKeys.includes(historyScopePick)
? historyScopePick
: historyScopeKeys[0]
return (
<div className="analysis-page">
<div className="analysis-page__header">
<div>
<h1 className="page-title" style={{ margin: 0 }}>KI-Analyse</h1>
<p style={{ fontSize: 12, color: 'var(--text3)', margin: '8px 0 0', lineHeight: 1.5, maxWidth: 520 }}>
Ziele und Fokusbereiche steuern den Kontext der Auswertungen –{' '}
<Link to="/goals" style={{ color: 'var(--accent)', fontWeight: 600 }}>unter „Ziele“ konfigurieren</Link>
{' '}(auch über die untere Navigation).
</p>
</div>
</div>
<div className="tabs">
<button type="button" className={'tab'+(tab==='run'?' active':'')} onClick={()=>setTab('run')}>Analysen starten</button>
<button type="button" className={'tab'+(tab==='history'?' active':'')} onClick={()=>setTab('history')}>
Verlauf
{allInsights.length>0 && <span style={{marginLeft:4,fontSize:10,background:'var(--accent)',
color:'white',padding:'1px 5px',borderRadius:8}}>{allInsights.length}</span>}
</button>
</div>
{error && (
<div style={{padding:'10px 14px',background:'#FCEBEB',borderRadius:8,fontSize:13,
color:'#D85A30',marginBottom:12,lineHeight:1.5}}>
{error.includes('nicht aktiviert') || error.includes('Limit')
? <>🔒 <strong>KI-Zugang eingeschränkt</strong><br/>
Dein Profil hat keinen Zugang zu KI-Analysen oder das Tageslimit wurde erreicht.
Bitte den Admin kontaktieren.</>
: error}
</div>
)}
{/* ── Analysen starten ── */}
{tab==='run' && (
<div>
{/* Fallback: Ergebnis oben nur wenn keine Pipeline-Split-Ansicht (z. B. keine Prompts) */}
{newResult && !(canUseAI && pipelinePrompts.length > 0) && (
<div style={{marginBottom:16}}>
<div style={{fontSize:12,fontWeight:600,color:'var(--accent)',marginBottom:8}}>
✅ Neue Analyse erstellt:
</div>
<InsightCard
ins={{...newResult, created: new Date().toISOString()}}
onDelete={deleteInsight}
defaultOpen={true}
prompts={prompts}
/>
{newResult.node_states && (
<WorkflowDebugPanel nodeStates={newResult.node_states} />
)}
</div>
)}
{!canUseAI && (
<div style={{padding:'14px 16px',background:'#FCEBEB',borderRadius:10,
border:'1px solid #D85A3033',marginBottom:16}}>
<div style={{fontSize:14,fontWeight:600,color:'#D85A30',marginBottom:4}}>
🔒 KI-Analysen nicht freigeschaltet
</div>
<div style={{fontSize:13,color:'var(--text2)',lineHeight:1.5}}>
Dein Profil hat keinen Zugang zu KI-Analysen.
Bitte den Admin bitten, KI für dein Profil zu aktivieren
(Einstellungen → Admin → Profil bearbeiten).
</div>
</div>
)}
{canUseAI && pipelinePrompts.length > 0 && (
<>
<p style={{fontSize:13,color:'var(--text2)',marginBottom:14,lineHeight:1.6}}>
Zuerst die <strong>Kategorie</strong> wählen (Chip-Leiste bzw. Seitenleiste). Alle{' '}
<strong>Pipeline- und Workflow-Auswertungen</strong> dieser Kategorie erscheinen im Detailbereich
(rechts auf Desktop, darunter auf Mobil). Bei Workflows legst du Kategorie, Titel und Kurztext in der{' '}
<strong>Start-Node</strong> des Workflow-Editors fest; bei Pipelines im Admin unter KI-Prompts.
</p>
<div className="analysis-split">
<div className="analysis-split__nav-wrap">
<nav className="analysis-split__nav" aria-label="KI-Analyse-Kategorien">
2026-04-05 09:04:08 +02:00
{pipelineGroups.map(({ categoryKey, label, prompts: inGroup }) => (
<button
key={categoryKey}
type="button"
className={
'analysis-split__nav-item' +
(activeCategoryKey === categoryKey ? ' analysis-split__nav-item--active' : '')
}
onClick={() => setActiveCategoryKey(categoryKey)}
aria-current={activeCategoryKey === categoryKey ? 'page' : undefined}
>
{label}
<span className="analysis-split__nav-cat-count">({inGroup.length})</span>
</button>
2026-04-05 09:04:08 +02:00
))}
</nav>
</div>
<div className="analysis-split__main">
{newResult && (
<div style={{ marginBottom: 20 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--accent)', marginBottom: 8 }}>
✅ Neue Analyse erstellt:
</div>
<InsightCard
ins={{ ...newResult, created: new Date().toISOString() }}
onDelete={deleteInsight}
defaultOpen={true}
prompts={prompts}
/>
{newResult.node_states && (
<WorkflowDebugPanel nodeStates={newResult.node_states} />
)}
</div>
)}
{activeCategoryKey && (() => {
const group = pipelineGroups.find(g => g.categoryKey === activeCategoryKey)
if (!group?.prompts?.length) return null
return (
<>
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 12 }}>
{group.label} · {group.prompts.length} {group.prompts.length === 1 ? 'Analyse' : 'Analysen'}
</div>
{group.prompts.map(p => {
const existing = allInsights.find(i => i.scope === p.slug)
return (
<div key={p.id} className="card" style={{marginBottom:16,borderColor:'var(--accent)',borderWidth:2}}>
<div style={{display:'flex',alignItems:'flex-start',gap:12,flexWrap:'wrap'}}>
<div style={{flex:1,minWidth:0}}>
<div className="badge-container-right" style={{fontWeight:700,fontSize:15,color:'var(--accent)'}}>
<span>{p.display_name || SLUG_LABELS[p.slug] || p.name}</span>
{aiUsage && <UsageBadge {...aiUsage} />}
</div>
{p.description && (
<div style={{fontSize:12,color:'var(--text2)',marginTop:3,lineHeight:1.5}}>
{p.description}
</div>
)}
{existing && (
<div style={{fontSize:11,color:'var(--text3)',marginTop:3}}>
Letzte Analyse: {dayjs(existing.created).format('DD.MM.YYYY, HH:mm')}
</div>
)}
</div>
<div
title={aiUsage && !aiUsage.allowed ? `Limit erreicht (${aiUsage.used}/${aiUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.` : ''}
style={{display:'inline-block'}}
>
<button
type="button"
className="btn btn-primary"
style={{flexShrink:0,minWidth:100, cursor: (aiUsage && !aiUsage.allowed) ? 'not-allowed' : 'pointer'}}
onClick={() => runPrompt(p.slug)}
disabled={!!loading||!canUseAI||(aiUsage && !aiUsage.allowed)}
>
{loading===p.slug
? (progress
? <><div className="spinner" style={{width:13,height:13}}/> {progress.completed_nodes}/{progress.total_nodes} Nodes</>
: <><div className="spinner" style={{width:13,height:13}}/> Läuft…</>)
: (aiUsage && !aiUsage.allowed) ? '🔒 Limit'
: <><Brain size={13}/> Starten</>}
</button>
</div>
</div>
{existing && newResult?.id !== existing.id && (
<div style={{marginTop:8,borderTop:'1px solid var(--border)',paddingTop:8}}>
<InsightCard ins={existing} onDelete={deleteInsight} defaultOpen={false} prompts={prompts}/>
</div>
)}
</div>
)
})}
</>
)
})()}
</div>
</div>
</>
)}
{canUseAI && pipelinePrompts.length === 0 && (
<div className="empty-state">
<p>Keine aktiven Pipeline- oder Workflow-Auswertungen verfügbar.</p>
<p style={{fontSize:12,color:'var(--text3)',marginTop:8}}>
Pipelines und Workflows werden im Admin unter KI-Prompts bzw. Workflow-Editor angelegt.
</p>
</div>
)}
</div>
)}
{/* ── Verlauf gruppiert ── */}
{tab==='history' && (
<div>
{allInsights.length===0 ? (
<div className="empty-state"><h3>Noch keine Analysen</h3></div>
) : (
<div className="analysis-split">
<div className="analysis-split__nav-wrap">
<nav className="analysis-split__nav" aria-label="Gespeicherte Analysen">
{historyScopeKeys.map(scope => (
<button
key={scope}
type="button"
className={'analysis-split__nav-item' + (activeHistoryScope === scope ? ' analysis-split__nav-item--active' : '')}
onClick={() => setHistoryScopePick(scope)}
aria-current={activeHistoryScope === scope ? 'page' : undefined}
>
{(() => {
const pr = prompts.find((p) => p.slug === scope)
return pr?.display_name || pr?.name || SLUG_LABELS[scope] || scope
})()}
<span className="muted" style={{ fontSize: 12 }}> ({grouped[scope].length})</span>
</button>
))}
</nav>
</div>
<div className="analysis-split__main">
{activeHistoryScope && grouped[activeHistoryScope]?.map(i => (
<InsightCard key={i.id} ins={i} onDelete={deleteInsight} prompts={prompts}/>
))}
</div>
</div>
)}
</div>
)}
</div>
)
}