diff --git a/backend/lernen.py b/backend/lernen.py index 9aa546a..ea2cb71 100644 --- a/backend/lernen.py +++ b/backend/lernen.py @@ -39,53 +39,41 @@ FRAGETYPEN = { } -def score_berechnen( - score_vor_frage: int, gut: bool, streak: int, tier2: bool, tier3: bool, - absolviert: bool, verstanden: bool, gemeistert: bool, -) -> int: - """Neuer Score nach einer Antwort · driftfrei (immer aus dem Basis-Score gerechnet). +# Antwort-Niveau (wie sehr der Kern getroffen ist) → Punkt-Delta. +NIVEAUS = { + "kaum": -1, # < 25 % richtig + "teilweise": 0, # 25–49 % + "solide": 1, # 50–74 % + "stark": 2, # 75–99 % + "komplett": 3, # 100 % +} - Drei Stufen, freigeschaltet über Guide-Flags: - - Tier 1 (tier2=False): +1 bei richtig, KEINE Strafe, Deckel NOETIG (3). - - Tier 2 (tier2, nicht tier3): +1 / −1, Boden 3, Deckel MASTERY (10). - - Tier 3 (tier3, Meisterpfad): +1 / −2, Boden 10, Deckel MEISTERN (25). - Streak-Bonus bei richtig: ab 3 in Folge +2 (Tier 2/3), ab 5 in Folge +3 (Tier 3). - `streak` = bisherige Folge VOR dieser Antwort; mit dieser Antwort zählt streak+1. - Boden vor dem Absolvieren ist 0; gemeistert friert bei MEISTERN ein. - Re-Bewertung nutzt denselben Basis-Score + dieselbe Streak (kein Doppelzählen). + +def score_berechnen( + score_vor_frage: int, niveau: str, nachgefragt: bool, + tier2: bool, tier3: bool, absolviert: bool, verstanden: bool, gemeistert: bool, +) -> int: + """Neuer Score · driftfrei (immer aus dem Basis-Score). Delta aus dem Antwort-Niveau: + je besser, desto mehr (−1/0/+1/+2/+3). Bei Nachfragen ist der Gewinn auf +1 gedeckelt. + Danach auf Boden/Deckel der aktiven Stufe geklemmt (3 / 10 / 25). Kein Streak mehr. """ if gemeistert: return MEISTERN - # Stufe richtet sich nach dem Baustein-Fortschritt, nicht nur nach den - # Guide-Flags: ein frischer Baustein durchläuft erst Tier 1 (0→3), auch - # wenn der Guide schon tier2/tier3 freigibt. Tier 2 erst ab absolviert, - # Tier 3 erst ab verstanden. + # Stufe nach Baustein-Fortschritt: frisch erst Tier 1 (0→3), dann ab absolviert/verstanden. tier2 = tier2 and absolviert tier3 = tier3 and verstanden if not tier2: - floor, cap, strafe = (NOETIG if absolviert else 0), NOETIG, 0 + floor, cap = (NOETIG if absolviert else 0), NOETIG elif not tier3: - floor, cap, strafe = NOETIG, MASTERY, -1 + floor, cap = NOETIG, MASTERY else: - floor, cap, strafe = MASTERY, MEISTERN, -2 - if gut: - s = streak + 1 - delta = 3 if (tier3 and s >= 5) else (2 if (tier2 and s >= 3) else 1) - else: - delta = strafe + floor, cap = MASTERY, MEISTERN + delta = NIVEAUS.get(niveau, 0) + if nachgefragt: + delta = min(delta, 1) # mit Hilfe (Nachfrage) höchstens +1 return max(floor, min(cap, score_vor_frage + delta)) -def streak_berechnen(streak_vor: int, gut: bool, tier2: bool, tier3: bool, neu_absolviert: bool, neu_verstanden: bool) -> int: - """Neue Streak nach einer Antwort · persistiert. Reset NUR am Deckel der aktiven Stufe: - Tier 1 → bei neu-absolviert (3), Tier 2 → bei neu-verstanden (10). Sonst läuft sie durch.""" - if not gut: - return 0 - if (not tier2 and neu_absolviert) or (tier2 and not tier3 and neu_verstanden): - return 0 - return streak_vor + 1 - - def _transcript(messages: list[dict]) -> str: return "\n".join( f"{'Nutzer' if m.get('role') == 'user' else 'Assistent'}: {m.get('content', '')}" @@ -124,14 +112,14 @@ def _frage_schema(data) -> dict | None: def _bewertung_schema(data) -> dict | None: - """{"feedback": str, "bewertung": "gut"|"schlecht", "bestanden": bool} · sonst None.""" + """{"feedback": str, "niveau": ∈ NIVEAUS} · sonst None.""" if not isinstance(data, dict): return None feedback = str(data.get("feedback", "")).strip() - bewertung = data.get("bewertung") - if not feedback or bewertung not in ("gut", "schlecht"): + niveau = data.get("niveau") + if not feedback or niveau not in NIVEAUS: return None - return {"feedback": feedback, "bewertung": bewertung, "bestanden": data.get("bestanden") is True} + return {"feedback": feedback, "niveau": niveau} async def _gen_call(name: str, role: str, schema, provider: str, timeout: int = PRUEFUNG_TIMEOUT, **kwargs) -> dict | None: @@ -164,7 +152,7 @@ def _kritik_block(vorversion: str, probleme: list[str]) -> str: def _bewertung_text(bew: dict) -> str: - return f"Bewertung: {bew['bewertung']}\nFeedback: {bew['feedback']}" + return f"Niveau: {bew['niveau']}\nFeedback: {bew['feedback']}" # Deterministischer Guard gegen Doppelfragen — der KI-Kritiker übersieht „…, und welchen…". @@ -215,12 +203,12 @@ async def _frage_mit_kritik( async def _bewertung_mit_kritik( topic: str, baustein: str, section_block: str, kompakt_block: str, - frage: str, transcript: str, gute_antworten: int, provider: str, role: str = "judge", + frage: str, transcript: str, begruendung_block: str, provider: str, role: str = "judge", ) -> dict | None: - """Antwort bewerten (gut/schlecht), vom Kritiker prüfen lassen, bei Fehlurteil neu. + """Antwort bewerten (Niveau), vom Kritiker prüfen lassen, bei Fehlurteil neu. - `frage` ankert die geprüfte Frage; der Dialog (transcript) liefert die Antwort und - eine etwaige Diskussion — so kann eine Re-Bewertung das Argument sehen. + `frage` ankert die geprüfte Frage; der Dialog (transcript) liefert Antwort + Diskussion. + `begruendung_block` = optionale Unzufriedenheit des Lerners (nur bei „Gründlich prüfen"). `role` = "judge" (schnell) oder "guide" (gründlich, starkes Modell mit Thinking). """ timeout = GRUENDLICH_TIMEOUT if role == "guide" else PRUEFUNG_TIMEOUT @@ -231,7 +219,7 @@ async def _bewertung_mit_kritik( "Baustein-Bewertung", role, _bewertung_schema, provider, timeout, topic=topic, baustein=baustein, section_block=section_block, kompakt_block=kompakt_block, frage=frage, transcript=transcript, - gute_antworten=gute_antworten, noetig=NOETIG, kritik_block=kritik_block, + begruendung_block=begruendung_block, kritik_block=kritik_block, ) if bew is None: return None @@ -285,22 +273,42 @@ async def pruefung_frage( return None +async def pruefung_bewertung_schnell( + topic: str, baustein: str, section: str, kompakt: str | None, + frage: str, messages: list[dict], provider: str = DEFAULT_PROVIDER, +) -> dict | None: + """Aktion 'antwort' (Agent 1, schnell): nur Evaluator, kein Kritiker. → {feedback, niveau}.""" + try: + section_block, kompakt_block = _bloecke(section, kompakt) + transcript = _transcript(messages) if messages else "(leer)" + return await _gen_call( + "Baustein-Bewertung", "judge", _bewertung_schema, provider, + topic=topic, baustein=baustein, section_block=section_block, kompakt_block=kompakt_block, + frage=frage.strip() or "(keine Frage übergeben)", transcript=transcript, + begruendung_block="(keine)", kritik_block="(keine)", + ) + except Exception: + log.warning("[%s] Schnell-Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True) + return None + + async def pruefung_bewertung( topic: str, baustein: str, section: str, kompakt: str | None, - frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER, - role: str = "judge", + frage: str, messages: list[dict], provider: str = DEFAULT_PROVIDER, + role: str = "judge", begruendung: str = "", ) -> dict | None: - """Aktion 'antwort_pruefen': verbindlich bewerten (Evaluator + Kritiker). + """Aktion 'antwort_pruefen' (Agent 2, genau): Evaluator + Kritiker. → {feedback, niveau}. - `role` = "guide" für „Gründlich prüfen" (starkes Modell), sonst "judge". - Gibt {"feedback", "bewertung": gut|schlecht, "bestanden"} · None bei Fehler. + `role` = "guide" für „Gründlich prüfen" (starkes Modell). `begruendung` = optionale + Unzufriedenheit des Lerners mit einer früheren Bewertung. """ try: section_block, kompakt_block = _bloecke(section, kompakt) transcript = _transcript(messages) if messages else "(leer)" + begruendung_block = begruendung.strip() or "(keine)" return await _bewertung_mit_kritik( topic, baustein, section_block, kompakt_block, - frage.strip() or "(keine Frage übergeben)", transcript, gute_antworten, provider, role, + frage.strip() or "(keine Frage übergeben)", transcript, begruendung_block, provider, role, ) except Exception: log.warning("[%s] Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True) diff --git a/backend/models.py b/backend/models.py index dadad3c..70f9882 100644 --- a/backend/models.py +++ b/backend/models.py @@ -214,11 +214,13 @@ class BausteinPruefungRequest(BaseModel): baustein: str = Field(min_length=1, max_length=200) section: str = Field(default="", max_length=20000) # ausführliche Fassung section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze) - aktion: Literal["frage", "diskussion", "antwort_pruefen"] = "frage" + aktion: Literal["frage", "diskussion", "antwort", "antwort_pruefen"] = "frage" frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort); Anker der Basis letzte_bewertung: str = Field(default="", max_length=2000) # Feedback der letzten Bewertung (Kontext für diskussion) vermeide: list[str] = [] # schon gestellte + vorgemerkte Fragen — sinngemäß nicht wiederholen - # Basis/Streak werden serverseitig je Frage geführt (offene_frage-Anker) — nicht mehr vom Client. + nachgefragt: bool = False # für diese Frage wurde nachgefragt → Gewinn auf +1 gedeckelt + begruendung: str = Field(default="", max_length=2000) # „Gründlich prüfen": warum mit der Bewertung unzufrieden + # Basis wird serverseitig je Frage geführt (offene_frage-Anker) — nicht mehr vom Client. tier2: bool = False # ganzer Guide absolviert (alle ≥3) → −1 bei falsch, Deckel 10 tier3: bool = False # ganzer Guide verstanden (alle ≥10) → Meisterpfad, −2 bei falsch, Deckel 25 messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage @@ -230,9 +232,9 @@ class BausteinPruefungResponse(BaseModel): frage: str | None = None reply: str | None = None feedback: str | None = None - bewertung: Literal["gut", "schlecht"] | None = None + niveau: str | None = None # kaum|teilweise|solide|stark|komplett + bewertung: Literal["gut", "neutral", "schlecht"] | None = None # abgeleitet, nur fürs Einfärben gute_antworten: int - streak: int | None = None # nur antwort_pruefen setzt die persistierte Streak absolviert: bool verstanden: bool = False gemeistert: bool = False diff --git a/backend/routes.py b/backend/routes.py index 9054b12..451bf73 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -14,11 +14,11 @@ from database import ( list_progress, set_progress, delete_progress, create_element, list_elements, get_element, update_element, delete_element, list_baustein_progress, get_baustein_progress, set_offene_frage, - set_baustein_score, set_baustein_streak, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten, + set_baustein_score, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten, ) from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion -from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_frage, score_berechnen, streak_berechnen +from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, score_berechnen from guide import generate_guide, guide_slot_dateien from pipeline import cancel_guide from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen @@ -247,14 +247,16 @@ def _pruef_lock(topic: str, baustein: str) -> asyncio.Lock: return lock -def _basis(stand: dict, frage: str) -> tuple[int, int, bool]: - """Score-/Streak-Basis VOR der Frage. Gleiche offene Frage → Re-Bewertung auf derselben - Basis (idempotent); sonst neue Frage auf dem aktuellen Stand. → (basis, streak_basis, re_bewertung).""" +def _basis(stand: dict, frage: str) -> tuple[int, bool]: + """Score-Basis VOR der Frage. Gleiche offene Frage → Re-Bewertung auf derselben Basis + (idempotent); sonst neue Frage auf dem aktuellen Stand. → (basis, re_bewertung).""" re_bewertung = stand["offene_frage"] == frage and stand["offene_basis"] is not None - if re_bewertung: - streak_basis = stand["offene_streak"] if stand["offene_streak"] is not None else stand["streak"] - return stand["offene_basis"], streak_basis, True - return stand["gute_antworten"], stand["streak"], False + return (stand["offene_basis"] if re_bewertung else stand["gute_antworten"]), re_bewertung + + +def _farbe(niveau: str) -> str: + """Niveau → grobe Einfärbung der Bubble.""" + return "gut" if niveau in ("solide", "stark", "komplett") else ("neutral" if niveau == "teilweise" else "schlecht") @router.post("/bausteine/pruefung", response_model=BausteinPruefungResponse) @@ -285,36 +287,38 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest): raise HTTPException(502, "Diskussion fehlgeschlagen — bitte erneut versuchen") return {"reply": reply, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert} - # aktion == "antwort_pruefen" — mindestens eine Nutzer-Antwort muss im Dialog stehen - # (nach einer Diskussion endet der Dialog mit dem Tutor; Re-Bewertung bleibt erlaubt). + # aktion "antwort" (Agent 1 schnell) / "antwort_pruefen" (Agent 2 genau). if not any(m.get("role") == "user" for m in msgs): raise HTTPException(400, "Antwort braucht eine Nutzer-Antwort") if not req.frage.strip(): raise HTTPException(400, "Antwort braucht eine laufende Frage") + genau = req.aktion == "antwort_pruefen" - # Verbindlich, persistiert. Serialisiert je Baustein, damit die Basis immer den - # geprüften Vorgänger-Stand sieht (kein Stale-Overwrite). Punkte kommen NUR hier. + # Persist serialisiert je Baustein, Basis driftfrei über den offene_frage-Anker + # (schnell setzt den Anker, genau re-bewertet dieselbe Frage). Beide vergeben Punkte. async with _pruef_lock(req.topic, req.baustein): - stand = await get_baustein_progress(req.topic, req.baustein) # nach evtl. Vorgänger-Persist neu lesen + stand = await get_baustein_progress(req.topic, req.baustein) absolviert = stand["absolviert"] is not None verstanden = stand["verstanden"] is not None gemeistert = stand["gemeistert"] is not None - basis, streak_basis, re_bewertung = _basis(stand, req.frage) + basis, re_bewertung = _basis(stand, req.frage) if not re_bewertung: - # Neue Frage: Basis/Streak einfrieren (Anker für spätere Re-Bewertung dieser Frage). - await set_offene_frage(req.topic, req.baustein, req.frage, basis, streak_basis) + await set_offene_frage(req.topic, req.baustein, req.frage, basis, 0) - data = await pruefung_bewertung( - req.topic, req.baustein, req.section, kompakt, req.frage, msgs, basis, provider=req.provider, - role="guide" if req.gruendlich else "judge", - ) + if genau: + data = await pruefung_bewertung( + req.topic, req.baustein, req.section, kompakt, req.frage, msgs, provider=req.provider, + role="guide" if req.gruendlich else "judge", begruendung=req.begruendung, + ) + else: + data = await pruefung_bewertung_schnell( + req.topic, req.baustein, req.section, kompakt, req.frage, msgs, provider=req.provider, + ) if data is None: raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen") - score = score_berechnen( - basis, data["bewertung"] == "gut", streak_basis, req.tier2, req.tier3, absolviert, verstanden, gemeistert, - ) - war_absolviert, war_verstanden = absolviert, verstanden + niveau = data["niveau"] + score = score_berechnen(basis, niveau, req.nachgefragt, req.tier2, req.tier3, absolviert, verstanden, gemeistert) gute = await set_baustein_score(req.topic, req.baustein, score) if score >= NOETIG and not absolviert: absolviert = True @@ -326,13 +330,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest): if score >= MEISTERN and not gemeistert: await set_baustein_gemeistert(req.topic, req.baustein) gemeistert = True - # Streak persistieren — Reset nur am Deckel der gerade aktiven Stufe. - streak = streak_berechnen( - streak_basis, data["bewertung"] == "gut", req.tier2, req.tier3, - absolviert and not war_absolviert, verstanden and not war_verstanden, - ) - await set_baustein_streak(req.topic, req.baustein, streak) - return {"feedback": data["feedback"], "bewertung": data["bewertung"], "gute_antworten": gute, "streak": streak, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert} + return {"feedback": data["feedback"], "niveau": niveau, "bewertung": _farbe(niveau), "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert} # --- Guides --- diff --git a/frontend/src/api.js b/frontend/src/api.js index 9b355a5..de5d02a 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -78,12 +78,12 @@ export async function chatBaustein({ topic, baustein, section, section_kompakt = export async function pruefeBaustein({ topic, baustein, section, section_kompakt = '', provider, aktion = 'frage', frage = '', letzte_bewertung = '', vermeide = [], - tier2 = false, tier3 = false, messages = [], gruendlich = false, + nachgefragt = false, begruendung = '', tier2 = false, tier3 = false, messages = [], gruendlich = false, }) { const res = await fetch(`${BASE}/bausteine/pruefung`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, vermeide, tier2, tier3, messages, provider, gruendlich }), + body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, vermeide, nachgefragt, begruendung, tier2, tier3, messages, provider, gruendlich }), }) return jsonOrThrow(res) } diff --git a/frontend/src/components/BausteinPanel.vue b/frontend/src/components/BausteinPanel.vue index 0638281..94468b1 100644 --- a/frontend/src/components/BausteinPanel.vue +++ b/frontend/src/components/BausteinPanel.vue @@ -56,6 +56,9 @@ const letztesFeedback = slot.letztesFeedback // Kontext für die Diskussion üb // Transient (pro Instanz): Eingabe-Entwurf, Lade-Spinner. const pruefInput = ref('') const pruefLoading = ref(false) +const nachgefragt = ref(false) // für die laufende Frage wurde nachgefragt → Gewinn max +1 +const gruendlichMsg = ref(null) // Feedback-Bubble, für die das Gründlich-Feld offen ist +const gruendlichText = ref('') // optionale Begründung für „Gründlich prüfen" const pruefMessagesEl = ref(null) const pruefInputEl = ref(null) @@ -109,6 +112,8 @@ async function pruefSenden(payload, onOk) { function frageZeigen(text) { aktuelleFrage.value = text letztesFeedback.value = '' + nachgefragt.value = false + gruendlichMsg.value = null pruefMessages.value.push({ role: 'assistant', kind: 'frage', content: text }) pruefPhase.value = 'frage_offen' } @@ -206,6 +211,7 @@ async function naechsteFrageZeigen() { function nachfragen() { const text = pruefInput.value.trim() if (!text || pruefLoading.value) return + nachgefragt.value = true // Hilfe genutzt → Gewinn für diese Frage max +1 pruefMessages.value.push({ role: 'user', kind: 'nachfrage', content: text }) pruefInput.value = '' pruefSenden( @@ -214,13 +220,15 @@ function nachfragen() { ) } -let letzteFeedbackMsg = null // Referenz auf die zuletzt gezeigte Bewertungs-Bubble -let pruefenRun = 0 // nur die jüngste Bewertung darf die Anzeige setzen +let letzteFeedbackMsg = null // zuletzt gezeigte Bewertungs-Bubble +let pruefenRun = 0 // nur die jüngste Schnell-Bewertung darf anzeigen -// Antwort verbindlich prüfen — der Prüfer (Bewerter + Kritiker) vergibt die Punkte. -// Während des Wartens zeigt das „…"-Bubble den Geprüft-Zustand. gruendlich=true: -// starkes Modell, re-bewertet dieselbe Frage und aktualisiert die bestehende Bubble. -async function bewertenLassen(gruendlich = false) { +function bewertungPayload() { + return { frage: aktuelleFrage.value, tier2: props.tier2, tier3: props.tier3, nachgefragt: nachgefragt.value } +} + +// Agent 1 (schnell): sofortiges Niveau + Punkte zeigen, dann Agent 2 (genau) im Hintergrund. +async function schnellBewerten() { const meine = ++pruefenRun pruefStick.value = true pruefLoading.value = true @@ -228,22 +236,17 @@ async function bewertenLassen(gruendlich = false) { try { const res = await pruefeBaustein({ topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt, - provider: props.provider, messages: pruefDialog(), - aktion: 'antwort_pruefen', frage: aktuelleFrage.value, tier2: props.tier2, tier3: props.tier3, gruendlich, + provider: props.provider, messages: pruefDialog(), aktion: 'antwort', ...bewertungPayload(), }) if (meine !== pruefenRun) return applyPruefung(res) letztesFeedback.value = res.feedback || '' - if (gruendlich && letzteFeedbackMsg) { - letzteFeedbackMsg.content = res.feedback || letzteFeedbackMsg.content - letzteFeedbackMsg.bewertung = res.bewertung - } else { - pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung, geprueft: true }) - letzteFeedbackMsg = pruefMessages.value[pruefMessages.value.length - 1] - pruefPhase.value = 'bewertet' - } + pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung, niveau: res.niveau, geprueft: false }) + letzteFeedbackMsg = pruefMessages.value[pruefMessages.value.length - 1] + pruefPhase.value = 'bewertet' pruefScroll() nextTick(() => pruefInputEl.value?.focus()) + genauBewerten() // Agent 2 korrigiert im Hintergrund } catch { if (meine === pruefenRun) pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Hat nicht geklappt — bitte erneut.' }) } finally { @@ -251,12 +254,36 @@ async function bewertenLassen(gruendlich = false) { } } +// Agent 2 (genau): Bewerter + Kritiker. Korrigiert Bubble + Score (Server-Wahrheit, immer übernehmen). +// gruendlich=true: starkes Modell, optional mit Begründung des Lerners. +async function genauBewerten(gruendlich = false, begruendung = '') { + const ziel = letzteFeedbackMsg + if (gruendlich) pruefLoading.value = true + try { + const res = await pruefeBaustein({ + topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt, + provider: props.provider, messages: pruefDialog(), aktion: 'antwort_pruefen', ...bewertungPayload(), gruendlich, begruendung, + }) + applyPruefung(res) + if (ziel) { + ziel.content = res.feedback || ziel.content + ziel.bewertung = res.bewertung + ziel.niveau = res.niveau + ziel.geprueft = true + } + } catch { + if (ziel) ziel.geprueft = true + } finally { + if (gruendlich) pruefLoading.value = false + } +} + function antwortAbgeben() { const text = pruefInput.value.trim() if (!text || pruefLoading.value) return pruefMessages.value.push({ role: 'user', kind: 'antwort', content: text }) pruefInput.value = '' - bewertenLassen(false) + schnellBewerten() } // Lief schon eine Diskussion? Dann darf der Verlauf direkt bewertet werden. @@ -265,17 +292,25 @@ const hatDiskussion = computed(() => pruefMessages.value.some((m) => m.kind === // „Verlauf bewerten": den bisherigen Dialog als Antwort werten (Feld wird ignoriert). function verlaufBewerten() { if (pruefLoading.value) return - bewertenLassen(false) + schnellBewerten() } -// „Gründlich prüfen": dieselbe Antwort vom starken Modell (role guide) neu bewerten. -function gruendlichPruefen() { +// Rechtsklick auf eine Bewertung → Gründlich-Feld öffnen; Absenden re-bewertet (mit Begründung). +function gruendlichOeffnen(msg) { + gruendlichText.value = '' + gruendlichMsg.value = msg + nextTick(() => document.getElementById('bp-gruendlich-input')?.focus()) +} +function gruendlichAbsenden() { if (pruefLoading.value) return - bewertenLassen(true) + letzteFeedbackMsg = gruendlichMsg.value || letzteFeedbackMsg + const text = gruendlichText.value.trim() + gruendlichMsg.value = null + genauBewerten(true, text) } // Abbruch wie im Chat: Run-Counter hochzählen → laufendes Ergebnis (pruefSenden -// und bewertenLassen) wird verworfen, Buttons sofort frei. Agent läuft serverseitig aus. +// und schnellBewerten) wird verworfen, Buttons sofort frei. Agent läuft serverseitig aus. function pruefAbbrechen() { if (!pruefLoading.value) return pruefRun++ @@ -298,10 +333,9 @@ onUnmounted(() => { window.removeEventListener('keydown', onPruefKey) }) -// ALT+1 Nachfragen/Nachhaken · ALT+2 primärer Button je Phase (idle: Frage anfordern, -// frage_offen: Antwort abgeben, bewertet: Nächste Frage) · ALT+3 Verlauf bewerten / Gründlich. -// window-Listener (nicht div), damit es auch ohne Fokus im Feld greift + den Browser-Default -// (Tab-Wechsel) unterdrückt. Nur im Prüfungs-Tab. +// ALT+1 Nachfragen (nur frage_offen) · ALT+2 primärer Button je Phase (idle: Frage anfordern, +// frage_offen: Antwort abgeben, bewertet: Nächste Frage) · ALT+3 Verlauf bewerten (frage_offen). +// Gründlich prüfen läuft per Rechtsklick auf die Bewertung, nicht über ALT. function onPruefKey(e) { if (!e.altKey || e.repeat) return if (props.mode !== 'full' || activeTab.value !== 'pruefung') return @@ -309,14 +343,13 @@ function onPruefKey(e) { e.preventDefault() if (pruefLoading.value) return if (e.key === '1') { - if (pruefPhase.value !== 'idle' && pruefInput.value.trim()) nachfragen() + if (pruefPhase.value === 'frage_offen' && pruefInput.value.trim()) nachfragen() } else if (e.key === '2') { if (pruefPhase.value === 'idle') frageAnfordern() else if (pruefPhase.value === 'frage_offen') { if (pruefInput.value.trim()) antwortAbgeben() } else naechsteFrageZeigen() } else if (e.key === '3') { - if (pruefPhase.value === 'bewertet') gruendlichPruefen() - else if (pruefPhase.value === 'frage_offen' && hatDiskussion.value) verlaufBewerten() + if (pruefPhase.value === 'frage_offen' && hatDiskussion.value) verlaufBewerten() } } @@ -372,16 +405,23 @@ function onPruefKey(e) {
✓✓✓ Gemeistert ({{ MEISTERN }}/{{ MEISTERN }}) — Max. Du kannst dich weiter prüfen, ohne Punkte. - Meisterpfad: {{ st.gute_antworten }}/{{ MEISTERN }}. Richtig = +1, falsch = −2 (nicht unter {{ MAX }}). Bei {{ MEISTERN }} gemeistert. + Meisterpfad: {{ st.gute_antworten }}/{{ MEISTERN }}. Je besser, desto mehr (+1/+2/+3), schwach = −1 (nicht unter {{ MAX }}). Bei {{ MEISTERN }} gemeistert. ✓✓ Verstanden. Der Meisterpfad ({{ MAX }}→{{ MEISTERN }}) öffnet, sobald der ganze Guide verstanden ist. - Mastery: {{ st.gute_antworten }}/{{ MAX }}. Richtig = +1, falsch = −1 (nicht unter {{ NOETIG }}). Bei {{ MAX }} verstanden. + Mastery: {{ st.gute_antworten }}/{{ MAX }}. Je besser, desto mehr (+1/+2/+3), schwach = −1 (nicht unter {{ NOETIG }}). Bei {{ MAX }} verstanden. ✓ Absolviert. Mehr ({{ NOETIG }}→{{ MAX }}) gibt's, sobald der ganze Guide absolviert ist. - {{ Math.min(st.gute_antworten, NOETIG) }}/{{ NOETIG }} guten Antworten. Frag nach, wenn etwas unklar ist — diskutieren ist erlaubt. + {{ Math.min(st.gute_antworten, NOETIG) }}/{{ NOETIG }} guten Antworten. Je besser deine Antwort, desto mehr Punkte (bis +3). Nachfragen ist erlaubt — dann max +1.
- + -