2026-07-07 11:14:00 +02:00
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
|
|
|
|
import {
|
|
|
|
|
|
type FeedbackDimension, type FeedbackDimensionPrompt,
|
|
|
|
|
|
loadFeedbackStructure, listFeedbackDimensionPrompts, updateFeedbackDimensionPrompt,
|
2026-08-08 17:35:05 +02:00
|
|
|
|
resetAllDimensionPromptsToDefault,
|
|
|
|
|
|
getOrSeedDefaultDimensionPromptTemplate, saveDefaultDimensionPromptTemplate,
|
|
|
|
|
|
getOrSeedDefaultCategoryPromptTemplate, saveDefaultCategoryPromptTemplate,
|
|
|
|
|
|
getOrSeedCriterionTaggingPromptTemplate, saveCriterionTaggingPromptTemplate,
|
2026-09-16 14:17:25 +02:00
|
|
|
|
getOrSeedChecklistTaggingPromptTemplate, saveChecklistTaggingPromptTemplate,
|
2026-07-07 11:14:00 +02:00
|
|
|
|
} from '../db'
|
|
|
|
|
|
import { AI_CONFIG } from '../config/constants'
|
2026-08-08 17:35:05 +02:00
|
|
|
|
import { getAiSettings, saveAiSettings } from '../utils/aiClient'
|
2026-07-07 11:14:00 +02:00
|
|
|
|
|
|
|
|
|
|
export default function AiPromptConfig() {
|
|
|
|
|
|
const [dimensions, setDimensions] = useState<FeedbackDimension[]>([])
|
|
|
|
|
|
const [prompts, setPrompts] = useState<FeedbackDimensionPrompt[]>([])
|
2026-08-08 17:35:05 +02:00
|
|
|
|
const [defaultTemplate, setDefaultTemplate] = useState('')
|
|
|
|
|
|
const [defaultExpanded, setDefaultExpanded] = useState(false)
|
|
|
|
|
|
const [defaultSaved, setDefaultSaved] = useState(false)
|
|
|
|
|
|
const [categoryTemplate, setCategoryTemplate] = useState('')
|
|
|
|
|
|
const [categoryExpanded, setCategoryExpanded] = useState(false)
|
|
|
|
|
|
const [categorySaved, setCategorySaved] = useState(false)
|
|
|
|
|
|
const [taggingTemplate, setTaggingTemplate] = useState('')
|
|
|
|
|
|
const [taggingExpanded, setTaggingExpanded] = useState(false)
|
|
|
|
|
|
const [taggingSaved, setTaggingSaved] = useState(false)
|
2026-09-16 14:17:25 +02:00
|
|
|
|
const [checklistTaggingTemplate, setChecklistTaggingTemplate] = useState('')
|
|
|
|
|
|
const [checklistTaggingExpanded, setChecklistTaggingExpanded] = useState(false)
|
|
|
|
|
|
const [checklistTaggingSaved, setChecklistTaggingSaved] = useState(false)
|
2026-07-07 11:14:00 +02:00
|
|
|
|
const [expanded, setExpanded] = useState<Set<number>>(new Set())
|
|
|
|
|
|
const [savedDimId, setSavedDimId] = useState<number | null>(null)
|
2026-08-08 17:35:05 +02:00
|
|
|
|
const [bulkResetMsg, setBulkResetMsg] = useState('')
|
2026-09-16 14:17:25 +02:00
|
|
|
|
const saveTimer = useRef<Map<number | 'default' | 'category' | 'tagging' | 'checklistTagging', ReturnType<typeof setTimeout>>>(new Map())
|
2026-08-08 17:35:05 +02:00
|
|
|
|
const [aiKey, setAiKey] = useState(getAiSettings().apiKey)
|
|
|
|
|
|
const [aiModel, setAiModel] = useState(getAiSettings().model)
|
|
|
|
|
|
const [aiSaved, setAiSaved] = useState(false)
|
2026-07-07 11:14:00 +02:00
|
|
|
|
|
|
|
|
|
|
const load = async () => {
|
2026-09-16 14:17:25 +02:00
|
|
|
|
const [structure, promptRows, defTemplate, catTemplate, taggingTpl, checklistTaggingTpl] = await Promise.all([
|
2026-07-07 11:14:00 +02:00
|
|
|
|
loadFeedbackStructure(),
|
|
|
|
|
|
listFeedbackDimensionPrompts(),
|
2026-08-08 17:35:05 +02:00
|
|
|
|
getOrSeedDefaultDimensionPromptTemplate(),
|
|
|
|
|
|
getOrSeedDefaultCategoryPromptTemplate(),
|
|
|
|
|
|
getOrSeedCriterionTaggingPromptTemplate(),
|
2026-09-16 14:17:25 +02:00
|
|
|
|
getOrSeedChecklistTaggingPromptTemplate(),
|
2026-07-07 11:14:00 +02:00
|
|
|
|
])
|
|
|
|
|
|
setDimensions([...structure.dimensions].sort((a, b) => a.order - b.order))
|
|
|
|
|
|
setPrompts(promptRows)
|
2026-08-08 17:35:05 +02:00
|
|
|
|
setDefaultTemplate(defTemplate)
|
|
|
|
|
|
setCategoryTemplate(catTemplate)
|
|
|
|
|
|
setTaggingTemplate(taggingTpl)
|
2026-09-16 14:17:25 +02:00
|
|
|
|
setChecklistTaggingTemplate(checklistTaggingTpl)
|
2026-07-07 11:14:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-08-08 17:35:05 +02:00
|
|
|
|
await updateFeedbackDimensionPrompt(existing.id, { promptTemplate: defaultTemplate })
|
|
|
|
|
|
setPrompts(prev => prev.map(p => p.dimensionId === dimId ? { ...p, promptTemplate: defaultTemplate } : p))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const resetAllToDefault = async () => {
|
|
|
|
|
|
if (!confirm('Alle Dimension-Prompts wirklich auf den Standard (1–10) setzen? Individuelle Anpassungen gehen verloren.')) return
|
|
|
|
|
|
const { updated } = await resetAllDimensionPromptsToDefault()
|
|
|
|
|
|
const promptRows = await listFeedbackDimensionPrompts()
|
|
|
|
|
|
setPrompts(promptRows)
|
|
|
|
|
|
setBulkResetMsg(`${updated} Dimension-Prompt(s) auf Standard gesetzt.`)
|
|
|
|
|
|
setTimeout(() => setBulkResetMsg(''), 4000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const saveDefaultTemplate = (value: string) => {
|
|
|
|
|
|
setDefaultTemplate(value)
|
|
|
|
|
|
const prevTimer = saveTimer.current.get('default')
|
|
|
|
|
|
if (prevTimer) clearTimeout(prevTimer)
|
|
|
|
|
|
saveTimer.current.set('default', setTimeout(async () => {
|
|
|
|
|
|
await saveDefaultDimensionPromptTemplate(value)
|
|
|
|
|
|
setDefaultSaved(true)
|
|
|
|
|
|
setTimeout(() => setDefaultSaved(false), 2000)
|
|
|
|
|
|
}, 600))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const saveCategoryTemplate = (value: string) => {
|
|
|
|
|
|
setCategoryTemplate(value)
|
|
|
|
|
|
const prevTimer = saveTimer.current.get('category')
|
|
|
|
|
|
if (prevTimer) clearTimeout(prevTimer)
|
|
|
|
|
|
saveTimer.current.set('category', setTimeout(async () => {
|
|
|
|
|
|
await saveDefaultCategoryPromptTemplate(value)
|
|
|
|
|
|
setCategorySaved(true)
|
|
|
|
|
|
setTimeout(() => setCategorySaved(false), 2000)
|
|
|
|
|
|
}, 600))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 14:17:25 +02:00
|
|
|
|
const resetTaggingToDefault = async () => {
|
|
|
|
|
|
const def = AI_CONFIG.defaultCriterionTaggingPromptTemplate
|
|
|
|
|
|
await saveCriterionTaggingPromptTemplate(def)
|
|
|
|
|
|
setTaggingTemplate(def)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const resetChecklistTaggingToDefault = async () => {
|
|
|
|
|
|
const def = AI_CONFIG.defaultChecklistTaggingPromptTemplate
|
|
|
|
|
|
await saveChecklistTaggingPromptTemplate(def)
|
|
|
|
|
|
setChecklistTaggingTemplate(def)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-08 17:35:05 +02:00
|
|
|
|
const saveTaggingTemplate = (value: string) => {
|
|
|
|
|
|
setTaggingTemplate(value)
|
|
|
|
|
|
const prevTimer = saveTimer.current.get('tagging')
|
|
|
|
|
|
if (prevTimer) clearTimeout(prevTimer)
|
|
|
|
|
|
saveTimer.current.set('tagging', setTimeout(async () => {
|
|
|
|
|
|
await saveCriterionTaggingPromptTemplate(value)
|
|
|
|
|
|
setTaggingSaved(true)
|
|
|
|
|
|
setTimeout(() => setTaggingSaved(false), 2000)
|
|
|
|
|
|
}, 600))
|
2026-07-07 11:14:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 14:17:25 +02:00
|
|
|
|
const saveChecklistTaggingTemplate = (value: string) => {
|
|
|
|
|
|
setChecklistTaggingTemplate(value)
|
|
|
|
|
|
const prevTimer = saveTimer.current.get('checklistTagging')
|
|
|
|
|
|
if (prevTimer) clearTimeout(prevTimer)
|
|
|
|
|
|
saveTimer.current.set('checklistTagging', setTimeout(async () => {
|
|
|
|
|
|
await saveChecklistTaggingPromptTemplate(value)
|
|
|
|
|
|
setChecklistTaggingSaved(true)
|
|
|
|
|
|
setTimeout(() => setChecklistTaggingSaved(false), 2000)
|
|
|
|
|
|
}, 600))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 11:14:00 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div>
|
2026-08-08 17:35:05 +02:00
|
|
|
|
<h2 className="text-lg font-bold text-gray-800">KI-Feedback (Kaskade)</h2>
|
2026-07-07 11:14:00 +02:00
|
|
|
|
<p className="text-sm text-gray-500 mt-1">
|
2026-08-08 17:35:05 +02:00
|
|
|
|
Stufe A: ein Call pro Kategorie (globaler Kategorie-Prompt). Stufe B: ein Call pro Dimension
|
|
|
|
|
|
(Verdichtung aus Kategorie-Ergebnissen — pro Dimension editierbar unten).
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="bg-white rounded-xl border border-blue-100 shadow-sm p-4 space-y-3">
|
|
|
|
|
|
<div className="text-sm font-semibold text-blue-800">OpenRouter API-Key</div>
|
|
|
|
|
|
<p className="text-xs text-gray-500">
|
|
|
|
|
|
Wird lokal im Browser gespeichert (<span className="font-mono">localStorage</span>) — nötig für
|
|
|
|
|
|
KI-Feedback und „Vorschläge analysieren". Key von{' '}
|
|
|
|
|
|
<span className="font-mono">openrouter.ai</span>.
|
2026-07-07 11:14:00 +02:00
|
|
|
|
</p>
|
2026-08-08 17:35:05 +02:00
|
|
|
|
<input
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
className="w-full border border-blue-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
|
|
|
|
|
|
placeholder="sk-or-..."
|
|
|
|
|
|
value={aiKey}
|
|
|
|
|
|
onChange={e => setAiKey(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<select
|
|
|
|
|
|
className="w-full border border-blue-200 rounded-lg px-3 py-2 text-sm bg-white"
|
|
|
|
|
|
value={aiModel}
|
|
|
|
|
|
onChange={e => setAiModel(e.target.value)}>
|
|
|
|
|
|
<option value="openai/gpt-4o-mini">GPT-4o mini (günstig)</option>
|
|
|
|
|
|
<option value="openai/gpt-4o">GPT-4o</option>
|
|
|
|
|
|
<option value="anthropic/claude-sonnet-4-5">Claude Sonnet 4.5</option>
|
|
|
|
|
|
<option value="anthropic/claude-haiku-4-5">Claude Haiku 4.5 (schnell)</option>
|
|
|
|
|
|
<option value="google/gemini-flash-1.5">Gemini Flash 1.5</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
saveAiSettings(aiKey, aiModel)
|
|
|
|
|
|
setAiSaved(true)
|
|
|
|
|
|
setTimeout(() => setAiSaved(false), 2000)
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="w-full py-2.5 bg-brand text-white rounded-xl text-sm font-semibold">
|
|
|
|
|
|
API-Key speichern
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{aiSaved && (
|
|
|
|
|
|
<div className="text-xs text-green-700 bg-green-50 border border-green-200 rounded-lg px-3 py-2 text-center">
|
|
|
|
|
|
Gespeichert.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Kategorie-Prompt Stufe A */}
|
|
|
|
|
|
<div className="bg-white rounded-xl border border-green-100 shadow-sm overflow-hidden">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setCategoryExpanded(p => !p)}
|
|
|
|
|
|
className="w-full flex items-center justify-between p-3 bg-green-50 border-b border-green-100 text-left">
|
|
|
|
|
|
<span className="font-semibold text-sm text-green-800">Stufe A — Kategorie-Prompt (1 Call pro Kategorie)</span>
|
|
|
|
|
|
<span className="text-xs text-green-400">{categoryExpanded ? '▲' : '▼'}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{categoryExpanded && (
|
|
|
|
|
|
<div className="p-3 space-y-2">
|
|
|
|
|
|
<p className="text-xs text-gray-500">
|
|
|
|
|
|
Wird für jede Kategorie einzeln aufgerufen. Der Trend-Vorschlag aus Meetings wird im Prompt
|
|
|
|
|
|
vorgegeben; die KI bestätigt oder passt maximal ±1 an.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<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-[280px]"
|
|
|
|
|
|
value={categoryTemplate}
|
|
|
|
|
|
onChange={e => saveCategoryTemplate(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{categorySaved && <span className="text-xs text-green-600 font-medium">Gespeichert.</span>}
|
|
|
|
|
|
<div className="grid gap-1.5 pt-2">
|
|
|
|
|
|
{AI_CONFIG.categoryPromptPlaceholders.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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Standard-Prompt Stufe B */}
|
|
|
|
|
|
<div className="bg-white rounded-xl border border-blue-100 shadow-sm overflow-hidden">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setDefaultExpanded(p => !p)}
|
|
|
|
|
|
className="w-full flex items-center justify-between p-3 bg-blue-50 border-b border-blue-100 text-left">
|
|
|
|
|
|
<span className="font-semibold text-sm text-blue-800">Stufe B — Dimensions-Verdichtung (Basis für neue Dimensionen & „Zurücksetzen")</span>
|
|
|
|
|
|
<span className="text-xs text-blue-400">{defaultExpanded ? '▲' : '▼'}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{defaultExpanded && (
|
|
|
|
|
|
<div className="p-3 space-y-2">
|
|
|
|
|
|
<p className="text-xs text-gray-500">
|
|
|
|
|
|
Input sind nur die Kategorie-Ergebnisse aus Stufe A — kein Rohprotokoll. Wird beim Anlegen
|
|
|
|
|
|
neuer Dimensionen automatisch übernommen und ist die Basis für „Auf Standard zurücksetzen".
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<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={defaultTemplate}
|
|
|
|
|
|
onChange={e => saveDefaultTemplate(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{defaultSaved && <span className="text-xs text-green-600 font-medium">Gespeichert.</span>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-3 flex-wrap">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={resetAllToDefault}
|
|
|
|
|
|
className="text-xs bg-amber-50 hover:bg-amber-100 text-amber-800 border border-amber-200 px-3 py-1.5 rounded-lg font-medium">
|
|
|
|
|
|
Alle Dimension-Prompts auf Standard (Stufe B) setzen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{bulkResetMsg && <span className="text-xs text-green-600 font-medium">{bulkResetMsg}</span>}
|
2026-07-07 11:14:00 +02:00
|
|
|
|
</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>
|
|
|
|
|
|
))}
|
2026-08-08 17:35:05 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
2026-09-16 14:17:25 +02:00
|
|
|
|
<h2 className="text-lg font-bold text-gray-800">KI-Prompts: Kriterien-Zuordnungsvorschläge</h2>
|
2026-08-08 17:35:05 +02:00
|
|
|
|
<p className="text-sm text-gray-500 mt-1">
|
2026-09-16 14:17:25 +02:00
|
|
|
|
„🏷 Vorschläge analysieren" im Meeting löst <strong>zwei getrennte KI-Calls</strong> aus: Protokoll-Freitext
|
|
|
|
|
|
(Antwortformat <code className="font-mono text-xs">ZEILE n:</code>) und Checklistenpunkte
|
|
|
|
|
|
(<code className="font-mono text-xs">PUNKT n:</code>).
|
2026-08-08 17:35:05 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="bg-white rounded-xl border border-indigo-100 shadow-sm overflow-hidden">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setTaggingExpanded(p => !p)}
|
|
|
|
|
|
className="w-full flex items-center justify-between p-3 bg-indigo-50 border-b border-indigo-100 text-left">
|
2026-09-16 14:17:25 +02:00
|
|
|
|
<span className="font-semibold text-sm text-indigo-800">Protokoll — Tagging-Prompt</span>
|
2026-08-08 17:35:05 +02:00
|
|
|
|
<span className="text-xs text-indigo-400">{taggingExpanded ? '▲' : '▼'}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{taggingExpanded && (
|
|
|
|
|
|
<div className="p-3 space-y-2">
|
2026-09-16 14:17:25 +02:00
|
|
|
|
<p className="text-xs text-gray-500">Freitext-Zeilen aus Gesprächsprotokoll und allgemeinen Notizen.</p>
|
2026-08-08 17:35:05 +02:00
|
|
|
|
<textarea
|
2026-09-16 14:17:25 +02:00
|
|
|
|
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-[280px]"
|
2026-08-08 17:35:05 +02:00
|
|
|
|
value={taggingTemplate}
|
|
|
|
|
|
onChange={e => saveTaggingTemplate(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{taggingSaved && <span className="text-xs text-green-600 font-medium">Gespeichert.</span>}
|
2026-09-16 14:17:25 +02:00
|
|
|
|
<button onClick={resetTaggingToDefault}
|
|
|
|
|
|
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-600 px-3 py-1.5 rounded-lg font-medium">
|
|
|
|
|
|
Protokoll-Tagging auf Standard zurücksetzen
|
|
|
|
|
|
</button>
|
2026-08-08 17:35:05 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="bg-gray-50 rounded-xl border border-gray-100 p-4 space-y-2">
|
2026-09-16 14:17:25 +02:00
|
|
|
|
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Platzhalter Protokoll-Tagging</div>
|
2026-08-08 17:35:05 +02:00
|
|
|
|
<div className="grid gap-1.5">
|
|
|
|
|
|
{AI_CONFIG.criterionTaggingPromptPlaceholders.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>
|
|
|
|
|
|
))}
|
2026-09-16 14:17:25 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="bg-white rounded-xl border border-teal-100 shadow-sm overflow-hidden">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setChecklistTaggingExpanded(p => !p)}
|
|
|
|
|
|
className="w-full flex items-center justify-between p-3 bg-teal-50 border-b border-teal-100 text-left">
|
|
|
|
|
|
<span className="font-semibold text-sm text-teal-800">Checkliste — Tagging-Prompt</span>
|
|
|
|
|
|
<span className="text-xs text-teal-400">{checklistTaggingExpanded ? '▲' : '▼'}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{checklistTaggingExpanded && (
|
|
|
|
|
|
<div className="p-3 space-y-2">
|
|
|
|
|
|
<p className="text-xs text-gray-500">
|
|
|
|
|
|
Strukturierte Prüfpunkte — nur Label (kein Score). Antwortformat: <code className="font-mono">PUNKT n:</code>.
|
|
|
|
|
|
In Config → Ass.-Typen als „Standard-Zuordnung (KI)" nutzbar; spart Meeting-Calls.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<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-[280px]"
|
|
|
|
|
|
value={checklistTaggingTemplate}
|
|
|
|
|
|
onChange={e => saveChecklistTaggingTemplate(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{checklistTaggingSaved && <span className="text-xs text-green-600 font-medium">Gespeichert.</span>}
|
|
|
|
|
|
<button onClick={resetChecklistTaggingToDefault}
|
|
|
|
|
|
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-600 px-3 py-1.5 rounded-lg font-medium">
|
|
|
|
|
|
Checklisten-Tagging auf Standard zurücksetzen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<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">Platzhalter Checklisten-Tagging</div>
|
|
|
|
|
|
<div className="grid gap-1.5">
|
|
|
|
|
|
{AI_CONFIG.checklistTaggingPromptPlaceholders.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>
|
|
|
|
|
|
))}
|
2026-07-07 11:14:00 +02:00
|
|
|
|
</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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|