Workflow Analysen V1.1 #73

Merged
Lars merged 5 commits from develop into main 2026-04-11 12:38:09 +02:00
5 changed files with 201 additions and 33 deletions
Showing only changes of commit 0ce98e8973 - Show all commits

View File

@ -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

View 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',
}

View File

@ -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>
))}

View File

@ -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 || ''}

View File

@ -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',
})
}
}))