From 28b6fb28d5c70d3fc3375d7a27773d8c3d5beae2 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 11 Apr 2026 11:32:46 +0200 Subject: [PATCH 1/5] =?UTF-8?q?neuer=20Viewport=20f=C3=BCr=20Admin-Seiten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/docs/working/Test_status_Wkf.md | 11 ++++ frontend/src/app.css | 16 ++++- test_placeholder_resolution.py | 85 +++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 .claude/docs/working/Test_status_Wkf.md create mode 100644 test_placeholder_resolution.py diff --git a/.claude/docs/working/Test_status_Wkf.md b/.claude/docs/working/Test_status_Wkf.md new file mode 100644 index 0000000..f62468c --- /dev/null +++ b/.claude/docs/working/Test_status_Wkf.md @@ -0,0 +1,11 @@ +Folgende Ergebnisse des Tests: +- Valididierung gibt immer noch keine Aufschlüsse was für Fehler und Warning es sind, Es zeigt immer nur noch die Anzahl der entsprechenden Fehler/Warnungen +- Speichern als kurzes PopUp -- gut +- In der Node selbst wird nun eine Fehlermeldung ausgegeben. Das ist gut. In größen Workflows aber schwierig den Fehler zu lokalisieren. +- In der automatischen Zusammenfassung in der Endnode kommt als Überschrift, z.B. Node 10, anstatt den Node-Name auszugeben. +- Alle Änderungen an Nodes scheinen automatisch in den Gesamtflow übernommen zu werden. Diese werden dann nach dem Speichern aktiv. Da muss man sehr vorsichtig sein, bei kurzen Änderungen und dem Ausprobieren. +- Der Testlauf "Execute" sollte auf dem aktuellen Workflowstand ausgeführt werden, auch wenn dieser vom gespeicherten Abweicht. Ich würde natürlich vor dem Speichern den Workflow testen können. Prüfe und bewerte diesen Punkt, setze ihn aber noch nicht um. +- Die Workflows werden aktuell nicht in Analyse und den verfügbaren KI-Asuwertungen angezeigt. ggf. weil wir sie aktuell noch keinem Bereich zuordnen können. Diesen könnten wir ggf. über die Start-Node im Workflow konfigurieren. +- Das löschen von Knoten und Kanten funktioniert aktuell nur über Backspace nicht über entfernen +- Wir sollten auch dafür sorgen, dass jeweils nur eine Start-Node, End-Node in einem Workflow existiert, Prüfe ob mehrere End-Nodes sinnvoll sind, da wir ja auch Logik-Pfade abbilden und ggf. auch eine route beschreiten, die ein anderes Ende hat. (Prüfe, ob das heute schon möglich wäre!) +- Als zukünftige Ausbaustufe sollten wir überlegen, ob wir auch Trigger implementieren, z.B. um Kurzstatements zu generieren, wenn neue Daten hereinkommen und wir diese Bewertungen aktualisieren wollen \ No newline at end of file diff --git a/frontend/src/app.css b/frontend/src/app.css index 9f4d219..a237ef6 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -18,6 +18,8 @@ --header-h: 52px; --font: system-ui, -apple-system, 'Segoe UI', sans-serif; --capture-content-max: 800px; + /* Admin: nutzt volle Hauptspalte bis zu dieser Obergrenze (siehe .app-main:has(.admin-shell)) */ + --admin-main-max: min(1560px, calc(100vw - 220px)); } @media (prefers-color-scheme: dark) { :root { @@ -619,13 +621,16 @@ a.analysis-split__nav-item { .admin-page { width: 100%; + min-width: 0; } +/* Desktop: volle Breite der Admin-Spalte (nicht wie Erfassung 800px); Lesegröße leicht skaliert */ @media (min-width: 1024px) { .admin-page { - max-width: var(--capture-content-max); - margin-left: auto; - margin-right: auto; + max-width: 100%; + margin-left: 0; + margin-right: 0; + font-size: clamp(15px, 0.88rem + 0.25vw, 18px); } } @@ -841,6 +846,11 @@ a.analysis-split__nav-item { box-sizing: border-box; } + /* Admin: mehr horizontaler Raum für Tabellen auf großen Screens (:has ~2022+, sonst bleibt 1200px) */ + .app-main:has(.admin-shell) { + max-width: var(--admin-main-max); + } + /* Dashboard (P3): Begrüßung + Kennzahlen-Zeile */ .dashboard-greeting { display: flex; diff --git a/test_placeholder_resolution.py b/test_placeholder_resolution.py new file mode 100644 index 0000000..d67f84b --- /dev/null +++ b/test_placeholder_resolution.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +""" +Test placeholder resolution in inline templates + +This script simulates what happens in workflow_executor.load_prompt_template() +""" +import sys +sys.path.insert(0, 'backend') + +from placeholder_resolver import get_placeholder_example_values, get_placeholder_catalog +from prompt_executor import resolve_placeholders + +# Test profile_id (use first profile in dev DB) +PROFILE_ID = "019601b5-d65a-738f-a1e7-b3f69bb97f69" # Lars profile from dev + +def test_placeholder_resolution(): + """Test the exact same logic as in workflow_executor.load_prompt_template()""" + + # Test template with spaces in placeholders (as user showed) + template = "Hallo {{ name }}, du bist {{ age }} Jahre alt und {{ geschlecht }}." + + print("=" * 80) + print("PLACEHOLDER RESOLUTION TEST") + print("=" * 80) + print(f"\nTemplate:\n{template}\n") + + # Step 1: Load placeholders (same as workflow_executor) + print("Step 1: Loading placeholders...") + processed_placeholders = get_placeholder_example_values(PROFILE_ID) + print(f" Loaded {len(processed_placeholders)} placeholders") + print(f" Sample keys (first 5): {list(processed_placeholders.keys())[:5]}") + + # Step 2: Clean keys (same as workflow_executor) + print("\nStep 2: Cleaning keys...") + cleaned_placeholders = { + key.replace('{{', '').replace('}}', '').strip(): value + for key, value in processed_placeholders.items() + } + print(f" Cleaned keys (first 5): {list(cleaned_placeholders.keys())[:5]}") + print(f" Sample values:") + print(f" name = {cleaned_placeholders.get('name')}") + print(f" age = {cleaned_placeholders.get('age')}") + print(f" geschlecht = {cleaned_placeholders.get('geschlecht')}") + + variables = cleaned_placeholders + + # Step 3: Load catalog + print("\nStep 3: Loading catalog...") + try: + catalog = get_placeholder_catalog(PROFILE_ID) + print(f" Catalog loaded with {len(catalog)} categories") + except Exception as e: + catalog = None + print(f" Catalog failed: {e}") + + # Step 4: Resolve placeholders + print("\nStep 4: Resolving placeholders...") + debug_info = {} + resolved = resolve_placeholders( + template=template, + variables=variables, + debug_info=debug_info, + catalog=catalog + ) + + print(f" Resolved placeholders: {debug_info.get('resolved_placeholders', {})}") + print(f" Unresolved placeholders: {debug_info.get('unresolved_placeholders', [])}") + + # Result + print("\n" + "=" * 80) + print("RESULT") + print("=" * 80) + print(f"\nResolved template:\n{resolved}\n") + + # Check if placeholders were resolved + if '{{' in resolved: + print("❌ FAILED: Some placeholders were not resolved!") + return False + else: + print("✅ SUCCESS: All placeholders resolved!") + return True + +if __name__ == "__main__": + success = test_placeholder_resolution() + sys.exit(0 if success else 1) From 300d96a9d82114317af95503c1805f9f03e29c4d Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 11 Apr 2026 11:42:54 +0200 Subject: [PATCH 2/5] feat: Enhance prompt execution for workflows and analysis offers - Added support for handling aggregated results in workflow prompts, allowing for various data formats (string, object). - Introduced a utility function to filter active prompts for both pipeline and workflow types in the analysis page. - Updated content handling in the analysis component to accommodate new workflow data structures. This improves the flexibility and usability of the prompt execution process in both backend and frontend components. --- backend/routers/prompts.py | 12 ++++++++++++ frontend/src/pages/Analysis.jsx | 25 ++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/backend/routers/prompts.py b/backend/routers/prompts.py index 732bfea..cc90c5c 100644 --- a/backend/routers/prompts.py +++ b/backend/routers/prompts.py @@ -1253,6 +1253,18 @@ async def execute_unified_prompt( content = list(final_output.values())[0] else: content = json.dumps(final_output, ensure_ascii=False) + elif result['type'] == 'workflow': + # Graph-Workflows: kein "output", sondern aggregated_result + agg = result.get('aggregated_result') + if isinstance(agg, dict) and len(agg) == 1: + v = list(agg.values())[0] + content = v if isinstance(v, str) else json.dumps(v, ensure_ascii=False) + elif agg is None: + content = '' + elif isinstance(agg, str): + content = agg + else: + content = json.dumps(agg, ensure_ascii=False) else: # For base prompts, use output directly content = result.get('output', '') diff --git a/frontend/src/pages/Analysis.jsx b/frontend/src/pages/Analysis.jsx index 25cea80..cea6271 100644 --- a/frontend/src/pages/Analysis.jsx +++ b/frontend/src/pages/Analysis.jsx @@ -45,6 +45,11 @@ function analysisCategoryLabel(key) { return ANALYSIS_CATEGORY_LABELS[k] || String(key) } +/** Analyse-Angebote: klassische Pipelines + graphbasierte KI-Workflows (`type === 'workflow'`) */ +function isAnalysisOfferPrompt(p) { + return p.active && (p.type === 'pipeline' || p.type === 'workflow') +} + /** Pipeline-Prompts nach `category` gruppieren (Backend-Feld), innerhalb Gruppe nach sort_order */ function buildPipelineGroups(pipelinePrompts) { const m = new Map() @@ -360,7 +365,7 @@ export default function Analysis() { },[]) useEffect(() => { - const list = prompts.filter(p => p.active && p.type === 'pipeline') + const list = prompts.filter(isAnalysisOfferPrompt) setActiveCategoryKey(prev => { if (!list.length) return null const groups = buildPipelineGroups(list) @@ -372,7 +377,7 @@ export default function Analysis() { useEffect(() => { if (!newResult?.scope) return - const list = prompts.filter(p => p.active && p.type === 'pipeline') + const list = prompts.filter(isAnalysisOfferPrompt) const groups = buildPipelineGroups(list) const g = groups.find(gg => gg.prompts.some(p => p.slug === newResult.scope)) if (g) setActiveCategoryKey(g.categoryKey) @@ -394,6 +399,16 @@ export default function Analysis() { } else { content = JSON.stringify(finalOutput, null, 2) } + } else if (result.type === 'workflow') { + const agg = result.aggregated_result + if (agg != null && typeof agg === 'object' && !Array.isArray(agg) && Object.keys(agg).length === 1) { + const v = Object.values(agg)[0] + content = typeof v === 'string' ? v : JSON.stringify(v, null, 2) + } else if (typeof agg === 'string') { + content = agg + } else { + content = JSON.stringify(agg ?? {}, null, 2) + } } else { // For base prompts, use output directly content = typeof result.output === 'string' ? result.output : JSON.stringify(result.output, null, 2) @@ -405,7 +420,7 @@ export default function Analysis() { const placeholders = {} const resolved = result.debug.resolved_placeholders - // For pipeline, collect from all stages + // For pipeline, collect from all stages (Workflow: kein gleiches debug-Schema) if (result.type === 'pipeline' && result.debug.stages) { for (const stage of result.debug.stages) { for (const promptDebug of (stage.prompts || [])) { @@ -456,9 +471,9 @@ export default function Analysis() { grouped[key].push(ins) }) - // Show only active pipeline-type prompts (und nach DB-Kategorie gruppiert) + // Aktive Pipeline- + Workflow-Prompts (nach DB-Kategorie gruppiert) const { pipelinePrompts, pipelineGroups } = useMemo(() => { - const list = prompts.filter(p => p.active && p.type === 'pipeline') + const list = prompts.filter(isAnalysisOfferPrompt) return { pipelinePrompts: list, pipelineGroups: buildPipelineGroups(list) } }, [prompts]) From d803f39de3664b09e4c104bfdf77d74b5f679234 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 11 Apr 2026 12:04:35 +0200 Subject: [PATCH 3/5] feat: Refactor workflow result handling in prompts and analysis components - Introduced a new utility function to streamline the extraction of user-facing content from aggregated workflow results. - Updated backend prompt handling to utilize the new function for improved clarity and maintainability. - Adjusted frontend analysis component to leverage the utility for consistent content display across different workflow result formats. These changes enhance the overall user experience by ensuring more reliable and readable output from workflow executions. --- backend/routers/prompts.py | 48 +++++++++++++++++++++------ frontend/src/pages/Analysis.jsx | 11 ++---- frontend/src/utils/workflowDisplay.js | 37 +++++++++++++++++++++ 3 files changed, 76 insertions(+), 20 deletions(-) create mode 100644 frontend/src/utils/workflowDisplay.js diff --git a/backend/routers/prompts.py b/backend/routers/prompts.py index cc90c5c..5e7d76f 100644 --- a/backend/routers/prompts.py +++ b/backend/routers/prompts.py @@ -31,6 +31,42 @@ OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "anthropic/claude-sonnet-4") router = APIRouter(prefix="/api/prompts", tags=["prompts"]) +# Metadaten-Schlüssel in workflow aggregate_results (nicht als „einziger“ Nutzer-Output) +_WORKFLOW_AGG_META_KEYS = frozenset({ + "combined_analysis", + "all_signals", + "total_nodes", + "executed_nodes", + "failed_nodes", + "skipped_nodes", +}) + + +def _workflow_user_facing_content(agg: object) -> str: + """ + Nutzer-sichtbarer Text wie im Admin WorkflowResultViewer („Final Output“): + primär aggregated_result['analysis_core'], nicht das gesamte JSON. + """ + if agg is None: + return "" + if isinstance(agg, str): + return agg + if not isinstance(agg, dict): + return json.dumps(agg, ensure_ascii=False) + core = agg.get("analysis_core") + if isinstance(core, str) and core.strip(): + return core + combined = agg.get("combined_analysis") + if isinstance(combined, str) and combined.strip(): + return combined + non_meta = [k for k in agg.keys() if k not in _WORKFLOW_AGG_META_KEYS] + if len(non_meta) == 1: + v = agg[non_meta[0]] + if isinstance(v, str): + return v + return json.dumps(v, ensure_ascii=False) + return json.dumps(agg, ensure_ascii=False) + @router.get("") def list_prompts(session: dict=Depends(require_auth)): @@ -1254,17 +1290,7 @@ async def execute_unified_prompt( else: content = json.dumps(final_output, ensure_ascii=False) elif result['type'] == 'workflow': - # Graph-Workflows: kein "output", sondern aggregated_result - agg = result.get('aggregated_result') - if isinstance(agg, dict) and len(agg) == 1: - v = list(agg.values())[0] - content = v if isinstance(v, str) else json.dumps(v, ensure_ascii=False) - elif agg is None: - content = '' - elif isinstance(agg, str): - content = agg - else: - content = json.dumps(agg, ensure_ascii=False) + content = _workflow_user_facing_content(result.get('aggregated_result')) else: # For base prompts, use output directly content = result.get('output', '') diff --git a/frontend/src/pages/Analysis.jsx b/frontend/src/pages/Analysis.jsx index cea6271..c8f8317 100644 --- a/frontend/src/pages/Analysis.jsx +++ b/frontend/src/pages/Analysis.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react' import { Brain, Trash2, ChevronDown, ChevronUp } from 'lucide-react' import { Link } from 'react-router-dom' import { api } from '../utils/api' +import { getWorkflowDisplayContent } from '../utils/workflowDisplay' import { useAuth } from '../context/AuthContext' import Markdown from '../utils/Markdown' import UsageBadge from '../components/UsageBadge' @@ -400,15 +401,7 @@ export default function Analysis() { content = JSON.stringify(finalOutput, null, 2) } } else if (result.type === 'workflow') { - const agg = result.aggregated_result - if (agg != null && typeof agg === 'object' && !Array.isArray(agg) && Object.keys(agg).length === 1) { - const v = Object.values(agg)[0] - content = typeof v === 'string' ? v : JSON.stringify(v, null, 2) - } else if (typeof agg === 'string') { - content = agg - } else { - content = JSON.stringify(agg ?? {}, null, 2) - } + content = getWorkflowDisplayContent(result.aggregated_result) } else { // For base prompts, use output directly content = typeof result.output === 'string' ? result.output : JSON.stringify(result.output, null, 2) diff --git a/frontend/src/utils/workflowDisplay.js b/frontend/src/utils/workflowDisplay.js new file mode 100644 index 0000000..ff971ca --- /dev/null +++ b/frontend/src/utils/workflowDisplay.js @@ -0,0 +1,37 @@ +/** + * Nutzer-sichtbare Textausgabe eines Workflow-Laufs – gleiche Logik wie + * WorkflowResultViewer („Final Output“): primär aggregated_result.analysis_core. + */ +const AGG_META_KEYS = new Set([ + 'combined_analysis', + 'all_signals', + 'total_nodes', + 'executed_nodes', + 'failed_nodes', + 'skipped_nodes', +]) + +export function getWorkflowDisplayContent(aggregatedResult) { + if (aggregatedResult == null) return '' + if (typeof aggregatedResult !== 'object' || Array.isArray(aggregatedResult)) { + return typeof aggregatedResult === 'string' + ? aggregatedResult + : JSON.stringify(aggregatedResult, null, 2) + } + + const core = aggregatedResult.analysis_core + if (typeof core === 'string' && core.trim() !== '') return core + + const combined = aggregatedResult.combined_analysis + if (typeof combined === 'string' && combined.trim() !== '') return combined + + const keys = Object.keys(aggregatedResult).filter((k) => !AGG_META_KEYS.has(k)) + if (keys.length === 1) { + const v = aggregatedResult[keys[0]] + if (typeof v === 'string') return v + if (v != null && typeof v === 'object') return JSON.stringify(v, null, 2) + return String(v) + } + + return JSON.stringify(aggregatedResult, null, 2) +} From 0ce98e89736e7c0cb368d0608a1ea46ae66572c2 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 11 Apr 2026 12:19:06 +0200 Subject: [PATCH 4/5] feat: Enhance StartNode and Workflow Editor with analysis metadata - Updated StartNode to display a trimmed analysis title if available, falling back to the label or 'Start'. - Refactored WorkflowEditorPage to include analysis metadata (title, description, category) in the start node configuration. - Improved serialization and deserialization of workflow graphs to handle new analysis fields. - Enhanced user interface to allow setting and displaying analysis metadata for better clarity in the workflow editor. These changes improve the user experience by providing clearer metadata handling in workflows and ensuring consistent display in analysis components. --- .../components/workflow/nodes/StartNode.jsx | 3 +- frontend/src/config/analysisCategories.js | 21 +++ frontend/src/pages/Analysis.jsx | 36 ++-- frontend/src/pages/WorkflowEditorPage.jsx | 162 ++++++++++++++++-- frontend/src/utils/workflowSerializer.js | 12 ++ 5 files changed, 201 insertions(+), 33 deletions(-) create mode 100644 frontend/src/config/analysisCategories.js diff --git a/frontend/src/components/workflow/nodes/StartNode.jsx b/frontend/src/components/workflow/nodes/StartNode.jsx index 327475c..3a4907f 100644 --- a/frontend/src/components/workflow/nodes/StartNode.jsx +++ b/frontend/src/components/workflow/nodes/StartNode.jsx @@ -8,10 +8,11 @@ import { Handle, Position } from 'reactflow' * - selected: Boolean (Node ist ausgewählt) */ export function StartNode({ data, selected }) { + const title = (data.analysis_title || '').trim() return (
🚀
-
{data.label || 'Start'}
+
{title || data.label || 'Start'}
{/* Nur Source Handle (kein Target, da Einstiegspunkt) */} { const na = String(a).toLowerCase() @@ -87,7 +78,8 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) { // Find matching prompt to get display_name const prompt = prompts.find(p => p.slug === ins.scope) - const displayName = prompt?.display_name || SLUG_LABELS[ins.scope] || ins.scope + const displayName = + prompt?.display_name || prompt?.name || SLUG_LABELS[ins.scope] || ins.scope // Use already-parsed metadata const metadata = metadataRaw @@ -546,9 +538,10 @@ export default function Analysis() { {canUseAI && pipelinePrompts.length > 0 && ( <>

- Zuerst die Kategorie wählen (Chip-Leiste bzw. Seitenleiste). Alle Pipeline-Analysen - dieser Kategorie erscheinen im Detailbereich (rechts auf Desktop, darunter auf Mobil). - Kategorien kommen aus dem Feld „Kategorie“ beim jeweiligen Prompt im Admin. + Zuerst die Kategorie wählen (Chip-Leiste bzw. Seitenleiste). Alle{' '} + Pipeline- und Workflow-Auswertungen dieser Kategorie erscheinen im Detailbereich + (rechts auf Desktop, darunter auf Mobil). Bei Workflows legst du Kategorie, Titel und Kurztext in der{' '} + Start-Node des Workflow-Editors fest; bei Pipelines im Admin unter KI-Prompts.

@@ -649,9 +642,9 @@ export default function Analysis() { {canUseAI && pipelinePrompts.length === 0 && (
-

Keine aktiven Pipeline-Prompts verfügbar.

+

Keine aktiven Pipeline- oder Workflow-Auswertungen verfügbar.

- Erstelle Pipeline-Prompts im Admin-Bereich unter Admin → KI-Prompts. + Pipelines und Workflows werden im Admin unter KI-Prompts bzw. Workflow-Editor angelegt.

)} @@ -675,7 +668,10 @@ export default function Analysis() { onClick={() => setHistoryScopePick(scope)} aria-current={activeHistoryScope === scope ? 'page' : undefined} > - {prompts.find(pr => pr.slug === scope)?.display_name || SLUG_LABELS[scope] || scope} + {(() => { + const pr = prompts.find((p) => p.slug === scope) + return pr?.display_name || pr?.name || SLUG_LABELS[scope] || scope + })()} ({grouped[scope].length}) ))} diff --git a/frontend/src/pages/WorkflowEditorPage.jsx b/frontend/src/pages/WorkflowEditorPage.jsx index c85f23b..42a875a 100644 --- a/frontend/src/pages/WorkflowEditorPage.jsx +++ b/frontend/src/pages/WorkflowEditorPage.jsx @@ -22,6 +22,28 @@ import { InlineTemplateEditor } from '../components/workflow/panels/InlineTempla import { Toast } from '../components/Toast' import { ConfirmDialog } from '../components/ConfirmDialog' import '../styles/workflowEditor.css' +import { + ANALYSIS_CATEGORY_ORDER, + ANALYSIS_CATEGORY_LABELS, +} from '../config/analysisCategories' + +/** Aus Start-Node → Felder für Analyse-UI / ai_prompts (display_name, description, category) */ +function getWorkflowAnalysisPublishFields(nodes, fallbackName) { + const start = nodes.find((n) => n.type === 'start') + const d = start?.data || {} + const title = + String(d.analysis_title ?? '').trim() || + String(fallbackName ?? '').trim() || + 'Workflow' + const description = String(d.analysis_description ?? '').trim() + const category = + String(d.analysis_category ?? 'ganzheitlich').toLowerCase().trim() || 'ganzheitlich' + return { + display_name: title, + description: description || null, + category, + } +} // Node-Type Mapping const nodeTypes = { @@ -45,7 +67,6 @@ export default function WorkflowEditorPage() { const selectedNode = selectedNodeId ? nodes.find(n => n.id === selectedNodeId) : null const [currentPrompt, setCurrentPrompt] = useState(null) const [workflowName, setWorkflowName] = useState('Neuer Workflow') - const [workflowDescription, setWorkflowDescription] = useState('') const [validationErrors, setValidationErrors] = useState([]) const [validationWarnings, setValidationWarnings] = useState([]) const [loading, setLoading] = useState(false) @@ -103,16 +124,26 @@ export default function WorkflowEditorPage() { }, []) const handleAddNode = (nodeType) => { - const newNode = { + const base = { id: `node_${nodeIdCounter++}`, type: nodeType, position: { x: 250, y: 100 + nodes.length * 100 }, data: { - label: `${nodeType.charAt(0).toUpperCase() + nodeType.slice(1)} ${nodeIdCounter - 1}` + label: `${nodeType.charAt(0).toUpperCase() + nodeType.slice(1)} ${nodeIdCounter - 1}`, + }, + } + if (nodeType === 'start') { + base.data = { + label: 'Start', + analysis_title: + workflowName && workflowName !== 'Neuer Workflow' + ? workflowName + : '', + analysis_description: '', + analysis_category: 'ganzheitlich', } } - - setNodes((nds) => [...nds, newNode]) + setNodes((nds) => [...nds, base]) } const handleNodeUpdate = (nodeId, updates) => { @@ -152,13 +183,20 @@ export default function WorkflowEditorPage() { }) console.log('📊 Serialized graph_data:', graph_data) + const { display_name, description, category } = getWorkflowAnalysisPublishFields( + nodes, + workflowName + ) + if (currentPrompt) { // Update existing console.log('📝 Updating existing workflow:', currentPrompt.id) await api.updateUnifiedPrompt(currentPrompt.id, { type: 'workflow', name: workflowName, - description: workflowDescription, + display_name, + description: description ?? undefined, + category, graph_data }) setToast({ message: '✅ Workflow gespeichert!', type: 'success' }) @@ -168,7 +206,9 @@ export default function WorkflowEditorPage() { const result = await api.createUnifiedPrompt({ type: 'workflow', name: workflowName, - description: workflowDescription, + display_name, + description: description ?? undefined, + category, graph_data }) console.log('✅ Workflow created:', result) @@ -203,11 +243,30 @@ export default function WorkflowEditorPage() { const { nodes: loadedNodes, edges: loadedEdges } = deserializeFromWorkflowGraph(prompt.graph_data) console.log('🎯 Deserialized:', { nodes: loadedNodes, edges: loadedEdges }) - setNodes(loadedNodes) + const mergedNodes = loadedNodes.map((n) => { + if (n.type !== 'start') return n + const hasStoredTitle = Object.prototype.hasOwnProperty.call(n.data || {}, 'analysis_title') + return { + ...n, + data: { + label: n.data?.label || 'Start', + ...n.data, + analysis_title: hasStoredTitle + ? n.data.analysis_title + : (prompt.display_name || prompt.name || ''), + analysis_description: Object.prototype.hasOwnProperty.call(n.data || {}, 'analysis_description') + ? n.data.analysis_description + : (prompt.description || ''), + analysis_category: + (n.data?.analysis_category || prompt.category || 'ganzheitlich').toLowerCase(), + }, + } + }) + + setNodes(mergedNodes) setEdges(loadedEdges) setCurrentPrompt(prompt) setWorkflowName(prompt.name) - setWorkflowDescription(prompt.description || '') // nodeIdCounter aktualisieren const maxId = Math.max( @@ -242,7 +301,6 @@ export default function WorkflowEditorPage() { setEdges([]) setCurrentPrompt(null) setWorkflowName('Neuer Workflow') - setWorkflowDescription('') setSelectedNodeId(null) navigate('/workflow-editor/new') } @@ -342,7 +400,8 @@ export default function WorkflowEditorPage() { type="text" value={workflowName} onChange={(e) => setWorkflowName(e.target.value)} - placeholder="Workflow-Name" + placeholder="Interner Workflow-Name (Slug-Basis)" + title="Technischer Name in der Datenbank. Den sichtbaren Titel für die KI-Analyse setzt du in der Start-Node." style={{ flex: 1, padding: '8px', borderRadius: '4px', border: '1px solid var(--border)' }} /> @@ -495,9 +554,88 @@ export default function WorkflowEditorPage() {
)} + {/* Start-Node: Metadaten für KI-Analyse (wie Pipeline: display_name, description, category) */} + {selectedNode.type === 'start' && ( +
+

Anzeige in KI-Analyse

+

+ Titel, Kurzbeschreibung und Kategorie erscheinen auf der Analyse-Seite, im Verlauf und in der + Kategorie-Navigation – analog zu Pipeline-Prompts im Admin. +

+ + + handleNodeUpdate(selectedNode.id, { analysis_title: e.target.value }) + } + placeholder="z. B. Ganzheitliche Auswertung" + style={{ + width: '100%', + padding: '8px', + borderRadius: '4px', + border: '1px solid var(--border)', + background: 'var(--surface)', + color: 'var(--text1)', + fontSize: '14px', + marginBottom: 12, + }} + /> + +