This commit is contained in:
team3
2026-06-14 14:02:27 +02:00
parent 822f6ee3e9
commit 2b89e21cd3
18 changed files with 378 additions and 119 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 12 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 23 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): 13 Beispielsätze oder ein Mini-Dialog, fremdsprachiger Teil *kursiv*, deutsche Übersetzung in Klammern wo nötig. - Sprach-Thema (Vokabeln, Grammatik, Formulierungen): 13 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 24 Sätzen (Situation → Anwendung → Wirkung), ein Schema oder eine Formel. - Konzept-Thema (Psychologie, Kommunikation, Methoden, Theorie, Mathe): ein Mini-Szenario in 24 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.

View File

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

View File

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

View File

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

View 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, 13 Sätze. Keine Einleitung, kein Markdown-Drumherum, kein Präfix wie "Assistent:". Gib NUR die Antwort aus.

View File

@@ -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.
- 12 zusätzliche Beispiele: eine andere Variante oder ein Grenzfall, nicht die Guide-Beispiele wiederholen. - 12 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.

View File

@@ -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 12 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)?

View File

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

View File

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