feat: Add WorkflowDebugPanel component to display per-node debug information
All checks were successful
Deploy Development / deploy (push) Successful in 56s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 17s

- Created WorkflowDebugPanel.jsx: Collapsible panel showing debug info for each workflow node
  - Shows prompt sent to AI
  - Shows raw AI response
  - Shows parsed results
  - Shows normalized signals
  - Color-coded status (executed/failed/skipped)
  - Expandable/collapsible per node

- Updated Analysis.jsx:
  - Added WorkflowDebugPanel import
  - Store node_states in newResult for debugging
  - Display WorkflowDebugPanel below InsightCard (both locations)

This makes it easy to debug workflow issues by seeing exactly what happened at each node.
This commit is contained in:
Lars 2026-04-13 12:41:12 +02:00
parent 12d4d7c63b
commit a515a5d563
2 changed files with 367 additions and 1 deletions

View File

@ -0,0 +1,352 @@
import React, { useState } from 'react'
import { ChevronDown, ChevronUp } from 'lucide-react'
/**
* WorkflowDebugPanel - Zeigt Debug-Informationen für jeden Node eines Workflows
*
* @param {Object} props
* @param {Array} props.nodeStates - Array von NodeExecutionState Objekten
*/
export default function WorkflowDebugPanel({ nodeStates }) {
const [expandedNodes, setExpandedNodes] = useState(new Set())
const [showAll, setShowAll] = useState(false)
if (!nodeStates || nodeStates.length === 0) {
return null
}
// Filter nodes that have debug information
const debugNodes = nodeStates.filter(ns =>
ns.debug_prompt || ns.debug_raw_response || ns.debug_node_type
)
if (debugNodes.length === 0) {
return (
<div style={{
padding: '12px',
background: 'var(--surface)',
borderRadius: '8px',
border: '1px solid var(--border)',
marginTop: '16px'
}}>
<p style={{ margin: 0, color: 'var(--text2)', fontSize: '13px' }}>
Keine Debug-Informationen verfügbar. Führe den Workflow mit debug=true aus.
</p>
</div>
)
}
const toggleNode = (nodeId) => {
const newExpanded = new Set(expandedNodes)
if (newExpanded.has(nodeId)) {
newExpanded.delete(nodeId)
} else {
newExpanded.add(nodeId)
}
setExpandedNodes(newExpanded)
}
const toggleAll = () => {
if (showAll) {
setExpandedNodes(new Set())
} else {
setExpandedNodes(new Set(debugNodes.map(n => n.node_id)))
}
setShowAll(!showAll)
}
const getStatusColor = (status) => {
switch (status) {
case 'executed': return '#1D9E75'
case 'failed': return '#D85A30'
case 'skipped': return '#888'
default: return 'var(--text2)'
}
}
const getNodeLabel = (node) => {
if (node.debug_prompt_slug) return node.debug_prompt_slug
if (node.debug_node_type) return `${node.debug_node_type}-${node.node_id.substring(0, 8)}`
return node.node_id
}
return (
<div style={{
marginTop: '24px',
padding: '16px',
background: 'var(--surface)',
borderRadius: '12px',
border: '1px solid var(--border)'
}}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px'
}}>
<h3 style={{
margin: 0,
fontSize: '16px',
fontWeight: 600,
color: 'var(--text1)'
}}>
🔍 Debug-Informationen ({debugNodes.length} Nodes)
</h3>
<button
onClick={toggleAll}
className="btn"
style={{
padding: '6px 12px',
fontSize: '13px',
minWidth: 'auto'
}}
>
{showAll ? 'Alle zuklappen' : 'Alle aufklappen'}
</button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{debugNodes.map((node, idx) => {
const isExpanded = expandedNodes.has(node.node_id)
const label = getNodeLabel(node)
const statusColor = getStatusColor(node.status)
return (
<div
key={node.node_id}
style={{
border: '1px solid var(--border)',
borderRadius: '8px',
background: 'var(--bg)',
overflow: 'hidden'
}}
>
{/* Node Header */}
<div
onClick={() => toggleNode(node.node_id)}
style={{
padding: '12px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
background: isExpanded ? 'var(--surface)' : 'transparent',
transition: 'background 0.2s'
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span style={{
fontSize: '13px',
color: 'var(--text3)',
fontFamily: 'monospace'
}}>
#{idx + 1}
</span>
<span style={{
fontWeight: 500,
color: 'var(--text1)',
fontSize: '14px'
}}>
{label}
</span>
<span style={{
fontSize: '12px',
padding: '2px 8px',
borderRadius: '4px',
background: statusColor,
color: 'white',
fontWeight: 500
}}>
{node.status}
</span>
{node.debug_node_type && (
<span style={{
fontSize: '11px',
padding: '2px 6px',
borderRadius: '4px',
background: 'var(--surface2)',
color: 'var(--text2)',
fontFamily: 'monospace'
}}>
{node.debug_node_type}
</span>
)}
</div>
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</div>
{/* Node Debug Content */}
{isExpanded && (
<div style={{ padding: '0 12px 12px 12px' }}>
{node.error && (
<div style={{
padding: '12px',
background: '#D85A3015',
border: '1px solid #D85A30',
borderRadius: '6px',
marginBottom: '12px'
}}>
<div style={{
fontSize: '12px',
fontWeight: 600,
color: '#D85A30',
marginBottom: '6px'
}}>
Error:
</div>
<pre style={{
margin: 0,
fontSize: '12px',
color: '#D85A30',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}>
{node.error}
</pre>
</div>
)}
{node.debug_prompt && (
<div style={{ marginBottom: '12px' }}>
<div style={{
fontSize: '12px',
fontWeight: 600,
color: 'var(--text2)',
marginBottom: '6px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
Prompt:
</div>
<pre style={{
margin: 0,
padding: '12px',
background: 'var(--surface2)',
borderRadius: '6px',
fontSize: '12px',
lineHeight: '1.6',
color: 'var(--text1)',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
maxHeight: '400px',
overflowY: 'auto',
border: '1px solid var(--border)'
}}>
{node.debug_prompt}
</pre>
</div>
)}
{node.debug_raw_response && (
<div style={{ marginBottom: '12px' }}>
<div style={{
fontSize: '12px',
fontWeight: 600,
color: 'var(--text2)',
marginBottom: '6px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
Rohe Antwort:
</div>
<pre style={{
margin: 0,
padding: '12px',
background: 'var(--surface2)',
borderRadius: '6px',
fontSize: '12px',
lineHeight: '1.6',
color: 'var(--text1)',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
maxHeight: '400px',
overflowY: 'auto',
border: '1px solid var(--border)'
}}>
{node.debug_raw_response}
</pre>
</div>
)}
{node.analysis_core && (
<div>
<div style={{
fontSize: '12px',
fontWeight: 600,
color: 'var(--text2)',
marginBottom: '6px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
Geparste Ergebnisse:
</div>
<pre style={{
margin: 0,
padding: '12px',
background: 'var(--surface2)',
borderRadius: '6px',
fontSize: '12px',
lineHeight: '1.6',
color: 'var(--text1)',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
maxHeight: '300px',
overflowY: 'auto',
border: '1px solid var(--border)'
}}>
{node.analysis_core}
</pre>
</div>
)}
{node.normalized_signals && node.normalized_signals.length > 0 && (
<div style={{ marginTop: '12px' }}>
<div style={{
fontSize: '12px',
fontWeight: 600,
color: 'var(--text2)',
marginBottom: '6px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
Signale ({node.normalized_signals.length}):
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{node.normalized_signals.map((sig, i) => (
<div
key={i}
style={{
padding: '8px',
background: 'var(--surface2)',
borderRadius: '4px',
fontSize: '12px',
border: '1px solid var(--border)'
}}
>
<span style={{ color: 'var(--text2)' }}>{sig.question_type}:</span>{' '}
<span style={{ fontWeight: 500, color: 'var(--text1)' }}>
"{sig.raw_value}" "{sig.normalized_value}"
</span>{' '}
<span style={{
fontSize: '11px',
padding: '2px 6px',
borderRadius: '3px',
background: sig.status === 'valid' ? '#1D9E7520' : '#D85A3020',
color: sig.status === 'valid' ? '#1D9E75' : '#D85A30'
}}>
{sig.status}
</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
)
})}
</div>
</div>
)
}

View File

@ -10,6 +10,7 @@ import {
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')
@ -442,7 +443,14 @@ export default function Analysis() {
}
}
setNewResult({ scope: slug, content, metadata })
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) {
@ -534,6 +542,9 @@ export default function Analysis() {
defaultOpen={true}
prompts={prompts}
/>
{newResult.node_states && (
<WorkflowDebugPanel nodeStates={newResult.node_states} />
)}
</div>
)}
@ -591,6 +602,9 @@ export default function Analysis() {
defaultOpen={true}
prompts={prompts}
/>
{newResult.node_states && (
<WorkflowDebugPanel nodeStates={newResult.node_states} />
)}
</div>
)}
{activeCategoryKey && (() => {