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 { useAuth } from '../context/AuthContext'
|
||||||
import Markdown from '../utils/Markdown'
|
import Markdown from '../utils/Markdown'
|
||||||
import UsageBadge from '../components/UsageBadge'
|
import UsageBadge from '../components/UsageBadge'
|
||||||
|
import WorkflowDebugPanel from '../components/WorkflowDebugPanel'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import 'dayjs/locale/de'
|
import 'dayjs/locale/de'
|
||||||
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()
|
await loadAll()
|
||||||
setTab('run')
|
setTab('run')
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|
@ -534,6 +542,9 @@ export default function Analysis() {
|
||||||
defaultOpen={true}
|
defaultOpen={true}
|
||||||
prompts={prompts}
|
prompts={prompts}
|
||||||
/>
|
/>
|
||||||
|
{newResult.node_states && (
|
||||||
|
<WorkflowDebugPanel nodeStates={newResult.node_states} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -591,6 +602,9 @@ export default function Analysis() {
|
||||||
defaultOpen={true}
|
defaultOpen={true}
|
||||||
prompts={prompts}
|
prompts={prompts}
|
||||||
/>
|
/>
|
||||||
|
{newResult.node_states && (
|
||||||
|
<WorkflowDebugPanel nodeStates={newResult.node_states} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{activeCategoryKey && (() => {
|
{activeCategoryKey && (() => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user