From 0ce98e89736e7c0cb368d0608a1ea46ae66572c2 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 11 Apr 2026 12:19:06 +0200 Subject: [PATCH] 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, + }} + /> + +