feat: Workflow Engine Part 3 - Inline Prompts (v0.9q)
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>
This commit is contained in:
parent
b453ce63c6
commit
a1723db387
|
|
@ -7,8 +7,8 @@ Semantic Versioning: MAJOR.MINOR.PATCH
|
||||||
- PATCH: Bugfix, kleine Änderung, Refactor
|
- PATCH: Bugfix, kleine Änderung, Refactor
|
||||||
"""
|
"""
|
||||||
|
|
||||||
APP_VERSION = "0.9p"
|
APP_VERSION = "0.9q"
|
||||||
BUILD_DATE = "2026-04-09"
|
BUILD_DATE = "2026-04-11"
|
||||||
DB_SCHEMA_VERSION = "20260409c" # 048/049 vitals_baseline.source csv + SAVEPOINT Import
|
DB_SCHEMA_VERSION = "20260409c" # 048/049 vitals_baseline.source csv + SAVEPOINT Import
|
||||||
|
|
||||||
MODULE_VERSIONS = {
|
MODULE_VERSIONS = {
|
||||||
|
|
@ -29,13 +29,26 @@ MODULE_VERSIONS = {
|
||||||
"exportdata": "1.1.0",
|
"exportdata": "1.1.0",
|
||||||
"importdata": "1.0.0",
|
"importdata": "1.0.0",
|
||||||
"membership": "2.1.0",
|
"membership": "2.1.0",
|
||||||
"workflow": "0.6.0", # Phase 4: End Node Template Engine
|
"workflow": "0.7.0", # Part 3: Inline Prompts (reference + inline mode)
|
||||||
"app_dashboard": "1.11.0", # Entitlements: DB-Override widget→features (AND), sonst Katalog
|
"app_dashboard": "1.11.0", # Entitlements: DB-Override widget→features (AND), sonst Katalog
|
||||||
"csv_import": "0.3.2", # Import-Fehler: enrich_row_error / freundlichere 500-Hinweise
|
"csv_import": "0.3.2", # Import-Fehler: enrich_row_error / freundlichere 500-Hinweise
|
||||||
"admin_csv_templates": "0.3.0", # POST /validate + Speichern nur bei valid (422 + warnings in Response)
|
"admin_csv_templates": "0.3.0", # POST /validate + Speichern nur bei valid (422 + warnings in Response)
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.9q",
|
||||||
|
"date": "2026-04-11",
|
||||||
|
"changes": [
|
||||||
|
"Workflow Engine Part 3: Inline Prompts",
|
||||||
|
"Frontend: Radio Buttons (Reference/Inline), InlineTemplateEditor Component",
|
||||||
|
"Frontend: Placeholder Picker für Inline-Templates, Cursor-Position Tracking",
|
||||||
|
"Backend: load_prompt_template() unterstützt inline_template",
|
||||||
|
"Backend: WorkflowNode.inline_template Feld hinzugefügt",
|
||||||
|
"Serialization: inline_template speichern/laden in graph_data",
|
||||||
|
"Validation: Prüft dass entweder prompt_slug ODER inline_template gesetzt",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.9p",
|
"version": "0.9p",
|
||||||
"date": "2026-04-09",
|
"date": "2026-04-09",
|
||||||
|
|
|
||||||
|
|
@ -278,9 +278,10 @@ async def execute_node(
|
||||||
|
|
||||||
# Analysis Nodes
|
# Analysis Nodes
|
||||||
if node.type == "analysis":
|
if node.type == "analysis":
|
||||||
# 1. Lade Prompt
|
# 1. Lade Prompt (Part 3: inline_template support)
|
||||||
prompt_template = await load_prompt_template(node.prompt_slug, context)
|
prompt_template = await load_prompt_template(node, context)
|
||||||
logger.debug(f"Node {node.id}: Loaded prompt '{node.prompt_slug}'")
|
source_type = "inline" if node.inline_template else "reference"
|
||||||
|
logger.debug(f"Node {node.id}: Loaded prompt from {source_type}")
|
||||||
|
|
||||||
# 2. Parse question_augmentations
|
# 2. Parse question_augmentations
|
||||||
questions = []
|
questions = []
|
||||||
|
|
@ -812,39 +813,64 @@ def _has_active_incoming_edge(node, graph: WorkflowGraph, context: Dict[str, Any
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def load_prompt_template(prompt_slug: str, context: Dict[str, Any]) -> str:
|
async def load_prompt_template(node: WorkflowNode, context: Dict[str, Any]) -> str:
|
||||||
"""
|
"""
|
||||||
Lädt Prompt-Template aus DB und resolved Platzhalter.
|
Lädt Prompt-Template aus DB (reference mode) oder direkt vom Node (inline mode).
|
||||||
|
|
||||||
|
Part 3: Inline Prompts - Unterstützt zwei Modi:
|
||||||
|
- Reference Mode: prompt_slug → Template aus ai_prompts Tabelle
|
||||||
|
- Inline Mode: inline_template → Template direkt vom Node
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
prompt_slug: Slug des Prompts (z.B. "pipeline_body")
|
node: WorkflowNode mit prompt_slug ODER inline_template
|
||||||
context: {"variables": {"name": "Lars", ...}, "profile_id": "..."}
|
context: {"variables": {"name": "Lars", ...}, "profile_id": "..."}
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Resolved prompt template
|
Resolved prompt template
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: Wenn weder prompt_slug noch inline_template gesetzt
|
||||||
|
|
||||||
Beispiel:
|
Beispiel:
|
||||||
>>> template = await load_prompt_template("pipeline_body", {"profile_id": "123"})
|
>>> node = WorkflowNode(id="n1", prompt_slug="pipeline_body")
|
||||||
|
>>> template = await load_prompt_template(node, {"profile_id": "123"})
|
||||||
>>> "{{name}}" not in template
|
>>> "{{name}}" not in template
|
||||||
True
|
True
|
||||||
"""
|
"""
|
||||||
from placeholder_resolver import get_placeholder_example_values, get_placeholder_catalog
|
from placeholder_resolver import get_placeholder_example_values, get_placeholder_catalog
|
||||||
from prompt_executor import resolve_placeholders
|
from prompt_executor import resolve_placeholders
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
with get_db() as conn:
|
# Mode 1: Inline Template (NEU)
|
||||||
cur = get_cursor(conn)
|
if node.inline_template:
|
||||||
cur.execute(
|
logger.debug(f"Node {node.id}: Using inline template ({len(node.inline_template)} chars)")
|
||||||
"SELECT template FROM ai_prompts WHERE slug = %s AND active = true",
|
template = node.inline_template
|
||||||
(prompt_slug,)
|
|
||||||
|
# Mode 2: Reference (bestehend)
|
||||||
|
elif node.prompt_slug:
|
||||||
|
logger.debug(f"Node {node.id}: Loading prompt '{node.prompt_slug}' from DB")
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cur.execute(
|
||||||
|
"SELECT template FROM ai_prompts WHERE slug = %s AND active = true",
|
||||||
|
(node.prompt_slug,)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Prompt not found: {node.prompt_slug}")
|
||||||
|
template = row['template']
|
||||||
|
|
||||||
|
# Mode 3: Error - weder inline noch reference
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Node {node.id}: Either prompt_slug or inline_template required"
|
||||||
)
|
)
|
||||||
row = cur.fetchone()
|
|
||||||
if not row:
|
|
||||||
raise ValueError(f"Prompt not found: {prompt_slug}")
|
|
||||||
|
|
||||||
template = row['template']
|
|
||||||
|
|
||||||
# Resolve Placeholders using modern prompt_executor method
|
# Resolve Placeholders using modern prompt_executor method
|
||||||
profile_id = context.get("profile_id")
|
profile_id = context.get("profile_id")
|
||||||
|
if not profile_id:
|
||||||
|
raise HTTPException(status_code=400, detail="profile_id required in context")
|
||||||
|
|
||||||
# Build variables dict with ALL registered placeholders
|
# Build variables dict with ALL registered placeholders
|
||||||
variables = {}
|
variables = {}
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,8 @@ class WorkflowNode(BaseModel):
|
||||||
position: Optional[Position] = Field(None, description="Position im visuellen Editor")
|
position: Optional[Position] = Field(None, description="Position im visuellen Editor")
|
||||||
|
|
||||||
# ANALYSIS-Knoten
|
# ANALYSIS-Knoten
|
||||||
prompt_slug: Optional[str] = Field(None, description="Slug des auszuführenden Prompts")
|
prompt_slug: Optional[str] = Field(None, description="Slug des auszuführenden Prompts (reference mode)")
|
||||||
|
inline_template: Optional[str] = Field(None, description="Inline-Prompt-Template (inline mode, Part 3)")
|
||||||
question_augmentations: Optional[List[QuestionAugmentation]] = Field(None, description="Fragenergänzungen (knotengebunden, überschreiben Prompt-Defaults)")
|
question_augmentations: Optional[List[QuestionAugmentation]] = Field(None, description="Fragenergänzungen (knotengebunden, überschreiben Prompt-Defaults)")
|
||||||
|
|
||||||
# LOGIC-Knoten
|
# LOGIC-Knoten
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { useRef } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* InlineTemplateEditor - Template-Editor für Inline-Prompts
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - value: Template-String
|
||||||
|
* - onChange: (template) => void
|
||||||
|
* - onPlaceholderPick: () => void - Öffnet Placeholder Picker
|
||||||
|
* - textareaRef: Ref für Cursor-Position (von Parent)
|
||||||
|
*/
|
||||||
|
export function InlineTemplateEditor({ value, onChange, onPlaceholderPick, textareaRef }) {
|
||||||
|
return (
|
||||||
|
<div className="inline-template-editor" style={{ marginTop: '12px' }}>
|
||||||
|
<label className="form-label">Template</label>
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
value={value || ''}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder="Analysiere folgende Daten: Gewicht: {{ weight_current }} Ziel: {{ goal_weight }} Gib eine Empfehlung..."
|
||||||
|
rows={12}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '13px',
|
||||||
|
padding: '12px',
|
||||||
|
paddingRight: '120px', // Platz für Button
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
background: 'var(--bg)',
|
||||||
|
color: 'var(--text1)',
|
||||||
|
resize: 'vertical',
|
||||||
|
lineHeight: '1.5'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={onPlaceholderPick}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '8px',
|
||||||
|
right: '8px',
|
||||||
|
fontSize: '11px',
|
||||||
|
padding: '6px 10px',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{'{{ }}'} Platzhalter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="help-text"
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'var(--text3)',
|
||||||
|
marginTop: '6px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
💡 Tipp: Verwende <code style={{
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontFamily: 'monospace'
|
||||||
|
}}>{'{{ placeholder_name }}'}</code> für dynamische Werte
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ import { EndNodeConfig } from '../components/workflow/panels/EndNodeConfig'
|
||||||
import { PlaceholderPicker } from '../components/workflow/panels/PlaceholderPicker'
|
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 { InlineTemplateEditor } from '../components/workflow/panels/InlineTemplateEditor'
|
||||||
import '../styles/workflowEditor.css'
|
import '../styles/workflowEditor.css'
|
||||||
|
|
||||||
// Node-Type Mapping
|
// Node-Type Mapping
|
||||||
|
|
@ -50,7 +51,9 @@ export default function WorkflowEditorPage() {
|
||||||
const [availablePrompts, setAvailablePrompts] = useState([])
|
const [availablePrompts, setAvailablePrompts] = useState([])
|
||||||
const [executionResult, setExecutionResult] = useState(null)
|
const [executionResult, setExecutionResult] = useState(null)
|
||||||
const [showPlaceholderPicker, setShowPlaceholderPicker] = useState(false)
|
const [showPlaceholderPicker, setShowPlaceholderPicker] = useState(false)
|
||||||
|
const [placeholderPickerTarget, setPlaceholderPickerTarget] = useState('end') // 'end' | 'inline'
|
||||||
const endNodeTextareaRef = useRef(null)
|
const endNodeTextareaRef = useRef(null)
|
||||||
|
const inlineTemplateTextareaRef = useRef(null)
|
||||||
|
|
||||||
// Load available basis prompts for Analysis nodes
|
// Load available basis prompts for Analysis nodes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -262,30 +265,60 @@ export default function WorkflowEditorPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePlaceholderSelect = (placeholderString) => {
|
const handlePlaceholderSelect = (placeholderString) => {
|
||||||
if (!selectedNode || selectedNode.type !== 'end') return
|
if (!selectedNode) return
|
||||||
|
|
||||||
const textarea = endNodeTextareaRef.current
|
// Target bestimmen: End Node oder Inline Template
|
||||||
const currentTemplate = selectedNode.data.template || ''
|
if (placeholderPickerTarget === 'end' && selectedNode.type === 'end') {
|
||||||
|
const textarea = endNodeTextareaRef.current
|
||||||
|
const currentTemplate = selectedNode.data.template || ''
|
||||||
|
|
||||||
// Wenn Textarea Ref verfügbar, an Cursor-Position einfügen
|
// Wenn Textarea Ref verfügbar, an Cursor-Position einfügen
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
const cursorPos = textarea.selectionStart || currentTemplate.length
|
const cursorPos = textarea.selectionStart || currentTemplate.length
|
||||||
const before = currentTemplate.substring(0, cursorPos)
|
const before = currentTemplate.substring(0, cursorPos)
|
||||||
const after = currentTemplate.substring(cursorPos)
|
const after = currentTemplate.substring(cursorPos)
|
||||||
const newTemplate = before + placeholderString + after
|
const newTemplate = before + placeholderString + after
|
||||||
|
|
||||||
handleNodeUpdate(selectedNode.id, { template: newTemplate })
|
handleNodeUpdate(selectedNode.id, { template: newTemplate })
|
||||||
|
|
||||||
// Cursor nach eingefügtem Platzhalter positionieren
|
// Cursor nach eingefügtem Platzhalter positionieren
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const newPos = cursorPos + placeholderString.length
|
const newPos = cursorPos + placeholderString.length
|
||||||
textarea.setSelectionRange(newPos, newPos)
|
textarea.setSelectionRange(newPos, newPos)
|
||||||
textarea.focus()
|
textarea.focus()
|
||||||
}, 0)
|
}, 0)
|
||||||
} else {
|
} else {
|
||||||
// Fallback: Am Ende einfügen
|
// Fallback: Am Ende einfügen
|
||||||
const newTemplate = currentTemplate + placeholderString
|
const newTemplate = currentTemplate + placeholderString
|
||||||
handleNodeUpdate(selectedNode.id, { template: newTemplate })
|
handleNodeUpdate(selectedNode.id, { template: newTemplate })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline Template (Analysis Node)
|
||||||
|
else if (placeholderPickerTarget === 'inline' && selectedNode.type === 'analysis') {
|
||||||
|
const textarea = inlineTemplateTextareaRef.current
|
||||||
|
const currentTemplate = selectedNode.data.inline_template || ''
|
||||||
|
|
||||||
|
// Wenn Textarea Ref verfügbar, an Cursor-Position einfügen
|
||||||
|
if (textarea) {
|
||||||
|
const cursorPos = textarea.selectionStart || currentTemplate.length
|
||||||
|
const before = currentTemplate.substring(0, cursorPos)
|
||||||
|
const after = currentTemplate.substring(cursorPos)
|
||||||
|
const newTemplate = before + placeholderString + after
|
||||||
|
|
||||||
|
handleNodeUpdate(selectedNode.id, { inline_template: newTemplate })
|
||||||
|
|
||||||
|
// Cursor nach eingefügtem Platzhalter positionieren
|
||||||
|
setTimeout(() => {
|
||||||
|
const newPos = cursorPos + placeholderString.length
|
||||||
|
textarea.setSelectionRange(newPos, newPos)
|
||||||
|
textarea.focus()
|
||||||
|
}, 0)
|
||||||
|
} else {
|
||||||
|
// Fallback: Am Ende einfügen
|
||||||
|
const newTemplate = currentTemplate + placeholderString
|
||||||
|
handleNodeUpdate(selectedNode.id, { inline_template: newTemplate })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -455,43 +488,100 @@ export default function WorkflowEditorPage() {
|
||||||
{/* Type-spezifische Konfiguration */}
|
{/* Type-spezifische Konfiguration */}
|
||||||
{selectedNode.type === 'analysis' && (
|
{selectedNode.type === 'analysis' && (
|
||||||
<>
|
<>
|
||||||
|
{/* Prompt Source Selector */}
|
||||||
<div className="config-section">
|
<div className="config-section">
|
||||||
<label>KI-Prompt auswählen</label>
|
<label className="form-label">Prompt-Quelle</label>
|
||||||
<select
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
value={selectedNode.data.prompt_slug ? String(selectedNode.data.prompt_slug) : ''}
|
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||||
onChange={(e) => {
|
<input
|
||||||
const promptSlug = e.target.value
|
type="radio"
|
||||||
console.log('🎯 Prompt selected:', promptSlug, 'Type:', typeof promptSlug)
|
name={`promptSource-${selectedNode.id}`}
|
||||||
const selectedPrompt = availablePrompts.find(p => p.slug === promptSlug)
|
checked={!!selectedNode.data.prompt_slug}
|
||||||
console.log('📋 Selected prompt object:', selectedPrompt)
|
onChange={() => {
|
||||||
handleNodeUpdate(selectedNode.id, {
|
handleNodeUpdate(selectedNode.id, {
|
||||||
prompt_slug: promptSlug || null,
|
prompt_slug: '',
|
||||||
prompt_name: selectedPrompt?.name || null
|
inline_template: null
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{ marginRight: '8px' }}
|
||||||
width: '100%',
|
/>
|
||||||
padding: '8px',
|
<span style={{ fontSize: '14px' }}>📚 Basis-Prompt referenzieren</span>
|
||||||
borderRadius: '4px',
|
</label>
|
||||||
border: '1px solid var(--border)',
|
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||||
background: 'var(--surface)',
|
<input
|
||||||
color: 'var(--text1)'
|
type="radio"
|
||||||
}}
|
name={`promptSource-${selectedNode.id}`}
|
||||||
>
|
checked={!!selectedNode.data.inline_template || (!selectedNode.data.prompt_slug && !selectedNode.data.inline_template)}
|
||||||
<option value="">-- Basis-Prompt wählen --</option>
|
onChange={() => {
|
||||||
{availablePrompts.map(prompt => (
|
handleNodeUpdate(selectedNode.id, {
|
||||||
<option key={prompt.id} value={prompt.slug}>
|
prompt_slug: null,
|
||||||
{prompt.name}
|
inline_template: ''
|
||||||
</option>
|
})
|
||||||
))}
|
}}
|
||||||
</select>
|
style={{ marginRight: '8px' }}
|
||||||
{selectedNode.data.prompt_slug && (
|
/>
|
||||||
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--text3)' }}>
|
<span style={{ fontSize: '14px' }}>✏️ Inline-Template erstellen</span>
|
||||||
Prompt: {selectedNode.data.prompt_slug} ({selectedNode.data.prompt_name || 'unbekannt'})
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Conditional Rendering: Reference oder Inline */}
|
||||||
|
{selectedNode.data.prompt_slug !== null && !selectedNode.data.inline_template && (
|
||||||
|
<div className="config-section">
|
||||||
|
<label>Basis-Prompt auswählen</label>
|
||||||
|
<select
|
||||||
|
value={selectedNode.data.prompt_slug ? String(selectedNode.data.prompt_slug) : ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const promptSlug = e.target.value
|
||||||
|
console.log('🎯 Prompt selected:', promptSlug, 'Type:', typeof promptSlug)
|
||||||
|
const selectedPrompt = availablePrompts.find(p => p.slug === promptSlug)
|
||||||
|
console.log('📋 Selected prompt object:', selectedPrompt)
|
||||||
|
handleNodeUpdate(selectedNode.id, {
|
||||||
|
prompt_slug: promptSlug || null,
|
||||||
|
prompt_name: selectedPrompt?.name || null,
|
||||||
|
inline_template: 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.slug}>
|
||||||
|
{prompt.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{selectedNode.data.prompt_slug && (
|
||||||
|
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--text3)' }}>
|
||||||
|
Prompt: {selectedNode.data.prompt_slug} ({selectedNode.data.prompt_name || 'unbekannt'})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Inline Template Editor */}
|
||||||
|
{(selectedNode.data.inline_template !== null || !selectedNode.data.prompt_slug) && (
|
||||||
|
<InlineTemplateEditor
|
||||||
|
value={selectedNode.data.inline_template || ''}
|
||||||
|
onChange={(template) => handleNodeUpdate(selectedNode.id, {
|
||||||
|
inline_template: template,
|
||||||
|
prompt_slug: null
|
||||||
|
})}
|
||||||
|
onPlaceholderPick={() => {
|
||||||
|
setPlaceholderPickerTarget('inline')
|
||||||
|
setShowPlaceholderPicker(true)
|
||||||
|
}}
|
||||||
|
textareaRef={inlineTemplateTextareaRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<QuestionAugmentationPanel node={selectedNode} onChange={handleNodeUpdate} />
|
<QuestionAugmentationPanel node={selectedNode} onChange={handleNodeUpdate} />
|
||||||
<FallbackConfig node={selectedNode} edges={edges} nodes={nodes} onChange={handleNodeUpdate} />
|
<FallbackConfig node={selectedNode} edges={edges} nodes={nodes} onChange={handleNodeUpdate} />
|
||||||
</>
|
</>
|
||||||
|
|
@ -517,7 +607,10 @@ export default function WorkflowEditorPage() {
|
||||||
<EndNodeConfig
|
<EndNodeConfig
|
||||||
node={selectedNode}
|
node={selectedNode}
|
||||||
onChange={handleNodeUpdate}
|
onChange={handleNodeUpdate}
|
||||||
onOpenPlaceholderPicker={() => setShowPlaceholderPicker(true)}
|
onOpenPlaceholderPicker={() => {
|
||||||
|
setPlaceholderPickerTarget('end')
|
||||||
|
setShowPlaceholderPicker(true)
|
||||||
|
}}
|
||||||
textareaRef={endNodeTextareaRef}
|
textareaRef={endNodeTextareaRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export function serializeToWorkflowGraph(nodes, edges, metadata = {}) {
|
||||||
// Type-spezifische Felder
|
// Type-spezifische Felder
|
||||||
...(node.type === 'analysis' && {
|
...(node.type === 'analysis' && {
|
||||||
prompt_slug: node.data.prompt_slug || null,
|
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,
|
prompt_name: node.data.prompt_name || null,
|
||||||
question_augmentations: node.data.questions || [], // Backend erwartet question_augmentations
|
question_augmentations: node.data.questions || [], // Backend erwartet question_augmentations
|
||||||
fallback_strategy: node.data.fallback_strategy || 'conservative_skip'
|
fallback_strategy: node.data.fallback_strategy || 'conservative_skip'
|
||||||
|
|
@ -84,6 +85,7 @@ export function deserializeFromWorkflowGraph(jsonbData) {
|
||||||
|
|
||||||
...(node.type === 'analysis' && {
|
...(node.type === 'analysis' && {
|
||||||
prompt_slug: node.prompt_slug || node.prompt_id || null, // Fallback für alte Workflows mit prompt_id
|
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
|
prompt_name: node.prompt_name || null, // Falls vom Backend mitgeliefert
|
||||||
questions: node.question_augmentations || node.questions || [], // Backend sendet question_augmentations
|
questions: node.question_augmentations || node.questions || [], // Backend sendet question_augmentations
|
||||||
fallback_strategy: node.fallback_strategy || 'conservative_skip'
|
fallback_strategy: node.fallback_strategy || 'conservative_skip'
|
||||||
|
|
|
||||||
|
|
@ -86,17 +86,29 @@ function validateLogic(nodes, edges, errors, warnings) {
|
||||||
// Analysis Nodes
|
// Analysis Nodes
|
||||||
if (node.type === 'analysis') {
|
if (node.type === 'analysis') {
|
||||||
const questions = node.data.questions || []
|
const questions = node.data.questions || []
|
||||||
|
const hasPromptSlug = node.data.prompt_slug != null && node.data.prompt_slug !== ''
|
||||||
|
const hasInlineTemplate = node.data.inline_template != null && node.data.inline_template.trim() !== ''
|
||||||
|
|
||||||
// Prompt ausgewählt?
|
// Part 3: Validation - Entweder prompt_slug ODER inline_template
|
||||||
if (!node.data.prompt_slug) {
|
if (!hasPromptSlug && !hasInlineTemplate) {
|
||||||
errors.push({
|
errors.push({
|
||||||
type: 'config',
|
type: 'config',
|
||||||
message: `Analysis-Node "${node.data.label}" hat keinen Prompt`,
|
message: `Analysis-Node "${node.data.label}" benötigt entweder Basis-Prompt oder Inline-Template`,
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
severity: 'error'
|
severity: 'error'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Warning wenn beide gesetzt (sollte nicht passieren, aber zur Sicherheit)
|
||||||
|
if (hasPromptSlug && hasInlineTemplate) {
|
||||||
|
warnings.push({
|
||||||
|
type: 'config',
|
||||||
|
message: `Analysis-Node "${node.data.label}" hat sowohl Basis-Prompt als auch Inline-Template - Inline hat Vorrang`,
|
||||||
|
nodeId: node.id,
|
||||||
|
severity: 'warning'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Fragen validieren
|
// Fragen validieren
|
||||||
questions.forEach((q, idx) => {
|
questions.forEach((q, idx) => {
|
||||||
if (!q.question?.trim()) {
|
if (!q.question?.trim()) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user