mitai-jinkendo/frontend/src/components/workflow/panels/WorkflowResultViewer.jsx

310 lines
10 KiB
React
Raw Normal View History

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'
}}
>
feat: End Node template placeholders + clean output display Three major improvements for workflow templates: 1. **Normalized Signal Placeholders:** - Signals now available as {{ node_4.signal_kalorienbilanz }} - Uses normalized_value (not raw decision_signals) - Enables structured decision-based outputs 2. **Question Text Placeholders:** - Question texts available as {{ node_4.question_kalorienbilanz }} - Extracted from workflow graph (question_augmentations) - Allows displaying questions alongside answers 3. **Clean End Node Output:** - End Node output no longer duplicated with "## node_4" headers - aggregate_results() detects End Nodes via graph.nodes - Only shows final template-rendered output - Backward compatible: Falls back to combined_analysis if no End Node Backend workflow_executor.py: - execute_end_node(): Added normalized signals to template context - execute_end_node(): Added question texts to template context - execute_workflow(): Added graph to context for End Node access - aggregate_results(): Signature change to accept graph - aggregate_results(): Detects End Nodes and uses only their output Frontend WorkflowResultViewer.jsx: - Now uses aggregated.analysis_core (primary output) - Removed fallback to combined_analysis (was showing duplicates) Example Template: ```jinja2 **Frage:** {{ node_4.question_kalorienbilanz }} **Antwort:** {{ node_4.signal_kalorienbilanz }} --- {{ node_4.analysis_core }} ``` Issue: Signal placeholders empty, question texts unavailable, duplicate output Version: 0.9p (workflow module) Part 3: End Node Template Engine - Complete Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 20:45:08 +02:00
{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) => (
<div
key={node.node_id}
style={{
border: '1px solid var(--border)',
borderRadius: '8px',
overflow: 'hidden'
}}
>
{/* 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.node_type === 'start' && '🚀'}
{node.node_type === 'analysis' && '🤖'}
{node.node_type === 'logic' && '⚡'}
{node.node_type === 'join' && '🔀'}
{node.node_type === 'end' && '🏁'}
{' '}
{node.node_label || 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' }}>
{node.output && (
<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>
)}
{node.error && (
<div style={{ color: 'var(--danger)', marginBottom: '8px' }}>
<strong>Error:</strong> {node.error}
</div>
)}
{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>
)
}