Gruppierung Analysen
This commit is contained in:
parent
1fa0edb3b5
commit
6d0e2de66d
|
|
@ -291,6 +291,84 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
|
||||||
color: rgba(255, 255, 255, 0.88) !important;
|
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 {
|
.analysis-split__main {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -319,6 +397,10 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.analysis-split__nav--grouped {
|
||||||
|
max-height: calc(100vh - 140px);
|
||||||
|
}
|
||||||
|
|
||||||
.analysis-split__nav-item {
|
.analysis-split__nav-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
|
||||||
|
|
@ -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 { Brain, Trash2, ChevronDown, ChevronUp, Target } from 'lucide-react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { api } from '../utils/api'
|
import { api } from '../utils/api'
|
||||||
|
|
@ -14,6 +14,63 @@ const SLUG_LABELS = {
|
||||||
pipeline: '🔬 Mehrstufige Gesamtanalyse'
|
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=[] }) {
|
function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
|
||||||
const [open, setOpen] = useState(defaultOpen)
|
const [open, setOpen] = useState(defaultOpen)
|
||||||
|
|
||||||
|
|
@ -312,7 +369,8 @@ export default function Analysis() {
|
||||||
setActivePipelineSlug(prev => {
|
setActivePipelineSlug(prev => {
|
||||||
if (!list.length) return null
|
if (!list.length) return null
|
||||||
if (prev && list.some(p => p.slug === prev)) return prev
|
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])
|
}, [prompts])
|
||||||
|
|
||||||
|
|
@ -398,8 +456,11 @@ export default function Analysis() {
|
||||||
grouped[key].push(ins)
|
grouped[key].push(ins)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Show only active pipeline-type prompts
|
// Show only active pipeline-type prompts (und nach DB-Kategorie gruppiert)
|
||||||
const pipelinePrompts = prompts.filter(p => p.active && p.type === 'pipeline')
|
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 historyScopeKeys = Object.keys(grouped).sort((a, b) => a.localeCompare(b))
|
||||||
const activeHistoryScope =
|
const activeHistoryScope =
|
||||||
|
|
@ -478,30 +539,56 @@ export default function Analysis() {
|
||||||
{canUseAI && pipelinePrompts.length > 0 && (
|
{canUseAI && pipelinePrompts.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<p style={{fontSize:13,color:'var(--text2)',marginBottom:14,lineHeight:1.6}}>
|
<p style={{fontSize:13,color:'var(--text2)',marginBottom:14,lineHeight:1.6}}>
|
||||||
Wähle in der Liste eine Analyse; auf dem Desktop erscheint der Detailbereich rechts, auf schmalen Screens darunter.
|
Analysen sind nach <strong>Kategorie</strong> gruppiert (Feld „Kategorie“ beim Prompt, wie im Admin).
|
||||||
|
Wähle einen Eintrag; der Detailbereich erscheint auf dem Desktop rechts, auf schmalen Screens darunter.
|
||||||
</p>
|
</p>
|
||||||
<div className="analysis-split">
|
<div className="analysis-split">
|
||||||
<div className="analysis-split__nav-wrap">
|
<div className="analysis-split__nav-wrap">
|
||||||
<nav className="analysis-split__nav" aria-label="Verfügbare KI-Analysen">
|
<nav
|
||||||
{pipelinePrompts.map(p => {
|
className="analysis-split__nav analysis-split__nav--grouped"
|
||||||
const existing = allInsights.find(i => i.scope === p.slug)
|
aria-label="Verfügbare KI-Analysen nach Kategorie"
|
||||||
return (
|
>
|
||||||
<button
|
{pipelineGroups.map(({ categoryKey, label, prompts: inGroup }) => (
|
||||||
key={p.id}
|
<div key={categoryKey} className="analysis-split__nav-group">
|
||||||
type="button"
|
<div
|
||||||
className={'analysis-split__nav-item' + (activePipelineSlug === p.slug ? ' analysis-split__nav-item--active' : '')}
|
className="analysis-split__nav-group-title"
|
||||||
onClick={() => setActivePipelineSlug(p.slug)}
|
id={analysisNavGroupDomId(categoryKey)}
|
||||||
aria-current={activePipelineSlug === p.slug ? 'page' : undefined}
|
|
||||||
>
|
>
|
||||||
{p.display_name || SLUG_LABELS[p.slug] || p.name}
|
{label}
|
||||||
{existing && (
|
<span className="analysis-split__nav-group-count">{inGroup.length}</span>
|
||||||
<span className="muted" style={{ fontSize: 11, fontWeight: 400 }}>
|
</div>
|
||||||
{' '}· {dayjs(existing.created).format('DD.MM.')}
|
<div
|
||||||
</span>
|
className="analysis-split__nav-group-items"
|
||||||
)}
|
role="group"
|
||||||
</button>
|
aria-labelledby={analysisNavGroupDomId(categoryKey)}
|
||||||
)
|
>
|
||||||
})}
|
{inGroup.map(p => {
|
||||||
|
const existing = allInsights.find(i => i.scope === p.slug)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
type="button"
|
||||||
|
className={
|
||||||
|
'analysis-split__nav-item' +
|
||||||
|
(activePipelineSlug === p.slug ? ' analysis-split__nav-item--active' : '')
|
||||||
|
}
|
||||||
|
onClick={() => setActivePipelineSlug(p.slug)}
|
||||||
|
aria-current={activePipelineSlug === p.slug ? 'page' : undefined}
|
||||||
|
>
|
||||||
|
<span className="analysis-split__nav-item-label">
|
||||||
|
{p.display_name || SLUG_LABELS[p.slug] || p.name}
|
||||||
|
</span>
|
||||||
|
{existing && (
|
||||||
|
<span className="muted analysis-split__nav-item-meta">
|
||||||
|
{dayjs(existing.created).format('DD.MM.')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div className="analysis-split__main">
|
<div className="analysis-split__main">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user