CG-Feedback-Monitor/src/pages/AiPromptConfig.tsx
2026-07-07 11:14:00 +02:00

117 lines
5.0 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
import {
type FeedbackDimension, type FeedbackDimensionPrompt,
loadFeedbackStructure, listFeedbackDimensionPrompts, updateFeedbackDimensionPrompt,
} from '../db'
import { AI_CONFIG } from '../config/constants'
export default function AiPromptConfig() {
const [dimensions, setDimensions] = useState<FeedbackDimension[]>([])
const [prompts, setPrompts] = useState<FeedbackDimensionPrompt[]>([])
const [expanded, setExpanded] = useState<Set<number>>(new Set())
const [savedDimId, setSavedDimId] = useState<number | null>(null)
const saveTimer = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map())
const load = async () => {
const [structure, promptRows] = await Promise.all([
loadFeedbackStructure(),
listFeedbackDimensionPrompts(),
])
setDimensions([...structure.dimensions].sort((a, b) => a.order - b.order))
setPrompts(promptRows)
}
useEffect(() => { load() }, [])
const getPrompt = (dimId: number) => prompts.find(p => p.dimensionId === dimId)
const saveTemplate = (dimId: number, value: string) => {
setPrompts(prev => prev.map(p => p.dimensionId === dimId ? { ...p, promptTemplate: value } : p))
const prevTimer = saveTimer.current.get(dimId)
if (prevTimer) clearTimeout(prevTimer)
saveTimer.current.set(dimId, setTimeout(async () => {
const existing = getPrompt(dimId)
if (!existing?.id) return
await updateFeedbackDimensionPrompt(existing.id, { promptTemplate: value })
setSavedDimId(dimId)
setTimeout(() => setSavedDimId(null), 2000)
}, 600))
}
const resetToDefault = async (dimId: number) => {
const existing = getPrompt(dimId)
if (!existing?.id) return
await updateFeedbackDimensionPrompt(existing.id, { promptTemplate: AI_CONFIG.defaultDimensionPromptTemplate })
setPrompts(prev => prev.map(p => p.dimensionId === dimId ? { ...p, promptTemplate: AI_CONFIG.defaultDimensionPromptTemplate } : p))
}
return (
<div className="space-y-4">
<div>
<h2 className="text-lg font-bold text-gray-800">KI-Prompts je Dimension</h2>
<p className="text-sm text-gray-500 mt-1">
Jede Dimension bekommt bei der KI-Feedback-Generierung einen eigenen, separaten Call. Der Prompt-Text
ist frei editierbar und wird bei neuen Dimensionen automatisch mit einem Standard-Prompt vorbelegt.
</p>
</div>
{/* Platzhalter-Legende */}
<div className="bg-gray-50 rounded-xl border border-gray-100 p-4 space-y-2">
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Verfügbare Platzhalter</div>
<div className="grid gap-1.5">
{AI_CONFIG.dimensionPromptPlaceholders.map(p => (
<div key={p.token} className="text-xs text-gray-600 flex gap-2">
<code className="bg-white border border-gray-200 rounded px-1.5 py-0.5 text-brand font-mono flex-shrink-0">{p.token}</code>
<span>{p.description}</span>
</div>
))}
</div>
</div>
{/* Dimensionen */}
{dimensions.map(dim => {
const isOpen = expanded.has(dim.id!)
const prompt = getPrompt(dim.id!)
return (
<div key={dim.id} className="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<button
onClick={() => setExpanded(prev => {
const next = new Set(prev)
if (next.has(dim.id!)) next.delete(dim.id!); else next.add(dim.id!)
return next
})}
className="w-full flex items-center justify-between p-3 bg-gray-50 border-b border-gray-100 text-left">
<span className="font-semibold text-sm text-gray-800">{dim.name}</span>
<span className="text-xs text-gray-400">{isOpen ? '▲' : '▼'}</span>
</button>
{isOpen && (
<div className="p-3 space-y-2">
<textarea
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-xs font-mono focus:outline-none focus:border-brand min-h-[320px]"
value={prompt?.promptTemplate ?? ''}
onChange={e => saveTemplate(dim.id!, e.target.value)}
/>
<div className="flex items-center gap-2">
<button onClick={() => resetToDefault(dim.id!)}
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-600 px-3 py-1.5 rounded-lg font-medium">
Auf Standard zurücksetzen
</button>
{savedDimId === dim.id && (
<span className="text-xs text-green-600 font-medium">Gespeichert.</span>
)}
</div>
</div>
)}
</div>
)
})}
{dimensions.length === 0 && (
<div className="text-sm text-gray-400 text-center py-6">
Noch keine Dimensionen angelegt siehe Tab Feedback".
</div>
)}
</div>
)
}