diff --git a/backend/models.py b/backend/models.py index 70f9882..7b59a3d 100644 --- a/backend/models.py +++ b/backend/models.py @@ -232,8 +232,8 @@ class BausteinPruefungResponse(BaseModel): frage: str | None = None reply: str | None = None feedback: str | None = None - niveau: str | None = None # kaum|teilweise|solide|stark|komplett - bewertung: Literal["gut", "neutral", "schlecht"] | None = None # abgeleitet, nur fürs Einfärben + punkte: int | None = None # Punkt-Delta dieser Antwort (−1 … +3); schnell = voraussichtlich + bewertung: Literal["gut", "neutral", "schlecht"] | None = None # aus Vorzeichen, fürs Einfärben gute_antworten: int absolviert: bool verstanden: bool = False diff --git a/backend/routes.py b/backend/routes.py index 451bf73..612a78c 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -254,9 +254,9 @@ def _basis(stand: dict, frage: str) -> tuple[int, bool]: 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") +def _farbe(punkte: int) -> str: + """Punkte-Delta → grobe Einfärbung der Bubble.""" + return "gut" if punkte > 0 else ("neutral" if punkte == 0 else "schlecht") @router.post("/bausteine/pruefung", response_model=BausteinPruefungResponse) @@ -292,10 +292,22 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest): 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" - # Persist serialisiert je Baustein, Basis driftfrei über den offene_frage-Anker - # (schnell setzt den Anker, genau re-bewertet dieselbe Frage). Beide vergeben Punkte. + if req.aktion == "antwort": + # Agent 1: nur Vorschau — Niveau + voraussichtliche Punkte, NICHTS persistieren, kein Anker. + 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") + basis, _ = _basis(stand, req.frage) + score = score_berechnen(basis, data["niveau"], req.nachgefragt, req.tier2, req.tier3, absolviert, verstanden, gemeistert) + punkte = score - basis + return {"feedback": data["feedback"], "punkte": punkte, "bewertung": _farbe(punkte), + "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert} + + # aktion "antwort_pruefen" (Agent 2 genau): verbindlich, persistiert. Serialisiert je Baustein, + # Basis driftfrei über den offene_frage-Anker. NUR hier ändert sich der Score. async with _pruef_lock(req.topic, req.baustein): stand = await get_baustein_progress(req.topic, req.baustein) absolviert = stand["absolviert"] is not None @@ -305,20 +317,15 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest): if not re_bewertung: await set_offene_frage(req.topic, req.baustein, req.frage, basis, 0) - 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, - ) + 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, + ) if data is None: raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen") - niveau = data["niveau"] - score = score_berechnen(basis, niveau, req.nachgefragt, req.tier2, req.tier3, absolviert, verstanden, gemeistert) + score = score_berechnen(basis, data["niveau"], req.nachgefragt, req.tier2, req.tier3, absolviert, verstanden, gemeistert) + punkte = score - basis gute = await set_baustein_score(req.topic, req.baustein, score) if score >= NOETIG and not absolviert: absolviert = True @@ -330,7 +337,8 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest): if score >= MEISTERN and not gemeistert: await set_baustein_gemeistert(req.topic, req.baustein) gemeistert = True - return {"feedback": data["feedback"], "niveau": niveau, "bewertung": _farbe(niveau), "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert} + return {"feedback": data["feedback"], "punkte": punkte, "bewertung": _farbe(punkte), + "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert} # --- Guides --- diff --git a/frontend/src/components/BausteinPanel.vue b/frontend/src/components/BausteinPanel.vue index 94468b1..9414a1e 100644 --- a/frontend/src/components/BausteinPanel.vue +++ b/frontend/src/components/BausteinPanel.vue @@ -239,14 +239,14 @@ async function schnellBewerten() { provider: props.provider, messages: pruefDialog(), aktion: 'antwort', ...bewertungPayload(), }) if (meine !== pruefenRun) return - applyPruefung(res) + // Agent 1 ändert den Score NICHT — nur Feedback + voraussichtliche Punkte zeigen. letztesFeedback.value = res.feedback || '' - pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung, niveau: res.niveau, geprueft: false }) + pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung, punkte: res.punkte, geprueft: false }) letzteFeedbackMsg = pruefMessages.value[pruefMessages.value.length - 1] pruefPhase.value = 'bewertet' pruefScroll() nextTick(() => pruefInputEl.value?.focus()) - genauBewerten() // Agent 2 korrigiert im Hintergrund + genauBewerten() // Agent 2 wertet verbindlich (setzt den Score) } catch { if (meine === pruefenRun) pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Hat nicht geklappt — bitte erneut.' }) } finally { @@ -268,7 +268,7 @@ async function genauBewerten(gruendlich = false, begruendung = '') { if (ziel) { ziel.content = res.feedback || ziel.content ziel.bewertung = res.bewertung - ziel.niveau = res.niveau + ziel.punkte = res.punkte ziel.geprueft = true } } catch { @@ -309,6 +309,11 @@ function gruendlichAbsenden() { genauBewerten(true, text) } +function punkteLabel(p) { + if (p == null) return '' + return p > 0 ? `+${p}` : p < 0 ? String(p) : '±0' +} + // Abbruch wie im Chat: Run-Counter hochzählen → laufendes Ergebnis (pruefSenden // und schnellBewerten) wird verworfen, Buttons sofort frei. Agent läuft serverseitig aus. function pruefAbbrechen() { @@ -414,8 +419,8 @@ function onPruefKey(e) {