From a9b2f5ab9e7ea850f9364f90bb8c31ca6ad3f221 Mon Sep 17 00:00:00 2001 From: team3 Date: Sun, 12 Jul 2026 21:21:41 +0200 Subject: [PATCH] update --- DESIGN.md | 12 +- Dockerfile | 5 +- backend/artefakte.py | 25 +-- backend/config.py | 8 +- backend/db.py | 8 +- backend/diagramme.py | 264 ------------------------------ backend/fake_agents.py | 21 +-- backend/guide.py | 87 ++++------ backend/main.py | 4 - backend/mermaid_check.mjs | 37 ----- backend/pipeline.py | 20 +-- backend/qa.py | 6 +- frontend/package.json | 2 - frontend/src/App.vue | 9 +- frontend/src/components/Board.vue | 5 - frontend/src/components/Guide.vue | 27 --- frontend/src/markdown.js | 2 +- frontend/src/style.css | 4 +- templates/Artefakt-Fix.md | 1 - templates/Artefakt-Generate.md | 3 +- templates/Diagramm-Plan.md | 22 --- templates/Diagramm-Spec.md | 20 --- templates/Diagramm-Verify.md | 21 --- templates/Guide-Writer.md | 4 +- tests/test_artefakte.py | 19 +-- tests/test_bausteine.py | 38 ++++- tests/test_diagramme.py | 136 --------------- tests/test_e2e.py | 2 +- 28 files changed, 106 insertions(+), 706 deletions(-) delete mode 100644 backend/diagramme.py delete mode 100644 backend/mermaid_check.mjs delete mode 100644 templates/Diagramm-Plan.md delete mode 100644 templates/Diagramm-Spec.md delete mode 100644 templates/Diagramm-Verify.md delete mode 100644 tests/test_diagramme.py diff --git a/DESIGN.md b/DESIGN.md index b95136c..3c32870 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -20,17 +20,14 @@ E1 Inventar Abschnitte (~12k Zeichen) → 2 Reader je Abschnitt → Atome mit Dedup: Anker-Overlap (auto) → Embedding-Kandidaten → 2er-Panel (einstimmig) Soll-Zuordnung je Atom; Soll-Punkt ohne Atom → gezielte Nachextraktion E2 Artefakte je Soll-Punkt-Gruppe: Flashcards + 1 typisiertes Beispiel pro Atom - (Form: text|mathe|code|tabelle|diagramm, beste Passung) → Form-Gate + (Form: text|mathe|code|tabelle, beste Passung) → Form-Gate (Syntax/Struktur parse-only, kein Ausführen) → Verify-Panel gegen Anker → Fix E3 Struktur Lernziele+Zuordnung aus Atomen (je Soll-Punkt) → Ziel-Check → Kanten auflösen → Zyklen brechen (Judge) → Bausteine = Ziel-Gruppen im Band (split/merge) → topologische Ordnung (Bausteine + Atome darin) -E3.5 Diagramme je Baustein: Judge plant welche+Typ (state/flow/tree — Term-DAGs waren - nutzlos, raus) → Spec-JSON (LLM, kein rohes Mermaid) → deterministisch → - Mermaid → Parse-Gate → Grounding-Panel. Optional. E4 Guide je Baustein: Writer (Facts inline, Atom-Marker; Ausgabe DELIMITED, nicht JSON — LaTeX-Backslashes) → det. Checks → Prüfer → Fix (Fix-Cap gegen Churn). - Beispiel-/Diagramm-Marker setzt der Code DETERMINISTISCH (nicht der Writer); + Beispiel-Marker setzt der Code DETERMINISTISCH (nicht der Writer); Assembly tauscht Marker gegen ```-Fence/Tabelle. ``` @@ -48,11 +45,10 @@ Stillstand oder 10 Iterationen. Erst dann startet die nächste Ebene. - `anker(atom_id, quelle_id, start, ende, zitat)` - `kanten(topic, von_atom, zu_atom, art braucht|verwandt, status)` - `artefakte(id, atom_id, typ flashcard|beispiel, inhalt JSON, status kandidat|verifiziert|verworfen)` - — beispiel-`inhalt` trägt `form` + formspezifische Felder (text|code+sprache|tabelle|spec+mermaid); - code/tabelle/diagramm kommen verbatim via `` in den Guide (Assembly-Swap) + — beispiel-`inhalt` trägt `form` + formspezifische Felder (text|code+sprache|tabelle); + code/tabelle kommen verbatim via `` in den Guide (Assembly-Swap) - `lernziele(id, topic, text, soll_id, status)` - `bausteine(id, topic, ziel_id, titel, ord, status)` -- `diagramme(id, topic, baustein_id, typ, quelle dag|llm, spec JSON, mermaid, status geplant|kandidat|verifiziert|verworfen|kein, befunde JSON)` - `sections(baustein_id, stage, text_kompakt, text_lang, befunde JSON)` - `leitner(artefakt_id, box, faellig, historie)` - `befunde(run_id, ebene, art, item, detail, status offen|repariert|freigesprochen)` diff --git a/Dockerfile b/Dockerfile index 8a3d3ba..f8e1df2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,9 +29,8 @@ RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/wh && pip install --no-cache-dir -r /app/backend/requirements.txt COPY --chown=app:app backend/ /app/backend/ -# katex + mermaid (+ jsdom fürs headless-DOM, das Mermaid zum Parsen braucht) fürs -# Parse-Gate (backend/{katex,mermaid}_check.mjs prüfen mit dem echten Renderer) -RUN cd /app/backend && npm install --no-audit --no-fund katex mermaid jsdom && chown -R app:app node_modules +# katex fürs Parse-Gate (backend/katex_check.mjs prüft mit dem echten Renderer) +RUN cd /app/backend && npm install --no-audit --no-fund katex && chown -R app:app node_modules COPY --chown=app:app templates/ /app/templates/ COPY --chown=app:app --from=frontend /build/dist /app/frontend/dist COPY --chown=app:app dev-ops/opencode.json /home/app/.config/opencode/opencode.json diff --git a/backend/artefakte.py b/backend/artefakte.py index e7dbe08..697dd9a 100644 --- a/backend/artefakte.py +++ b/backend/artefakte.py @@ -11,7 +11,6 @@ import subprocess import tempfile import db -import diagramme import llm from config import ARTEFAKT_CHUNK_ATOME, BEISPIEL_FORMEN, VERIFY_PANEL @@ -30,8 +29,8 @@ _QUELLEN_REFERENZ = re.compile( def _prosa_teile(inhalt: dict) -> str: - """Nur menschenlesbare Prosa fürs Quellen-Referenz-Gate — NICHT Code/Mermaid/ - Tabelle (dort matchen Bezeichner/Kommentare den Regex falsch, z. B. „blatt 3").""" + """Nur menschenlesbare Prosa fürs Quellen-Referenz-Gate — NICHT Code/Tabelle + (dort matchen Bezeichner/Kommentare den Regex falsch, z. B. „blatt 3").""" return " ".join(str(inhalt.get(k, "")) for k in ("frage", "antwort", "text")) @@ -80,9 +79,7 @@ def _beispiel_inhalt(e: dict) -> dict: "text": str(e.get("text", "")), "code": str(e.get("code", "")), "sprache": str(e.get("sprache", "")).strip().lower(), - "tabelle": str(e.get("tabelle", "")), - "spec": e.get("spec") if isinstance(e.get("spec"), dict) else {}, - "mermaid": ""} + "tabelle": str(e.get("tabelle", ""))} async def _generieren(ctx: llm.Kontext, chunk: list[dict]) -> None: @@ -188,7 +185,7 @@ async def _fixen(ctx: llm.Kontext, k: dict, maengel: list[str], atom: dict) -> N status="verifiziert" if eintrag.get("ok") else "verworfen") -_VERBATIM_FORMEN = ("code", "tabelle", "diagramm") # kommen verbatim in den Guide +_VERBATIM_FORMEN = ("code", "tabelle") # kommen verbatim in den Guide def _tabelle_ok(md: str) -> bool: @@ -242,24 +239,12 @@ def _code_ok(sprache: str, code: str) -> bool: def _beispiel_pruefen(inh: dict) -> tuple[bool, dict]: """Form-spezifisches Gate (deterministisch, parse-only — nie Ausführung). - → (ok, inhalt); der Diagramm-Zweig füllt `mermaid`. Unbekannte/woven Formen - passieren (der Judge prüft inhaltlich).""" + → (ok, inhalt). Unbekannte/woven Formen passieren (der Judge prüft inhaltlich).""" form = inh.get("form", "text") if form == "tabelle": return _tabelle_ok(inh.get("tabelle", "")), inh if form == "code": return _code_ok(inh.get("sprache", ""), inh.get("code", "")), inh - if form == "diagramm": - # Struktur-JSON → Mermaid (deterministisch) → Parse-Gate; wiederverwendet die - # Diagramm-Ebene. Der LLM schreibt NIE rohes Mermaid — nur die Spec. - spec = diagramme._spec_normalisieren(inh.get("spec") or {}, - (inh.get("spec") or {}).get("typ", "flow")) - if not spec["knoten"]: - return False, inh - mermaid = diagramme._spec_zu_mermaid(spec) - if not mermaid or 0 in diagramme._mermaid_fehler([{"id": 0, "mermaid": mermaid}]): - return False, inh - return True, {**inh, "spec": spec, "mermaid": mermaid} return True, inh diff --git a/backend/config.py b/backend/config.py index 6e8c904..bdd3fcf 100644 --- a/backend/config.py +++ b/backend/config.py @@ -89,7 +89,7 @@ LUECKEN_RUNDEN_MAX = 2 # gezielte Nachextraktion pro Soll-Lücke BRAUCHT_KANDIDATEN = 8 # Kandidaten-Cap je unaufgelöstem braucht-Titel (Lektion 34) # ── Ebene 2: Artefakte ──────────────────────────────────────────────────────── -BEISPIEL_FORMEN = ("text", "mathe", "code", "tabelle", "diagramm") # Worked-Example-Formen +BEISPIEL_FORMEN = ("text", "mathe", "code", "tabelle") # Worked-Example-Formen CODE_SPRACHEN = ("python", "javascript") # Syntax-Gate parse-only (Container: python3, node) ARTEFAKT_CHUNK_ATOME = 8 # Atome pro Generate-Call (ein Soll-Punkt, gestückelt) VERIFY_PANEL = 2 # unabhängige Prüfer, einstimmig = verifiziert @@ -107,9 +107,6 @@ BAUSTEIN_MAX_ATOME = 8 ZIELE_CHUNK_ATOME = 30 # Atome pro Lernziel-Call KAPITEL_BAUSTEINE = 5 # Richtwert Bausteine je Kapitel (Kapitel-Schnitt-Judge) -# ── Ebene 3.5: Diagramme (nur LLM: Automat/Ablauf/Baum — Term-DAGs waren nutzlos) ── -DIAGRAMM_TYPEN = ("state", "flow", "tree") - # ── Ebene 4: Guide ──────────────────────────────────────────────────────────── SECTION_WOERTER_PRO_ATOM = (40, 220) # Längenband ausführlicher Teil je Atom @@ -137,9 +134,6 @@ TIMEOUTS = { "writer": (450, 60), "pruefer": (600, 5), "qa_judge": (600, 0), - "diagramm_plan": (200, 0), - "diagramm_spec": (300, 0), - "diagramm_verify": (200, 5), } diff --git a/backend/db.py b/backend/db.py index 16b0079..ada28a4 100644 --- a/backend/db.py +++ b/backend/db.py @@ -96,12 +96,6 @@ CREATE TABLE IF NOT EXISTS kapitel( id INTEGER PRIMARY KEY, topic TEXT NOT NULL, titel TEXT NOT NULL, intro TEXT NOT NULL DEFAULT '', ord INTEGER DEFAULT 0, art TEXT NOT NULL DEFAULT 'judge', level TEXT NOT NULL DEFAULT 'M'); -CREATE TABLE IF NOT EXISTS diagramme( - id INTEGER PRIMARY KEY, topic TEXT NOT NULL, baustein_id INTEGER NOT NULL, - typ TEXT NOT NULL, quelle TEXT NOT NULL DEFAULT 'dag', - spec TEXT NOT NULL DEFAULT '{}', mermaid TEXT NOT NULL DEFAULT '', - status TEXT NOT NULL DEFAULT 'geplant', befunde TEXT NOT NULL DEFAULT '[]'); -CREATE INDEX IF NOT EXISTS idx_diagramme_baustein ON diagramme(baustein_id); CREATE TABLE IF NOT EXISTS sections( baustein_id INTEGER PRIMARY KEY, stage TEXT NOT NULL DEFAULT 'writer', text_kompakt TEXT DEFAULT '', text_lang TEXT DEFAULT '', befunde TEXT DEFAULT '[]', @@ -116,7 +110,7 @@ CREATE TABLE IF NOT EXISTS befunde( # Tabellen, deren Änderungen das Live-Board interessieren. _LIVE_TABELLEN = {"topics", "runs", "quellen", "soll", "atome", "artefakte", - "lernziele", "bausteine", "kapitel", "sections", "befunde", "diagramme"} + "lernziele", "bausteine", "kapitel", "sections", "befunde"} def _init_schema(con: sqlite3.Connection) -> None: diff --git a/backend/diagramme.py b/backend/diagramme.py deleted file mode 100644 index a67adf5..0000000 --- a/backend/diagramme.py +++ /dev/null @@ -1,264 +0,0 @@ -"""Ebene 3.5: Diagramme. Pro Baustein ein Mermaid-Diagramm, wenn es Struktur oder -Prozess sichtbar macht (Multimedia-Prinzip: instruktive Bilder heben Transfer, -Deko schadet). Zuverlässigkeits-Prinzip: der LLM liefert NIE rohes Mermaid, nur -Struktur-JSON; Python kompiliert deterministisch; ein Parse-Gate (mermaid_check.mjs) -ist das Sicherheitsnetz. Abhängigkeitsdiagramme kommen direkt aus dem braucht-DAG, -ganz ohne LLM — korrekt per Konstruktion, null Halluzination. - -Phasen von bauen(): planen (welche + Typ) → Spec (nur LLM-Zweig) → kompilieren → -Gate → verifizieren (Grounding). Platziert werden Diagramme erst im Guide-Writer -(E4) über ``; das Assembly tauscht den Marker gegen den Fence.""" - -import json -import logging -import os -import re -import subprocess -from pathlib import Path - -import db -import llm -from config import DIAGRAMM_TYPEN, DURCHGANG, VERIFY_PANEL - -log = logging.getLogger("creator2.diagramme") - -EBENE = "diagramme" - -_MERMAID_SKRIPT = Path(__file__).resolve().parent / "mermaid_check.mjs" -_MERMAID_BASE = Path(__file__).resolve().parent.parent / "frontend" -_mermaid_gewarnt = False - - -def _mermaid_fehler(diagramme: list[dict]) -> dict[int, str]: - """Parse-Gate: Diagramme durch DENSELBEN Parser wie das Frontend jagen (Zwilling - zu guide._katex_fehler). → {id: fehlermeldung} nur der fehlerhaften. Fake-Modus - überspringt (deterministisch); fail-open MIT Warnung, wenn node/mermaid fehlt.""" - global _mermaid_gewarnt - if not diagramme or os.getenv("CREATOR_FAKE_AGENTS"): - return {} - try: - res = subprocess.run( - ["node", str(_MERMAID_SKRIPT)], - input=json.dumps([{"id": d["id"], "code": d["mermaid"]} for d in diagramme]), - capture_output=True, text=True, timeout=60, - env={**os.environ, "MERMAID_BASE": str(_MERMAID_BASE)}) - if res.returncode: - raise RuntimeError(res.stderr.strip()[:200]) - return {e["id"]: e["fehler"] for e in json.loads(res.stdout)} - except Exception as e: # nie fail-open OHNE Signal (Befund-Prinzip) - if not _mermaid_gewarnt: - _mermaid_gewarnt = True - log.warning("Mermaid-Gate nicht verfügbar (%s) — Diagramm-Validierung fällt aus", e) - return {} - - -def _bausteine(topic: str) -> list[dict]: - return db.query("SELECT * FROM bausteine WHERE topic=? ORDER BY ord", (topic,)) - - -def _atome_von(baustein_id: int) -> list[dict]: - return db.query("SELECT * FROM atome WHERE baustein_id=? AND status NOT IN" - " ('gemerged','verworfen') ORDER BY ord", (baustein_id,)) - - -# ── Kompilieren (deterministisch, kein LLM) ─────────────────────────────────── - -def _sid(raw) -> str: - """Mermaid-Knoten-ID: nur alnum/_ erlaubt, führender Buchstabe.""" - return "n" + re.sub(r"[^A-Za-z0-9]", "_", str(raw)) - - -def _lbl(text) -> str: - """Label für Mermaid entschärfen: keine Anführungszeichen/Klammern/Zeilenumbrüche, - die die Syntax brechen. Gekürzt (Lesbarkeit).""" - t = str(text).replace('"', "'").replace("\n", " ") - t = t.replace("[", "(").replace("]", ")").replace("{", "(").replace("}", ")") - return t.strip()[:80] or "?" - - -def _spec_zu_mermaid(spec: dict) -> str: - """Struktur-JSON → Mermaid-Quelltext. state → stateDiagram-v2, sonst flowchart.""" - knoten = spec.get("knoten") or [] - kanten = spec.get("kanten") or [] - if not knoten: - return "" - if spec.get("typ") == "state": - zeilen = ["stateDiagram-v2"] - for k in kanten: - lab = _lbl(k.get("label", "")) - pfeil = f": {lab}" if lab and lab != "?" else "" - zeilen.append(f" {_sid(k['von'])} --> {_sid(k['zu'])}{pfeil}") - return "\n".join(zeilen) - zeilen = ["flowchart TD"] - for n in knoten: - zeilen.append(f' {_sid(n["id"])}["{_lbl(n["label"])}"]') - for k in kanten: - lab = _lbl(k.get("label", "")) - pfeil = f"-- {lab} -->" if lab and lab != "?" else "-->" - zeilen.append(f' {_sid(k["von"])} {pfeil} {_sid(k["zu"])}') - return "\n".join(zeilen) - - -def _kompilieren(topic: str) -> None: - for d in db.query("SELECT * FROM diagramme WHERE topic=? AND status='geplant'" - " AND spec!='{}'", (topic,)): - mermaid = _spec_zu_mermaid(db.uj(d["spec"], {})) - if mermaid: - db.update("diagramme", "id", d["id"], mermaid=mermaid, status="kandidat") - - -# ── Planung (welche + Typ) ──────────────────────────────────────────────────── - -def _atom_liste(atome: list[dict]) -> str: - return "\n".join(f"{a['id']}: {a['titel']} — {a['definition']}" for a in atome) - - -async def _plan_judge(ctx: llm.Kontext) -> None: - """LLM-Zweig, Schritt „welche + Typ": je Baustein OHNE Diagramm-Zeile entscheidet - ein Judge, ob ein Automaten-/Ablauf-/Baum-Diagramm nötig ist (Multimedia-Regel - im Prompt). „noetig" → status='geplant'/quelle='llm'; sonst eine 'kein'-Zeile, - damit der Judge nicht jede Repair-Iteration erneut fragt (idempotent).""" - alle = _bausteine(ctx.topic) - je_kap: dict = {} - for b in alle: - je_kap.setdefault(b["kapitel_id"], []).append(b["titel"]) - - async def einer(b: dict) -> None: - if db.one("SELECT id FROM diagramme WHERE baustein_id=?", (b["id"],)): - return - atome = _atome_von(b["id"]) - if not atome: - return - nachbarn = "\n".join(f"- {t}" for t in je_kap.get(b["kapitel_id"], []) - if t != b["titel"]) or "(keine)" - res = await llm.call(ctx, stage="diagramm_plan", template="Diagramm-Plan", - role="judge", n=len(atome), item=f"dp{b['id']}", - werte={"titel": b["titel"], "atome": _atom_liste(atome), - "nachbarn": nachbarn, - "durchgang": DURCHGANG.get(b["level"], DURCHGANG["M"])}, - erwartet=dict) - if res is None: - return # Ausfall → nächste Runde erneut (kein 'kein'-Eintrag) - typ = str(res.get("typ", "")).strip() - if res.get("noetig") and typ in DIAGRAMM_TYPEN: - db.insert("diagramme", topic=ctx.topic, baustein_id=b["id"], typ=typ, - quelle="llm", status="geplant") - else: - db.insert("diagramme", topic=ctx.topic, baustein_id=b["id"], typ="", - quelle="llm", status="kein") # kein Diagramm — nicht erneut fragen - - await llm.alle(einer(b) for b in alle) - - -def _spec_normalisieren(res: dict, typ: str) -> dict: - """LLM-Spec säubern: nur Knoten mit id+label, nur Kanten zwischen echten Knoten.""" - knoten = [{"id": str(n.get("id", "")).strip(), "label": str(n.get("label", "")).strip()} - for n in (res.get("knoten") or []) if isinstance(n, dict) and n.get("id")] - gueltig = {n["id"] for n in knoten} - kanten = [{"von": str(k.get("von", "")).strip(), "zu": str(k.get("zu", "")).strip(), - "label": str(k.get("label", "")).strip()} - for k in (res.get("kanten") or []) - if isinstance(k, dict) and str(k.get("von")) in gueltig - and str(k.get("zu")) in gueltig] - return {"typ": typ, "knoten": knoten, "kanten": kanten} - - -async def _spec_bauen(ctx: llm.Kontext) -> None: - """LLM liefert NUR Struktur-JSON (kein Mermaid) — die Hauptfehlerquelle - (roh geschriebenes Mermaid) fällt so weg. Kompiliert wird deterministisch.""" - offen = db.query("SELECT * FROM diagramme WHERE topic=? AND status='geplant'" - " AND quelle='llm' AND spec='{}'", (ctx.topic,)) - - async def einer(d: dict) -> None: - b = db.one("SELECT * FROM bausteine WHERE id=?", (d["baustein_id"],)) - atome = _atome_von(d["baustein_id"]) - res = await llm.call(ctx, stage="diagramm_spec", template="Diagramm-Spec", - role="guide", n=len(atome), item=f"ds{d['id']}", - werte={"titel": b["titel"] if b else "", "typ": d["typ"], - "atome": _atom_liste(atome)}, erwartet=dict) - if res is None: - return # Ausfall → bleibt 'geplant', nächste Runde erneut - spec = _spec_normalisieren(res, d["typ"]) - if spec["knoten"]: - db.update("diagramme", "id", d["id"], spec=db.j(spec)) - - await llm.alle(einer(d) for d in offen) - - -async def _verifizieren(ctx: llm.Kontext) -> None: - """Grounding-Panel für LLM-Diagramme: jeder Knoten muss aus einem Atom ableitbar - sein, keine erfundene Struktur. Verwerfen (destruktiv) nur einstimmig - (llm.einstimmig) — Panel-Ausfall lässt den Kandidaten stehen.""" - offen = db.query("SELECT * FROM diagramme WHERE topic=? AND status='kandidat'" - " AND quelle='llm'", (ctx.topic,)) - - def urteil(stimme): - if stimme and isinstance(stimme[0], dict) and "ok" in stimme[0]: - return bool(stimme[0]["ok"]) - return None - - async def einer(d: dict) -> None: - atome = _atome_von(d["baustein_id"]) - stimmen = await llm.panel(ctx, VERIFY_PANEL, stage="diagramm_verify", - template="Diagramm-Verify", schritt="diagramm_verify", - role="judge", n=len(atome), item=f"dv{d['id']}", - werte={"atome": _atom_liste(atome), "mermaid": d["mermaid"]}, - erwartet=list) - if llm.einstimmig(stimmen, VERIFY_PANEL, urteil): - db.update("diagramme", "id", d["id"], status="verifiziert") - elif llm.einstimmig(stimmen, VERIFY_PANEL, - lambda s: None if urteil(s) is None else not urteil(s)): - db.update("diagramme", "id", d["id"], status="verworfen", - befunde=db.j(["Grounding-Panel: nicht aus den Atomen ableitbar"])) - # sonst: Kandidat bleibt (Panel-Ausfall / uneins) — nächste Runde erneut - - await llm.alle(einer(d) for d in offen) - - -# ── Ebenen-Vertrag ──────────────────────────────────────────────────────────── - -async def bauen(ctx: llm.Kontext) -> None: - ctx.ebene = EBENE - await _plan_judge(ctx) # welche/Typ: Judge (Automaten/Abläufe/Bäume) — kein DAG mehr - await _spec_bauen(ctx) # LLM → Struktur-JSON (kein rohes Mermaid) - _kompilieren(ctx.topic) # Spec → Mermaid (deterministisch) - _gate_kandidaten(ctx.topic) # Parse-Gate: ungültige Mermaid-Quelle → verworfen - await _verifizieren(ctx) # Grounding-Panel gegen die Atome - - -def _gate_kandidaten(topic: str) -> None: - """Kandidaten durchs Parse-Gate: Syntaxfehler → verworfen (mit Befund). Ein - kaputtes Diagramm darf nie in den Guide (fail-closed).""" - kandidaten = db.query("SELECT * FROM diagramme WHERE topic=? AND status='kandidat'" - " AND mermaid!=''", (topic,)) - fehler = _mermaid_fehler(kandidaten) - for d in kandidaten: - if d["id"] in fehler: - db.update("diagramme", "id", d["id"], status="verworfen", - befunde=db.j([f"Parse-Fehler: {fehler[d['id']]}"])) - - -def messen(ctx: llm.Kontext) -> list[dict]: - befunde = [] - for d in db.query("SELECT * FROM diagramme WHERE topic=? AND status='kandidat'", - (ctx.topic,)): - # hängengebliebener Kandidat (Spec/Verify offen) — sichtbar, kein stiller Verlust - befunde.append({"art": "diagramm_unverifiziert", "item": str(d["id"]), - "detail": f"Baustein {d['baustein_id']}"}) - return befunde - - -async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool: - ctx.ebene = EBENE - if not befunde: - return False - await bauen(ctx) - return True - - -def gate(ctx: llm.Kontext) -> str | None: - """Diagramme sind optional — KEINE Diagramme blockiert den Lauf nicht. Backstop: - ein verifiziertes Diagramm ohne Mermaid-Quelltext darf nie ausgeliefert werden.""" - tot = db.one("SELECT id FROM diagramme WHERE topic=? AND status='verifiziert'" - " AND mermaid='' LIMIT 1", (ctx.topic,)) - return "verifiziertes Diagramm ohne Quelltext" if tot else None diff --git a/backend/fake_agents.py b/backend/fake_agents.py index b9f4c84..4eb3002 100644 --- a/backend/fake_agents.py +++ b/backend/fake_agents.py @@ -237,7 +237,7 @@ def _writer(prompt: str): absaetze = [f"Ziel dieser Einheit: {ziel.group(1) if ziel else 'Lernen'}. Formal: $n \\ge 0$."] fuellung = "Daran anknüpfend gehen wir das Schritt für Schritt am Beispiel durch. " * 5 for (mk, titel), d in zip(marker, defs): - # Beispiel-/Diagramm-Marker setzt jetzt _marker_platzieren deterministisch, + # Beispiel-Marker setzt jetzt _marker_platzieren deterministisch, # nicht mehr der Writer. absaetze.append(f"#### {titel} verstehen\n{mk}\n{d} {fuellung}") lang = "\n\n".join(absaetze) @@ -247,23 +247,6 @@ def _writer(prompt: str): "lang": lang} -def _diagramm_plan(prompt: str): - # deterministisch: die Fake-Welt braucht kein Diagramm. Unit-Tests für den - # LLM-Diagramm-Zweig monkeypatchen diesen Handler auf noetig=True. - return {"noetig": False, "typ": ""} - - -def _diagramm_spec(prompt: str): - ids = [i for i in re.findall(r"^(\d+):", _text_nach(prompt, "ATOME:"), re.MULTILINE)] - knoten = [{"id": i, "label": f"Konzept {i}"} for i in ids] - kanten = [{"von": ids[k], "zu": ids[k + 1], "label": ""} for k in range(len(ids) - 1)] - return {"typ": "flow", "knoten": knoten, "kanten": kanten} - - -def _diagramm_verify(prompt: str): - return [{"ok": True, "mangel": ""}] - - def _pruefer(prompt: str): return {"befunde": []} @@ -298,6 +281,4 @@ _HANDLER = { "Kanten-Zyklus": _zyklus, "Braucht-Aufloesung": _braucht_aufloesung, "Guide-Writer": _writer, "Guide-Pruefer": _pruefer, "Guide-Fix": _fix, "QA-Guide-Falsch": _qa_falsch, "QA-Guide-Falsch-Check": _qa_falsch_check, - "Diagramm-Plan": _diagramm_plan, "Diagramm-Spec": _diagramm_spec, - "Diagramm-Verify": _diagramm_verify, } diff --git a/backend/guide.py b/backend/guide.py index 8a21496..10488a5 100644 --- a/backend/guide.py +++ b/backend/guide.py @@ -55,16 +55,10 @@ _MARKER = re.compile(r"") -_MARKER_DIAGRAMM = re.compile(r"") _MARKER_BEISPIEL = re.compile(r"") -# Kurzbeschreibung je Diagramm-Typ (Writer-Angebot) -_DIAGRAMM_BESCHR = {"dependency": "Abhängigkeitsgraph der Begriffe dieses Bausteins", - "state": "Zustandsdiagramm/Automat", "flow": "Ablaufdiagramm", - "tree": "Baumstruktur"} -_BEISPIEL_BESCHR = {"code": "Code-Beispiel", "tabelle": "Tabelle/Vergleich", - "diagramm": "Diagramm-Beispiel"} -_VERBATIM_FORMEN = ("code", "tabelle", "diagramm") +_BEISPIEL_BESCHR = {"code": "Code-Beispiel", "tabelle": "Tabelle/Vergleich"} +_VERBATIM_FORMEN = ("code", "tabelle") def _ohne_marker(text: str) -> str: @@ -72,12 +66,12 @@ def _ohne_marker(text: str) -> str: ALLE Text-Checks (Stil, Mathe, Länge, Vorwärtsverweise) laufen auf dem gestrippten Text — sonst erzeugen Atom-TITEL wie „a_n" oder „Satz 6.26" unfixierbare Aufträge (der Fix darf Marker nie ändern → Endlos-Loop). - Diagramm- und Beispiel-Marker gehören ebenso raus.""" - return _MARKER_BEISPIEL.sub("", _MARKER_DIAGRAMM.sub("", _MARKER_VOLL.sub("", text))) + Beispiel-Marker gehören ebenso raus.""" + return _MARKER_BEISPIEL.sub("", _MARKER_VOLL.sub("", text)) def _beispiel_verbatim(atom_id: int) -> list[tuple[int, dict]]: - """Verifizierte Beispiele des Atoms in VERBATIM-Form (code/tabelle/diagramm) — + """Verifizierte Beispiele des Atoms in VERBATIM-Form (code/tabelle) — (artefakt_id, inhalt). Woven-Formen (text/mathe) gehen über `_beispiel`.""" out = [] for r in db.query("SELECT id, inhalt FROM artefakte WHERE atom_id=? AND typ='beispiel'" @@ -89,23 +83,20 @@ def _beispiel_verbatim(atom_id: int) -> list[tuple[int, dict]]: def _beispiel_render(inh: dict) -> str: - """Verbatim-Beispiel → Markdown (Code-Fence / Tabelle / Mermaid-Fence).""" + """Verbatim-Beispiel → Markdown (Code-Fence / Tabelle).""" form = inh.get("form") if form == "code" and inh.get("code"): return f"```{inh.get('sprache', '')}\n{inh['code']}\n```" if form == "tabelle" and inh.get("tabelle"): return inh["tabelle"] - if form == "diagramm" and inh.get("mermaid"): - return f"```mermaid\n{inh['mermaid']}\n```" return "" def _marker_platzieren(b: dict, lang: str) -> str: """Verbatim-Marker DETERMINISTISCH setzen (der Writer tut es nicht mehr — er ignorierte die Regel, 2/13 platziert). Beispiel-Marker hinter den Atom-Absatz - (atom-genau via Atom-Marker); Diagramme (baustein-scoped) ans Section-Ende. - Idempotent: alte Verbatim-Marker erst raus.""" - lang = _MARKER_BEISPIEL.sub("", _MARKER_DIAGRAMM.sub("", lang)) + (atom-genau via Atom-Marker). Idempotent: alte Verbatim-Marker erst raus.""" + lang = _MARKER_BEISPIEL.sub("", lang) je_atom = {} for a in _atome_von(b["id"]): vs = _beispiel_verbatim(a["id"]) @@ -124,10 +115,6 @@ def _marker_platzieren(b: dict, lang: str) -> str: if pending is not None: out += ["", f""] lang = "\n".join(out) - diags = _diagramme_von(b["id"]) - if diags: - block = "\n".join(f"" for d in diags) - lang = lang.rstrip() + "\n\n" + block return lang @@ -145,24 +132,6 @@ def _beispiel_einsetzen(baustein_id: int, lang: str) -> str: return _MARKER_BEISPIEL.sub(sub, lang) -def _diagramme_von(baustein_id: int) -> list[dict]: - return db.query("SELECT * FROM diagramme WHERE baustein_id=? AND status='verifiziert'" - " ORDER BY id", (baustein_id,)) - - -def _diagramm_einsetzen(baustein_id: int, lang: str) -> str: - """Assembly: `` → ```mermaid-Fence des verifizierten - Diagramms. Tote/unverifizierte Marker fallen ersatzlos weg.""" - if "`-Marker rein.""" + (code/tabelle) kommen über den ``-Marker rein.""" row = db.one("SELECT inhalt FROM artefakte WHERE atom_id=? AND typ='beispiel'" " AND status='verifiziert' LIMIT 1", (atom_id,)) if not row: @@ -233,23 +202,35 @@ def _anknuepf_kontext(b: dict) -> str: # ── Stages ──────────────────────────────────────────────────────────────────── +def _writer_saeubern(s: str) -> str: + """Geleakte Writer-Delimiter/Modelltokens entfernen. Der Writer beendet seine + Ausgabe mit `=====`; steht diese Zeile unter einem Bullet, ist sie eine + Setext-H1-Unterstreichung → marked macht daraus ein riesiges

. Reine + `===…`-Zeilen und ``/``-Tokens fallen daher ganz raus.""" + s = re.sub(r"", "", s or "") + s = re.sub(r"(?m)^\s*={3,}\s*$", "", s) + return s.strip() + + def _split_writer(text: str) -> tuple[str, str]: """Delimited-Ausgabe des Writers/Fix parsen: ===LANG===… / ===KOMPAKT===…. Grund: rohe LaTeX-Backslashes im Text sind invalides JSON (34 % Parse-Fehler). Robust: Blöcke unabhängig von der Reihenfolge; fehlt der Delimiter, jsonx-Fallback - (alt-JSON), sonst ganzer Text = lang.""" + (alt-JSON), sonst ganzer Text = lang. Beide Blöcke werden von geleakten + Terminatoren (`=====`/``) gesäubert.""" def block(name: str) -> str | None: m = re.search(rf"===\s*{name}\s*===\s*(.*?)(?=\n===\s*[A-ZÄÖÜ]+\s*===|$)", text or "", re.S) - return m.group(1).strip() if m else None + return _writer_saeubern(m.group(1)) if m else None lang, kompakt = block("LANG"), block("KOMPAKT") if lang is not None or kompakt is not None: return kompakt or "", lang or "" daten = jsonx.parse(text or "") # Fallback: altes JSON-Format if isinstance(daten, dict): - return str(daten.get("kompakt", "")), str(daten.get("lang", "")) - return "", (text or "").strip() + return (_writer_saeubern(str(daten.get("kompakt", ""))), + _writer_saeubern(str(daten.get("lang", "")))) + return "", _writer_saeubern(text or "") async def _stage_writer(ctx: llm.Kontext, b: dict) -> str: @@ -275,7 +256,7 @@ async def _stage_writer(ctx: llm.Kontext, b: dict) -> str: k2, l2 = _split_writer(res2) if not _marker_fehlend(l2, atome): kompakt, lang = k2, l2 - lang = _marker_platzieren(b, lang) # Beispiel-/Diagramm-Marker deterministisch setzen + lang = _marker_platzieren(b, lang) # Beispiel-Marker deterministisch setzen db.update("sections", "baustein_id", b["id"], text_kompakt=kompakt, text_lang=lang) return "pruefer" @@ -604,15 +585,17 @@ def kapitel_struktur(topic: str) -> list[dict]: "titel": k.get("titel", "Weitere Themen"), "intro": k.get("intro", ""), "level": k.get("level", b["level"]), "sections": []}) - # Diagramm- und Beispiel-Marker gegen ihre Fences/Tabellen tauschen (Assembly); - # Atom-Marker bleiben — die strippt erst das Frontend. + # Beispiel-Marker gegen ihre Fences/Tabellen tauschen (Assembly); + # Atom-Marker bleiben — die strippt erst das Frontend. _writer_saeubern hier + # AUCH beim Lesen: bestehende Sections mit geleaktem `=====`/`` (Setext-H1 + # → riesiger Bullet) werden ohne Neu-Generierung sauber ausgeliefert. lang = "" if sec: - lang = _beispiel_einsetzen(b["id"], _diagramm_einsetzen(b["id"], sec["text_lang"])) + lang = _beispiel_einsetzen(b["id"], _writer_saeubern(sec["text_lang"])) kapitel[-1]["sections"].append({ "baustein": b["id"], "titel": b["titel"], "ziel": ziele.get(b["ziel_id"], {}).get("text", ""), - "kompakt": sec["text_kompakt"] if sec else "", + "kompakt": _writer_saeubern(sec["text_kompakt"]) if sec else "", "lang": lang}) # NULL-kapitel_id durch eindeutige synthetische Keys ersetzen — mehrere # kapitellose Gruppen kollidierten sonst im Vue-:key (null == null). @@ -658,12 +641,6 @@ def messen(ctx: llm.Kontext) -> list[dict]: for i in _marker_fremd_ids(lang, atome): befunde.append({"art": "marker_fremd", "item": str(b["id"]), "detail": f"Atom {i}"}) - gueltige_d = {d["id"] for d in _diagramme_von(b["id"])} - for m in _MARKER_DIAGRAMM.findall(lang): - if int(m) not in gueltige_d: # Marker ohne verifiziertes Diagramm → entfernen - befunde.append({"art": "diagramm_marker_tot", "item": str(b["id"]), - "detail": f"Stray Diagramm-Marker " - f" entfernen (kein solches verifiziertes Diagramm)"}) gueltige_b = {aid for a in atome for aid, _ in _beispiel_verbatim(a["id"])} for m in _MARKER_BEISPIEL.findall(lang): if int(m) not in gueltige_b: # Marker ohne verifiziertes Verbatim-Beispiel diff --git a/backend/main.py b/backend/main.py index 810cbaf..f397ef9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -153,10 +153,6 @@ def state(topic: str): "bausteine": db.query( "SELECT b.*, s.stage FROM bausteine b LEFT JOIN sections s ON s.baustein_id=b.id" " WHERE b.topic=? ORDER BY b.ord", (topic,)), - "diagramme": db.query( - "SELECT d.id, d.baustein_id, d.typ, d.quelle, d.status, b.titel, b.ord" - " FROM diagramme d JOIN bausteine b ON b.id=d.baustein_id" - " WHERE d.topic=? AND d.status!='kein' ORDER BY b.ord, d.id", (topic,)), # topic-scoped: offene Befunde früherer Läufe bleiben nach Resume sichtbar "befunde": db.query( "SELECT b.* FROM befunde b JOIN runs r ON r.id=b.run_id" diff --git a/backend/mermaid_check.mjs b/backend/mermaid_check.mjs deleted file mode 100644 index 1705826..0000000 --- a/backend/mermaid_check.mjs +++ /dev/null @@ -1,37 +0,0 @@ -// Mermaid-Parse-Gate: prüft Diagramm-Quelltext mit DEMSELBEN Parser wie das Frontend. -// stdin: JSON [{id, code}] → stdout: JSON [{id, fehler}, …] (nur die fehlerhaften). -// Zwilling zu katex_check.mjs: was hier parst, rendert auch im Guide. -// -// Mermaid braucht zum Sanitisieren der Labels ein DOM — headless via jsdom, sonst -// wirft parse() für JEDES Diagramm mit Label „DOMPurify.addHook is not a function" -// (nicht ein Diagramm-Fehler, sondern ein Umgebungsfehler → alles fälschlich verworfen). -import { createRequire } from 'node:module' -import { readFileSync } from 'node:fs' - -function laden(name) { - try { - return createRequire(import.meta.url)(name) // Container: backend/node_modules - } catch { - const basis = process.env.MERMAID_BASE // Dev: frontend/node_modules - return createRequire(basis.endsWith('/') ? basis : basis + '/')(name) - } -} - -const { JSDOM } = laden('jsdom') -const dom = new JSDOM('') -globalThis.window = dom.window -globalThis.document = dom.window.document - -const mermaid = laden('mermaid').default -mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' }) - -const diagramme = JSON.parse(readFileSync(0, 'utf8')) -const fehler = [] -for (const d of diagramme) { - try { - await mermaid.parse(d.code) // wirft bei Syntaxfehler - } catch (e) { - fehler.push({ id: d.id, fehler: String(e.message || e).slice(0, 160) }) - } -} -process.stdout.write(JSON.stringify(fehler)) diff --git a/backend/pipeline.py b/backend/pipeline.py index bbe5bfd..b3f8931 100644 --- a/backend/pipeline.py +++ b/backend/pipeline.py @@ -10,7 +10,6 @@ import agents import artefakte import auto_loop import db -import diagramme import guide import inventar import korpus @@ -27,11 +26,10 @@ EBENEN = [ ("inventar", inventar, "inventar_fertig"), ("artefakte", artefakte, "artefakte_fertig"), ("struktur", struktur, "struktur_fertig"), - ("diagramme", diagramme, "diagramme_fertig"), ("guide", guide, "fertig"), ] _ORDNUNG = ["neu", "korpus", "korpus_fertig", "inventar_fertig", - "artefakte_fertig", "struktur_fertig", "diagramme_fertig", "fertig"] + "artefakte_fertig", "struktur_fertig", "fertig"] _laeufe: dict[str, asyncio.Task] = {} @@ -65,7 +63,7 @@ def lauf_stoppen(topic: str) -> None: db.update("runs", "id", run["id"], status="stopped", beendet=db.now()) -_ROLLBACK = {"guide": "diagramme_fertig", "diagramme": "struktur_fertig", +_ROLLBACK = {"guide": "struktur_fertig", "struktur": "artefakte_fertig", "artefakte": "inventar_fertig", "inventar": "korpus_fertig", "korpus": "neu"} @@ -77,25 +75,23 @@ def ebenen_entfernen(topic: str, ebene: str) -> None: if ebene not in _ROLLBACK: raise ValueError(f"unbekannte Ebene: {ebene}") lauf_stoppen(topic) - stufe = ["guide", "diagramme", "struktur", "artefakte", "inventar", "korpus"].index(ebene) + stufe = ["guide", "struktur", "artefakte", "inventar", "korpus"].index(ebene) db.execute("DELETE FROM sections WHERE baustein_id IN" " (SELECT id FROM bausteine WHERE topic=?)", (topic,)) db.execute("UPDATE bausteine SET status='neu' WHERE topic=?", (topic,)) db.execute("UPDATE kapitel SET intro='' WHERE topic=?", (topic,)) # Intro ist Guide-Text - if stufe >= 1: # diagramme - db.execute("DELETE FROM diagramme WHERE topic=?", (topic,)) - if stufe >= 2: # struktur + if stufe >= 1: # struktur db.execute("DELETE FROM bausteine WHERE topic=?", (topic,)) db.execute("DELETE FROM kapitel WHERE topic=?", (topic,)) db.execute("DELETE FROM lernziele WHERE topic=?", (topic,)) db.execute("UPDATE atome SET ziel_id=NULL, baustein_id=NULL, ord=0 WHERE topic=?", (topic,)) - if stufe >= 3: # artefakte + if stufe >= 2: # artefakte db.execute("DELETE FROM leitner WHERE artefakt_id IN (SELECT ar.id FROM artefakte ar" " JOIN atome a ON ar.atom_id=a.id WHERE a.topic=?)", (topic,)) db.execute("DELETE FROM artefakte WHERE atom_id IN" " (SELECT id FROM atome WHERE topic=?)", (topic,)) - if stufe >= 4: # inventar + if stufe >= 3: # inventar db.execute("DELETE FROM anker WHERE atom_id IN (SELECT id FROM atome WHERE topic=?)", (topic,)) db.execute("DELETE FROM kanten WHERE topic=?", (topic,)) @@ -103,7 +99,7 @@ def ebenen_entfernen(topic: str, ebene: str) -> None: # atome_stand mit-nullen — sonst überspringt der Resume-Guard die Neu-Extraktion db.execute("UPDATE quellen SET status='extrahiert', atome_stand='' WHERE topic=?", (topic,)) - if stufe >= 5: # korpus + if stufe >= 4: # korpus db.execute("DELETE FROM soll WHERE topic=?", (topic,)) db.execute("DELETE FROM quellen WHERE topic=?", (topic,)) import shutil @@ -118,7 +114,7 @@ def ebenen_entfernen(topic: str, ebene: str) -> None: # append-only), aber die Zählung beginnt hinter dem Reset-Marker neu. letzte = db.one("SELECT MAX(id) AS m FROM events")["m"] or 0 resets = db.uj(db.one("SELECT resets FROM topics WHERE name=?", (topic,))["resets"], {}) - for name in ["guide", "diagramme", "struktur", "artefakte", "inventar", "korpus"][:stufe + 1]: + for name in ["guide", "struktur", "artefakte", "inventar", "korpus"][:stufe + 1]: resets[name] = letzte db.update("topics", "name", topic, status=_ROLLBACK[ebene], resets=db.j(resets)) diff --git a/backend/qa.py b/backend/qa.py index b485db1..e7c7ffe 100644 --- a/backend/qa.py +++ b/backend/qa.py @@ -9,7 +9,6 @@ import logging import artefakte import db -import diagramme import guide import inventar import korpus @@ -19,7 +18,7 @@ import struktur log = logging.getLogger("creator2.qa") MODULE = {"korpus": korpus, "inventar": inventar, "artefakte": artefakte, - "struktur": struktur, "diagramme": diagramme, "guide": guide} + "struktur": struktur, "guide": guide} # Gewichte je Befund-Art: kritisch 3.0 (verletzt Kernprinzip), mittel 1.5, stil 0.5. GEWICHTE = { @@ -34,8 +33,6 @@ GEWICHTE = { "section_fehlt": 3.0, "marker_fehlend": 3.0, "marker_fremd": 3.0, "ziel_ohne_anker": 3.0, "fachlich_falsch": 3.0, "laenge": 0.5, "vorwaerts": 0.5, "det_check": 0.5, "fix_offen": 1.5, - "diagramm_parse_fehler": 3.0, "diagramm_ungegroundet": 3.0, - "diagramm_unverifiziert": 0.5, "diagramm_marker_tot": 3.0, "beispiel_marker_tot": 3.0, } @@ -49,7 +46,6 @@ def _basis(topic: str, ebene: str) -> int: " AND status NOT IN ('gemerged','verworfen')", "struktur": "SELECT COUNT(*) n FROM atome WHERE topic=?" " AND status NOT IN ('gemerged','verworfen')", - "diagramme": "SELECT COUNT(*) n FROM bausteine WHERE topic=?", "guide": "SELECT COUNT(*) n FROM bausteine WHERE topic=?"}[ebene] row = db.one(z, (topic,)) return max(1, int(row["n"]) if row else 1) diff --git a/frontend/package.json b/frontend/package.json index d6f2067..90218ad 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,11 +12,9 @@ "@fontsource-variable/inter": "^5.2.8", "dompurify": "^3.4.7", "highlight.js": "^11.11.1", - "jsdom": "^29.1.1", "katex": "^0.17.0", "marked": "^18.0.4", "marked-highlight": "^2.2.4", - "mermaid": "^11.4.1", "vue": "^3.5.32" }, "devDependencies": { diff --git a/frontend/src/App.vue b/frontend/src/App.vue index ce63ec5..1009ab9 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -7,9 +7,9 @@ import Ueben from './components/Ueben.vue' import Kennzahlen from './components/Kennzahlen.vue' const topics = ref([]) -const topic = ref('') +const topic = ref(localStorage.getItem('topic') || '') // Auswahl über Reload halten const state = ref(null) -const tab = ref('board') +const tab = ref(localStorage.getItem('tab') || 'board') const neuName = ref('') const neuArt = ref('thema') const fehler = ref('') @@ -24,6 +24,9 @@ function navToggle() { localStorage.setItem('navAuf', navAuf.value ? '1' : '0') } watch(navAuf, (a) => document.body.classList.toggle('nav-zu', !a), { immediate: true }) +// Ansicht über Reload halten: gewähltes Topic + Tab merken +watch(topic, (t) => localStorage.setItem('topic', t)) +watch(tab, (t) => localStorage.setItem('tab', t)) let wsSchliessen = null let timer = null @@ -40,6 +43,8 @@ media.addEventListener('change', themeAnwenden) async function topicsLaden() { topics.value = await api.topics() + // gemerktes Topic gelöscht? → verwerfen, dann erst auf das erste zurückfallen + if (topic.value && !topics.value.some((t) => t.name === topic.value)) topic.value = '' if (!topic.value && topics.value.length) topic.value = topics.value[0].name } diff --git a/frontend/src/components/Board.vue b/frontend/src/components/Board.vue index d19f7ca..d95cc8b 100644 --- a/frontend/src/components/Board.vue +++ b/frontend/src/components/Board.vue @@ -41,7 +41,6 @@ const EBENEN = [ { key: 'inventar', titel: 'Atome' }, { key: 'artefakte', titel: 'Artefakte' }, { key: 'struktur', titel: 'Bausteine' }, - { key: 'diagramme', titel: 'Diagramme' }, { key: 'guide', titel: 'Guide' }, ] @@ -80,10 +79,6 @@ function karten(key) { return s.bausteine.map((b) => ({ id: b.id, text: `${b.ord + 1}. ${b.titel}`, badges: [[b.status, statusFarbe(b.status)]] })) } - if (key === 'diagramme') { - return (s.diagramme || []).map((d) => ({ id: d.id, text: `${d.ord + 1}. ${d.titel}`, - badges: [[d.status, statusFarbe(d.status)], [`${d.typ} · ${d.quelle}`, 'blau']] })) - } return s.bausteine.map((b) => ({ id: b.id, text: `${b.ord + 1}. ${b.titel}`, badges: [[b.stage || 'offen', b.stage === 'done' ? 'gruen' : 'blau']] })) } diff --git a/frontend/src/components/Guide.vue b/frontend/src/components/Guide.vue index 9f23cb2..32536a6 100644 --- a/frontend/src/components/Guide.vue +++ b/frontend/src/components/Guide.vue @@ -1,14 +1,8 @@