117 lines
5.0 KiB
TypeScript
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>
|
|
)
|
|
}
|