Ermöglicht Analysis Nodes zwischen zwei Prompt-Modi zu wählen: - Reference Mode: Basis-Prompt aus DB referenzieren (bestehend) - Inline Mode: Template direkt im Node editieren (NEU) Frontend: - InlineTemplateEditor Component (~80 Zeilen) - Radio Buttons in WorkflowEditorPage für Mode-Auswahl - Placeholder Picker für beide Modi (End Node + Inline Template) - Cursor-Position Tracking mit textareaRef - Conditional Rendering basierend auf promptSource - Validation: Entweder prompt_slug ODER inline_template Backend: - load_prompt_template() akzeptiert ganzen WorkflowNode (statt nur slug) - Unterstützt inline_template (Mode 1) und prompt_slug (Mode 2) - WorkflowNode.inline_template Feld hinzugefügt - Validation: HTTPException wenn weder slug noch template Serialization: - inline_template in graph_data speichern/laden - Backward-compatible mit bestehenden Workflows Version: 0.9q Module: workflow 0.7.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
131 lines
4.0 KiB
JavaScript
131 lines
4.0 KiB
JavaScript
/**
|
|
* Workflow Serialization Utilities
|
|
*
|
|
* Konvertiert zwischen React Flow (Canvas) und Backend-Format (JSONB).
|
|
*/
|
|
|
|
/**
|
|
* Serialisiert React Flow Graph zu Backend-kompatiblem Format
|
|
*
|
|
* @param {Array} nodes - React Flow nodes
|
|
* @param {Array} edges - React Flow edges
|
|
* @param {Object} metadata - Zusätzliche Metadaten
|
|
* @returns {Object} JSONB-kompatibles Objekt für ai_prompts.graph_data
|
|
*/
|
|
export function serializeToWorkflowGraph(nodes, edges, metadata = {}) {
|
|
const workflowNodes = nodes.map(node => ({
|
|
id: node.id,
|
|
type: node.type,
|
|
label: node.data.label || node.type,
|
|
position: { x: node.position.x, y: node.position.y },
|
|
|
|
// Type-spezifische Felder
|
|
...(node.type === 'analysis' && {
|
|
prompt_slug: node.data.prompt_slug || null,
|
|
inline_template: node.data.inline_template || null, // Part 3: Inline Prompts
|
|
prompt_name: node.data.prompt_name || null,
|
|
question_augmentations: node.data.questions || [], // Backend erwartet question_augmentations
|
|
fallback_strategy: node.data.fallback_strategy || 'conservative_skip'
|
|
}),
|
|
|
|
...(node.type === 'logic' && {
|
|
condition: node.data.condition || null,
|
|
fallback_strategy: node.data.fallback_strategy || 'conservative_skip'
|
|
}),
|
|
|
|
...(node.type === 'join' && {
|
|
join_strategy: node.data.join_strategy || 'wait_all',
|
|
skip_handling: node.data.skip_handling || 'ignore_skipped',
|
|
min_paths: node.data.min_paths || 2
|
|
}),
|
|
|
|
...(node.type === 'end' && {
|
|
output_mode: node.data.output_mode || 'auto',
|
|
template: node.data.template || null
|
|
})
|
|
}))
|
|
|
|
const workflowEdges = edges.map(edge => ({
|
|
id: edge.id,
|
|
source: edge.source,
|
|
target: edge.target,
|
|
label: edge.data?.label || null,
|
|
sourceHandle: edge.sourceHandle || null,
|
|
targetHandle: edge.targetHandle || null
|
|
}))
|
|
|
|
return {
|
|
nodes: workflowNodes,
|
|
edges: workflowEdges,
|
|
metadata: {
|
|
created_at: metadata.created_at || new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
version: metadata.version || '1.0'
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deserialisiert Backend-Format zu React Flow Graph
|
|
*
|
|
* @param {Object} jsonbData - ai_prompts.graph_data (JSONB)
|
|
* @returns {Object} { nodes, edges, metadata }
|
|
*/
|
|
export function deserializeFromWorkflowGraph(jsonbData) {
|
|
if (!jsonbData || !jsonbData.nodes || !jsonbData.edges) {
|
|
throw new Error('Invalid workflow graph data')
|
|
}
|
|
|
|
const reactFlowNodes = jsonbData.nodes.map(node => ({
|
|
id: node.id,
|
|
type: node.type,
|
|
position: { x: node.position.x, y: node.position.y },
|
|
data: {
|
|
label: node.label,
|
|
|
|
...(node.type === 'analysis' && {
|
|
prompt_slug: node.prompt_slug || node.prompt_id || null, // Fallback für alte Workflows mit prompt_id
|
|
inline_template: node.inline_template || null, // Part 3: Inline Prompts
|
|
prompt_name: node.prompt_name || null, // Falls vom Backend mitgeliefert
|
|
questions: node.question_augmentations || node.questions || [], // Backend sendet question_augmentations
|
|
fallback_strategy: node.fallback_strategy || 'conservative_skip'
|
|
}),
|
|
|
|
...(node.type === 'logic' && {
|
|
condition: node.condition || null,
|
|
fallback_strategy: node.fallback_strategy || 'conservative_skip'
|
|
}),
|
|
|
|
...(node.type === 'join' && {
|
|
join_strategy: node.join_strategy || 'wait_all',
|
|
skip_handling: node.skip_handling || 'ignore_skipped',
|
|
min_paths: node.min_paths || 2
|
|
}),
|
|
|
|
...(node.type === 'end' && {
|
|
output_mode: node.output_mode || 'auto',
|
|
template: node.template || null
|
|
})
|
|
}
|
|
}))
|
|
|
|
const reactFlowEdges = jsonbData.edges.map(edge => ({
|
|
id: edge.id,
|
|
source: edge.source,
|
|
target: edge.target,
|
|
sourceHandle: edge.sourceHandle || null,
|
|
targetHandle: edge.targetHandle || null,
|
|
data: {
|
|
label: edge.label || null
|
|
},
|
|
type: 'default',
|
|
animated: false
|
|
}))
|
|
|
|
return {
|
|
nodes: reactFlowNodes,
|
|
edges: reactFlowEdges,
|
|
metadata: jsonbData.metadata || {}
|
|
}
|
|
}
|