Workflow Analysen V1.1 #73
|
|
@ -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