fix: Phase 5 - Workflow save + node persistence bugs
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 <noreply@anthropic.com>
This commit is contained in:
parent
e3ef18674a
commit
7d22b052dd
|
|
@ -177,12 +177,12 @@ class StageCreate(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class UnifiedPromptCreate(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
|
name: str
|
||||||
slug: str
|
slug: Optional[str] = None # Auto-generated from name if not provided
|
||||||
display_name: Optional[str] = None
|
display_name: Optional[str] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
type: str # 'base' or 'pipeline'
|
type: str # 'base' | 'pipeline' | 'workflow'
|
||||||
category: str = 'ganzheitlich'
|
category: str = 'ganzheitlich'
|
||||||
active: bool = True
|
active: bool = True
|
||||||
sort_order: int = 0
|
sort_order: int = 0
|
||||||
|
|
@ -195,6 +195,9 @@ class UnifiedPromptCreate(BaseModel):
|
||||||
# For pipeline prompts (multi-stage workflow)
|
# For pipeline prompts (multi-stage workflow)
|
||||||
stages: Optional[list[StageCreate]] = None # Required if type='pipeline'
|
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):
|
class UnifiedPromptUpdate(BaseModel):
|
||||||
"""Update an existing unified prompt"""
|
"""Update an existing unified prompt"""
|
||||||
|
|
@ -209,6 +212,7 @@ class UnifiedPromptUpdate(BaseModel):
|
||||||
output_format: Optional[str] = None
|
output_format: Optional[str] = None
|
||||||
output_schema: Optional[dict] = None
|
output_schema: Optional[dict] = None
|
||||||
stages: Optional[list[StageCreate]] = None
|
stages: Optional[list[StageCreate]] = None
|
||||||
|
graph_data: Optional[dict] = None # For workflow type
|
||||||
|
|
||||||
|
|
||||||
# ── Pipeline Config Models (Issue #28) ─────────────────────────────────────
|
# ── Pipeline Config Models (Issue #28) ─────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -1380,20 +1380,26 @@ async def execute_unified_prompt(
|
||||||
@router.post("/unified")
|
@router.post("/unified")
|
||||||
def create_unified_prompt(p: UnifiedPromptCreate, session: dict = Depends(require_admin)):
|
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.
|
Admin only.
|
||||||
"""
|
"""
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(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
|
# Check for duplicate slug
|
||||||
cur.execute("SELECT id FROM ai_prompts WHERE slug=%s", (p.slug,))
|
cur.execute("SELECT id FROM ai_prompts WHERE slug=%s", (p.slug,))
|
||||||
if cur.fetchone():
|
if cur.fetchone():
|
||||||
raise HTTPException(status_code=400, detail="Slug already exists")
|
raise HTTPException(status_code=400, detail="Slug already exists")
|
||||||
|
|
||||||
# Validate type
|
# Validate type
|
||||||
if p.type not in ['base', 'pipeline']:
|
if p.type not in ['base', 'pipeline', 'workflow']:
|
||||||
raise HTTPException(status_code=400, detail="Type must be 'base' or 'pipeline'")
|
raise HTTPException(status_code=400, detail="Type must be 'base', 'pipeline', or 'workflow'")
|
||||||
|
|
||||||
# Validate base type has template
|
# Validate base type has template
|
||||||
if p.type == 'base' and not p.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:
|
if p.type == 'pipeline' and not p.stages:
|
||||||
raise HTTPException(status_code=400, detail="Pipeline prompts require 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
|
# Convert stages to JSONB
|
||||||
stages_json = None
|
stages_json = None
|
||||||
if p.stages:
|
if p.stages:
|
||||||
|
|
@ -1426,16 +1436,22 @@ def create_unified_prompt(p: UnifiedPromptCreate, session: dict = Depends(requir
|
||||||
|
|
||||||
prompt_id = str(uuid.uuid4())
|
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(
|
cur.execute(
|
||||||
"""INSERT INTO ai_prompts
|
"""INSERT INTO ai_prompts
|
||||||
(id, slug, name, display_name, description, template, category, active, sort_order,
|
(id, slug, name, display_name, description, template, category, active, sort_order,
|
||||||
type, stages, output_format, output_schema)
|
type, stages, output_format, output_schema, graph_data)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
|
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,
|
prompt_id, p.slug, p.name, p.display_name, p.description,
|
||||||
p.template, p.category, p.active, p.sort_order,
|
p.template, p.category, p.active, p.sort_order,
|
||||||
p.type, stages_json, p.output_format,
|
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')
|
updates.append('description=%s')
|
||||||
values.append(p.description)
|
values.append(p.description)
|
||||||
if p.type is not None:
|
if p.type is not None:
|
||||||
if p.type not in ['base', 'pipeline']:
|
if p.type not in ['base', 'pipeline', 'workflow']:
|
||||||
raise HTTPException(status_code=400, detail="Type must be 'base' or 'pipeline'")
|
raise HTTPException(status_code=400, detail="Type must be 'base', 'pipeline', or 'workflow'")
|
||||||
updates.append('type=%s')
|
updates.append('type=%s')
|
||||||
values.append(p.type)
|
values.append(p.type)
|
||||||
if p.category is not None:
|
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')
|
updates.append('stages=%s')
|
||||||
values.append(stages_json)
|
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:
|
if not updates:
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,16 @@ export default function WorkflowEditorPage() {
|
||||||
setValidationWarnings(warnings)
|
setValidationWarnings(warnings)
|
||||||
}, [nodes, edges])
|
}, [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 ──────────────────────────────────────────────────────────────
|
// ── Handlers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const onConnect = useCallback(
|
const onConnect = useCallback(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user