update
This commit is contained in:
@@ -39,53 +39,41 @@ FRAGETYPEN = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def score_berechnen(
|
# Antwort-Niveau (wie sehr der Kern getroffen ist) → Punkt-Delta.
|
||||||
score_vor_frage: int, gut: bool, streak: int, tier2: bool, tier3: bool,
|
NIVEAUS = {
|
||||||
absolviert: bool, verstanden: bool, gemeistert: bool,
|
"kaum": -1, # < 25 % richtig
|
||||||
) -> int:
|
"teilweise": 0, # 25–49 %
|
||||||
"""Neuer Score nach einer Antwort · driftfrei (immer aus dem Basis-Score gerechnet).
|
"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).
|
def score_berechnen(
|
||||||
- Tier 2 (tier2, nicht tier3): +1 / −1, Boden 3, Deckel MASTERY (10).
|
score_vor_frage: int, niveau: str, nachgefragt: bool,
|
||||||
- Tier 3 (tier3, Meisterpfad): +1 / −2, Boden 10, Deckel MEISTERN (25).
|
tier2: bool, tier3: bool, absolviert: bool, verstanden: bool, gemeistert: bool,
|
||||||
Streak-Bonus bei richtig: ab 3 in Folge +2 (Tier 2/3), ab 5 in Folge +3 (Tier 3).
|
) -> int:
|
||||||
`streak` = bisherige Folge VOR dieser Antwort; mit dieser Antwort zählt streak+1.
|
"""Neuer Score · driftfrei (immer aus dem Basis-Score). Delta aus dem Antwort-Niveau:
|
||||||
Boden vor dem Absolvieren ist 0; gemeistert friert bei MEISTERN ein.
|
je besser, desto mehr (−1/0/+1/+2/+3). Bei Nachfragen ist der Gewinn auf +1 gedeckelt.
|
||||||
Re-Bewertung nutzt denselben Basis-Score + dieselbe Streak (kein Doppelzählen).
|
Danach auf Boden/Deckel der aktiven Stufe geklemmt (3 / 10 / 25). Kein Streak mehr.
|
||||||
"""
|
"""
|
||||||
if gemeistert:
|
if gemeistert:
|
||||||
return MEISTERN
|
return MEISTERN
|
||||||
# Stufe richtet sich nach dem Baustein-Fortschritt, nicht nur nach den
|
# Stufe nach Baustein-Fortschritt: frisch erst Tier 1 (0→3), dann ab absolviert/verstanden.
|
||||||
# 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.
|
|
||||||
tier2 = tier2 and absolviert
|
tier2 = tier2 and absolviert
|
||||||
tier3 = tier3 and verstanden
|
tier3 = tier3 and verstanden
|
||||||
if not tier2:
|
if not tier2:
|
||||||
floor, cap, strafe = (NOETIG if absolviert else 0), NOETIG, 0
|
floor, cap = (NOETIG if absolviert else 0), NOETIG
|
||||||
elif not tier3:
|
elif not tier3:
|
||||||
floor, cap, strafe = NOETIG, MASTERY, -1
|
floor, cap = NOETIG, MASTERY
|
||||||
else:
|
else:
|
||||||
floor, cap, strafe = MASTERY, MEISTERN, -2
|
floor, cap = MASTERY, MEISTERN
|
||||||
if gut:
|
delta = NIVEAUS.get(niveau, 0)
|
||||||
s = streak + 1
|
if nachgefragt:
|
||||||
delta = 3 if (tier3 and s >= 5) else (2 if (tier2 and s >= 3) else 1)
|
delta = min(delta, 1) # mit Hilfe (Nachfrage) höchstens +1
|
||||||
else:
|
|
||||||
delta = strafe
|
|
||||||
return max(floor, min(cap, score_vor_frage + delta))
|
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:
|
def _transcript(messages: list[dict]) -> str:
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
f"{'Nutzer' if m.get('role') == 'user' else 'Assistent'}: {m.get('content', '')}"
|
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:
|
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):
|
if not isinstance(data, dict):
|
||||||
return None
|
return None
|
||||||
feedback = str(data.get("feedback", "")).strip()
|
feedback = str(data.get("feedback", "")).strip()
|
||||||
bewertung = data.get("bewertung")
|
niveau = data.get("niveau")
|
||||||
if not feedback or bewertung not in ("gut", "schlecht"):
|
if not feedback or niveau not in NIVEAUS:
|
||||||
return None
|
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:
|
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:
|
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…".
|
# Deterministischer Guard gegen Doppelfragen — der KI-Kritiker übersieht „…, und welchen…".
|
||||||
@@ -215,12 +203,12 @@ async def _frage_mit_kritik(
|
|||||||
|
|
||||||
async def _bewertung_mit_kritik(
|
async def _bewertung_mit_kritik(
|
||||||
topic: str, baustein: str, section_block: str, kompakt_block: str,
|
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:
|
) -> 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
|
`frage` ankert die geprüfte Frage; der Dialog (transcript) liefert Antwort + Diskussion.
|
||||||
eine etwaige Diskussion — so kann eine Re-Bewertung das Argument sehen.
|
`begruendung_block` = optionale Unzufriedenheit des Lerners (nur bei „Gründlich prüfen").
|
||||||
`role` = "judge" (schnell) oder "guide" (gründlich, starkes Modell mit Thinking).
|
`role` = "judge" (schnell) oder "guide" (gründlich, starkes Modell mit Thinking).
|
||||||
"""
|
"""
|
||||||
timeout = GRUENDLICH_TIMEOUT if role == "guide" else PRUEFUNG_TIMEOUT
|
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,
|
"Baustein-Bewertung", role, _bewertung_schema, provider, timeout,
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
kompakt_block=kompakt_block, frage=frage, transcript=transcript,
|
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:
|
if bew is None:
|
||||||
return None
|
return None
|
||||||
@@ -285,22 +273,42 @@ async def pruefung_frage(
|
|||||||
return None
|
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(
|
async def pruefung_bewertung(
|
||||||
topic: str, baustein: str, section: str, kompakt: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
|
frage: str, messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
||||||
role: str = "judge",
|
role: str = "judge", begruendung: str = "",
|
||||||
) -> dict | None:
|
) -> 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".
|
`role` = "guide" für „Gründlich prüfen" (starkes Modell). `begruendung` = optionale
|
||||||
Gibt {"feedback", "bewertung": gut|schlecht, "bestanden"} · None bei Fehler.
|
Unzufriedenheit des Lerners mit einer früheren Bewertung.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
section_block, kompakt_block = _bloecke(section, kompakt)
|
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||||
transcript = _transcript(messages) if messages else "(leer)"
|
transcript = _transcript(messages) if messages else "(leer)"
|
||||||
|
begruendung_block = begruendung.strip() or "(keine)"
|
||||||
return await _bewertung_mit_kritik(
|
return await _bewertung_mit_kritik(
|
||||||
topic, baustein, section_block, kompakt_block,
|
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:
|
except Exception:
|
||||||
log.warning("[%s] Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
log.warning("[%s] Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||||
|
|||||||
@@ -214,11 +214,13 @@ class BausteinPruefungRequest(BaseModel):
|
|||||||
baustein: str = Field(min_length=1, max_length=200)
|
baustein: str = Field(min_length=1, max_length=200)
|
||||||
section: str = Field(default="", max_length=20000) # ausführliche Fassung
|
section: str = Field(default="", max_length=20000) # ausführliche Fassung
|
||||||
section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze)
|
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
|
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)
|
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
|
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
|
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
|
tier3: bool = False # ganzer Guide verstanden (alle ≥10) → Meisterpfad, −2 bei falsch, Deckel 25
|
||||||
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
|
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
|
||||||
@@ -230,9 +232,9 @@ class BausteinPruefungResponse(BaseModel):
|
|||||||
frage: str | None = None
|
frage: str | None = None
|
||||||
reply: str | None = None
|
reply: str | None = None
|
||||||
feedback: 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
|
gute_antworten: int
|
||||||
streak: int | None = None # nur antwort_pruefen setzt die persistierte Streak
|
|
||||||
absolviert: bool
|
absolviert: bool
|
||||||
verstanden: bool = False
|
verstanden: bool = False
|
||||||
gemeistert: bool = False
|
gemeistert: bool = False
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ from database import (
|
|||||||
list_progress, set_progress, delete_progress,
|
list_progress, set_progress, delete_progress,
|
||||||
create_element, list_elements, get_element, update_element, delete_element,
|
create_element, list_elements, get_element, update_element, delete_element,
|
||||||
list_baustein_progress, get_baustein_progress, set_offene_frage,
|
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 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 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 guide import generate_guide, guide_slot_dateien
|
||||||
from pipeline import cancel_guide
|
from pipeline import cancel_guide
|
||||||
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
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
|
return lock
|
||||||
|
|
||||||
|
|
||||||
def _basis(stand: dict, frage: str) -> tuple[int, int, bool]:
|
def _basis(stand: dict, frage: str) -> tuple[int, bool]:
|
||||||
"""Score-/Streak-Basis VOR der Frage. Gleiche offene Frage → Re-Bewertung auf derselben
|
"""Score-Basis VOR der Frage. Gleiche offene Frage → Re-Bewertung auf derselben Basis
|
||||||
Basis (idempotent); sonst neue Frage auf dem aktuellen Stand. → (basis, streak_basis, re_bewertung)."""
|
(idempotent); sonst neue Frage auf dem aktuellen Stand. → (basis, re_bewertung)."""
|
||||||
re_bewertung = stand["offene_frage"] == frage and stand["offene_basis"] is not None
|
re_bewertung = stand["offene_frage"] == frage and stand["offene_basis"] is not None
|
||||||
if re_bewertung:
|
return (stand["offene_basis"] if re_bewertung else stand["gute_antworten"]), 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
|
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)
|
@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")
|
raise HTTPException(502, "Diskussion fehlgeschlagen — bitte erneut versuchen")
|
||||||
return {"reply": reply, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
return {"reply": reply, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||||
|
|
||||||
# aktion == "antwort_pruefen" — mindestens eine Nutzer-Antwort muss im Dialog stehen
|
# aktion "antwort" (Agent 1 schnell) / "antwort_pruefen" (Agent 2 genau).
|
||||||
# (nach einer Diskussion endet der Dialog mit dem Tutor; Re-Bewertung bleibt erlaubt).
|
|
||||||
if not any(m.get("role") == "user" for m in msgs):
|
if not any(m.get("role") == "user" for m in msgs):
|
||||||
raise HTTPException(400, "Antwort braucht eine Nutzer-Antwort")
|
raise HTTPException(400, "Antwort braucht eine Nutzer-Antwort")
|
||||||
if not req.frage.strip():
|
if not req.frage.strip():
|
||||||
raise HTTPException(400, "Antwort braucht eine laufende Frage")
|
raise HTTPException(400, "Antwort braucht eine laufende Frage")
|
||||||
|
genau = req.aktion == "antwort_pruefen"
|
||||||
|
|
||||||
# Verbindlich, persistiert. Serialisiert je Baustein, damit die Basis immer den
|
# Persist serialisiert je Baustein, Basis driftfrei über den offene_frage-Anker
|
||||||
# geprüften Vorgänger-Stand sieht (kein Stale-Overwrite). Punkte kommen NUR hier.
|
# (schnell setzt den Anker, genau re-bewertet dieselbe Frage). Beide vergeben Punkte.
|
||||||
async with _pruef_lock(req.topic, req.baustein):
|
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
|
absolviert = stand["absolviert"] is not None
|
||||||
verstanden = stand["verstanden"] is not None
|
verstanden = stand["verstanden"] is not None
|
||||||
gemeistert = stand["gemeistert"] 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:
|
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, 0)
|
||||||
await set_offene_frage(req.topic, req.baustein, req.frage, basis, streak_basis)
|
|
||||||
|
|
||||||
data = await pruefung_bewertung(
|
if genau:
|
||||||
req.topic, req.baustein, req.section, kompakt, req.frage, msgs, basis, provider=req.provider,
|
data = await pruefung_bewertung(
|
||||||
role="guide" if req.gruendlich else "judge",
|
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:
|
if data is None:
|
||||||
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
||||||
|
|
||||||
score = score_berechnen(
|
niveau = data["niveau"]
|
||||||
basis, data["bewertung"] == "gut", streak_basis, req.tier2, req.tier3, absolviert, verstanden, gemeistert,
|
score = score_berechnen(basis, niveau, req.nachgefragt, req.tier2, req.tier3, absolviert, verstanden, gemeistert)
|
||||||
)
|
|
||||||
war_absolviert, war_verstanden = absolviert, verstanden
|
|
||||||
gute = await set_baustein_score(req.topic, req.baustein, score)
|
gute = await set_baustein_score(req.topic, req.baustein, score)
|
||||||
if score >= NOETIG and not absolviert:
|
if score >= NOETIG and not absolviert:
|
||||||
absolviert = True
|
absolviert = True
|
||||||
@@ -326,13 +330,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
if score >= MEISTERN and not gemeistert:
|
if score >= MEISTERN and not gemeistert:
|
||||||
await set_baustein_gemeistert(req.topic, req.baustein)
|
await set_baustein_gemeistert(req.topic, req.baustein)
|
||||||
gemeistert = True
|
gemeistert = True
|
||||||
# Streak persistieren — Reset nur am Deckel der gerade aktiven Stufe.
|
return {"feedback": data["feedback"], "niveau": niveau, "bewertung": _farbe(niveau), "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||||
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}
|
|
||||||
|
|
||||||
|
|
||||||
# --- Guides ---
|
# --- Guides ---
|
||||||
|
|||||||
@@ -78,12 +78,12 @@ export async function chatBaustein({ topic, baustein, section, section_kompakt =
|
|||||||
export async function pruefeBaustein({
|
export async function pruefeBaustein({
|
||||||
topic, baustein, section, section_kompakt = '', provider,
|
topic, baustein, section, section_kompakt = '', provider,
|
||||||
aktion = 'frage', frage = '', letzte_bewertung = '', vermeide = [],
|
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`, {
|
const res = await fetch(`${BASE}/bausteine/pruefung`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ const letztesFeedback = slot.letztesFeedback // Kontext für die Diskussion üb
|
|||||||
// Transient (pro Instanz): Eingabe-Entwurf, Lade-Spinner.
|
// Transient (pro Instanz): Eingabe-Entwurf, Lade-Spinner.
|
||||||
const pruefInput = ref('')
|
const pruefInput = ref('')
|
||||||
const pruefLoading = ref(false)
|
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 pruefMessagesEl = ref(null)
|
||||||
const pruefInputEl = ref(null)
|
const pruefInputEl = ref(null)
|
||||||
@@ -109,6 +112,8 @@ async function pruefSenden(payload, onOk) {
|
|||||||
function frageZeigen(text) {
|
function frageZeigen(text) {
|
||||||
aktuelleFrage.value = text
|
aktuelleFrage.value = text
|
||||||
letztesFeedback.value = ''
|
letztesFeedback.value = ''
|
||||||
|
nachgefragt.value = false
|
||||||
|
gruendlichMsg.value = null
|
||||||
pruefMessages.value.push({ role: 'assistant', kind: 'frage', content: text })
|
pruefMessages.value.push({ role: 'assistant', kind: 'frage', content: text })
|
||||||
pruefPhase.value = 'frage_offen'
|
pruefPhase.value = 'frage_offen'
|
||||||
}
|
}
|
||||||
@@ -206,6 +211,7 @@ async function naechsteFrageZeigen() {
|
|||||||
function nachfragen() {
|
function nachfragen() {
|
||||||
const text = pruefInput.value.trim()
|
const text = pruefInput.value.trim()
|
||||||
if (!text || pruefLoading.value) return
|
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 })
|
pruefMessages.value.push({ role: 'user', kind: 'nachfrage', content: text })
|
||||||
pruefInput.value = ''
|
pruefInput.value = ''
|
||||||
pruefSenden(
|
pruefSenden(
|
||||||
@@ -214,13 +220,15 @@ function nachfragen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let letzteFeedbackMsg = null // Referenz auf die zuletzt gezeigte Bewertungs-Bubble
|
let letzteFeedbackMsg = null // zuletzt gezeigte Bewertungs-Bubble
|
||||||
let pruefenRun = 0 // nur die jüngste Bewertung darf die Anzeige setzen
|
let pruefenRun = 0 // nur die jüngste Schnell-Bewertung darf anzeigen
|
||||||
|
|
||||||
// Antwort verbindlich prüfen — der Prüfer (Bewerter + Kritiker) vergibt die Punkte.
|
function bewertungPayload() {
|
||||||
// Während des Wartens zeigt das „…"-Bubble den Geprüft-Zustand. gruendlich=true:
|
return { frage: aktuelleFrage.value, tier2: props.tier2, tier3: props.tier3, nachgefragt: nachgefragt.value }
|
||||||
// starkes Modell, re-bewertet dieselbe Frage und aktualisiert die bestehende Bubble.
|
}
|
||||||
async function bewertenLassen(gruendlich = false) {
|
|
||||||
|
// Agent 1 (schnell): sofortiges Niveau + Punkte zeigen, dann Agent 2 (genau) im Hintergrund.
|
||||||
|
async function schnellBewerten() {
|
||||||
const meine = ++pruefenRun
|
const meine = ++pruefenRun
|
||||||
pruefStick.value = true
|
pruefStick.value = true
|
||||||
pruefLoading.value = true
|
pruefLoading.value = true
|
||||||
@@ -228,22 +236,17 @@ async function bewertenLassen(gruendlich = false) {
|
|||||||
try {
|
try {
|
||||||
const res = await pruefeBaustein({
|
const res = await pruefeBaustein({
|
||||||
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
||||||
provider: props.provider, messages: pruefDialog(),
|
provider: props.provider, messages: pruefDialog(), aktion: 'antwort', ...bewertungPayload(),
|
||||||
aktion: 'antwort_pruefen', frage: aktuelleFrage.value, tier2: props.tier2, tier3: props.tier3, gruendlich,
|
|
||||||
})
|
})
|
||||||
if (meine !== pruefenRun) return
|
if (meine !== pruefenRun) return
|
||||||
applyPruefung(res)
|
applyPruefung(res)
|
||||||
letztesFeedback.value = res.feedback || ''
|
letztesFeedback.value = res.feedback || ''
|
||||||
if (gruendlich && letzteFeedbackMsg) {
|
pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung, niveau: res.niveau, geprueft: false })
|
||||||
letzteFeedbackMsg.content = res.feedback || letzteFeedbackMsg.content
|
letzteFeedbackMsg = pruefMessages.value[pruefMessages.value.length - 1]
|
||||||
letzteFeedbackMsg.bewertung = res.bewertung
|
pruefPhase.value = 'bewertet'
|
||||||
} 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'
|
|
||||||
}
|
|
||||||
pruefScroll()
|
pruefScroll()
|
||||||
nextTick(() => pruefInputEl.value?.focus())
|
nextTick(() => pruefInputEl.value?.focus())
|
||||||
|
genauBewerten() // Agent 2 korrigiert im Hintergrund
|
||||||
} catch {
|
} catch {
|
||||||
if (meine === pruefenRun) pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Hat nicht geklappt — bitte erneut.' })
|
if (meine === pruefenRun) pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Hat nicht geklappt — bitte erneut.' })
|
||||||
} finally {
|
} 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() {
|
function antwortAbgeben() {
|
||||||
const text = pruefInput.value.trim()
|
const text = pruefInput.value.trim()
|
||||||
if (!text || pruefLoading.value) return
|
if (!text || pruefLoading.value) return
|
||||||
pruefMessages.value.push({ role: 'user', kind: 'antwort', content: text })
|
pruefMessages.value.push({ role: 'user', kind: 'antwort', content: text })
|
||||||
pruefInput.value = ''
|
pruefInput.value = ''
|
||||||
bewertenLassen(false)
|
schnellBewerten()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lief schon eine Diskussion? Dann darf der Verlauf direkt bewertet werden.
|
// 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).
|
// „Verlauf bewerten": den bisherigen Dialog als Antwort werten (Feld wird ignoriert).
|
||||||
function verlaufBewerten() {
|
function verlaufBewerten() {
|
||||||
if (pruefLoading.value) return
|
if (pruefLoading.value) return
|
||||||
bewertenLassen(false)
|
schnellBewerten()
|
||||||
}
|
}
|
||||||
|
|
||||||
// „Gründlich prüfen": dieselbe Antwort vom starken Modell (role guide) neu bewerten.
|
// Rechtsklick auf eine Bewertung → Gründlich-Feld öffnen; Absenden re-bewertet (mit Begründung).
|
||||||
function gruendlichPruefen() {
|
function gruendlichOeffnen(msg) {
|
||||||
|
gruendlichText.value = ''
|
||||||
|
gruendlichMsg.value = msg
|
||||||
|
nextTick(() => document.getElementById('bp-gruendlich-input')?.focus())
|
||||||
|
}
|
||||||
|
function gruendlichAbsenden() {
|
||||||
if (pruefLoading.value) return
|
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
|
// 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() {
|
function pruefAbbrechen() {
|
||||||
if (!pruefLoading.value) return
|
if (!pruefLoading.value) return
|
||||||
pruefRun++
|
pruefRun++
|
||||||
@@ -298,10 +333,9 @@ onUnmounted(() => {
|
|||||||
window.removeEventListener('keydown', onPruefKey)
|
window.removeEventListener('keydown', onPruefKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ALT+1 Nachfragen/Nachhaken · ALT+2 primärer Button je Phase (idle: Frage anfordern,
|
// 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 / Gründlich.
|
// frage_offen: Antwort abgeben, bewertet: Nächste Frage) · ALT+3 Verlauf bewerten (frage_offen).
|
||||||
// window-Listener (nicht div), damit es auch ohne Fokus im Feld greift + den Browser-Default
|
// Gründlich prüfen läuft per Rechtsklick auf die Bewertung, nicht über ALT.
|
||||||
// (Tab-Wechsel) unterdrückt. Nur im Prüfungs-Tab.
|
|
||||||
function onPruefKey(e) {
|
function onPruefKey(e) {
|
||||||
if (!e.altKey || e.repeat) return
|
if (!e.altKey || e.repeat) return
|
||||||
if (props.mode !== 'full' || activeTab.value !== 'pruefung') return
|
if (props.mode !== 'full' || activeTab.value !== 'pruefung') return
|
||||||
@@ -309,14 +343,13 @@ function onPruefKey(e) {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (pruefLoading.value) return
|
if (pruefLoading.value) return
|
||||||
if (e.key === '1') {
|
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') {
|
} else if (e.key === '2') {
|
||||||
if (pruefPhase.value === 'idle') frageAnfordern()
|
if (pruefPhase.value === 'idle') frageAnfordern()
|
||||||
else if (pruefPhase.value === 'frage_offen') { if (pruefInput.value.trim()) antwortAbgeben() }
|
else if (pruefPhase.value === 'frage_offen') { if (pruefInput.value.trim()) antwortAbgeben() }
|
||||||
else naechsteFrageZeigen()
|
else naechsteFrageZeigen()
|
||||||
} else if (e.key === '3') {
|
} else if (e.key === '3') {
|
||||||
if (pruefPhase.value === 'bewertet') gruendlichPruefen()
|
if (pruefPhase.value === 'frage_offen' && hatDiskussion.value) verlaufBewerten()
|
||||||
else if (pruefPhase.value === 'frage_offen' && hatDiskussion.value) verlaufBewerten()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -372,16 +405,23 @@ function onPruefKey(e) {
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<p class="bp-hint">
|
<p class="bp-hint">
|
||||||
<template v-if="st.gemeistert">✓✓✓ Gemeistert ({{ MEISTERN }}/{{ MEISTERN }}) — Max. Du kannst dich weiter prüfen, ohne Punkte.</template>
|
<template v-if="st.gemeistert">✓✓✓ Gemeistert ({{ MEISTERN }}/{{ MEISTERN }}) — Max. Du kannst dich weiter prüfen, ohne Punkte.</template>
|
||||||
<template v-else-if="st.verstanden && tier3">Meisterpfad: {{ st.gute_antworten }}/{{ MEISTERN }}. Richtig = +1, falsch = −2 (nicht unter {{ MAX }}). Bei {{ MEISTERN }} gemeistert.</template>
|
<template v-else-if="st.verstanden && tier3">Meisterpfad: {{ st.gute_antworten }}/{{ MEISTERN }}. Je besser, desto mehr (+1/+2/+3), schwach = −1 (nicht unter {{ MAX }}). Bei {{ MEISTERN }} gemeistert.</template>
|
||||||
<template v-else-if="st.verstanden">✓✓ Verstanden. Der Meisterpfad ({{ MAX }}→{{ MEISTERN }}) öffnet, sobald der ganze Guide verstanden ist.</template>
|
<template v-else-if="st.verstanden">✓✓ Verstanden. Der Meisterpfad ({{ MAX }}→{{ MEISTERN }}) öffnet, sobald der ganze Guide verstanden ist.</template>
|
||||||
<template v-else-if="st.absolviert && tier2">Mastery: {{ st.gute_antworten }}/{{ MAX }}. Richtig = +1, falsch = −1 (nicht unter {{ NOETIG }}). Bei {{ MAX }} verstanden.</template>
|
<template v-else-if="st.absolviert && tier2">Mastery: {{ st.gute_antworten }}/{{ MAX }}. Je besser, desto mehr (+1/+2/+3), schwach = −1 (nicht unter {{ NOETIG }}). Bei {{ MAX }} verstanden.</template>
|
||||||
<template v-else-if="st.absolviert">✓ Absolviert. Mehr ({{ NOETIG }}→{{ MAX }}) gibt's, sobald der ganze Guide absolviert ist.</template>
|
<template v-else-if="st.absolviert">✓ Absolviert. Mehr ({{ NOETIG }}→{{ MAX }}) gibt's, sobald der ganze Guide absolviert ist.</template>
|
||||||
<template v-else>{{ Math.min(st.gute_antworten, NOETIG) }}/{{ NOETIG }} guten Antworten. Frag nach, wenn etwas unklar ist — diskutieren ist erlaubt.</template>
|
<template v-else>{{ Math.min(st.gute_antworten, NOETIG) }}/{{ NOETIG }} guten Antworten. Je besser deine Antwort, desto mehr Punkte (bis +3). Nachfragen ist erlaubt — dann max +1.</template>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-if="pruefMessages.length" ref="pruefMessagesEl" class="bp-messages" @scroll="onPruefScroll">
|
<div v-if="pruefMessages.length" ref="pruefMessagesEl" class="bp-messages" @scroll="onPruefScroll">
|
||||||
<template v-for="(m, i) in pruefMessages" :key="i">
|
<template v-for="(m, i) in pruefMessages" :key="i">
|
||||||
<div v-if="m.kind === 'feedback'" class="bp-feedback" :class="m.bewertung">{{ m.content }}<span v-if="!m.geprueft" class="bp-pruefend"> · wird geprüft…</span></div>
|
<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="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>
|
||||||
|
<button class="bp-action" @click="gruendlichMsg = null">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else-if="m.kind === 'fehler'" class="bp-error">{{ m.content }}</div>
|
<div v-else-if="m.kind === 'fehler'" class="bp-error">{{ m.content }}</div>
|
||||||
<div v-else-if="m.role === 'assistant'" class="bp-msg assistant markdown" v-html="renderMarkdown(m.content)"></div>
|
<div v-else-if="m.role === 'assistant'" class="bp-msg assistant markdown" v-html="renderMarkdown(m.content)"></div>
|
||||||
<div v-else class="bp-msg user">{{ m.content }}</div>
|
<div v-else class="bp-msg user">{{ m.content }}</div>
|
||||||
@@ -395,14 +435,14 @@ function onPruefKey(e) {
|
|||||||
<button v-else class="bp-action primary" @click="frageAnfordern">Frage anfordern</button>
|
<button v-else class="bp-action primary" @click="frageAnfordern">Frage anfordern</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Phase frage_offen / bewertet: Textfeld + Aktionen -->
|
<!-- Phase frage_offen: Textfeld + Aktionen · bewertet: nur „Nächste Frage" -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="bp-input">
|
<div v-if="pruefPhase === 'frage_offen'" class="bp-input">
|
||||||
<textarea
|
<textarea
|
||||||
ref="pruefInputEl"
|
ref="pruefInputEl"
|
||||||
v-model="pruefInput"
|
v-model="pruefInput"
|
||||||
rows="2"
|
rows="2"
|
||||||
:placeholder="pruefPhase === 'frage_offen' ? 'Antwort — oder Nachfrage bei Unklarheit…' : 'Nachhaken oder diskutieren…'"
|
placeholder="Antwort — oder Nachfrage bei Unklarheit…"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="bp-actions">
|
<div class="bp-actions">
|
||||||
@@ -413,9 +453,7 @@ function onPruefKey(e) {
|
|||||||
<button v-if="hatDiskussion" class="bp-action" title="Alt+3" @click="verlaufBewerten"><span class="bp-kbd">3</span>Verlauf bewerten</button>
|
<button v-if="hatDiskussion" class="bp-action" title="Alt+3" @click="verlaufBewerten"><span class="bp-kbd">3</span>Verlauf bewerten</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button class="bp-action" title="Alt+1" :disabled="!pruefInput.trim()" @click="nachfragen"><span class="bp-kbd">1</span>Nachhaken</button>
|
|
||||||
<button class="bp-action primary" title="Alt+2" @click="naechsteFrageZeigen"><span class="bp-kbd">2</span>Nächste Frage</button>
|
<button class="bp-action primary" title="Alt+2" @click="naechsteFrageZeigen"><span class="bp-kbd">2</span>Nächste Frage</button>
|
||||||
<button class="bp-action" title="Alt+3" @click="gruendlichPruefen"><span class="bp-kbd">3</span>Gründlich prüfen</button>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -530,8 +568,18 @@ function onPruefKey(e) {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
.bp-pruefend { font-style: italic; opacity: 0.7; font-size: 0.92em; }
|
.bp-pruefend { font-style: italic; opacity: 0.7; font-size: 0.92em; }
|
||||||
|
.bp-feedback { cursor: context-menu; }
|
||||||
.bp-feedback.gut { background: var(--success-soft); border-color: var(--success-border); color: var(--success); }
|
.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); }
|
.bp-feedback.schlecht { background: var(--danger-soft, #fee2e2); border-color: var(--danger-border, #f87171); color: var(--danger); }
|
||||||
|
.bp-niveau { font-weight: 700; text-transform: uppercase; font-size: 0.62rem; letter-spacing: 0.03em; margin-right: 0.4rem; opacity: 0.85; }
|
||||||
|
/* Gründlich-prüfen-Feld (per Rechtsklick) innerhalb der Bewertungs-Bubble */
|
||||||
|
.bp-gruendlich { display: flex; gap: 0.3rem; margin-top: 0.4rem; align-items: center; }
|
||||||
|
.bp-gruendlich input {
|
||||||
|
flex: 1; min-width: 0; padding: 0.3rem 0.5rem; font: inherit; font-size: 0.8rem;
|
||||||
|
border: 1px solid var(--border-strong); border-radius: 6px; background: var(--panel); color: var(--text);
|
||||||
|
}
|
||||||
|
.bp-gruendlich .bp-action { margin-top: 0; }
|
||||||
|
|
||||||
.bp-input { display: flex; gap: 0.4rem; margin-top: 0.55rem; align-items: flex-end; }
|
.bp-input { display: flex; gap: 0.4rem; margin-top: 0.55rem; align-items: flex-end; }
|
||||||
.bp-input textarea {
|
.bp-input textarea {
|
||||||
|
|||||||
@@ -12,19 +12,22 @@ KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
|||||||
PRÜFUNGS-VERLAUF (Antwort des Lerners und etwaige Diskussion):
|
PRÜFUNGS-VERLAUF (Antwort des Lerners und etwaige Diskussion):
|
||||||
{transcript}
|
{transcript}
|
||||||
|
|
||||||
ZU PRÜFENDE BEWERTUNG:
|
ZU PRÜFENDE BEWERTUNG (enthält das vergebene Niveau):
|
||||||
{bewertung_block}
|
{bewertung_block}
|
||||||
|
|
||||||
PRÜFE GEGEN DIESE KRITERIEN:
|
NIVEAU-SKALA (Anteil des getroffenen Kerns):
|
||||||
- 50%-Schwelle: Eine Antwort, die den Kern trifft und MINDESTENS zur Hälfte korrekt ist, MUSS "gut" sein. Nur weniger als die Hälfte oder klar falsch ist "schlecht".
|
- kaum < 25 % · teilweise 25–49 % · solide 50–74 % · stark 75–99 % · komplett 100 %.
|
||||||
- Kein Fordern über das Material hinaus: Die Bewertung darf NICHTS verlangen, was nicht aus Guide/Vertiefung folgt. Wurde der Lerner für fehlenden guide-externen Stoff abgewertet → Fehlurteil. „Nicht im Material" darf nie zu seinen Lasten gehen.
|
|
||||||
- Kein Ablese-Test: Wurde eine sachlich RICHTIGE Antwort als "schlecht" gewertet, nur weil sie nicht den Guide-Begriff nutzt oder über den Guide hinausgeht → Fehlurteil. Korrekte Synonyme und gleichbedeutende Begriffe müssen "gut" sein.
|
|
||||||
- Asymmetrie: Weltwissen darf nur zum ANERKENNEN richtiger Antworten dienen, nie zum strengeren Fordern.
|
|
||||||
- "schlecht" ist nur fair, wenn die Antwort sachlich falsch ist oder dem Material widerspricht.
|
|
||||||
- KEIN Durchwinken: Wurde eine sachlich FALSCHE oder dem Material widersprechende Antwort als "gut" gewertet → Fehlurteil. Prüfe die Antwort SELBST auf Korrektheit (gegen Material UND Logik), nicht nur die Fairness — in BEIDE Richtungen.
|
|
||||||
- Widerspruchs-Check: Widerspricht sich das Feedback selbst (bejaht die Antwort UND nennt zugleich die Gegen-Lösung) oder passt es nicht zur Bewertung → Fehlurteil.
|
|
||||||
|
|
||||||
Beanstande NUR echte Fehlurteile. Ist die Bewertung fair, korrekt und materialtreu, ist sie in Ordnung.
|
PRÜFE GEGEN DIESE KRITERIEN:
|
||||||
|
- Passt das Niveau zum tatsächlichen Anteil des Kerns? Zu STRENG (richtige/vollständige Antwort zu niedrig) ODER zu MILDE (falsche/dünne Antwort zu hoch) → Fehlurteil. Beide Richtungen prüfen.
|
||||||
|
- Kern sachlich FALSCH (Gegenteil) → muss "kaum" sein, egal wie selbstsicher.
|
||||||
|
- Kein Fordern über das Material hinaus: „Nicht im Material" darf nie zu Lasten gehen.
|
||||||
|
- Kein Ablese-Test: sachlich RICHTIG mit anderen Worten/Synonymen ist voll zu werten, nicht abzuwerten.
|
||||||
|
- Asymmetrie: Weltwissen nur zum ANERKENNEN richtiger Antworten, nie zum strengeren Fordern.
|
||||||
|
- Widerspruchs-Check: Feedback passt zum Niveau und widerspricht sich nicht.
|
||||||
|
- Prüfe die Antwort SELBST auf Korrektheit (Material UND Logik), nicht nur die Fairness.
|
||||||
|
|
||||||
|
Beanstande NUR echte Fehlurteile. Ist das Niveau fair, korrekt und materialtreu, ist es in Ordnung.
|
||||||
|
|
||||||
Gib NUR JSON aus (kein weiterer Text):
|
Gib NUR JSON aus (kein weiterer Text):
|
||||||
- Bewertung in Ordnung: {{"ok": true}}
|
- Bewertung in Ordnung: {{"ok": true}}
|
||||||
|
|||||||
@@ -9,38 +9,41 @@ BAUSTEIN AUS DEM GUIDE:
|
|||||||
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
||||||
{kompakt_block}
|
{kompakt_block}
|
||||||
|
|
||||||
STAND: {gute_antworten} von {noetig} Antworten waren bisher gut.
|
|
||||||
|
|
||||||
PRÜFUNGS-VERLAUF (Antwort des Lerners und etwaige Diskussion):
|
PRÜFUNGS-VERLAUF (Antwort des Lerners und etwaige Diskussion):
|
||||||
{transcript}
|
{transcript}
|
||||||
|
|
||||||
Bewerte die Antwort des Lerners auf die GEPRÜFTE FRAGE — auf Basis seiner Antwort UND der Diskussion im Verlauf.
|
UNZUFRIEDENHEIT DES LERNERS MIT EINER FRÜHEREN BEWERTUNG (falls vorhanden — ernst nehmen, aber nur nachgeben, wenn er sachlich recht hat):
|
||||||
|
{begruendung_block}
|
||||||
|
|
||||||
|
Bewerte die Antwort auf die GEPRÜFTE FRAGE — auf Basis der Antwort UND der Diskussion im Verlauf.
|
||||||
|
|
||||||
FINALER STAND ZÄHLT — nicht die erste Aussage:
|
FINALER STAND ZÄHLT — nicht die erste Aussage:
|
||||||
- Bewerte den Erkenntnisstand, den der Lerner am ENDE des Verlaufs SELBST erreicht hat.
|
- Bewerte den Erkenntnisstand, den der Lerner am ENDE des Verlaufs SELBST erreicht hat.
|
||||||
- Eine zuerst falsche, dann im Dialog selbst korrigierte und richtig formulierte Einsicht ist "gut". Die Diskussion ist ein erlaubter Lernweg.
|
- Zuerst falsch, dann im Dialog selbst korrigiert = zählt (erlaubter Lernweg).
|
||||||
- ABER: Hat der Tutor die Lösung vorgesagt und der Lerner sie nur bestätigt oder nachgesprochen ("ja", "genau", bloße Wiederholung), ist das "schlecht". Der tragende Schluss muss erkennbar vom Lerner kommen.
|
- ABER: Hat der Tutor die Lösung vorgesagt und der Lerner sie nur nachgesprochen ("ja", "genau", bloße Wiederholung), zählt sie NICHT. Der tragende Schluss muss vom Lerner kommen.
|
||||||
|
|
||||||
SO BEWERTEST DU — die 50%-Schwelle:
|
NIVEAU — wie viel vom KERN der Frage ist richtig getroffen? Wähle GENAU EINS:
|
||||||
- "gut" = der Kern der Antwort ist sachlich RICHTIG und mindestens zur Hälfte korrekt. Eine knappe, richtige Antwort reicht.
|
- "kaum": unter 25 % — fast nichts richtig, oder klar falsch / das Gegenteil.
|
||||||
- "schlecht" = weniger als die Hälfte richtig, oder klar falsch.
|
- "teilweise": 25–49 % — ein Bruchstück stimmt, der Kern fehlt.
|
||||||
- Ist der Kern sachlich FALSCH (z. B. das Gegenteil der richtigen Aussage), ist das "schlecht" — egal wie selbstsicher es formuliert ist. Die Lockerheit unten gilt NUR für richtige Inhalte, nie um Falsches zu retten.
|
- "solide": 50–74 % — der Kern ist richtig getroffen, Details fehlen.
|
||||||
- Die Hälfte misst sich an dem, was die FRAGE verlangt — in JEDER korrekten Formulierung, nicht am Wortlaut des Guides. KEINE ideale Vollantwort verlangen.
|
- "stark": 75–99 % — größtenteils vollständig und richtig, nur eine kleine Lücke.
|
||||||
- MASSSTAB = die ursprünglich GEPRÜFTE FRAGE, NICHT tiefere Folge-Fragen aus der Diskussion. Hat der Lerner die gestellte Frage korrekt beantwortet, ist es "gut" — auch wenn der Tutor danach weiter gebohrt hat und ein noch genaueres Detail offenblieb. Tieferes Bohren erhöht die Messlatte NICHT.
|
- "komplett": 100 % — der Kern ist vollständig und korrekt beantwortet.
|
||||||
|
|
||||||
|
MASSSTAB:
|
||||||
|
- Gemessen an dem, was die FRAGE verlangt — in JEDER korrekten Formulierung, nicht am Wortlaut des Guides. Für "komplett" muss die Frage erfüllt sein, mehr nicht — verlange keine ideale Vollantwort.
|
||||||
|
- Ist der Kern sachlich FALSCH (Gegenteil), ist es "kaum" — egal wie selbstsicher formuliert.
|
||||||
|
- Maßstab ist die ursprünglich GEPRÜFTE FRAGE, nicht tiefere Folge-Fragen. Tieferes Bohren erhöht die Messlatte NICHT.
|
||||||
|
|
||||||
DU PRÜFST VERSTÄNDNIS, NICHT ABLESEN — asymmetrische Material-Grenze:
|
DU PRÜFST VERSTÄNDNIS, NICHT ABLESEN — asymmetrische Material-Grenze:
|
||||||
- FORDERN: Verlange NIE mehr, als Frage und Material hergeben. Kein Detail, keine Technik, kein Begriff, der über das Material hinausgeht, darf gefordert werden. Zeigt der Lerner zu Recht, dass etwas nicht im Material steht → KEIN Fehler.
|
- FORDERN: Verlange NIE mehr, als Frage und Material hergeben. „Nicht im Material" geht nie zu Lasten des Lerners.
|
||||||
- AKZEPTIEREN: Eine sachlich RICHTIGE Antwort ist "gut" — auch mit anderen Worten als der Guide oder mit korrektem Wissen ÜBER den Guide hinaus. Korrekte Synonyme und gleichbedeutende Begriffe zählen voll. Mehr-richtig ist ein Plus, NIE ein Minus.
|
- AKZEPTIEREN: Sachlich RICHTIG zählt hoch — auch mit anderen Worten oder korrektem Wissen über den Guide hinaus. Synonyme zählen voll.
|
||||||
- "schlecht" NUR, wenn die Antwort sachlich FALSCH ist oder dem Material WIDERSPRICHT — NIEMALS, weil sie nicht den Guide-Begriff nutzt.
|
- WELTWISSEN: nur zum ANERKENNEN richtiger Antworten, nie zum strengeren Fordern.
|
||||||
- WELTWISSEN: Nutze es, um eine richtige Antwort als "gut" ANZUERKENNEN. Nutze es NIE, um mehr zu fordern oder strenger zu prüfen.
|
- Behaupte nichts Erfundenes. Gib nach, wenn der Lerner sachlich recht hat — nicht aus Höflichkeit oder auf bloßes Beharren.
|
||||||
- Behaupte selbst nichts Erfundenes. Gib nach, wenn der Lerner sachlich recht hat — aber NICHT aus Höflichkeit oder auf bloßes Beharren hin.
|
|
||||||
|
|
||||||
FELDER:
|
FELD `feedback`: max. 1 Satz, sprich den Lerner direkt an. Begründe knapp das Niveau. KEINE neue Frage. Kein Widerspruch — bejahe nicht die Antwort und nenne zugleich die Gegen-Lösung.
|
||||||
- `feedback`: max. 1 Satz, sprich den Lerner direkt an. Begründe knapp, warum gut oder schlecht. KEINE neue Frage. Kein Widerspruch — bejahe nicht die Antwort und nenne zugleich die Gegen-Lösung; entscheide dich.
|
|
||||||
- `bestanden`: true NUR, wenn du schon vor den {noetig} guten Antworten sicher bist, dass der Lerner den Baustein versteht. Im Zweifel false.
|
|
||||||
|
|
||||||
HINWEISE DES PRÜFERS ZUR LETZTEN FASSUNG:
|
HINWEISE DES PRÜFERS ZUR LETZTEN FASSUNG:
|
||||||
{kritik_block}
|
{kritik_block}
|
||||||
|
|
||||||
Gib NUR dieses JSON aus (kein weiterer Text):
|
Gib NUR dieses JSON aus (kein weiterer Text):
|
||||||
{{"feedback": "ein Satz", "bewertung": "gut" | "schlecht", "bestanden": false}}
|
{{"feedback": "ein Satz", "niveau": "kaum" | "teilweise" | "solide" | "stark" | "komplett"}}
|
||||||
|
|||||||
Reference in New Issue
Block a user