Responsive Gui - partially Workflow #61
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,106 +540,87 @@ 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">
|
<button
|
||||||
<div
|
key={categoryKey}
|
||||||
className="analysis-split__nav-group-title"
|
type="button"
|
||||||
id={analysisNavGroupDomId(categoryKey)}
|
className={
|
||||||
>
|
'analysis-split__nav-item' +
|
||||||
{label}
|
(activeCategoryKey === categoryKey ? ' analysis-split__nav-item--active' : '')
|
||||||
<span className="analysis-split__nav-group-count">{inGroup.length}</span>
|
}
|
||||||
</div>
|
onClick={() => setActiveCategoryKey(categoryKey)}
|
||||||
<div
|
aria-current={activeCategoryKey === categoryKey ? 'page' : undefined}
|
||||||
className="analysis-split__nav-group-items"
|
>
|
||||||
role="group"
|
{label}
|
||||||
aria-labelledby={analysisNavGroupDomId(categoryKey)}
|
<span className="analysis-split__nav-cat-count">({inGroup.length})</span>
|
||||||
>
|
</button>
|
||||||
{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">
|
||||||
{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
|
||||||
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 style={{display:'flex',alignItems:'flex-start',gap:12,flexWrap:'wrap'}}>
|
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 12 }}>
|
||||||
<div style={{flex:1,minWidth:0}}>
|
{group.label} · {group.prompts.length} {group.prompts.length === 1 ? 'Analyse' : 'Analysen'}
|
||||||
<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>
|
</div>
|
||||||
{existing && newResult?.id !== existing.id && (
|
{group.prompts.map(p => {
|
||||||
<div style={{marginTop:8,borderTop:'1px solid var(--border)',paddingTop:8}}>
|
const existing = allInsights.find(i => i.scope === p.slug)
|
||||||
<InsightCard ins={existing} onDelete={deleteInsight} defaultOpen={false} prompts={prompts}/>
|
return (
|
||||||
</div>
|
<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>
|
<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>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user