import { useCallback, useEffect, useState } from 'react' import { Navigate } from 'react-router-dom' import { Sparkles } from 'lucide-react' import { useAuth } from '../context/AuthContext' import api from '../utils/api' import AdminPageNav from '../components/AdminPageNav' /** * Pflege von ai_prompts (OpenRouter-Vorlagen) — nur Superadmin. */ export default function AdminAiPromptsPage() { const { user } = useAuth() const isSuperadmin = user?.role === 'superadmin' const [prompts, setPrompts] = useState([]) const [catalog, setCatalog] = useState(null) const [selectedId, setSelectedId] = useState(null) const [detail, setDetail] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState('') const [draftName, setDraftName] = useState('') const [draftDesc, setDraftDesc] = useState('') const [draftTemplate, setDraftTemplate] = useState('') const [draftOpenrouterModel, setDraftOpenrouterModel] = useState('') const [draftActive, setDraftActive] = useState(true) const [pvTitle, setPvTitle] = useState('Testübung') const [pvGoal, setPvGoal] = useState('

Ziel hier

') const [pvExec, setPvExec] = useState('

Ablauf hier

') const [pvHint, setPvHint] = useState('') const [pvFocusId, setPvFocusId] = useState('') const [pvGoalQuery, setPvGoalQuery] = useState( 'Mae Geri vom Grundschritt bis zur kontrollierten Kumite-Nähe' ) const [pvUserNotes, setPvUserNotes] = useState('Fokus Breitensport, ohne Wettkampfdruck.') const [pvMaxSteps, setPvMaxSteps] = useState('5') const [pvSearchQuery, setPvSearchQuery] = useState('') const [pvPreview, setPvPreview] = useState(null) const selectedSlug = (detail?.slug || '').trim().toLowerCase() const isExercisePreviewSlug = [ 'exercise_summary', 'exercise_skill_suggestions', 'exercise_instruction_rewrite', ].includes(selectedSlug) const isPlanningPreviewSlug = selectedSlug.startsWith('planning_') const loadList = useCallback(async () => { const [pList, cat] = await Promise.all([ api.listAdminAiPrompts(), api.getAdminAiPromptPlaceholdersCatalog(), ]) setPrompts(Array.isArray(pList) ? pList : []) setCatalog(cat || null) }, []) useEffect(() => { if (!isSuperadmin) return let cancelled = false ;(async () => { setLoading(true) setError('') try { await loadList() } catch (e) { if (!cancelled) setError(e.message || String(e)) } finally { if (!cancelled) setLoading(false) } })() return () => { cancelled = true } }, [isSuperadmin, loadList]) useEffect(() => { if (!isSuperadmin || !selectedId) { setDetail(null) return } let cancelled = false ;(async () => { try { const d = await api.getAdminAiPrompt(selectedId) if (!cancelled) { setDetail(d) setDraftName(d.display_name || '') setDraftDesc(d.description || '') setDraftTemplate(d.template || '') setDraftOpenrouterModel( typeof d.openrouter_model === 'string' ? d.openrouter_model : '' ) setDraftActive(!!d.active) setPvPreview(null) } } catch (e) { if (!cancelled) setError(e.message || String(e)) } })() return () => { cancelled = true } }, [isSuperadmin, selectedId]) const save = async () => { if (!detail?.id) return setSaving(true) setError('') try { await api.updateAdminAiPrompt(detail.id, { template: draftTemplate, display_name: draftName, description: draftDesc, active: draftActive, openrouter_model: draftOpenrouterModel.trim(), }) await loadList() const nd = await api.getAdminAiPrompt(detail.id) setDetail(nd) setPvPreview(null) } catch (e) { setError(e.message || String(e)) } finally { setSaving(false) } } const resetTemplate = async () => { if (!detail?.id || !detail.has_reference_template) return if (!confirm('Template auf gespeicherten Referenztext zurücksetzen?')) return setSaving(true) try { const nd = await api.resetAdminAiPromptTemplate(detail.id) setDetail(nd) setDraftTemplate(nd.template || '') await loadList() } catch (e) { setError(e.message || String(e)) } finally { setSaving(false) } } const runPreview = async () => { if (!detail?.id) return setError('') try { const body = {} if (isPlanningPreviewSlug) { body.goal_query = pvGoalQuery.trim() || undefined body.user_notes = pvUserNotes.trim() || undefined const ms = parseInt(String(pvMaxSteps).trim(), 10) if (Number.isFinite(ms) && ms >= 2 && ms <= 10) body.max_steps = ms const sq = pvSearchQuery.trim() if (sq) body.search_query = sq } else if (isExercisePreviewSlug) { body.title = pvTitle body.goal = pvGoal body.execution = pvExec body.focus_hint = pvHint || undefined const fid = parseInt(String(pvFocusId).trim(), 10) if (Number.isFinite(fid) && fid >= 1) { body.focus_areas_context = [{ focus_area_id: fid, is_primary: true }] } } const r = await api.previewAdminAiPrompt(detail.id, body) setPvPreview(r) } catch (e) { setError(e.message || String(e)) } } if (!isSuperadmin) return if (loading) { return (
) } return (

KI Prompts

Datenbankvorlagen (ai_prompts) für Übungs- und Planungs-KI. Platzhalter im Mustache-Stil werden serverseitig aufgelöst — die Vorschau unten ruft kein externes Modell auf.

{error ?

{error}

: null}
Prompts
    {(prompts || []).map((p) => (
  • ))}
{!selectedId ? (

Prompt links wählen.

) : (
slug: {detail?.slug} · Ausgabe:{' '} {detail?.output_format} · Kategorie: {detail?.category}
setDraftName(e.target.value)} />