feat: Part 2 - Workflow Frontend Execute Integration
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 18s

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:
Lars 2026-04-09 12:58:03 +02:00
parent 01e328b6b4
commit 46d39bad38
5 changed files with 479 additions and 3 deletions

View File

@ -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 = {

View 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>
)
}

View 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>
)
}

View File

@ -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>
)
}

View File

@ -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)),