diff --git a/frontend/src/components/PlaceholderPicker.jsx b/frontend/src/components/PlaceholderPicker.jsx new file mode 100644 index 0000000..d143966 --- /dev/null +++ b/frontend/src/components/PlaceholderPicker.jsx @@ -0,0 +1,258 @@ +import { useState, useEffect } from 'react' +import { api } from '../utils/api' +import { Search, X } from 'lucide-react' + +/** + * Placeholder Picker with grouped categories and search + * + * Loads placeholders dynamically from backend catalog. + * Grouped by category (Profil, Körper, Ernährung, Training, etc.) + */ +export default function PlaceholderPicker({ onSelect, onClose }) { + const [catalog, setCatalog] = useState({}) + const [search, setSearch] = useState('') + const [loading, setLoading] = useState(true) + const [expandedCategories, setExpandedCategories] = useState(new Set()) + + useEffect(() => { + loadCatalog() + }, []) + + const loadCatalog = async () => { + try { + setLoading(true) + const data = await api.listPlaceholders() + setCatalog(data) + // Expand all categories by default + setExpandedCategories(new Set(Object.keys(data))) + } catch (e) { + console.error('Failed to load placeholders:', e) + } finally { + setLoading(false) + } + } + + const toggleCategory = (category) => { + const newExpanded = new Set(expandedCategories) + if (newExpanded.has(category)) { + newExpanded.delete(category) + } else { + newExpanded.add(category) + } + setExpandedCategories(newExpanded) + } + + const handleSelect = (key) => { + onSelect(`{{${key}}}`) + onClose() + } + + // Filter placeholders by search + const filteredCatalog = {} + const searchLower = search.toLowerCase() + + Object.entries(catalog).forEach(([category, items]) => { + const filtered = items.filter(item => + item.key.toLowerCase().includes(searchLower) || + item.description.toLowerCase().includes(searchLower) + ) + if (filtered.length > 0) { + filteredCatalog[category] = filtered + } + }) + + return ( +
+
e.stopPropagation()} + style={{ + background: 'var(--bg)', + borderRadius: 12, + maxWidth: 800, + width: '100%', + maxHeight: '80vh', + display: 'flex', + flexDirection: 'column', + overflow: 'hidden' + }} + > + {/* Header */} +
+

+ Platzhalter auswählen +

+ +
+ + {/* Search */} +
+
+ + setSearch(e.target.value)} + placeholder="Platzhalter suchen..." + style={{ + width: '100%', + paddingLeft: 40, + textAlign: 'left' + }} + /> +
+
+ + {/* Categories */} +
+ {loading ? ( +
+ Lädt Platzhalter... +
+ ) : Object.keys(filteredCatalog).length === 0 ? ( +
+ Keine Platzhalter gefunden +
+ ) : ( + Object.entries(filteredCatalog).map(([category, items]) => ( +
+
toggleCategory(category)} + style={{ + padding: '8px 12px', + background: 'var(--surface)', + borderRadius: 8, + cursor: 'pointer', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8 + }} + > +

+ {category} ({items.length}) +

+ + {expandedCategories.has(category) ? '▼' : '▶'} + +
+ + {expandedCategories.has(category) && ( +
+ {items.map(item => ( +
handleSelect(item.key)} + style={{ + padding: '8px 12px', + background: 'var(--surface2)', + borderRadius: 6, + cursor: 'pointer', + border: '1px solid transparent', + transition: 'all 0.2s' + }} + onMouseEnter={e => { + e.currentTarget.style.borderColor = 'var(--accent)' + e.currentTarget.style.background = 'var(--surface)' + }} + onMouseLeave={e => { + e.currentTarget.style.borderColor = 'transparent' + e.currentTarget.style.background = 'var(--surface2)' + }} + > +
+
+ + {`{{${item.key}}}`} + +
+ {item.description} +
+
+ {item.example && ( +
+ {item.example} +
+ )} +
+
+ ))} +
+ )} +
+ )) + )} +
+ + {/* Footer */} +
+ Klicke auf einen Platzhalter zum Einfügen +
+
+
+ ) +} diff --git a/frontend/src/components/UnifiedPromptModal.jsx b/frontend/src/components/UnifiedPromptModal.jsx index d9d51bd..74b0926 100644 --- a/frontend/src/components/UnifiedPromptModal.jsx +++ b/frontend/src/components/UnifiedPromptModal.jsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react' import { api } from '../utils/api' -import { X, Plus, Trash2, MoveUp, MoveDown } from 'lucide-react' +import { X, Plus, Trash2, MoveUp, MoveDown, Code } from 'lucide-react' +import PlaceholderPicker from './PlaceholderPicker' /** * Unified Prompt Editor Modal (Issue #28 Phase 3) @@ -36,6 +37,8 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const [showPlaceholderPicker, setShowPlaceholderPicker] = useState(false) + const [pickerTarget, setPickerTarget] = useState(null) // 'base' or {stage, promptIdx} useEffect(() => { loadAvailablePrompts() @@ -390,31 +393,23 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) { style={{ width: '100%', textAlign: 'left', resize: 'vertical', fontFamily: 'monospace', fontSize: 12 }} />
-
- Verwende {'{{'} und {'}}'} für Platzhalter: -
-
- {['weight_data', 'nutrition_data', 'activity_data', 'sleep_data', 'vitals_baseline', 'blood_pressure', 'profile_id', 'today'].map(ph => ( - { - const placeholder = `{{${ph}}}` - setTemplate(template + placeholder) - }} - style={{ - padding: '2px 6px', - background: 'var(--surface2)', - borderRadius: 4, - fontSize: 10, - cursor: 'pointer', - border: '1px solid var(--border)' - }} - title="Klicken zum Einfügen" - > - {'{{'}{ph}{'}}'} - - ))} -
+
)} @@ -536,29 +531,23 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) { style={{ width: '100%', fontSize: 12, textAlign: 'left', resize: 'vertical', fontFamily: 'monospace' }} />
-
- {['weight_data', 'nutrition_data', 'activity_data', 'sleep_data', 'vitals_baseline', 'blood_pressure', 'profile_id', 'today'].map(ph => ( - { - const placeholder = `{{${ph}}}` - const currentValue = p.template || '' - updateStagePrompt(stage.stage, pIdx, 'template', currentValue + placeholder) - }} - style={{ - padding: '2px 4px', - background: 'var(--surface2)', - borderRadius: 4, - fontSize: 9, - cursor: 'pointer', - border: '1px solid var(--border)' - }} - title="Klicken zum Einfügen" - > - {'{{'}{ph}{'}}'} - - ))} -
+
)} @@ -595,6 +584,37 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) { + + {/* Placeholder Picker */} + {showPlaceholderPicker && ( + { + if (pickerTarget === 'base') { + // Insert into base template + setTemplate(template + placeholder) + } else if (pickerTarget && typeof pickerTarget === 'object') { + // Insert into pipeline stage template + const { stage: stageNum, promptIdx } = pickerTarget + setStages(stages.map(s => { + if (s.stage === stageNum) { + const newPrompts = [...s.prompts] + const currentTemplate = newPrompts[promptIdx].template || '' + newPrompts[promptIdx] = { + ...newPrompts[promptIdx], + template: currentTemplate + placeholder + } + return { ...s, prompts: newPrompts } + } + return s + })) + } + }} + onClose={() => { + setShowPlaceholderPicker(false) + setPickerTarget(null) + }} + /> + )} ) }