Bündelt die Neuentwicklung in AssigmentMonitorV2 (eigene Ports/DB), die Umstellung auf numerische Bewertungen, Meeting-Checklisten, KI-Tagging und die geplante Feedback-Kaskade — als Basis für Versionsverwaltung in Gitea. Co-authored-by: Cursor <cursoragent@cursor.com>
229 lines
12 KiB
TypeScript
229 lines
12 KiB
TypeScript
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
|
||
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.
|
||
|
||
BERATER: {{BERATER_NAME}}
|
||
ASSIGNMENT: {{ASSIGNMENT_TITEL}} | Kunde: {{ASSIGNMENT_KUNDE}}
|
||
DIMENSION: {{DIMENSION_NAME}}
|
||
KATEGORIE: {{KATEGORIE_NAME}}
|
||
{{KATEGORIE_ERLAEUTERUNG}}
|
||
|
||
KRITERIEN & BEOBACHTUNGEN (nur diese Kategorie):
|
||
{{KRITERIEN_SCORES}}
|
||
|
||
MANUELLE KATEGORIE-BEWERTUNG: {{KATEGORIE_BEWERTUNG}}
|
||
ENTWICKLUNG ÜBER MEETINGS: {{ENTWICKLUNG_VERLAUF}}
|
||
{{TREND_VORSCHLAG}}
|
||
|
||
GETAGGTE PROTOKOLLZEILEN (dieser Kategorie zugeordnet):
|
||
{{PROTOKOLL_GETAGGT}}
|
||
|
||
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.
|
||
|
||
Nutze ausschließlich die oben genannten Daten. Reicht die Datenlage nicht: SCORE weglassen, BENEFITS/CONCERNS mit "- n/a".
|
||
|
||
Antwortformat (Marker in eigener Zeile oder mit Text in derselben Zeile), ohne Markdown-Codefences:
|
||
|
||
SCORE: <1-10>
|
||
BENEFITS:
|
||
- <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.
|
||
|
||
BERATER: {{BERATER_NAME}}
|
||
ASSIGNMENT: {{ASSIGNMENT_TITEL}} | Kunde: {{ASSIGNMENT_KUNDE}}
|
||
DIMENSION: {{DIMENSION_NAME}}
|
||
|
||
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:
|
||
|
||
ACHIEVEMENTS:
|
||
- <Hauptpunkt>
|
||
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' },
|
||
],
|
||
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)' },
|
||
],
|
||
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
|