mitai-jinkendo/frontend/src/pages/WorkflowEditorPage.jsx

393 lines
13 KiB
React
Raw Normal View History

import { useState, useCallback, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useNodesState, useEdgesState, addEdge } from 'reactflow'
import { api } from '../utils/api'
import { validateWorkflowGraph } from '../utils/workflowValidation'
import { serializeToWorkflowGraph, deserializeFromWorkflowGraph } from '../utils/workflowSerializer'
import { WorkflowCanvas } from '../components/workflow/WorkflowCanvas'
import { StartNode } from '../components/workflow/nodes/StartNode'
import { EndNode } from '../components/workflow/nodes/EndNode'
import { AnalysisNode } from '../components/workflow/nodes/AnalysisNode'
import { LogicNode } from '../components/workflow/nodes/LogicNode'
import { JoinNode } from '../components/workflow/nodes/JoinNode'
import { QuestionAugmentationPanel } from '../components/workflow/panels/QuestionAugmentationPanel'
import { LogicExpressionEditor } from '../components/workflow/panels/LogicExpressionEditor'
import { FallbackConfig } from '../components/workflow/panels/FallbackConfig'
import { JoinConfig } from '../components/workflow/panels/JoinConfig'
import '../styles/workflowEditor.css'
// Node-Type Mapping
const nodeTypes = {
start: StartNode,
end: EndNode,
analysis: AnalysisNode,
logic: LogicNode,
join: JoinNode
}
let nodeIdCounter = 1
export default function WorkflowEditorPage() {
const navigate = useNavigate()
const { id } = useParams() // prompt_id wenn vorhanden
// State
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
const [selectedNode, setSelectedNode] = useState(null)
const [currentPrompt, setCurrentPrompt] = useState(null)
const [workflowName, setWorkflowName] = useState('Neuer Workflow')
const [workflowDescription, setWorkflowDescription] = useState('')
const [validationErrors, setValidationErrors] = useState([])
const [validationWarnings, setValidationWarnings] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
// Load workflow wenn ID vorhanden
useEffect(() => {
if (id && id !== 'new') {
loadWorkflow(parseInt(id))
}
}, [id])
// Auto-Validation
useEffect(() => {
const { errors, warnings } = validateWorkflowGraph(nodes, edges)
setValidationErrors(errors)
setValidationWarnings(warnings)
}, [nodes, edges])
// ── Handlers ──────────────────────────────────────────────────────────────
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
)
const onNodeClick = useCallback((event, node) => {
setSelectedNode(node)
}, [])
const handleAddNode = (nodeType) => {
const newNode = {
id: `node_${nodeIdCounter++}`,
type: nodeType,
position: { x: 250, y: 100 + nodes.length * 100 },
data: {
label: `${nodeType.charAt(0).toUpperCase() + nodeType.slice(1)} ${nodeIdCounter - 1}`
}
}
setNodes((nds) => [...nds, newNode])
}
const handleNodeUpdate = (nodeId, updates) => {
setNodes((nds) =>
nds.map((n) => (n.id === nodeId ? { ...n, data: { ...n.data, ...updates } } : n))
)
}
const handleDeleteNode = () => {
if (!selectedNode) return
setNodes((nds) => nds.filter((n) => n.id !== selectedNode.id))
setEdges((eds) => eds.filter((e) => e.source !== selectedNode.id && e.target !== selectedNode.id))
setSelectedNode(null)
}
const handleSave = async () => {
try {
setLoading(true)
setError(null)
// Validierung
const { errors, isValid } = validateWorkflowGraph(nodes, edges)
if (!isValid) {
setError(`Validierung fehlgeschlagen: ${errors.length} Fehler gefunden`)
return
}
// Serialisieren
const graph_data = serializeToWorkflowGraph(nodes, edges, {
created_at: currentPrompt?.created_at,
version: '1.0'
})
if (currentPrompt) {
// Update existing
await api.updateUnifiedPrompt(currentPrompt.id, {
type: 'workflow',
name: workflowName,
description: workflowDescription,
graph_data
})
alert('Workflow gespeichert!')
} else {
// Create new
const result = await api.createUnifiedPrompt({
type: 'workflow',
name: workflowName,
description: workflowDescription,
graph_data
})
setCurrentPrompt({ id: result.id, name: workflowName })
navigate(`/workflow-editor/${result.id}`)
alert('Workflow erstellt!')
}
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
const loadWorkflow = async (promptId) => {
try {
setLoading(true)
setError(null)
const prompt = await api.getPrompt(promptId)
if (prompt.type !== 'workflow') {
throw new Error('Nicht ein Workflow')
}
// Deserialisieren
const { nodes: loadedNodes, edges: loadedEdges } = deserializeFromWorkflowGraph(prompt.graph_data)
setNodes(loadedNodes)
setEdges(loadedEdges)
setCurrentPrompt(prompt)
setWorkflowName(prompt.name)
setWorkflowDescription(prompt.description || '')
// nodeIdCounter aktualisieren
const maxId = Math.max(
...loadedNodes.map((n) => parseInt(n.id.replace('node_', '')) || 0),
0
)
nodeIdCounter = maxId + 1
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
const handleValidate = () => {
const { errors, warnings } = validateWorkflowGraph(nodes, edges)
setValidationErrors(errors)
setValidationWarnings(warnings)
if (errors.length === 0) {
alert(`✅ Workflow ist valide!\n\n${warnings.length} Warnungen`)
} else {
alert(`❌ Validierung fehlgeschlagen!\n\n${errors.length} Fehler, ${warnings.length} Warnungen`)
}
}
const handleNew = () => {
if (confirm('Neuen Workflow erstellen? Ungespeicherte Änderungen gehen verloren.')) {
setNodes([])
setEdges([])
setCurrentPrompt(null)
setWorkflowName('Neuer Workflow')
setWorkflowDescription('')
setSelectedNode(null)
navigate('/workflow-editor/new')
}
}
const handleDelete = async () => {
if (!currentPrompt) return
if (!confirm(`Workflow "${workflowName}" wirklich löschen?`)) return
try {
setLoading(true)
await api.deletePrompt(currentPrompt.id)
alert('Workflow gelöscht')
navigate('/admin/prompts')
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
// ── Render ────────────────────────────────────────────────────────────────
return (
<div className="workflow-editor">
{/* Toolbar */}
<div className="workflow-toolbar">
<button className="btn-secondary" onClick={() => navigate('/admin/prompts')}>
← Zurück
</button>
<input
type="text"
value={workflowName}
onChange={(e) => setWorkflowName(e.target.value)}
placeholder="Workflow-Name"
style={{ flex: 1, padding: '8px', borderRadius: '4px', border: '1px solid var(--border)' }}
/>
<button className="btn-secondary" onClick={handleNew}>
Neu
</button>
<button className="btn-secondary" onClick={handleValidate}>
Validieren
</button>
<button className="btn-primary" onClick={handleSave} disabled={loading}>
{loading ? 'Speichern...' : 'Speichern'}
</button>
{currentPrompt && (
<button className="btn-secondary" onClick={handleDelete} disabled={loading}>
Löschen
</button>
)}
</div>
{error && (
<div style={{ padding: '12px', background: 'var(--danger)', color: 'white' }}>
❌ {error}
</div>
)}
{/* Main Content */}
<div className="workflow-content">
{/* Sidebar */}
<div className="workflow-sidebar">
<div className="sidebar-section">
<h3>Workflow-Knoten</h3>
<div className="node-palette">
<button className="node-palette-button" onClick={() => handleAddNode('start')}>
<span className="icon">🚀</span> Start
</button>
<button className="node-palette-button" onClick={() => handleAddNode('analysis')}>
<span className="icon">🤖</span> Analyse
</button>
<button className="node-palette-button" onClick={() => handleAddNode('logic')}>
<span className="icon">⚡</span> Logik
</button>
<button className="node-palette-button" onClick={() => handleAddNode('join')}>
<span className="icon">🔀</span> Join
</button>
<button className="node-palette-button" onClick={() => handleAddNode('end')}>
<span className="icon">🏁</span> Ende
</button>
</div>
</div>
{selectedNode && (
<div className="sidebar-section">
<h3>Aktionen</h3>
<button className="btn-secondary btn-full" onClick={handleDeleteNode}>
🗑️ Node löschen
</button>
</div>
)}
<div className="sidebar-section">
<h3>Info</h3>
<div style={{ fontSize: '12px', color: 'var(--text3)' }}>
<div>Nodes: {nodes.length}</div>
<div>Edges: {edges.length}</div>
<div>Errors: {validationErrors.length}</div>
<div>Warnings: {validationWarnings.length}</div>
</div>
</div>
</div>
{/* Canvas */}
<div className="workflow-canvas-container">
<WorkflowCanvas
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={onNodeClick}
/>
</div>
{/* Config Panel */}
{selectedNode && (
<div className="workflow-config-panel">
<h2 style={{ margin: '0 0 16px 0' }}>{selectedNode.data.label}</h2>
{/* Basis-Konfiguration */}
<div className="config-section">
<label>Label</label>
<input
type="text"
value={selectedNode.data.label || ''}
onChange={(e) => handleNodeUpdate(selectedNode.id, { label: e.target.value })}
/>
</div>
{/* Type-spezifische Konfiguration */}
{selectedNode.type === 'analysis' && (
<>
<QuestionAugmentationPanel node={selectedNode} onChange={handleNodeUpdate} />
<FallbackConfig node={selectedNode} edges={edges} onChange={handleNodeUpdate} />
</>
)}
{selectedNode.type === 'logic' && (
<>
<LogicExpressionEditor
node={selectedNode}
nodes={nodes}
edges={edges}
onChange={handleNodeUpdate}
/>
<FallbackConfig node={selectedNode} edges={edges} onChange={handleNodeUpdate} />
</>
)}
{selectedNode.type === 'join' && (
<JoinConfig node={selectedNode} onChange={handleNodeUpdate} />
)}
</div>
)}
</div>
{/* Validation Panel */}
{(validationErrors.length > 0 || validationWarnings.length > 0) && (
<div className="validation-panel">
{validationErrors.map((err, i) => (
<div key={i} className="validation-error" onClick={() => {
if (err.nodeId) {
const node = nodes.find(n => n.id === err.nodeId)
if (node) setSelectedNode(node)
}
}}>
❌ {err.message}
</div>
))}
{validationWarnings.map((warn, i) => (
<div key={i} className="validation-warning" onClick={() => {
if (warn.nodeId) {
const node = nodes.find(n => n.id === warn.nodeId)
if (node) setSelectedNode(node)
}
}}>
⚠️ {warn.message}
</div>
))}
{validationErrors.length === 0 && validationWarnings.length > 0 && (
<div className="validation-success">
✅ Workflow ist valide ({validationWarnings.length} Warnungen)
</div>
)}
</div>
)}
</div>
)
}