feat: Add WorkflowDebugPanel component to display per-node debug information
- 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:
parent
12d4d7c63b
commit
a515a5d563
352
frontend/src/components/WorkflowDebugPanel.jsx
Normal file
352
frontend/src/components/WorkflowDebugPanel.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 && (() => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user