fix: Part 3 - PlaceholderPicker enhancements (4 critical fixes)
**User Feedback Issues Fixed:** 1. **Registrierte Platzhalter fehlten** ❌ → ✅ - PlaceholderPicker lädt jetzt ~120+ Platzhalter vom Backend - Endpoint: GET /api/prompts/placeholders (get_placeholder_catalog) - Kategorisiert nach: Profil, Körper, Ernährung, Training, etc. - Icons pro Kategorie (👤 💪 🍎 🏃 😴 ❤️ 🎯) 2. **Node-Namen nicht sichtbar** ❌ → ✅ - Vorher: "{{ node_2.analysis_core }}" - Jetzt: "Körper-Analyse (node_2) - Hauptausgabe" - Node-Label wird in Beschreibung angezeigt 3. **Cursor-Position ignoriert** ❌ → ✅ - Platzhalter werden jetzt an Cursor-Position eingefügt - textareaRef von WorkflowEditorPage an EndNodeConfig übergeben - textarea.selectionStart/End für exakte Position - Cursor wird nach Einfügen an korrekte Stelle gesetzt - Focus zurück auf Textarea 4. **Fragen-Kontext unklar** ⚠️ → ✅ - Signal-Platzhalter zeigen jetzt Frage-Text - Format: "Körper-Analyse - Signal: Ist Gewichtstrend positiv?" - Frage wird auf 50 Zeichen gekürzt wenn zu lang **Komponenten-Änderungen:** PlaceholderPicker.jsx: - useEffect zum Laden von Backend-Platzhaltern - Gruppierung nach Kategorien (System + Workflow) - System-Platzhalter: ~120+ aus placeholder_registrations - Workflow-Platzhalter: Node Outputs + Signals - Bessere Beschreibungen mit Node-Label - Stats-Anzeige: "X Platzhalter gefunden (Y Workflow, Z System)" - Loading State während Backend-Call EndNodeConfig.jsx: - useRef für Textarea - textareaRef Prop (optional, Fallback zu lokalem ref) - ref an Textarea gebunden WorkflowEditorPage.jsx: - useRef Hook importiert - endNodeTextareaRef erstellt - handlePlaceholderSelect umgebaut: - Liest selectionStart vom Textarea - Fügt an Cursor-Position ein (before + placeholder + after) - Setzt Cursor nach Platzhalter - Fokussiert Textarea wieder - Fallback: Am Ende einfügen wenn ref nicht verfügbar - textareaRef an EndNodeConfig übergeben **UX-Verbesserungen:** - Suchfunktion durchsucht jetzt auch Kategorie-Namen - Sticky Category Headers beim Scrollen - Example-Werte in Beschreibung (wenn vorhanden) - AutoFocus auf Suchfeld beim Öffnen - Gruppiert: Workflow-Outputs immer oben, dann System-Platzhalter **Testing Required:** - [ ] End Node öffnen → Template Mode → Platzhalter-Button klicken - [ ] Prüfen: ~120+ Platzhalter sichtbar (nicht nur 2-3 Workflow-Outputs) - [ ] Prüfen: Node-Namen in Beschreibungen ("Körper-Analyse (node_2)") - [ ] Cursor an beliebige Stelle setzen → Platzhalter einfügen - [ ] Prüfen: Einfügen an Cursor-Position (nicht am Ende) Part 3 Bugfixes - User Feedback Complete Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
228010a6d3
commit
b779c2f2a8
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useRef } from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EndNodeConfig - Konfiguration für End Nodes
|
* EndNodeConfig - Konfiguration für End Nodes
|
||||||
*
|
*
|
||||||
|
|
@ -5,12 +7,17 @@
|
||||||
* - node: React Flow Node object (type='end')
|
* - node: React Flow Node object (type='end')
|
||||||
* - onChange: (nodeId, updates) => void
|
* - onChange: (nodeId, updates) => void
|
||||||
* - onOpenPlaceholderPicker: () => void (optional, für späteren Placeholder Picker)
|
* - onOpenPlaceholderPicker: () => void (optional, für späteren Placeholder Picker)
|
||||||
|
* - textareaRef: React ref für Textarea (für Cursor-Position beim Einfügen)
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Output Mode: AUTO (concatenate all analyses) vs. TEMPLATE (custom Jinja2 template)
|
* - Output Mode: AUTO (concatenate all analyses) vs. TEMPLATE (custom Jinja2 template)
|
||||||
* - Template Editor (Textarea für Jinja2 syntax)
|
* - Template Editor (Textarea für Jinja2 syntax)
|
||||||
|
* - Cursor-Positionserhaltung beim Platzhalter-Einfügen
|
||||||
*/
|
*/
|
||||||
export function EndNodeConfig({ node, onChange, onOpenPlaceholderPicker }) {
|
export function EndNodeConfig({ node, onChange, onOpenPlaceholderPicker, textareaRef }) {
|
||||||
|
const localTextareaRef = useRef(null)
|
||||||
|
const activeRef = textareaRef || localTextareaRef
|
||||||
|
|
||||||
const outputMode = node.data.output_mode || 'auto'
|
const outputMode = node.data.output_mode || 'auto'
|
||||||
const template = node.data.template || ''
|
const template = node.data.template || ''
|
||||||
|
|
||||||
|
|
@ -99,6 +106,7 @@ export function EndNodeConfig({ node, onChange, onOpenPlaceholderPicker }) {
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
ref={activeRef}
|
||||||
value={template}
|
value={template}
|
||||||
onChange={handleTemplateChange}
|
onChange={handleTemplateChange}
|
||||||
placeholder="# Finale Analyse {{ node_1.analysis_core }} {{ node_2.analysis_core }}"
|
placeholder="# Finale Analyse {{ node_1.analysis_core }} {{ node_2.analysis_core }}"
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,81 @@
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { api } from '../../../utils/api'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PlaceholderPicker - Modal zur Auswahl von Template-Platzhaltern
|
* PlaceholderPicker - Modal zur Auswahl von Template-Platzhaltern
|
||||||
*
|
*
|
||||||
* Props:
|
* Props:
|
||||||
* - nodes: Array of workflow nodes (to extract available placeholders)
|
* - nodes: Array of workflow nodes (to extract workflow-specific placeholders)
|
||||||
* - onSelect: (placeholderString) => void - Callback when placeholder is selected
|
* - onSelect: (placeholderString) => void - Callback when placeholder is selected
|
||||||
* - onClose: () => void
|
* - onClose: () => void
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Lists all available node outputs
|
* - Lädt registrierte Platzhalter vom Backend (~120+)
|
||||||
* - Copy to clipboard or insert into template
|
* - Extrahiert Workflow-spezifische Node-Outputs
|
||||||
* - Kategorisiert nach Node-Typ
|
* - Zeigt Node-Namen (nicht nur IDs)
|
||||||
|
* - Kategorisiert: System + Workflow
|
||||||
|
* - Suchfunktion über alle Kategorien
|
||||||
*/
|
*/
|
||||||
export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [systemPlaceholders, setSystemPlaceholders] = useState([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
// Extrahiere verfügbare Platzhalter aus Nodes
|
// Lade Backend-Platzhalter beim Mount
|
||||||
const placeholders = extractPlaceholders(nodes)
|
useEffect(() => {
|
||||||
|
async function loadPlaceholders() {
|
||||||
|
try {
|
||||||
|
const catalog = await api.listPlaceholders()
|
||||||
|
// Konvertiere Katalog zu Flat-Liste
|
||||||
|
const flattened = []
|
||||||
|
Object.entries(catalog).forEach(([category, items]) => {
|
||||||
|
items.forEach(item => {
|
||||||
|
flattened.push({
|
||||||
|
placeholder: `{{ ${item.key.trim()} }}`,
|
||||||
|
description: item.description || 'Keine Beschreibung',
|
||||||
|
example: item.example || '',
|
||||||
|
category: category,
|
||||||
|
icon: getCategoryIcon(category)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
setSystemPlaceholders(flattened)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load placeholders:', e)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadPlaceholders()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Extrahiere Workflow-spezifische Platzhalter
|
||||||
|
const workflowPlaceholders = extractWorkflowPlaceholders(nodes)
|
||||||
|
|
||||||
|
// Kombiniere beide Listen
|
||||||
|
const allPlaceholders = [
|
||||||
|
...workflowPlaceholders,
|
||||||
|
...systemPlaceholders
|
||||||
|
]
|
||||||
|
|
||||||
// Filtere basierend auf Suchquery
|
// Filtere basierend auf Suchquery
|
||||||
const filteredPlaceholders = placeholders.filter(p =>
|
const filteredPlaceholders = allPlaceholders.filter(p =>
|
||||||
p.placeholder.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
p.placeholder.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
p.description.toLowerCase().includes(searchQuery.toLowerCase())
|
p.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
(p.category && p.category.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Gruppiere nach Kategorie
|
||||||
|
const grouped = {}
|
||||||
|
filteredPlaceholders.forEach(p => {
|
||||||
|
const cat = p.category || 'Sonstige'
|
||||||
|
if (!grouped[cat]) grouped[cat] = []
|
||||||
|
grouped[cat].push(p)
|
||||||
|
})
|
||||||
|
|
||||||
const handleSelect = (placeholderString) => {
|
const handleSelect = (placeholderString) => {
|
||||||
onSelect(placeholderString)
|
onSelect(placeholderString)
|
||||||
onClose() // Schließe Modal nach Auswahl
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -51,7 +99,7 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
background: 'var(--surface)',
|
background: 'var(--surface)',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
padding: '24px',
|
padding: '24px',
|
||||||
maxWidth: '600px',
|
maxWidth: '700px',
|
||||||
width: '90%',
|
width: '90%',
|
||||||
maxHeight: '80vh',
|
maxHeight: '80vh',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
@ -69,7 +117,10 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
marginBottom: '16px'
|
marginBottom: '16px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2 style={{ margin: 0, fontSize: '18px' }}>Platzhalter auswählen</h2>
|
<h2 style={{ margin: 0, fontSize: '18px' }}>
|
||||||
|
Platzhalter auswählen
|
||||||
|
{loading && <span style={{ fontSize: '14px', color: 'var(--text3)', marginLeft: '12px' }}>Lädt...</span>}
|
||||||
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -89,9 +140,10 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Suche nach Node oder Variable..."
|
placeholder="Suche nach Platzhalter, Kategorie oder Beschreibung..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
autoFocus
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
|
|
@ -104,7 +156,19 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Placeholder List */}
|
{/* Stats */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'var(--text3)',
|
||||||
|
marginBottom: '12px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filteredPlaceholders.length} Platzhalter gefunden
|
||||||
|
{!searchQuery && ` (${workflowPlaceholders.length} Workflow, ${systemPlaceholders.length} System)`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Placeholder List (Grouped) */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
@ -113,7 +177,7 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
border: '1px solid var(--border)'
|
border: '1px solid var(--border)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{filteredPlaceholders.length === 0 ? (
|
{loading ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: '24px',
|
padding: '24px',
|
||||||
|
|
@ -122,17 +186,48 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
fontSize: '14px'
|
fontSize: '14px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{searchQuery ? 'Keine Platzhalter gefunden' : 'Keine Nodes im Workflow'}
|
Lade Platzhalter...
|
||||||
|
</div>
|
||||||
|
) : Object.keys(grouped).length === 0 ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '24px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'var(--text3)',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{searchQuery ? 'Keine Platzhalter gefunden' : 'Keine Platzhalter verfügbar'}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
<div>
|
||||||
{filteredPlaceholders.map((p, idx) => (
|
{Object.entries(grouped).map(([category, items], catIdx) => (
|
||||||
|
<div key={catIdx}>
|
||||||
|
{/* Category Header */}
|
||||||
<div
|
<div
|
||||||
key={idx}
|
style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--text2)',
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{category} ({items.length})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Items */}
|
||||||
|
{items.map((p, idx) => (
|
||||||
|
<div
|
||||||
|
key={`${catIdx}-${idx}`}
|
||||||
onClick={() => handleSelect(p.placeholder)}
|
onClick={() => handleSelect(p.placeholder)}
|
||||||
style={{
|
style={{
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
borderBottom: idx < filteredPlaceholders.length - 1 ? '1px solid var(--border)' : 'none',
|
borderBottom: '1px solid var(--border)',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'background 0.2s'
|
transition: 'background 0.2s'
|
||||||
}}
|
}}
|
||||||
|
|
@ -143,16 +238,18 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center'
|
alignItems: 'flex-start',
|
||||||
|
gap: '12px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
color: 'var(--accent)',
|
color: 'var(--accent)',
|
||||||
marginBottom: '4px'
|
marginBottom: '4px',
|
||||||
|
wordBreak: 'break-all'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{p.placeholder}
|
{p.placeholder}
|
||||||
|
|
@ -160,16 +257,28 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: 'var(--text3)'
|
color: 'var(--text2)',
|
||||||
|
marginBottom: p.example ? '4px' : '0'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{p.description}
|
{p.description}
|
||||||
</div>
|
</div>
|
||||||
|
{p.example && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '11px',
|
||||||
|
color: 'var(--text3)',
|
||||||
|
fontStyle: 'italic'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Beispiel: {p.example}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: '20px',
|
fontSize: '20px',
|
||||||
marginLeft: '12px',
|
flexShrink: 0,
|
||||||
color: 'var(--text3)'
|
color: 'var(--text3)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -179,6 +288,8 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -193,9 +304,9 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
color: 'var(--text3)'
|
color: 'var(--text3)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
💡 <strong>Syntax:</strong> <code style={{ background: 'var(--surface2)', padding: '2px 6px', borderRadius: '4px' }}>{'{{ node_id.field }}'}</code>
|
💡 <strong>Syntax:</strong> <code style={{ background: 'var(--surface2)', padding: '2px 6px', borderRadius: '4px' }}>{'{{ placeholder_name }}'}</code>
|
||||||
<br />
|
<br />
|
||||||
Verfügbare Felder: <code>analysis_core</code>, <code>signal_*</code> (wenn Fragen vorhanden)
|
Klicke auf einen Platzhalter um ihn einzufügen
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -203,45 +314,37 @@ export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extrahiert verfügbare Platzhalter aus Workflow-Nodes
|
* Extrahiert Workflow-spezifische Platzhalter aus Nodes
|
||||||
*/
|
*/
|
||||||
function extractPlaceholders(nodes) {
|
function extractWorkflowPlaceholders(nodes) {
|
||||||
const placeholders = []
|
const placeholders = []
|
||||||
|
|
||||||
// Globale Platzhalter
|
|
||||||
placeholders.push({
|
|
||||||
placeholder: '{{ analysis_core }}',
|
|
||||||
description: 'Finale Analyse (AUTO-Modus Fallback)',
|
|
||||||
icon: '🏁',
|
|
||||||
category: 'global'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Node-spezifische Platzhalter
|
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
if (node.type === 'end') return // End Node hat keine Outputs
|
if (node.type === 'end') return // End Node hat keine Outputs
|
||||||
|
|
||||||
const nodeId = node.id
|
const nodeId = node.id
|
||||||
const label = node.data.label || nodeId
|
const nodeLabel = node.data.label || nodeId
|
||||||
|
|
||||||
// analysis_core für alle Analysis/Logic/Join Nodes
|
// analysis_core für alle Analysis/Logic/Join Nodes
|
||||||
if (node.type === 'analysis' || node.type === 'logic' || node.type === 'join') {
|
if (node.type === 'analysis' || node.type === 'logic' || node.type === 'join') {
|
||||||
placeholders.push({
|
placeholders.push({
|
||||||
placeholder: `{{ ${nodeId}.analysis_core }}`,
|
placeholder: `{{ ${nodeId}.analysis_core }}`,
|
||||||
description: `${getNodeIcon(node.type)} ${label} - Hauptausgabe`,
|
description: `${nodeLabel} (${nodeId}) - Hauptausgabe`,
|
||||||
icon: getNodeIcon(node.type),
|
icon: getNodeIcon(node.type),
|
||||||
category: 'node_outputs'
|
category: 'Workflow - Node Outputs'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signals für Analysis Nodes mit Fragen
|
// Signals für Analysis Nodes mit Fragen
|
||||||
if (node.type === 'analysis' && node.data.questions && node.data.questions.length > 0) {
|
if (node.type === 'analysis' && node.data.questions && node.data.questions.length > 0) {
|
||||||
node.data.questions.forEach(q => {
|
node.data.questions.forEach((q, qIdx) => {
|
||||||
const questionId = q.id || `q${placeholders.length}`
|
const questionId = q.id || `q${qIdx + 1}`
|
||||||
|
const questionText = q.question || `Frage ${qIdx + 1}`
|
||||||
placeholders.push({
|
placeholders.push({
|
||||||
placeholder: `{{ ${nodeId}.signal_${questionId} }}`,
|
placeholder: `{{ ${nodeId}.signal_${questionId} }}`,
|
||||||
description: `${label} - Signal: ${q.question?.substring(0, 40)}...`,
|
description: `${nodeLabel} - Signal: ${questionText.substring(0, 50)}${questionText.length > 50 ? '...' : ''}`,
|
||||||
icon: '📊',
|
icon: '📊',
|
||||||
category: 'signals'
|
category: 'Workflow - Signals'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -263,3 +366,21 @@ function getNodeIcon(type) {
|
||||||
}
|
}
|
||||||
return icons[type] || '📦'
|
return icons[type] || '📦'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kategorie zu Icon
|
||||||
|
*/
|
||||||
|
function getCategoryIcon(category) {
|
||||||
|
const icons = {
|
||||||
|
'Profil': '👤',
|
||||||
|
'Körper': '💪',
|
||||||
|
'Ernährung': '🍎',
|
||||||
|
'Training': '🏃',
|
||||||
|
'Schlaf': '😴',
|
||||||
|
'Vitalwerte': '❤️',
|
||||||
|
'Ziele': '🎯',
|
||||||
|
'Scores': '📊',
|
||||||
|
'Korrelationen': '📈'
|
||||||
|
}
|
||||||
|
return icons[category] || '📦'
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useCallback, useEffect } from 'react'
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { useNodesState, useEdgesState, addEdge } from 'reactflow'
|
import { useNodesState, useEdgesState, addEdge } from 'reactflow'
|
||||||
import { api } from '../utils/api'
|
import { api } from '../utils/api'
|
||||||
|
|
@ -50,6 +50,7 @@ export default function WorkflowEditorPage() {
|
||||||
const [availablePrompts, setAvailablePrompts] = useState([])
|
const [availablePrompts, setAvailablePrompts] = useState([])
|
||||||
const [executionResult, setExecutionResult] = useState(null)
|
const [executionResult, setExecutionResult] = useState(null)
|
||||||
const [showPlaceholderPicker, setShowPlaceholderPicker] = useState(false)
|
const [showPlaceholderPicker, setShowPlaceholderPicker] = useState(false)
|
||||||
|
const endNodeTextareaRef = useRef(null)
|
||||||
|
|
||||||
// Load available basis prompts for Analysis nodes
|
// Load available basis prompts for Analysis nodes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -263,10 +264,29 @@ export default function WorkflowEditorPage() {
|
||||||
const handlePlaceholderSelect = (placeholderString) => {
|
const handlePlaceholderSelect = (placeholderString) => {
|
||||||
if (!selectedNode || selectedNode.type !== 'end') return
|
if (!selectedNode || selectedNode.type !== 'end') return
|
||||||
|
|
||||||
|
const textarea = endNodeTextareaRef.current
|
||||||
const currentTemplate = selectedNode.data.template || ''
|
const currentTemplate = selectedNode.data.template || ''
|
||||||
const newTemplate = currentTemplate + placeholderString
|
|
||||||
|
// Wenn Textarea Ref verfügbar, an Cursor-Position einfügen
|
||||||
|
if (textarea) {
|
||||||
|
const cursorPos = textarea.selectionStart || currentTemplate.length
|
||||||
|
const before = currentTemplate.substring(0, cursorPos)
|
||||||
|
const after = currentTemplate.substring(cursorPos)
|
||||||
|
const newTemplate = before + placeholderString + after
|
||||||
|
|
||||||
handleNodeUpdate(selectedNode.id, { template: newTemplate })
|
handleNodeUpdate(selectedNode.id, { template: newTemplate })
|
||||||
|
|
||||||
|
// Cursor nach eingefügtem Platzhalter positionieren
|
||||||
|
setTimeout(() => {
|
||||||
|
const newPos = cursorPos + placeholderString.length
|
||||||
|
textarea.setSelectionRange(newPos, newPos)
|
||||||
|
textarea.focus()
|
||||||
|
}, 0)
|
||||||
|
} else {
|
||||||
|
// Fallback: Am Ende einfügen
|
||||||
|
const newTemplate = currentTemplate + placeholderString
|
||||||
|
handleNodeUpdate(selectedNode.id, { template: newTemplate })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Render ────────────────────────────────────────────────────────────────
|
// ── Render ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -498,6 +518,7 @@ export default function WorkflowEditorPage() {
|
||||||
node={selectedNode}
|
node={selectedNode}
|
||||||
onChange={handleNodeUpdate}
|
onChange={handleNodeUpdate}
|
||||||
onOpenPlaceholderPicker={() => setShowPlaceholderPicker(true)}
|
onOpenPlaceholderPicker={() => setShowPlaceholderPicker(true)}
|
||||||
|
textareaRef={endNodeTextareaRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user