feat: Refactor analysis navigation to use category-based grouping and update state management
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s

This commit is contained in:
Lars 2026-04-05 10:06:29 +02:00
parent 6d0e2de66d
commit 630a3de88a
2 changed files with 90 additions and 178 deletions

View File

@ -291,82 +291,16 @@ 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-cat-count {
.analysis-split__nav--grouped { margin-left: 6px;
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-size: 11px;
font-weight: 700; font-weight: 500;
color: var(--text3); opacity: 0.92;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 6px 4px 10px;
border-bottom: 1px solid var(--border);
} }
.analysis-split__nav-group-count { .analysis-split__nav-item--active .analysis-split__nav-cat-count {
font-size: 10px; color: rgba(255, 255, 255, 0.95);
font-weight: 600; opacity: 1;
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 {
@ -397,10 +331,6 @@ 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;

View File

@ -45,11 +45,6 @@ function analysisCategoryLabel(key) {
return ANALYSIS_CATEGORY_LABELS[k] || String(key) 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 */ /** Pipeline-Prompts nach `category` gruppieren (Backend-Feld), innerhalb Gruppe nach sort_order */
function buildPipelineGroups(pipelinePrompts) { function buildPipelineGroups(pipelinePrompts) {
const m = new Map() const m = new Map()
@ -343,7 +338,8 @@ export default function Analysis() {
const [tab, setTab] = useState('run') const [tab, setTab] = useState('run')
const [newResult, setNewResult] = useState(null) const [newResult, setNewResult] = useState(null)
const [aiUsage, setAiUsage] = useState(null) const [aiUsage, setAiUsage] = useState(null)
const [activePipelineSlug, setActivePipelineSlug] = useState(null) /** Kategorie-Schlüssel aus `buildPipelineGroups` (Navigation); Detail = alle Pipelines dieser Kategorie */
const [activeCategoryKey, setActiveCategoryKey] = useState(null)
const [historyScopePick, setHistoryScopePick] = useState(null) const [historyScopePick, setHistoryScopePick] = useState(null)
const loadAll = async () => { const loadAll = async () => {
@ -366,17 +362,22 @@ export default function Analysis() {
useEffect(() => { useEffect(() => {
const list = prompts.filter(p => p.active && p.type === 'pipeline') const list = prompts.filter(p => p.active && p.type === 'pipeline')
setActivePipelineSlug(prev => { setActiveCategoryKey(prev => {
if (!list.length) return null if (!list.length) return null
if (prev && list.some(p => p.slug === prev)) return prev
const groups = buildPipelineGroups(list) const groups = buildPipelineGroups(list)
return groups[0]?.prompts[0]?.slug ?? list[0].slug const keys = groups.map(g => g.categoryKey)
if (prev && keys.includes(prev)) return prev
return groups[0]?.categoryKey ?? null
}) })
}, [prompts]) }, [prompts])
useEffect(() => { useEffect(() => {
if (newResult?.scope) setActivePipelineSlug(newResult.scope) if (!newResult?.scope) return
}, [newResult?.scope]) const list = prompts.filter(p => p.active && p.type === 'pipeline')
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) => { const runPrompt = async (slug) => {
setLoading(slug); setError(null); setNewResult(null) setLoading(slug); setError(null); setNewResult(null)
@ -539,62 +540,40 @@ 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}}>
Analysen sind nach <strong>Kategorie</strong> gruppiert (Feld Kategorie beim Prompt, wie im Admin). Zuerst die <strong>Kategorie</strong> wählen (Chip-Leiste bzw. Seitenleiste). Alle Pipeline-Analysen
Wähle einen Eintrag; der Detailbereich erscheint auf dem Desktop rechts, auf schmalen Screens darunter. dieser Kategorie erscheinen im Detailbereich (rechts auf Desktop, darunter auf Mobil).
Kategorien kommen aus dem Feld Kategorie beim jeweiligen Prompt im Admin.
</p> </p>
<div className="analysis-split"> <div className="analysis-split">
<div className="analysis-split__nav-wrap"> <div className="analysis-split__nav-wrap">
<nav <nav className="analysis-split__nav" aria-label="KI-Analyse-Kategorien">
className="analysis-split__nav analysis-split__nav--grouped"
aria-label="Verfügbare KI-Analysen nach Kategorie"
>
{pipelineGroups.map(({ categoryKey, label, prompts: inGroup }) => ( {pipelineGroups.map(({ categoryKey, label, prompts: inGroup }) => (
<div key={categoryKey} className="analysis-split__nav-group">
<div
className="analysis-split__nav-group-title"
id={analysisNavGroupDomId(categoryKey)}
>
{label}
<span className="analysis-split__nav-group-count">{inGroup.length}</span>
</div>
<div
className="analysis-split__nav-group-items"
role="group"
aria-labelledby={analysisNavGroupDomId(categoryKey)}
>
{inGroup.map(p => {
const existing = allInsights.find(i => i.scope === p.slug)
return (
<button <button
key={p.id} key={categoryKey}
type="button" type="button"
className={ className={
'analysis-split__nav-item' + 'analysis-split__nav-item' +
(activePipelineSlug === p.slug ? ' analysis-split__nav-item--active' : '') (activeCategoryKey === categoryKey ? ' analysis-split__nav-item--active' : '')
} }
onClick={() => setActivePipelineSlug(p.slug)} onClick={() => setActiveCategoryKey(categoryKey)}
aria-current={activePipelineSlug === p.slug ? 'page' : undefined} aria-current={activeCategoryKey === categoryKey ? 'page' : undefined}
> >
<span className="analysis-split__nav-item-label"> {label}
{p.display_name || SLUG_LABELS[p.slug] || p.name} <span className="analysis-split__nav-cat-count">({inGroup.length})</span>
</span>
{existing && (
<span className="muted analysis-split__nav-item-meta">
{dayjs(existing.created).format('DD.MM.')}
</span>
)}
</button> </button>
)
})}
</div>
</div>
))} ))}
</nav> </nav>
</div> </div>
<div className="analysis-split__main"> <div className="analysis-split__main">
{activePipelineSlug && (() => { {activeCategoryKey && (() => {
const p = pipelinePrompts.find(x => x.slug === activePipelineSlug) const group = pipelineGroups.find(g => g.categoryKey === activeCategoryKey)
if (!p) return null 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) const existing = allInsights.find(i => i.scope === p.slug)
return ( return (
<div key={p.id} className="card" style={{marginBottom:16,borderColor:'var(--accent)',borderWidth:2}}> <div key={p.id} className="card" style={{marginBottom:16,borderColor:'var(--accent)',borderWidth:2}}>
@ -640,6 +619,9 @@ export default function Analysis() {
)} )}
</div> </div>
) )
})}
</>
)
})()} })()}
</div> </div>
</div> </div>