mitai-jinkendo/frontend/src/components/workflow/panels/WorkflowResultViewer.jsx
Lars b7062d32bf
All checks were successful
Deploy Development / deploy (push) Successful in 55s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 17s
Revert "feat: Show node.name from workflow editor in debug panel"
This reverts commit 5fa2ea2e6b.
2026-04-13 15:54:22 +02:00

334 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
/**
* WorkflowResultViewer - Zeigt Execution-Ergebnisse eines Workflows
*
* Features:
* - Aggregated Result (Final Output)
* - Node States (wenn Debug Mode aktiv)
* - Collapsible Sections
* - Copy to Clipboard
*
* Part 2: Frontend Execute Integration
*/
export function WorkflowResultViewer({ result, onClose }) {
const [expandedNodes, setExpandedNodes] = useState({})
if (!result) {
return null
}
const toggleNode = (nodeId) => {
setExpandedNodes((prev) => ({ ...prev, [nodeId]: !prev[nodeId] }))
}
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text)
alert('In Zwischenablage kopiert')
}
const aggregated = result.aggregated_result || {}
const nodeStates = result.node_states || []
return (
<div
style={{
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
width: '600px',
background: 'var(--surface)',
borderLeft: '1px solid var(--border)',
boxShadow: '-2px 0 8px rgba(0,0,0,0.1)',
display: 'flex',
flexDirection: 'column',
zIndex: 1000
}}
>
{/* Header */}
<div
style={{
padding: '16px',
borderBottom: '1px solid var(--border)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
background: 'var(--surface2)'
}}
>
<h2 style={{ margin: 0, fontSize: '18px' }}>
Workflow-Ergebnis
{result.status === 'completed' && (
<span style={{ color: 'var(--accent)', marginLeft: '8px' }}></span>
)}
{result.status === 'failed' && (
<span style={{ color: 'var(--danger)', marginLeft: '8px' }}></span>
)}
</h2>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: 'var(--text3)',
padding: '4px 8px',
lineHeight: 1
}}
title="Schließen"
>
×
</button>
</div>
{/* Content */}
<div
style={{
flex: 1,
overflowY: 'auto',
padding: '16px'
}}
>
{/* Execution Info */}
<div
style={{
marginBottom: '16px',
padding: '12px',
background: 'var(--bg)',
borderRadius: '8px',
fontSize: '12px'
}}
>
<div><strong>Execution ID:</strong> {result.execution_id}</div>
<div><strong>Status:</strong> {result.status}</div>
{aggregated.total_nodes && (
<div><strong>Nodes:</strong> {aggregated.executed_nodes}/{aggregated.total_nodes}</div>
)}
{aggregated.failed_nodes > 0 && (
<div style={{ color: 'var(--danger)' }}>
<strong>Failed Nodes:</strong> {aggregated.failed_nodes}
</div>
)}
</div>
{/* Final Output */}
<div style={{ marginBottom: '16px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '8px'
}}
>
<h3 style={{ margin: 0, fontSize: '14px', fontWeight: 600 }}>
Final Output
</h3>
{aggregated.analysis_core && (
<button
className="btn-secondary"
style={{ fontSize: '11px', padding: '4px 8px' }}
onClick={() => copyToClipboard(aggregated.analysis_core)}
>
📋 Copy
</button>
)}
</div>
<div
style={{
padding: '12px',
background: 'var(--bg)',
borderRadius: '8px',
border: '1px solid var(--border)',
whiteSpace: 'pre-wrap',
fontSize: '13px',
lineHeight: 1.6,
maxHeight: '400px',
overflowY: 'auto'
}}
>
{aggregated.analysis_core || '(Kein Output)'}
</div>
</div>
{/* All Signals */}
{aggregated.all_signals && aggregated.all_signals.length > 0 && (
<div style={{ marginBottom: '16px' }}>
<h3 style={{ margin: '0 0 8px 0', fontSize: '14px', fontWeight: 600 }}>
All Signals ({aggregated.all_signals.length})
</h3>
<div
style={{
padding: '12px',
background: 'var(--bg)',
borderRadius: '8px',
border: '1px solid var(--border)',
fontSize: '12px',
maxHeight: '200px',
overflowY: 'auto'
}}
>
{aggregated.all_signals.map((signal, idx) => (
<div key={idx} style={{ marginBottom: '8px', fontFamily: 'monospace' }}>
<div>
<strong>{signal.question_type || 'unknown'}:</strong>{' '}
{signal.normalized_value || signal.raw_value || 'null'}{' '}
<span style={{ color: 'var(--text3)' }}>
({signal.status})
</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Node States (Debug Info) */}
{nodeStates.length > 0 && (
<div>
<h3 style={{ margin: '0 0 8px 0', fontSize: '14px', fontWeight: 600 }}>
Node States (Debug)
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{nodeStates.map((node) => {
const hasFailed = node.status === 'failed'
return (
<div
key={node.node_id}
style={{
border: hasFailed ? '2px solid #D85A30' : '1px solid var(--border)',
borderRadius: '8px',
background: hasFailed ? '#D85A3010' : 'transparent',
overflow: 'hidden',
boxShadow: hasFailed ? '0 0 0 2px #D85A3020' : 'none'
}}
>
{/* Node Header */}
<div
onClick={() => toggleNode(node.node_id)}
style={{
padding: '10px 12px',
background: 'var(--surface2)',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontSize: '13px',
fontWeight: 500
}}
>
<div>
{(node.debug_node_type || node.node_type) === 'start' && '🚀'}
{(node.debug_node_type || node.node_type) === 'analysis' && '🤖'}
{(node.debug_node_type || node.node_type) === 'logic' && '⚡'}
{(node.debug_node_type || node.node_type) === 'join' && '🔀'}
{(node.debug_node_type || node.node_type) === 'end' && '🏁'}
{' '}
{node.debug_prompt_slug || node.node_label || ((node.debug_node_type || node.node_type) ? `${node.debug_node_type || node.node_type}-${node.node_id.substring(0, 8)}` : node.node_id)}
{node.status === 'skipped' && (
<span style={{ color: 'var(--text3)', marginLeft: '8px' }}>
(skipped)
</span>
)}
</div>
<div style={{ fontSize: '16px' }}>
{expandedNodes[node.node_id] ? '▼' : '▶'}
</div>
</div>
{/* Node Details */}
{expandedNodes[node.node_id] && (
<div style={{ padding: '12px', fontSize: '12px' }}>
{/* Error (show first) */}
{node.error && (
<div style={{ marginBottom: '12px', padding: '12px', background: '#D85A3015', border: '1px solid #D85A30', borderRadius: '6px' }}>
<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>
)}
{/* Debug Prompt */}
{node.debug_prompt && (
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', fontWeight: 600, color: 'var(--text2)', marginBottom: '6px', textTransform: 'uppercase' }}>Prompt:</div>
<pre style={{ margin: 0, padding: '12px', background: 'var(--surface2)', borderRadius: '6px', fontSize: '11px', color: 'var(--text1)', whiteSpace: 'pre-wrap', maxHeight: '300px', overflowY: 'auto', border: '1px solid var(--border)' }}>{node.debug_prompt}</pre>
</div>
)}
{/* Debug Raw Response */}
{node.debug_raw_response && (
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', fontWeight: 600, color: 'var(--text2)', marginBottom: '6px', textTransform: 'uppercase' }}>Rohe Antwort:</div>
<pre style={{ margin: 0, padding: '12px', background: 'var(--surface2)', borderRadius: '6px', fontSize: '11px', color: 'var(--text1)', whiteSpace: 'pre-wrap', maxHeight: '300px', overflowY: 'auto', border: '1px solid var(--border)' }}>{node.debug_raw_response}</pre>
</div>
)}
{/* Analysis Core */}
{node.analysis_core && (
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', fontWeight: 600, color: 'var(--text2)', marginBottom: '6px', textTransform: 'uppercase' }}>Ergebnis:</div>
<pre style={{ margin: 0, padding: '12px', background: 'var(--surface2)', borderRadius: '6px', fontSize: '11px', color: 'var(--text1)', whiteSpace: 'pre-wrap', maxHeight: '300px', overflowY: 'auto', border: '1px solid var(--border)' }}>{node.analysis_core}</pre>
</div>
)}
{/* Normalized Signals */}
{node.normalized_signals && node.normalized_signals.length > 0 && (
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', fontWeight: 600, color: 'var(--text2)', marginBottom: '6px', textTransform: 'uppercase' }}>Signale ({node.normalized_signals.length}):</div>
{node.normalized_signals.map((sig, i) => (
<div key={i} style={{ padding: '6px', background: 'var(--surface2)', borderRadius: '4px', fontSize: '11px', marginBottom: '4px', border: '1px solid var(--border)' }}>
<span style={{ color: 'var(--text2)' }}>{sig.question_type}:</span> <span style={{ fontWeight: 500 }}>"{sig.raw_value}" "{sig.normalized_value}"</span> <span style={{ fontSize: '10px', padding: '2px 4px', borderRadius: '2px', background: sig.status === 'valid' ? '#1D9E7520' : '#D85A3020', color: sig.status === 'valid' ? '#1D9E75' : '#D85A30' }}>{sig.status}</span>
</div>
))}
</div>
)}
{/* Output (fallback) */}
{node.output && !node.analysis_core && (
<div style={{ marginBottom: '8px' }}>
<strong>Output:</strong>
<pre style={{ marginTop: '4px', padding: '8px', background: 'var(--bg)', borderRadius: '4px', fontSize: '11px', overflowX: 'auto', maxHeight: '200px', overflowY: 'auto' }}>
{typeof node.output === 'string' ? node.output : JSON.stringify(node.output, null, 2)}
</pre>
</div>
)}
{/* Metadata */}
{node.metadata && (
<div style={{ marginTop: '8px' }}>
<strong>Metadata:</strong>
<pre style={{ marginTop: '4px', padding: '8px', background: 'var(--bg)', borderRadius: '4px', fontSize: '11px', overflowX: 'auto' }}>
{JSON.stringify(node.metadata, null, 2)}
</pre>
</div>
)}
</div>
)}
</div>
)})}
</div>
</div>
)}
{/* Error (if failed) */}
{result.error && (
<div
style={{
marginTop: '16px',
padding: '12px',
background: 'var(--danger)',
color: 'white',
borderRadius: '8px',
fontSize: '13px'
}}
>
<strong>Error:</strong> {result.error}
</div>
)}
</div>
</div>
)
}