feat: AI-Prompts flexibilisierung - Frontend complete (Issue #28, Part 2)
Frontend components: - PromptEditModal.jsx: Full editor with preview, generator, optimizer - PromptGenerator.jsx: KI-assisted prompt creation from goal description - Extended api.js with 10 new prompt endpoints Navigation: - Added /admin/prompts route to App.jsx - Added KI-Prompts section to AdminPanel with navigation button Features complete: ✅ Admin can create/edit/delete/duplicate prompts ✅ Category filtering and reordering ✅ Preview prompts with real user data ✅ KI generates prompts from goal + example data ✅ KI analyzes and optimizes existing prompts ✅ Side-by-side comparison original vs optimized Ready for testing: http://dev.mitai.jinkendo.de/admin/prompts Issue #28 Phase 2 complete - 13-18h estimated, ~14h actual Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
500de132b9
commit
c8cf375399
|
|
@ -30,6 +30,7 @@ import AdminUserRestrictionsPage from './pages/AdminUserRestrictionsPage'
|
||||||
import AdminTrainingTypesPage from './pages/AdminTrainingTypesPage'
|
import AdminTrainingTypesPage from './pages/AdminTrainingTypesPage'
|
||||||
import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage'
|
import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage'
|
||||||
import AdminTrainingProfiles from './pages/AdminTrainingProfiles'
|
import AdminTrainingProfiles from './pages/AdminTrainingProfiles'
|
||||||
|
import AdminPromptsPage from './pages/AdminPromptsPage'
|
||||||
import SubscriptionPage from './pages/SubscriptionPage'
|
import SubscriptionPage from './pages/SubscriptionPage'
|
||||||
import SleepPage from './pages/SleepPage'
|
import SleepPage from './pages/SleepPage'
|
||||||
import RestDaysPage from './pages/RestDaysPage'
|
import RestDaysPage from './pages/RestDaysPage'
|
||||||
|
|
@ -184,6 +185,7 @@ function AppShell() {
|
||||||
<Route path="/admin/training-types" element={<AdminTrainingTypesPage/>}/>
|
<Route path="/admin/training-types" element={<AdminTrainingTypesPage/>}/>
|
||||||
<Route path="/admin/activity-mappings" element={<AdminActivityMappingsPage/>}/>
|
<Route path="/admin/activity-mappings" element={<AdminActivityMappingsPage/>}/>
|
||||||
<Route path="/admin/training-profiles" element={<AdminTrainingProfiles/>}/>
|
<Route path="/admin/training-profiles" element={<AdminTrainingProfiles/>}/>
|
||||||
|
<Route path="/admin/prompts" element={<AdminPromptsPage/>}/>
|
||||||
<Route path="/subscription" element={<SubscriptionPage/>}/>
|
<Route path="/subscription" element={<SubscriptionPage/>}/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
379
frontend/src/components/PromptEditModal.jsx
Normal file
379
frontend/src/components/PromptEditModal.jsx
Normal file
|
|
@ -0,0 +1,379 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { api } from '../utils/api'
|
||||||
|
import PromptGenerator from './PromptGenerator'
|
||||||
|
|
||||||
|
export default function PromptEditModal({ prompt, onSave, onClose }) {
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [slug, setSlug] = useState('')
|
||||||
|
const [description, setDescription] = useState('')
|
||||||
|
const [category, setCategory] = useState('ganzheitlich')
|
||||||
|
const [template, setTemplate] = useState('')
|
||||||
|
const [active, setActive] = useState(true)
|
||||||
|
|
||||||
|
const [preview, setPreview] = useState(null)
|
||||||
|
const [unknownPlaceholders, setUnknownPlaceholders] = useState([])
|
||||||
|
const [showGenerator, setShowGenerator] = useState(false)
|
||||||
|
const [optimization, setOptimization] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ 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(() => {
|
||||||
|
if (prompt) {
|
||||||
|
setName(prompt.name || '')
|
||||||
|
setSlug(prompt.slug || '')
|
||||||
|
setDescription(prompt.description || '')
|
||||||
|
setCategory(prompt.category || 'ganzheitlich')
|
||||||
|
setTemplate(prompt.template || '')
|
||||||
|
setActive(prompt.active ?? true)
|
||||||
|
}
|
||||||
|
}, [prompt])
|
||||||
|
|
||||||
|
const handlePreview = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const result = await api.previewPrompt(template)
|
||||||
|
setPreview(result.resolved)
|
||||||
|
setUnknownPlaceholders(result.unknown_placeholders || [])
|
||||||
|
} catch (e) {
|
||||||
|
alert('Fehler bei Vorschau: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOptimize = async () => {
|
||||||
|
if (!prompt?.id) {
|
||||||
|
alert('Prompt muss erst gespeichert werden bevor er optimiert werden kann')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const result = await api.optimizePrompt(prompt.id)
|
||||||
|
setOptimization(result)
|
||||||
|
} catch (e) {
|
||||||
|
alert('Fehler bei Optimierung: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleApplyOptimized = () => {
|
||||||
|
if (optimization?.optimized_prompt) {
|
||||||
|
setTemplate(optimization.optimized_prompt)
|
||||||
|
setOptimization(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!name.trim()) {
|
||||||
|
alert('Bitte Titel eingeben')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!template.trim()) {
|
||||||
|
alert('Bitte Template eingeben')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSaving(true)
|
||||||
|
|
||||||
|
if (prompt?.id) {
|
||||||
|
// Update existing
|
||||||
|
await api.updatePrompt(prompt.id, {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
template,
|
||||||
|
active
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Create new
|
||||||
|
if (!slug.trim()) {
|
||||||
|
alert('Bitte Slug eingeben')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await api.createPrompt({
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
template,
|
||||||
|
active
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave()
|
||||||
|
} catch (e) {
|
||||||
|
alert('Fehler beim Speichern: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGeneratorResult = (generated) => {
|
||||||
|
setName(generated.suggested_title)
|
||||||
|
setCategory(generated.suggested_category)
|
||||||
|
setTemplate(generated.template)
|
||||||
|
setShowGenerator(false)
|
||||||
|
|
||||||
|
// Auto-generate slug if new prompt
|
||||||
|
if (!prompt?.id) {
|
||||||
|
const autoSlug = generated.suggested_title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '_')
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
setSlug(autoSlug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position:'fixed', inset:0, background:'rgba(0,0,0,0.5)',
|
||||||
|
display:'flex', alignItems:'center', justifyContent:'center',
|
||||||
|
zIndex:1000, padding:20
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background:'var(--bg)', borderRadius:12, maxWidth:900, width:'100%',
|
||||||
|
maxHeight:'90vh', overflow:'auto', padding:24
|
||||||
|
}}>
|
||||||
|
<h2 style={{margin:'0 0 24px 0', fontSize:20, fontWeight:600}}>
|
||||||
|
{prompt ? 'Prompt bearbeiten' : 'Neuer Prompt'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div style={{display:'flex', gap:8, marginBottom:24, flexWrap:'wrap'}}>
|
||||||
|
<button
|
||||||
|
className="btn"
|
||||||
|
onClick={() => setShowGenerator(true)}
|
||||||
|
style={{fontSize:13}}
|
||||||
|
>
|
||||||
|
🤖 Von KI erstellen lassen
|
||||||
|
</button>
|
||||||
|
{prompt?.id && (
|
||||||
|
<button
|
||||||
|
className="btn"
|
||||||
|
onClick={handleOptimize}
|
||||||
|
disabled={loading}
|
||||||
|
style={{fontSize:13}}
|
||||||
|
>
|
||||||
|
✨ Von KI optimieren lassen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="btn"
|
||||||
|
onClick={handlePreview}
|
||||||
|
disabled={loading || !template.trim()}
|
||||||
|
style={{fontSize:13}}
|
||||||
|
>
|
||||||
|
👁️ Vorschau
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Fields */}
|
||||||
|
<div style={{display:'grid', gap:16, marginBottom:24}}>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Titel *</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
placeholder="z.B. Protein-Analyse"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!prompt?.id && (
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Slug * (eindeutig, keine Leerzeichen)</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={slug}
|
||||||
|
onChange={e => setSlug(e.target.value)}
|
||||||
|
placeholder="z.B. protein_analyse"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={description}
|
||||||
|
onChange={e => setDescription(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
placeholder="Wofür ist dieser Prompt? (für Admin sichtbar)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Kategorie</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={category}
|
||||||
|
onChange={e => setCategory(e.target.value)}
|
||||||
|
>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<option key={cat.id} value={cat.id}>{cat.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Template *</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={template}
|
||||||
|
onChange={e => setTemplate(e.target.value)}
|
||||||
|
rows={15}
|
||||||
|
placeholder="Du bist ein Ernährungsexperte. Analysiere folgende Daten: {{nutrition_summary}}"
|
||||||
|
style={{fontFamily:'monospace', fontSize:12}}
|
||||||
|
/>
|
||||||
|
<div style={{fontSize:11, color:'var(--text3)', marginTop:4}}>
|
||||||
|
Nutze Platzhalter wie {`{{weight_aktuell}}`}, {`{{protein_avg}}`}, etc.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{display:'flex', alignItems:'center', gap:8}}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={active}
|
||||||
|
onChange={e => setActive(e.target.checked)}
|
||||||
|
id="active-checkbox"
|
||||||
|
/>
|
||||||
|
<label htmlFor="active-checkbox" style={{margin:0, cursor:'pointer'}}>
|
||||||
|
Prompt ist aktiv (für Nutzer sichtbar)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Unknown Placeholders Warning */}
|
||||||
|
{unknownPlaceholders.length > 0 && (
|
||||||
|
<div style={{
|
||||||
|
padding:12, background:'#fff3cd', border:'1px solid #ffc107',
|
||||||
|
borderRadius:8, marginBottom:16, fontSize:12
|
||||||
|
}}>
|
||||||
|
<strong>⚠️ Unbekannte Platzhalter:</strong> {unknownPlaceholders.join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Preview */}
|
||||||
|
{preview && (
|
||||||
|
<div style={{marginBottom:24}}>
|
||||||
|
<h3 style={{fontSize:14, fontWeight:600, marginBottom:8}}>
|
||||||
|
Vorschau (mit echten Daten):
|
||||||
|
</h3>
|
||||||
|
<pre style={{
|
||||||
|
background:'var(--surface2)', padding:16, borderRadius:8,
|
||||||
|
fontSize:12, lineHeight:1.6, whiteSpace:'pre-wrap',
|
||||||
|
maxHeight:300, overflow:'auto'
|
||||||
|
}}>
|
||||||
|
{preview}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Optimization Results */}
|
||||||
|
{optimization && (
|
||||||
|
<div style={{
|
||||||
|
marginBottom:24, padding:16, background:'var(--surface)',
|
||||||
|
border:'1px solid var(--border)', borderRadius:8
|
||||||
|
}}>
|
||||||
|
<h3 style={{fontSize:16, fontWeight:600, marginBottom:12}}>
|
||||||
|
✨ Optimierungsvorschläge
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div style={{display:'grid', gap:12, fontSize:13}}>
|
||||||
|
<div>
|
||||||
|
<strong>Score:</strong> {optimization.score}/100
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<strong>✅ Stärken:</strong>
|
||||||
|
<ul style={{marginTop:4, paddingLeft:20}}>
|
||||||
|
{optimization.strengths?.map((s, i) => (
|
||||||
|
<li key={i}>{s}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<strong>⚠️ Verbesserungspotenzial:</strong>
|
||||||
|
<ul style={{marginTop:4, paddingLeft:20}}>
|
||||||
|
{optimization.weaknesses?.map((w, i) => (
|
||||||
|
<li key={i}>{w}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<strong>Änderungen:</strong>
|
||||||
|
<p style={{marginTop:4}}>{optimization.changes_summary}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Side-by-side comparison */}
|
||||||
|
<div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:12}}>
|
||||||
|
<div>
|
||||||
|
<strong>Original:</strong>
|
||||||
|
<pre style={{
|
||||||
|
marginTop:4, padding:12, background:'var(--surface2)',
|
||||||
|
borderRadius:8, fontSize:11, maxHeight:200, overflow:'auto'
|
||||||
|
}}>
|
||||||
|
{template}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Optimiert:</strong>
|
||||||
|
<pre style={{
|
||||||
|
marginTop:4, padding:12, background:'#d4edda',
|
||||||
|
borderRadius:8, fontSize:11, maxHeight:200, overflow:'auto'
|
||||||
|
}}>
|
||||||
|
{optimization.optimized_prompt}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleApplyOptimized}
|
||||||
|
>
|
||||||
|
✅ Optimierte Version übernehmen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div style={{display:'flex', gap:12, justifyContent:'flex-end'}}>
|
||||||
|
<button className="btn" onClick={onClose}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
{saving ? 'Speichern...' : 'Speichern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Generator Modal */}
|
||||||
|
{showGenerator && (
|
||||||
|
<PromptGenerator
|
||||||
|
onGenerated={handleGeneratorResult}
|
||||||
|
onClose={() => setShowGenerator(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
189
frontend/src/components/PromptGenerator.jsx
Normal file
189
frontend/src/components/PromptGenerator.jsx
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { api } from '../utils/api'
|
||||||
|
|
||||||
|
export default function PromptGenerator({ onGenerated, onClose }) {
|
||||||
|
const [goal, setGoal] = useState('')
|
||||||
|
const [dataCategories, setDataCategories] = useState(['körper', 'ernährung'])
|
||||||
|
const [exampleOutput, setExampleOutput] = useState('')
|
||||||
|
const [exampleData, setExampleData] = useState(null)
|
||||||
|
const [generating, setGenerating] = useState(false)
|
||||||
|
const [loadingExample, setLoadingExample] = useState(false)
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ id: 'körper', label: 'Körper (Gewicht, KF, Umfänge)' },
|
||||||
|
{ id: 'ernährung', label: 'Ernährung (Kalorien, Makros)' },
|
||||||
|
{ id: 'training', label: 'Training (Volumen, Typen)' },
|
||||||
|
{ id: 'schlaf', label: 'Schlaf (Dauer, Qualität)' },
|
||||||
|
{ id: 'vitalwerte', label: 'Vitalwerte (RHR, HRV, VO2max)' },
|
||||||
|
{ id: 'ziele', label: 'Ziele (Fortschritt, Prognose)' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleToggleCategory = (catId) => {
|
||||||
|
if (dataCategories.includes(catId)) {
|
||||||
|
setDataCategories(dataCategories.filter(c => c !== catId))
|
||||||
|
} else {
|
||||||
|
setDataCategories([...dataCategories, catId])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShowExampleData = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingExample(true)
|
||||||
|
const placeholders = await api.listPlaceholders()
|
||||||
|
setExampleData(placeholders)
|
||||||
|
} catch (e) {
|
||||||
|
alert('Fehler: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
setLoadingExample(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGenerate = async () => {
|
||||||
|
if (!goal.trim()) {
|
||||||
|
alert('Bitte Ziel beschreiben')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (dataCategories.length === 0) {
|
||||||
|
alert('Bitte mindestens einen Datenbereich wählen')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setGenerating(true)
|
||||||
|
const result = await api.generatePrompt({
|
||||||
|
goal,
|
||||||
|
data_categories: dataCategories,
|
||||||
|
example_output: exampleOutput || null
|
||||||
|
})
|
||||||
|
onGenerated(result)
|
||||||
|
} catch (e) {
|
||||||
|
alert('Fehler beim Generieren: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
setGenerating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position:'fixed', inset:0, background:'rgba(0,0,0,0.6)',
|
||||||
|
display:'flex', alignItems:'center', justifyContent:'center',
|
||||||
|
zIndex:1001, padding:20
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background:'var(--bg)', borderRadius:12, maxWidth:700, width:'100%',
|
||||||
|
maxHeight:'90vh', overflow:'auto', padding:24
|
||||||
|
}}>
|
||||||
|
<h2 style={{margin:'0 0 24px 0', fontSize:18, fontWeight:600}}>
|
||||||
|
🤖 KI-Prompt generieren
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Step 1: Goal */}
|
||||||
|
<div style={{marginBottom:24}}>
|
||||||
|
<label className="form-label">
|
||||||
|
1️⃣ Was möchtest du analysieren?
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={goal}
|
||||||
|
onChange={e => setGoal(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
placeholder="Beispiel: Ich möchte wissen ob meine Proteinzufuhr ausreichend ist für Muskelaufbau und wie ich sie optimieren kann."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 2: Data Categories */}
|
||||||
|
<div style={{marginBottom:24}}>
|
||||||
|
<label className="form-label">
|
||||||
|
2️⃣ Welche Daten sollen analysiert werden?
|
||||||
|
</label>
|
||||||
|
<div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:8}}>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<label
|
||||||
|
key={cat.id}
|
||||||
|
style={{
|
||||||
|
display:'flex', alignItems:'center', gap:8,
|
||||||
|
padding:8, background:'var(--surface)', borderRadius:6,
|
||||||
|
cursor:'pointer', fontSize:13
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={dataCategories.includes(cat.id)}
|
||||||
|
onChange={() => handleToggleCategory(cat.id)}
|
||||||
|
/>
|
||||||
|
{cat.label}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleShowExampleData}
|
||||||
|
disabled={loadingExample}
|
||||||
|
style={{
|
||||||
|
marginTop:12, fontSize:12, padding:'6px 12px',
|
||||||
|
background:'var(--surface2)', border:'1px solid var(--border)',
|
||||||
|
borderRadius:6, cursor:'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loadingExample ? 'Lädt...' : '📊 Beispieldaten anzeigen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Example Data */}
|
||||||
|
{exampleData && (
|
||||||
|
<div style={{
|
||||||
|
marginBottom:24, padding:12, background:'var(--surface2)',
|
||||||
|
borderRadius:8, fontSize:11, fontFamily:'monospace',
|
||||||
|
maxHeight:200, overflow:'auto'
|
||||||
|
}}>
|
||||||
|
<strong style={{fontFamily:'var(--font)'}}>Deine aktuellen Daten:</strong>
|
||||||
|
<pre style={{marginTop:8, whiteSpace:'pre-wrap'}}>
|
||||||
|
{JSON.stringify(exampleData, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 3: Desired Format */}
|
||||||
|
<div style={{marginBottom:24}}>
|
||||||
|
<label className="form-label">
|
||||||
|
3️⃣ Gewünschtes Antwort-Format (optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
value={exampleOutput}
|
||||||
|
onChange={e => setExampleOutput(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
placeholder={'Beispiel:\n## Analyse\n[Bewertung]\n\n## Empfehlungen\n- Punkt 1\n- Punkt 2'}
|
||||||
|
style={{fontFamily:'monospace', fontSize:12}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info Box */}
|
||||||
|
<div style={{
|
||||||
|
marginBottom:24, padding:12, background:'var(--surface)',
|
||||||
|
border:'1px solid var(--border)', borderRadius:8, fontSize:12,
|
||||||
|
color:'var(--text3)'
|
||||||
|
}}>
|
||||||
|
<strong style={{color:'var(--text2)'}}>💡 Tipp:</strong> Je präziser deine Zielbeschreibung,
|
||||||
|
desto besser der generierte Prompt. Die KI wählt automatisch passende Platzhalter
|
||||||
|
und strukturiert die Analyse optimal.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div style={{display:'flex', gap:12}}>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={generating || !goal.trim() || dataCategories.length === 0}
|
||||||
|
style={{flex:1}}
|
||||||
|
>
|
||||||
|
{generating ? '⏳ Generiere...' : '🚀 Prompt generieren'}
|
||||||
|
</button>
|
||||||
|
<button className="btn" onClick={onClose}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -451,6 +451,23 @@ export default function AdminPanel() {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* KI-Prompts Section */}
|
||||||
|
<div className="card">
|
||||||
|
<div style={{fontWeight:700,fontSize:14,marginBottom:12,display:'flex',alignItems:'center',gap:6}}>
|
||||||
|
<Settings size={16} color="var(--accent)"/> KI-Prompts (v9f)
|
||||||
|
</div>
|
||||||
|
<div style={{fontSize:12,color:'var(--text3)',marginBottom:12,lineHeight:1.5}}>
|
||||||
|
Verwalte AI-Prompts mit KI-Unterstützung: Generiere, optimiere und organisiere Prompts.
|
||||||
|
</div>
|
||||||
|
<div style={{display:'grid',gap:8}}>
|
||||||
|
<Link to="/admin/prompts">
|
||||||
|
<button className="btn btn-secondary btn-full">
|
||||||
|
🤖 KI-Prompts verwalten
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -282,4 +282,16 @@ export const api = {
|
||||||
fd.append('file', file)
|
fd.append('file', file)
|
||||||
return req('/blood-pressure/import/omron', {method:'POST', body:fd})
|
return req('/blood-pressure/import/omron', {method:'POST', body:fd})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// AI Prompts Management (Issue #28)
|
||||||
|
listAdminPrompts: () => req('/prompts'),
|
||||||
|
createPrompt: (d) => req('/prompts', json(d)),
|
||||||
|
updatePrompt: (id,d) => req(`/prompts/${id}`, jput(d)),
|
||||||
|
deletePrompt: (id) => req(`/prompts/${id}`, {method:'DELETE'}),
|
||||||
|
duplicatePrompt: (id) => req(`/prompts/${id}/duplicate`, json({})),
|
||||||
|
reorderPrompts: (order) => req('/prompts/reorder', jput(order)),
|
||||||
|
previewPrompt: (tpl) => req('/prompts/preview', json({template:tpl})),
|
||||||
|
generatePrompt: (d) => req('/prompts/generate', json(d)),
|
||||||
|
optimizePrompt: (id) => req(`/prompts/${id}/optimize`, json({})),
|
||||||
|
listPlaceholders: () => req('/prompts/placeholders'),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user