feat: Part 2 - Workflow Frontend Execute Integration
Frontend-Komponenten für Workflow-Ausführung implementiert: **Neue Komponenten:** - WorkflowExecutePanel.jsx (~140 Zeilen) - Execute Button mit Loading State - Debug Mode Toggle - Error Handling Display - WorkflowResultViewer.jsx (~300 Zeilen) - Fixed Panel (rechts, 600px) - Final Output mit Copy-Button - Node States (collapsible, Debug Mode) - All Signals Display - Error Display **Integration:** - WorkflowEditorPage.jsx - ExecutePanel in Toolbar - executionResult State - handleExecutionComplete Handler - Slug wird beim Erstellen gespeichert **API:** - api.executeWorkflow(slug, variables, debug, save) - Nutzt /prompts/execute Endpoint - Debug Mode Default: true **Part 2 Status:** ~80% abgeschlossen - ✅ Execute Button - ✅ Result Viewer - ⏸️ Execution History (später entscheiden) Version: v0.9o Date: 2026-04-09 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
01e328b6b4
commit
46d39bad38
|
|
@ -7,8 +7,8 @@ Semantic Versioning: MAJOR.MINOR.PATCH
|
|||
- PATCH: Bugfix, kleine Änderung, Refactor
|
||||
"""
|
||||
|
||||
APP_VERSION = "0.9n"
|
||||
BUILD_DATE = "2026-04-05"
|
||||
APP_VERSION = "0.9o"
|
||||
BUILD_DATE = "2026-04-09"
|
||||
DB_SCHEMA_VERSION = "20260406e" # Migration 041
|
||||
|
||||
MODULE_VERSIONS = {
|
||||
|
|
|
|||
139
frontend/src/components/workflow/panels/WorkflowExecutePanel.jsx
Normal file
139
frontend/src/components/workflow/panels/WorkflowExecutePanel.jsx
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { useState } from 'react'
|
||||
import { api } from '../../../utils/api'
|
||||
|
||||
/**
|
||||
* WorkflowExecutePanel - Execution Controls für Workflow Editor
|
||||
*
|
||||
* Features:
|
||||
* - Execute Button mit Loading State
|
||||
* - Error Handling
|
||||
* - Success State
|
||||
* - Debug Mode Toggle
|
||||
*
|
||||
* Part 2: Frontend Execute Integration
|
||||
*/
|
||||
export function WorkflowExecutePanel({
|
||||
currentPrompt,
|
||||
onExecutionComplete,
|
||||
disabled = false
|
||||
}) {
|
||||
const [executing, setExecuting] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [debugMode, setDebugMode] = useState(true)
|
||||
|
||||
const handleExecute = async () => {
|
||||
if (!currentPrompt || !currentPrompt.slug) {
|
||||
setError('Workflow muss zuerst gespeichert werden (benötigt slug)')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setExecuting(true)
|
||||
setError(null)
|
||||
|
||||
console.log('🚀 Executing workflow:', currentPrompt.slug)
|
||||
|
||||
const result = await api.executeWorkflow(
|
||||
currentPrompt.slug,
|
||||
null, // variables (später erweiterbar)
|
||||
debugMode,
|
||||
false // save (nicht in ai_insights speichern)
|
||||
)
|
||||
|
||||
console.log('✅ Workflow execution completed:', result)
|
||||
|
||||
// Success - kurze Bestätigung
|
||||
if (result.status === 'completed') {
|
||||
// Callback mit result
|
||||
if (onExecutionComplete) {
|
||||
onExecutionComplete(result)
|
||||
}
|
||||
} else if (result.status === 'failed') {
|
||||
setError(result.error || 'Workflow-Ausführung fehlgeschlagen')
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('❌ Workflow execution error:', e)
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setExecuting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
{/* Debug Mode Toggle */}
|
||||
<label
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
fontSize: '12px',
|
||||
color: 'var(--text2)',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
title="Debug-Modus zeigt detaillierte Node-States im Ergebnis"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={debugMode}
|
||||
onChange={(e) => setDebugMode(e.target.checked)}
|
||||
disabled={executing}
|
||||
/>
|
||||
Debug
|
||||
</label>
|
||||
|
||||
{/* Execute Button */}
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={handleExecute}
|
||||
disabled={disabled || executing || !currentPrompt}
|
||||
title={
|
||||
!currentPrompt
|
||||
? 'Workflow muss zuerst gespeichert werden'
|
||||
: disabled
|
||||
? 'Workflow hat Validierungsfehler'
|
||||
: 'Workflow ausführen'
|
||||
}
|
||||
style={{
|
||||
minWidth: '120px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
{executing ? (
|
||||
<>
|
||||
<span className="spinner" style={{ width: 14, height: 14 }} />
|
||||
Executing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
▶️ Execute
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: 'var(--danger)',
|
||||
color: 'white',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
maxWidth: '300px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
title={error}
|
||||
>
|
||||
❌ {error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
303
frontend/src/components/workflow/panels/WorkflowResultViewer.jsx
Normal file
303
frontend/src/components/workflow/panels/WorkflowResultViewer.jsx
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
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 || aggregated.combined_analysis || '(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' }}>
|
||||
• {signal}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@ import { QuestionAugmentationPanel } from '../components/workflow/panels/Questio
|
|||
import { LogicExpressionEditor } from '../components/workflow/panels/LogicExpressionEditor'
|
||||
import { FallbackConfig } from '../components/workflow/panels/FallbackConfig'
|
||||
import { JoinConfig } from '../components/workflow/panels/JoinConfig'
|
||||
import { WorkflowExecutePanel } from '../components/workflow/panels/WorkflowExecutePanel'
|
||||
import { WorkflowResultViewer } from '../components/workflow/panels/WorkflowResultViewer'
|
||||
import '../styles/workflowEditor.css'
|
||||
|
||||
// Node-Type Mapping
|
||||
|
|
@ -44,6 +46,7 @@ export default function WorkflowEditorPage() {
|
|||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [availablePrompts, setAvailablePrompts] = useState([])
|
||||
const [executionResult, setExecutionResult] = useState(null)
|
||||
|
||||
// Load available basis prompts for Analysis nodes
|
||||
useEffect(() => {
|
||||
|
|
@ -156,7 +159,7 @@ export default function WorkflowEditorPage() {
|
|||
graph_data
|
||||
})
|
||||
console.log('✅ Workflow created:', result)
|
||||
setCurrentPrompt({ id: result.id, name: workflowName })
|
||||
setCurrentPrompt({ id: result.id, slug: result.slug, name: workflowName })
|
||||
alert('Workflow erstellt!')
|
||||
console.log('🚀 Navigating to:', `/workflow-editor/${result.id}`)
|
||||
navigate(`/workflow-editor/${result.id}`)
|
||||
|
|
@ -249,6 +252,11 @@ export default function WorkflowEditorPage() {
|
|||
}
|
||||
}
|
||||
|
||||
const handleExecutionComplete = (result) => {
|
||||
console.log('🎯 Execution completed:', result)
|
||||
setExecutionResult(result)
|
||||
}
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────────────────
|
||||
|
||||
return (
|
||||
|
|
@ -273,6 +281,14 @@ export default function WorkflowEditorPage() {
|
|||
<button className="btn-secondary" onClick={handleValidate}>
|
||||
Validieren {validationErrors.length > 0 ? `(${validationErrors.length} ⚠️)` : ''}
|
||||
</button>
|
||||
|
||||
{/* Execute Panel */}
|
||||
<WorkflowExecutePanel
|
||||
currentPrompt={currentPrompt}
|
||||
onExecutionComplete={handleExecutionComplete}
|
||||
disabled={validationErrors.length > 0}
|
||||
/>
|
||||
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={handleSave}
|
||||
|
|
@ -498,6 +514,14 @@ export default function WorkflowEditorPage() {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Execution Result Viewer */}
|
||||
{executionResult && (
|
||||
<WorkflowResultViewer
|
||||
result={executionResult}
|
||||
onClose={() => setExecutionResult(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -392,6 +392,16 @@ export const api = {
|
|||
if (timeframes) body.timeframes = timeframes
|
||||
return req('/prompts/execute?' + params, json(body))
|
||||
},
|
||||
|
||||
// Workflow Execution (Part 2: Frontend Execute Integration)
|
||||
executeWorkflow: (slug, variables=null, debug=true, save=false) => {
|
||||
const params = new URLSearchParams({ prompt_slug: slug })
|
||||
if (debug) params.append('debug', 'true')
|
||||
if (save) params.append('save', 'true')
|
||||
const body = variables ? { variables } : {}
|
||||
return req('/prompts/execute?' + params, json(body))
|
||||
},
|
||||
|
||||
createUnifiedPrompt: (d) => req('/prompts/unified', json(d)),
|
||||
updateUnifiedPrompt: (id,d) => req(`/prompts/unified/${id}`, jput(d)),
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user