update
This commit is contained in:
@@ -159,16 +159,20 @@ async def _frage_mit_kritik(
|
|||||||
|
|
||||||
async def _bewertung_mit_kritik(
|
async def _bewertung_mit_kritik(
|
||||||
topic: str, baustein: str, section_block: str, vertiefung_block: str,
|
topic: str, baustein: str, section_block: str, vertiefung_block: str,
|
||||||
transcript: str, gute_antworten: int, provider: str,
|
frage: str, transcript: str, gute_antworten: int, provider: str,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""Letzte Antwort bewerten, vom Kritiker prüfen lassen, bei Fehlurteil neu."""
|
"""Antwort zur Frage bewerten, vom Kritiker prüfen lassen, bei Fehlurteil neu.
|
||||||
|
|
||||||
|
`frage` ankert, welche Frage geprüft wird; der Dialog (transcript) liefert die
|
||||||
|
Antwort und eine etwaige Diskussion — so kann eine Re-Bewertung das Argument sehen.
|
||||||
|
"""
|
||||||
kritik_block = "(keine)"
|
kritik_block = "(keine)"
|
||||||
bew = None
|
bew = None
|
||||||
for _ in range(KRITIK_MAX_RUNDEN):
|
for _ in range(KRITIK_MAX_RUNDEN):
|
||||||
bew = await _gen_call(
|
bew = await _gen_call(
|
||||||
"Baustein-Bewertung", "judge", _bewertung_schema, provider,
|
"Baustein-Bewertung", "judge", _bewertung_schema, provider,
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
vertiefung_block=vertiefung_block, transcript=transcript,
|
vertiefung_block=vertiefung_block, frage=frage, transcript=transcript,
|
||||||
gute_antworten=gute_antworten, noetig=NOETIG, kritik_block=kritik_block,
|
gute_antworten=gute_antworten, noetig=NOETIG, kritik_block=kritik_block,
|
||||||
)
|
)
|
||||||
if bew is None:
|
if bew is None:
|
||||||
@@ -176,7 +180,7 @@ async def _bewertung_mit_kritik(
|
|||||||
probleme = await _kritik_call(
|
probleme = await _kritik_call(
|
||||||
"Baustein-Bewertung-Kritik", provider,
|
"Baustein-Bewertung-Kritik", provider,
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
vertiefung_block=vertiefung_block, transcript=transcript,
|
vertiefung_block=vertiefung_block, frage=frage, transcript=transcript,
|
||||||
bewertung_block=_bewertung_text(bew),
|
bewertung_block=_bewertung_text(bew),
|
||||||
)
|
)
|
||||||
if not probleme:
|
if not probleme:
|
||||||
@@ -185,38 +189,74 @@ async def _bewertung_mit_kritik(
|
|||||||
return bew # best-effort nach der letzten Runde
|
return bew # best-effort nach der letzten Runde
|
||||||
|
|
||||||
|
|
||||||
async def baustein_pruefung(
|
def _bloecke(section: str, vertiefung: str | None) -> tuple[str, str]:
|
||||||
topic: str, baustein: str, section: str, vertiefung: str | None,
|
return (
|
||||||
messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
|
section.strip() or "(keine Guide-Fassung übergeben)",
|
||||||
) -> dict | None:
|
(vertiefung or "").strip() or "(keine)",
|
||||||
"""Ein Prüfungs-Turn: erst (falls Antwort vorliegt) bewerten, dann nächste Frage.
|
)
|
||||||
|
|
||||||
Generator + Kritiker-Loop je Schritt. Gibt
|
|
||||||
{"feedback", "frage", "bewertung", "bestanden"} zurück · None bei Fehler.
|
async def pruefung_frage(
|
||||||
Bewertet wird nur, wenn der Verlauf mit einer Nutzer-Antwort endet — sonst
|
topic: str, baustein: str, section: str, vertiefung: str | None,
|
||||||
bekäme der Evaluator keine Antwort zum Beurteilen.
|
messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
||||||
|
) -> str | None:
|
||||||
|
"""Aktion 'frage': nächste Frage generieren (Generator + Kritiker) · None bei Fehler."""
|
||||||
|
try:
|
||||||
|
section_block, vertiefung_block = _bloecke(section, vertiefung)
|
||||||
|
transcript = _transcript(messages) if messages else "(leer)"
|
||||||
|
return await _frage_mit_kritik(topic, baustein, section_block, vertiefung_block, transcript, provider)
|
||||||
|
except Exception:
|
||||||
|
log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def pruefung_bewertung(
|
||||||
|
topic: str, baustein: str, section: str, vertiefung: str | None,
|
||||||
|
frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
|
||||||
|
) -> dict | None:
|
||||||
|
"""Aktion 'antwort': Antwort zur Frage bewerten (Evaluator + Kritiker).
|
||||||
|
|
||||||
|
Gibt {"feedback", "bewertung", "bestanden"} · None bei Fehler.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
section_block = section.strip() or "(keine Guide-Fassung übergeben)"
|
section_block, vertiefung_block = _bloecke(section, vertiefung)
|
||||||
vertiefung_block = (vertiefung or "").strip() or "(keine)"
|
|
||||||
transcript = _transcript(messages) if messages else "(leer)"
|
transcript = _transcript(messages) if messages else "(leer)"
|
||||||
bewerten = bool(messages) and messages[-1].get("role") == "user"
|
return await _bewertung_mit_kritik(
|
||||||
|
topic, baustein, section_block, vertiefung_block,
|
||||||
feedback, bewertung, bestanden = None, None, False
|
frage.strip() or "(keine Frage übergeben)", transcript, gute_antworten, provider,
|
||||||
if bewerten:
|
)
|
||||||
bew = await _bewertung_mit_kritik(
|
|
||||||
topic, baustein, section_block, vertiefung_block, transcript, gute_antworten, provider,
|
|
||||||
)
|
|
||||||
if bew is None:
|
|
||||||
return None
|
|
||||||
feedback, bewertung, bestanden = bew["feedback"], bew["bewertung"], bew["bestanden"]
|
|
||||||
|
|
||||||
frage = await _frage_mit_kritik(topic, baustein, section_block, vertiefung_block, transcript, provider)
|
|
||||||
if frage is None:
|
|
||||||
return None
|
|
||||||
return {"feedback": feedback, "frage": frage, "bewertung": bewertung, "bestanden": bestanden}
|
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning("[%s] Prüfung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
log.warning("[%s] Bewertung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def baustein_diskussion(
|
||||||
|
topic: str, baustein: str, section: str, vertiefung: str | None,
|
||||||
|
frage: str, letzte_bewertung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
||||||
|
) -> str | None:
|
||||||
|
"""Aktion 'diskussion': Tutor erklärt/diskutiert die Frage oder eine Bewertung.
|
||||||
|
|
||||||
|
Kein Bewerten, kein Kritiker — hier ist der Mensch der Prüfer. None bei Fehler.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
section_block, vertiefung_block = _bloecke(section, vertiefung)
|
||||||
|
prompt = _prompt(
|
||||||
|
"Baustein-Pruefung-Diskussion",
|
||||||
|
topic=topic, baustein=baustein,
|
||||||
|
section_block=section_block, vertiefung_block=vertiefung_block,
|
||||||
|
frage=frage.strip() or "(keine Frage übergeben)",
|
||||||
|
letzte_bewertung_block=(letzte_bewertung or "").strip() or "(noch keine)",
|
||||||
|
transcript=_transcript(messages) if messages else "(leer)",
|
||||||
|
)
|
||||||
|
returncode, stdout, _ = await run_agent(
|
||||||
|
"pruefungdiskussion-" + str(uuid.uuid4()), prompt, CHAT_TIMEOUT,
|
||||||
|
provider=provider, role="fast", capabilities="none", lane="interactive",
|
||||||
|
)
|
||||||
|
if returncode != 0:
|
||||||
|
return None
|
||||||
|
return stdout.strip() or None
|
||||||
|
except Exception:
|
||||||
|
log.warning("[%s] Prüfungs-Diskussion fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -191,13 +191,18 @@ class BausteinPruefungRequest(BaseModel):
|
|||||||
topic: str = Field(min_length=1, max_length=100)
|
topic: str = Field(min_length=1, max_length=100)
|
||||||
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)
|
section: str = Field(default="", max_length=20000)
|
||||||
messages: list[ChatMessage] = [] # leer = KI stellt die erste Frage
|
aktion: Literal["frage", "diskussion", "antwort"] = "frage"
|
||||||
|
frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort)
|
||||||
|
letzte_bewertung: str = Field(default="", max_length=2000) # Feedback der letzten Bewertung (Kontext für diskussion)
|
||||||
|
frage_schon_gut: bool = False # diese Frage wurde schon einmal "gut" bewertet → nicht doppelt zählen
|
||||||
|
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
|
||||||
provider: ProviderType = "claude"
|
provider: ProviderType = "claude"
|
||||||
|
|
||||||
|
|
||||||
class BausteinPruefungResponse(BaseModel):
|
class BausteinPruefungResponse(BaseModel):
|
||||||
|
frage: str | None = None
|
||||||
|
reply: str | None = None
|
||||||
feedback: str | None = None
|
feedback: str | None = None
|
||||||
frage: str
|
|
||||||
bewertung: Literal["gut", "schlecht"] | None = None
|
bewertung: Literal["gut", "schlecht"] | None = None
|
||||||
gute_antworten: int
|
gute_antworten: int
|
||||||
absolviert: bool
|
absolviert: bool
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from database import (
|
|||||||
)
|
)
|
||||||
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine
|
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine
|
||||||
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, baustein_chat, baustein_element_anlegen, baustein_pruefung, vertiefung_generieren
|
from lernen import NOETIG, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_frage, vertiefung_generieren
|
||||||
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
|
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand
|
||||||
@@ -210,24 +210,48 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
(p for p in await list_baustein_progress(req.topic) if p["baustein"] == req.baustein),
|
(p for p in await list_baustein_progress(req.topic) if p["baustein"] == req.baustein),
|
||||||
{"gute_antworten": 0, "absolviert": None},
|
{"gute_antworten": 0, "absolviert": None},
|
||||||
)
|
)
|
||||||
|
gute = stand["gute_antworten"]
|
||||||
|
absolviert = stand["absolviert"] is not None
|
||||||
vertiefung = await _bester_text(req.topic, req.baustein)
|
vertiefung = await _bester_text(req.topic, req.baustein)
|
||||||
data = await baustein_pruefung(
|
msgs = [m.model_dump() for m in req.messages]
|
||||||
req.topic, req.baustein, req.section, vertiefung,
|
|
||||||
[m.model_dump() for m in req.messages], stand["gute_antworten"], provider=req.provider,
|
if req.aktion == "frage":
|
||||||
|
frage = await pruefung_frage(req.topic, req.baustein, req.section, vertiefung, msgs, provider=req.provider)
|
||||||
|
if frage is None:
|
||||||
|
raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen")
|
||||||
|
return {"frage": frage, "gute_antworten": gute, "absolviert": absolviert}
|
||||||
|
|
||||||
|
if req.aktion == "diskussion":
|
||||||
|
if not req.frage.strip():
|
||||||
|
raise HTTPException(400, "Diskussion braucht eine laufende Frage")
|
||||||
|
reply = await baustein_diskussion(
|
||||||
|
req.topic, req.baustein, req.section, vertiefung,
|
||||||
|
req.frage, req.letzte_bewertung or None, msgs, provider=req.provider,
|
||||||
|
)
|
||||||
|
if reply is None:
|
||||||
|
raise HTTPException(502, "Diskussion fehlgeschlagen — bitte erneut versuchen")
|
||||||
|
return {"reply": reply, "gute_antworten": gute, "absolviert": absolviert}
|
||||||
|
|
||||||
|
# aktion == "antwort" — mindestens eine Nutzer-Antwort muss im Dialog stehen
|
||||||
|
# (nach einer Diskussion endet der Dialog mit dem Tutor; Re-Bewertung bleibt erlaubt).
|
||||||
|
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")
|
||||||
|
data = await pruefung_bewertung(
|
||||||
|
req.topic, req.baustein, req.section, vertiefung, req.frage, msgs, gute, provider=req.provider,
|
||||||
)
|
)
|
||||||
if data is None:
|
if data is None:
|
||||||
raise HTTPException(502, "Prüfung fehlgeschlagen — bitte erneut versuchen")
|
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
||||||
|
|
||||||
gute = stand["gute_antworten"]
|
if data["bewertung"] == "gut" and not req.frage_schon_gut:
|
||||||
if data["bewertung"] == "gut":
|
|
||||||
gute = await add_gute_antwort(req.topic, req.baustein)
|
gute = await add_gute_antwort(req.topic, req.baustein)
|
||||||
absolviert = stand["absolviert"] is not None
|
|
||||||
if gute >= NOETIG or data["bestanden"]:
|
if gute >= NOETIG or data["bestanden"]:
|
||||||
frisch = await set_baustein_absolviert(req.topic, req.baustein)
|
frisch = await set_baustein_absolviert(req.topic, req.baustein)
|
||||||
absolviert = True
|
absolviert = True
|
||||||
if frisch:
|
if frisch:
|
||||||
asyncio.create_task(baustein_element_anlegen(req.topic, req.baustein, req.section, req.provider))
|
asyncio.create_task(baustein_element_anlegen(req.topic, req.baustein, req.section, req.provider))
|
||||||
return {"feedback": data["feedback"], "frage": data["frage"], "bewertung": data["bewertung"], "gute_antworten": gute, "absolviert": absolviert}
|
return {"feedback": data["feedback"], "bewertung": data["bewertung"], "gute_antworten": gute, "absolviert": absolviert}
|
||||||
|
|
||||||
|
|
||||||
# --- Guides ---
|
# --- Guides ---
|
||||||
|
|||||||
26
frontend/package-lock.json
generated
26
frontend/package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dompurify": "^3.4.7",
|
"dompurify": "^3.4.7",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
"katex": "^0.17.0",
|
||||||
"marked": "^18.0.4",
|
"marked": "^18.0.4",
|
||||||
"marked-highlight": "^2.2.4",
|
"marked-highlight": "^2.2.4",
|
||||||
"vue": "^3.5.32"
|
"vue": "^3.5.32"
|
||||||
@@ -1187,6 +1188,15 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/convert-source-map": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
@@ -1468,6 +1478,22 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/katex": {
|
||||||
|
"version": "0.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/katex/-/katex-0.17.0.tgz",
|
||||||
|
"integrity": "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==",
|
||||||
|
"funding": [
|
||||||
|
"https://opencollective.com/katex",
|
||||||
|
"https://github.com/sponsors/katex"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^8.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"katex": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/kolorist": {
|
"node_modules/kolorist": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dompurify": "^3.4.7",
|
"dompurify": "^3.4.7",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
"katex": "^0.17.0",
|
||||||
"marked": "^18.0.4",
|
"marked": "^18.0.4",
|
||||||
"marked-highlight": "^2.2.4",
|
"marked-highlight": "^2.2.4",
|
||||||
"vue": "^3.5.32"
|
"vue": "^3.5.32"
|
||||||
|
|||||||
@@ -91,11 +91,14 @@ export async function chatBaustein({ topic, baustein, section, messages, provide
|
|||||||
return jsonOrThrow(res)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pruefeBaustein({ topic, baustein, section, messages, provider }) {
|
export async function pruefeBaustein({
|
||||||
|
topic, baustein, section, provider,
|
||||||
|
aktion = 'frage', frage = '', letzte_bewertung = '', frage_schon_gut = false, messages = [],
|
||||||
|
}) {
|
||||||
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, messages, provider }),
|
body: JSON.stringify({ topic, baustein, section, aktion, frage, letzte_bewertung, frage_schon_gut, messages, provider }),
|
||||||
})
|
})
|
||||||
return jsonOrThrow(res)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
margin: 0 0 0.5em;
|
margin: 0 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* KaTeX: lange Block-Formeln scrollen statt das Layout zu sprengen */
|
||||||
|
.markdown .katex-display {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding: 0.2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
.markdown p:last-child {
|
.markdown p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const activeTab = ref(null) // null | 'vertiefung' | 'deepdive' | 'chat' | 'prue
|
|||||||
function toggle(tab) {
|
function toggle(tab) {
|
||||||
activeTab.value = activeTab.value === tab ? null : tab
|
activeTab.value = activeTab.value === tab ? null : tab
|
||||||
if (activeTab.value === 'vertiefung' || activeTab.value === 'deepdive') openText(activeTab.value)
|
if (activeTab.value === 'vertiefung' || activeTab.value === 'deepdive') openText(activeTab.value)
|
||||||
if (activeTab.value === 'pruefung') startPruefung()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Vertiefung (kurz) + Deep Dive (lang), beide persistiert ---
|
// --- Vertiefung (kurz) + Deep Dive (lang), beide persistiert ---
|
||||||
@@ -68,42 +67,97 @@ const chat = useChat((msgs) => chatBaustein({
|
|||||||
messages: msgs, provider: props.provider,
|
messages: msgs, provider: props.provider,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// --- Prüfung (Verlauf flüchtig, Zähler serverseitig) ---
|
// --- Prüfung: gesteuerter Dialog (Verlauf flüchtig, Zähler serverseitig) ---
|
||||||
const pruefung = useChat(async (msgs) => {
|
// Phasen: 'idle' (Frage anfordern) | 'frage_offen' (antworten/nachfragen) | 'bewertet' (diskutieren/neu bewerten/weiter)
|
||||||
const res = await pruefeBaustein({
|
const pruefMessages = ref([]) // {role, kind: 'frage'|'nachfrage'|'antwort'|'feedback'|'diskussion'|'fehler', content, bewertung?}
|
||||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
const pruefInput = ref('')
|
||||||
messages: msgs, provider: props.provider,
|
const pruefPhase = ref('idle')
|
||||||
})
|
const pruefLoading = ref(false)
|
||||||
applyPruefung(res)
|
const aktuelleFrage = ref('') // ankert Bewertung/Diskussion
|
||||||
return res
|
const letztesFeedback = ref('') // Kontext für die Diskussion über eine Bewertung
|
||||||
})
|
const frageSchonGut = ref(false) // diese Frage schon "gut" → nicht doppelt zählen
|
||||||
const startLoading = ref(false)
|
const pruefMessagesEl = ref(null)
|
||||||
|
const pruefInputEl = ref(null)
|
||||||
|
let pruefRun = 0
|
||||||
|
|
||||||
function applyPruefung(res) {
|
function applyPruefung(res) {
|
||||||
emit('statusChanged', {
|
emit('statusChanged', { ...st.value, gute_antworten: res.gute_antworten, absolviert: res.absolviert })
|
||||||
...st.value,
|
|
||||||
gute_antworten: res.gute_antworten,
|
|
||||||
absolviert: res.absolviert,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startPruefung() {
|
async function pruefScroll() {
|
||||||
if (pruefung.messages.value.length || startLoading.value) return
|
await nextTick()
|
||||||
startLoading.value = true
|
if (pruefMessagesEl.value) pruefMessagesEl.value.scrollTop = pruefMessagesEl.value.scrollHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur echte Gesprächs-Turns ans Backend; Feedback bleibt reines UI-Artefakt.
|
||||||
|
function pruefDialog() {
|
||||||
|
return pruefMessages.value
|
||||||
|
.filter((m) => m.kind !== 'feedback' && m.kind !== 'fehler')
|
||||||
|
.map((m) => ({ role: m.role, content: m.content }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pruefSenden(payload, onOk) {
|
||||||
|
const run = ++pruefRun
|
||||||
|
pruefLoading.value = true
|
||||||
|
pruefScroll()
|
||||||
try {
|
try {
|
||||||
const res = await pruefeBaustein({
|
const res = await pruefeBaustein({
|
||||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
topic: props.topic, baustein: props.baustein, section: props.section,
|
||||||
messages: [], provider: props.provider,
|
provider: props.provider, messages: pruefDialog(), ...payload,
|
||||||
})
|
})
|
||||||
pruefung.messages.value.push({ role: 'assistant', content: res.frage, feedback: null })
|
if (run !== pruefRun) return
|
||||||
|
onOk(res)
|
||||||
applyPruefung(res)
|
applyPruefung(res)
|
||||||
nextTick(() => pruefung.inputEl.value?.focus())
|
pruefScroll()
|
||||||
|
nextTick(() => pruefInputEl.value?.focus())
|
||||||
} catch {
|
} catch {
|
||||||
pruefung.messages.value.push({ role: 'assistant', content: 'Fehler beim Start der Prüfung — Tab erneut öffnen.' })
|
if (run === pruefRun) pruefMessages.value.push({ role: 'assistant', kind: 'fehler', content: 'Hat nicht geklappt — bitte erneut.' })
|
||||||
} finally {
|
} finally {
|
||||||
startLoading.value = false
|
if (run === pruefRun) pruefLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function frageAnfordern() {
|
||||||
|
if (pruefLoading.value) return
|
||||||
|
pruefSenden({ aktion: 'frage' }, (res) => {
|
||||||
|
aktuelleFrage.value = res.frage
|
||||||
|
letztesFeedback.value = ''
|
||||||
|
frageSchonGut.value = false
|
||||||
|
pruefMessages.value.push({ role: 'assistant', kind: 'frage', content: res.frage })
|
||||||
|
pruefPhase.value = 'frage_offen'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function nachfragen() {
|
||||||
|
const text = pruefInput.value.trim()
|
||||||
|
if (!text || pruefLoading.value) return
|
||||||
|
pruefMessages.value.push({ role: 'user', kind: 'nachfrage', content: text })
|
||||||
|
pruefInput.value = ''
|
||||||
|
pruefSenden(
|
||||||
|
{ aktion: 'diskussion', frage: aktuelleFrage.value, letzte_bewertung: letztesFeedback.value },
|
||||||
|
(res) => pruefMessages.value.push({ role: 'assistant', kind: 'diskussion', content: res.reply }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bewerten(res) {
|
||||||
|
letztesFeedback.value = res.feedback || ''
|
||||||
|
if (res.bewertung === 'gut') frageSchonGut.value = true
|
||||||
|
pruefMessages.value.push({ role: 'assistant', kind: 'feedback', content: res.feedback || '', bewertung: res.bewertung })
|
||||||
|
pruefPhase.value = 'bewertet'
|
||||||
|
}
|
||||||
|
|
||||||
|
function antwortAbgeben() {
|
||||||
|
const text = pruefInput.value.trim()
|
||||||
|
if (!text || pruefLoading.value) return
|
||||||
|
pruefMessages.value.push({ role: 'user', kind: 'antwort', content: text })
|
||||||
|
pruefInput.value = ''
|
||||||
|
pruefSenden({ aktion: 'antwort', frage: aktuelleFrage.value, frage_schon_gut: frageSchonGut.value }, bewerten)
|
||||||
|
}
|
||||||
|
|
||||||
|
function neuBewerten() {
|
||||||
|
if (pruefLoading.value) return
|
||||||
|
pruefSenden({ aktion: 'antwort', frage: aktuelleFrage.value, frage_schon_gut: frageSchonGut.value }, bewerten)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -166,35 +220,50 @@ async function startPruefung() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Prüfung -->
|
<!-- Prüfung: gesteuerter Dialog -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<p class="bp-hint">
|
<p class="bp-hint">
|
||||||
<template v-if="st.absolviert">✓ Absolviert — du kannst dich weiter prüfen lassen.</template>
|
<template v-if="st.absolviert">✓ Absolviert — du kannst dich weiter prüfen lassen.</template>
|
||||||
<template v-else>{{ Math.min(st.gute_antworten, NOETIG) }}/{{ NOETIG }} guten Antworten. Erkläre in eigenen Worten — das Material darfst du nutzen.</template>
|
<template v-else>{{ Math.min(st.gute_antworten, NOETIG) }}/{{ NOETIG }} guten Antworten. Frag nach, wenn etwas unklar ist — diskutieren ist erlaubt.</template>
|
||||||
</p>
|
</p>
|
||||||
<div :ref="pruefung.messagesEl" class="bp-messages">
|
|
||||||
<div v-if="startLoading" class="bp-msg assistant bp-typing">Erste Frage kommt…</div>
|
<div v-if="pruefMessages.length" :ref="pruefMessagesEl" class="bp-messages">
|
||||||
<template v-for="(m, i) in pruefung.messages.value" :key="i">
|
<template v-for="(m, i) in pruefMessages" :key="i">
|
||||||
<template v-if="m.role === 'assistant'">
|
<div v-if="m.kind === 'feedback'" class="bp-feedback" :class="m.bewertung">{{ m.content }}</div>
|
||||||
<div v-if="m.feedback" class="bp-feedback" :class="m.bewertung">{{ m.feedback }}</div>
|
<div v-else-if="m.kind === 'fehler'" class="bp-error">{{ m.content }}</div>
|
||||||
<div 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>
|
||||||
</template>
|
|
||||||
<div v-else class="bp-msg user">{{ m.content }}</div>
|
<div v-else class="bp-msg user">{{ m.content }}</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="pruefung.loading.value" class="bp-msg assistant bp-typing">Bewertet…</div>
|
<div v-if="pruefLoading" class="bp-msg assistant bp-typing">…</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bp-input">
|
|
||||||
<textarea
|
<!-- Phase idle: Frage anfordern -->
|
||||||
:ref="pruefung.inputEl"
|
<div v-if="pruefPhase === 'idle'" class="bp-actions">
|
||||||
v-model="pruefung.input.value"
|
<button class="bp-action primary" :disabled="pruefLoading" @click="frageAnfordern">Frage anfordern</button>
|
||||||
rows="2"
|
|
||||||
placeholder="Deine Erklärung…"
|
|
||||||
@keydown.enter.exact.prevent="pruefung.send"
|
|
||||||
></textarea>
|
|
||||||
<button :disabled="!pruefung.input.value.trim() && !pruefung.loading.value" :class="{ cancel: pruefung.loading.value }" @click="pruefung.send">
|
|
||||||
{{ pruefung.loading.value ? '✕' : '➤' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Phase frage_offen / bewertet: Textfeld + Aktionen -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="bp-input">
|
||||||
|
<textarea
|
||||||
|
:ref="pruefInputEl"
|
||||||
|
v-model="pruefInput"
|
||||||
|
rows="2"
|
||||||
|
:placeholder="pruefPhase === 'frage_offen' ? 'Antwort — oder Nachfrage bei Unklarheit…' : 'Nachhaken oder diskutieren…'"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="bp-actions">
|
||||||
|
<template v-if="pruefPhase === 'frage_offen'">
|
||||||
|
<button class="bp-action" :disabled="pruefLoading || !pruefInput.trim()" @click="nachfragen">Nachfragen</button>
|
||||||
|
<button class="bp-action primary" :disabled="pruefLoading || !pruefInput.trim()" @click="antwortAbgeben">Antwort abgeben</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button class="bp-action" :disabled="pruefLoading || !pruefInput.trim()" @click="nachfragen">Nachhaken</button>
|
||||||
|
<button class="bp-action" :disabled="pruefLoading" @click="neuBewerten">Neu bewerten</button>
|
||||||
|
<button class="bp-action primary" :disabled="pruefLoading" @click="frageAnfordern">Nächste Frage</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -249,6 +318,12 @@ async function startPruefung() {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.bp-action:hover { border-color: var(--accent); }
|
.bp-action:hover { border-color: var(--accent); }
|
||||||
|
.bp-action:disabled { opacity: 0.5; cursor: default; }
|
||||||
|
.bp-action.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||||||
|
.bp-action.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
||||||
|
|
||||||
|
.bp-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.5rem; }
|
||||||
|
.bp-actions .bp-action { margin-top: 0; }
|
||||||
|
|
||||||
.bp-messages { display: flex; flex-direction: column; gap: 0.4rem; max-height: 320px; overflow-y: auto; }
|
.bp-messages { display: flex; flex-direction: column; gap: 0.4rem; max-height: 320px; overflow-y: auto; }
|
||||||
.bp-msg {
|
.bp-msg {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { marked } from 'marked'
|
|||||||
import { markedHighlight } from 'marked-highlight'
|
import { markedHighlight } from 'marked-highlight'
|
||||||
import hljs from 'highlight.js'
|
import hljs from 'highlight.js'
|
||||||
import 'highlight.js/styles/github-dark.css'
|
import 'highlight.js/styles/github-dark.css'
|
||||||
|
import katex from 'katex'
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
|
|
||||||
marked.use(markedHighlight({
|
marked.use(markedHighlight({
|
||||||
@@ -15,6 +17,40 @@ marked.use(markedHighlight({
|
|||||||
}))
|
}))
|
||||||
marked.setOptions({ breaks: true, gfm: true })
|
marked.setOptions({ breaks: true, gfm: true })
|
||||||
|
|
||||||
|
// LaTeX-Mathe via KaTeX. Eigene marked-Extensions (statt marked-katex-extension,
|
||||||
|
// die marked v18 hinterherhinkt). marked tokenisiert Code zuerst → $…$ in Code-
|
||||||
|
// Blöcken wird NICHT als Mathe erkannt. throwOnError:false zeigt defektes TeX rot.
|
||||||
|
function renderTex(tex, displayMode) {
|
||||||
|
return katex.renderToString(tex, { displayMode, throwOnError: false, output: 'html' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockMath = {
|
||||||
|
name: 'blockMath',
|
||||||
|
level: 'block',
|
||||||
|
start(src) { const i = src.indexOf('$$'); return i < 0 ? undefined : i },
|
||||||
|
tokenizer(src) {
|
||||||
|
const m = /^\$\$([\s\S]+?)\$\$/.exec(src)
|
||||||
|
if (m) return { type: 'blockMath', raw: m[0], text: m[1].trim() }
|
||||||
|
},
|
||||||
|
renderer(token) { return renderTex(token.text, true) },
|
||||||
|
}
|
||||||
|
|
||||||
|
const inlineMath = {
|
||||||
|
name: 'inlineMath',
|
||||||
|
level: 'inline',
|
||||||
|
start(src) { const i = src.indexOf('$'); return i < 0 ? undefined : i },
|
||||||
|
tokenizer(src) {
|
||||||
|
// $…$: kein $$, kein Leerzeichen direkt hinter dem öffnenden $ und vor dem
|
||||||
|
// schließenden $ (pandoc-Stil) → mindert Kollisionen mit Fließtext-Dollarzeichen.
|
||||||
|
const m = /^\$(?![\s$])((?:\\\$|[^$])+?)\$/.exec(src)
|
||||||
|
if (!m || /\s$/.test(m[1])) return
|
||||||
|
return { type: 'inlineMath', raw: m[0], text: m[1].trim() }
|
||||||
|
},
|
||||||
|
renderer(token) { return renderTex(token.text, false) },
|
||||||
|
}
|
||||||
|
|
||||||
|
marked.use({ extensions: [blockMath, inlineMath] })
|
||||||
|
|
||||||
// Rohes HTML im Markdown (z. B. <p>, <img> ohne Backticks aus Agenten-Output)
|
// Rohes HTML im Markdown (z. B. <p>, <img> ohne Backticks aus Agenten-Output)
|
||||||
// als Text anzeigen statt rendern — sonst verschluckt der Browser den Inhalt.
|
// als Text anzeigen statt rendern — sonst verschluckt der Browser den Inhalt.
|
||||||
marked.use({
|
marked.use({
|
||||||
|
|||||||
@@ -1,44 +1,51 @@
|
|||||||
SECTION-AUFBAU
|
SECTION-AUFBAU
|
||||||
|
|
||||||
Jeder Baustein wird GENAU eine Section mit:
|
Jeder Baustein ist ein kleiner, eigenständiger Lern-Guide: er stellt EIN Konzept vor, erklärt es von Grund auf und macht es nutzbar. Der Leser bringt KEIN Vorwissen mit — du holst ihn ab und bringst ihm die Sache bei. Eine Section ist kein Stichwort-Zettel zum Nachschlagen.
|
||||||
1. Titel — der Baustein-Titel (kommt aus dem Marker, nicht in den Body schreiben)
|
|
||||||
2. Beschreibung — was es ist und wozu: MAXIMAL 1–2 Sätze
|
Aufbau je Baustein — drei Beats, fließend ineinander, OHNE Zwischenüberschriften:
|
||||||
3. Beispiele — KURZ und SIMPEL: das Minimalbeispiel im themengerechten Format (siehe BEISPIELFORMAT), keine Realwelt-Komplexität. Höchstens 1 knapper Satz Einordnung dazu. Ein Beispiel pro relevanter Variante: simple Bausteine eines, variantenreiche mehrere. Geordnet vom Üblichen zum Speziellen. Weglassen, wenn ohne Mehrwert.
|
1. Einordnung — welche Frage beantwortet der Baustein, welches Problem löst er? Ein Satz, der den Leser abholt. Bei selbsterklärenden Bausteinen weglassen.
|
||||||
|
2. Erklärung — was es ist UND wie/warum es funktioniert. Alltagssprache, von der Intuition zum Detail. Fachbegriffe beim ersten Auftreten in einem Halbsatz auflösen. Eine Analogie oder ein Bild ist erlaubt und oft besser als eine Definition.
|
||||||
|
3. Beispiel(e) — das Konzept konkret gemacht (siehe BEISPIELFORMAT).
|
||||||
|
|
||||||
|
LÄNGE — so lang wie nötig, so kurz wie möglich:
|
||||||
|
- KEIN festes Wort- oder Satzlimit. Die Länge richtet sich nach der Schwierigkeit des Konzepts: ein einfacher Baustein braucht 2–3 Sätze, ein kniffliger einen kurzen Absatz.
|
||||||
|
- Verständnis-Test (er entscheidet über die Länge): Versteht ein Anfänger das Konzept allein aus dieser Section? Wenn nein → eine Stufe einfacher erklären, NICHT mehr Fakten stapeln. Wenn ja und kein Satz lässt sich streichen, ohne dass Verständnis verloren geht → genau richtig.
|
||||||
|
- Kürze entsteht durch WEGLASSEN von Überflüssigem, nicht durch Verdichten von Nötigem.
|
||||||
|
- Weglassen: Füllsätze, Einleitungsfloskeln („In diesem Abschnitt…"), Wiederholungen, Fazit/Zusammenfassung. Nicht jeden Randfall nennen — das Übliche erklären; Varianten gehören in die Beispiele, mehr Tiefe in die Vertiefung.
|
||||||
|
|
||||||
BEISPIELFORMAT — am Thema ausrichten, nicht pauschal an Code:
|
BEISPIELFORMAT — am Thema ausrichten, nicht pauschal an Code:
|
||||||
- Code-/Tool-Thema (Sprache, Framework, CLI, Konfiguration): Codeblock mit Sprachangabe, wenige Zeilen, Minimalbeispiel.
|
- Code-/Tool-Thema (Sprache, Framework, CLI, Konfiguration): Codeblock mit Sprachangabe, wenige Zeilen, Minimalbeispiel.
|
||||||
- Sprach-Thema (Vokabeln, Grammatik, Formulierungen): 1–3 Beispielsätze oder ein Mini-Dialog, fremdsprachiger Teil *kursiv*, deutsche Übersetzung in Klammern wo nötig.
|
- Sprach-Thema (Vokabeln, Grammatik, Formulierungen): 1–3 Beispielsätze oder ein Mini-Dialog, fremdsprachiger Teil *kursiv*, deutsche Übersetzung in Klammern wo nötig.
|
||||||
- Konzept-Thema (Psychologie, Kommunikation, Methoden, Theorie): ein Mini-Szenario in 2–4 Sätzen (Situation → Anwendung → Wirkung), ein Schema oder eine Formel.
|
- Konzept-Thema (Psychologie, Kommunikation, Methoden, Theorie, Mathe): ein Mini-Szenario in 2–4 Sätzen (Situation → Anwendung → Wirkung), ein Schema oder eine durchgerechnete Formel mit kleinen Zahlen.
|
||||||
Mischthemen: pro Beispiel das Format wählen, das den Punkt am direktesten zeigt.
|
Mischthemen: pro Beispiel das Format wählen, das den Punkt am direktesten zeigt.
|
||||||
Ein Beispiel ist immer KONKRET (echter Code, echte Sätze, echte Situation) — nie die Beschreibung, was ein Beispiel zeigen würde.
|
Ein Beispiel ist immer KONKRET (echter Code, echte Sätze, echte Situation) — nie die Beschreibung, was ein Beispiel zeigen würde.
|
||||||
Jedes Beispiel benennt seine Variante: in Code als Kommentar in der Code-Syntax (z. B. `<!-- Einzelner Absatz -->`, `// Mit Default-Wert`), in Prosa als vorangestelltes fettes Label (z. B. **Höfliche Bitte:**).
|
Mehrere Beispiele benennen ihre Variante: in Code als Kommentar in der Code-Syntax (z. B. `<!-- Einzelner Absatz -->`, `// Mit Default-Wert`), in Prosa als vorangestelltes fettes Label (z. B. **Höfliche Bitte:**). Bei nur einem Beispiel ist kein Label nötig.
|
||||||
|
|
||||||
Jede Section ist ATOMAR: allein verständlich, ohne dass der Leser eine andere Section gelesen hat. Test: Ergibt der Text Sinn, wenn man NUR diese Section liest? Verweise auf andere Bausteine sind erlaubt, ihr Inhalt darf aber nie vorausgesetzt werden — benutzte Begriffe in einem Halbsatz auflösen.
|
Jede Section ist ATOMAR: allein verständlich, ohne dass der Leser eine andere Section gelesen hat. Test: Ergibt der Text Sinn, wenn man NUR diese Section liest? Verweise auf andere Bausteine sind erlaubt, ihr Inhalt darf aber nie vorausgesetzt werden — benutzte Begriffe in einem Halbsatz auflösen.
|
||||||
|
|
||||||
Umfang: kurz. Die Länge einer Section kommt aus der ZAHL der Beispiele (Varianten), nie aus langen Texten.
|
Tonalität: klares, direktes Deutsch. Du erklärst, du referierst nicht. Praxisorientiert, ohne Füllsätze.
|
||||||
|
|
||||||
Tonalität: klares Deutsch, direkt, praxisorientiert. Fachbegriffe beim ersten Auftreten kurz erklären. Keine Füllsätze, keine Einleitungsfloskeln.
|
Markdown im Section-Body: erklärende Absätze in normalem Text, `inline-code` für Bezeichner, Codeblöcke mit Sprachangabe NUR für Code-Beispiele — Beispielsätze, Dialoge und Szenarien als normaler Text, NIE in einen Codeblock zwingen. **fett** sparsam für Kernaussagen und Beispiel-Labels. Keine eigenen Überschriften außer `### Beispiel` bzw. `### Beispiele` vor den Beispielen.
|
||||||
|
|
||||||
Markdown im Section-Body: normale Absätze, `inline-code` für Bezeichner, Codeblöcke mit Sprachangabe NUR für Code-Beispiele — Beispielsätze, Dialoge und Szenarien als normaler Text, NIE in einen Codeblock zwingen. **fett** sparsam für Kernaussagen. Keine eigenen Überschriften außer `### Beispiel` bzw. `### Beispiele` vor den Beispielen.
|
Mathematik IMMER als LaTeX schreiben: inline zwischen `$…$` (z. B. `$\Sigma^*$`, `$L \subseteq U$`, `$k = 3$`), abgesetzte Formeln zwischen `$$…$$`. KEINE Unicode-Sonderzeichen als Mathe-Ersatz (nicht `x₁`, `¬`, `∨`, `≤` — stattdessen `$x_1$`, `$\neg$`, `$\lor$`, `$\le$`) und keine nackten Formeln ohne `$`. Außerhalb von Mathe normaler Text.
|
||||||
|
|
||||||
Beispiel einer fertigen Section (Code-Thema, nur der Body):
|
Beispiel einer fertigen Section (Code-Thema, nur der Body):
|
||||||
|
|
||||||
Arrays speichern mehrere Werte unter einem Namen. PHP unterscheidet indizierte Arrays (`[0 => 'a']`) und assoziative Arrays (`['key' => 'wert']`) — intern sind beide geordnete Hashmaps.
|
Arrays lösen ein simples Problem: Du willst viele Werte unter einem Namen halten, statt für jeden eine eigene Variable. In PHP gibt es zwei Sorten. Indizierte Arrays nummerieren die Werte durch (`[0 => 'a']`). Assoziative Arrays geben jedem Wert einen eigenen Schlüssel (`['key' => 'wert']`) — praktisch, wenn die Position egal ist, der Name aber zählt. Intern sind beide dasselbe: geordnete Hashmaps.
|
||||||
|
|
||||||
### Beispiel
|
### Beispiel
|
||||||
```php
|
```php
|
||||||
$preise = ['apfel' => 1.20, 'birne' => 1.50];
|
$preise = ['apfel' => 1.20, 'birne' => 1.50];
|
||||||
$preise['kirsche'] = 3.90; // ergänzen
|
$preise['kirsche'] = 3.90; // neuen Schlüssel ergänzen
|
||||||
echo $preise['apfel']; // 1.2
|
echo $preise['apfel']; // 1.2 — Zugriff über den Namen
|
||||||
```
|
```
|
||||||
Assoziative Arrays sind der Arbeitsalltag: Datenbankzeilen, Konfiguration, JSON.
|
So sieht der Alltag aus: Datenbankzeilen, Konfiguration, JSON landen fast immer in assoziativen Arrays.
|
||||||
|
|
||||||
Beispiel einer fertigen Section (Konzept-Thema, nur der Body):
|
Beispiel einer fertigen Section (Konzept-Thema, nur der Body):
|
||||||
|
|
||||||
Paraphrasieren wiederholt die Aussage des Gegenübers in eigenen Worten, um Verständnis zu prüfen und Eskalation zu bremsen.
|
Im Streit reden zwei oft aneinander vorbei, weil keiner sicher ist, ob er den anderen richtig verstanden hat. Paraphrasieren setzt genau hier an: Du wiederholst die Aussage des Gegenübers in eigenen Worten und fragst nach, ob das so stimmt. Das prüft dein Verständnis und nimmt Tempo aus dem Konflikt — der andere fühlt sich gehört, statt sich verteidigen zu müssen. Wichtig: Du bestätigst nicht den Vorwurf, du spiegelst nur die Botschaft dahinter.
|
||||||
|
|
||||||
### Beispiel
|
### Beispiel
|
||||||
**Vorwurf abfedern:**
|
|
||||||
A: „Nie hältst du dich an Absprachen!"
|
A: „Nie hältst du dich an Absprachen!"
|
||||||
B: „Du bist sauer, weil ich den Termin gestern verschoben habe — richtig?"
|
B: „Du bist sauer, weil ich den Termin gestern verschoben habe — richtig?"
|
||||||
Die Paraphrase bestätigt nicht den Vorwurf, sondern prüft die Botschaft dahinter.
|
B übernimmt nicht das Wort „nie", sondern benennt das konkrete Anliegen. Das öffnet das Gespräch, statt es zu eskalieren.
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
Du bist Qualitäts-Prüfer für Bewertungen in einer Prüfung zum Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Ein anderer Agent hat die letzte Antwort des Lerners bewertet. Prüfe, ob die Bewertung fair und korrekt ist.
|
Du bist Qualitäts-Prüfer für Bewertungen in einer Prüfung zum Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Ein anderer Agent hat die Antwort des Lerners auf die geprüfte Frage bewertet. Prüfe, ob die Bewertung fair und korrekt ist.
|
||||||
|
|
||||||
|
GEPRÜFTE FRAGE:
|
||||||
|
{frage}
|
||||||
|
|
||||||
BAUSTEIN AUS DEM GUIDE:
|
BAUSTEIN AUS DEM GUIDE:
|
||||||
{section_block}
|
{section_block}
|
||||||
@@ -6,7 +9,7 @@ BAUSTEIN AUS DEM GUIDE:
|
|||||||
VERTIEFUNG (falls vorhanden):
|
VERTIEFUNG (falls vorhanden):
|
||||||
{vertiefung_block}
|
{vertiefung_block}
|
||||||
|
|
||||||
PRÜFUNGS-VERLAUF (die letzte Nutzer-Antwort wurde bewertet):
|
PRÜFUNGS-VERLAUF (Antwort des Lerners und etwaige Diskussion):
|
||||||
{transcript}
|
{transcript}
|
||||||
|
|
||||||
ZU PRÜFENDE BEWERTUNG:
|
ZU PRÜFENDE BEWERTUNG:
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
Du bewertest die LETZTE Antwort eines Lerners in einer Prüfung zum Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}".
|
Du bewertest, ob ein Lerner die geprüfte Frage verstanden beantwortet hat — Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}".
|
||||||
|
|
||||||
|
GEPRÜFTE FRAGE:
|
||||||
|
{frage}
|
||||||
|
|
||||||
BAUSTEIN AUS DEM GUIDE:
|
BAUSTEIN AUS DEM GUIDE:
|
||||||
{section_block}
|
{section_block}
|
||||||
@@ -8,13 +11,15 @@ VERTIEFUNG (falls vorhanden):
|
|||||||
|
|
||||||
STAND: {gute_antworten} von {noetig} Antworten waren bisher gut.
|
STAND: {gute_antworten} von {noetig} Antworten waren bisher gut.
|
||||||
|
|
||||||
PRÜFUNGS-VERLAUF (die letzte Nutzer-Antwort bewertest du):
|
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.
|
||||||
|
|
||||||
FACHLICHE REFERENZ — WICHTIG:
|
FACHLICHE REFERENZ — WICHTIG:
|
||||||
- Die Guide-Fassung und die Vertiefung oben sind die fachliche Referenz. Deine Bewertung darf ihr NIE widersprechen.
|
- Die Guide-Fassung und die Vertiefung oben sind die fachliche Referenz. Deine Bewertung darf ihr NIE widersprechen.
|
||||||
- Behaupte nichts, was nicht aus dem Material folgt. Erfinde keine Zusatzannahmen.
|
- Behaupte nichts, was nicht aus dem Material folgt. Erfinde keine Zusatzannahmen.
|
||||||
- Widerspricht dir der Lerner mit Bezug aufs Material: Prüfe ZUERST deine eigene Annahme gegen die Referenz. Hat der Lerner recht, gib es offen zu und bewerte als "gut".
|
- Widerspricht dir der Lerner mit Bezug aufs Material: Prüfe ZUERST deine eigene Annahme gegen die Referenz. Hat der Lerner SACHLICH und mit Material-Bezug recht, gib es offen zu und bewerte als "gut" — aber gib NICHT aus Höflichkeit oder auf bloßes Beharren hin nach.
|
||||||
|
|
||||||
SO BEWERTEST DU:
|
SO BEWERTEST DU:
|
||||||
- "gut" = die Erklärung zeigt echtes Verständnis in eigenen Worten. Eine knappe, richtige Antwort reicht — verlange keine Vollständigkeit über die Frage hinaus.
|
- "gut" = die Erklärung zeigt echtes Verständnis in eigenen Worten. Eine knappe, richtige Antwort reicht — verlange keine Vollständigkeit über die Frage hinaus.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
Schreibe einen Deep Dive zum Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Der Leser kennt die kompakte Fassung und will tief einsteigen.
|
Schreibe einen Deep Dive zum Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Der Leser kennt die Guide-Fassung — die erklärt das Konzept schon — und will jetzt tief einsteigen.
|
||||||
|
|
||||||
KOMPAKTE FASSUNG AUS DEM GUIDE:
|
GUIDE-FASSUNG DES BAUSTEINS:
|
||||||
{section_block}
|
{section_block}
|
||||||
|
|
||||||
Inhalt des Deep Dives:
|
Inhalt des Deep Dives:
|
||||||
- Erkläre das Konzept gründlicher: das Warum hinter den Regeln, nicht nur das Wie.
|
- Geh deutlich tiefer als die Guide-Fassung: das Warum hinter den Regeln gründlich, Mechanik im Detail, nicht nur das Wie.
|
||||||
- Mehr und reichere Beispiele als im Guide — Varianten, Grenzfälle, ein realistischer Anwendungsfall.
|
- Mehr und reichere Beispiele als im Guide — Varianten, Grenzfälle, ein realistischer Anwendungsfall.
|
||||||
- Typische Fehler und Missverständnisse, jeweils mit Korrektur.
|
- Typische Fehler und Missverständnisse, jeweils mit Korrektur.
|
||||||
- Abgrenzung zu verwandten Konzepten, wo Verwechslungsgefahr besteht.
|
- Abgrenzung zu verwandten Konzepten, wo Verwechslungsgefahr besteht.
|
||||||
|
|||||||
27
templates/Prompt/Baustein-Pruefung-Diskussion.md
Normal file
27
templates/Prompt/Baustein-Pruefung-Diskussion.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Du bist Tutor in einer Prüfung zum Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Der Lerner diskutiert mit dir — über die geprüfte Frage oder über deine letzte Bewertung. Du DISKUTIERST, du bewertest NICHT.
|
||||||
|
|
||||||
|
GEPRÜFTE FRAGE:
|
||||||
|
{frage}
|
||||||
|
|
||||||
|
DEINE LETZTE BEWERTUNG (falls vorhanden):
|
||||||
|
{letzte_bewertung_block}
|
||||||
|
|
||||||
|
BAUSTEIN AUS DEM GUIDE:
|
||||||
|
{section_block}
|
||||||
|
|
||||||
|
VERTIEFUNG (falls vorhanden):
|
||||||
|
{vertiefung_block}
|
||||||
|
|
||||||
|
BISHERIGER VERLAUF:
|
||||||
|
{transcript}
|
||||||
|
|
||||||
|
Antworte als Tutor auf die letzte Nutzer-Nachricht.
|
||||||
|
|
||||||
|
WICHTIG:
|
||||||
|
- Bleib bei der geprüften Frage und beim Material. Guide-Fassung und Vertiefung sind die fachliche Referenz.
|
||||||
|
- Ist die Frage unklar: erkläre sie, ohne die Lösung zu verraten.
|
||||||
|
- Zeigt der Lerner SACHLICH und mit Material-Bezug, dass deine Frage oder eine vorige Bewertung falsch war: räume es offen ein. Schlage dann vor, die Antwort erneut bewerten zu lassen.
|
||||||
|
- Gib NICHT aus Höflichkeit oder auf bloßes Beharren hin nach. Nur ein echtes Sach-Argument zählt.
|
||||||
|
- Du vergibst KEINE Bewertung und stellst KEINE neue Prüfungsfrage.
|
||||||
|
|
||||||
|
Antwortstil: kurz und klar, 1–3 Sätze. Keine Einleitung, kein Markdown-Drumherum, kein Präfix wie "Assistent:". Gib NUR die Antwort aus.
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
Schreibe eine kurze Vertiefung zum Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Der Leser kennt die kompakte Fassung und will einen Schritt weiter — nicht mehr.
|
Schreibe eine kurze Vertiefung zum Baustein "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Der Leser kennt die Guide-Fassung — die erklärt das Konzept bereits — und will einen Schritt weiter, nicht mehr.
|
||||||
|
|
||||||
KOMPAKTE FASSUNG AUS DEM GUIDE:
|
GUIDE-FASSUNG DES BAUSTEINS:
|
||||||
{section_block}
|
{section_block}
|
||||||
|
|
||||||
Inhalt:
|
Inhalt:
|
||||||
- Erkläre kurz das Warum hinter dem Konzept — den einen Punkt, der im Guide fehlt.
|
- Geh über die Guide-Fassung hinaus: ein tieferer Grund, eine Feinheit oder ein Aspekt, den der Guide bewusst weglässt. Wiederhole NICHT das Warum, das der Guide schon erklärt.
|
||||||
- 1–2 zusätzliche Beispiele: eine andere Variante oder ein Grenzfall, nicht die Guide-Beispiele wiederholen.
|
- 1–2 zusätzliche Beispiele: eine andere Variante oder ein Grenzfall, nicht die Guide-Beispiele wiederholen.
|
||||||
- Höchstens ein typischer Fehler, wenn er wirklich häufig ist.
|
- Höchstens ein typischer Fehler, wenn er wirklich häufig ist.
|
||||||
- Nichts wiederholen, was die kompakte Fassung schon sagt.
|
- Nichts wiederholen, was die kompakte Fassung schon sagt.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ SECTIONS:
|
|||||||
{sections}
|
{sections}
|
||||||
|
|
||||||
Prüfe jede Section:
|
Prüfe jede Section:
|
||||||
1. Ist die Beschreibung für Anfänger verständlich und maximal 1–2 Sätze?
|
1. Lehrt die Section das Konzept für einen Anfänger ohne Vorwissen verständlich — ordnet sie es ein, erklärt sie das Wie/Warum, macht ein Beispiel es konkret? Sie soll so lang wie nötig und so kurz wie möglich sein: kein Roman, keine Füllsätze, keine Einleitungsfloskeln — aber auch nicht so verdichtet, dass nur jemand sie versteht, der das Thema schon kennt.
|
||||||
2. Sind die Beispiele kurz, simpel, plausibel korrekt — und im themengerechten Format laut Spezifikation (kein Codeblock um Prosa-Beispiele, kein Prosa-Pseudo-Beispiel, wo Code gefragt ist)?
|
2. Sind die Beispiele kurz, simpel, plausibel korrekt — und im themengerechten Format laut Spezifikation (kein Codeblock um Prosa-Beispiele, kein Prosa-Pseudo-Beispiel, wo Code gefragt ist)?
|
||||||
3. Ist das Markdown sauber (keine abgebrochenen Code-Blöcke, keine Platzhalter, kein Fremdtext)?
|
3. Ist das Markdown sauber (keine abgebrochenen Code-Blöcke, keine Platzhalter, kein Fremdtext)?
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Behebe pro Section NUR das notierte Problem; was in Ordnung ist, bleibt inhaltli
|
|||||||
Schreibe NUR die Datei {out_path} in GENAU diesem Format — für JEDE beanstandete Section ein section-Marker (Titel EXAKT wie oben), darunter der vollständige neue Markdown-Body:
|
Schreibe NUR die Datei {out_path} in GENAU diesem Format — für JEDE beanstandete Section ein section-Marker (Titel EXAKT wie oben), darunter der vollständige neue Markdown-Body:
|
||||||
|
|
||||||
<!-- section: Exakter Section-Titel -->
|
<!-- section: Exakter Section-Titel -->
|
||||||
Beschreibung…
|
Erklärung (Einordnung → Wie/Warum) laut SECTION-SPEZIFIKATION…
|
||||||
|
|
||||||
### Beispiel
|
### Beispiel
|
||||||
(Beispiel im themengerechten Format laut SECTION-SPEZIFIKATION: Codeblock NUR bei Code-Themen, sonst Beispielsätze oder Mini-Szenario)
|
(Beispiel im themengerechten Format laut SECTION-SPEZIFIKATION: Codeblock NUR bei Code-Themen, sonst Beispielsätze oder Mini-Szenario)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Schreibe NUR die Datei {out_path} in GENAU diesem Format — pro Kapitel ein kap
|
|||||||
|
|
||||||
<!-- kapitel: Kapiteltitel -->
|
<!-- kapitel: Kapiteltitel -->
|
||||||
<!-- section: Exakter Baustein-Titel -->
|
<!-- section: Exakter Baustein-Titel -->
|
||||||
Beschreibung…
|
Erklärung (Einordnung → Wie/Warum) laut SECTION-SPEZIFIKATION…
|
||||||
|
|
||||||
### Beispiel
|
### Beispiel
|
||||||
(Beispiel im themengerechten Format laut SECTION-SPEZIFIKATION: Codeblock NUR bei Code-Themen, sonst Beispielsätze oder Mini-Szenario)
|
(Beispiel im themengerechten Format laut SECTION-SPEZIFIKATION: Codeblock NUR bei Code-Themen, sonst Beispielsätze oder Mini-Szenario)
|
||||||
|
|||||||
Reference in New Issue
Block a user