mitai-jinkendo/frontend/src/components/WorkflowDebugPanel.jsx
Lars 0a27533262
Some checks failed
Build Test / lint-backend (push) Waiting to run
Build Test / build-frontend (push) Waiting to run
Deploy Development / deploy (push) Successful in 54s
Build Test / pytest-backend (push) Has been cancelled
feat: Highlight failed nodes in WorkflowDebugPanel
- Failed nodes now have:
  - Red border (2px instead of 1px)
  - Light red background (#D85A3010)
  - Red shadow/glow effect

Makes it immediately obvious which nodes had errors.
2026-04-13 12:58:25 +02:00

357 lines
12 KiB
JavaScript

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)
const hasFailed = node.status === 'failed'
const hasError = node.error != null
return (
<div
key={node.node_id}
style={{
border: hasFailed ? '2px solid #D85A30' : '1px solid var(--border)',
borderRadius: '8px',
background: hasFailed ? '#D85A3010' : 'var(--bg)',
overflow: 'hidden',
boxShadow: hasFailed ? '0 0 0 2px #D85A3020' : 'none'
}}
>
{/* 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>
)
}