feat: placeholder chips + convert to base prompt (Issue #28)
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s

New features:
1. Placeholder chips now visible in pipeline inline templates
   - Click to insert: weight_data, nutrition_data, activity_data, etc.
   - Same UX as base prompts

2. Convert to Base Prompt button
   - New icon (ArrowDownToLine) in actions column
   - Only visible for 1-stage pipeline prompts
   - Converts pipeline → base by extracting inline template
   - Validates: must be 1-stage, 1-prompt, inline source

This allows migrated prompts to be properly categorized as base prompts
for reuse in other pipelines.
This commit is contained in:
Lars 2026-03-25 21:59:43 +01:00
parent 7dda520c9b
commit b058b0fd6f
2 changed files with 94 additions and 9 deletions

View File

@ -526,6 +526,7 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
))} ))}
</select> </select>
) : ( ) : (
<div>
<textarea <textarea
className="form-input" className="form-input"
value={p.template || ''} value={p.template || ''}
@ -534,6 +535,32 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
placeholder="Inline-Template mit {{placeholders}}..." placeholder="Inline-Template mit {{placeholders}}..."
style={{ width: '100%', fontSize: 12, textAlign: 'left', resize: 'vertical', fontFamily: 'monospace' }} style={{ width: '100%', fontSize: 12, textAlign: 'left', resize: 'vertical', fontFamily: 'monospace' }}
/> />
<div style={{ fontSize: 10, color: 'var(--text3)', marginTop: 4 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 }}>
{['weight_data', 'nutrition_data', 'activity_data', 'sleep_data', 'vitals_baseline', 'blood_pressure', 'profile_id', 'today'].map(ph => (
<code
key={ph}
onClick={() => {
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}{'}}'}
</code>
))}
</div>
</div>
</div>
)} )}
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { api } from '../utils/api' import { api } from '../utils/api'
import UnifiedPromptModal from '../components/UnifiedPromptModal' import UnifiedPromptModal from '../components/UnifiedPromptModal'
import { Star, Trash2, Edit, Copy, Filter } from 'lucide-react' import { Star, Trash2, Edit, Copy, Filter, ArrowDownToLine } from 'lucide-react'
/** /**
* Admin Prompts Page - Unified System (Issue #28 Phase 3) * Admin Prompts Page - Unified System (Issue #28 Phase 3)
@ -94,6 +94,49 @@ export default function AdminPromptsPage() {
} }
} }
const handleConvertToBase = async (prompt) => {
// Convert a 1-stage pipeline to a base prompt
if (prompt.type !== 'pipeline') {
alert('Nur Pipeline-Prompts können konvertiert werden')
return
}
const stages = typeof prompt.stages === 'string'
? JSON.parse(prompt.stages)
: prompt.stages
if (!stages || stages.length !== 1) {
alert('Nur 1-stage Pipeline-Prompts können zu Basis-Prompts konvertiert werden')
return
}
const stage1 = stages[0]
if (!stage1.prompts || stage1.prompts.length !== 1) {
alert('Stage muss genau einen Prompt haben')
return
}
const firstPrompt = stage1.prompts[0]
if (firstPrompt.source !== 'inline' || !firstPrompt.template) {
alert('Nur inline Templates können konvertiert werden')
return
}
if (!confirm(`"${prompt.name}" zu Basis-Prompt konvertieren?`)) return
try {
await api.updateUnifiedPrompt(prompt.id, {
type: 'base',
template: firstPrompt.template,
output_format: firstPrompt.output_format || 'text',
stages: null
})
await loadPrompts()
} catch (e) {
alert('Fehler: ' + e.message)
}
}
const handleSave = async () => { const handleSave = async () => {
setEditingPrompt(null) setEditingPrompt(null)
setShowNewPrompt(false) setShowNewPrompt(false)
@ -388,6 +431,21 @@ export default function AdminPromptsPage() {
> >
<Edit size={16} color="var(--accent)" /> <Edit size={16} color="var(--accent)" />
</button> </button>
{/* Show convert button for 1-stage pipelines */}
{prompt.type === 'pipeline' && getStageCount(prompt) === 1 && (
<button
onClick={() => handleConvertToBase(prompt)}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 4
}}
title="Zu Basis-Prompt konvertieren"
>
<ArrowDownToLine size={16} color="#6366f1" />
</button>
)}
<button <button
onClick={() => handleDuplicate(prompt)} onClick={() => handleDuplicate(prompt)}
style={{ style={{