Ki Prompts aktualisiert
This commit is contained in:
parent
1a1f3bb33f
commit
019c686254
|
|
@ -149,7 +149,7 @@ MeetingView liest: `const { id, meetingId: meetingIdParam } = useParams()`
|
|||
| 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-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`). Nachträglich (gleicher Tag, nach erstem Testlauf) zwei Lücken gefixt/ergänzt: (1) `database.ts` migrierte nur neue Tabellen, nie Spalten einer bereits bestehenden Tabelle — `suggestedAchievements` u.ä. auf `feedbackDimensionTexts` fehlten dadurch auf bereits laufenden Installationen und scheiterten erst zur Laufzeit; jetzt gleicht `database.ts` zusätzlich per `PRAGMA table_info` ab und zieht fehlende Spalten per `ALTER TABLE ... ADD COLUMN` nach (siehe Datenbank-Abschnitt oben). (2) Prompt-Vorschau ergänzt: pro Dimension ein „👁 KI-Prompt ansehen"-Toggle auf `AssignmentFeedbackPage.tsx`, der den vollständig gerenderten Prompt (Template + aufgelöste Platzhalter) ohne API-Call anzeigt — Feedback war, dass die generierten Texte „brauchbar, aber nicht konkret genug" waren und ohne Einblick in den tatsächlich gesendeten Prompt nicht nachvollziehbar. Zusätzlich neues optionales Feld `FeedbackCriterionItem.description` (editierbar in `FeedbackStructureConfig.tsx`, direkt unter dem Kriteriumsnamen), das automatisch in `{{KRITERIEN_SCORES}}` einfließt (`src/utils/aiPrompt.ts`) — macht die KI-Vorgabe pro Kriterium einmal zentral pflegbar statt in jedem Dimension-Prompt wiederholt werden zu müssen. Bewusst **nicht** reaktiviert: die bereits vorhandene, aber seit Langem unverdrahtete Legacy-Tabelle `CriterionLevelDescription` (Text pro Kriterium UND Rating-Stufe) — wäre eine präzisere Rubrik gewesen, aber deutlich mehr Pflege-/UI-Aufwand; bei Bedarf spätere, bewusste Entscheidung.
|
||||
|
||||
**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.
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ export const TABLE_DEFINITIONS: TableDef[] = [
|
|||
col('name', 'TEXT'),
|
||||
col('weight', 'REAL'),
|
||||
col('order', 'INTEGER'),
|
||||
col('description', 'TEXT'),
|
||||
]},
|
||||
{ name: 'assignmentFeedbacks', columns: [
|
||||
col('assignmentId', 'INTEGER'),
|
||||
|
|
|
|||
|
|
@ -11,14 +11,16 @@ export const RATING_OPTIONS: ReadonlyArray<{
|
|||
value: FeedbackRating
|
||||
short: string
|
||||
label: string
|
||||
/** Ausgeschriebene Bezeichnung fürs KI-Prompting — "Not" allein ist für ein Modell nicht aussagekräftig, "Not Client Ready" transportiert das eigentliche Bewertungsziel des Trainee-Programms. */
|
||||
full: string
|
||||
color: string
|
||||
active: string
|
||||
}> = [
|
||||
{ value: 'na', short: 'N/A', label: 'N/A (0)', color: 'bg-gray-100 text-gray-500 border-gray-300', active: 'bg-gray-400 text-white border-gray-400' },
|
||||
{ value: 'not_client_ready', short: 'Not', label: 'Not (1)', color: 'bg-red-100 text-red-700 border-red-300', active: 'bg-red-500 text-white border-red-500' },
|
||||
{ value: 'partially_client_ready', short: 'Partially', label: 'Partially (2)', color: 'bg-orange-100 text-orange-700 border-orange-300', active: 'bg-orange-500 text-white border-orange-500' },
|
||||
{ value: 'nearly_client_ready', short: 'Nearly', label: 'Nearly (3)', color: 'bg-green-100 text-green-600 border-green-300', active: 'bg-green-400 text-white border-green-400' },
|
||||
{ value: 'client_ready', short: 'Fully', label: 'Fully (4)', color: 'bg-green-100 text-green-800 border-green-400', active: 'bg-green-700 text-white border-green-700' },
|
||||
{ value: 'na', short: 'N/A', label: 'N/A (0)', full: 'Not Applicable', color: 'bg-gray-100 text-gray-500 border-gray-300', active: 'bg-gray-400 text-white border-gray-400' },
|
||||
{ value: 'not_client_ready', short: 'Not', label: 'Not (1)', full: 'Not Client Ready', color: 'bg-red-100 text-red-700 border-red-300', active: 'bg-red-500 text-white border-red-500' },
|
||||
{ value: 'partially_client_ready', short: 'Partially', label: 'Partially (2)', full: 'Partially Client Ready', color: 'bg-orange-100 text-orange-700 border-orange-300', active: 'bg-orange-500 text-white border-orange-500' },
|
||||
{ value: 'nearly_client_ready', short: 'Nearly', label: 'Nearly (3)', full: 'Nearly Client Ready', color: 'bg-green-100 text-green-600 border-green-300', active: 'bg-green-400 text-white border-green-400' },
|
||||
{ value: 'client_ready', short: 'Fully', label: 'Fully (4)', full: 'Fully Client Ready', color: 'bg-green-100 text-green-800 border-green-400', active: 'bg-green-700 text-white border-green-700' },
|
||||
]
|
||||
|
||||
/** Farb-Schemata für Teilnehmer-Badges (Index = Reihenfolge im Assignment). */
|
||||
|
|
@ -43,7 +45,7 @@ FÜLLWÖRTER GESAMT: {{FUELLWOERTER_GESAMT}}
|
|||
KATEGORIEN DIESER DIMENSION:
|
||||
{{KATEGORIEN_STRUKTUR}}
|
||||
|
||||
BEOBACHTETE KRITERIEN UND SCORES (0-4):
|
||||
BEOBACHTETE KRITERIEN (nur mit tatsächlicher Bewertung oder Notiz):
|
||||
{{KRITERIEN_SCORES}}
|
||||
|
||||
MANUELLE KATEGORIE-BEWERTUNGEN:
|
||||
|
|
@ -55,24 +57,27 @@ ENTWICKLUNG ÜBER DIE LAUFZEIT (chronologisch je Kategorie):
|
|||
ALLGEMEINE MEETING-NOTIZEN (Berater-unabhängig, chronologisch):
|
||||
{{NOTIZEN_ALLGEMEIN_MEETING}}
|
||||
|
||||
NICHT EINEM KRITERIUM ZUGEORDNETE PROTOKOLL-NOTIZEN DIESES BERATERS (chronologisch, Notation: + gut, ! negativ, > Kundeninput):
|
||||
NICHT EINEM KRITERIUM ZUGEORDNETE PROTOKOLL-NOTIZEN DIESES BERATERS (chronologisch, Bewertungs-Notation bereits als "Not/Partially/Nearly/Fully Client Ready — Text" bzw. "Kundenzitat — Text" aufgelöst):
|
||||
{{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.
|
||||
Schreibe für JEDE der oben gelisteten Kategorien 1-3 kurze Stichpunkte (konkret, auf Beobachtungen gestützt, jede Zeile beginnt mit "- "). Schreibe abschließend für die gesamte Dimension einen Achievements-Abschnitt (2-4 Stichpunkte, anerkennend, jede Zeile beginnt mit "- ") und einen Development-Needs-Abschnitt (2-4 Stichpunkte, jeder als konkrete Handlungsempfehlung formuliert, z.B. "Sollte darauf achten, dass ..." oder "Empfehlenswert wäre, ...", jede Zeile beginnt mit "- "). 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 und ganze Fließtext-Absätze.
|
||||
|
||||
Antworte AUSSCHLIESSLICH in exakt diesem Format, ohne zusätzliche Erklärungen, ohne Markdown-Formatierung:
|
||||
|
||||
KATEGORIE: <Name der ersten Kategorie>
|
||||
<Bewertungstext>
|
||||
- <Stichpunkt>
|
||||
- <Stichpunkt>
|
||||
|
||||
KATEGORIE: <Name der zweiten Kategorie>
|
||||
<Bewertungstext>
|
||||
- <Stichpunkt>
|
||||
|
||||
ACHIEVEMENTS:
|
||||
<Text>
|
||||
- <Stichpunkt>
|
||||
- <Stichpunkt>
|
||||
|
||||
DEVELOPMENT_NEEDS:
|
||||
<Text>`,
|
||||
- <Handlungsempfehlung, z.B. "Sollte darauf achten, dass ...">
|
||||
- <Handlungsempfehlung>`,
|
||||
dimensionPromptPlaceholders: [
|
||||
{ token: '{{BERATER_NAME}}', description: 'Vor- und Nachname des Institutee' },
|
||||
{ token: '{{ASSIGNMENT_TITEL}}', description: 'Titel des Assignments' },
|
||||
|
|
|
|||
|
|
@ -166,6 +166,8 @@ export interface FeedbackCriterionItem {
|
|||
name: string
|
||||
weight?: number
|
||||
order: number
|
||||
/** Konkretisierung, worauf bei diesem Kriterium zu achten ist — fließt in {{KRITERIEN_SCORES}} der KI-Prompts ein. */
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface AssignmentFeedback {
|
||||
|
|
|
|||
|
|
@ -96,6 +96,9 @@ export default function AssignmentFeedbackPage() {
|
|||
const [generationProgress, setGenerationProgress] = useState('')
|
||||
const [aiError, setAiError] = useState('')
|
||||
const [dimensionErrors, setDimensionErrors] = useState<Record<number, string>>({})
|
||||
const [previewOpen, setPreviewOpen] = useState<Set<number>>(new Set())
|
||||
const [previewText, setPreviewText] = useState<Record<number, string>>({})
|
||||
const [previewLoading, setPreviewLoading] = useState<Set<number>>(new Set())
|
||||
const saveTimer = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map())
|
||||
|
||||
const load = useCallback(async () => {
|
||||
|
|
@ -264,6 +267,41 @@ export default function AssignmentFeedbackPage() {
|
|||
if (existing?.id) await updateFeedbackCategoryText(existing.id, { suggestedText: '' })
|
||||
}
|
||||
|
||||
// ── Prompt-Vorschau (ohne API-Call) ───────────────────────────────────────
|
||||
|
||||
const togglePreview = async (dim: FeedbackDimension) => {
|
||||
if (previewOpen.has(dim.id!)) {
|
||||
setPreviewOpen(prev => { const next = new Set(prev); next.delete(dim.id!); return next })
|
||||
return
|
||||
}
|
||||
setPreviewOpen(prev => new Set(prev).add(dim.id!))
|
||||
if (previewText[dim.id!] || !institutee || !assignment) return
|
||||
|
||||
setPreviewLoading(prev => new Set(prev).add(dim.id!))
|
||||
try {
|
||||
const meetings = await listDoneMeetingsChronological(assignmentId)
|
||||
const meetingIds = meetings.map(m => m.id!)
|
||||
const [allAssessments, allEntries] = await Promise.all([
|
||||
listAssessmentsForMeetings(meetingIds),
|
||||
listConversationEntriesForMeetings(meetingIds),
|
||||
])
|
||||
const instAssessments = allAssessments.filter(a => a.instituteeId === institutee.id)
|
||||
const instEntries = allEntries.filter(e => e.instituteeId === institutee.id)
|
||||
const dimCategories = categories.filter(c => c.dimensionId === dim.id)
|
||||
const dimItems = items.filter(i => dimCategories.some(c => c.id === i.categoryId))
|
||||
|
||||
const promptCfg = await getFeedbackDimensionPrompt(dim.id!)
|
||||
const context = buildDimensionPromptContext({
|
||||
institutee, assignment, dimension: dim, dimCategories, dimItems,
|
||||
ratings, catTrends, instAssessments, instEntries, meetings,
|
||||
})
|
||||
const rendered = renderDimensionPrompt(promptCfg.promptTemplate, context)
|
||||
setPreviewText(prev => ({ ...prev, [dim.id!]: rendered }))
|
||||
} finally {
|
||||
setPreviewLoading(prev => { const next = new Set(prev); next.delete(dim.id!); return next })
|
||||
}
|
||||
}
|
||||
|
||||
// ── KI-Generierung: ein Call pro Dimension ────────────────────────────────
|
||||
|
||||
const generateOneDimension = async (
|
||||
|
|
@ -526,6 +564,32 @@ export default function AssignmentFeedbackPage() {
|
|||
|
||||
{isOpen && (
|
||||
<div className="border-t border-gray-100 p-4 space-y-4">
|
||||
{/* Prompt-Vorschau */}
|
||||
<div>
|
||||
<button onClick={() => togglePreview(dim)}
|
||||
className="text-xs text-gray-400 hover:text-gray-600 underline">
|
||||
{previewOpen.has(dim.id!) ? 'KI-Prompt ausblenden' : '👁 KI-Prompt für diese Dimension ansehen'}
|
||||
</button>
|
||||
{previewOpen.has(dim.id!) && (
|
||||
<div className="mt-2 bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
{previewLoading.has(dim.id!) ? (
|
||||
<div className="text-xs text-gray-400">Lädt…</div>
|
||||
) : (
|
||||
<>
|
||||
<pre className="text-xs text-gray-700 whitespace-pre-wrap font-mono max-h-96 overflow-y-auto">
|
||||
{previewText[dim.id!]}
|
||||
</pre>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(previewText[dim.id!] ?? '')}
|
||||
className="mt-2 text-xs bg-gray-200 hover:bg-gray-300 text-gray-700 px-2 py-1 rounded-lg font-medium">
|
||||
Kopieren
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Kategorien */}
|
||||
{dimCats.map(cat => {
|
||||
const catItems = items.filter(i => i.categoryId === cat.id)
|
||||
|
|
|
|||
|
|
@ -109,6 +109,11 @@ export default function FeedbackStructureConfig() {
|
|||
setItems(prev => prev.map(i => i.id === id ? { ...i, weight } : i))
|
||||
}
|
||||
|
||||
const updateItemDescription = async (id: number, description: string) => {
|
||||
await updateFeedbackCriterionItem(id, { description })
|
||||
setItems(prev => prev.map(i => i.id === id ? { ...i, description } : i))
|
||||
}
|
||||
|
||||
const sorted = [...dimensions].sort((a, b) => a.order - b.order)
|
||||
|
||||
return (
|
||||
|
|
@ -184,7 +189,8 @@ export default function FeedbackStructureConfig() {
|
|||
{isCatOpen && (
|
||||
<div className="p-2 space-y-1.5">
|
||||
{catItems.map(item => (
|
||||
<div key={item.id} className="flex items-center gap-2">
|
||||
<div key={item.id} className="space-y-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-300 text-xs">·</span>
|
||||
<input
|
||||
className="flex-1 text-xs text-gray-600 border-b border-transparent focus:border-gray-300 focus:outline-none bg-transparent py-0.5"
|
||||
|
|
@ -201,6 +207,13 @@ export default function FeedbackStructureConfig() {
|
|||
<button onClick={() => deleteItem(item.id!)}
|
||||
className="text-xs text-red-200 hover:text-red-400">×</button>
|
||||
</div>
|
||||
<input
|
||||
className="w-full ml-3.5 text-xs text-gray-400 border-b border-transparent focus:border-gray-300 focus:outline-none bg-transparent py-0.5"
|
||||
placeholder="Beschreibung, worauf zu achten ist (optional, fließt in KI-Prompts ein)…"
|
||||
value={item.description ?? ''}
|
||||
onChange={e => updateItemDescription(item.id!, e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-1.5 mt-1">
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -3,12 +3,26 @@ import type {
|
|||
FeedbackCategoryRating, Assessment, ConversationEntry, MeetingInstance, FeedbackRating,
|
||||
} from '../db/types'
|
||||
import { RATING_OPTIONS, RATING_NUM_MAP } from '../config/constants'
|
||||
import { splitNoteLines } from './notationParser'
|
||||
import type { TrendResult } from './ratingTrend'
|
||||
import { splitNoteLines, parseNotationPrefix } from './notationParser'
|
||||
import { numberToRating, type TrendResult } from './ratingTrend'
|
||||
|
||||
const ratingToNum = (r: FeedbackRating | null | undefined): number =>
|
||||
r ? (RATING_NUM_MAP[r] ?? 0) : 0
|
||||
|
||||
const fullRatingLabel = (r: FeedbackRating): string => RATING_OPTIONS.find(o => o.value === r)?.full ?? r
|
||||
|
||||
/**
|
||||
* Löst ein Notation-Präfix (`+`/`(+)`/`(!)`/`!`/`>`) am Zeilenanfang zu einem ausgeschriebenen,
|
||||
* mit " — " abgetrennten Label auf. Bewusst KEINE eckigen Klammern als Wrapper — die Notizen
|
||||
* nutzen `[Titel]` bereits für Slide-/Themen-Referenzen (siehe SLIDE_RE in notationParser.tsx),
|
||||
* eckige Klammern um das Label würden damit für die KI ununterscheidbar kollidieren.
|
||||
*/
|
||||
const formatNotationLine = (line: string): string => {
|
||||
const { rating, isQuote, text } = parseNotationPrefix(line)
|
||||
const tag = isQuote ? 'Kundenzitat' : rating ? fullRatingLabel(rating) : null
|
||||
return tag ? `${tag} — ${text}` : text
|
||||
}
|
||||
|
||||
export interface DimensionPromptInput {
|
||||
institutee: Consultant
|
||||
assignment: Assignment
|
||||
|
|
@ -46,17 +60,24 @@ export function buildDimensionPromptContext(input: DimensionPromptInput): Record
|
|||
const catItems = dimItems.filter(i => i.categoryId === cat.id)
|
||||
const lines = catItems.map(c => {
|
||||
const scores = instAssessments.filter(a => a.criteriaId === c.id && a.score !== null && a.score !== 'na')
|
||||
const avg = scores.length > 0
|
||||
? (scores.reduce((s, a) => s + ratingToNum(a.score), 0) / scores.length).toFixed(1)
|
||||
: '—'
|
||||
const notes = [
|
||||
...instAssessments.filter(a => a.criteriaId === c.id && a.note).map(a => a.note),
|
||||
...allLineTags.filter(t => t.criterionItemId === c.id).map(t => t.text),
|
||||
]
|
||||
return ` ${c.name}: ${avg}/4${notes.length > 0 ? ` | Notizen: ${notes.join('; ')}` : ''}`
|
||||
}).join('\n')
|
||||
return ` ${cat.name}:\n${lines || ' (keine Kriterien)'}`
|
||||
}).join('\n') || '(keine Scores erfasst)'
|
||||
].map(formatNotationLine)
|
||||
// Kriterien ohne jede Beobachtung (weder Score noch Notiz) auslassen — reines Rauschen für die KI.
|
||||
if (scores.length === 0 && notes.length === 0) return null
|
||||
const avgNum = scores.length > 0
|
||||
? scores.reduce((s, a) => s + ratingToNum(a.score), 0) / scores.length
|
||||
: null
|
||||
const avgLabel = avgNum !== null ? `${fullRatingLabel(numberToRating(avgNum))} (${avgNum.toFixed(1)})` : 'nur Notiz, keine Bewertung'
|
||||
const label = c.description ? `${c.name} (${c.description})` : c.name
|
||||
return ` ${label}: ${avgLabel}${notes.length > 0 ? ` | Notizen: ${notes.join('; ')}` : ''}`
|
||||
}).filter((l): l is string => l !== null)
|
||||
// Kategorien ohne jede Beobachtung ganz auslassen — tauchen bereits über KATEGORIEN_STRUKTUR/
|
||||
// KATEGORIE_BEWERTUNGEN/ENTWICKLUNG_VERLAUF auf, hier nur zusätzliches Rauschen.
|
||||
if (lines.length === 0) return null
|
||||
return ` ${cat.name}:\n${lines.join('\n')}`
|
||||
}).filter((l): l is string => l !== null).join('\n') || '(keine Scores erfasst)'
|
||||
|
||||
const kategorieBewertungen = dimCategories.map(cat => {
|
||||
const r = ratings.find(r => r.feedbackCategoryId === cat.id)
|
||||
|
|
@ -76,8 +97,12 @@ export function buildDimensionPromptContext(input: DimensionPromptInput): Record
|
|||
}).join('\n') || '(keine Kategorien)'
|
||||
|
||||
const notizenAllgemeinMeeting = meetings
|
||||
.filter(m => m.generalNotes && m.generalNotes.trim() !== '')
|
||||
.map(m => ` [${m.date}] ${m.generalNotes}`)
|
||||
.map(m => {
|
||||
if (!m.generalNotes || m.generalNotes.trim() === '') return null
|
||||
const lines = splitNoteLines(m.generalNotes).map(line => ` - ${formatNotationLine(line)}`)
|
||||
return ` [${m.date}]\n${lines.join('\n')}`
|
||||
})
|
||||
.filter((x): x is string => x !== null)
|
||||
.join('\n') || '(keine allgemeinen Meeting-Notizen)'
|
||||
|
||||
const protokollUnzugeordnet = meetings
|
||||
|
|
@ -88,7 +113,7 @@ export function buildDimensionPromptContext(input: DimensionPromptInput): Record
|
|||
line, tagged: (e.lineTags ?? []).some(t => t.text === line),
|
||||
})))
|
||||
.filter(x => !x.tagged)
|
||||
.map(x => ` - ${x.line}`)
|
||||
.map(x => ` - ${formatNotationLine(x.line)}`)
|
||||
if (lines.length === 0) return null
|
||||
return ` [${m.date}]\n${lines.join('\n')}`
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,11 +11,25 @@ const NOTATION_RATING_ORDER: [string, FeedbackRating][] = [
|
|||
]
|
||||
|
||||
export function detectNotationRating(line: string): FeedbackRating | null {
|
||||
return parseNotationPrefix(line).rating
|
||||
}
|
||||
|
||||
export interface NotationPrefixMatch {
|
||||
rating: FeedbackRating | null
|
||||
/** `>`-Präfix — Zitat/Kundeninput, keine Bewertung. */
|
||||
isQuote: boolean
|
||||
/** Zeile ohne das erkannte Präfix. */
|
||||
text: string
|
||||
}
|
||||
|
||||
/** Wie `detectNotationRating`, liefert zusätzlich die Zitat-Kennzeichnung und den Text ohne Präfix. */
|
||||
export function parseNotationPrefix(line: string): NotationPrefixMatch {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed.startsWith('>')) return { rating: null, isQuote: true, text: trimmed.slice(1).trim() }
|
||||
for (const [prefix, rating] of NOTATION_RATING_ORDER) {
|
||||
if (trimmed.startsWith(prefix)) return rating
|
||||
if (trimmed.startsWith(prefix)) return { rating, isQuote: false, text: trimmed.slice(prefix.length).trim() }
|
||||
}
|
||||
return null
|
||||
return { rating: null, isQuote: false, text: trimmed }
|
||||
}
|
||||
|
||||
// Notations-Regeln (Reihenfolge wichtig: längere Symbole zuerst)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user