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.
This commit is contained in:
parent
d803f39de3
commit
0ce98e8973
|
|
@ -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 (
|
||||
<div className={`workflow-node start-node ${selected ? 'selected' : ''}`}>
|
||||
<div className="node-icon">🚀</div>
|
||||
<div className="node-label">{data.label || 'Start'}</div>
|
||||
<div className="node-label">{title || data.label || 'Start'}</div>
|
||||
|
||||
{/* Nur Source Handle (kein Target, da Einstiegspunkt) */}
|
||||
<Handle
|
||||
|
|
|
|||
21
frontend/src/config/analysisCategories.js
Normal file
21
frontend/src/config/analysisCategories.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/** DB `ai_prompts.category` – Reihenfolge der Gruppen in der KI-Analyse-Navigation */
|
||||
|
||||
export const ANALYSIS_CATEGORY_ORDER = [
|
||||
'körper',
|
||||
'ernährung',
|
||||
'training',
|
||||
'schlaf',
|
||||
'vitalwerte',
|
||||
'ziele',
|
||||
'ganzheitlich',
|
||||
]
|
||||
|
||||
export const ANALYSIS_CATEGORY_LABELS = {
|
||||
körper: 'Körper',
|
||||
ernährung: 'Ernährung',
|
||||
training: 'Training',
|
||||
schlaf: 'Schlaf',
|
||||
vitalwerte: 'Vitalwerte',
|
||||
ziele: 'Ziele',
|
||||
ganzheitlich: 'Ganzheitlich',
|
||||
}
|
||||
|
|
@ -3,6 +3,10 @@ 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 {
|
||||
ANALYSIS_CATEGORY_ORDER,
|
||||
ANALYSIS_CATEGORY_LABELS,
|
||||
} from '../config/analysisCategories'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import Markdown from '../utils/Markdown'
|
||||
import UsageBadge from '../components/UsageBadge'
|
||||
|
|
@ -15,19 +19,6 @@ const SLUG_LABELS = {
|
|||
pipeline: '🔬 Mehrstufige Gesamtanalyse'
|
||||
}
|
||||
|
||||
/** DB `ai_prompts.category` – Reihenfolge der Gruppen in der Analyse-Navigation */
|
||||
const ANALYSIS_CATEGORY_ORDER = ['körper', 'ernährung', 'training', 'schlaf', 'vitalwerte', 'ziele', 'ganzheitlich']
|
||||
|
||||
const ANALYSIS_CATEGORY_LABELS = {
|
||||
körper: 'Körper',
|
||||
ernährung: 'Ernährung',
|
||||
training: 'Training',
|
||||
schlaf: 'Schlaf',
|
||||
vitalwerte: 'Vitalwerte',
|
||||
ziele: 'Ziele',
|
||||
ganzheitlich: 'Ganzheitlich',
|
||||
}
|
||||
|
||||
function sortAnalysisCategoryKeys(keys) {
|
||||
return [...keys].sort((a, b) => {
|
||||
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 && (
|
||||
<>
|
||||
<p style={{fontSize:13,color:'var(--text2)',marginBottom:14,lineHeight:1.6}}>
|
||||
Zuerst die <strong>Kategorie</strong> 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 <strong>Kategorie</strong> wählen (Chip-Leiste bzw. Seitenleiste). Alle{' '}
|
||||
<strong>Pipeline- und Workflow-Auswertungen</strong> dieser Kategorie erscheinen im Detailbereich
|
||||
(rechts auf Desktop, darunter auf Mobil). Bei Workflows legst du Kategorie, Titel und Kurztext in der{' '}
|
||||
<strong>Start-Node</strong> des Workflow-Editors fest; bei Pipelines im Admin unter KI-Prompts.
|
||||
</p>
|
||||
<div className="analysis-split">
|
||||
<div className="analysis-split__nav-wrap">
|
||||
|
|
@ -649,9 +642,9 @@ export default function Analysis() {
|
|||
|
||||
{canUseAI && pipelinePrompts.length === 0 && (
|
||||
<div className="empty-state">
|
||||
<p>Keine aktiven Pipeline-Prompts verfügbar.</p>
|
||||
<p>Keine aktiven Pipeline- oder Workflow-Auswertungen verfügbar.</p>
|
||||
<p style={{fontSize:12,color:'var(--text3)',marginTop:8}}>
|
||||
Erstelle Pipeline-Prompts im Admin-Bereich unter Admin → KI-Prompts.
|
||||
Pipelines und Workflows werden im Admin unter KI-Prompts bzw. Workflow-Editor angelegt.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -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
|
||||
})()}
|
||||
<span className="muted" style={{ fontSize: 12 }}> ({grouped[scope].length})</span>
|
||||
</button>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Start-Node: Metadaten für KI-Analyse (wie Pipeline: display_name, description, category) */}
|
||||
{selectedNode.type === 'start' && (
|
||||
<div className="config-section" style={{ marginBottom: 20 }}>
|
||||
<h3 style={{ margin: '0 0 10px', fontSize: 15 }}>Anzeige in KI-Analyse</h3>
|
||||
<p style={{ fontSize: 12, color: 'var(--text3)', lineHeight: 1.5, marginBottom: 12 }}>
|
||||
Titel, Kurzbeschreibung und Kategorie erscheinen auf der Analyse-Seite, im Verlauf und in der
|
||||
Kategorie-Navigation – analog zu Pipeline-Prompts im Admin.
|
||||
</p>
|
||||
<label style={{ display: 'block', marginBottom: 6, fontWeight: 600, fontSize: 13 }}>
|
||||
Titel (sichtbar für Nutzer)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedNode.data.analysis_title ?? ''}
|
||||
onChange={(e) =>
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
<label style={{ display: 'block', marginBottom: 6, fontWeight: 600, fontSize: 13 }}>
|
||||
Kurzbeschreibung (optional)
|
||||
</label>
|
||||
<textarea
|
||||
value={selectedNode.data.analysis_description ?? ''}
|
||||
onChange={(e) =>
|
||||
handleNodeUpdate(selectedNode.id, { analysis_description: e.target.value })
|
||||
}
|
||||
placeholder="Ein Satz, worum es bei dieser Auswertung geht …"
|
||||
rows={4}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface)',
|
||||
color: 'var(--text1)',
|
||||
fontSize: '13px',
|
||||
lineHeight: 1.5,
|
||||
marginBottom: 12,
|
||||
resize: 'vertical',
|
||||
}}
|
||||
/>
|
||||
<label style={{ display: 'block', marginBottom: 6, fontWeight: 600, fontSize: 13 }}>
|
||||
Kategorie
|
||||
</label>
|
||||
<select
|
||||
value={selectedNode.data.analysis_category || 'ganzheitlich'}
|
||||
onChange={(e) =>
|
||||
handleNodeUpdate(selectedNode.id, { analysis_category: e.target.value })
|
||||
}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface)',
|
||||
color: 'var(--text1)',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
{ANALYSIS_CATEGORY_ORDER.map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{ANALYSIS_CATEGORY_LABELS[key] || key}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Basis-Konfiguration */}
|
||||
<div className="config-section">
|
||||
<label>Node-Name</label>
|
||||
<label>Node-Name {selectedNode.type === 'start' && '(auf dem Canvas)'}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedNode.data.label || ''}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@ export function serializeToWorkflowGraph(nodes, edges, metadata = {}) {
|
|||
...(node.type === 'end' && {
|
||||
output_mode: node.data.output_mode || 'auto',
|
||||
template: node.data.template || null
|
||||
}),
|
||||
|
||||
...(node.type === 'start' && {
|
||||
analysis_title: node.data.analysis_title || '',
|
||||
analysis_description: node.data.analysis_description || '',
|
||||
analysis_category: node.data.analysis_category || 'ganzheitlich',
|
||||
})
|
||||
}))
|
||||
|
||||
|
|
@ -105,6 +111,12 @@ export function deserializeFromWorkflowGraph(jsonbData) {
|
|||
...(node.type === 'end' && {
|
||||
output_mode: node.output_mode || 'auto',
|
||||
template: node.template || null
|
||||
}),
|
||||
|
||||
...(node.type === 'start' && {
|
||||
analysis_title: node.analysis_title || '',
|
||||
analysis_description: node.analysis_description || '',
|
||||
analysis_category: node.analysis_category || 'ganzheitlich',
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user