2026-04-04 17:56:00 +02:00
|
|
|
|
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([])
|
2026-04-04 21:16:15 +02:00
|
|
|
|
const [selectedNodeId, setSelectedNodeId] = useState(null)
|
|
|
|
|
|
const selectedNode = selectedNodeId ? nodes.find(n => n.id === selectedNodeId) : null
|
2026-04-04 17:56:00 +02:00
|
|
|
|
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)
|
2026-04-04 18:59:47 +02:00
|
|
|
|
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()
|
|
|
|
|
|
}, [])
|
2026-04-04 17:56:00 +02:00
|
|
|
|
|
|
|
|
|
|
// 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) => {
|
2026-04-04 21:16:15 +02:00
|
|
|
|
setSelectedNodeId(node.id)
|
2026-04-04 17:56:00 +02:00
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
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))
|
2026-04-04 21:16:15 +02:00
|
|
|
|
setSelectedNodeId(null)
|
2026-04-04 17:56:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 })
|
|
|
|
|
|
alert('Workflow erstellt!')
|
2026-04-04 21:16:15 +02:00
|
|
|
|
navigate(`/workflow-editor/${result.id}`)
|
2026-04-04 17:56:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
} 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('')
|
2026-04-04 21:16:15 +02:00
|
|
|
|
setSelectedNodeId(null)
|
2026-04-04 17:56:00 +02:00
|
|
|
|
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}>
|
2026-04-04 18:59:47 +02:00
|
|
|
|
Validieren {validationErrors.length > 0 ? `(${validationErrors.length} ⚠️)` : ''}
|
2026-04-04 17:56:00 +02:00
|
|
|
|
</button>
|
2026-04-04 18:59:47 +02:00
|
|
|
|
<button
|
|
|
|
|
|
className="btn-primary"
|
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
|
disabled={loading}
|
|
|
|
|
|
title={validationErrors.length > 0
|
|
|
|
|
|
? `Speichern blockiert: ${validationErrors.length} Validierungsfehler`
|
|
|
|
|
|
: 'Workflow in Datenbank speichern'}
|
|
|
|
|
|
>
|
|
|
|
|
|
{loading ? 'Speichern...' : validationErrors.length > 0 ? '🔒 Speichern' : '💾 Speichern'}
|
2026-04-04 17:56:00 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
{currentPrompt && (
|
|
|
|
|
|
<button className="btn-secondary" onClick={handleDelete} disabled={loading}>
|
|
|
|
|
|
Löschen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{error && (
|
2026-04-04 18:59:47 +02:00
|
|
|
|
<div style={{ padding: '12px', background: 'var(--danger)', color: 'white', borderRadius: '4px', marginBottom: '8px' }}>
|
2026-04-04 17:56:00 +02:00
|
|
|
|
❌ {error}
|
2026-04-04 18:59:47 +02:00
|
|
|
|
{validationErrors.length > 0 && (
|
|
|
|
|
|
<div style={{ marginTop: 8, fontSize: 12 }}>
|
|
|
|
|
|
Tipp: Behebe die Validierungsfehler unten, um speichern zu können.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-04-04 17:56:00 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Main Content */}
|
|
|
|
|
|
<div className="workflow-content">
|
|
|
|
|
|
{/* Sidebar */}
|
2026-04-04 21:16:15 +02:00
|
|
|
|
<div className="workflow-sidebar" style={{ display: selectedNode ? 'none' : 'block' }}>
|
2026-04-04 17:56:00 +02:00
|
|
|
|
<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">
|
2026-04-04 18:59:47 +02:00
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
|
|
|
|
|
<h2 style={{ margin: 0 }}>Node-Konfiguration</h2>
|
|
|
|
|
|
<button
|
2026-04-04 21:16:15 +02:00
|
|
|
|
onClick={() => setSelectedNodeId(null)}
|
2026-04-04 18:59:47 +02:00
|
|
|
|
style={{
|
|
|
|
|
|
background: 'none',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
fontSize: 24,
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
color: 'var(--text3)',
|
|
|
|
|
|
padding: 4,
|
|
|
|
|
|
lineHeight: 1
|
|
|
|
|
|
}}
|
|
|
|
|
|
title="Schließen"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-04-04 17:56:00 +02:00
|
|
|
|
|
|
|
|
|
|
{/* Basis-Konfiguration */}
|
|
|
|
|
|
<div className="config-section">
|
2026-04-04 18:59:47 +02:00
|
|
|
|
<label>Node-Name</label>
|
2026-04-04 17:56:00 +02:00
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={selectedNode.data.label || ''}
|
|
|
|
|
|
onChange={(e) => handleNodeUpdate(selectedNode.id, { label: e.target.value })}
|
2026-04-04 18:59:47 +02:00
|
|
|
|
placeholder="z.B. Gewichtsanalyse"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
padding: '8px',
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
border: '1px solid var(--border)',
|
|
|
|
|
|
background: 'var(--surface)',
|
|
|
|
|
|
color: 'var(--text1)',
|
|
|
|
|
|
fontSize: '14px'
|
|
|
|
|
|
}}
|
2026-04-04 17:56:00 +02:00
|
|
|
|
/>
|
2026-04-04 18:59:47 +02:00
|
|
|
|
<div style={{ marginTop: 4, fontSize: 11, color: 'var(--text3)' }}>
|
|
|
|
|
|
Änderungen werden automatisch übernommen
|
|
|
|
|
|
</div>
|
2026-04-04 17:56:00 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Type-spezifische Konfiguration */}
|
|
|
|
|
|
{selectedNode.type === 'analysis' && (
|
|
|
|
|
|
<>
|
2026-04-04 18:59:47 +02:00
|
|
|
|
<div className="config-section">
|
|
|
|
|
|
<label>KI-Prompt auswählen</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={selectedNode.data.prompt_id || ''}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const promptId = e.target.value
|
|
|
|
|
|
const selectedPrompt = availablePrompts.find(p => p.id === parseInt(promptId))
|
|
|
|
|
|
handleNodeUpdate(selectedNode.id, {
|
|
|
|
|
|
prompt_id: promptId ? parseInt(promptId) : null,
|
|
|
|
|
|
prompt_name: selectedPrompt?.name || null
|
|
|
|
|
|
})
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
padding: '8px',
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
border: '1px solid var(--border)',
|
|
|
|
|
|
background: 'var(--surface)',
|
|
|
|
|
|
color: 'var(--text1)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">-- Basis-Prompt wählen --</option>
|
|
|
|
|
|
{availablePrompts.map(prompt => (
|
|
|
|
|
|
<option key={prompt.id} value={prompt.id}>
|
|
|
|
|
|
{prompt.name}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
{selectedNode.data.prompt_name && (
|
|
|
|
|
|
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--text3)' }}>
|
|
|
|
|
|
Gewählt: {selectedNode.data.prompt_name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-04 17:56:00 +02:00
|
|
|
|
<QuestionAugmentationPanel node={selectedNode} onChange={handleNodeUpdate} />
|
2026-04-04 21:16:15 +02:00
|
|
|
|
<FallbackConfig node={selectedNode} edges={edges} nodes={nodes} onChange={handleNodeUpdate} />
|
2026-04-04 17:56:00 +02:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{selectedNode.type === 'logic' && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<LogicExpressionEditor
|
|
|
|
|
|
node={selectedNode}
|
|
|
|
|
|
nodes={nodes}
|
|
|
|
|
|
edges={edges}
|
|
|
|
|
|
onChange={handleNodeUpdate}
|
|
|
|
|
|
/>
|
2026-04-04 21:16:15 +02:00
|
|
|
|
<FallbackConfig node={selectedNode} edges={edges} nodes={nodes} onChange={handleNodeUpdate} />
|
2026-04-04 17:56:00 +02:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{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) {
|
2026-04-04 21:16:15 +02:00
|
|
|
|
setSelectedNodeId(err.nodeId)
|
2026-04-04 17:56:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
}}>
|
|
|
|
|
|
❌ {err.message}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
{validationWarnings.map((warn, i) => (
|
|
|
|
|
|
<div key={i} className="validation-warning" onClick={() => {
|
|
|
|
|
|
if (warn.nodeId) {
|
2026-04-04 21:16:15 +02:00
|
|
|
|
setSelectedNodeId(warn.nodeId)
|
2026-04-04 17:56:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
}}>
|
|
|
|
|
|
⚠️ {warn.message}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
{validationErrors.length === 0 && validationWarnings.length > 0 && (
|
|
|
|
|
|
<div className="validation-success">
|
|
|
|
|
|
✅ Workflow ist valide ({validationWarnings.length} Warnungen)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|