import { useState, useEffect } from 'react' import { api } from '../../../utils/api' /** * PlaceholderPicker - Modal zur Auswahl von Template-Platzhaltern * * Props: * - nodes: Array of workflow nodes (to extract workflow-specific placeholders) * - currentNodeId: ID des aktuellen Nodes (wird aus Placeholders ausgeschlossen) * - onSelect: (placeholderString) => void - Callback when placeholder is selected * - onClose: () => void * * Features: * - Lädt registrierte Platzhalter vom Backend (~120+) * - Extrahiert Workflow-spezifische Node-Outputs * - Filtert Selbst-Referenzierung (Node kann sich nicht selbst referenzieren) * - Zeigt Node-Namen (nicht nur IDs) * - Kategorisiert: System + Workflow * - Suchfunktion über alle Kategorien */ export function PlaceholderPicker({ nodes, currentNodeId, onSelect, onClose }) { const [searchQuery, setSearchQuery] = useState('') const [systemPlaceholders, setSystemPlaceholders] = useState([]) const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState(null) // Lade Backend-Platzhalter beim Mount useEffect(() => { async function loadPlaceholders() { try { console.log('🔄 Loading placeholders from backend...') const catalog = await api.listPlaceholders() console.log('✅ Catalog received:', catalog) console.log('📊 Catalog keys:', Object.keys(catalog)) // Konvertiere Katalog zu Flat-Liste const flattened = [] Object.entries(catalog).forEach(([category, items]) => { console.log(`📁 Category "${category}": ${items?.length || 0} items`) if (!Array.isArray(items)) { console.warn(`⚠️ Category "${category}" items is not an array:`, items) return } items.forEach(item => { flattened.push({ placeholder: `{{ ${item.key.trim()} }}`, description: item.description || 'Keine Beschreibung', example: item.example || '', category: category, icon: getCategoryIcon(category) }) }) }) console.log(`✅ Loaded ${flattened.length} system placeholders`) setSystemPlaceholders(flattened) } catch (e) { console.error('❌ Failed to load placeholders:', e) setLoadError(e.message) } finally { setLoading(false) } } loadPlaceholders() }, []) // Extrahiere Workflow-spezifische Platzhalter (ohne aktuellen Node) const workflowPlaceholders = extractWorkflowPlaceholders(nodes, currentNodeId) // Kombiniere beide Listen const allPlaceholders = [ ...workflowPlaceholders, ...systemPlaceholders ] // Filtere basierend auf Suchquery const filteredPlaceholders = allPlaceholders.filter(p => p.placeholder.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) => { onSelect(placeholderString) onClose() } return (
e.stopPropagation()} > {/* Header */}

Platzhalter auswählen {loading && Lädt...}

{/* Search */} setSearchQuery(e.target.value)} autoFocus style={{ width: '100%', padding: '8px 12px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg)', color: 'var(--text1)', fontSize: '14px', marginBottom: '16px' }} /> {/* Stats */}
{filteredPlaceholders.length} Platzhalter gefunden {!searchQuery && ` (${workflowPlaceholders.length} Workflow, ${systemPlaceholders.length} System)`}
{/* Placeholder List (Grouped) */}
{loading ? (
Lade Platzhalter...
) : loadError ? (
❌ Fehler beim Laden: {loadError}
Workflow-Platzhalter sind trotzdem verfügbar.
) : Object.keys(grouped).length === 0 ? (
{searchQuery ? 'Keine Platzhalter gefunden' : 'Keine Platzhalter verfügbar'}
) : (
{Object.entries(grouped).map(([category, items], catIdx) => (
{/* Category Header */}
{category} ({items.length})
{/* Category Items */} {items.map((p, idx) => (
handleSelect(p.placeholder)} style={{ padding: '12px', borderBottom: '1px solid var(--border)', cursor: 'pointer', transition: 'background 0.2s' }} onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface2)'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'} >
{p.placeholder}
{p.description}
{p.example && (
Beispiel: {p.example}
)}
{p.icon}
))}
))}
)}
{/* Footer */}
💡 Syntax: {'{{ placeholder_name }}'}
Klicke auf einen Platzhalter um ihn einzufügen
) } /** * Extrahiert Workflow-spezifische Platzhalter aus Nodes * * @param {Array} nodes - Alle Workflow-Nodes * @param {string} currentNodeId - ID des aktuellen Nodes (wird ausgeschlossen) */ function extractWorkflowPlaceholders(nodes, currentNodeId) { const placeholders = [] console.log('🔍 Extracting workflow placeholders from nodes:', nodes) console.log('🚫 Excluding current node:', currentNodeId) nodes.forEach(node => { if (node.type === 'end') return // End Node hat keine Outputs if (node.id === currentNodeId) return // Selbst-Referenzierung verhindern const nodeId = node.id const nodeLabel = node.data?.label || nodeId console.log(`📦 Node ${nodeId}:`, { type: node.type, label: node.data?.label, nodeLabel: nodeLabel, data: node.data }) // analysis_core für alle Analysis/Logic/Join Nodes if (node.type === 'analysis' || node.type === 'logic' || node.type === 'join') { const desc = `${nodeLabel} (${nodeId}) - Hauptausgabe` console.log(` ➕ Adding placeholder: {{ ${nodeId}.analysis_core }} → "${desc}"`) placeholders.push({ placeholder: `{{ ${nodeId}.analysis_core }}`, description: desc, icon: getNodeIcon(node.type), category: 'Workflow - Node Outputs' }) } // Signals und Fragen für Analysis Nodes if (node.type === 'analysis' && node.data.questions && node.data.questions.length > 0) { node.data.questions.forEach((q, qIdx) => { const questionId = q.id || `q${qIdx + 1}` const questionType = q.type || 'unknown' const questionText = q.question || `Frage ${qIdx + 1}` // Signal-Platzhalter (Antwort) - VERWENDET ID für Eindeutigkeit! placeholders.push({ placeholder: `{{ ${nodeId}.signal_${questionId} }}`, description: `${nodeLabel} - ${questionId} (${questionType}): ${questionText.substring(0, 45)}${questionText.length > 45 ? '...' : ''}`, icon: '📊', category: 'Workflow - Signals' }) // Frage-Text-Platzhalter placeholders.push({ placeholder: `{{ ${nodeId}.question_${questionId} }}`, description: `${nodeLabel} - ${questionId} (${questionType}): ${questionText.substring(0, 45)}${questionText.length > 45 ? '...' : ''}`, icon: '❓', category: 'Workflow - Questions' }) }) } }) return placeholders } /** * Node-Typ zu Icon */ function getNodeIcon(type) { const icons = { start: '🚀', analysis: '🤖', logic: '⚡', join: '🔀', end: '🏁' } 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] || '📦' }