Merge pull request 'bug fix und Debug Workflow' (#83) from develop into main
Reviewed-on: #83
This commit is contained in:
commit
424270f0e8
|
|
@ -166,13 +166,19 @@ async def execute_workflow(
|
||||||
enable_debug=enable_debug
|
enable_debug=enable_debug
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add human-readable label to node_state for debug UI
|
||||||
|
# Priority: node.label (user-defined) > prompt_slug > node_type-id
|
||||||
|
node_label = node.label if hasattr(node, 'label') and node.label else (
|
||||||
|
node.prompt_slug if hasattr(node, 'prompt_slug') and node.prompt_slug else f"{node.type.value}-{node_id[:8]}"
|
||||||
|
)
|
||||||
|
if not node_state.debug_prompt_slug:
|
||||||
|
node_state.debug_prompt_slug = node_label
|
||||||
|
|
||||||
node_states.append(node_state)
|
node_states.append(node_state)
|
||||||
context["node_results"][node_id] = node_state
|
context["node_results"][node_id] = node_state
|
||||||
|
|
||||||
# NEW: Progress-Callback aufrufen (für SSE Streaming)
|
# NEW: Progress-Callback aufrufen (für SSE Streaming)
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
# Create a meaningful label for the node
|
|
||||||
node_label = node.prompt_slug if hasattr(node, 'prompt_slug') and node.prompt_slug else f"{node.type.value}-{node_id[:8]}"
|
|
||||||
await progress_callback("node_complete", {
|
await progress_callback("node_complete", {
|
||||||
"node_id": node_id,
|
"node_id": node_id,
|
||||||
"node_type": node.type,
|
"node_type": node.type,
|
||||||
|
|
@ -314,11 +320,11 @@ async def execute_node(
|
||||||
|
|
||||||
# Logic Nodes (Phase 3)
|
# Logic Nodes (Phase 3)
|
||||||
if node.type == "logic":
|
if node.type == "logic":
|
||||||
return execute_logic_node(node, context, graph)
|
return execute_logic_node(node, context, graph, enable_debug)
|
||||||
|
|
||||||
# Join Nodes (Phase 4)
|
# Join Nodes (Phase 4)
|
||||||
if node.type == "join":
|
if node.type == "join":
|
||||||
return execute_join_node(node, context, graph)
|
return execute_join_node(node, context, graph, enable_debug)
|
||||||
|
|
||||||
# Analysis Nodes
|
# Analysis Nodes
|
||||||
if node.type == "analysis":
|
if node.type == "analysis":
|
||||||
|
|
@ -398,6 +404,11 @@ async def execute_node(
|
||||||
decision_signals=parsed["decision_signals"],
|
decision_signals=parsed["decision_signals"],
|
||||||
normalized_signals=normalized_signals,
|
normalized_signals=normalized_signals,
|
||||||
reasoning_anchors=parsed.get("reasoning_anchors"),
|
reasoning_anchors=parsed.get("reasoning_anchors"),
|
||||||
|
# Debug information (nur wenn enable_debug=True)
|
||||||
|
debug_prompt=augmented_prompt if enable_debug else None,
|
||||||
|
debug_raw_response=llm_response if enable_debug else None,
|
||||||
|
debug_node_type="analysis",
|
||||||
|
debug_prompt_slug=node.prompt_slug if enable_debug else None,
|
||||||
started_at=started_at,
|
started_at=started_at,
|
||||||
completed_at=datetime.utcnow().isoformat()
|
completed_at=datetime.utcnow().isoformat()
|
||||||
)
|
)
|
||||||
|
|
@ -419,7 +430,8 @@ async def execute_node(
|
||||||
def execute_logic_node(
|
def execute_logic_node(
|
||||||
node,
|
node,
|
||||||
context: Dict[str, Any],
|
context: Dict[str, Any],
|
||||||
graph: WorkflowGraph
|
graph: WorkflowGraph,
|
||||||
|
enable_debug: bool = False
|
||||||
) -> NodeExecutionState:
|
) -> NodeExecutionState:
|
||||||
"""
|
"""
|
||||||
Führt Logic Node aus (Phase 3).
|
Führt Logic Node aus (Phase 3).
|
||||||
|
|
@ -519,7 +531,15 @@ def execute_logic_node(
|
||||||
"evaluation_result": result if not error else None,
|
"evaluation_result": result if not error else None,
|
||||||
"error": error,
|
"error": error,
|
||||||
"activated_edges": activated_edges
|
"activated_edges": activated_edges
|
||||||
})
|
}),
|
||||||
|
# Debug information
|
||||||
|
debug_prompt=json.dumps(expression.model_dump() if hasattr(expression, 'model_dump') else str(expression), ensure_ascii=False) if enable_debug and expression else None,
|
||||||
|
debug_raw_response=json.dumps({
|
||||||
|
"result": result,
|
||||||
|
"error": error,
|
||||||
|
"activated_edges": activated_edges
|
||||||
|
}, ensure_ascii=False) if enable_debug else None,
|
||||||
|
debug_node_type="logic"
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -536,7 +556,8 @@ def execute_logic_node(
|
||||||
def execute_join_node(
|
def execute_join_node(
|
||||||
node,
|
node,
|
||||||
context: Dict[str, Any],
|
context: Dict[str, Any],
|
||||||
graph: WorkflowGraph
|
graph: WorkflowGraph,
|
||||||
|
enable_debug: bool = False
|
||||||
) -> NodeExecutionState:
|
) -> NodeExecutionState:
|
||||||
"""
|
"""
|
||||||
Führt Join Node aus (Phase 4).
|
Führt Join Node aus (Phase 4).
|
||||||
|
|
@ -608,7 +629,16 @@ def execute_join_node(
|
||||||
normalized_signals=consolidated_signals_list,
|
normalized_signals=consolidated_signals_list,
|
||||||
metadata=join_result.metadata,
|
metadata=join_result.metadata,
|
||||||
started_at=started_at,
|
started_at=started_at,
|
||||||
completed_at=datetime.utcnow().isoformat()
|
completed_at=datetime.utcnow().isoformat(),
|
||||||
|
# Debug information
|
||||||
|
debug_prompt=f"Join Strategy: {node.join_strategy.value if node.join_strategy else 'wait_all'}\nPaths: {total_count}\nMinimum Required: {node.min_paths if node.min_paths else 'all'}" if enable_debug else None,
|
||||||
|
debug_raw_response=json.dumps({
|
||||||
|
"executed_paths": executed_count,
|
||||||
|
"total_paths": total_count,
|
||||||
|
"strategy": node.join_strategy.value if node.join_strategy else 'wait_all',
|
||||||
|
"consolidated_nodes": list(join_result.consolidated_analysis_core.keys())
|
||||||
|
}, ensure_ascii=False) if enable_debug else None,
|
||||||
|
debug_node_type="join"
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,7 @@ class WorkflowNode(BaseModel):
|
||||||
"""
|
"""
|
||||||
id: str = Field(..., description="Eindeutige Knoten-ID")
|
id: str = Field(..., description="Eindeutige Knoten-ID")
|
||||||
type: NodeType = Field(..., description="Knotentyp")
|
type: NodeType = Field(..., description="Knotentyp")
|
||||||
|
label: Optional[str] = Field(None, description="Node-Label (vom Editor, z.B. 'Qualitätseinschätzung')")
|
||||||
position: Optional[Position] = Field(None, description="Position im visuellen Editor")
|
position: Optional[Position] = Field(None, description="Position im visuellen Editor")
|
||||||
|
|
||||||
# ANALYSIS-Knoten
|
# ANALYSIS-Knoten
|
||||||
|
|
@ -349,6 +350,15 @@ class NodeExecutionState(BaseModel):
|
||||||
normalized_signals: List[NormalizedSignal] = Field(default_factory=list, description="Normalisierte Signale (Phase 2)")
|
normalized_signals: List[NormalizedSignal] = Field(default_factory=list, description="Normalisierte Signale (Phase 2)")
|
||||||
reasoning_anchors: Optional[str] = Field(None, description="Begründungsanker aus ## Begründung")
|
reasoning_anchors: Optional[str] = Field(None, description="Begründungsanker aus ## Begründung")
|
||||||
|
|
||||||
|
# Debug Information (nur wenn enable_debug=True)
|
||||||
|
debug_prompt: Optional[str] = Field(None, description="Vollständiger Prompt der an die KI gesendet wurde")
|
||||||
|
debug_raw_response: Optional[str] = Field(None, description="Rohe KI-Antwort (ungeparst)")
|
||||||
|
debug_node_type: Optional[str] = Field(None, description="Node-Typ (analysis, logic, join, etc.)")
|
||||||
|
debug_prompt_slug: Optional[str] = Field(None, description="Verwendeter Prompt-Slug (bei ANALYSIS nodes)")
|
||||||
|
|
||||||
|
# Metadata (für Join Nodes und andere Zusatzinfos)
|
||||||
|
metadata: Optional[Dict[str, Any]] = Field(None, description="Zusätzliche Metadaten (z.B. Join-Statistiken)")
|
||||||
|
|
||||||
# Error & Timing
|
# Error & Timing
|
||||||
error: Optional[str] = Field(None, description="Fehlermeldung bei failed")
|
error: Optional[str] = Field(None, description="Fehlermeldung bei failed")
|
||||||
started_at: Optional[str] = Field(None, description="Start-Timestamp (ISO)")
|
started_at: Optional[str] = Field(None, description="Start-Timestamp (ISO)")
|
||||||
|
|
|
||||||
356
frontend/src/components/WorkflowDebugPanel.jsx
Normal file
356
frontend/src/components/WorkflowDebugPanel.jsx
Normal file
|
|
@ -0,0 +1,356 @@
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -192,13 +192,17 @@ export function WorkflowResultViewer({ result, onClose }) {
|
||||||
Node States (Debug)
|
Node States (Debug)
|
||||||
</h3>
|
</h3>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
{nodeStates.map((node) => (
|
{nodeStates.map((node) => {
|
||||||
|
const hasFailed = node.status === 'failed'
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={node.node_id}
|
key={node.node_id}
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid var(--border)',
|
border: hasFailed ? '2px solid #D85A30' : '1px solid var(--border)',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
overflow: 'hidden'
|
background: hasFailed ? '#D85A3010' : 'transparent',
|
||||||
|
overflow: 'hidden',
|
||||||
|
boxShadow: hasFailed ? '0 0 0 2px #D85A3020' : 'none'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Node Header */}
|
{/* Node Header */}
|
||||||
|
|
@ -216,13 +220,13 @@ export function WorkflowResultViewer({ result, onClose }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{node.node_type === 'start' && '🚀'}
|
{(node.debug_node_type || node.node_type) === 'start' && '🚀'}
|
||||||
{node.node_type === 'analysis' && '🤖'}
|
{(node.debug_node_type || node.node_type) === 'analysis' && '🤖'}
|
||||||
{node.node_type === 'logic' && '⚡'}
|
{(node.debug_node_type || node.node_type) === 'logic' && '⚡'}
|
||||||
{node.node_type === 'join' && '🔀'}
|
{(node.debug_node_type || node.node_type) === 'join' && '🔀'}
|
||||||
{node.node_type === 'end' && '🏁'}
|
{(node.debug_node_type || node.node_type) === 'end' && '🏁'}
|
||||||
{' '}
|
{' '}
|
||||||
{node.node_label || node.node_id}
|
{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' && (
|
{node.status === 'skipped' && (
|
||||||
<span style={{ color: 'var(--text3)', marginLeft: '8px' }}>
|
<span style={{ color: 'var(--text3)', marginLeft: '8px' }}>
|
||||||
(skipped)
|
(skipped)
|
||||||
|
|
@ -237,45 +241,65 @@ export function WorkflowResultViewer({ result, onClose }) {
|
||||||
{/* Node Details */}
|
{/* Node Details */}
|
||||||
{expandedNodes[node.node_id] && (
|
{expandedNodes[node.node_id] && (
|
||||||
<div style={{ padding: '12px', fontSize: '12px' }}>
|
<div style={{ padding: '12px', fontSize: '12px' }}>
|
||||||
{node.output && (
|
{/* 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' }}>
|
<div style={{ marginBottom: '8px' }}>
|
||||||
<strong>Output:</strong>
|
<strong>Output:</strong>
|
||||||
<pre
|
<pre style={{ marginTop: '4px', padding: '8px', background: 'var(--bg)', borderRadius: '4px', fontSize: '11px', overflowX: 'auto', maxHeight: '200px', overflowY: 'auto' }}>
|
||||||
style={{
|
{typeof node.output === 'string' ? node.output : JSON.stringify(node.output, null, 2)}
|
||||||
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>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{node.error && (
|
|
||||||
<div style={{ color: 'var(--danger)', marginBottom: '8px' }}>
|
{/* Metadata */}
|
||||||
<strong>Error:</strong> {node.error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{node.metadata && (
|
{node.metadata && (
|
||||||
<div style={{ marginTop: '8px' }}>
|
<div style={{ marginTop: '8px' }}>
|
||||||
<strong>Metadata:</strong>
|
<strong>Metadata:</strong>
|
||||||
<pre
|
<pre style={{ marginTop: '4px', padding: '8px', background: 'var(--bg)', borderRadius: '4px', fontSize: '11px', overflowX: 'auto' }}>
|
||||||
style={{
|
|
||||||
marginTop: '4px',
|
|
||||||
padding: '8px',
|
|
||||||
background: 'var(--bg)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '11px',
|
|
||||||
overflowX: 'auto'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{JSON.stringify(node.metadata, null, 2)}
|
{JSON.stringify(node.metadata, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -283,7 +307,7 @@ export function WorkflowResultViewer({ result, onClose }) {
|
||||||
</div>
|
</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')
|
||||||
|
|
@ -382,7 +383,7 @@ export default function Analysis() {
|
||||||
setLoading(slug); setError(null); setNewResult(null); setProgress(null)
|
setLoading(slug); setError(null); setNewResult(null); setProgress(null)
|
||||||
try {
|
try {
|
||||||
// Use SSE-based executor for long-running workflows
|
// Use SSE-based executor for long-running workflows
|
||||||
const result = await api.executeUnifiedPromptStream(slug, null, null, false, true, (event) => {
|
const result = await api.executeUnifiedPromptStream(slug, null, null, true, true, (event) => {
|
||||||
// Progress callback: update UI in real-time
|
// Progress callback: update UI in real-time
|
||||||
if (event.type === 'execution_started') {
|
if (event.type === 'execution_started') {
|
||||||
setProgress({ total_nodes: 0, completed_nodes: 0, current_node_label: 'Starte...' })
|
setProgress({ total_nodes: 0, completed_nodes: 0, current_node_label: 'Starte...' })
|
||||||
|
|
@ -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