import React, { useState, useEffect, useMemo } from 'react' import { Brain, Trash2, ChevronDown, ChevronUp } from 'lucide-react' import { Link } from 'react-router-dom' import { api } from '../utils/api' import { getWorkflowDisplayContent } from '../utils/workflowDisplay' import { ANALYSIS_CATEGORY_ORDER, ANALYSIS_CATEGORY_LABELS, } from '../config/analysisCategories' import { useAuth } from '../context/AuthContext' import Markdown from '../utils/Markdown' import UsageBadge from '../components/UsageBadge' import WorkflowDebugPanel from '../components/WorkflowDebugPanel' import dayjs from 'dayjs' import 'dayjs/locale/de' dayjs.locale('de') // Legacy fallback labels (display_name takes precedence) const SLUG_LABELS = { pipeline: '🔬 Mehrstufige Gesamtanalyse' } 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) } /** Analyse-Angebote: klassische Pipelines + graphbasierte KI-Workflows (`type === 'workflow'`) */ function isAnalysisOfferPrompt(p) { return p.active && (p.type === 'pipeline' || p.type === 'workflow') } /** 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) // Parse metadata early to determine showOnlyValues const metadataRaw = ins.metadata ? (typeof ins.metadata === 'string' ? JSON.parse(ins.metadata) : ins.metadata) : null const isBasePrompt = metadataRaw?.prompt_type === 'base' const isJsonOutput = ins.content && (ins.content.trim().startsWith('{') || ins.content.trim().startsWith('[')) const placeholdersRaw = metadataRaw?.placeholders || {} const showOnlyValues = isBasePrompt && isJsonOutput && Object.keys(placeholdersRaw).length > 0 const [showValues, setShowValues] = useState(showOnlyValues) // Auto-expand for base prompts with JSON const [expertMode, setExpertMode] = useState(false) // Show empty/technical placeholders // Find matching prompt to get display_name const prompt = prompts.find(p => p.slug === ins.scope) const displayName = prompt?.display_name || prompt?.name || SLUG_LABELS[ins.scope] || ins.scope // Use already-parsed metadata const metadata = metadataRaw const allPlaceholders = placeholdersRaw // Filter placeholders: In normal mode, hide empty values and raw stage outputs const placeholders = expertMode ? allPlaceholders : Object.fromEntries( Object.entries(allPlaceholders).filter(([key, data]) => { // Hide raw stage outputs (JSON) in normal mode if (data.is_stage_raw) return false // Hide empty values const val = data.value || '' return val.trim() !== '' && val !== 'nicht verfügbar' && val !== '[Nicht verfügbar]' }) ) const placeholderCount = Object.keys(placeholders).length const hiddenCount = Object.keys(allPlaceholders).length - placeholderCount // Group placeholders by category const groupedPlaceholders = Object.entries(placeholders).reduce((acc, [key, data]) => { const category = data.category || 'Sonstiges' if (!acc[category]) acc[category] = [] acc[category].push([key, data]) return acc }, {}) // Sort categories: Regular categories first, then Stage outputs, then Rohdaten const sortedCategories = Object.keys(groupedPlaceholders).sort((a, b) => { const aIsStage = a.startsWith('Stage') const bIsStage = b.startsWith('Stage') const aIsRohdaten = a.includes('Rohdaten') const bIsRohdaten = b.includes('Rohdaten') // Rohdaten last if (aIsRohdaten && !bIsRohdaten) return 1 if (!aIsRohdaten && bIsRohdaten) return -1 // Stage outputs after regular categories if (!aIsStage && bIsStage) return -1 if (aIsStage && !bIsStage) return 1 // Otherwise alphabetical return a.localeCompare(b) }) return (
setOpen(o=>!o)}>
{displayName}
{dayjs(ins.created).format('DD. MMMM YYYY, HH:mm')}
{open ? : }
{open && ( <> {/* For base prompts with JSON: Only show value table */} {showOnlyValues && (
ℹ️ Basis-Prompt Rohdaten (JSON-Struktur für technische Nutzung)
Technische Daten anzeigen
                  {ins.content}
                
)} {/* For other prompts: Show full content */} {!showOnlyValues && } {/* Value Table */} {placeholderCount > 0 && (
setShowValues(!showValues)} style={{ cursor: 'pointer', fontSize: 12, color: 'var(--text2)', fontWeight: 600, display: 'flex', alignItems: 'center', gap: 6 }} > {showValues ? : } 📊 Verwendete Werte ({placeholderCount}) {hiddenCount > 0 && !expertMode && ( (+{hiddenCount} ausgeblendet) )}
{showValues && Object.keys(allPlaceholders).length > 0 && ( )}
{showValues && (
{sortedCategories.map(category => ( {/* Category Header */} {/* Category Values */} {groupedPlaceholders[category].map(([key, data]) => { const isExtracted = data.is_extracted const isStageRaw = data.is_stage_raw return ( ) })} ))}
Platzhalter Wert Beschreibung
{category}
{isExtracted && '↳ '} {isStageRaw && '🔬 '} {key} {isStageRaw ? (
JSON anzeigen â–Ľ
                                    {data.value}
                                  
) : data.value}
{data.description || '—'}
)}
)} )}
) } export default function Analysis() { const { canUseAI } = useAuth() const [prompts, setPrompts] = useState([]) const [allInsights, setAllInsights] = useState([]) const [loading, setLoading] = useState(null) const [error, setError] = useState(null) const [tab, setTab] = useState('run') const [newResult, setNewResult] = useState(null) const [aiUsage, setAiUsage] = useState(null) /** Kategorie-Schlüssel aus `buildPipelineGroups` (Navigation); Detail = alle Pipelines dieser Kategorie */ const [activeCategoryKey, setActiveCategoryKey] = useState(null) const [historyScopePick, setHistoryScopePick] = useState(null) // NEW: Progress tracking for SSE workflows const [progress, setProgress] = useState(null) // { total_nodes, completed_nodes, current_node_label } const loadAll = async () => { const [p, i] = await Promise.all([ api.listPrompts(), api.listInsights() ]) setPrompts(Array.isArray(p)?p:[]) setAllInsights(Array.isArray(i)?i:[]) } useEffect(()=>{ loadAll() // Load feature usage for badges api.getFeatureUsage().then(features => { const aiFeature = features.find(f => f.feature_id === 'ai_calls') setAiUsage(aiFeature) }).catch(err => console.error('Failed to load usage:', err)) },[]) useEffect(() => { const list = prompts.filter(isAnalysisOfferPrompt) setActiveCategoryKey(prev => { if (!list.length) return null const groups = buildPipelineGroups(list) const keys = groups.map(g => g.categoryKey) if (prev && keys.includes(prev)) return prev return groups[0]?.categoryKey ?? null }) }, [prompts]) useEffect(() => { if (!newResult?.scope) return const list = prompts.filter(isAnalysisOfferPrompt) 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); setProgress(null) try { // Use SSE-based executor for long-running workflows const result = await api.executeUnifiedPromptStream(slug, null, null, true, true, (event) => { // Progress callback: update UI in real-time if (event.type === 'execution_started') { setProgress({ total_nodes: 0, completed_nodes: 0, current_node_label: 'Starte...' }) } else if (event.type === 'node_complete') { setProgress({ total_nodes: event.total_nodes || 0, completed_nodes: event.completed_nodes || 0, current_node_label: event.node_label || `Node ${event.node_id}` }) } }) // Transform result to match old format for InsightCard let content = '' if (result.type === 'pipeline') { // For pipeline, extract final output const finalOutput = result.output || {} if (typeof finalOutput === 'object' && Object.keys(finalOutput).length === 1) { content = Object.values(finalOutput)[0] } else { content = JSON.stringify(finalOutput, null, 2) } } else if (result.type === 'workflow') { content = getWorkflowDisplayContent(result.aggregated_result) } else { // For base prompts, use output directly content = typeof result.output === 'string' ? result.output : JSON.stringify(result.output, null, 2) } // Build metadata from debug info (same logic as backend) let metadata = null if (result.debug && result.debug.resolved_placeholders) { const placeholders = {} const resolved = result.debug.resolved_placeholders // For pipeline, collect from all stages (Workflow: kein gleiches debug-Schema) if (result.type === 'pipeline' && result.debug.stages) { for (const stage of result.debug.stages) { for (const promptDebug of (stage.prompts || [])) { const stageResolved = promptDebug.resolved_placeholders || promptDebug.ref_debug?.resolved_placeholders || {} for (const [key, value] of Object.entries(stageResolved)) { if (!placeholders[key]) { placeholders[key] = { value, description: '' } } } } } } else { // For base prompts for (const [key, value] of Object.entries(resolved)) { placeholders[key] = { value, description: '' } } } if (Object.keys(placeholders).length > 0) { metadata = { prompt_type: result.type, placeholders } } } setNewResult({ scope: slug, content, metadata, node_states: result.node_states, // For workflow debug panel result_type: result.type, aggregated_result: result.aggregated_result }) await loadAll() setTab('run') } catch(e) { setError('Fehler: ' + e.message) } finally { setLoading(null) setProgress(null) // Clear progress } } const deleteInsight = async (id) => { if (!confirm('Analyse löschen?')) return try { await api.deleteInsight(id) if (newResult?.id === id) setNewResult(null) await loadAll() } catch (e) { setError('Löschen fehlgeschlagen: ' + e.message) } } // Group insights by scope for history view const grouped = {} allInsights.forEach(ins => { const key = ins.scope || 'sonstige' grouped[key] = grouped[key] || [] grouped[key].push(ins) }) // Aktive Pipeline- + Workflow-Prompts (nach DB-Kategorie gruppiert) const { pipelinePrompts, pipelineGroups } = useMemo(() => { const list = prompts.filter(isAnalysisOfferPrompt) return { pipelinePrompts: list, pipelineGroups: buildPipelineGroups(list) } }, [prompts]) const historyScopeKeys = Object.keys(grouped).sort((a, b) => a.localeCompare(b)) const activeHistoryScope = historyScopeKeys.length === 0 ? null : historyScopeKeys.includes(historyScopePick) ? historyScopePick : historyScopeKeys[0] return (

KI-Analyse

Ziele und Fokusbereiche steuern den Kontext der Auswertungen –{' '} unter „Ziele“ konfigurieren {' '}(auch über die untere Navigation).

{error && (
{error.includes('nicht aktiviert') || error.includes('Limit') ? <>🔒 KI-Zugang eingeschränkt
Dein Profil hat keinen Zugang zu KI-Analysen oder das Tageslimit wurde erreicht. Bitte den Admin kontaktieren. : error}
)} {/* ── Analysen starten ── */} {tab==='run' && (
{/* Fallback: Ergebnis oben nur wenn keine Pipeline-Split-Ansicht (z. B. keine Prompts) */} {newResult && !(canUseAI && pipelinePrompts.length > 0) && (
âś… Neue Analyse erstellt:
{newResult.node_states && ( )}
)} {!canUseAI && (
đź”’ KI-Analysen nicht freigeschaltet
Dein Profil hat keinen Zugang zu KI-Analysen. Bitte den Admin bitten, KI für dein Profil zu aktivieren (Einstellungen → Admin → Profil bearbeiten).
)} {canUseAI && pipelinePrompts.length > 0 && ( <>

Zuerst die Kategorie wählen (Chip-Leiste bzw. Seitenleiste). Alle{' '} Pipeline- und Workflow-Auswertungen dieser Kategorie erscheinen im Detailbereich (rechts auf Desktop, darunter auf Mobil). Bei Workflows legst du Kategorie, Titel und Kurztext in der{' '} Start-Node des Workflow-Editors fest; bei Pipelines im Admin unter KI-Prompts.

{newResult && (
âś… Neue Analyse erstellt:
{newResult.node_states && ( )}
)} {activeCategoryKey && (() => { const group = pipelineGroups.find(g => g.categoryKey === activeCategoryKey) if (!group?.prompts?.length) return null return ( <>
{group.label} · {group.prompts.length} {group.prompts.length === 1 ? 'Analyse' : 'Analysen'}
{group.prompts.map(p => { const existing = allInsights.find(i => i.scope === p.slug) return (
{p.display_name || SLUG_LABELS[p.slug] || p.name} {aiUsage && }
{p.description && (
{p.description}
)} {existing && (
Letzte Analyse: {dayjs(existing.created).format('DD.MM.YYYY, HH:mm')}
)}
{existing && newResult?.id !== existing.id && (
)}
) })} ) })()}
)} {canUseAI && pipelinePrompts.length === 0 && (

Keine aktiven Pipeline- oder Workflow-Auswertungen verfĂĽgbar.

Pipelines und Workflows werden im Admin unter KI-Prompts bzw. Workflow-Editor angelegt.

)}
)} {/* ── Verlauf gruppiert ── */} {tab==='history' && (
{allInsights.length===0 ? (

Noch keine Analysen

) : (
{activeHistoryScope && grouped[activeHistoryScope]?.map(i => ( ))}
)}
)}
) }