CG-Feedback-Monitor/src/config/constants.ts

229 lines
12 KiB
TypeScript
Raw Normal View History

import type { RatingBandKey, RatingColorBand, RatingScore } from '../db/types'
export const BRAND_COLOR = '#0070AD'
export const RATING_MIN = 1
export const RATING_MAX = 10
/** Kanonische Scores 1–10 — einzige Quelle für Picker/Selects. */
export const RATING_SCORES: readonly RatingScore[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
/**
* Abwärtskompatible Options-Liste (short/label/full = Zahl als Text).
* Farben kommen dynamisch über `resolveScoreStyle` / Farbbänder — `color`/`active` hier nur Fallback.
*/
export const RATING_OPTIONS: ReadonlyArray<{
value: RatingScore
short: string
label: string
2026-07-08 09:35:08 +02:00
full: string
color: string
active: string
}> = RATING_SCORES.map(n => ({
value: n,
short: String(n),
label: String(n),
full: String(n),
color: 'bg-gray-100 text-gray-600 border-gray-300',
active: 'bg-brand text-white border-brand',
}))
export const BAND_STYLES: Record<RatingBandKey, { color: string; active: string; hex: string }> = {
red: { color: 'bg-red-100 text-red-700 border-red-300', active: 'bg-red-500 text-white border-red-500', hex: '#ef4444' },
orange: { color: 'bg-orange-100 text-orange-700 border-orange-300', active: 'bg-orange-500 text-white border-orange-500', hex: '#f97316' },
// Unselected: light lime vs saturated green — both used bg-green-100 before and looked identical.
lightGreen: { color: 'bg-lime-50 text-lime-700 border-lime-300', active: 'bg-lime-400 text-white border-lime-400', hex: '#a3e635' },
darkGreen: { color: 'bg-green-200 text-green-900 border-green-700', active: 'bg-green-700 text-white border-green-700', hex: '#15803d' },
}
/** Default: 1–2 rot, 3–5 orange, 6–8 hellgrün, 9–10 dunkelgrün (siehe docs/SCALE_1_TO_10.md). */
export const DEFAULT_RATING_COLOR_BANDS: RatingColorBand[] = [
{ max: 2, key: 'red' },
{ max: 5, key: 'orange' },
{ max: 8, key: 'lightGreen' },
{ max: 10, key: 'darkGreen' },
]
export function isValidRatingScore(value: unknown): value is RatingScore {
const n = typeof value === 'number' ? value : typeof value === 'string' && value.trim() !== '' ? Number(value) : NaN
return Number.isInteger(n) && n >= RATING_MIN && n <= RATING_MAX
}
/** Normalisiert DB-/UI-Werte auf 1–10 oder null (SQLite kann INTEGER-Spalten als String liefern). */
export function normalizeRatingScore(value: unknown): RatingScore | null {
if (value === null || value === undefined || value === '') return null
const n = typeof value === 'number' ? value : Number(value)
if (!Number.isInteger(n) || n < RATING_MIN || n > RATING_MAX) return null
return n
}
/** Rundet Mittelwerte auf 1–10; außerhalb / NaN → null. */
export function clampRatingScore(avg: number): RatingScore | null {
if (!Number.isFinite(avg)) return null
const rounded = Math.round(avg)
if (rounded < RATING_MIN || rounded > RATING_MAX) return null
return rounded
}
export function normalizeRatingColorBands(bands: RatingColorBand[] | null | undefined): RatingColorBand[] {
if (!bands?.length) return DEFAULT_RATING_COLOR_BANDS.map(b => ({ ...b }))
const sorted = [...bands].sort((a, b) => a.max - b.max)
if (sorted[sorted.length - 1]?.max !== RATING_MAX) return DEFAULT_RATING_COLOR_BANDS.map(b => ({ ...b }))
return sorted
}
export function bandForScore(score: RatingScore, bands: RatingColorBand[] = DEFAULT_RATING_COLOR_BANDS): RatingColorBand {
const normalized = normalizeRatingColorBands(bands)
return normalized.find(b => score <= b.max) ?? normalized[normalized.length - 1]
}
export function resolveScoreStyle(
score: RatingScore | null | undefined,
bands: RatingColorBand[] = DEFAULT_RATING_COLOR_BANDS,
): { value: RatingScore | null; short: string; color: string; active: string; hex: string } {
const normalized = normalizeRatingScore(score)
if (normalized === null) {
return {
value: null,
short: '–',
color: 'bg-gray-100 text-gray-500 border-gray-300',
active: 'bg-gray-400 text-white border-gray-400',
hex: '#9ca3af',
}
}
const band = bandForScore(normalized, bands)
const style = BAND_STYLES[band.key]
return { value: normalized, short: String(normalized), color: style.color, active: style.active, hex: style.hex }
}
/** Farb-Schemata für Teilnehmer-Badges (Index = Reihenfolge im Assignment). */
export const INST_COLORS = [
{ bg: 'bg-blue-50', border: 'border-blue-300', tag: 'bg-blue-500', text: 'text-blue-700' },
{ bg: 'bg-purple-50', border: 'border-purple-300', tag: 'bg-purple-500', text: 'text-purple-700' },
{ bg: 'bg-emerald-50', border: 'border-emerald-300', tag: 'bg-emerald-500', text: 'text-emerald-700' },
{ bg: 'bg-orange-50', border: 'border-orange-300', tag: 'bg-orange-500', text: 'text-orange-700' },
] as const
export const AI_CONFIG = {
localStorageKeyApiKey: 'ai_api_key',
localStorageKeyModel: 'ai_model',
defaultModel: 'openai/gpt-4o-mini',
openRouterBaseUrl: 'https://openrouter.ai/api/v1',
/**
* Seit 2026-07-14 nur noch einmaliger Erstbefüllungs-Wert für `appSettings.defaultDimensionPromptTemplate`
* (siehe `getOrSeedDefaultDimensionPromptTemplate()` in server/db/queries/appSettings.ts) — der tatsächlich
* aktive Standard-Prompt lebt danach in der DB und ist im Admin-Tab „KI-Prompts" editierbar, nicht mehr hier.
*/
defaultCategoryPromptTemplate: `Du bist Führungskraft bei Capgemini. Erstelle Feedback-Inhalte für EINE Kategorie auf Basis der Beobachtungsdaten aus Kundenterminen.
2026-07-07 11:14:00 +02:00
BERATER: {{BERATER_NAME}}
ASSIGNMENT: {{ASSIGNMENT_TITEL}} | Kunde: {{ASSIGNMENT_KUNDE}}
DIMENSION: {{DIMENSION_NAME}}
KATEGORIE: {{KATEGORIE_NAME}}
{{KATEGORIE_ERLAEUTERUNG}}
2026-07-07 11:14:00 +02:00
KRITERIEN & BEOBACHTUNGEN (nur diese Kategorie):
2026-07-07 11:14:00 +02:00
{{KRITERIEN_SCORES}}
MANUELLE KATEGORIE-BEWERTUNG: {{KATEGORIE_BEWERTUNG}}
ENTWICKLUNG ÜBER MEETINGS: {{ENTWICKLUNG_VERLAUF}}
{{TREND_VORSCHLAG}}
2026-07-07 11:14:00 +02:00
GETAGGTE PROTOKOLLZEILEN (dieser Kategorie zugeordnet):
{{PROTOKOLL_GETAGGT}}
2026-07-07 11:14:00 +02:00
Schreibe:
- SCORE: 1–10 (Trend-Vorschlag beachten — bestätigen oder maximal ±1 abweichen)
- BENEFITS: 1–3 kurze positive Beobachtungen (Stichpunkte mit "- ")
- CONCERNS: 1–3 kurze kritische Beobachtungen (Stichpunkte mit "- "). Concerns sind Feststellungen, KEINE Handlungsempfehlungen.
2026-07-07 11:14:00 +02:00
Nutze ausschließlich die oben genannten Daten. Reicht die Datenlage nicht: SCORE weglassen, BENEFITS/CONCERNS mit "- n/a".
2026-07-07 11:14:00 +02:00
Antwortformat (Marker in eigener Zeile oder mit Text in derselben Zeile), ohne Markdown-Codefences:
2026-07-07 11:14:00 +02:00
SCORE: <1-10>
BENEFITS:
2026-07-08 09:35:08 +02:00
- <Stichpunkt>
CONCERNS:
- <Stichpunkt>`,
defaultDimensionPromptTemplate: `Du bist Führungskraft bei Capgemini. Verdichte die folgenden Kategorie-Feedbacks zu übergreifenden Dimension-Hauptpunkten — ohne Rohprotokoll, nur auf Basis der Kategorie-Ergebnisse.
2026-07-07 11:14:00 +02:00
BERATER: {{BERATER_NAME}}
ASSIGNMENT: {{ASSIGNMENT_TITEL}} | Kunde: {{ASSIGNMENT_KUNDE}}
DIMENSION: {{DIMENSION_NAME}}
2026-07-07 11:14:00 +02:00
KATEGORIE-ERGEBNISSE:
{{KATEGORIE_ERGEBNISSE}}
Schreibe für die gesamte Dimension:
- ACHIEVEMENTS: genau 3 bis 5 übergreifende Benefits (keine bloße Wiederholung einzelner Kategorien)
- DEVELOPMENT_NEEDS: genau 3 bis 5 übergreifende Concerns als Beobachtungen (keine Empfehlungsformeln wie "Sollte …")
Nutze ausschließlich die Kategorie-Ergebnisse oben. Antwortformat:
2026-07-07 11:14:00 +02:00
ACHIEVEMENTS:
- <Hauptpunkt>
2026-07-07 11:14:00 +02:00
DEVELOPMENT_NEEDS:
- <Beobachtung>`,
categoryPromptPlaceholders: [
{ 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 übergeordneten Dimension' },
{ token: '{{KATEGORIE_NAME}}', description: 'Name der aktuellen Kategorie' },
{ token: '{{KATEGORIE_ERLAEUTERUNG}}', description: 'Optional: Erläuterung der Kategorie' },
{ token: '{{KRITERIEN_SCORES}}', description: 'Kriterien dieser Kategorie mit Ø-Score und Notizen' },
{ token: '{{KATEGORIE_BEWERTUNG}}', description: 'Manuelle Kategorie-Bewertung (1–10)' },
{ token: '{{ENTWICKLUNG_VERLAUF}}', description: 'Meeting-Verlauf dieser Kategorie' },
{ token: '{{TREND_VORSCHLAG}}', description: 'Vorgeschlagener Score aus Meeting-Trend (±1 Regel)' },
{ token: '{{PROTOKOLL_GETAGGT}}', description: 'Protokollzeilen, die Kriterien dieser Kategorie zugeordnet sind' },
],
2026-07-07 11:14:00 +02:00
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: '{{KATEGORIE_ERGEBNISSE}}', description: 'Scores + Benefits/Concerns aller Kategorien (Stufe A)' },
2026-07-07 11:14:00 +02:00
],
defaultCriterionTaggingPromptTemplate: `Du bist Assistent bei der Auswertung von Kundentermin-Protokollen für Capgemini-Trainees (Institutees). Analysiere die folgenden noch nicht einem Bewertungskriterium zugeordneten Protokollzeilen und schlage pro Zeile passende Kriterien vor.
ASSIGNMENT-TYP: {{ASSIGNMENT_TYP}}
VERFÜGBARE KRITERIEN (Dimension → Kategorie → Kriterium, referenziert über die Zahl in eckigen Klammern):
{{KRITERIEN_STRUKTUR}}
BEREITS BEKANNTE, TYPISCHE FORMULIERUNGSMUSTER PRO KRITERIUM (aus früheren Analyse-Läufen, sofern vorhanden):
{{GELERNTE_MUSTER}}
ÄHNLICHE, BEREITS ZUGEORDNETE ALT-ZEILEN (Kontext-Hinweis, keine feste Vorgabe):
{{AEHNLICHE_ALT_ZUORDNUNGEN}}
NOCH NICHT ZUGEORDNETE PROTOKOLLZEILEN (nummeriert):
{{PROTOKOLL_ZEILEN}}
Ordne jede Zeile 0 bis 3 Kriterien zu, priorisiert nach Passung (wahrscheinlichstes zuerst). Nutze ausschließlich die oben gelisteten Kriterium-IDs. Wenn keine sinnvolle Zuordnung erkennbar ist, gib "-" an. Erfinde keine Kriterien und keine Beobachtungen, die nicht im Zeilentext stehen.
Antworte AUSSCHLIESSLICH in exakt diesem Format, ohne zusätzliche Erklärungen, ohne Markdown-Formatierung:
ZEILE <Nummer>: <Kriterium-ID>, <Kriterium-ID>, ...
ZEILE <Nummer>: -
Ergänze danach, nur für Kriterien mit mindestens einer Zuordnung in dieser Analyse, kurze generalisierte Formulierungsmuster (1-2 Sätze, keine Zitate einzelner Zeilen):
MUSTER:
<Kriterium-ID>: <kurze Beschreibung typischer Formulierungen für dieses Kriterium>`,
criterionTaggingPromptPlaceholders: [
{ token: '{{ASSIGNMENT_TYP}}', description: 'Name des Assignment-Typs des aktuellen Meetings' },
{ token: '{{KRITERIEN_STRUKTUR}}', description: 'ID-referenzierte Struktur Dimension → Kategorie → Kriterium, gefiltert auf die im Assignment-Typ sichtbaren Kriterien, inkl. aller Beschreibungen' },
{ token: '{{GELERNTE_MUSTER}}', description: 'KI-destillierte Formulierungsmuster pro Kriterium aus früheren Analyse-Läufen (Nebenprodukt, siehe FeedbackCriterionItem.learnedPatternHints)' },
{ token: '{{AEHNLICHE_ALT_ZUORDNUNGEN}}', description: 'Lokal (ohne KI) ermittelte, ähnlichste bereits getaggte Alt-Zeilen je noch offener Zeile' },
{ token: '{{PROTOKOLL_ZEILEN}}', description: 'Nummerierte, noch nicht zugeordnete Zeilen des gesamten offenen Protokolls (Gesprächsbeiträge + allgemeine Notizen)' },
],
} as const
/** Default-Zeitgewichtung für den Bewertungs-Vorschlag, überschreibbar über Config > Gewichtung. */
export const DEFAULT_RATING_TREND_WEIGHT_STEP = 0.5
/** Mindest-Punktedelta auf der 1–10-Skala zwischen erstem und letztem Meeting, ab dem ein Trend gilt. */
export const RATING_TREND_DELTA_THRESHOLD = 1