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-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-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('')
|
|
|
|
|
|
const saveTimer = useRef<Map<number | 'default' | 'category' | 'tagging', ReturnType<typeof setTimeout>>>(new Map())
|
|
|
|
|
|
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-08-08 17:35:05 +02:00
|
|
|
|
const [structure, promptRows, defTemplate, catTemplate, taggingTpl] = 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-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-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))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
<h2 className="text-lg font-bold text-gray-800">KI-Prompt: Kriterien-Zuordnungsvorschläge</h2>
|
|
|
|
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
|
|
|
|
Wird durch den „🏷 Vorschläge analysieren"-Button im Protokoll ausgelöst — ein gebündelter Call pro
|
|
|
|
|
|
Protokoll-Ansicht, nicht pro Zeile. Lernt aus bereits vergebenen Kriterium-Zuordnungen.
|
|
|
|
|
|
</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">
|
|
|
|
|
|
<span className="font-semibold text-sm text-indigo-800">🏷 Tagging-Prompt</span>
|
|
|
|
|
|
<span className="text-xs text-indigo-400">{taggingExpanded ? '▲' : '▼'}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{taggingExpanded && (
|
|
|
|
|
|
<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={taggingTemplate}
|
|
|
|
|
|
onChange={e => saveTaggingTemplate(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{taggingSaved && <span className="text-xs text-green-600 font-medium">Gespeichert.</span>}
|
|
|
|
|
|
</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 im Tagging-Prompt</div>
|
|
|
|
|
|
<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-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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|