2026-08-08 17:35:05 +02:00
import type { RatingBandKey , RatingColorBand , RatingScore } from '../db/types'
2026-07-02 10:18:51 +02:00
export const BRAND_COLOR = '#0070AD'
2026-08-08 17:35:05 +02:00
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 ]
2026-07-02 10:18:51 +02:00
/ * *
2026-08-08 17:35:05 +02:00
* Abwärtskompatible Options - Liste ( short / label / full = Zahl als Text ) .
* Farben kommen dynamisch ü ber ` resolveScoreStyle ` / Farbbänder — ` color ` / ` active ` hier nur Fallback .
2026-07-02 10:18:51 +02:00
* /
export const RATING_OPTIONS : ReadonlyArray < {
2026-08-08 17:35:05 +02:00
value : RatingScore
2026-07-02 10:18:51 +02:00
short : string
label : string
2026-07-08 09:35:08 +02:00
full : string
2026-07-02 10:18:51 +02:00
color : string
active : string
2026-08-08 17:35:05 +02:00
} > = 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' } ,
2026-07-02 10:18:51 +02:00
]
2026-08-08 17:35:05 +02:00
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 }
}
2026-07-02 10:18:51 +02:00
/** 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' ,
2026-08-08 17:35:05 +02:00
/ * *
* 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 } }
2026-08-08 17:35:05 +02:00
DIMENSION : { { DIMENSION_NAME } }
KATEGORIE : { { KATEGORIE_NAME } }
{ { KATEGORIE_ERLAEUTERUNG } }
2026-07-07 11:14:00 +02:00
2026-08-08 17:35:05 +02:00
KRITERIEN & BEOBACHTUNGEN ( nur diese Kategorie ) :
2026-07-07 11:14:00 +02:00
{ { KRITERIEN_SCORES } }
2026-08-08 17:35:05 +02:00
MANUELLE KATEGORIE - BEWERTUNG : { { KATEGORIE_BEWERTUNG } }
ENTWICKLUNG Ü BER MEETINGS : { { ENTWICKLUNG_VERLAUF } }
{ { TREND_VORSCHLAG } }
2026-07-07 11:14:00 +02:00
2026-08-08 17:35:05 +02:00
GETAGGTE PROTOKOLLZEILEN ( dieser Kategorie zugeordnet ) :
{ { PROTOKOLL_GETAGGT } }
2026-07-07 11:14:00 +02:00
2026-08-08 17:35:05 +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
2026-08-08 17:35:05 +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
2026-08-08 17:35:05 +02:00
Antwortformat ( Marker in eigener Zeile oder mit Text in derselben Zeile ) , ohne Markdown - Codefences :
2026-07-07 11:14:00 +02:00
2026-08-08 17:35:05 +02:00
SCORE : < 1 - 10 >
BENEFITS :
2026-07-08 09:35:08 +02:00
- < Stichpunkt >
2026-08-08 17:35:05 +02:00
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
2026-08-08 17:35:05 +02:00
BERATER : { { BERATER_NAME } }
ASSIGNMENT : { { ASSIGNMENT_TITEL } } | Kunde : { { ASSIGNMENT_KUNDE } }
DIMENSION : { { DIMENSION_NAME } }
2026-07-07 11:14:00 +02:00
2026-08-08 17:35:05 +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
2026-08-08 17:35:05 +02:00
ACHIEVEMENTS :
- < Hauptpunkt >
2026-07-07 11:14:00 +02:00
DEVELOPMENT_NEEDS :
2026-08-08 17:35:05 +02:00
- < 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 : [
2026-08-08 17:35:05 +02:00
{ 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
] ,
2026-08-08 17:35:05 +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.
2026-07-02 10:18:51 +02:00
2026-08-08 17:35:05 +02:00
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
2026-07-03 11:56:22 +02:00
/** Default-Zeitgewichtung für den Bewertungs-Vorschlag, überschreibbar über Config > Gewichtung. */
export const DEFAULT_RATING_TREND_WEIGHT_STEP = 0.5
2026-08-08 17:35:05 +02:00
/** Mindest-Punktedelta auf der 1– 10-Skala zwischen erstem und letztem Meeting, ab dem ein Trend gilt. */
export const RATING_TREND_DELTA_THRESHOLD = 1