This commit is contained in:
team3
2026-06-19 14:13:22 +02:00
parent 0868a768a2
commit ba529e61a8
3 changed files with 40 additions and 27 deletions

View File

@@ -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

View File

@@ -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 ---

View File

@@ -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) {
<div v-if="pruefMessages.length" ref="pruefMessagesEl" class="bp-messages" @scroll="onPruefScroll">
<template v-for="(m, i) in pruefMessages" :key="i">
<div v-if="m.kind === 'feedback'" class="bp-feedback" :class="m.bewertung" title="Rechtsklick: gründlich prüfen" @contextmenu.prevent="gruendlichOeffnen(m)">
<span v-if="m.niveau" class="bp-niveau">{{ m.niveau }}</span>{{ m.content }}<span v-if="!m.geprueft" class="bp-pruefend"> · wird geprüft</span>
<div v-if="m.kind === 'feedback'" class="bp-feedback" :class="m.bewertung" title="Klick: gründlich prüfen" @click="gruendlichOeffnen(m)">
<span v-if="m.punkte != null" class="bp-niveau">{{ punkteLabel(m.punkte) }}</span>{{ m.content }}<span v-if="!m.geprueft" class="bp-pruefend"> · wird geprüft</span>
<div v-if="gruendlichMsg === m" class="bp-gruendlich" @click.stop>
<input id="bp-gruendlich-input" v-model="gruendlichText" placeholder="Warum unzufrieden? (optional)" @keyup.enter="gruendlichAbsenden" />
<button class="bp-action primary" @click="gruendlichAbsenden">Gründlich prüfen</button>
@@ -568,7 +573,7 @@ function onPruefKey(e) {
border: 1px solid var(--border);
}
.bp-pruefend { font-style: italic; opacity: 0.7; font-size: 0.92em; }
.bp-feedback { cursor: context-menu; }
.bp-feedback { cursor: pointer; }
.bp-feedback.gut { background: var(--success-soft); border-color: var(--success-border); color: var(--success); }
.bp-feedback.neutral { background: var(--warning-soft); border-color: var(--warning-border); color: var(--warning); }
.bp-feedback.schlecht { background: var(--danger-soft, #fee2e2); border-color: var(--danger-border, #f87171); color: var(--danger); }