mitai-jinkendo/frontend/src/pages/AdminPromptsPage.jsx
Lars 500de132b9 feat: AI-Prompts flexibilisierung - Backend & Admin UI (Issue #28, Part 1)
Backend complete:
- Migration 017: Add category column to ai_prompts
- placeholder_resolver.py: 20+ placeholders with resolver functions
- Extended routers/prompts.py with CRUD endpoints:
  * POST /api/prompts (create)
  * PUT /api/prompts/:id (update)
  * DELETE /api/prompts/:id (delete)
  * POST /api/prompts/:id/duplicate
  * PUT /api/prompts/reorder
  * POST /api/prompts/preview
  * GET /api/prompts/placeholders
  * POST /api/prompts/generate (KI-assisted generation)
  * POST /api/prompts/:id/optimize (KI analysis)
- Extended models.py with PromptCreate, PromptUpdate, PromptGenerateRequest

Frontend:
- AdminPromptsPage.jsx: Full CRUD UI with category filter, reordering

Meta-Features:
- KI generates prompts from goal description + example data
- KI analyzes and optimizes existing prompts

Next: PromptEditModal, PromptGenerator, api.js integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 15:32:25 +01:00

294 lines
9.3 KiB
JavaScript

import { useState, useEffect } from 'react'
import { api } from '../utils/api'
import PromptEditModal from '../components/PromptEditModal'
export default function AdminPromptsPage() {
const [prompts, setPrompts] = useState([])
const [filteredPrompts, setFilteredPrompts] = useState([])
const [category, setCategory] = useState('all')
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [editingPrompt, setEditingPrompt] = useState(null)
const [showNewPrompt, setShowNewPrompt] = useState(false)
const categories = [
{ id: 'all', label: 'Alle Kategorien' },
{ id: 'körper', label: 'Körper' },
{ id: 'ernährung', label: 'Ernährung' },
{ id: 'training', label: 'Training' },
{ id: 'schlaf', label: 'Schlaf' },
{ id: 'vitalwerte', label: 'Vitalwerte' },
{ id: 'ziele', label: 'Ziele' },
{ id: 'ganzheitlich', label: 'Ganzheitlich' }
]
useEffect(() => {
loadPrompts()
}, [])
useEffect(() => {
if (category === 'all') {
setFilteredPrompts(prompts)
} else {
setFilteredPrompts(prompts.filter(p => p.category === category))
}
}, [category, prompts])
const loadPrompts = async () => {
try {
setLoading(true)
const data = await api.listAdminPrompts()
setPrompts(data)
setError(null)
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
const handleToggleActive = async (prompt) => {
try {
await api.updatePrompt(prompt.id, { active: !prompt.active })
await loadPrompts()
} catch (e) {
alert('Fehler: ' + e.message)
}
}
const handleDelete = async (prompt) => {
if (!confirm(`Prompt "${prompt.name}" wirklich löschen?`)) return
try {
await api.deletePrompt(prompt.id)
await loadPrompts()
} catch (e) {
alert('Fehler: ' + e.message)
}
}
const handleDuplicate = async (prompt) => {
try {
await api.duplicatePrompt(prompt.id)
await loadPrompts()
} catch (e) {
alert('Fehler: ' + e.message)
}
}
const handleMoveUp = async (prompt) => {
const idx = filteredPrompts.findIndex(p => p.id === prompt.id)
if (idx === 0) return // Already at top
const above = filteredPrompts[idx - 1]
const newOrder = filteredPrompts.map(p => p.id)
newOrder[idx] = above.id
newOrder[idx - 1] = prompt.id
try {
await api.reorderPrompts(newOrder)
await loadPrompts()
} catch (e) {
alert('Fehler: ' + e.message)
}
}
const handleMoveDown = async (prompt) => {
const idx = filteredPrompts.findIndex(p => p.id === prompt.id)
if (idx === filteredPrompts.length - 1) return // Already at bottom
const below = filteredPrompts[idx + 1]
const newOrder = filteredPrompts.map(p => p.id)
newOrder[idx] = below.id
newOrder[idx + 1] = prompt.id
try {
await api.reorderPrompts(newOrder)
await loadPrompts()
} catch (e) {
alert('Fehler: ' + e.message)
}
}
const handleSavePrompt = async () => {
await loadPrompts()
setEditingPrompt(null)
setShowNewPrompt(false)
}
if (loading) {
return (
<div className="page">
<div style={{textAlign:'center', padding:40}}>
<div className="spinner"/>
</div>
</div>
)
}
return (
<div className="page">
<div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:24}}>
<h1 className="page-title">KI-Prompts Verwaltung</h1>
<button
className="btn btn-primary"
onClick={() => setShowNewPrompt(true)}
>
+ Neuer Prompt
</button>
</div>
{error && (
<div style={{padding:12, background:'#fee', color:'#c00', borderRadius:8, marginBottom:16}}>
{error}
</div>
)}
{/* Category Filter */}
<div style={{marginBottom:24}}>
<label style={{fontSize:13, fontWeight:600, marginBottom:8, display:'block'}}>
Kategorie filtern:
</label>
<select
className="form-select"
value={category}
onChange={e => setCategory(e.target.value)}
style={{maxWidth:300}}
>
{categories.map(cat => (
<option key={cat.id} value={cat.id}>{cat.label}</option>
))}
</select>
</div>
{/* Prompts Table */}
<div className="card">
<table style={{width:'100%', borderCollapse:'collapse'}}>
<thead>
<tr style={{borderBottom:'2px solid var(--border)', textAlign:'left'}}>
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>
Titel
</th>
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>
Kategorie
</th>
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>
Aktiv
</th>
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>
Reihenfolge
</th>
<th style={{padding:'12px 8px', fontSize:12, fontWeight:600, color:'var(--text3)'}}>
Aktionen
</th>
</tr>
</thead>
<tbody>
{filteredPrompts.map((prompt, idx) => (
<tr key={prompt.id} style={{borderBottom:'1px solid var(--border)'}}>
<td style={{padding:'12px 8px'}}>
<div style={{fontWeight:500, fontSize:14}}>{prompt.name}</div>
{prompt.description && (
<div style={{fontSize:11, color:'var(--text3)', marginTop:2}}>
{prompt.description}
</div>
)}
<div style={{fontSize:10, color:'var(--text3)', marginTop:4}}>
{prompt.slug}
</div>
</td>
<td style={{padding:'12px 8px'}}>
<span style={{
padding:'4px 8px', borderRadius:4, fontSize:11, fontWeight:500,
background:'var(--surface2)', color:'var(--text2)'
}}>
{prompt.category || 'ganzheitlich'}
</span>
</td>
<td style={{padding:'12px 8px'}}>
<input
type="checkbox"
checked={prompt.active}
onChange={() => handleToggleActive(prompt)}
style={{cursor:'pointer'}}
/>
</td>
<td style={{padding:'12px 8px'}}>
<div style={{display:'flex', gap:4}}>
<button
onClick={() => handleMoveUp(prompt)}
disabled={idx === 0}
style={{
padding:'4px 8px', fontSize:12, cursor: idx === 0 ? 'not-allowed' : 'pointer',
opacity: idx === 0 ? 0.5 : 1
}}
title="Nach oben"
>
</button>
<button
onClick={() => handleMoveDown(prompt)}
disabled={idx === filteredPrompts.length - 1}
style={{
padding:'4px 8px', fontSize:12,
cursor: idx === filteredPrompts.length - 1 ? 'not-allowed' : 'pointer',
opacity: idx === filteredPrompts.length - 1 ? 0.5 : 1
}}
title="Nach unten"
>
</button>
</div>
</td>
<td style={{padding:'12px 8px'}}>
<div style={{display:'flex', gap:6}}>
<button
className="btn"
onClick={() => setEditingPrompt(prompt)}
style={{fontSize:12, padding:'6px 12px'}}
>
Bearbeiten
</button>
<button
className="btn"
onClick={() => handleDuplicate(prompt)}
style={{fontSize:12, padding:'6px 12px'}}
>
Duplizieren
</button>
<button
className="btn"
onClick={() => handleDelete(prompt)}
style={{fontSize:12, padding:'6px 12px', color:'#D85A30'}}
>
Löschen
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{filteredPrompts.length === 0 && (
<div style={{padding:40, textAlign:'center', color:'var(--text3)'}}>
Keine Prompts in dieser Kategorie
</div>
)}
</div>
{/* Edit Modal */}
{(editingPrompt || showNewPrompt) && (
<PromptEditModal
prompt={editingPrompt}
onSave={handleSavePrompt}
onClose={() => {
setEditingPrompt(null)
setShowNewPrompt(false)
}}
/>
)}
</div>
)
}