266 lines
7.7 KiB
React
266 lines
7.7 KiB
React
|
|
import { useState } from 'react'
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* PlaceholderPicker - Modal zur Auswahl von Template-Platzhaltern
|
|||
|
|
*
|
|||
|
|
* Props:
|
|||
|
|
* - nodes: Array of workflow nodes (to extract available placeholders)
|
|||
|
|
* - onSelect: (placeholderString) => void - Callback when placeholder is selected
|
|||
|
|
* - onClose: () => void
|
|||
|
|
*
|
|||
|
|
* Features:
|
|||
|
|
* - Lists all available node outputs
|
|||
|
|
* - Copy to clipboard or insert into template
|
|||
|
|
* - Kategorisiert nach Node-Typ
|
|||
|
|
*/
|
|||
|
|
export function PlaceholderPicker({ nodes, onSelect, onClose }) {
|
|||
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|||
|
|
|
|||
|
|
// Extrahiere verfügbare Platzhalter aus Nodes
|
|||
|
|
const placeholders = extractPlaceholders(nodes)
|
|||
|
|
|
|||
|
|
// Filtere basierend auf Suchquery
|
|||
|
|
const filteredPlaceholders = placeholders.filter(p =>
|
|||
|
|
p.placeholder.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|||
|
|
p.description.toLowerCase().includes(searchQuery.toLowerCase())
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const handleSelect = (placeholderString) => {
|
|||
|
|
onSelect(placeholderString)
|
|||
|
|
onClose() // Schließe Modal nach Auswahl
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
position: 'fixed',
|
|||
|
|
top: 0,
|
|||
|
|
left: 0,
|
|||
|
|
right: 0,
|
|||
|
|
bottom: 0,
|
|||
|
|
background: 'rgba(0,0,0,0.5)',
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
zIndex: 2000
|
|||
|
|
}}
|
|||
|
|
onClick={onClose}
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
background: 'var(--surface)',
|
|||
|
|
borderRadius: '12px',
|
|||
|
|
padding: '24px',
|
|||
|
|
maxWidth: '600px',
|
|||
|
|
width: '90%',
|
|||
|
|
maxHeight: '80vh',
|
|||
|
|
display: 'flex',
|
|||
|
|
flexDirection: 'column',
|
|||
|
|
boxShadow: '0 4px 16px rgba(0,0,0,0.2)'
|
|||
|
|
}}
|
|||
|
|
onClick={(e) => e.stopPropagation()}
|
|||
|
|
>
|
|||
|
|
{/* Header */}
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
justifyContent: 'space-between',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
marginBottom: '16px'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<h2 style={{ margin: 0, fontSize: '18px' }}>Platzhalter auswählen</h2>
|
|||
|
|
<button
|
|||
|
|
onClick={onClose}
|
|||
|
|
style={{
|
|||
|
|
background: 'none',
|
|||
|
|
border: 'none',
|
|||
|
|
fontSize: '24px',
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
color: 'var(--text3)',
|
|||
|
|
padding: '4px 8px',
|
|||
|
|
lineHeight: 1
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
×
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Search */}
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
placeholder="Suche nach Node oder Variable..."
|
|||
|
|
value={searchQuery}
|
|||
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|||
|
|
style={{
|
|||
|
|
width: '100%',
|
|||
|
|
padding: '8px 12px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid var(--border)',
|
|||
|
|
background: 'var(--bg)',
|
|||
|
|
color: 'var(--text1)',
|
|||
|
|
fontSize: '14px',
|
|||
|
|
marginBottom: '16px'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{/* Placeholder List */}
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
flex: 1,
|
|||
|
|
overflowY: 'auto',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid var(--border)'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{filteredPlaceholders.length === 0 ? (
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
padding: '24px',
|
|||
|
|
textAlign: 'center',
|
|||
|
|
color: 'var(--text3)',
|
|||
|
|
fontSize: '14px'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{searchQuery ? 'Keine Platzhalter gefunden' : 'Keine Nodes im Workflow'}
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|||
|
|
{filteredPlaceholders.map((p, idx) => (
|
|||
|
|
<div
|
|||
|
|
key={idx}
|
|||
|
|
onClick={() => handleSelect(p.placeholder)}
|
|||
|
|
style={{
|
|||
|
|
padding: '12px',
|
|||
|
|
borderBottom: idx < filteredPlaceholders.length - 1 ? '1px solid var(--border)' : 'none',
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
transition: 'background 0.2s'
|
|||
|
|
}}
|
|||
|
|
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface2)'}
|
|||
|
|
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
justifyContent: 'space-between',
|
|||
|
|
alignItems: 'center'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div style={{ flex: 1 }}>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
fontFamily: 'monospace',
|
|||
|
|
fontSize: '13px',
|
|||
|
|
color: 'var(--accent)',
|
|||
|
|
marginBottom: '4px'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{p.placeholder}
|
|||
|
|
</div>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
fontSize: '12px',
|
|||
|
|
color: 'var(--text3)'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{p.description}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
fontSize: '20px',
|
|||
|
|
marginLeft: '12px',
|
|||
|
|
color: 'var(--text3)'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{p.icon}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Footer */}
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
marginTop: '16px',
|
|||
|
|
padding: '12px',
|
|||
|
|
background: 'var(--bg)',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
fontSize: '12px',
|
|||
|
|
color: 'var(--text3)'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
💡 <strong>Syntax:</strong> <code style={{ background: 'var(--surface2)', padding: '2px 6px', borderRadius: '4px' }}>{'{{ node_id.field }}'}</code>
|
|||
|
|
<br />
|
|||
|
|
Verfügbare Felder: <code>analysis_core</code>, <code>signal_*</code> (wenn Fragen vorhanden)
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Extrahiert verfügbare Platzhalter aus Workflow-Nodes
|
|||
|
|
*/
|
|||
|
|
function extractPlaceholders(nodes) {
|
|||
|
|
const placeholders = []
|
|||
|
|
|
|||
|
|
// Globale Platzhalter
|
|||
|
|
placeholders.push({
|
|||
|
|
placeholder: '{{ analysis_core }}',
|
|||
|
|
description: 'Finale Analyse (AUTO-Modus Fallback)',
|
|||
|
|
icon: '🏁',
|
|||
|
|
category: 'global'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// Node-spezifische Platzhalter
|
|||
|
|
nodes.forEach(node => {
|
|||
|
|
if (node.type === 'end') return // End Node hat keine Outputs
|
|||
|
|
|
|||
|
|
const nodeId = node.id
|
|||
|
|
const label = node.data.label || nodeId
|
|||
|
|
|
|||
|
|
// analysis_core für alle Analysis/Logic/Join Nodes
|
|||
|
|
if (node.type === 'analysis' || node.type === 'logic' || node.type === 'join') {
|
|||
|
|
placeholders.push({
|
|||
|
|
placeholder: `{{ ${nodeId}.analysis_core }}`,
|
|||
|
|
description: `${getNodeIcon(node.type)} ${label} - Hauptausgabe`,
|
|||
|
|
icon: getNodeIcon(node.type),
|
|||
|
|
category: 'node_outputs'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Signals für Analysis Nodes mit Fragen
|
|||
|
|
if (node.type === 'analysis' && node.data.questions && node.data.questions.length > 0) {
|
|||
|
|
node.data.questions.forEach(q => {
|
|||
|
|
const questionId = q.id || `q${placeholders.length}`
|
|||
|
|
placeholders.push({
|
|||
|
|
placeholder: `{{ ${nodeId}.signal_${questionId} }}`,
|
|||
|
|
description: `${label} - Signal: ${q.question?.substring(0, 40)}...`,
|
|||
|
|
icon: '📊',
|
|||
|
|
category: 'signals'
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return placeholders
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Node-Typ zu Icon
|
|||
|
|
*/
|
|||
|
|
function getNodeIcon(type) {
|
|||
|
|
const icons = {
|
|||
|
|
start: '🚀',
|
|||
|
|
analysis: '🤖',
|
|||
|
|
logic: '⚡',
|
|||
|
|
join: '🔀',
|
|||
|
|
end: '🏁'
|
|||
|
|
}
|
|||
|
|
return icons[type] || '📦'
|
|||
|
|
}
|