From 7d22b052dd56987b90f10a0ef5dce65af688a73f Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 4 Apr 2026 19:17:41 +0200 Subject: [PATCH] fix: Phase 5 - Workflow save + node persistence bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KRITISCHE FIXES: 1. Backend: Workflow-Type Support - models.py: graph_data Feld hinzugefügt - models.py: slug Optional (auto-generiert) - prompts.py: 'workflow' in erlaubten Typen - prompts.py: graph_data in INSERT/UPDATE - prompts.py: Auto-Slug-Generierung aus Name - FIX: "Field required: slug" Error behoben 2. Frontend: Node-Updates Persistence - selectedNode sync mit nodes array (useEffect) - FIX: Änderungen gingen verloren (stale state) - FIX: Prompt-Auswahl nicht sichtbar nach Edit - FIX: Fallback-Strategy nicht gespeichert - FIX: Node-Name Änderungen nicht übernommen BEHOBEN: - ❌ Save fehlgeschlagen → ✅ Workflows speicherbar - ❌ Node-Name ignoriert → ✅ Live-Update - ❌ Prompt verschwindet → ✅ Bleibt sichtbar - ❌ Fallback nicht saved → ✅ Persistiert Tested: Backend API akzeptiert jetzt type='workflow' Co-Authored-By: Claude Opus 4.6 --- backend/models.py | 10 +++++-- backend/routers/prompts.py | 35 +++++++++++++++++------ frontend/src/pages/WorkflowEditorPage.jsx | 10 +++++++ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/backend/models.py b/backend/models.py index 4c0c39b..65feaee 100644 --- a/backend/models.py +++ b/backend/models.py @@ -177,12 +177,12 @@ class StageCreate(BaseModel): class UnifiedPromptCreate(BaseModel): - """Create a new unified prompt (base or pipeline type)""" + """Create a new unified prompt (base, pipeline, or workflow type)""" name: str - slug: str + slug: Optional[str] = None # Auto-generated from name if not provided display_name: Optional[str] = None description: Optional[str] = None - type: str # 'base' or 'pipeline' + type: str # 'base' | 'pipeline' | 'workflow' category: str = 'ganzheitlich' active: bool = True sort_order: int = 0 @@ -195,6 +195,9 @@ class UnifiedPromptCreate(BaseModel): # For pipeline prompts (multi-stage workflow) stages: Optional[list[StageCreate]] = None # Required if type='pipeline' + # For workflow prompts (visual graph editor) + graph_data: Optional[dict] = None # Required if type='workflow' + class UnifiedPromptUpdate(BaseModel): """Update an existing unified prompt""" @@ -209,6 +212,7 @@ class UnifiedPromptUpdate(BaseModel): output_format: Optional[str] = None output_schema: Optional[dict] = None stages: Optional[list[StageCreate]] = None + graph_data: Optional[dict] = None # For workflow type # ── Pipeline Config Models (Issue #28) ───────────────────────────────────── diff --git a/backend/routers/prompts.py b/backend/routers/prompts.py index 6cecd4e..c92bd23 100644 --- a/backend/routers/prompts.py +++ b/backend/routers/prompts.py @@ -1380,20 +1380,26 @@ async def execute_unified_prompt( @router.post("/unified") def create_unified_prompt(p: UnifiedPromptCreate, session: dict = Depends(require_admin)): """ - Create a new unified prompt (base or pipeline type). + Create a new unified prompt (base, pipeline, or workflow type). Admin only. """ with get_db() as conn: cur = get_cursor(conn) + # Auto-generate slug if not provided (for workflows) + if not p.slug: + import re + base_slug = re.sub(r'[^a-z0-9_]+', '_', p.name.lower()).strip('_') + p.slug = f"{base_slug}_{uuid.uuid4().hex[:6]}" + # Check for duplicate slug cur.execute("SELECT id FROM ai_prompts WHERE slug=%s", (p.slug,)) if cur.fetchone(): raise HTTPException(status_code=400, detail="Slug already exists") # Validate type - if p.type not in ['base', 'pipeline']: - raise HTTPException(status_code=400, detail="Type must be 'base' or 'pipeline'") + if p.type not in ['base', 'pipeline', 'workflow']: + raise HTTPException(status_code=400, detail="Type must be 'base', 'pipeline', or 'workflow'") # Validate base type has template if p.type == 'base' and not p.template: @@ -1403,6 +1409,10 @@ def create_unified_prompt(p: UnifiedPromptCreate, session: dict = Depends(requir if p.type == 'pipeline' and not p.stages: raise HTTPException(status_code=400, detail="Pipeline prompts require stages") + # Validate workflow type has graph_data + if p.type == 'workflow' and not p.graph_data: + raise HTTPException(status_code=400, detail="Workflow prompts require graph_data") + # Convert stages to JSONB stages_json = None if p.stages: @@ -1426,16 +1436,22 @@ def create_unified_prompt(p: UnifiedPromptCreate, session: dict = Depends(requir prompt_id = str(uuid.uuid4()) + # Convert graph_data to JSONB + graph_data_json = None + if p.graph_data: + graph_data_json = json.dumps(p.graph_data) + cur.execute( """INSERT INTO ai_prompts (id, slug, name, display_name, description, template, category, active, sort_order, - type, stages, output_format, output_schema) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""", + type, stages, output_format, output_schema, graph_data) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""", ( prompt_id, p.slug, p.name, p.display_name, p.description, p.template, p.category, p.active, p.sort_order, p.type, stages_json, p.output_format, - json.dumps(p.output_schema) if p.output_schema else None + json.dumps(p.output_schema) if p.output_schema else None, + graph_data_json ) ) @@ -1470,8 +1486,8 @@ def update_unified_prompt(prompt_id: str, p: UnifiedPromptUpdate, session: dict updates.append('description=%s') values.append(p.description) if p.type is not None: - if p.type not in ['base', 'pipeline']: - raise HTTPException(status_code=400, detail="Type must be 'base' or 'pipeline'") + if p.type not in ['base', 'pipeline', 'workflow']: + raise HTTPException(status_code=400, detail="Type must be 'base', 'pipeline', or 'workflow'") updates.append('type=%s') values.append(p.type) if p.category is not None: @@ -1512,6 +1528,9 @@ def update_unified_prompt(prompt_id: str, p: UnifiedPromptUpdate, session: dict ]) updates.append('stages=%s') values.append(stages_json) + if p.graph_data is not None: + updates.append('graph_data=%s') + values.append(json.dumps(p.graph_data)) if not updates: return {"ok": True} diff --git a/frontend/src/pages/WorkflowEditorPage.jsx b/frontend/src/pages/WorkflowEditorPage.jsx index f14b7f3..1caddc0 100644 --- a/frontend/src/pages/WorkflowEditorPage.jsx +++ b/frontend/src/pages/WorkflowEditorPage.jsx @@ -73,6 +73,16 @@ export default function WorkflowEditorPage() { setValidationWarnings(warnings) }, [nodes, edges]) + // Keep selectedNode in sync with nodes array (wichtig für Config Panel!) + useEffect(() => { + if (selectedNode) { + const updatedNode = nodes.find(n => n.id === selectedNode.id) + if (updatedNode && updatedNode !== selectedNode) { + setSelectedNode(updatedNode) + } + } + }, [nodes]) + // ── Handlers ────────────────────────────────────────────────────────────── const onConnect = useCallback(