diff --git a/CLAUDE.md b/CLAUDE.md
index ec10616..dbcb6b9 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -28,9 +28,11 @@ src/
pages/
meeting/ ← MeetingView.tsx, ConversationTab.tsx, AssessmentTab.tsx, CriterionTagPicker.tsx
RatingWeightConfig.tsx ← Settings-Tab für Zeitgewichtungs-Faktor (in Configuration.tsx eingebunden)
+ AiPromptConfig.tsx ← Settings-Tab „KI-Prompts" — ein editierbares Prompt-Template je Dimension, Default-Vorbelegung + Reset (in Configuration.tsx eingebunden, seit 2026-07-07)
...übrige Pages unverändert am alten Ort
utils/
ratingTrend.ts ← zeitgewichteter Bewertungs-Vorschlag + Trend-Erkennung + weightedAverageScore/weightedAverageRating (genutzt von Evaluation.tsx, AssignmentFeedbackPage.tsx, AssessmentTab.tsx, AssignmentDetail.tsx)
+ aiPrompt.ts ← Prompt-Engine für die KI-Feedback-Generierung (seit 2026-07-07): Platzhalter-Kontext pro Dimension, Rendering, Antwort-Parsing — siehe eigener Abschnitt „KI-Feedback-Generierung" unten
server/
index.ts ← Express-App, ein Endpunkt POST /api/rpc
database.ts ← node:sqlite-Verbindung (server/data/assignment-monitor.sqlite3), Schema aus tableRegistry.ts
@@ -48,6 +50,8 @@ Grund: Mehrere Browser (Chrome, Edge, ...) auf demselben Laptop sollen dieselben
**Abweichung vom ursprünglich gewählten Stack:** `better-sqlite3` scheiterte lokal an einer fehlenden/kaputten Python-Installation (node-gyp-Kompilierung). `node:sqlite` bietet dieselbe synchrone API (`DatabaseSync`, `.prepare().run()/.get()/.all()`), braucht keine native Kompilierung und ist ab dieser Node-Version (25.x) ohne Flag nutzbar (Node meldet es noch als "experimental" — rein informativ, funktioniert stabil). Falls auf einem anderen Rechner/Server `node:sqlite` fehlt (ältere Node-Version), wäre `better-sqlite3` dort nachzuholen, sofern eine funktionierende Build-Toolchain (Python + C++-Compiler) vorhanden ist.
+**Spalten-Migration bei bestehenden Tabellen (seit 2026-07-07):** `database.ts` legt fehlende Tabellen per `CREATE TABLE IF NOT EXISTS` an — das migriert aber **nie** eine bereits existierende Tabelle. Wird `tableRegistry.ts` um eine neue Spalte einer bereits vorhandenen Tabelle ergänzt (z.B. `suggestedAchievements` auf `feedbackDimensionTexts`), gleicht `database.ts` das zusätzlich per `PRAGMA table_info` ab und zieht fehlende Spalten per `ALTER TABLE ... ADD COLUMN` automatisch nach. Ohne das bleibt die alte Spaltenmenge bestehen und jeder Zugriff auf die neue Spalte scheitert erst zur Laufzeit mit „no column named ..." — genau das ist beim Einführen des KI-Prompt-Systems passiert und wurde dabei gefixt.
+
**RPC statt REST:** Ein einziger Endpunkt `POST /api/rpc` mit Body `{ module, fn, args }` statt ~80 einzelnen Routen — Client (`src/db/rpcClient.ts`) und Server (`server/rpcModules.ts`) sind darüber 1:1 gekoppelt: jede Funktion aus `src/db/queries/*.ts` hat ein gleichnamiges Pendant in `server/db/queries/*.ts`.
**Dexie/IndexedDB ist toter Code, aber noch nicht entfernt:** `src/db/schema.ts` und `src/db/seeds/*.ts` bleiben unverändert im Repo liegen, werden aber von nichts mehr gelesen (Seed-Aufrufe in `main.tsx` wurden entfernt). Vollständiges Entfernen ist eine spätere, bewusste Entscheidung.
@@ -121,6 +125,7 @@ MeetingView liest: `const { id, meetingId: meetingIdParam } = useParams()`
- Meeting-Filter bei Aggregationen über mehrere Meetings: immer `!m.deletedAt && m.status === 'done'` — sonst fließen Papierkorb-/unfertige Meetings mit ein
- **Weichmacher/Füllwörter in Formulierungen** (z.B. "ein bisschen") werden bewusst **nicht** über eine eigene Text-Erkennungslogik behandelt — stattdessen legt man dafür ein eigenes, niedrig gewichtetes Kriterium an (z.B. "Sprachliche Präzision", Gewicht < 1) und taggt/notiert entsprechende Zeilen ganz normal (meist mit `(!)`). Die bestehende Tagging- + Notation-Vorschlag-Mechanik deckt das automatisch ab.
- Der `äh`-Zähler (`ConversationEntry.fillerCount`) bleibt bewusst **rein informativ** — keine automatische Schwellenwert-Bewertung. Würde eine eigene Design-Entscheidung brauchen (ab wann gilt die Zahl als "zu hoch"), aktuell reicht die reine Anzeige als Grundlage für die manuelle Bewertung.
+- **Cross-Boundary-Imports von `src/` nach `server/`**: Dateien, die (wie `src/db/types.ts`) sowohl vom Client als auch vom Server importiert werden, müssen frei von Laufzeit-Abhängigkeiten auf Browser-Only-Code sein — auch `import type` reicht nicht als Schutz, wenn die importierte Datei selbst wieder aus dem `'../db'`-Barrel importiert (der zieht Dexie/rpcClient/Seeds mit, siehe `src/config/constants.ts`-Fix vom 2026-07-07: Import auf `'../db/types'` direkt umgestellt). Jede neue Datei dieser Art muss zusätzlich explizit in `tsconfig.server.json`s `include` aufgenommen werden (sonst „File is not listed within the file list of project" bzw. sie wird stillschweigend nicht mitkompiliert) — Muster: `src/db/types.ts`, `src/config/constants.ts`.
- `ConversationEntry` entspricht weiterhin **einem ganzen Sprecher-Turn** (ein zusammenhängender, mehrzeiliger Text-Block bis "✓ Schließen") — bewusst so belassen, ein Zwischenstand mit "ein Entry pro Zeile" wurde am 2026-07-02 wieder verworfen, weil er die Lesbarkeit des Protokolls zerstört hat. Kriterium-Zuordnung passiert stattdessen **pro Zeile innerhalb** eines Entry über `ConversationEntry.lineTags` (Matching per exaktem Zeilentext, nicht per Index — robust gegenüber nachträglichem Einfügen/Löschen von Zeilen). Zeilen splitten immer über `splitNoteLines()` in `src/utils/notationParser.tsx`, nicht erneut `note.split('\n')` inline schreiben (sonst laufen Trim-/Filter-Verhalten auseinander).
---
@@ -138,14 +143,14 @@ MeetingView liest: `const { id, meetingId: meetingIdParam } = useParams()`
| Prio | Aufgabe | Datei |
|---|---|---|
-| H | **KI-Generierung derzeit funktionsunfähig**: `generateWithAi()` schlägt beim Ausführen fehl, vermutlich blockiert das Capgemini-Netzwerk/Firewall den Call zu `openrouter.ai`. Root Cause noch nicht untersucht — ggf. mit internem IT-Support klären. | `src/pages/AssignmentFeedbackPage.tsx` |
-| H | **Größeres Vorhaben (noch nicht gescoped):** Mehrstufiges, pro Dimension konfigurierbares Prompt-System mit Spezialwissen je Dimension + Platzhaltersystem — Ablösung des aktuellen Einzel-Prompts in `buildAiPrompt`. Braucht eigene Design-Session. | `src/pages/AssignmentFeedbackPage.tsx` |
| M | **Selbstlernendes Tagging (Idee, noch nicht gescoped):** Zeile-zu-Kriterium-Zuordnungen sammeln und daraus beim Tippen automatische Tag-Vorschläge ableiten (ähnliche Formulierungen → gleiches Kriterium). Braucht eigene Design-Session (Ähnlichkeits-/Lernmechanik, Datenbasis, UI). | `src/pages/meeting/CriterionTagPicker.tsx` |
| M | **Migration der echten Daten aussteht:** Aktuelle IndexedDB-Daten müssen noch einmalig über Config → Backup → Export (alte Version) exportiert und über Config → Backup → Import (neue, serverbasierte Version) in die SQLite-DB eingespielt werden. Siehe Datenbank-Abschnitt oben. | — |
| L | Dexie/IndexedDB-Legacy-Code vollständig entfernen (`src/db/schema.ts`, `src/db/seeds/`) — aktuell bewusst nur stillgelegt, nicht gelöscht | `src/db/schema.ts`, `src/db/seeds/` |
| L | Server-seitiges "Seed-if-empty" für Assignment-Typen/Feedback-Struktur (aktuell nur clientseitig vorhanden und nicht mehr wirksam) — nicht dringend, da der Backup-Import denselben Zweck erfüllt | `server/db/queries/` (neu) |
| L | SQLite WASM + OPFS Migration — durch den lokalen Server-Ansatz (2026-07-04) überholt/hinfällig | `src/db/schema.ts` |
+**Erledigt (2026-07-07):** KI-Feedback-Generierung neu konzipiert und umgesetzt — löst den zuvor diagnostizierten `response_format`/Markdown-Codefence-Bug strukturell statt punktuell. Statt einem Monolith-Call für alle Dimensionen macht `generateAllDimensions()` (`AssignmentFeedbackPage.tsx`) jetzt **sequenziell einen Call pro Dimension**, mit Fortschrittsanzeige und Teil-Fehlerbehandlung (fehlgeschlagene Dimensionen einzeln erneut versuchbar, `dimensionErrors`). Jede Dimension hat einen **vollständig frei editierbaren Prompt** (`FeedbackDimensionPrompt`, neuer Admin-Tab „KI-Prompts" → `AiPromptConfig.tsx`), der bei `addFeedbackDimension()` automatisch mit einem Default-Template vorbelegt wird (`AI_CONFIG.defaultDimensionPromptTemplate`) — neue Dimensionen funktionieren dadurch ohne Zutun. Antwortformat ist jetzt einfacher Text statt JSON (`KATEGORIE:`/`ACHIEVEMENTS:`/`DEVELOPMENT_NEEDS:`-Marker, geparst in `src/utils/aiPrompt.ts::parseDimensionAiResponse`), da pro Call ohnehin nur eine Dimension behandelt wird — macht den JSON/Codefence-Bug strukturell unmöglich statt ihn nur zu patchen. KI-generierte Texte landen zunächst als **Vorschlag** (`suggestedAchievements`/`suggestedDevelopmentNeeds` auf `FeedbackDimensionText`, `suggestedText` auf neuer Tabelle `FeedbackCategoryText`) und werden erst durch expliziten „Übernehmen"-Klick in die echten Felder kopiert (gleiches Muster wie die bestehende Rating-Vorschlags-Badge) — persistiert, übersteht also einen Seitenwechsel. Neu: kurzer KI-Text zusätzlich auf Kategorie-Ebene (`FeedbackCategoryText`), nicht nur auf Dimension-Ebene. Cross-Boundary-Falle dabei gefunden: `src/config/constants.ts` importierte bisher `FeedbackRating` aus dem Barrel `'../db'` (re-exportiert Dexie/rpcClient/Seeds) statt direkt aus `'../db/types'` — beim Import von `AI_CONFIG` in `server/db/queries/feedbackStructure.ts` hätte das transitiv Browser-only-Code (`fetch`, `localStorage`) in den Server-TS-Scope gezogen; auf direkten Typ-Import umgestellt und `src/config/constants.ts` zusätzlich explizit in `tsconfig.server.json`s `include` aufgenommen (gleiches Muster wie das dort schon gelistete `src/db/types.ts`).
+
**Erledigt (2026-07-04, zweite Runde):** Lokaler Node/Express-Server mit `node:sqlite` (statt IndexedDB) eingeführt, RPC-Bridge (`POST /api/rpc`) zwischen `src/db/queries/*.ts` (Client) und neuem `server/db/queries/*.ts` (Server) — siehe eigener Abschnitt "Datenbank: lokaler Server statt IndexedDB" oben für Details, Abweichungen (node:sqlite statt better-sqlite3) und die wichtige `npx tsc -b`-statt-`--noEmit`-Korrektur. Ziel: mehrere Browser auf demselben Laptop teilen sich dieselben Daten, unabhängig vom Browser-Cache, als Zwischenschritt vor einem Docker/Linux-Deployment.
**Erledigt (2026-07-04):** `db/queries/`-Layer eingeführt — alle 20 Komponenten/Utils mit vorherigem direktem `db.
.*`-Zugriff nutzen jetzt benannte Funktionen aus `src/db/queries/*.ts` (siehe eigener Abschnitt oben), reine Extraktion ohne beabsichtigte Verhaltensänderung. Dabei bewusst mitkonsolidiert: die drei unabhängig implementierten Varianten der gewichteten-Durchschnitt-Logik (`ratingTrend.ts`, `AssessmentTab.tsx`, `AssignmentDetail.tsx`) laufen jetzt über eine einzige Funktion `weightedAverageScore()`/`weightedAverageRating()` in `ratingTrend.ts` — dabei einen fehlenden `Σweight===0`-Guard in `AssignmentDetail.tsx` gefixt (identische Bug-Klasse wie die zweite Bug-Klasse oben, dort war er noch nicht behoben). Ebenfalls konsolidiert: die "AssignmentType.criteriaIds schränkt sichtbare Kriterien ein"-Filterlogik (vorher dupliziert in `MeetingView.tsx`/`AssignmentDetail.tsx`, jetzt `filterVisibleFeedbackStructure()`) und die doppelten Datenabfragen in `meetingExport.ts`s Markdown-/JSON-Export (jetzt `getMeetingExportData()` in `meetings.ts`). `dbBackup.ts` und Seed-Code bleiben bewusst bei direktem `db`-Zugriff (siehe Abschnitt oben). `npx tsc --noEmit` läuft fehlerfrei.
diff --git a/server/database.ts b/server/database.ts
index db47569..197cee3 100644
--- a/server/database.ts
+++ b/server/database.ts
@@ -10,9 +10,25 @@ mkdirSync(dirname(dbPath), { recursive: true })
export const db = new DatabaseSync(dbPath)
+/**
+ * `CREATE TABLE IF NOT EXISTS` legt eine fehlende Tabelle vollständig an, migriert aber niemals eine
+ * bereits bestehende — neue Spalten, die tableRegistry.ts für eine vorhandene Tabelle ergänzt (z.B.
+ * suggestedAchievements auf feedbackDimensionTexts, 2026-07-07), fehlen dort sonst dauerhaft und
+ * führen erst zur Laufzeit zu "no column named ..."-Fehlern. Deshalb zusätzlich per PRAGMA table_info
+ * abgleichen und fehlende Spalten per ALTER TABLE nachziehen.
+ */
for (const table of TABLE_DEFINITIONS) {
const columnsSql = table.columns.map(c => `"${c.name}" ${c.sqlType}`).join(', ')
db.exec(`CREATE TABLE IF NOT EXISTS "${table.name}" (id INTEGER PRIMARY KEY AUTOINCREMENT, ${columnsSql})`)
+
+ const existingColumns = new Set(
+ (db.prepare(`PRAGMA table_info("${table.name}")`).all() as { name: string }[]).map(c => c.name),
+ )
+ for (const col of table.columns) {
+ if (!existingColumns.has(col.name)) {
+ db.exec(`ALTER TABLE "${table.name}" ADD COLUMN "${col.name}" ${col.sqlType}`)
+ }
+ }
}
console.log(`[db] SQLite bereit unter ${dbPath}`)
diff --git a/server/db/queries/assignmentFeedback.ts b/server/db/queries/assignmentFeedback.ts
index 9ab9856..4217990 100644
--- a/server/db/queries/assignmentFeedback.ts
+++ b/server/db/queries/assignmentFeedback.ts
@@ -1,5 +1,7 @@
import { getRowById, getRowsWhereEquals, insertRow, updateRow } from '../crud'
-import type { FeedbackDraft, AssignmentFeedback, FeedbackCategoryRating, FeedbackDimensionText } from '../../../src/db/types'
+import type {
+ FeedbackDraft, AssignmentFeedback, FeedbackCategoryRating, FeedbackDimensionText, FeedbackCategoryText,
+} from '../../../src/db/types'
export const getFeedbackDraft = (assignmentId: number, instituteeId: number): FeedbackDraft | undefined =>
getRowsWhereEquals('feedbackDrafts', 'assignmentId', assignmentId).find(d => d.instituteeId === instituteeId)
@@ -42,3 +44,15 @@ export const getFeedbackDimensionText = (
export const addFeedbackDimensionText = (t: Omit): number => insertRow('feedbackDimensionTexts', t)
export const updateFeedbackDimensionText = (id: number, patch: Partial): number =>
updateRow('feedbackDimensionTexts', id, patch)
+
+export const listFeedbackCategoryTexts = (assignmentFeedbackId: number): FeedbackCategoryText[] =>
+ getRowsWhereEquals('feedbackCategoryTexts', 'assignmentFeedbackId', assignmentFeedbackId)
+export const getFeedbackCategoryText = (
+ assignmentFeedbackId: number,
+ feedbackCategoryId: number,
+): FeedbackCategoryText | undefined =>
+ getRowsWhereEquals('feedbackCategoryTexts', 'assignmentFeedbackId', assignmentFeedbackId)
+ .find(t => t.feedbackCategoryId === feedbackCategoryId)
+export const addFeedbackCategoryText = (t: Omit): number => insertRow('feedbackCategoryTexts', t)
+export const updateFeedbackCategoryText = (id: number, patch: Partial): number =>
+ updateRow('feedbackCategoryTexts', id, patch)
diff --git a/server/db/queries/feedbackStructure.ts b/server/db/queries/feedbackStructure.ts
index 6a1cd38..5f64bbd 100644
--- a/server/db/queries/feedbackStructure.ts
+++ b/server/db/queries/feedbackStructure.ts
@@ -2,7 +2,8 @@ import {
getAllRows, getRowsWhereIn, insertRow, updateRow,
getRowsWhereEquals, deleteRowsWhereEquals, deleteRowById,
} from '../crud'
-import type { FeedbackDimension, FeedbackCategory, FeedbackCriterionItem } from '../../../src/db/types'
+import type { FeedbackDimension, FeedbackCategory, FeedbackCriterionItem, FeedbackDimensionPrompt } from '../../../src/db/types'
+import { AI_CONFIG } from '../../../src/config/constants'
export interface FeedbackStructure {
dimensions: FeedbackDimension[]
@@ -27,20 +28,53 @@ export const listFeedbackCriterionItemsAll = (): FeedbackCriterionItem[] => getA
export const getFeedbackCriterionItemsByIds = (ids: number[]): FeedbackCriterionItem[] =>
getRowsWhereIn('feedbackCriterionItems', 'id', ids)
-export const addFeedbackDimension = (d: Omit): number => insertRow('feedbackDimensions', d)
+/** Legt zusätzlich einen Default-KI-Prompt für die neue Dimension an, siehe CLAUDE.md „Mehrstufiges Prompt-System". */
+export function addFeedbackDimension(d: Omit): number {
+ const id = insertRow('feedbackDimensions', d)
+ insertRow('feedbackDimensionPrompts', { dimensionId: id, promptTemplate: AI_CONFIG.defaultDimensionPromptTemplate })
+ return id
+}
export const updateFeedbackDimension = (id: number, patch: Partial): number =>
updateRow('feedbackDimensions', id, patch)
-/** Cascade: Items der Kategorien → Kategorien → Dimension. */
+/** Cascade: Items der Kategorien → Kategorien → Prompt-Konfiguration → Dimension. */
export function deleteFeedbackDimension(id: number): void {
const cats = getRowsWhereEquals('feedbackCategories', 'dimensionId', id)
for (const cat of cats) {
deleteRowsWhereEquals('feedbackCriterionItems', 'categoryId', cat.id!)
}
deleteRowsWhereEquals('feedbackCategories', 'dimensionId', id)
+ deleteRowsWhereEquals('feedbackDimensionPrompts', 'dimensionId', id)
deleteRowById('feedbackDimensions', id)
}
+/**
+ * Selbstheilend: Dimensionen, die vor Einführung des Prompt-Systems (2026-07-07) angelegt wurden,
+ * haben noch keine `feedbackDimensionPrompts`-Zeile — wird hier per Backfill nachgezogen, statt eine
+ * separate Migration zu brauchen.
+ */
+export function listFeedbackDimensionPrompts(): FeedbackDimensionPrompt[] {
+ const dims = getAllRows('feedbackDimensions')
+ const existingDimIds = new Set(getAllRows('feedbackDimensionPrompts').map(p => p.dimensionId))
+ for (const dim of dims) {
+ if (!existingDimIds.has(dim.id!)) {
+ insertRow('feedbackDimensionPrompts', { dimensionId: dim.id!, promptTemplate: AI_CONFIG.defaultDimensionPromptTemplate })
+ }
+ }
+ return getAllRows('feedbackDimensionPrompts')
+}
+
+/** Selbstheilend wie `listFeedbackDimensionPrompts` — legt die Zeile bei Bedarf lazy an. */
+export function getFeedbackDimensionPrompt(dimensionId: number): FeedbackDimensionPrompt {
+ const existing = getRowsWhereEquals('feedbackDimensionPrompts', 'dimensionId', dimensionId)[0]
+ if (existing) return existing
+ const id = insertRow('feedbackDimensionPrompts', { dimensionId, promptTemplate: AI_CONFIG.defaultDimensionPromptTemplate })
+ return { id, dimensionId, promptTemplate: AI_CONFIG.defaultDimensionPromptTemplate }
+}
+
+export const updateFeedbackDimensionPrompt = (id: number, patch: Partial): number =>
+ updateRow('feedbackDimensionPrompts', id, patch)
+
export const addFeedbackCategory = (c: Omit): number => insertRow('feedbackCategories', c)
export const updateFeedbackCategory = (id: number, patch: Partial): number =>
updateRow('feedbackCategories', id, patch)
diff --git a/server/db/tableRegistry.ts b/server/db/tableRegistry.ts
index c901e97..dd7fd72 100644
--- a/server/db/tableRegistry.ts
+++ b/server/db/tableRegistry.ts
@@ -142,6 +142,18 @@ export const TABLE_DEFINITIONS: TableDef[] = [
col('feedbackDimensionId', 'INTEGER'),
col('achievements', 'TEXT'),
col('developmentNeeds', 'TEXT'),
+ col('suggestedAchievements', 'TEXT'),
+ col('suggestedDevelopmentNeeds', 'TEXT'),
+ ]},
+ { name: 'feedbackDimensionPrompts', columns: [
+ col('dimensionId', 'INTEGER'),
+ col('promptTemplate', 'TEXT'),
+ ]},
+ { name: 'feedbackCategoryTexts', columns: [
+ col('assignmentFeedbackId', 'INTEGER'),
+ col('feedbackCategoryId', 'INTEGER'),
+ col('text', 'TEXT'),
+ col('suggestedText', 'TEXT'),
]},
{ name: 'criterionCategoryMappings', columns: [
col('criterionId', 'INTEGER'),
diff --git a/src/config/constants.ts b/src/config/constants.ts
index ac97f0f..e62fce6 100644
--- a/src/config/constants.ts
+++ b/src/config/constants.ts
@@ -1,4 +1,4 @@
-import type { FeedbackRating } from '../db'
+import type { FeedbackRating } from '../db/types'
export const BRAND_COLOR = '#0070AD'
@@ -34,6 +34,58 @@ export const AI_CONFIG = {
localStorageKeyModel: 'ai_model',
defaultModel: 'openai/gpt-4o-mini',
openRouterBaseUrl: 'https://openrouter.ai/api/v1',
+ defaultDimensionPromptTemplate: `Du bist Führungskraft und Mentor bei Capgemini. Erstelle einen professionellen, konstruktiven Beitrag zum Performance-Feedback für einen Berater/Trainee, beschränkt auf die Dimension "{{DIMENSION_NAME}}", auf Basis der folgenden Beobachtungsdaten aus echten Kundenterminen.
+
+BERATER: {{BERATER_NAME}}
+ASSIGNMENT: {{ASSIGNMENT_TITEL}} | Kunde: {{ASSIGNMENT_KUNDE}}
+FÜLLWÖRTER GESAMT: {{FUELLWOERTER_GESAMT}}
+
+KATEGORIEN DIESER DIMENSION:
+{{KATEGORIEN_STRUKTUR}}
+
+BEOBACHTETE KRITERIEN UND SCORES (0-4):
+{{KRITERIEN_SCORES}}
+
+MANUELLE KATEGORIE-BEWERTUNGEN:
+{{KATEGORIE_BEWERTUNGEN}}
+
+ENTWICKLUNG ÜBER DIE LAUFZEIT (chronologisch je Kategorie):
+{{ENTWICKLUNG_VERLAUF}}
+
+ALLGEMEINE MEETING-NOTIZEN (Berater-unabhängig, chronologisch):
+{{NOTIZEN_ALLGEMEIN_MEETING}}
+
+NICHT EINEM KRITERIUM ZUGEORDNETE PROTOKOLL-NOTIZEN DIESES BERATERS (chronologisch, Notation: + gut, ! negativ, > Kundeninput):
+{{PROTOKOLL_UNZUGEORDNET}}
+
+Schreibe für JEDE der oben gelisteten Kategorien einen kurzen Bewertungstext (2-3 Sätze, konkret, auf Beobachtungen gestützt) sowie abschließend einen Achievements-Absatz (2-4 Sätze, anerkennend) und einen Development-Needs-Absatz (2-4 Sätze, konstruktiv, handlungsorientiert) für die gesamte Dimension. Falls eine Kategorie aus den Daten nicht beurteilbar ist, schreibe "n/a". Beziehe erkennbare Verbesserungen/Verschlechterungen aus der Entwicklung über die Laufzeit ein. Schreibe direkt und persönlich (du-Form), vermeide generische Phrasen.
+
+Antworte AUSSCHLIESSLICH in exakt diesem Format, ohne zusätzliche Erklärungen, ohne Markdown-Formatierung:
+
+KATEGORIE:
+
+
+KATEGORIE:
+
+
+ACHIEVEMENTS:
+
+
+DEVELOPMENT_NEEDS:
+`,
+ dimensionPromptPlaceholders: [
+ { token: '{{BERATER_NAME}}', description: 'Vor- und Nachname des Institutee' },
+ { token: '{{ASSIGNMENT_TITEL}}', description: 'Titel des Assignments' },
+ { token: '{{ASSIGNMENT_KUNDE}}', description: 'Kundenname des Assignments' },
+ { token: '{{DIMENSION_NAME}}', description: 'Name der aktuellen Dimension' },
+ { token: '{{KATEGORIEN_STRUKTUR}}', description: 'Liste der Kategorienamen dieser Dimension' },
+ { token: '{{KRITERIEN_SCORES}}', description: 'Kriterien mit Ø-Score und zusammengeführten Notizen, gruppiert nach Kategorie' },
+ { token: '{{KATEGORIE_BEWERTUNGEN}}', description: 'Manuell gesetzte Kategorie-Bewertungen (Not/Partially/Nearly/Fully)' },
+ { token: '{{ENTWICKLUNG_VERLAUF}}', description: 'Chronologischer Bewertungs-Trend je Kategorie über die Meetings' },
+ { token: '{{NOTIZEN_ALLGEMEIN_MEETING}}', description: 'Allgemeine, Berater-unabhängige Notizen je Meeting, chronologisch' },
+ { token: '{{PROTOKOLL_UNZUGEORDNET}}', description: 'Nicht getaggte Protokollzeilen dieses Beraters, chronologisch je Meeting' },
+ { token: '{{FUELLWOERTER_GESAMT}}', description: 'Füllwörter-Summe dieses Beraters über alle Meetings' },
+ ],
} as const
/** Numerische Gewichte für Rating-Aggregation. N/A wird aus Berechnungen ausgeschlossen. */
diff --git a/src/db/queries/assignmentFeedback.ts b/src/db/queries/assignmentFeedback.ts
index 2ef02f5..2df0d55 100644
--- a/src/db/queries/assignmentFeedback.ts
+++ b/src/db/queries/assignmentFeedback.ts
@@ -1,5 +1,7 @@
import { rpc } from '../rpcClient'
-import type { FeedbackDraft, AssignmentFeedback, FeedbackCategoryRating, FeedbackDimensionText } from '../types'
+import type {
+ FeedbackDraft, AssignmentFeedback, FeedbackCategoryRating, FeedbackDimensionText, FeedbackCategoryText,
+} from '../types'
export const getFeedbackDraft = (assignmentId: number, instituteeId: number): Promise =>
rpc('assignmentFeedback', 'getFeedbackDraft', assignmentId, instituteeId)
@@ -39,3 +41,15 @@ export const addFeedbackDimensionText = (t: Omit):
rpc('assignmentFeedback', 'addFeedbackDimensionText', t)
export const updateFeedbackDimensionText = (id: number, patch: Partial): Promise =>
rpc('assignmentFeedback', 'updateFeedbackDimensionText', id, patch)
+
+export const listFeedbackCategoryTexts = (assignmentFeedbackId: number): Promise =>
+ rpc('assignmentFeedback', 'listFeedbackCategoryTexts', assignmentFeedbackId)
+export const getFeedbackCategoryText = (
+ assignmentFeedbackId: number,
+ feedbackCategoryId: number,
+): Promise =>
+ rpc('assignmentFeedback', 'getFeedbackCategoryText', assignmentFeedbackId, feedbackCategoryId)
+export const addFeedbackCategoryText = (t: Omit): Promise =>
+ rpc('assignmentFeedback', 'addFeedbackCategoryText', t)
+export const updateFeedbackCategoryText = (id: number, patch: Partial): Promise =>
+ rpc('assignmentFeedback', 'updateFeedbackCategoryText', id, patch)
diff --git a/src/db/queries/feedbackStructure.ts b/src/db/queries/feedbackStructure.ts
index fa5ec0b..c566b47 100644
--- a/src/db/queries/feedbackStructure.ts
+++ b/src/db/queries/feedbackStructure.ts
@@ -1,5 +1,5 @@
import { rpc } from '../rpcClient'
-import type { FeedbackDimension, FeedbackCategory, FeedbackCriterionItem } from '../types'
+import type { FeedbackDimension, FeedbackCategory, FeedbackCriterionItem, FeedbackDimensionPrompt } from '../types'
export interface FeedbackStructure {
dimensions: FeedbackDimension[]
@@ -54,3 +54,11 @@ export const addFeedbackCriterionItem = (i: Omit):
export const updateFeedbackCriterionItem = (id: number, patch: Partial): Promise =>
rpc('feedbackStructure', 'updateFeedbackCriterionItem', id, patch)
export const deleteFeedbackCriterionItem = (id: number): Promise => rpc('feedbackStructure', 'deleteFeedbackCriterionItem', id)
+
+export const listFeedbackDimensionPrompts = (): Promise =>
+ rpc('feedbackStructure', 'listFeedbackDimensionPrompts')
+/** Selbstheilend — legt serverseitig bei Bedarf eine fehlende Prompt-Zeile mit Default-Template an. */
+export const getFeedbackDimensionPrompt = (dimensionId: number): Promise =>
+ rpc('feedbackStructure', 'getFeedbackDimensionPrompt', dimensionId)
+export const updateFeedbackDimensionPrompt = (id: number, patch: Partial): Promise =>
+ rpc('feedbackStructure', 'updateFeedbackDimensionPrompt', id, patch)
diff --git a/src/db/types.ts b/src/db/types.ts
index 5986120..ff781fd 100644
--- a/src/db/types.ts
+++ b/src/db/types.ts
@@ -190,6 +190,24 @@ export interface FeedbackDimensionText {
feedbackDimensionId: number
achievements: string
developmentNeeds: string
+ suggestedAchievements?: string
+ suggestedDevelopmentNeeds?: string
+}
+
+// ─── KI-Prompt-Konfiguration & Kategorie-Texte ────────────────────────────────
+
+export interface FeedbackDimensionPrompt {
+ id?: number
+ dimensionId: number
+ promptTemplate: string
+}
+
+export interface FeedbackCategoryText {
+ id?: number
+ assignmentFeedbackId: number
+ feedbackCategoryId: number
+ text: string
+ suggestedText?: string
}
// ─── Legacy-Mappings ──────────────────────────────────────────────────────────
diff --git a/src/pages/AiPromptConfig.tsx b/src/pages/AiPromptConfig.tsx
new file mode 100644
index 0000000..0de3c10
--- /dev/null
+++ b/src/pages/AiPromptConfig.tsx
@@ -0,0 +1,116 @@
+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([])
+ const [prompts, setPrompts] = useState([])
+ const [expanded, setExpanded] = useState>(new Set())
+ const [savedDimId, setSavedDimId] = useState(null)
+ const saveTimer = useRef