diff --git a/backend/bausteine.py b/backend/bausteine.py index 4e43e53..59ac853 100644 --- a/backend/bausteine.py +++ b/backend/bausteine.py @@ -27,12 +27,12 @@ from lernen import FRAGETYPEN from paths import arbeit_dir, bausteine_path, frage_muster_path, project_dir, subbausteine_path, quelle_path, quelle_crawl_dir, safe_ordner from crawl import crawl from pipeline import ( - CANCELLED, FAILED, GenContext, _extra, _gather_fortschritt, _log, _prompt, _race, _relevanz_schema, _rest_schema, + CANCELLED, FAILED, GenContext, _extra, _gather_fortschritt, _log, _prompt, _race, _relevanz_schema, _runde_schema, _semaphore, _str_liste, _stufen_schema, _timeout, run_single_slot, ) from textkit import ( _eindeutige_titel, _lade_bausteine, _norm_titel, _parse_auswahl, _parse_subbausteine, _titel, - _titel_aufloesen, _titel_index, _vormerge, + _titel_aufloesen, _titel_index, ) # Subbausteine (Websuche je Baustein) chunken: 1 Agent je ~10 Bausteine, gedeckelt. @@ -490,31 +490,13 @@ def _relevanz_komplett(data) -> bool: ) -def _frage_muster_schema(data) -> list[dict] | None: - """{"muster": [{subbaustein, typ, frage}, …]} → Liste valider Einträge · sonst None. - - typ muss ein bekannter Fragetyp sein; subbaustein + frage nicht leer. Leere Liste → None. - """ - if not isinstance(data, dict) or not isinstance(data.get("muster"), list): - return None - out = [] - for e in data["muster"]: - if not isinstance(e, dict): - return None - sub = str(e.get("subbaustein", "")).strip() - typ = str(e.get("typ", "")).strip().casefold() - frage = str(e.get("frage", "")).strip() - if not sub or typ not in FRAGETYPEN or not frage: - return None - out.append({"subbaustein": sub, "typ": typ, "frage": frage}) - return out or None def _frage_muster_chunk_schema(data) -> list[dict] | None: """{"muster": [{baustein, subbaustein, typ, frage}, …]} → Liste valider Einträge · sonst None. - Wie _frage_muster_schema, aber mit `baustein` (Zuordnung im 10er-Chunk). Ungültige - Einzel-Einträge werden übersprungen (nicht die ganze Liste verworfen).""" + Mit `baustein` (Zuordnung im 10er-Chunk). Ungültige Einzel-Einträge werden + übersprungen (nicht die ganze Liste verworfen).""" if not isinstance(data, dict) or not isinstance(data.get("muster"), list): return None out = [] @@ -567,54 +549,10 @@ def _lpt_chunks(gewichte: list[int], target: int) -> list[list[int]]: return [b for b in bins if b] -def _merge_finder(num: int, idx: dict, finder: list[dict]) -> tuple[list[str], list[str]]: - """Subbausteine eines Bausteins über die Finder mergen → (konsens ≥2, rest ==1).""" - counts: dict[str, int] = {} - repr_text: dict[str, str] = {} - order: list[str] = [] - for d in finder: - # die Marker-Liste dieses Finders für genau diesen Baustein - subs = next((s for marker, s in d.items() if _titel_aufloesen(idx, marker) == num), []) - gesehen: set[str] = set() - for sub in subs: - key = _norm_titel(sub) - if not key or key in gesehen: - continue - gesehen.add(key) - if key not in counts: - counts[key], repr_text[key] = 0, sub - order.append(key) - counts[key] += 1 - konsens = [repr_text[k] for k in order if counts[k] >= 2] - rest = [repr_text[k] for k in order if counts[k] == 1] - return konsens, rest -def _final_text(chunk: list[int], entries: dict, daten: dict) -> str: - """Marker-Datei aus reinem Konsens (wenn kein Judge nötig).""" - teile = [] - for num in chunk: - konsens = daten[num][0] - if konsens: - teile.append(f"\n" + "\n".join(f"- {s}" for s in konsens)) - return "\n".join(teile) + "\n" -def _judge_block(chunk: list[int], entries: dict, daten: dict) -> str: - """Eingabe für den Subbaustein-Judge: pro Baustein Konsens + Strittiges.""" - lines = [] - for num in chunk: - konsens, rest = daten[num] - lines.append(f"BAUSTEIN: {_titel(entries[num])}") - lines.append("Konsens:") - lines.extend(f"- {s}" for s in konsens) - if not konsens: - lines.append("- (noch keiner)") - if rest: - lines.append("Strittig:") - lines.extend(f"- {s}" for s in rest) - lines.append("") - return "\n".join(lines) async def _subbausteine_block(ctx: GenContext, set_p, files: dict, entries: dict, instructions: str) -> dict | None: @@ -1191,14 +1129,6 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict # ── Inventar in der DB: Recherche-Loop · Konsolidierung · Klärung ──────────── -def _cited_sources(text: str) -> set[str]: - """Zitierte Quellen je Eintrag = 3. ' — '-Segment (URL bzw. Dateiname), kleingeschrieben.""" - out = set() - for eintrag in _parse_auswahl(text).values(): - teile = [t.strip() for t in eintrag.split(" — ")] - if len(teile) >= 3 and teile[-1]: - out.add(teile[-1].lower()) - return out def _crawl_index(ordner) -> dict[str, str]: @@ -1221,29 +1151,8 @@ def _crawl_index(ordner) -> dict[str, str]: return idx -def _abgedeckt(zitiert: set[str], crawl_idx: dict[str, str]) -> set[str]: - """Zitierte Quellen → Menge kanonischer Crawl-Seiten-Keys (was nicht passt, fällt weg).""" - out = set() - for z in zitiert: - key = crawl_idx.get(z) or crawl_idx.get(z.rstrip("/")) - if key: - out.add(key) - return out -def _fokus_text(bereits: list[str], offen: list[str]) -> str: - """Re-Prompt-Block: noch nicht abgedeckte Crawl-Seiten + bereits gefundene Titel.""" - teile = [] - if offen: - liste = "\n".join(f"- {n}" for n in offen[:150]) - teile.append( - "NOCH NICHT ABGEDECKTE QUELLEN-DATEIEN — lies ZUERST genau diese im Quell-Ordner " - f"und ergänze daraus die noch fehlenden Bausteine:\n{liste}" - ) - if bereits: - liste = "\n".join(f"- {t}" for t in bereits) - teile.append("BEREITS GEFUNDEN (NICHT wiederholen — liefere nur NEUE Bausteine):\n" + liste) - return ("\n\n" + "\n\n".join(teile)) if teile else "" async def _set_inventar(topic: str, eintrag: str, status: str) -> None: diff --git a/backend/database.py b/backend/database.py index 512b127..42be3ff 100644 --- a/backend/database.py +++ b/backend/database.py @@ -492,17 +492,6 @@ async def set_baustein_score(topic: str, baustein: str, score: int) -> int: return score -async def set_baustein_streak(topic: str, baustein: str, streak: int) -> None: - """Persistiert die Streak-Folge (Bonus) absolut — überlebt Reload/Session-Wechsel.""" - db = await get_db() - await db.execute( - """INSERT INTO baustein_progress (topic, baustein, streak, updated_at) - VALUES (?, ?, ?, ?) - ON CONFLICT(topic, baustein) DO UPDATE SET - streak = excluded.streak, updated_at = excluded.updated_at""", - (topic, baustein, streak, _now()), - ) - await db.commit() async def set_baustein_verstanden(topic: str, baustein: str) -> bool: @@ -793,11 +782,6 @@ async def get_step_status(topic: str, schritt: str) -> str: return row[0] if row else "offen" -async def list_pipeline_state(topic: str) -> dict[str, str]: - db = await get_db() - cursor = await db.execute("SELECT schritt, status FROM pipeline_state WHERE topic = ?", (topic,)) - rows = await cursor.fetchall() - return {s: st for s, st in rows} async def delete_pipeline_state(topic: str, schritte: list[str] | None = None) -> None: @@ -810,24 +794,8 @@ async def delete_pipeline_state(topic: str, schritte: list[str] | None = None) - await db.commit() -async def set_quelle(topic: str, type: str, ort: str = "", spec: str = "") -> None: - db = await get_db() - await db.execute( - """INSERT INTO quelle (topic, type, ort, spec, updated_at) VALUES (?, ?, ?, ?, ?) - ON CONFLICT(topic) DO UPDATE SET - type = excluded.type, ort = excluded.ort, spec = excluded.spec, updated_at = excluded.updated_at""", - (topic, type, ort, spec, _now()), - ) - await db.commit() -async def get_quelle(topic: str) -> dict | None: - db = await get_db() - cursor = await db.execute("SELECT type, ort, spec FROM quelle WHERE topic = ?", (topic,)) - row = await cursor.fetchone() - if row is None: - return None - return {"type": row[0], "ort": row[1], "spec": row[2]} async def delete_quelle(topic: str) -> None: diff --git a/backend/guide.py b/backend/guide.py index f54b384..1c6d3b1 100644 --- a/backend/guide.py +++ b/backend/guide.py @@ -79,7 +79,7 @@ async def _load_subbausteine(topic: str) -> dict[str, list[dict]]: def _zuteilung_subs(chunk: list[dict], entries: dict[int, str], subs_by_titel: dict[str, list[dict]]) -> str: - """Wie _zuteilung_text, aber listet pro Baustein seine Subbausteine mit Stufe.""" + """Listet je Kapitel die Bausteine, darunter ihre Subbausteine mit Stufe.""" lines: list[str] = [] for ch in chunk: lines.append(f"KAPITEL: {ch['title']}") diff --git a/backend/pipeline.py b/backend/pipeline.py index a8b2efa..c8c8adc 100644 --- a/backend/pipeline.py +++ b/backend/pipeline.py @@ -116,11 +116,6 @@ def _str_liste(val) -> list[str] | None: return None if any(not x for x in out) else out -def _rest_schema(data): - """{"uebernehmen": [str]} → Liste (leer erlaubt) · sonst None.""" - if not isinstance(data, dict): - return None - return _str_liste(data.get("uebernehmen")) def _runde_schema(data, final: bool = False): diff --git a/backend/regeln.py b/backend/regeln.py index cb3513f..a4beea1 100644 --- a/backend/regeln.py +++ b/backend/regeln.py @@ -54,11 +54,6 @@ def _content_json(topic: str, fmt: str) -> dict | None: return None -def _kapitel_titel(topic: str, fmt: str) -> set[str] | None: - content = _content_json(topic, fmt) - if content is None: - return None - return {c.get("title") for c in content.get("chapters", [])} def _section_titel(topic: str, fmt: str) -> set[str] | None: diff --git a/backend/textkit.py b/backend/textkit.py index 0f10e19..6ed9d13 100644 --- a/backend/textkit.py +++ b/backend/textkit.py @@ -42,28 +42,6 @@ def _eindeutige_titel(entries: dict[int, str]) -> dict[int, str]: return out -def _vormerge(listen: list[dict[int, str]]) -> list[tuple[str, int]]: - """Vereinigt Baustein-Listen: exakte Titel-Dubletten (per _norm_titel) zusammenführen. - - → [("Titel — Beschreibung", nennungen)] in Erstnennungs-Reihenfolge. - nennungen = Anzahl der Listen, die den Titel nennen (Dublette innerhalb - einer Liste zählt nicht doppelt). Repräsentant ist die Erstnennung; ein - drittes " — "-Segment (Quelle) wird verworfen. - """ - merged: dict[str, tuple[str, int]] = {} - for liste in listen: - gesehen: set[str] = set() - for text in liste.values(): - key = _norm_titel(_titel(text)) - if key in gesehen: - continue - gesehen.add(key) - if key in merged: - repr_text, n = merged[key] - merged[key] = (repr_text, n + 1) - else: - merged[key] = (" — ".join(text.split(" — ")[:2]), 1) - return list(merged.values()) def _titel_index(entries: dict[int, str]) -> dict[str, int]: @@ -231,9 +209,3 @@ def _split_chunks(chapters: list[dict], n: int) -> list[list[dict]]: return chunks -def _zuteilung_text(chunk: list[dict], entries: dict[int, str]) -> str: - lines = [] - for ch in chunk: - lines.append(f"KAPITEL: {ch['title']}") - lines.extend(f"- {entries[num]}" for num in ch["nums"]) - return "\n".join(lines) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 868f389..e08fae7 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -14,6 +14,7 @@ const selectedTopic = ref(null) const previewGuide = ref(null) const sidebarPinned = ref(localStorage.getItem('sidebarPinned') !== 'false') const sidebarSticky = ref(false) +const fokusOffen = ref(false) // Baustein-Vollbild aktiv → Sidebar als Overlay über das Fokus-Overlay heben const darkMode = ref( localStorage.getItem('darkMode') === null ? window.matchMedia('(prefers-color-scheme: dark)').matches @@ -374,9 +375,9 @@ onMounted(async () => {