feat: Refactor analysis navigation to use category-based grouping and update state management
This commit is contained in:
parent
6d0e2de66d
commit
630a3de88a
|
|
@ -291,82 +291,16 @@ 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;
|
||||
.analysis-split__nav-cat-count {
|
||||
margin-left: 6px;
|
||||
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);
|
||||
font-weight: 500;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.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__nav-item--active .analysis-split__nav-cat-count {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.analysis-split__main {
|
||||
|
|
@ -397,10 +331,6 @@ 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;
|
||||
|
|
|
|||
|
|
@ -45,11 +45,6 @@ function analysisCategoryLabel(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 */
|
||||
function buildPipelineGroups(pipelinePrompts) {
|
||||
const m = new Map()
|
||||
|
|
@ -343,7 +338,8 @@ export default function Analysis() {
|
|||
const [tab, setTab] = useState('run')
|
||||
const [newResult, setNewResult] = 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 loadAll = async () => {
|
||||
|
|
@ -366,17 +362,22 @@ export default function Analysis() {
|
|||
|
||||
useEffect(() => {
|
||||
const list = prompts.filter(p => p.active && p.type === 'pipeline')
|
||||
setActivePipelineSlug(prev => {
|
||||
setActiveCategoryKey(prev => {
|
||||
if (!list.length) return null
|
||||
if (prev && list.some(p => p.slug === prev)) return prev
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
if (newResult?.scope) setActivePipelineSlug(newResult.scope)
|
||||
}, [newResult?.scope])
|
||||
if (!newResult?.scope) return
|
||||
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) => {
|
||||
setLoading(slug); setError(null); setNewResult(null)
|
||||
|
|
@ -539,106 +540,87 @@ export default function Analysis() {
|
|||
{canUseAI && pipelinePrompts.length > 0 && (
|
||||
<>
|
||||
<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).
|
||||
Wähle einen Eintrag; der Detailbereich erscheint auf dem Desktop rechts, auf schmalen Screens darunter.
|
||||
Zuerst die <strong>Kategorie</strong> wählen (Chip-Leiste bzw. Seitenleiste). Alle Pipeline-Analysen
|
||||
dieser Kategorie erscheinen im Detailbereich (rechts auf Desktop, darunter auf Mobil).
|
||||
Kategorien kommen aus dem Feld „Kategorie“ beim jeweiligen Prompt im Admin.
|
||||
</p>
|
||||
<div className="analysis-split">
|
||||
<div className="analysis-split__nav-wrap">
|
||||
<nav
|
||||
className="analysis-split__nav analysis-split__nav--grouped"
|
||||
aria-label="Verfügbare KI-Analysen nach Kategorie"
|
||||
>
|
||||
<nav className="analysis-split__nav" aria-label="KI-Analyse-Kategorien">
|
||||
{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
|
||||
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>
|
||||
<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>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="analysis-split__main">
|
||||
{activePipelineSlug && (() => {
|
||||
const p = pipelinePrompts.find(x => x.slug === activePipelineSlug)
|
||||
if (!p) return null
|
||||
const existing = allInsights.find(i => i.scope === p.slug)
|
||||
{activeCategoryKey && (() => {
|
||||
const group = pipelineGroups.find(g => g.categoryKey === activeCategoryKey)
|
||||
if (!group?.prompts?.length) return null
|
||||
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
|
||||
? <><div className="spinner" style={{width:13,height:13}}/> Läuft…</>
|
||||
: (aiUsage && !aiUsage.allowed) ? '🔒 Limit'
|
||||
: <><Brain size={13}/> Starten</>}
|
||||
</button>
|
||||
</div>
|
||||
<>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 12 }}>
|
||||
{group.label} · {group.prompts.length} {group.prompts.length === 1 ? 'Analyse' : 'Analysen'}
|
||||
</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>
|
||||
{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
|
||||
? <><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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user