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 [selectedNodeId, setSelectedNodeId] = useState(null) const selectedNode = selectedNodeId ? nodes.find(n => n.id === selectedNodeId) : 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) const [availablePrompts, setAvailablePrompts] = useState([]) // Load available basis prompts for Analysis nodes useEffect(() => { async function loadPrompts() { try { const prompts = await api.listAdminPrompts() // Filter nur type='base' Prompts const basisPrompts = prompts.filter(p => p.type === 'base') setAvailablePrompts(basisPrompts) } catch (e) { console.error('Failed to load prompts:', e) } } loadPrompts() }, []) // Load workflow wenn ID vorhanden useEffect(() => { if (id && id !== 'new') { console.log('πŸ” useEffect: Loading workflow with ID:', id) loadWorkflow(id) // UUID as string, no parseInt! } }, [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) => { setSelectedNodeId(node.id) }, []) 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) => { console.log('πŸ”§ handleNodeUpdate:', { nodeId, updates }) setNodes((nds) => { const updated = nds.map((n) => (n.id === nodeId ? { ...n, data: { ...n.data, ...updates } } : n)) console.log('πŸ“ Nodes after update:', updated.find(n => n.id === nodeId)) return updated }) } 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)) setSelectedNodeId(null) } const handleSave = async () => { console.log('πŸ’Ύ handleSave called') 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' }) console.log('πŸ“Š Serialized graph_data:', graph_data) if (currentPrompt) { // Update existing console.log('πŸ“ Updating existing workflow:', currentPrompt.id) await api.updateUnifiedPrompt(currentPrompt.id, { type: 'workflow', name: workflowName, description: workflowDescription, graph_data }) alert('Workflow gespeichert!') } else { // Create new console.log('✨ Creating new workflow') const result = await api.createUnifiedPrompt({ type: 'workflow', name: workflowName, description: workflowDescription, graph_data }) console.log('βœ… Workflow created:', result) setCurrentPrompt({ id: result.id, name: workflowName }) alert('Workflow erstellt!') console.log('πŸš€ Navigating to:', `/workflow-editor/${result.id}`) navigate(`/workflow-editor/${result.id}`) } } catch (e) { console.error('❌ handleSave error:', e) setError(e.message) } finally { setLoading(false) } } const loadWorkflow = async (promptId) => { console.log('πŸ“¦ loadWorkflow called with:', promptId) try { setLoading(true) setError(null) const prompt = await api.getPrompt(promptId) console.log('βœ… Prompt loaded:', prompt) console.log('πŸ“Š graph_data:', prompt.graph_data) if (prompt.type !== 'workflow') { throw new Error('Nicht ein Workflow') } // Deserialisieren const { nodes: loadedNodes, edges: loadedEdges } = deserializeFromWorkflowGraph(prompt.graph_data) console.log('🎯 Deserialized:', { nodes: loadedNodes, edges: loadedEdges }) 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 console.log('βœ… Workflow loaded successfully, nodes:', loadedNodes.length, 'edges:', loadedEdges.length) } catch (e) { console.error('❌ loadWorkflow error:', 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('') setSelectedNodeId(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 (
{/* Toolbar */}
setWorkflowName(e.target.value)} placeholder="Workflow-Name" style={{ flex: 1, padding: '8px', borderRadius: '4px', border: '1px solid var(--border)' }} /> {currentPrompt && ( )}
{error && (
❌ {error} {validationErrors.length > 0 && (
Tipp: Behebe die Validierungsfehler unten, um speichern zu kΓΆnnen.
)}
)} {/* Main Content */}
{/* Sidebar */}

Workflow-Knoten

{selectedNode && (

Aktionen

)}

Info

Nodes: {nodes.length}
Edges: {edges.length}
Errors: {validationErrors.length}
Warnings: {validationWarnings.length}
{/* Canvas */}
{/* Config Panel */} {selectedNode && (

Node-Konfiguration

{/* Basis-Konfiguration */}
handleNodeUpdate(selectedNode.id, { label: e.target.value })} placeholder="z.B. Gewichtsanalyse" style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text1)', fontSize: '14px' }} />
Γ„nderungen werden automatisch ΓΌbernommen
{/* Type-spezifische Konfiguration */} {selectedNode.type === 'analysis' && ( <>
{selectedNode.data.prompt_id && (
Prompt ID: {selectedNode.data.prompt_id} ({selectedNode.data.prompt_name || 'unbekannt'})
)}
)} {selectedNode.type === 'logic' && ( <> )} {selectedNode.type === 'join' && ( )}
)}
{/* Validation Panel */} {(validationErrors.length > 0 || validationWarnings.length > 0) && (
{validationErrors.map((err, i) => (
{ if (err.nodeId) { setSelectedNodeId(err.nodeId) } }}> ❌ {err.message}
))} {validationWarnings.map((warn, i) => (
{ if (warn.nodeId) { setSelectedNodeId(warn.nodeId) } }}> ⚠️ {warn.message}
))} {validationErrors.length === 0 && validationWarnings.length > 0 && (
βœ… Workflow ist valide ({validationWarnings.length} Warnungen)
)}
)}
) }