update
This commit is contained in:
3
Makefile
3
Makefile
@@ -12,7 +12,8 @@ auth:
|
|||||||
@echo "Verzeichnisse angelegt und auf uid 1000 chowned."
|
@echo "Verzeichnisse angelegt und auf uid 1000 chowned."
|
||||||
|
|
||||||
install:
|
install:
|
||||||
pip install --break-system-packages fastapi uvicorn[standard] aiosqlite uv playwright
|
pip install --break-system-packages fastapi uvicorn[standard] aiosqlite uv playwright transformers
|
||||||
|
pip install --break-system-packages torch --index-url https://download.pytorch.org/whl/cpu
|
||||||
python3 -m playwright install chromium
|
python3 -m playwright install chromium
|
||||||
@echo "Falls Chromium OS-Libs fehlen: 'sudo python3 -m playwright install-deps chromium' einmalig ausführen."
|
@echo "Falls Chromium OS-Libs fehlen: 'sudo python3 -m playwright install-deps chromium' einmalig ausführen."
|
||||||
@which pdftotext >/dev/null 2>&1 || sudo apt-get install -y poppler-utils
|
@which pdftotext >/dev/null 2>&1 || sudo apt-get install -y poppler-utils
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ UNI_DIR = PROJECT_ROOT / "uni"
|
|||||||
|
|
||||||
MAX_CONCURRENT_GENERATIONS = 10
|
MAX_CONCURRENT_GENERATIONS = 10
|
||||||
|
|
||||||
|
# Lesbarkeits-Gate: deterministischer Prüfer (kleines deutsches Komplexitäts-Modell,
|
||||||
|
# Skala 1–7). Zu schwere Sections gehen in die Lese-Prüfungs-Überarbeitung.
|
||||||
|
# Fehlen transformers/torch oder das Modell → Gate stumm aus.
|
||||||
|
LESBARKEIT_AKTIV = True
|
||||||
|
LESBARKEIT_MODELL = "MiriUll/distilbert-german-text-complexity"
|
||||||
|
# Anker auf der 1–7-Skala (TextComplexityDE): Leichte Sprache ~1,2; Wikipedia-Schnitt
|
||||||
|
# ~3,22; ab MOS > 4 gilt ein Satz als „echt komplex" (Vereinfachungs-Grenze des Papers).
|
||||||
|
LESBARKEIT_MAX = 3.5 # Section zu schwer, wenn der Satz-Schnitt darüber liegt
|
||||||
|
LESBARKEIT_HART = 4.0 # Einzelsatz ab hier „hart"
|
||||||
|
LESBARKEIT_HART_ANTEIL = 0.30 # … ODER wenn dieser Anteil der Sätze hart ist
|
||||||
|
|
||||||
# Deckel für gleichzeitige CLI-Agenten-Prozesse (über alle Generierungen hinweg).
|
# Deckel für gleichzeitige CLI-Agenten-Prozesse (über alle Generierungen hinweg).
|
||||||
# Eigene Spur für interaktive Aufrufe (Chat, Elemente), damit sie nicht hinter
|
# Eigene Spur für interaktive Aufrufe (Chat, Elemente), damit sie nicht hinter
|
||||||
# laufenden Writern in der Warteschlange hängen.
|
# laufenden Writern in der Warteschlange hängen.
|
||||||
|
|||||||
@@ -312,39 +312,6 @@ def _now() -> str:
|
|||||||
return datetime.now(timezone.utc).isoformat()
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
async def get_vertiefung(topic: str, baustein: str, art: str) -> str | None:
|
|
||||||
db = await get_db()
|
|
||||||
cursor = await db.execute(
|
|
||||||
"SELECT md FROM baustein_texte WHERE topic = ? AND baustein = ? AND art = ?",
|
|
||||||
(topic, baustein, art),
|
|
||||||
)
|
|
||||||
row = await cursor.fetchone()
|
|
||||||
return row[0] if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def set_vertiefung(topic: str, baustein: str, art: str, md: str) -> None:
|
|
||||||
db = await get_db()
|
|
||||||
now = _now()
|
|
||||||
await db.execute(
|
|
||||||
"""INSERT INTO baustein_texte (topic, baustein, art, md, created_at, updated_at)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
ON CONFLICT(topic, baustein, art) DO UPDATE SET md = excluded.md, updated_at = excluded.updated_at""",
|
|
||||||
(topic, baustein, art, md, now, now),
|
|
||||||
)
|
|
||||||
await db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
async def list_vertiefungen(topic: str) -> dict[str, set[str]]:
|
|
||||||
"""Baustein-Titel → vorhandene Text-Arten ('vertiefung'/'deepdive')."""
|
|
||||||
db = await get_db()
|
|
||||||
cursor = await db.execute("SELECT baustein, art FROM baustein_texte WHERE topic = ?", (topic,))
|
|
||||||
rows = await cursor.fetchall()
|
|
||||||
out: dict[str, set[str]] = {}
|
|
||||||
for baustein, art in rows:
|
|
||||||
out.setdefault(baustein, set()).add(art)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
async def list_baustein_progress(topic: str) -> list[dict]:
|
async def list_baustein_progress(topic: str) -> list[dict]:
|
||||||
db = await get_db()
|
db = await get_db()
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ from agents import run_agent
|
|||||||
from bausteine import _pdfs_konvertieren, quelle_ordner
|
from bausteine import _pdfs_konvertieren, quelle_ordner
|
||||||
from config import (
|
from config import (
|
||||||
DEFAULT_PROVIDER, FORMAT_ANTEIL, KONSENS_GRACE, KONSENS_MAX_RUNDEN,
|
DEFAULT_PROVIDER, FORMAT_ANTEIL, KONSENS_GRACE, KONSENS_MAX_RUNDEN,
|
||||||
TEMPLATES_DIR,
|
LESBARKEIT_AKTIV, TEMPLATES_DIR,
|
||||||
)
|
)
|
||||||
|
import lesbarkeit
|
||||||
from database import list_guides, update_guide
|
from database import list_guides, update_guide
|
||||||
from fsutil import atomic_write_json
|
from fsutil import atomic_write_json
|
||||||
from jsonio import read_json_file as _json_datei
|
from jsonio import read_json_file as _json_datei
|
||||||
@@ -709,6 +710,17 @@ async def _generate_sections(
|
|||||||
num = _titel_aufloesen(idx, item["section"])
|
num = _titel_aufloesen(idx, item["section"])
|
||||||
if num in geltung and num in by_num and num not in probleme_by_num:
|
if num in geltung and num in by_num and num not in probleme_by_num:
|
||||||
probleme_by_num[num] = item["problem"]
|
probleme_by_num[num] = item["problem"]
|
||||||
|
|
||||||
|
# Deterministisches Lesbarkeits-Gate: zu schwere Sections in dieselbe
|
||||||
|
# Überarbeitung einreihen (LLM-Beanstandung hat Vorrang). Gate aus → no-op.
|
||||||
|
if LESBARKEIT_AKTIV:
|
||||||
|
md_by_num = {num: by_num[num]["md"] for nums in scope for num in nums if num in by_num}
|
||||||
|
hinweise = await asyncio.to_thread(lesbarkeit.bewerte_sections, md_by_num)
|
||||||
|
if hinweise:
|
||||||
|
_log(topic, f"Lesbarkeit: {len(hinweise)} Section(s) zu schwer")
|
||||||
|
for num, hinweis in hinweise.items():
|
||||||
|
probleme_by_num.setdefault(num, hinweis)
|
||||||
|
|
||||||
if not probleme_by_num:
|
if not probleme_by_num:
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -756,7 +768,8 @@ async def _generate_sections(
|
|||||||
chapters: list[dict] = []
|
chapters: list[dict] = []
|
||||||
for ch in plan:
|
for ch in plan:
|
||||||
sections = [
|
sections = [
|
||||||
{"num": num, "title": _titel(entries[num]), "md": by_num[num]["md"]}
|
{"num": num, "title": _titel(entries[num]), "md": by_num[num]["md"],
|
||||||
|
"kompakt": by_num[num].get("kompakt", "")}
|
||||||
for num in ch["nums"] if num in by_num
|
for num in ch["nums"] if num in by_num
|
||||||
]
|
]
|
||||||
if sections:
|
if sections:
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ log = logging.getLogger("creator.lernen")
|
|||||||
NOETIG = 3 # gute Antworten bis "absolviert" (Tier 1)
|
NOETIG = 3 # gute Antworten bis "absolviert" (Tier 1)
|
||||||
MASTERY = 10 # Score bis "verstanden" (Tier 2)
|
MASTERY = 10 # Score bis "verstanden" (Tier 2)
|
||||||
MEISTERN = 25 # Score bis "gemeistert" (Tier 3, Maximum)
|
MEISTERN = 25 # Score bis "gemeistert" (Tier 3, Maximum)
|
||||||
VERTIEFUNG_TIMEOUT = 600
|
|
||||||
CHAT_TIMEOUT = 240
|
CHAT_TIMEOUT = 240
|
||||||
PRUEFUNG_TIMEOUT = 120 # kurze JSON-Turns; deckelt die Serien-Latenz pro Prüfungs-Schritt
|
PRUEFUNG_TIMEOUT = 120 # kurze JSON-Turns; deckelt die Serien-Latenz pro Prüfungs-Schritt
|
||||||
GRUENDLICH_TIMEOUT = 600 # „Gründlich prüfen": starkes Modell (role guide) braucht länger
|
GRUENDLICH_TIMEOUT = 600 # „Gründlich prüfen": starkes Modell (role guide) braucht länger
|
||||||
@@ -84,37 +83,13 @@ def _transcript(messages: list[dict]) -> str:
|
|||||||
) or "(leer)"
|
) or "(leer)"
|
||||||
|
|
||||||
|
|
||||||
async def vertiefung_generieren(topic: str, baustein: str, section: str, art: str = "vertiefung", provider: str = DEFAULT_PROVIDER) -> str | None:
|
async def baustein_chat(topic: str, baustein: str, section: str, kompakt: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER) -> str:
|
||||||
"""Ausführlichere Fassung des Bausteins als Markdown · None bei Fehler.
|
|
||||||
|
|
||||||
art "vertiefung" = gleicher Stoff, nur umfangreicher;
|
|
||||||
"deepdive" = Label „Amateur": gleicher Stoff, für Einsteiger aufbereitet.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
prompt = _prompt(
|
|
||||||
"Baustein-Deepdive" if art == "deepdive" else "Baustein-Vertiefung",
|
|
||||||
topic=topic, baustein=baustein,
|
|
||||||
section_block=section.strip() or "(keine Guide-Fassung übergeben)",
|
|
||||||
)
|
|
||||||
returncode, stdout, _ = await run_agent(
|
|
||||||
"vertiefung-" + str(uuid.uuid4()), prompt, VERTIEFUNG_TIMEOUT,
|
|
||||||
provider=provider, role="fast", capabilities="none", lane="interactive",
|
|
||||||
)
|
|
||||||
if returncode != 0:
|
|
||||||
return None
|
|
||||||
return stdout.strip() or None
|
|
||||||
except Exception:
|
|
||||||
log.warning("[%s] Vertiefung fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def baustein_chat(topic: str, baustein: str, section: str, vertiefung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER) -> str:
|
|
||||||
try:
|
try:
|
||||||
prompt = _prompt(
|
prompt = _prompt(
|
||||||
"Baustein-Chat",
|
"Baustein-Chat",
|
||||||
topic=topic, baustein=baustein,
|
topic=topic, baustein=baustein,
|
||||||
section_block=section.strip() or "(keine Guide-Fassung übergeben)",
|
section_block=section.strip() or "(keine Guide-Fassung übergeben)",
|
||||||
vertiefung_block=(vertiefung or "").strip() or "(keine)",
|
kompakt_block=(kompakt or "").strip() or "(keine)",
|
||||||
transcript=_transcript(messages),
|
transcript=_transcript(messages),
|
||||||
)
|
)
|
||||||
returncode, stdout, _ = await run_agent(
|
returncode, stdout, _ = await run_agent(
|
||||||
@@ -197,7 +172,7 @@ def _doppelfrage_mangel(frage: str) -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
async def _frage_mit_kritik(
|
async def _frage_mit_kritik(
|
||||||
topic: str, baustein: str, section_block: str, vertiefung_block: str,
|
topic: str, baustein: str, section_block: str, kompakt_block: str,
|
||||||
transcript: str, provider: str,
|
transcript: str, provider: str,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN)."""
|
"""Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN)."""
|
||||||
@@ -207,7 +182,7 @@ async def _frage_mit_kritik(
|
|||||||
data = await _gen_call(
|
data = await _gen_call(
|
||||||
"Baustein-Frage", "fast", _frage_schema, provider,
|
"Baustein-Frage", "fast", _frage_schema, provider,
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
vertiefung_block=vertiefung_block, transcript=transcript, kritik_block=kritik_block,
|
kompakt_block=kompakt_block, transcript=transcript, kritik_block=kritik_block,
|
||||||
)
|
)
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
@@ -215,7 +190,7 @@ async def _frage_mit_kritik(
|
|||||||
probleme = await _kritik_call(
|
probleme = await _kritik_call(
|
||||||
"Baustein-Frage-Kritik", provider, role="guide", # starke KI prüft die Regeln
|
"Baustein-Frage-Kritik", provider, role="guide", # starke KI prüft die Regeln
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
vertiefung_block=vertiefung_block, transcript=transcript, frage=frage,
|
kompakt_block=kompakt_block, transcript=transcript, frage=frage,
|
||||||
)
|
)
|
||||||
hart = _doppelfrage_mangel(frage) # erzwingt Neugenerierung, auch wenn der KI-Kritiker es übersah
|
hart = _doppelfrage_mangel(frage) # erzwingt Neugenerierung, auch wenn der KI-Kritiker es übersah
|
||||||
if hart:
|
if hart:
|
||||||
@@ -227,7 +202,7 @@ 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, kompakt_block: str,
|
||||||
frage: str, transcript: str, gute_antworten: int, provider: str, role: str = "judge",
|
frage: str, transcript: str, gute_antworten: int, provider: str, role: str = "judge",
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""Antwort bewerten (gut/schlecht), vom Kritiker prüfen lassen, bei Fehlurteil neu.
|
"""Antwort bewerten (gut/schlecht), vom Kritiker prüfen lassen, bei Fehlurteil neu.
|
||||||
@@ -243,7 +218,7 @@ async def _bewertung_mit_kritik(
|
|||||||
bew = await _gen_call(
|
bew = await _gen_call(
|
||||||
"Baustein-Bewertung", role, _bewertung_schema, provider, timeout,
|
"Baustein-Bewertung", role, _bewertung_schema, provider, timeout,
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
vertiefung_block=vertiefung_block, frage=frage, transcript=transcript,
|
kompakt_block=kompakt_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:
|
||||||
@@ -251,7 +226,7 @@ async def _bewertung_mit_kritik(
|
|||||||
probleme = await _kritik_call(
|
probleme = await _kritik_call(
|
||||||
"Baustein-Bewertung-Kritik", provider, role=role, timeout=timeout,
|
"Baustein-Bewertung-Kritik", provider, role=role, timeout=timeout,
|
||||||
topic=topic, baustein=baustein, section_block=section_block,
|
topic=topic, baustein=baustein, section_block=section_block,
|
||||||
vertiefung_block=vertiefung_block, frage=frage, transcript=transcript,
|
kompakt_block=kompakt_block, frage=frage, transcript=transcript,
|
||||||
bewertung_block=_bewertung_text(bew),
|
bewertung_block=_bewertung_text(bew),
|
||||||
)
|
)
|
||||||
if not probleme:
|
if not probleme:
|
||||||
@@ -260,29 +235,29 @@ async def _bewertung_mit_kritik(
|
|||||||
return bew # best-effort nach der letzten Runde
|
return bew # best-effort nach der letzten Runde
|
||||||
|
|
||||||
|
|
||||||
def _bloecke(section: str, vertiefung: str | None) -> tuple[str, str]:
|
def _bloecke(section: str, kompakt: str | None) -> tuple[str, str]:
|
||||||
return (
|
return (
|
||||||
section.strip() or "(keine Guide-Fassung übergeben)",
|
section.strip() or "(keine Guide-Fassung übergeben)",
|
||||||
(vertiefung or "").strip() or "(keine)",
|
(kompakt or "").strip() or "(keine)",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def pruefung_frage(
|
async def pruefung_frage(
|
||||||
topic: str, baustein: str, section: str, vertiefung: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Aktion 'frage': nächste Frage generieren (Generator + Kritiker) · None bei Fehler."""
|
"""Aktion 'frage': nächste Frage generieren (Generator + Kritiker) · None bei Fehler."""
|
||||||
try:
|
try:
|
||||||
section_block, vertiefung_block = _bloecke(section, vertiefung)
|
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||||
transcript = _transcript(messages) if messages else "(leer)"
|
transcript = _transcript(messages) if messages else "(leer)"
|
||||||
return await _frage_mit_kritik(topic, baustein, section_block, vertiefung_block, transcript, provider)
|
return await _frage_mit_kritik(topic, baustein, section_block, kompakt_block, transcript, provider)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def pruefung_bewertung(
|
async def pruefung_bewertung(
|
||||||
topic: str, baustein: str, section: str, vertiefung: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
|
frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
|
||||||
role: str = "judge",
|
role: str = "judge",
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
@@ -292,10 +267,10 @@ async def pruefung_bewertung(
|
|||||||
Gibt {"feedback", "bewertung": gut|schlecht, "bestanden"} · None bei Fehler.
|
Gibt {"feedback", "bewertung": gut|schlecht, "bestanden"} · None bei Fehler.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
section_block, vertiefung_block = _bloecke(section, vertiefung)
|
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||||
transcript = _transcript(messages) if messages else "(leer)"
|
transcript = _transcript(messages) if messages else "(leer)"
|
||||||
return await _bewertung_mit_kritik(
|
return await _bewertung_mit_kritik(
|
||||||
topic, baustein, section_block, vertiefung_block,
|
topic, baustein, section_block, kompakt_block,
|
||||||
frage.strip() or "(keine Frage übergeben)", transcript, gute_antworten, provider, role,
|
frage.strip() or "(keine Frage übergeben)", transcript, gute_antworten, provider, role,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -304,7 +279,7 @@ async def pruefung_bewertung(
|
|||||||
|
|
||||||
|
|
||||||
async def pruefung_bewertung_schnell(
|
async def pruefung_bewertung_schnell(
|
||||||
topic: str, baustein: str, section: str, vertiefung: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
|
frage: str, messages: list[dict], gute_antworten: int, provider: str = DEFAULT_PROVIDER,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""Aktion 'antwort' (Vorschau): nur Evaluator, KEIN Kritiker — sofortiges Urteil.
|
"""Aktion 'antwort' (Vorschau): nur Evaluator, KEIN Kritiker — sofortiges Urteil.
|
||||||
@@ -312,12 +287,12 @@ async def pruefung_bewertung_schnell(
|
|||||||
Wird optimistisch angezeigt; 'antwort_pruefen' liefert danach das geprüfte Urteil.
|
Wird optimistisch angezeigt; 'antwort_pruefen' liefert danach das geprüfte Urteil.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
section_block, vertiefung_block = _bloecke(section, vertiefung)
|
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||||
transcript = _transcript(messages) if messages else "(leer)"
|
transcript = _transcript(messages) if messages else "(leer)"
|
||||||
return await _gen_call(
|
return 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, frage=frage.strip() or "(keine Frage übergeben)",
|
kompakt_block=kompakt_block, frage=frage.strip() or "(keine Frage übergeben)",
|
||||||
transcript=transcript, gute_antworten=gute_antworten, noetig=NOETIG, kritik_block="(keine)",
|
transcript=transcript, gute_antworten=gute_antworten, noetig=NOETIG, kritik_block="(keine)",
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -326,7 +301,7 @@ async def pruefung_bewertung_schnell(
|
|||||||
|
|
||||||
|
|
||||||
async def baustein_diskussion(
|
async def baustein_diskussion(
|
||||||
topic: str, baustein: str, section: str, vertiefung: str | None,
|
topic: str, baustein: str, section: str, kompakt: str | None,
|
||||||
frage: str, letzte_bewertung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
frage: str, letzte_bewertung: str | None, messages: list[dict], provider: str = DEFAULT_PROVIDER,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Aktion 'diskussion': Tutor erklärt/diskutiert die Frage oder eine Bewertung.
|
"""Aktion 'diskussion': Tutor erklärt/diskutiert die Frage oder eine Bewertung.
|
||||||
@@ -334,11 +309,11 @@ async def baustein_diskussion(
|
|||||||
Kein Bewerten, kein Kritiker — hier ist der Mensch der Prüfer. None bei Fehler.
|
Kein Bewerten, kein Kritiker — hier ist der Mensch der Prüfer. None bei Fehler.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
section_block, vertiefung_block = _bloecke(section, vertiefung)
|
section_block, kompakt_block = _bloecke(section, kompakt)
|
||||||
prompt = _prompt(
|
prompt = _prompt(
|
||||||
"Baustein-Pruefung-Diskussion",
|
"Baustein-Pruefung-Diskussion",
|
||||||
topic=topic, baustein=baustein,
|
topic=topic, baustein=baustein,
|
||||||
section_block=section_block, vertiefung_block=vertiefung_block,
|
section_block=section_block, kompakt_block=kompakt_block,
|
||||||
frage=frage.strip() or "(keine Frage übergeben)",
|
frage=frage.strip() or "(keine Frage übergeben)",
|
||||||
letzte_bewertung_block=(letzte_bewertung or "").strip() or "(noch keine)",
|
letzte_bewertung_block=(letzte_bewertung or "").strip() or "(noch keine)",
|
||||||
transcript=_transcript(messages) if messages else "(leer)",
|
transcript=_transcript(messages) if messages else "(leer)",
|
||||||
|
|||||||
115
backend/lesbarkeit.py
Normal file
115
backend/lesbarkeit.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"""Deterministisches Lesbarkeits-Gate für Guide-Sections.
|
||||||
|
|
||||||
|
Ein kleines deutsches Komplexitäts-Modell (DistilBERT, GermEval 2022, Skala 1–7)
|
||||||
|
bewertet die Verständlichkeit der Fließtext-Prosa. guide.py meldet zu schwere
|
||||||
|
Sections in die bestehende Lese-Prüfungs-/Überarbeitungs-Schleife — kein Prompt,
|
||||||
|
kein Raten.
|
||||||
|
|
||||||
|
Optional: fehlen `transformers`/`torch` oder lädt das Modell nicht, ist das Gate
|
||||||
|
stumm deaktiviert (das Backend läuft unverändert weiter). CPU genügt; der Aufrufer
|
||||||
|
wrappt die Bewertung in `asyncio.to_thread` (blockierende Modell-Inferenz).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
LESBARKEIT_AKTIV, LESBARKEIT_HART, LESBARKEIT_HART_ANTEIL, LESBARKEIT_MAX, LESBARKEIT_MODELL,
|
||||||
|
)
|
||||||
|
|
||||||
|
log = logging.getLogger("creator.lesbarkeit")
|
||||||
|
|
||||||
|
_modell_cache = None # (tokenizer, model, torch) — Singleton
|
||||||
|
_ladeversuch = False # schon versucht zu laden?
|
||||||
|
|
||||||
|
# Markup raus → reiner Fließtext (Code zählt nicht zur Lesbarkeit).
|
||||||
|
_CODE_FENCE = re.compile(r"```.*?```", re.DOTALL)
|
||||||
|
_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
|
||||||
|
_INLINE_CODE = re.compile(r"`[^`]*`")
|
||||||
|
_LINK = re.compile(r"\[([^\]]*)\]\([^)]*\)")
|
||||||
|
_MD_MARK = re.compile(r"^[ \t]*([#>]+|[-*+]\s)|[*_~|]", re.MULTILINE)
|
||||||
|
_WS = re.compile(r"\s+")
|
||||||
|
_SATZ = re.compile(r"(?<=[.!?])\s+")
|
||||||
|
|
||||||
|
|
||||||
|
def _modell():
|
||||||
|
"""Lädt das Modell einmalig. None = Gate aus (deaktiviert oder Lade-Fehler)."""
|
||||||
|
global _modell_cache, _ladeversuch
|
||||||
|
if _ladeversuch:
|
||||||
|
return _modell_cache
|
||||||
|
_ladeversuch = True
|
||||||
|
if not LESBARKEIT_AKTIV:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
||||||
|
tok = AutoTokenizer.from_pretrained(LESBARKEIT_MODELL)
|
||||||
|
model = AutoModelForSequenceClassification.from_pretrained(LESBARKEIT_MODELL)
|
||||||
|
model.eval()
|
||||||
|
_modell_cache = (tok, model, torch)
|
||||||
|
log.info("Lesbarkeits-Modell geladen: %s (num_labels=%d)", LESBARKEIT_MODELL, model.config.num_labels)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("Lesbarkeits-Gate deaktiviert (Modell nicht ladbar): %s", e)
|
||||||
|
_modell_cache = None
|
||||||
|
return _modell_cache
|
||||||
|
|
||||||
|
|
||||||
|
def _prosa(md: str) -> str:
|
||||||
|
"""Markdown/Code strippen → reiner Fließtext für die Bewertung."""
|
||||||
|
t = _CODE_FENCE.sub(" ", md)
|
||||||
|
t = _COMMENT.sub(" ", t)
|
||||||
|
t = _INLINE_CODE.sub(" ", t)
|
||||||
|
t = _LINK.sub(r"\1", t)
|
||||||
|
t = _MD_MARK.sub(" ", t)
|
||||||
|
return _WS.sub(" ", t).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _saetze(text: str) -> list[str]:
|
||||||
|
"""Fließtext in Sätze splitten; sehr kurze Fragmente verwerfen."""
|
||||||
|
return [s.strip() for s in _SATZ.split(text) if len(s.strip()) >= 15]
|
||||||
|
|
||||||
|
|
||||||
|
def _scores(saetze: list[str]) -> list[float]:
|
||||||
|
"""Komplexität je Satz (1–7). Regression (num_labels=1) oder Erwartungswert über Klassen."""
|
||||||
|
tok, model, torch = _modell_cache
|
||||||
|
werte: list[float] = []
|
||||||
|
n = model.config.num_labels
|
||||||
|
for i in range(0, len(saetze), 16):
|
||||||
|
batch = saetze[i:i + 16]
|
||||||
|
enc = tok(batch, return_tensors="pt", truncation=True, max_length=256, padding=True)
|
||||||
|
with torch.no_grad():
|
||||||
|
logits = model(**enc).logits
|
||||||
|
if n == 1:
|
||||||
|
vals = logits.reshape(-1).tolist()
|
||||||
|
else:
|
||||||
|
probs = torch.softmax(logits, dim=-1)
|
||||||
|
stufen = torch.arange(1, n + 1, dtype=probs.dtype)
|
||||||
|
vals = (probs * stufen).sum(-1).reshape(-1).tolist()
|
||||||
|
werte.extend(vals)
|
||||||
|
return werte
|
||||||
|
|
||||||
|
|
||||||
|
def bewerte_sections(md_by_num: dict[int, str]) -> dict[int, str]:
|
||||||
|
"""{num: section_md} → {num: Hinweis} nur für zu schwere Sections.
|
||||||
|
|
||||||
|
Leeres dict, wenn das Gate aus ist. Blockierend (CPU) — in to_thread aufrufen.
|
||||||
|
"""
|
||||||
|
if _modell() is None:
|
||||||
|
return {}
|
||||||
|
out: dict[int, str] = {}
|
||||||
|
for num, md in md_by_num.items():
|
||||||
|
saetze = _saetze(_prosa(md or ""))
|
||||||
|
if len(saetze) < 2: # fast nur Code / zu kurz → überspringen
|
||||||
|
continue
|
||||||
|
werte = _scores(saetze)
|
||||||
|
if not werte:
|
||||||
|
continue
|
||||||
|
schnitt = sum(werte) / len(werte)
|
||||||
|
hart = sum(1 for w in werte if w > LESBARKEIT_HART) / len(werte)
|
||||||
|
# Zu schwer = hoher Schnitt ODER zu viele harte Einzelsätze (Ausreißer-Nester).
|
||||||
|
if schnitt > LESBARKEIT_MAX or hart >= LESBARKEIT_HART_ANTEIL:
|
||||||
|
out[num] = (
|
||||||
|
f"Zu schwer lesbar (Ø {schnitt:.1f}/7, {hart * 100:.0f}% harte Sätze): "
|
||||||
|
"kürzere Sätze, einfachere Wörter, weniger Schachtelsätze, mehr Beispiele."
|
||||||
|
)
|
||||||
|
return out
|
||||||
@@ -196,25 +196,11 @@ class ProgressResponse(BaseModel):
|
|||||||
|
|
||||||
# --- Baustein-Lernen ---
|
# --- Baustein-Lernen ---
|
||||||
|
|
||||||
VertiefungArt = Literal["vertiefung", "deepdive"]
|
|
||||||
|
|
||||||
|
|
||||||
class VertiefungRequest(BaseModel):
|
|
||||||
topic: str = Field(min_length=1, max_length=100)
|
|
||||||
baustein: str = Field(min_length=1, max_length=200)
|
|
||||||
section: str = Field(default="", max_length=20000)
|
|
||||||
art: VertiefungArt = "vertiefung"
|
|
||||||
provider: ProviderType = "claude"
|
|
||||||
|
|
||||||
|
|
||||||
class VertiefungResponse(BaseModel):
|
|
||||||
md: str
|
|
||||||
|
|
||||||
|
|
||||||
class BausteinChatRequest(BaseModel):
|
class BausteinChatRequest(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) # ausführliche Fassung
|
||||||
|
section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze)
|
||||||
messages: list[ChatMessage] = Field(min_length=1)
|
messages: list[ChatMessage] = Field(min_length=1)
|
||||||
provider: ProviderType = "claude"
|
provider: ProviderType = "claude"
|
||||||
|
|
||||||
@@ -226,7 +212,8 @@ class BausteinChatResponse(BaseModel):
|
|||||||
class BausteinPruefungRequest(BaseModel):
|
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) # ausführliche Fassung
|
||||||
|
section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze)
|
||||||
aktion: Literal["frage", "diskussion", "antwort", "antwort_pruefen"] = "frage"
|
aktion: Literal["frage", "diskussion", "antwort", "antwort_pruefen"] = "frage"
|
||||||
frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort)
|
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)
|
letzte_bewertung: str = Field(default="", max_length=2000) # Feedback der letzten Bewertung (Kontext für diskussion)
|
||||||
@@ -257,8 +244,6 @@ class BausteinLernstand(BaseModel):
|
|||||||
absolviert: bool
|
absolviert: bool
|
||||||
verstanden: bool
|
verstanden: bool
|
||||||
gemeistert: bool
|
gemeistert: bool
|
||||||
vertiefung: bool
|
|
||||||
deepdive: bool
|
|
||||||
|
|
||||||
|
|
||||||
class BausteinLernstandResponse(BaseModel):
|
class BausteinLernstandResponse(BaseModel):
|
||||||
|
|||||||
@@ -2,3 +2,5 @@ fastapi
|
|||||||
uvicorn[standard]
|
uvicorn[standard]
|
||||||
aiosqlite
|
aiosqlite
|
||||||
playwright
|
playwright
|
||||||
|
transformers
|
||||||
|
torch
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ from database import (
|
|||||||
create_topic, list_topics as db_list_topics, delete_topic,
|
create_topic, list_topics as db_list_topics, delete_topic,
|
||||||
list_progress, set_progress, delete_progress,
|
list_progress, set_progress, delete_progress,
|
||||||
create_element, list_elements, get_element, update_element, delete_element,
|
create_element, list_elements, get_element, update_element, delete_element,
|
||||||
get_vertiefung, set_vertiefung, list_vertiefungen,
|
|
||||||
list_baustein_progress, set_baustein_score, set_baustein_streak, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten,
|
list_baustein_progress, set_baustein_score, set_baustein_streak, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten,
|
||||||
)
|
)
|
||||||
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht
|
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht
|
||||||
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
|
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
|
||||||
from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, score_berechnen, streak_berechnen, vertiefung_generieren
|
from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, score_berechnen, streak_berechnen
|
||||||
from guide import generate_guide, guide_slot_dateien
|
from guide import generate_guide, guide_slot_dateien
|
||||||
from pipeline import cancel_guide
|
from pipeline import cancel_guide
|
||||||
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
||||||
@@ -32,7 +31,6 @@ from models import (
|
|||||||
ElementRefineRequest, ElementRefineResponse,
|
ElementRefineRequest, ElementRefineResponse,
|
||||||
ProgressUpdate, ProgressResponse, ProjectResponse, ProviderInfo,
|
ProgressUpdate, ProgressResponse, ProjectResponse, ProviderInfo,
|
||||||
FolderResponse, BausteineQuelleUpdate, BausteineQuelleResponse, BausteinUebersicht,
|
FolderResponse, BausteineQuelleUpdate, BausteineQuelleResponse, BausteinUebersicht,
|
||||||
VertiefungRequest, VertiefungResponse,
|
|
||||||
BausteinChatRequest, BausteinChatResponse,
|
BausteinChatRequest, BausteinChatResponse,
|
||||||
BausteinPruefungRequest, BausteinPruefungResponse, BausteinLernstandResponse,
|
BausteinPruefungRequest, BausteinPruefungResponse, BausteinLernstandResponse,
|
||||||
)
|
)
|
||||||
@@ -207,18 +205,12 @@ async def get_bausteine_uebersicht(topic: str):
|
|||||||
return lade_uebersicht(topic)
|
return lade_uebersicht(topic)
|
||||||
|
|
||||||
|
|
||||||
# --- Baustein-Lernen: Vertiefung, Chat, Prüfung ---
|
# --- Baustein-Lernen: Chat, Prüfung ---
|
||||||
|
|
||||||
async def _bester_text(topic: str, baustein: str) -> str | None:
|
|
||||||
"""Kontext für Chat/Prüfung: Amateur-Fassung (art 'deepdive') bevorzugt, sonst Vertiefung."""
|
|
||||||
return await get_vertiefung(topic, baustein, "deepdive") or await get_vertiefung(topic, baustein, "vertiefung")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse)
|
@router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse)
|
||||||
async def baustein_lernstand(topic: str):
|
async def baustein_lernstand(topic: str):
|
||||||
"""Prüfungs-Stand + Vertiefungs-/Deepdive-Existenz pro Baustein (roher Titel als Key)."""
|
"""Prüfungs-Stand pro Baustein (roher Titel als Key)."""
|
||||||
progress = await list_baustein_progress(topic)
|
progress = await list_baustein_progress(topic)
|
||||||
texte = await list_vertiefungen(topic)
|
|
||||||
bausteine = {
|
bausteine = {
|
||||||
p["baustein"]: {
|
p["baustein"]: {
|
||||||
"gute_antworten": p["gute_antworten"],
|
"gute_antworten": p["gute_antworten"],
|
||||||
@@ -226,44 +218,16 @@ async def baustein_lernstand(topic: str):
|
|||||||
"absolviert": p["absolviert"] is not None,
|
"absolviert": p["absolviert"] is not None,
|
||||||
"verstanden": p["verstanden"] is not None,
|
"verstanden": p["verstanden"] is not None,
|
||||||
"gemeistert": p["gemeistert"] is not None,
|
"gemeistert": p["gemeistert"] is not None,
|
||||||
"vertiefung": "vertiefung" in texte.get(p["baustein"], set()),
|
|
||||||
"deepdive": "deepdive" in texte.get(p["baustein"], set()),
|
|
||||||
}
|
}
|
||||||
for p in progress
|
for p in progress
|
||||||
}
|
}
|
||||||
for b, arten in texte.items():
|
|
||||||
if b not in bausteine:
|
|
||||||
bausteine[b] = {
|
|
||||||
"gute_antworten": 0, "streak": 0, "absolviert": False, "verstanden": False, "gemeistert": False,
|
|
||||||
"vertiefung": "vertiefung" in arten, "deepdive": "deepdive" in arten,
|
|
||||||
}
|
|
||||||
return {"bausteine": bausteine}
|
return {"bausteine": bausteine}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/bausteine/vertiefung", response_model=VertiefungResponse)
|
|
||||||
async def get_baustein_vertiefung(topic: str, baustein: str, art: str = "vertiefung"):
|
|
||||||
if art not in ("vertiefung", "deepdive"):
|
|
||||||
raise HTTPException(400, "Unbekannte Art")
|
|
||||||
md = await get_vertiefung(topic, baustein, art)
|
|
||||||
if md is None:
|
|
||||||
raise HTTPException(404, "Kein Text vorhanden")
|
|
||||||
return {"md": md}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/bausteine/vertiefung", response_model=VertiefungResponse)
|
|
||||||
async def create_baustein_vertiefung(req: VertiefungRequest):
|
|
||||||
md = await vertiefung_generieren(req.topic, req.baustein, req.section, art=req.art, provider=req.provider)
|
|
||||||
if md is None:
|
|
||||||
raise HTTPException(502, "Generierung fehlgeschlagen — bitte erneut versuchen")
|
|
||||||
await set_vertiefung(req.topic, req.baustein, req.art, md)
|
|
||||||
return {"md": md}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/bausteine/chat", response_model=BausteinChatResponse)
|
@router.post("/bausteine/chat", response_model=BausteinChatResponse)
|
||||||
async def baustein_chat_route(req: BausteinChatRequest):
|
async def baustein_chat_route(req: BausteinChatRequest):
|
||||||
vertiefung = await _bester_text(req.topic, req.baustein)
|
|
||||||
reply = await baustein_chat(
|
reply = await baustein_chat(
|
||||||
req.topic, req.baustein, req.section, vertiefung,
|
req.topic, req.baustein, req.section, req.section_kompakt,
|
||||||
[m.model_dump() for m in req.messages], provider=req.provider,
|
[m.model_dump() for m in req.messages], provider=req.provider,
|
||||||
)
|
)
|
||||||
return {"reply": reply}
|
return {"reply": reply}
|
||||||
@@ -279,11 +243,11 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
absolviert = stand["absolviert"] is not None
|
absolviert = stand["absolviert"] is not None
|
||||||
verstanden = stand["verstanden"] is not None
|
verstanden = stand["verstanden"] is not None
|
||||||
gemeistert = stand["gemeistert"] is not None
|
gemeistert = stand["gemeistert"] is not None
|
||||||
vertiefung = await _bester_text(req.topic, req.baustein)
|
kompakt = req.section_kompakt
|
||||||
msgs = [m.model_dump() for m in req.messages]
|
msgs = [m.model_dump() for m in req.messages]
|
||||||
|
|
||||||
if req.aktion == "frage":
|
if req.aktion == "frage":
|
||||||
frage = await pruefung_frage(req.topic, req.baustein, req.section, vertiefung, msgs, provider=req.provider)
|
frage = await pruefung_frage(req.topic, req.baustein, req.section, kompakt, msgs, provider=req.provider)
|
||||||
if frage is None:
|
if frage is None:
|
||||||
raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen")
|
raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen")
|
||||||
return {"frage": frage, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
return {"frage": frage, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert}
|
||||||
@@ -292,7 +256,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
if not req.frage.strip():
|
if not req.frage.strip():
|
||||||
raise HTTPException(400, "Diskussion braucht eine laufende Frage")
|
raise HTTPException(400, "Diskussion braucht eine laufende Frage")
|
||||||
reply = await baustein_diskussion(
|
reply = await baustein_diskussion(
|
||||||
req.topic, req.baustein, req.section, vertiefung,
|
req.topic, req.baustein, req.section, kompakt,
|
||||||
req.frage, req.letzte_bewertung or None, msgs, provider=req.provider,
|
req.frage, req.letzte_bewertung or None, msgs, provider=req.provider,
|
||||||
)
|
)
|
||||||
if reply is None:
|
if reply is None:
|
||||||
@@ -309,7 +273,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
if req.aktion == "antwort":
|
if req.aktion == "antwort":
|
||||||
# Vorschau: nur Bewerter, kein Kritiker, KEIN Persist, KEINE Meilensteine.
|
# Vorschau: nur Bewerter, kein Kritiker, KEIN Persist, KEINE Meilensteine.
|
||||||
data = await pruefung_bewertung_schnell(
|
data = await pruefung_bewertung_schnell(
|
||||||
req.topic, req.baustein, req.section, vertiefung, req.frage, msgs, gute, provider=req.provider,
|
req.topic, req.baustein, req.section, kompakt, req.frage, msgs, gute, provider=req.provider,
|
||||||
)
|
)
|
||||||
if data is None:
|
if data is None:
|
||||||
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen")
|
||||||
@@ -319,7 +283,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
|
|
||||||
# aktion == "antwort_pruefen": verbindlich (Bewerter + Kritiker), persistiert Score + Meilensteine.
|
# aktion == "antwort_pruefen": verbindlich (Bewerter + Kritiker), persistiert Score + Meilensteine.
|
||||||
data = await pruefung_bewertung(
|
data = await pruefung_bewertung(
|
||||||
req.topic, req.baustein, req.section, vertiefung, req.frage, msgs, gute, provider=req.provider,
|
req.topic, req.baustein, req.section, kompakt, req.frage, msgs, gute, provider=req.provider,
|
||||||
role="guide" if req.gruendlich else "judge",
|
role="guide" if req.gruendlich else "judge",
|
||||||
)
|
)
|
||||||
if data is None:
|
if data is None:
|
||||||
|
|||||||
@@ -121,20 +121,25 @@ _FRAGMENT_KAPITEL_RE = re.compile(r"<!--\s*kapitel\s*:\s*(.*?)\s*-->", re.IGNORE
|
|||||||
_FRAGMENT_SECTION_RE = re.compile(r"<!--\s*section\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
_FRAGMENT_SECTION_RE = re.compile(r"<!--\s*section\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
||||||
_FRAGMENT_SUB_RE = re.compile(r"<!--\s*sub\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
_FRAGMENT_SUB_RE = re.compile(r"<!--\s*sub\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
||||||
_FRAGMENT_BAUSTEIN_RE = re.compile(r"<!--\s*baustein\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
_FRAGMENT_BAUSTEIN_RE = re.compile(r"<!--\s*baustein\s*:\s*(.*?)\s*-->", re.IGNORECASE)
|
||||||
|
# Zwei Lese-Schichten je Section: kompakt (Merksätze) + ausführlich (Erklärung).
|
||||||
|
_FRAGMENT_KOMPAKT_RE = re.compile(r"<!--\s*kompakt\s*-->", re.IGNORECASE)
|
||||||
|
_FRAGMENT_AUSF_RE = re.compile(r"<!--\s*ausf(?:ü|ue)hrlich\s*-->", re.IGNORECASE)
|
||||||
_STUFEN = ("einfach", "mittel", "schwer")
|
_STUFEN = ("einfach", "mittel", "schwer")
|
||||||
|
|
||||||
|
|
||||||
def _parse_fragment(text: str) -> list[dict]:
|
def _parse_fragment(text: str) -> list[dict]:
|
||||||
"""Parst eine Writer-Datei → [{"kapitel", "titel", "md", "subs"}] in Datei-Reihenfolge.
|
"""Parst eine Writer-Datei → [{"kapitel", "titel", "md", "kompakt", "subs"}] in Reihenfolge.
|
||||||
|
|
||||||
Optionale Subbaustein-Marker `<!-- sub: stufe | Titel -->` innerhalb einer
|
Zwei optionale Lese-Schichten je Section über `<!-- kompakt -->` / `<!-- ausführlich -->`:
|
||||||
Section sammeln sich in `sec["subs"] = [{"stufe", "titel", "md"}]`. `sec["md"]`
|
Zeilen nach `kompakt` landen in `sec["kompakt"]`, sonst in `sec["md"]` (= ausführlich,
|
||||||
bleibt der vollständige Body (alle Sub-Texte zusammengesetzt) — auch ohne Subs.
|
auch der Default ohne Marker → rückwärtskompatibel). Optionale `<!-- sub: -->`-Marker
|
||||||
|
sammeln sich weiter in `sec["subs"]`.
|
||||||
"""
|
"""
|
||||||
sections: list[dict] = []
|
sections: list[dict] = []
|
||||||
kapitel = None
|
kapitel = None
|
||||||
current = None
|
current = None
|
||||||
cur_sub = None
|
cur_sub = None
|
||||||
|
cur_layer = "md" # Default: alles ohne Schicht-Marker ist die ausführliche Fassung
|
||||||
for line in text.splitlines():
|
for line in text.splitlines():
|
||||||
s = line.strip()
|
s = line.strip()
|
||||||
m = _FRAGMENT_KAPITEL_RE.match(s)
|
m = _FRAGMENT_KAPITEL_RE.match(s)
|
||||||
@@ -145,10 +150,19 @@ def _parse_fragment(text: str) -> list[dict]:
|
|||||||
continue
|
continue
|
||||||
m = _FRAGMENT_SECTION_RE.match(s)
|
m = _FRAGMENT_SECTION_RE.match(s)
|
||||||
if m:
|
if m:
|
||||||
current = {"kapitel": kapitel, "titel": m.group(1), "md": [], "subs": []}
|
current = {"kapitel": kapitel, "titel": m.group(1), "md": [], "kompakt": [], "subs": []}
|
||||||
cur_sub = None
|
cur_sub = None
|
||||||
|
cur_layer = "md"
|
||||||
sections.append(current)
|
sections.append(current)
|
||||||
continue
|
continue
|
||||||
|
if current is not None and _FRAGMENT_KOMPAKT_RE.match(s):
|
||||||
|
cur_layer = "kompakt"
|
||||||
|
cur_sub = None
|
||||||
|
continue
|
||||||
|
if current is not None and _FRAGMENT_AUSF_RE.match(s):
|
||||||
|
cur_layer = "md"
|
||||||
|
cur_sub = None
|
||||||
|
continue
|
||||||
m = _FRAGMENT_SUB_RE.match(s)
|
m = _FRAGMENT_SUB_RE.match(s)
|
||||||
if m and current is not None:
|
if m and current is not None:
|
||||||
teil = m.group(1).split("|", 1)
|
teil = m.group(1).split("|", 1)
|
||||||
@@ -161,11 +175,12 @@ def _parse_fragment(text: str) -> list[dict]:
|
|||||||
current["subs"].append(cur_sub)
|
current["subs"].append(cur_sub)
|
||||||
continue
|
continue
|
||||||
if current is not None:
|
if current is not None:
|
||||||
current["md"].append(line)
|
current[cur_layer].append(line)
|
||||||
if cur_sub is not None:
|
if cur_sub is not None and cur_layer == "md":
|
||||||
cur_sub["md"].append(line)
|
cur_sub["md"].append(line)
|
||||||
for sec in sections:
|
for sec in sections:
|
||||||
sec["md"] = "\n".join(sec["md"]).strip()
|
sec["md"] = "\n".join(sec["md"]).strip()
|
||||||
|
sec["kompakt"] = "\n".join(sec["kompakt"]).strip()
|
||||||
for sub in sec["subs"]:
|
for sub in sec["subs"]:
|
||||||
sub["md"] = "\n".join(sub["md"]).strip()
|
sub["md"] = "\n".join(sub["md"]).strip()
|
||||||
sec["subs"] = [sub for sub in sec["subs"] if sub["md"]]
|
sec["subs"] = [sub for sub in sec["subs"] if sub["md"]]
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const provider = ref(localStorage.getItem('provider') || 'claude')
|
|||||||
const providers = ref([])
|
const providers = ref([])
|
||||||
const folders = ref({ projekt: [], uni: [] }) // Ordner für die Quellen-Auswahl
|
const folders = ref({ projekt: [], uni: [] }) // Ordner für die Quellen-Auswahl
|
||||||
const bausteineView = ref(false) // Bausteine-Übersicht im Hauptbereich
|
const bausteineView = ref(false) // Bausteine-Übersicht im Hauptbereich
|
||||||
|
const ansichtModus = ref('kompakt') // kompakt | erklärend — je Thema, Default kompakt
|
||||||
const stats = ref(null)
|
const stats = ref(null)
|
||||||
const fortschritt = ref({})
|
const fortschritt = ref({})
|
||||||
const locks = ref({}) // Sperr-Gründe pro Format (Backend = einzige Regel-Quelle)
|
const locks = ref({}) // Sperr-Gründe pro Format (Backend = einzige Regel-Quelle)
|
||||||
@@ -201,6 +202,7 @@ function selectTopic(topic) {
|
|||||||
elementsView.value = false
|
elementsView.value = false
|
||||||
bausteineView.value = false
|
bausteineView.value = false
|
||||||
elementOpenId.value = null
|
elementOpenId.value = null
|
||||||
|
ansichtModus.value = localStorage.getItem('ansicht_' + topic) || 'kompakt'
|
||||||
localStorage.setItem('lastTopic', topic)
|
localStorage.setItem('lastTopic', topic)
|
||||||
loadBausteine()
|
loadBausteine()
|
||||||
nextTick(autoPreview)
|
nextTick(autoPreview)
|
||||||
@@ -268,6 +270,15 @@ async function handleUpdateQuelle({ topic, type, ort, spec }) {
|
|||||||
await loadBausteine() // Schritt-Anzeige folgt ggf. der neuen Quelle (z.B. Link → „Quelle laden")
|
await loadBausteine() // Schritt-Anzeige folgt ggf. der neuen Quelle (z.B. Link → „Quelle laden")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setAnsicht(modus) {
|
||||||
|
ansichtModus.value = modus
|
||||||
|
if (selectedTopic.value) localStorage.setItem('ansicht_' + selectedTopic.value, modus)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAnsicht() {
|
||||||
|
setAnsicht(ansichtModus.value === 'kompakt' ? 'erklärend' : 'kompakt')
|
||||||
|
}
|
||||||
|
|
||||||
function handleOpenBausteineView() {
|
function handleOpenBausteineView() {
|
||||||
if (!selectedTopic.value) return
|
if (!selectedTopic.value) return
|
||||||
bausteineView.value = true
|
bausteineView.value = true
|
||||||
@@ -384,8 +395,10 @@ onMounted(async () => {
|
|||||||
:provider="provider"
|
:provider="provider"
|
||||||
:providers="providers"
|
:providers="providers"
|
||||||
:folders="folders"
|
:folders="folders"
|
||||||
|
:ansichtModus="ansichtModus"
|
||||||
@setProvider="setProvider"
|
@setProvider="setProvider"
|
||||||
@toggleDark="toggleDark"
|
@toggleDark="toggleDark"
|
||||||
|
@toggleAnsicht="toggleAnsicht"
|
||||||
@select="selectTopic"
|
@select="selectTopic"
|
||||||
@create="createTopic"
|
@create="createTopic"
|
||||||
@createThema="handleCreateThema"
|
@createThema="handleCreateThema"
|
||||||
@@ -424,7 +437,9 @@ onMounted(async () => {
|
|||||||
:elementsOpen="elementsOpen"
|
:elementsOpen="elementsOpen"
|
||||||
:doneByFormat="doneByFormat"
|
:doneByFormat="doneByFormat"
|
||||||
:themaAbgeschlossen="!!fortschritt.abgeschlossen"
|
:themaAbgeschlossen="!!fortschritt.abgeschlossen"
|
||||||
|
:ansichtModus="ansichtModus"
|
||||||
@progressChanged="loadStats(); loadBausteine()"
|
@progressChanged="loadStats(); loadBausteine()"
|
||||||
|
@setAnsicht="setAnsicht"
|
||||||
/>
|
/>
|
||||||
<div v-else class="empty-main">
|
<div v-else class="empty-main">
|
||||||
<p>Thema in der Sidebar anlegen oder auswählen.</p>
|
<p>Thema in der Sidebar anlegen oder auswählen.</p>
|
||||||
|
|||||||
@@ -59,47 +59,31 @@ export async function deleteBausteine(topic) {
|
|||||||
await fetch(`${BASE}/bausteine?topic=${encodeURIComponent(topic)}`, { method: 'DELETE' })
|
await fetch(`${BASE}/bausteine?topic=${encodeURIComponent(topic)}`, { method: 'DELETE' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Baustein-Lernen: Vertiefung, Chat, Prüfung ---
|
// --- Baustein-Lernen: Chat, Prüfung ---
|
||||||
|
|
||||||
export async function fetchBausteinLernstand(topic) {
|
export async function fetchBausteinLernstand(topic) {
|
||||||
const res = await fetch(`${BASE}/bausteine/lernstand?topic=${encodeURIComponent(topic)}`)
|
const res = await fetch(`${BASE}/bausteine/lernstand?topic=${encodeURIComponent(topic)}`)
|
||||||
return jsonOrThrow(res)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchVertiefung(topic, baustein, art = 'vertiefung') {
|
export async function chatBaustein({ topic, baustein, section, section_kompakt = '', messages, provider }) {
|
||||||
const res = await fetch(
|
|
||||||
`${BASE}/bausteine/vertiefung?topic=${encodeURIComponent(topic)}&baustein=${encodeURIComponent(baustein)}&art=${art}`
|
|
||||||
)
|
|
||||||
return jsonOrThrow(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createVertiefung({ topic, baustein, section, art = 'vertiefung', provider }) {
|
|
||||||
const res = await fetch(`${BASE}/bausteine/vertiefung`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ topic, baustein, section, art, provider }),
|
|
||||||
})
|
|
||||||
return jsonOrThrow(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function chatBaustein({ topic, baustein, section, messages, provider }) {
|
|
||||||
const res = await fetch(`${BASE}/bausteine/chat`, {
|
const res = await fetch(`${BASE}/bausteine/chat`, {
|
||||||
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, section_kompakt, messages, provider }),
|
||||||
})
|
})
|
||||||
return jsonOrThrow(res)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pruefeBaustein({
|
export async function pruefeBaustein({
|
||||||
topic, baustein, section, provider,
|
topic, baustein, section, section_kompakt = '', provider,
|
||||||
aktion = 'frage', frage = '', letzte_bewertung = '', score_vor_frage = 0, streak = 0,
|
aktion = 'frage', frage = '', letzte_bewertung = '', score_vor_frage = 0, streak = 0,
|
||||||
tier2 = false, tier3 = false, messages = [], gruendlich = false,
|
tier2 = false, tier3 = false, messages = [], gruendlich = false,
|
||||||
}) {
|
}) {
|
||||||
const res = await fetch(`${BASE}/bausteine/pruefung`, {
|
const res = await fetch(`${BASE}/bausteine/pruefung`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ topic, baustein, section, aktion, frage, letzte_bewertung, score_vor_frage, streak, tier2, tier3, messages, provider, gruendlich }),
|
body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, score_vor_frage, streak, tier2, tier3, messages, provider, gruendlich }),
|
||||||
})
|
})
|
||||||
return jsonOrThrow(res)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const props = defineProps({
|
|||||||
tier2: { type: Boolean, default: false },
|
tier2: { type: Boolean, default: false },
|
||||||
tier3: { type: Boolean, default: false },
|
tier3: { type: Boolean, default: false },
|
||||||
tab: { type: String, default: 'pruefung' },
|
tab: { type: String, default: 'pruefung' },
|
||||||
|
ansicht: { type: String, default: 'kompakt' }, // kompakt | erklärend — linke Guide-Spalte
|
||||||
hasPrev: { type: Boolean, default: false },
|
hasPrev: { type: Boolean, default: false },
|
||||||
hasNext: { type: Boolean, default: false },
|
hasNext: { type: Boolean, default: false },
|
||||||
fortschritt: { type: Object, default: () => ({ total: 0, absolviert: 0, verstanden: 0, gemeistert: 0 }) },
|
fortschritt: { type: Object, default: () => ({ total: 0, absolviert: 0, verstanden: 0, gemeistert: 0 }) },
|
||||||
@@ -17,7 +18,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'prev', 'next', 'statusChanged'])
|
const emit = defineEmits(['close', 'prev', 'next', 'statusChanged', 'setAnsicht'])
|
||||||
|
|
||||||
const guideEl = ref(null) // linke Guide-Spalte (Scroll-Ziel für ALT+↑/↓)
|
const guideEl = ref(null) // linke Guide-Spalte (Scroll-Ziel für ALT+↑/↓)
|
||||||
const rightEl = ref(null) // rechte Spalte (Prüfungs-Panel mit Eingabefeld)
|
const rightEl = ref(null) // rechte Spalte (Prüfungs-Panel mit Eingabefeld)
|
||||||
@@ -88,7 +89,7 @@ const standFarbe = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="fokus-body">
|
<div class="fokus-body">
|
||||||
<div ref="guideEl" class="fokus-col left">
|
<div ref="guideEl" class="fokus-col left">
|
||||||
<div class="markdown" v-html="renderMarkdown(baustein.md)"></div>
|
<div class="markdown" v-html="renderMarkdown(ansicht === 'kompakt' ? (baustein.kompakt || baustein.md) : baustein.md)"></div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="rightEl" class="fokus-col right">
|
<div ref="rightEl" class="fokus-col right">
|
||||||
<BausteinPanel
|
<BausteinPanel
|
||||||
@@ -98,10 +99,13 @@ const standFarbe = computed(() => {
|
|||||||
:topic="topic"
|
:topic="topic"
|
||||||
:baustein="baustein.title"
|
:baustein="baustein.title"
|
||||||
:section="baustein.md"
|
:section="baustein.md"
|
||||||
|
:section-kompakt="baustein.kompakt || ''"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
:status="status"
|
:status="status"
|
||||||
:tier2="tier2"
|
:tier2="tier2"
|
||||||
:tier3="tier3"
|
:tier3="tier3"
|
||||||
|
:ansicht="ansicht"
|
||||||
|
@set-ansicht="$emit('setAnsicht', $event)"
|
||||||
@status-changed="$emit('statusChanged', $event)"
|
@status-changed="$emit('statusChanged', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { chatBaustein, createVertiefung, fetchVertiefung, pruefeBaustein } from '../api.js'
|
import { chatBaustein, pruefeBaustein } from '../api.js'
|
||||||
import { usePruefSlot } from '../pruefungCache.js'
|
import { usePruefSlot } from '../pruefungCache.js'
|
||||||
import { renderMarkdown } from '../markdown.js'
|
import { renderMarkdown } from '../markdown.js'
|
||||||
import { useChat, istUnten } from '../composables/useChat.js'
|
import { useChat, istUnten } from '../composables/useChat.js'
|
||||||
@@ -8,77 +8,38 @@ import { useChat, istUnten } from '../composables/useChat.js'
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
topic: { type: String, required: true },
|
topic: { type: String, required: true },
|
||||||
baustein: { type: String, required: true },
|
baustein: { type: String, required: true },
|
||||||
section: { type: String, default: '' },
|
section: { type: String, default: '' }, // ausführliche Fassung
|
||||||
|
sectionKompakt: { type: String, default: '' }, // kompakte Fassung (Merksätze) — Prüfungs-/Chat-Kontext
|
||||||
provider: { type: String, default: 'claude' },
|
provider: { type: String, default: 'claude' },
|
||||||
status: { type: Object, default: null }, // {gute_antworten, absolviert, verstanden, gemeistert, vertiefung}
|
status: { type: Object, default: null }, // {gute_antworten, streak, absolviert, verstanden, gemeistert}
|
||||||
tier2: { type: Boolean, default: false }, // Tier 2 frei (ganzer Guide absolviert)
|
tier2: { type: Boolean, default: false }, // Tier 2 frei (ganzer Guide absolviert)
|
||||||
tier3: { type: Boolean, default: false }, // Meisterpfad frei (ganzer Guide verstanden)
|
tier3: { type: Boolean, default: false }, // Meisterpfad frei (ganzer Guide verstanden)
|
||||||
// 'trigger' (Liste): nur Tab-Leiste, Klick öffnet den Vollbild-Fokus.
|
// 'trigger' (Liste): nur Tab-Leiste, Klick öffnet den Vollbild-Fokus.
|
||||||
// 'full' (Fokus): Tabs + Panel-Inhalt wie gehabt.
|
// 'full' (Fokus): Tabs + Panel-Inhalt wie gehabt.
|
||||||
mode: { type: String, default: 'trigger' },
|
mode: { type: String, default: 'trigger' },
|
||||||
initialTab: { type: String, default: null },
|
initialTab: { type: String, default: null },
|
||||||
|
ansicht: { type: String, default: 'kompakt' }, // kompakt | erklärend — steuert die linke Guide-Spalte
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['statusChanged', 'openFokus'])
|
const emit = defineEmits(['statusChanged', 'openFokus', 'setAnsicht'])
|
||||||
|
|
||||||
const NOETIG = 3 // absolviert
|
const NOETIG = 3 // absolviert
|
||||||
const MAX = 10 // verstanden
|
const MAX = 10 // verstanden
|
||||||
const MEISTERN = 25 // gemeistert (Maximum)
|
const MEISTERN = 25 // gemeistert (Maximum)
|
||||||
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, absolviert: false, verstanden: false, gemeistert: false, vertiefung: false, deepdive: false })
|
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, absolviert: false, verstanden: false, gemeistert: false })
|
||||||
|
|
||||||
// --- Toggle-Bereich ---
|
// --- Toggle-Bereich ---
|
||||||
const activeTab = ref(props.mode === 'full' ? props.initialTab : null) // null | 'vertiefung' | 'deepdive' | 'chat' | 'pruefung'
|
const activeTab = ref(props.mode === 'full' ? props.initialTab : null) // null | 'chat' | 'pruefung'
|
||||||
|
|
||||||
// Klick auf einen Tab: in der Liste den Fokus öffnen, im Fokus den Tab umschalten.
|
// Klick auf einen Tab: in der Liste den Fokus öffnen, im Fokus den Tab umschalten.
|
||||||
function tabKlick(tab) {
|
function tabKlick(tab) {
|
||||||
if (props.mode === 'trigger') { emit('openFokus', tab); return }
|
if (props.mode === 'trigger') { emit('openFokus', tab); return }
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Vertiefung (gleicher Stoff, mehr) + Amateur (gleicher Stoff, für Einsteiger), beide persistiert (intern art 'deepdive') ---
|
|
||||||
const texte = ref({
|
|
||||||
vertiefung: { md: null, loading: false, error: '' },
|
|
||||||
deepdive: { md: null, loading: false, error: '' },
|
|
||||||
})
|
|
||||||
|
|
||||||
async function openText(art) {
|
|
||||||
const t = texte.value[art]
|
|
||||||
if (t.md !== null || t.loading || !st.value[art]) return
|
|
||||||
t.loading = true
|
|
||||||
t.error = ''
|
|
||||||
try {
|
|
||||||
t.md = (await fetchVertiefung(props.topic, props.baustein, art)).md
|
|
||||||
} catch (e) {
|
|
||||||
t.error = e.message
|
|
||||||
} finally {
|
|
||||||
t.loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateText(art) {
|
|
||||||
const t = texte.value[art]
|
|
||||||
t.loading = true
|
|
||||||
t.error = ''
|
|
||||||
try {
|
|
||||||
t.md = (await createVertiefung({
|
|
||||||
topic: props.topic, baustein: props.baustein, section: props.section, art, provider: props.provider,
|
|
||||||
})).md
|
|
||||||
emit('statusChanged', { ...st.value, [art]: true })
|
|
||||||
} catch (e) {
|
|
||||||
t.error = e.message
|
|
||||||
} finally {
|
|
||||||
t.loading = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Bausteinchat (flüchtig) ---
|
// --- Bausteinchat (flüchtig) ---
|
||||||
const chat = useChat((msgs) => chatBaustein({
|
const chat = useChat((msgs) => chatBaustein({
|
||||||
topic: props.topic, baustein: props.baustein, section: props.section,
|
topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt,
|
||||||
messages: msgs, provider: props.provider,
|
messages: msgs, provider: props.provider,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -131,7 +92,7 @@ async function pruefSenden(payload, onOk) {
|
|||||||
pruefScroll()
|
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, section_kompakt: props.sectionKompakt,
|
||||||
provider: props.provider, messages: pruefDialog(), ...payload,
|
provider: props.provider, messages: pruefDialog(), ...payload,
|
||||||
})
|
})
|
||||||
if (run !== pruefRun) return
|
if (run !== pruefRun) return
|
||||||
@@ -175,7 +136,7 @@ function istBekannt(text) {
|
|||||||
|
|
||||||
async function frageEinzeln() {
|
async function frageEinzeln() {
|
||||||
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, section_kompakt: props.sectionKompakt,
|
||||||
provider: props.provider, aktion: 'frage', messages: pruefDialog(),
|
provider: props.provider, aktion: 'frage', messages: pruefDialog(),
|
||||||
})
|
})
|
||||||
return res.frage
|
return res.frage
|
||||||
@@ -275,7 +236,7 @@ async function verdictPruefen(gruendlich = false) {
|
|||||||
if (gruendlich) pruefLoading.value = true
|
if (gruendlich) pruefLoading.value = true
|
||||||
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, section_kompakt: props.sectionKompakt,
|
||||||
provider: props.provider, messages: pruefDialog(), ...antwortPayload(), aktion: 'antwort_pruefen', gruendlich,
|
provider: props.provider, messages: pruefDialog(), ...antwortPayload(), aktion: 'antwort_pruefen', gruendlich,
|
||||||
})
|
})
|
||||||
if (meine !== pruefenRun) return // eine neuere Bewertung läuft → diese verwerfen
|
if (meine !== pruefenRun) return // eine neuere Bewertung läuft → diese verwerfen
|
||||||
@@ -336,8 +297,6 @@ onMounted(() => {
|
|||||||
if (props.mode !== 'full') return // Trigger-Modus (Liste): nur Tab-Leiste, keine Prüfungs-/Text-Logik
|
if (props.mode !== 'full') return // Trigger-Modus (Liste): nur Tab-Leiste, keine Prüfungs-/Text-Logik
|
||||||
window.addEventListener('keydown', onWindowKey)
|
window.addEventListener('keydown', onWindowKey)
|
||||||
window.addEventListener('keydown', onPruefKey)
|
window.addEventListener('keydown', onPruefKey)
|
||||||
// Direkt im Fokus geöffneter Vertiefung-/Amateur-Tab: Text laden (sonst nur via toggle).
|
|
||||||
if (activeTab.value === 'vertiefung' || activeTab.value === 'deepdive') openText(activeTab.value)
|
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('keydown', onWindowKey)
|
window.removeEventListener('keydown', onWindowKey)
|
||||||
@@ -370,12 +329,11 @@ function onPruefKey(e) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="bp">
|
<div class="bp">
|
||||||
<div class="bp-toggles">
|
<div class="bp-toggles">
|
||||||
<button :class="{ active: activeTab === 'vertiefung' }" @click="tabKlick('vertiefung')">
|
<!-- Ansicht-Umschalter (nur Vollbild): steuert die linke Guide-Spalte, links vor den Tabs -->
|
||||||
Vertiefung
|
<div v-if="mode === 'full'" class="bp-ansicht">
|
||||||
</button>
|
<button :class="{ active: ansicht === 'kompakt' }" title="Merksätze" @click="emit('setAnsicht', 'kompakt')">Kompakt</button>
|
||||||
<button :class="{ active: activeTab === 'deepdive' }" @click="tabKlick('deepdive')">
|
<button :class="{ active: ansicht === 'erklärend' }" title="Ausführliche Erklärung" @click="emit('setAnsicht', 'erklärend')">Erklärend</button>
|
||||||
Amateur
|
</div>
|
||||||
</button>
|
|
||||||
<button :class="{ active: activeTab === 'chat' }" @click="tabKlick('chat')">
|
<button :class="{ active: activeTab === 'chat' }" @click="tabKlick('chat')">
|
||||||
Chat
|
Chat
|
||||||
</button>
|
</button>
|
||||||
@@ -391,24 +349,8 @@ function onPruefKey(e) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="mode === 'full' && activeTab" class="bp-panel">
|
<div v-if="mode === 'full' && activeTab" class="bp-panel">
|
||||||
<!-- Vertiefung (gleicher Stoff, mehr) / Amateur (gleicher Stoff, für Einsteiger) -->
|
|
||||||
<div v-if="activeTab === 'vertiefung' || activeTab === 'deepdive'">
|
|
||||||
<p v-if="texte[activeTab].loading" class="bp-hint">{{ texte[activeTab].md === null ? 'Generiere…' : 'Lade…' }}</p>
|
|
||||||
<template v-else-if="texte[activeTab].md">
|
|
||||||
<div class="markdown" v-html="renderMarkdown(texte[activeTab].md)"></div>
|
|
||||||
<button class="bp-action" @click="generateText(activeTab)">Neu generieren</button>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<p class="bp-hint">{{ activeTab === 'deepdive' ? 'Noch keine Amateur-Fassung zu diesem Baustein.' : 'Noch keine Vertiefung zu diesem Baustein.' }}</p>
|
|
||||||
<button class="bp-action" @click="generateText(activeTab)">
|
|
||||||
{{ activeTab === 'deepdive' ? 'Amateur generieren' : 'Vertiefung generieren' }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<p v-if="texte[activeTab].error" class="bp-error">{{ texte[activeTab].error }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bausteinchat -->
|
<!-- Bausteinchat -->
|
||||||
<div v-else-if="activeTab === 'chat'">
|
<div v-if="activeTab === 'chat'">
|
||||||
<div :ref="chat.messagesEl" class="bp-messages" @scroll="chat.onScroll">
|
<div :ref="chat.messagesEl" class="bp-messages" @scroll="chat.onScroll">
|
||||||
<p v-if="!chat.messages.value.length" class="bp-hint">Frag etwas zu diesem Baustein. Der Verlauf wird nicht gespeichert.</p>
|
<p v-if="!chat.messages.value.length" class="bp-hint">Frag etwas zu diesem Baustein. Der Verlauf wird nicht gespeichert.</p>
|
||||||
<template v-for="(m, i) in chat.messages.value" :key="i">
|
<template v-for="(m, i) in chat.messages.value" :key="i">
|
||||||
@@ -504,6 +446,21 @@ function onPruefKey(e) {
|
|||||||
.bp-toggles button:hover { border-color: var(--border-strong); color: var(--text); }
|
.bp-toggles button:hover { border-color: var(--border-strong); color: var(--text); }
|
||||||
.bp-toggles button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
.bp-toggles button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||||||
|
|
||||||
|
/* Ansicht-Umschalter (Kompakt/Erklärend) — segmentiert, links vor den Tabs */
|
||||||
|
.bp-ansicht { display: inline-flex; margin-right: 0.5rem; }
|
||||||
|
.bp-ansicht button {
|
||||||
|
padding: 0.25rem 0.6rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--panel-soft);
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.bp-ansicht button:first-child { border-radius: 999px 0 0 999px; }
|
||||||
|
.bp-ansicht button:last-child { border-radius: 0 999px 999px 0; border-left: none; }
|
||||||
|
.bp-ansicht button:hover { color: var(--text); }
|
||||||
|
.bp-ansicht button.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||||||
|
|
||||||
.bp-chip {
|
.bp-chip {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
padding: 0 0.35rem;
|
padding: 0 0.35rem;
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Baustein relevant = hat ≥1 relevanten Subbaustein (gleiche Regel wie ProGuide).
|
||||||
|
// Ohne Relevanz-Daten (Alt-Themen) nicht dimmen.
|
||||||
|
function relevant(b) {
|
||||||
|
const mitRelevanz = (b.subbausteine || []).filter((s) => s.relevanz)
|
||||||
|
return !mitRelevanz.length || mitRelevanz.some((s) => s.relevanz === 'relevant')
|
||||||
|
}
|
||||||
|
|
||||||
// Nur nicht-leere Stufen-Gruppen je Baustein (v-if + v-for nicht auf einem Element)
|
// Nur nicht-leere Stufen-Gruppen je Baustein (v-if + v-for nicht auf einem Element)
|
||||||
function gruppen(b) {
|
function gruppen(b) {
|
||||||
return STUFEN
|
return STUFEN
|
||||||
@@ -58,7 +65,13 @@ const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subbausteine
|
|||||||
<div v-else-if="!items.length" class="bk-empty-state">Noch keine Bausteine.</div>
|
<div v-else-if="!items.length" class="bk-empty-state">Noch keine Bausteine.</div>
|
||||||
|
|
||||||
<div v-else class="bk-grid">
|
<div v-else class="bk-grid">
|
||||||
<article v-for="b in items" :key="b.num" class="bk-card">
|
<article
|
||||||
|
v-for="b in items"
|
||||||
|
:key="b.num"
|
||||||
|
class="bk-card"
|
||||||
|
:class="{ 'bk-irrelevant': !relevant(b) }"
|
||||||
|
:title="relevant(b) ? null : 'Nicht relevant — nur im FullGuide'"
|
||||||
|
>
|
||||||
<h3 class="bk-title"><span class="bk-num">{{ b.num }}</span>{{ b.titel }}</h3>
|
<h3 class="bk-title"><span class="bk-num">{{ b.num }}</span>{{ b.titel }}</h3>
|
||||||
<p v-if="b.beschreibung" class="bk-desc">{{ b.beschreibung }}</p>
|
<p v-if="b.beschreibung" class="bk-desc">{{ b.beschreibung }}</p>
|
||||||
<div v-if="b.subbausteine && b.subbausteine.length" class="bk-stufen">
|
<div v-if="b.subbausteine && b.subbausteine.length" class="bk-stufen">
|
||||||
@@ -136,6 +149,8 @@ const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subbausteine
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 1rem 1.1rem;
|
padding: 1rem 1.1rem;
|
||||||
}
|
}
|
||||||
|
/* Nicht relevante Bausteine (kein relevanter Subbaustein) gedimmt */
|
||||||
|
.bk-card.bk-irrelevant { opacity: 0.5; }
|
||||||
|
|
||||||
.bk-title {
|
.bk-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -182,18 +197,17 @@ const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subbausteine
|
|||||||
color: var(--st);
|
color: var(--st);
|
||||||
}
|
}
|
||||||
.bk-stufe ul {
|
.bk-stufe ul {
|
||||||
list-style: none;
|
list-style: disc;
|
||||||
margin: 0.25rem 0 0;
|
margin: 0.25rem 0 0;
|
||||||
padding: 0;
|
padding-left: 1.15rem;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
}
|
||||||
.bk-stufe li {
|
.bk-stufe li {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
.bk-stufe li::marker { color: var(--text-faint); }
|
||||||
.bk-stufe li.rand { color: var(--text-faint); }
|
.bk-stufe li.rand { color: var(--text-faint); }
|
||||||
.rand-tag {
|
.rand-tag {
|
||||||
margin-left: 0.4rem;
|
margin-left: 0.4rem;
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ const props = defineProps({
|
|||||||
elementsOpen: { type: Boolean, default: false }, // Element-Sidebar offen → Chat nach links
|
elementsOpen: { type: Boolean, default: false }, // Element-Sidebar offen → Chat nach links
|
||||||
doneByFormat: { type: Object, default: () => ({}) }, // Format → fertiger Guide (Themen-bezogen)
|
doneByFormat: { type: Object, default: () => ({}) }, // Format → fertiger Guide (Themen-bezogen)
|
||||||
themaAbgeschlossen: { type: Boolean, default: false },
|
themaAbgeschlossen: { type: Boolean, default: false },
|
||||||
|
ansichtModus: { type: String, default: 'kompakt' }, // kompakt | erklärend
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['progressChanged'])
|
const emit = defineEmits(['progressChanged', 'setAnsicht'])
|
||||||
|
|
||||||
const isOnePager = computed(() => props.previewGuide?.format === 'OnePager')
|
const isOnePager = computed(() => props.previewGuide?.format === 'OnePager')
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ const CH_COLORS = ['#3b82f6', '#8b5cf6', '#14b8a6', '#f59e0b', '#22c55e', '#6366
|
|||||||
const content = ref(null)
|
const content = ref(null)
|
||||||
const loadError = ref(null)
|
const loadError = ref(null)
|
||||||
const scrollEl = ref(null)
|
const scrollEl = ref(null)
|
||||||
|
const lernstand = ref({}) // Prüfungs-Stand pro Baustein-Titel — VOR dem immediate-Watch (loadContent nutzt es)
|
||||||
|
|
||||||
watch(() => props.previewGuide?.id, loadContent, { immediate: true })
|
watch(() => props.previewGuide?.id, loadContent, { immediate: true })
|
||||||
|
|
||||||
@@ -49,9 +51,7 @@ async function loadContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Baustein-Lernen: Prüfungs-Stand pro Baustein-Titel ---
|
// --- Baustein-Lernen: Prüfungs-Stand pro Baustein-Titel (lernstand oben deklariert) ---
|
||||||
const lernstand = ref({})
|
|
||||||
|
|
||||||
function onBausteinStatus(baustein, status) {
|
function onBausteinStatus(baustein, status) {
|
||||||
const warAbsolviert = lernstand.value[baustein]?.absolviert
|
const warAbsolviert = lernstand.value[baustein]?.absolviert
|
||||||
lernstand.value = { ...lernstand.value, [baustein]: status }
|
lernstand.value = { ...lernstand.value, [baustein]: status }
|
||||||
@@ -199,7 +199,7 @@ function extractContext() {
|
|||||||
<span v-else-if="lernstand[s.title]?.verstanden" class="baustein-done verstanden" title="Vollständig verstanden (10/10)">✓✓ Verstanden</span>
|
<span v-else-if="lernstand[s.title]?.verstanden" class="baustein-done verstanden" title="Vollständig verstanden (10/10)">✓✓ Verstanden</span>
|
||||||
<span v-else-if="lernstand[s.title]?.absolviert" class="baustein-done" title="Prüfung bestanden">✓ Absolviert</span>
|
<span v-else-if="lernstand[s.title]?.absolviert" class="baustein-done" title="Prüfung bestanden">✓ Absolviert</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="section-body markdown" v-html="renderMarkdown(s.md)"></div>
|
<div class="section-body markdown" v-html="renderMarkdown(!isOnePager && ansichtModus === 'kompakt' ? (s.kompakt || s.md) : s.md)"></div>
|
||||||
<BausteinPanel
|
<BausteinPanel
|
||||||
v-if="!isOnePager"
|
v-if="!isOnePager"
|
||||||
mode="trigger"
|
mode="trigger"
|
||||||
@@ -234,11 +234,13 @@ function extractContext() {
|
|||||||
:tier2="tier2"
|
:tier2="tier2"
|
||||||
:tier3="tier3"
|
:tier3="tier3"
|
||||||
:tab="fokusTab"
|
:tab="fokusTab"
|
||||||
|
:ansicht="ansichtModus"
|
||||||
:has-prev="fokusIndex > 0"
|
:has-prev="fokusIndex > 0"
|
||||||
:has-next="fokusIndex < bausteine.length - 1"
|
:has-next="fokusIndex < bausteine.length - 1"
|
||||||
@prev="fokusPrev"
|
@prev="fokusPrev"
|
||||||
@next="fokusNext"
|
@next="fokusNext"
|
||||||
@close="fokusIndex = null"
|
@close="fokusIndex = null"
|
||||||
|
@set-ansicht="$emit('setAnsicht', $event)"
|
||||||
@status-changed="(st) => onBausteinStatus(st.baustein, st)"
|
@status-changed="(st) => onBausteinStatus(st.baustein, st)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ const props = defineProps({
|
|||||||
provider: { type: String, default: 'claude' },
|
provider: { type: String, default: 'claude' },
|
||||||
providers: { type: Array, default: () => [] },
|
providers: { type: Array, default: () => [] },
|
||||||
folders: { type: Object, default: () => ({ projekt: [], uni: [] }) },
|
folders: { type: Object, default: () => ({ projekt: [], uni: [] }) },
|
||||||
|
ansichtModus: { type: String, default: 'kompakt' }, // kompakt | erklärend
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['select', 'create', 'createThema', 'updateQuelle', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'dismissError', 'dismissUiError', 'preview', 'openBausteineView', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
|
const emit = defineEmits(['select', 'create', 'createThema', 'updateQuelle', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'dismissError', 'dismissUiError', 'preview', 'openBausteineView', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'toggleAnsicht', 'setProvider'])
|
||||||
|
|
||||||
// Accordion: höchstens ein Panel offen. IDs: 'bausteine', 'fmt-<Format>', 'topic-<Name>'.
|
// Accordion: höchstens ein Panel offen. IDs: 'bausteine', 'fmt-<Format>', 'topic-<Name>'.
|
||||||
const openPanel = ref(null)
|
const openPanel = ref(null)
|
||||||
@@ -282,6 +283,13 @@ function saveQuelle() {
|
|||||||
:title="dark ? 'Hellmodus' : 'Dunkelmodus'"
|
:title="dark ? 'Hellmodus' : 'Dunkelmodus'"
|
||||||
@click="emit('toggleDark')"
|
@click="emit('toggleDark')"
|
||||||
>{{ dark ? '☀' : '🌙' }}</button>
|
>{{ dark ? '☀' : '🌙' }}</button>
|
||||||
|
<button
|
||||||
|
v-if="selectedTopic"
|
||||||
|
class="theme-btn ansicht-btn"
|
||||||
|
:class="{ erkl: ansichtModus === 'erklärend' }"
|
||||||
|
:title="ansichtModus === 'kompakt' ? 'Ansicht: kompakt — klicken für erklärend' : 'Ansicht: erklärend — klicken für kompakt'"
|
||||||
|
@click="emit('toggleAnsicht')"
|
||||||
|
>{{ ansichtModus === 'kompakt' ? 'K' : 'E' }}</button>
|
||||||
<button class="new-topic-toggle" :class="{ active: dlg }" title="Thema erstellen" @click="toggleErstellen">+</button>
|
<button class="new-topic-toggle" :class="{ active: dlg }" title="Thema erstellen" @click="toggleErstellen">+</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -555,6 +563,9 @@ function saveQuelle() {
|
|||||||
border-color: var(--accent-border);
|
border-color: var(--accent-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-topic .ansicht-btn { font-weight: 700; }
|
||||||
|
.new-topic .ansicht-btn.erkl { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
|
||||||
|
|
||||||
.stats-bar {
|
.stats-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ GEPRÜFTE FRAGE:
|
|||||||
BAUSTEIN AUS DEM GUIDE:
|
BAUSTEIN AUS DEM GUIDE:
|
||||||
{section_block}
|
{section_block}
|
||||||
|
|
||||||
VERTIEFUNG (falls vorhanden):
|
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
||||||
{vertiefung_block}
|
{kompakt_block}
|
||||||
|
|
||||||
PRÜFUNGS-VERLAUF (Antwort des Lerners und etwaige Diskussion):
|
PRÜFUNGS-VERLAUF (Antwort des Lerners und etwaige Diskussion):
|
||||||
{transcript}
|
{transcript}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ GEPRÜFTE FRAGE:
|
|||||||
BAUSTEIN AUS DEM GUIDE:
|
BAUSTEIN AUS DEM GUIDE:
|
||||||
{section_block}
|
{section_block}
|
||||||
|
|
||||||
VERTIEFUNG (falls vorhanden):
|
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
||||||
{vertiefung_block}
|
{kompakt_block}
|
||||||
|
|
||||||
STAND: {gute_antworten} von {noetig} Antworten waren bisher gut.
|
STAND: {gute_antworten} von {noetig} Antworten waren bisher gut.
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ Du bist ein hilfreicher Tutor für den Baustein "{baustein}" aus dem Lern-Guide
|
|||||||
BAUSTEIN AUS DEM GUIDE:
|
BAUSTEIN AUS DEM GUIDE:
|
||||||
{section_block}
|
{section_block}
|
||||||
|
|
||||||
VERTIEFUNG (falls vorhanden):
|
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
||||||
{vertiefung_block}
|
{kompakt_block}
|
||||||
|
|
||||||
BISHERIGER CHAT-VERLAUF:
|
BISHERIGER CHAT-VERLAUF:
|
||||||
{transcript}
|
{transcript}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
Schreibe eine Einsteiger-Fassung („Amateur") des Bausteins "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Gleicher Stoff wie im Guide — aber für absolute Anfänger aufbereitet: maximal verständlich, kleinschrittig, nichts vorausgesetzt.
|
|
||||||
|
|
||||||
GUIDE-FASSUNG DES BAUSTEINS:
|
|
||||||
{section_block}
|
|
||||||
|
|
||||||
So baust du es auf:
|
|
||||||
- Genau dasselbe Konzept wie der Guide — nur viel ausführlicher und einfacher erklärt.
|
|
||||||
- Sehr kleinschrittig: Gedanke für Gedanke, nichts überspringen.
|
|
||||||
- Viele Alltags-Analogien und Bilder, die das Konzept greifbar machen.
|
|
||||||
- Jeden Fachbegriff beim ersten Auftreten ausführlich erklären, in Alltagssprache.
|
|
||||||
- Beispiele langsam durchgehen — Schritt für Schritt zeigen, was passiert.
|
|
||||||
|
|
||||||
Was NICHT:
|
|
||||||
- NICHT schwerer oder tiefer. Keine Grenzfälle, keine Abgrenzung, kein Experten-Stoff.
|
|
||||||
- Du bringst mehr ERKLÄRUNG, nicht mehr Konzepte.
|
|
||||||
- Setze nichts voraus — auch nicht, dass der Leser die Guide-Fassung verstanden hat.
|
|
||||||
|
|
||||||
Stil:
|
|
||||||
- Klares, geduldiges Deutsch. Du nimmst den Leser an die Hand.
|
|
||||||
- Ausführlicher als die Guide-Fassung, aber kein Roman. Die Länge folgt dem Verständnis, nicht dem Wortzähler.
|
|
||||||
- Mathematik IMMER als LaTeX: inline `$…$` (z. B. `$\Sigma^*$`, `$O(n^d)$`), abgesetzt `$$…$$`. KEINE Unicode-Krücken (`x₁`, `¬`, `≤`).
|
|
||||||
- Markdown: Absätze, `inline-code`, Codeblöcke mit Sprachangabe nur für Code, `###` für Zwischenüberschriften, **fett** sparsam. KEINE Zusammenfassung am Ende, kein Fazit-Abschnitt.
|
|
||||||
|
|
||||||
Gib NUR das Markdown der Einsteiger-Fassung aus — keine Einleitung wie "Hier ist…", kein Code-Fence um das Ganze, kein JSON.
|
|
||||||
@@ -3,8 +3,8 @@ Du bist Qualitäts-Prüfer für Prüfungsfragen in einem Lern-Guide zum Thema "{
|
|||||||
BAUSTEIN AUS DEM GUIDE:
|
BAUSTEIN AUS DEM GUIDE:
|
||||||
{section_block}
|
{section_block}
|
||||||
|
|
||||||
VERTIEFUNG (falls vorhanden):
|
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
||||||
{vertiefung_block}
|
{kompakt_block}
|
||||||
|
|
||||||
BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten):
|
BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten):
|
||||||
{transcript}
|
{transcript}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ Du bist Prüfer in einem Lern-Guide zum Thema "{topic}". Stelle dem Lerner EINE
|
|||||||
BAUSTEIN AUS DEM GUIDE:
|
BAUSTEIN AUS DEM GUIDE:
|
||||||
{section_block}
|
{section_block}
|
||||||
|
|
||||||
VERTIEFUNG (falls vorhanden):
|
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
||||||
{vertiefung_block}
|
{kompakt_block}
|
||||||
|
|
||||||
BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten):
|
BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten):
|
||||||
{transcript}
|
{transcript}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ DEINE LETZTE BEWERTUNG (falls vorhanden):
|
|||||||
BAUSTEIN AUS DEM GUIDE:
|
BAUSTEIN AUS DEM GUIDE:
|
||||||
{section_block}
|
{section_block}
|
||||||
|
|
||||||
VERTIEFUNG (falls vorhanden):
|
KOMPAKTE FASSUNG (Merksätze, falls vorhanden):
|
||||||
{vertiefung_block}
|
{kompakt_block}
|
||||||
|
|
||||||
BISHERIGER VERLAUF:
|
BISHERIGER VERLAUF:
|
||||||
{transcript}
|
{transcript}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
Schreibe eine ausführlichere Fassung des Bausteins "{baustein}" aus dem Lern-Guide zum Thema "{topic}". Gleicher Stoff wie im Guide — nur umfangreicher. Keine Erweiterung, nur MEHR.
|
|
||||||
|
|
||||||
GUIDE-FASSUNG DES BAUSTEINS:
|
|
||||||
{section_block}
|
|
||||||
|
|
||||||
Was „mehr" heißt:
|
|
||||||
- Genau dasselbe Konzept, dieselbe Stufe, derselbe Leser (Anfänger ohne Vorwissen).
|
|
||||||
- Jeden Punkt der Guide-Fassung etwas weiter ausführen, langsameres Tempo.
|
|
||||||
- Mehr Beispiele und zusätzliche Varianten zum SELBEN Konzept.
|
|
||||||
|
|
||||||
Was NICHT:
|
|
||||||
- KEINE neuen Konzepte, keine Grenzfälle, keine Abgrenzung zu Nachbarthemen.
|
|
||||||
- Nichts „über den Guide hinaus" — du gehst nicht tiefer, du gehst breiter.
|
|
||||||
- Kein Aufblähen mit Floskeln. Mehr Text nur, wo er echtes Verständnis bringt.
|
|
||||||
|
|
||||||
Stil:
|
|
||||||
- Klares, direktes Deutsch. Merklich ausführlicher als die Guide-Fassung, aber kein Roman.
|
|
||||||
- Fachbegriffe beim ersten Auftreten in einem Halbsatz auflösen.
|
|
||||||
- Mathematik IMMER als LaTeX: inline `$…$` (z. B. `$\Sigma^*$`, `$O(n^d)$`), abgesetzt `$$…$$`. KEINE Unicode-Krücken (`x₁`, `¬`, `≤`).
|
|
||||||
- Markdown: Absätze, `inline-code`, Codeblöcke mit Sprachangabe nur für Code. KEINE Zwischenüberschriften, KEINE Zusammenfassung, kein Fazit.
|
|
||||||
|
|
||||||
Gib NUR das Markdown der Vertiefung aus — keine Einleitung wie "Hier ist…", kein Code-Fence um das Ganze, kein JSON.
|
|
||||||
@@ -10,9 +10,14 @@ ZU ÜBERARBEITEN — je Section der aktuelle Inhalt und das notierte Problem:
|
|||||||
|
|
||||||
Behebe pro Section NUR das notierte Problem; was in Ordnung ist, bleibt inhaltlich erhalten.
|
Behebe pro Section NUR das notierte Problem; was in Ordnung ist, bleibt inhaltlich erhalten.
|
||||||
|
|
||||||
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:
|
Jede Section hat ZWEI Fassungen mit denselben Inhalten: **kompakt** (je Subbaustein ein Merksatz als Stichpunkt) und **ausführlich** (zusammenhängender Anfänger-Text). Gib beide neu aus und halte sie deckungsgleich.
|
||||||
|
|
||||||
|
Schreibe NUR die Datei {out_path} in GENAU diesem Format — für JEDE beanstandete Section ein section-Marker (Titel EXAKT wie oben) mit `kompakt`- und `ausführlich`-Block:
|
||||||
|
|
||||||
<!-- section: Exakter Section-Titel -->
|
<!-- section: Exakter Section-Titel -->
|
||||||
|
<!-- kompakt -->
|
||||||
|
- Merksatz je Subbaustein (knapp, ohne Erklärung)
|
||||||
|
<!-- ausführlich -->
|
||||||
Erklärung (Einordnung → Wie/Warum) laut SECTION-SPEZIFIKATION…
|
Erklärung (Einordnung → Wie/Warum) laut SECTION-SPEZIFIKATION…
|
||||||
|
|
||||||
### Beispiel
|
### Beispiel
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jede zugeteil
|
|||||||
|
|
||||||
Das Format ({format_name}) beeinflusst NICHT, wie ein Baustein geschrieben wird — nur, wie viele Bausteine das Thema abdecken. Derselbe Baustein bekommt in MiniGuide, Guide und FullGuide denselben Text.
|
Das Format ({format_name}) beeinflusst NICHT, wie ein Baustein geschrieben wird — nur, wie viele Bausteine das Thema abdecken. Derselbe Baustein bekommt in MiniGuide, Guide und FullGuide denselben Text.
|
||||||
|
|
||||||
SO SCHREIBST DU einen Baustein — EIN zusammenhängender Text, kein Stichwort-Stakkato:
|
Jeder Baustein bekommt ZWEI Fassungen mit DENSELBEN Inhalten (dieselben Subbausteine):
|
||||||
|
- **kompakt**: je Subbaustein EIN Merksatz als Stichpunkt (`- …`). Zum Erinnern. Nur benennen, nicht erklären. Gleiche Reihenfolge wie ausführlich.
|
||||||
|
- **ausführlich**: der zusammenhängende Anfänger-Lerntext (siehe unten).
|
||||||
|
Beide MÜSSEN dieselben Subbausteine abdecken. Nichts, das nur in einer Fassung steht.
|
||||||
|
|
||||||
|
SO SCHREIBST DU die ausführliche Fassung — EIN zusammenhängender Text, kein Stichwort-Stakkato:
|
||||||
- Schreibe pro Baustein EINEN fließenden Lern-Text laut SECTION-SPEZIFIKATION (Einordnung → Erklärung → Beispiel). Ein roter Faden, mit Übergängen.
|
- Schreibe pro Baustein EINEN fließenden Lern-Text laut SECTION-SPEZIFIKATION (Einordnung → Erklärung → Beispiel). Ein roter Faden, mit Übergängen.
|
||||||
- Die Subbausteine sind deine INHALTS-CHECKLISTE, keine Überschriften: webe sie in den Fließtext ein. Behandle jeden, aber als Teil eines Ganzen — nicht als isolierten Mini-Absatz.
|
- Die Subbausteine sind deine INHALTS-CHECKLISTE, keine Überschriften: webe sie in den Fließtext ein. Behandle jeden, aber als Teil eines Ganzen — nicht als isolierten Mini-Absatz.
|
||||||
- Reihenfolge: erst die `[einfach]`-Punkte (das Fundament), dann `[mittel]` (Details/Varianten), zuletzt `[schwer]` (Sonderfälle/Tücken — knapp, als fortgeschrittener Hinweis).
|
- Reihenfolge: erst die `[einfach]`-Punkte (das Fundament), dann `[mittel]` (Details/Varianten), zuletzt `[schwer]` (Sonderfälle/Tücken — knapp, als fortgeschrittener Hinweis).
|
||||||
@@ -23,14 +28,17 @@ Nutze diese Inhalte. Erfinde nichts dazu, lass nichts Wesentliches weg. Lehre si
|
|||||||
SECTION-SPEZIFIKATION (gilt je Baustein):
|
SECTION-SPEZIFIKATION (gilt je Baustein):
|
||||||
{spec}
|
{spec}
|
||||||
|
|
||||||
Schreibe NUR die Datei {out_path} in GENAU diesem Format — pro Kapitel ein kapitel-Marker, pro Baustein ein section-Marker (Titel EXAKT aus der Zuteilung), darunter der zusammenhängende Markdown-Body:
|
Schreibe NUR die Datei {out_path} in GENAU diesem Format — pro Kapitel ein kapitel-Marker, pro Baustein ein section-Marker (Titel EXAKT aus der Zuteilung), darin ein `kompakt`- und ein `ausführlich`-Block:
|
||||||
|
|
||||||
<!-- kapitel: Kapiteltitel -->
|
<!-- kapitel: Kapiteltitel -->
|
||||||
<!-- section: Exakter Baustein-Titel -->
|
<!-- section: Exakter Baustein-Titel -->
|
||||||
|
<!-- kompakt -->
|
||||||
|
- Merksatz je Subbaustein (knapp, ohne Erklärung)
|
||||||
|
<!-- ausführlich -->
|
||||||
Erklärung (Einordnung → Wie/Warum) laut SECTION-SPEZIFIKATION…
|
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)
|
||||||
|
|
||||||
Die Marker-Zeilen exakt so schreiben. Kein Text außerhalb der Sections, kein Dokument-Titel, kein Inhaltsverzeichnis.
|
Die Marker-Zeilen exakt so schreiben (jede Section hat genau einen `<!-- kompakt -->`- und einen `<!-- ausführlich -->`-Block). Kein Text außerhalb der Sections, kein Dokument-Titel, kein Inhaltsverzeichnis.
|
||||||
{extra}
|
{extra}
|
||||||
|
|||||||
Reference in New Issue
Block a user