diff --git a/frontend/src/app.css b/frontend/src/app.css index 91b989e..a9a4426 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -291,6 +291,84 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we color: rgba(255, 255, 255, 0.88) !important; } +/* Analyse „Analysen starten“: Unternavigation nach DB-Kategorie gruppiert */ +.analysis-split__nav--grouped { + flex-direction: column; + overflow-x: visible; + overflow-y: auto; + padding-bottom: 0; + gap: 0; + max-height: min(70vh, 560px); +} + +.analysis-split__nav-group { + margin-bottom: 14px; +} + +.analysis-split__nav-group:last-child { + margin-bottom: 0; +} + +.analysis-split__nav-group-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + font-size: 11px; + font-weight: 700; + color: var(--text3); + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 6px 4px 10px; + border-bottom: 1px solid var(--border); +} + +.analysis-split__nav-group-count { + font-size: 10px; + font-weight: 600; + text-transform: none; + letter-spacing: 0; + color: var(--text3); + background: var(--surface2); + padding: 2px 7px; + border-radius: 8px; +} + +.analysis-split__nav-group-items { + display: flex; + flex-direction: column; + gap: 8px; + padding-top: 10px; +} + +.analysis-split__nav--grouped .analysis-split__nav-item { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + width: 100%; + white-space: normal; + text-align: left; + border-radius: 10px; +} + +.analysis-split__nav-item-label { + flex: 1; + min-width: 0; + line-height: 1.35; +} + +.analysis-split__nav-item-meta { + font-size: 11px; + font-weight: 400; + flex-shrink: 0; + line-height: 1.35; +} + +.analysis-split__nav-item--active .analysis-split__nav-item-meta { + color: rgba(255, 255, 255, 0.9); +} + .analysis-split__main { min-width: 0; } @@ -319,6 +397,10 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we gap: 8px; } + .analysis-split__nav--grouped { + max-height: calc(100vh - 140px); + } + .analysis-split__nav-item { width: 100%; justify-content: flex-start; diff --git a/frontend/src/pages/Analysis.jsx b/frontend/src/pages/Analysis.jsx index 1b8e66b..529ea61 100644 --- a/frontend/src/pages/Analysis.jsx +++ b/frontend/src/pages/Analysis.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useMemo } from 'react' import { Brain, Trash2, ChevronDown, ChevronUp, Target } from 'lucide-react' import { useNavigate } from 'react-router-dom' import { api } from '../utils/api' @@ -14,6 +14,63 @@ const SLUG_LABELS = { pipeline: '🔬 Mehrstufige Gesamtanalyse' } +/** DB `ai_prompts.category` – Reihenfolge der Gruppen in der Analyse-Navigation */ +const ANALYSIS_CATEGORY_ORDER = ['körper', 'ernährung', 'training', 'schlaf', 'vitalwerte', 'ziele', 'ganzheitlich'] + +const ANALYSIS_CATEGORY_LABELS = { + körper: 'Körper', + ernährung: 'Ernährung', + training: 'Training', + schlaf: 'Schlaf', + vitalwerte: 'Vitalwerte', + ziele: 'Ziele', + ganzheitlich: 'Ganzheitlich', +} + +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) +} + +/** Statische DOM-Id für aria-labelledby (Kategorie kann Umlaute enthalten) */ +function analysisNavGroupDomId(categoryKey) { + return `analysis-nav-cat-${String(categoryKey).replace(/[^a-zA-Z0-9_-]/g, '_')}` +} + +/** 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) @@ -312,7 +369,8 @@ export default function Analysis() { setActivePipelineSlug(prev => { if (!list.length) return null if (prev && list.some(p => p.slug === prev)) return prev - return list[0].slug + const groups = buildPipelineGroups(list) + return groups[0]?.prompts[0]?.slug ?? list[0].slug }) }, [prompts]) @@ -398,8 +456,11 @@ export default function Analysis() { grouped[key].push(ins) }) - // Show only active pipeline-type prompts - const pipelinePrompts = prompts.filter(p => p.active && p.type === 'pipeline') + // Show only active pipeline-type prompts (und nach DB-Kategorie gruppiert) + const { pipelinePrompts, pipelineGroups } = useMemo(() => { + const list = prompts.filter(p => p.active && p.type === 'pipeline') + return { pipelinePrompts: list, pipelineGroups: buildPipelineGroups(list) } + }, [prompts]) const historyScopeKeys = Object.keys(grouped).sort((a, b) => a.localeCompare(b)) const activeHistoryScope = @@ -478,30 +539,56 @@ export default function Analysis() { {canUseAI && pipelinePrompts.length > 0 && ( <>

- Wähle in der Liste eine Analyse; auf dem Desktop erscheint der Detailbereich rechts, auf schmalen Screens darunter. + Analysen sind nach Kategorie gruppiert (Feld „Kategorie“ beim Prompt, wie im Admin). + Wähle einen Eintrag; der Detailbereich erscheint auf dem Desktop rechts, auf schmalen Screens darunter.

-
+
+ {inGroup.map(p => { + const existing = allInsights.find(i => i.scope === p.slug) + return ( + + ) + })} +
+
+ ))}