feat: Part 3 - End Node Template Editor
**Neue Features:** - End Node Output Mode: AUTO vs. TEMPLATE - Jinja2 Template Editor mit Syntax-Beispiel - Placeholder Picker Modal (dynamische Node-Liste) - Template Serialisierung/Deserialisierung **Komponenten (NEU):** 1. EndNodeConfig.jsx (~150 Zeilen) - Output Mode Toggle (AUTO/TEMPLATE) - Template Textarea (monospace, 12 Zeilen) - Placeholder-Button (öffnet Picker) - Help-Text mit Beispiel-Syntax - Auto-Insert Default Template beim Wechsel zu TEMPLATE 2. PlaceholderPicker.jsx (~260 Zeilen) - Modal mit Suchfunktion - Dynamische Placeholder-Liste aus Workflow-Nodes - Kategorien: Global, Node Outputs, Signals - Click-to-Insert (schließt Modal automatisch) - Icons pro Node-Typ (🚀🤖⚡🔀🏁) **Integration:** - WorkflowEditorPage.jsx - EndNodeConfig im Config Panel (wenn type='end') - PlaceholderPicker State + Modal - handlePlaceholderSelect (fügt in Template ein) **Serialisierung:** - workflowSerializer.js - Serialize: output_mode + template für End Nodes - Deserialize: output_mode + template laden - Fallback: auto Mode wenn nicht gesetzt **Backend Status:** - ✅ Backend bereits fertig (execute_end_node() in workflow_executor.py) - ✅ Beide Modi (AUTO/TEMPLATE) funktionieren - ✅ Jinja2 Template Rendering implementiert **Part 3 Status:** Frontend Complete - ✅ End Node Config UI - ✅ Template Editor - ✅ Placeholder Picker - ⏸️ Testing ausstehend **Nächster Schritt:** Browser-Test auf dev.mitai.jinkendo.de Version: v0.9p Date: 2026-04-09 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2994df54ad
commit
228010a6d3
|
|
@ -7,7 +7,7 @@ Semantic Versioning: MAJOR.MINOR.PATCH
|
||||||
- PATCH: Bugfix, kleine Änderung, Refactor
|
- PATCH: Bugfix, kleine Änderung, Refactor
|
||||||
"""
|
"""
|
||||||
|
|
||||||
APP_VERSION = "0.9o"
|
APP_VERSION = "0.9p"
|
||||||
BUILD_DATE = "2026-04-09"
|
BUILD_DATE = "2026-04-09"
|
||||||
DB_SCHEMA_VERSION = "20260406e" # Migration 041
|
DB_SCHEMA_VERSION = "20260406e" # Migration 041
|
||||||
|
|
||||||
|
|
|
||||||
162
frontend/src/components/workflow/panels/EndNodeConfig.jsx
Normal file
162
frontend/src/components/workflow/panels/EndNodeConfig.jsx
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
/**
|
||||||
|
* EndNodeConfig - Konfiguration für End Nodes
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - node: React Flow Node object (type='end')
|
||||||
|
* - onChange: (nodeId, updates) => void
|
||||||
|
* - onOpenPlaceholderPicker: () => void (optional, für späteren Placeholder Picker)
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Output Mode: AUTO (concatenate all analyses) vs. TEMPLATE (custom Jinja2 template)
|
||||||
|
* - Template Editor (Textarea für Jinja2 syntax)
|
||||||
|
*/
|
||||||
|
export function EndNodeConfig({ node, onChange, onOpenPlaceholderPicker }) {
|
||||||
|
const outputMode = node.data.output_mode || 'auto'
|
||||||
|
const template = node.data.template || ''
|
||||||
|
|
||||||
|
const handleModeChange = (e) => {
|
||||||
|
const newMode = e.target.value
|
||||||
|
onChange(node.id, { output_mode: newMode })
|
||||||
|
|
||||||
|
// Wenn zu TEMPLATE gewechselt wird und kein Template vorhanden, Beispiel einfügen
|
||||||
|
if (newMode === 'template' && !template) {
|
||||||
|
onChange(node.id, {
|
||||||
|
output_mode: newMode,
|
||||||
|
template: '# Finale Analyse\n\n{{ analysis_core }}\n\n---\nGeneriert mit Workflow Engine'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTemplateChange = (e) => {
|
||||||
|
onChange(node.id, { template: e.target.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="config-section">
|
||||||
|
<h3>End Node Konfiguration</h3>
|
||||||
|
|
||||||
|
<label>Output-Modus</label>
|
||||||
|
<select
|
||||||
|
value={outputMode}
|
||||||
|
onChange={handleModeChange}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
background: 'var(--surface)',
|
||||||
|
color: 'var(--text1)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="auto">AUTO - Automatisch zusammenfassen</option>
|
||||||
|
<option value="template">TEMPLATE - Eigenes Template</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="help-text"
|
||||||
|
style={{
|
||||||
|
marginTop: '8px',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'var(--text3)',
|
||||||
|
lineHeight: '1.4'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{outputMode === 'auto' && (
|
||||||
|
<>
|
||||||
|
<strong>AUTO-Modus:</strong> Alle analysis_core Werte werden automatisch
|
||||||
|
zusammengefasst (backward compatible).
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{outputMode === 'template' && (
|
||||||
|
<>
|
||||||
|
<strong>TEMPLATE-Modus:</strong> Verwende Jinja2-Syntax für eigenes Output-Format.
|
||||||
|
Verfügbare Variablen: Alle Node-IDs und deren Outputs.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{outputMode === 'template' && (
|
||||||
|
<>
|
||||||
|
<label style={{ marginTop: '16px' }}>
|
||||||
|
Jinja2 Template
|
||||||
|
{onOpenPlaceholderPicker && (
|
||||||
|
<button
|
||||||
|
onClick={onOpenPlaceholderPicker}
|
||||||
|
style={{
|
||||||
|
marginLeft: '8px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: '11px',
|
||||||
|
background: 'var(--accent)',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
title="Placeholder-Auswahl öffnen"
|
||||||
|
>
|
||||||
|
📋 Platzhalter
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={template}
|
||||||
|
onChange={handleTemplateChange}
|
||||||
|
placeholder="# Finale Analyse {{ node_1.analysis_core }} {{ node_2.analysis_core }}"
|
||||||
|
rows={12}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
background: 'var(--bg)',
|
||||||
|
color: 'var(--text1)',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '13px',
|
||||||
|
lineHeight: '1.6',
|
||||||
|
resize: 'vertical'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="help-text"
|
||||||
|
style={{
|
||||||
|
marginTop: '8px',
|
||||||
|
fontSize: '11px',
|
||||||
|
color: 'var(--text3)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
💡 Beispiel-Syntax:
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
marginTop: '4px',
|
||||||
|
padding: '6px',
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '10px',
|
||||||
|
overflow: 'auto'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`{{ node_1.analysis_core }}
|
||||||
|
{% if node_2.signal_intensity == "hoch" %}
|
||||||
|
Hohe Intensität erkannt!
|
||||||
|
{% endif %}`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="help-text"
|
||||||
|
style={{
|
||||||
|
marginTop: '16px',
|
||||||
|
fontSize: '11px',
|
||||||
|
color: 'var(--text3)',
|
||||||
|
paddingTop: '12px',
|
||||||
|
borderTop: '1px solid var(--border)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🏁 Part 3: End Node Template Engine
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
265
frontend/src/components/workflow/panels/PlaceholderPicker.jsx
Normal file
265
frontend/src/components/workflow/panels/PlaceholderPicker.jsx
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PlaceholderPicker - Modal zur Auswahl von Template-Platzhaltern
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - nodes: Array of workflow nodes (to extract available placeholders)
|
||||||
|
* - onSelect: (placeholderString) => void - Callback when placeholder is selected
|
||||||
|
* - onClose: () => void
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Lists all available node outputs
|
||||||
|
* - Copy to clipboard or insert into template
|
||||||
|
* - Kategorisiert nach Node-Typ
|
||||||
|
*/
|
||||||
|
export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
|
||||||
|
// Extrahiere verfügbare Platzhalter aus Nodes
|
||||||
|
const placeholders = extractPlaceholders(nodes)
|
||||||
|
|
||||||
|
// Filtere basierend auf Suchquery
|
||||||
|
const filteredPlaceholders = placeholders.filter(p =>
|
||||||
|
p.placeholder.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
p.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSelect = (placeholderString) => {
|
||||||
|
onSelect(placeholderString)
|
||||||
|
onClose() // Schließe Modal nach Auswahl
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 2000
|
||||||
|
}}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'var(--surface)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '24px',
|
||||||
|
maxWidth: '600px',
|
||||||
|
width: '90%',
|
||||||
|
maxHeight: '80vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
boxShadow: '0 4px 16px rgba(0,0,0,0.2)'
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '16px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 style={{ margin: 0, fontSize: '18px' }}>Platzhalter auswählen</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '24px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: 'var(--text3)',
|
||||||
|
padding: '4px 8px',
|
||||||
|
lineHeight: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Suche nach Node oder Variable..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '8px 12px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
background: 'var(--bg)',
|
||||||
|
color: 'var(--text1)',
|
||||||
|
fontSize: '14px',
|
||||||
|
marginBottom: '16px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Placeholder List */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'auto',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid var(--border)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filteredPlaceholders.length === 0 ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '24px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'var(--text3)',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{searchQuery ? 'Keine Platzhalter gefunden' : 'Keine Nodes im Workflow'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{filteredPlaceholders.map((p, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
onClick={() => handleSelect(p.placeholder)}
|
||||||
|
style={{
|
||||||
|
padding: '12px',
|
||||||
|
borderBottom: idx < filteredPlaceholders.length - 1 ? '1px solid var(--border)' : 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'background 0.2s'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface2)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: 'var(--accent)',
|
||||||
|
marginBottom: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.placeholder}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'var(--text3)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '20px',
|
||||||
|
marginLeft: '12px',
|
||||||
|
color: 'var(--text3)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.icon}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: '16px',
|
||||||
|
padding: '12px',
|
||||||
|
background: 'var(--bg)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'var(--text3)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
💡 <strong>Syntax:</strong> <code style={{ background: 'var(--surface2)', padding: '2px 6px', borderRadius: '4px' }}>{'{{ node_id.field }}'}</code>
|
||||||
|
<br />
|
||||||
|
Verfügbare Felder: <code>analysis_core</code>, <code>signal_*</code> (wenn Fragen vorhanden)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrahiert verfügbare Platzhalter aus Workflow-Nodes
|
||||||
|
*/
|
||||||
|
function extractPlaceholders(nodes) {
|
||||||
|
const placeholders = []
|
||||||
|
|
||||||
|
// Globale Platzhalter
|
||||||
|
placeholders.push({
|
||||||
|
placeholder: '{{ analysis_core }}',
|
||||||
|
description: 'Finale Analyse (AUTO-Modus Fallback)',
|
||||||
|
icon: '🏁',
|
||||||
|
category: 'global'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Node-spezifische Platzhalter
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (node.type === 'end') return // End Node hat keine Outputs
|
||||||
|
|
||||||
|
const nodeId = node.id
|
||||||
|
const label = node.data.label || nodeId
|
||||||
|
|
||||||
|
// analysis_core für alle Analysis/Logic/Join Nodes
|
||||||
|
if (node.type === 'analysis' || node.type === 'logic' || node.type === 'join') {
|
||||||
|
placeholders.push({
|
||||||
|
placeholder: `{{ ${nodeId}.analysis_core }}`,
|
||||||
|
description: `${getNodeIcon(node.type)} ${label} - Hauptausgabe`,
|
||||||
|
icon: getNodeIcon(node.type),
|
||||||
|
category: 'node_outputs'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signals für Analysis Nodes mit Fragen
|
||||||
|
if (node.type === 'analysis' && node.data.questions && node.data.questions.length > 0) {
|
||||||
|
node.data.questions.forEach(q => {
|
||||||
|
const questionId = q.id || `q${placeholders.length}`
|
||||||
|
placeholders.push({
|
||||||
|
placeholder: `{{ ${nodeId}.signal_${questionId} }}`,
|
||||||
|
description: `${label} - Signal: ${q.question?.substring(0, 40)}...`,
|
||||||
|
icon: '📊',
|
||||||
|
category: 'signals'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return placeholders
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node-Typ zu Icon
|
||||||
|
*/
|
||||||
|
function getNodeIcon(type) {
|
||||||
|
const icons = {
|
||||||
|
start: '🚀',
|
||||||
|
analysis: '🤖',
|
||||||
|
logic: '⚡',
|
||||||
|
join: '🔀',
|
||||||
|
end: '🏁'
|
||||||
|
}
|
||||||
|
return icons[type] || '📦'
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,8 @@ import { QuestionAugmentationPanel } from '../components/workflow/panels/Questio
|
||||||
import { LogicExpressionEditor } from '../components/workflow/panels/LogicExpressionEditor'
|
import { LogicExpressionEditor } from '../components/workflow/panels/LogicExpressionEditor'
|
||||||
import { FallbackConfig } from '../components/workflow/panels/FallbackConfig'
|
import { FallbackConfig } from '../components/workflow/panels/FallbackConfig'
|
||||||
import { JoinConfig } from '../components/workflow/panels/JoinConfig'
|
import { JoinConfig } from '../components/workflow/panels/JoinConfig'
|
||||||
|
import { EndNodeConfig } from '../components/workflow/panels/EndNodeConfig'
|
||||||
|
import { PlaceholderPicker } from '../components/workflow/panels/PlaceholderPicker'
|
||||||
import { WorkflowExecutePanel } from '../components/workflow/panels/WorkflowExecutePanel'
|
import { WorkflowExecutePanel } from '../components/workflow/panels/WorkflowExecutePanel'
|
||||||
import { WorkflowResultViewer } from '../components/workflow/panels/WorkflowResultViewer'
|
import { WorkflowResultViewer } from '../components/workflow/panels/WorkflowResultViewer'
|
||||||
import '../styles/workflowEditor.css'
|
import '../styles/workflowEditor.css'
|
||||||
|
|
@ -47,6 +49,7 @@ export default function WorkflowEditorPage() {
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [availablePrompts, setAvailablePrompts] = useState([])
|
const [availablePrompts, setAvailablePrompts] = useState([])
|
||||||
const [executionResult, setExecutionResult] = useState(null)
|
const [executionResult, setExecutionResult] = useState(null)
|
||||||
|
const [showPlaceholderPicker, setShowPlaceholderPicker] = useState(false)
|
||||||
|
|
||||||
// Load available basis prompts for Analysis nodes
|
// Load available basis prompts for Analysis nodes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -257,6 +260,15 @@ export default function WorkflowEditorPage() {
|
||||||
setExecutionResult(result)
|
setExecutionResult(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePlaceholderSelect = (placeholderString) => {
|
||||||
|
if (!selectedNode || selectedNode.type !== 'end') return
|
||||||
|
|
||||||
|
const currentTemplate = selectedNode.data.template || ''
|
||||||
|
const newTemplate = currentTemplate + placeholderString
|
||||||
|
|
||||||
|
handleNodeUpdate(selectedNode.id, { template: newTemplate })
|
||||||
|
}
|
||||||
|
|
||||||
// ── Render ────────────────────────────────────────────────────────────────
|
// ── Render ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -480,6 +492,14 @@ export default function WorkflowEditorPage() {
|
||||||
{selectedNode.type === 'join' && (
|
{selectedNode.type === 'join' && (
|
||||||
<JoinConfig node={selectedNode} onChange={handleNodeUpdate} />
|
<JoinConfig node={selectedNode} onChange={handleNodeUpdate} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{selectedNode.type === 'end' && (
|
||||||
|
<EndNodeConfig
|
||||||
|
node={selectedNode}
|
||||||
|
onChange={handleNodeUpdate}
|
||||||
|
onOpenPlaceholderPicker={() => setShowPlaceholderPicker(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -522,6 +542,15 @@ export default function WorkflowEditorPage() {
|
||||||
onClose={() => setExecutionResult(null)}
|
onClose={() => setExecutionResult(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Placeholder Picker Modal */}
|
||||||
|
{showPlaceholderPicker && (
|
||||||
|
<PlaceholderPicker
|
||||||
|
nodes={nodes}
|
||||||
|
onSelect={handlePlaceholderSelect}
|
||||||
|
onClose={() => setShowPlaceholderPicker(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,11 @@ export function serializeToWorkflowGraph(nodes, edges, metadata = {}) {
|
||||||
join_strategy: node.data.join_strategy || 'wait_all',
|
join_strategy: node.data.join_strategy || 'wait_all',
|
||||||
skip_handling: node.data.skip_handling || 'ignore_skipped',
|
skip_handling: node.data.skip_handling || 'ignore_skipped',
|
||||||
min_paths: node.data.min_paths || 2
|
min_paths: node.data.min_paths || 2
|
||||||
|
}),
|
||||||
|
|
||||||
|
...(node.type === 'end' && {
|
||||||
|
output_mode: node.data.output_mode || 'auto',
|
||||||
|
template: node.data.template || null
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
@ -93,6 +98,11 @@ export function deserializeFromWorkflowGraph(jsonbData) {
|
||||||
join_strategy: node.join_strategy || 'wait_all',
|
join_strategy: node.join_strategy || 'wait_all',
|
||||||
skip_handling: node.skip_handling || 'ignore_skipped',
|
skip_handling: node.skip_handling || 'ignore_skipped',
|
||||||
min_paths: node.min_paths || 2
|
min_paths: node.min_paths || 2
|
||||||
|
}),
|
||||||
|
|
||||||
|
...(node.type === 'end' && {
|
||||||
|
output_mode: node.output_mode || 'auto',
|
||||||
|
template: node.template || null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user