From 12f97031e86bf455dd1c28dc202ba328c171c57e Mon Sep 17 00:00:00 2001 From: team3 Date: Mon, 29 Jun 2026 17:11:23 +0200 Subject: [PATCH] update --- backend/bausteine.py | 105 +++++++++++- backend/models.py | 13 ++ backend/routes.py | 15 +- frontend/src/App.vue | 32 +++- frontend/src/api.js | 13 +- .../src/components/BausteineUebersicht.vue | 162 +++++++++++++++++- frontend/src/components/TopicSidebar.vue | 28 +-- 7 files changed, 330 insertions(+), 38 deletions(-) diff --git a/backend/bausteine.py b/backend/bausteine.py index 58ede56..85cd912 100644 --- a/backend/bausteine.py +++ b/backend/bausteine.py @@ -356,6 +356,17 @@ def _resume_step(topic: str) -> int: return _step_idx(topic, "Stufen finden") +def _feine_status(topic: str, current: int | None) -> list[dict]: + """Feiner Teilschritt-Status: je Schritt {label, phase, state}. state aus `current` + (done = idx). Phasen-Label aus PHASEN.""" + schritt_phase = {s: label for label, schritte in PHASEN for s in schritte} + out = [] + for i, s in enumerate(_bausteine_steps(topic)): + state = "pending" if current is None or current < i else "done" if current > i else "active" + out.append({"label": s, "phase": schritt_phase.get(s, ""), "state": state}) + return out + + def bausteine_status(topic: str) -> dict: # Intern feingranular (Resume/Progress); für die Anzeige zu 5 groben Phasen gebündelt. feine = _bausteine_steps(topic) @@ -376,6 +387,7 @@ def bausteine_status(topic: str) -> dict: "error": _bausteine_errors.get(topic), "partial": partial, "steps": _phasen_status(topic, current), + "feine_steps": _feine_status(topic, current), } @@ -450,6 +462,91 @@ def _reset_ab_phase(topic: str, label: str) -> None: files["final"].unlink(missing_ok=True) +async def _reset_ab_step(topic: str, step_idx: int) -> None: + """Feiner Reset AB einem Teilschritt (0-basierter Index in _bausteine_steps). Setzt + pipeline_state + Artefakte + DB ab hier zurück; frühere Schritte bleiben. Inventar-Teilschritte + rekonstruieren den DB-Status aus den Artefakten (Dedup/Filter sicher; Klärung fällt robust auf + Konsolidierung zurück, weil die Klärung umbenennt → Titel-Mismatch wäre fragil).""" + feine = list(_bausteine_steps(topic)) + if not (0 <= step_idx < len(feine)): + return + ab = set(feine[step_idx:]) + files = _bausteine_files(topic) + arbeit = files["arbeit"] + + def gd(pat: str) -> None: + if arbeit.is_dir(): + for p in arbeit.glob(pat): + p.unlink(missing_ok=True) + + await db.delete_pipeline_state(topic, list(feine[step_idx:])) + # Spätere Artefakte/DB kumulativ ab dem betroffenen Schritt (von hinten nach vorn). + if {"Beispiele", "Karteikarten"} & ab: + files["artefakte"].unlink(missing_ok=True); gd("artefakt-*"); await db.delete_sub_artefakte(topic) + if any(s.startswith("Fragen") for s in ab): + files["frage_muster"].unlink(missing_ok=True); gd("frage-muster-*"); await db.delete_frage_muster(topic) + if "Gliederung" in ab: + files["gliederung"].unlink(missing_ok=True); gd("gliederung-*"); await db.delete_gliederung(topic) + if any(s.startswith("Relevanz") for s in ab): + gd("relevanz-*") + if any(s.startswith("Stufen") for s in ab): + gd("stufe-*") + if any(s.startswith("Fakten") for s in ab): + files["fakten"].unlink(missing_ok=True); gd("fakten-*") + # Sidecar trägt Subbausteine + ihre Felder stufe/relevanz/fakten. Ab Subbausteinen ganz neu; + # sonst nur die Felder der neu-zu-bauenden Phasen strippen — Subbausteine bleiben erhalten. + if any(s.startswith("Subbaustein") for s in ab): + files["sidecar"].unlink(missing_ok=True) + else: + strip = {f for s, f in (("Fakten", "fakten"), ("Stufen", "stufe"), ("Relevanz", "relevanz")) + if any(x.startswith(s) for x in ab)} + if strip: + sc = _json_datei(files["sidecar"]) + if isinstance(sc, dict): + for subs in sc.values(): + for s in (subs if isinstance(subs, list) else []): + if isinstance(s, dict): + for f in strip: + s.pop(f, None) + atomic_write_json(files["sidecar"], sc, indent=1) + if any(s.startswith("Subbaustein") for s in ab): + files["sub_roh"].unlink(missing_ok=True); gd("subbaustein-*"); await db.delete_subbausteine(topic) + # --- Inventar (DB-Status kaskadiert) --- + if "Bausteine-Filter" in ab and not ({"Klärung", "Konsolidierung", "Recherche", "Dedup"} & ab): + # Nur Filter neu: degradierte Bausteine zurück auf konsens. + d = _json_datei(arbeit / "inventar-filter.json") + for f in (d.get("fragmente", []) if isinstance(d, dict) else []): + await db.set_baustein_status(topic, _norm_titel(f.get("fragment", "")), "konsens") + gd("inventar-filter*") + if "Dedup" in ab and not ({"Klärung", "Konsolidierung", "Recherche"} & ab): + # Dedup (+Filter) neu: alle im Dedup/Filter verworfenen Bausteine zurück auf konsens. + for art in ("dedup-runde-1.json", "inventar-filter.json"): + d = _json_datei(arbeit / art) + titel = ([t for g in d.get("gruppen", []) for t in g] if isinstance(d, dict) and "gruppen" in d + else [f.get("fragment", "") for f in d.get("fragmente", [])] if isinstance(d, dict) else []) + for t in titel: + await db.set_baustein_status(topic, _norm_titel(t), "konsens") + gd("dedup-*"); gd("inventar-filter*") + if {"Klärung", "Konsolidierung"} & ab and not ({"Recherche"} & ab): + # Klärung/Konsolidierung neu: Inventar-DB leeren (Recherche-Reader bleiben). Klärungs-Rückbau + # wäre wegen Umbenennung fragil → ab Konsolidierung sauber neu aufbauen. + await db.delete_bausteine(topic) + gd("klaerung*"); gd("konsolidierung-*"); gd("dedup-*"); gd("inventar-filter*") + if "Recherche" in ab: # ganzes Inventar wie Phase-Reset + for p_alt in _alle_slot_dateien(files): + p_alt.unlink(missing_ok=True) + files["final"].unlink(missing_ok=True) + await db.delete_bausteine(topic) + + +async def reset_bausteine_ab_step(topic: str, step_idx: int) -> None: + """Öffentlich: NUR ab einem Teilschritt zurücksetzen — kein Neu-Generieren. Lässt einen + Teil-Stand zurück (die Schritte ab hier gelten als offen). Läuft eine Generierung → ignorieren.""" + if topic in _bausteine_progress: + return + await _reset_ab_step(topic, step_idx) + + def _ergaenzung_schema(data): """{"bausteine": [{"titel", "beschreibung"}]} → Liste (leer erlaubt) · sonst None.""" if not isinstance(data, dict) or not isinstance(data.get("bausteine"), list): @@ -2999,7 +3096,7 @@ async def _reset_db_ab_phase(topic: str, label: str) -> None: await db.delete_pipeline_state(topic, ["Quelle aufbereiten"]) -async def generate_bausteine(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_phase: int | None = None) -> None: +async def generate_bausteine(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_phase: int | None = None, ab_step: int | None = None) -> None: if topic in _bausteine_progress: return _bausteine_progress[topic] = "Wartend…" @@ -3029,7 +3126,9 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str = files["arbeit"].mkdir(parents=True, exist_ok=True) # Re-Run ab gewählter Phase: Artefakte ab dort löschen; der Frischstart-Block # unten wird übersprungen (er würde bei erhaltener Sidecar sonst alles wischen). - if ab_phase is not None: + if ab_step is not None: # feiner Teilschritt-Re-Run (Vorrang vor ab_phase) + await _reset_ab_step(topic, ab_step) + elif ab_phase is not None: phasen = _phasen(topic) label = phasen[ab_phase - 1][0] if 1 <= ab_phase <= len(phasen) else "Inventar" _reset_ab_phase(topic, label) @@ -3043,7 +3142,7 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str = # Sidecar) → kompletter Frischstart. Liegt bausteine.md ohne Sidecar vor, # ist das ein Teil-Stand (Block B/C offen) → Resume, nicht wischen. # Bei explizitem Re-Run (ab_phase) hat _reset_ab_phase das schon erledigt. - fertig = ab_phase is None and final_path.exists() and _sidecar_schema(_json_datei(files["sidecar"])) is not None + fertig = ab_phase is None and ab_step is None and final_path.exists() and _sidecar_schema(_json_datei(files["sidecar"])) is not None if fertig: for p_alt in _alle_slot_dateien(files): p_alt.unlink(missing_ok=True) diff --git a/backend/models.py b/backend/models.py index dbca1ec..8edce2e 100644 --- a/backend/models.py +++ b/backend/models.py @@ -31,6 +31,12 @@ class BausteineCreateRequest(BaseModel): source_type: SourceType = "thema" source_ort: str = Field(default="", max_length=2000) ab_phase: int | None = Field(default=None, ge=1, le=9) # Re-Run ab grober Phase (Position in _phasen(topic), 1-based; bis 9: …Gliederung/Fragen/Artefakte); None = Resume/Fortsetzen ohne Löschen + ab_step: int | None = Field(default=None, ge=0) # Re-Run ab feinem Teilschritt (0-basierter Index in _bausteine_steps); hat Vorrang vor ab_phase + + +class BausteineResetStepRequest(BaseModel): + topic: str = Field(min_length=1, max_length=100) + ab_step: int = Field(ge=0) # NUR zurücksetzen ab diesem Teilschritt (kein Neu-Generieren) class BausteineStep(BaseModel): @@ -38,6 +44,12 @@ class BausteineStep(BaseModel): state: Literal["done", "active", "pending"] +class BausteineFeinStep(BaseModel): + label: str + phase: str = "" + state: Literal["done", "active", "pending"] + + class BausteineStatusResponse(BaseModel): ready: bool generating: bool @@ -45,6 +57,7 @@ class BausteineStatusResponse(BaseModel): error: str | None = None partial: bool = False steps: list[BausteineStep] = [] + feine_steps: list[BausteineFeinStep] = [] class ProjectResponse(BaseModel): diff --git a/backend/routes.py b/backend/routes.py index 21b2940..a8b0dae 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -20,7 +20,7 @@ from database import ( delete_topic_pipeline, delete_quelle, get_guide_content, delete_guide_content, get_sub_artefakte, ) -from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, subbausteine_frei, lade_frage_muster, lade_frage_muster_frei +from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, reset_bausteine_ab_step, lade_quelle, lade_uebersicht, subbausteine_titel, subbausteine_frei, lade_frage_muster, lade_frage_muster_frei from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion from lernen import baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, pruefung_frage_variante, quiz_generieren, lueckwahl_generieren, lueckentext_generieren, lueckentext_pruefen, huerden_distraktor_block, score_berechnen, floor_aus_score, stufe_aus_score, cap_final, cap_aktuell, freie_ebene, schwellen, punkte_delta, deckel_nachfrage from guide import generate_guide, guide_slot_dateien, guide_fertig_step, block_pruefen, block_uebernehmen, content_fuer_ebene @@ -29,7 +29,7 @@ from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lern from models import ( GuideCreateRequest, GuideResponse, TopicCreateRequest, - BausteineCreateRequest, BausteineStatusResponse, + BausteineCreateRequest, BausteineResetStepRequest, BausteineStatusResponse, GuideChatRequest, GuideChatResponse, ElementCreateRequest, ElementChatRequest, ElementChatResponse, ElementResponse, ElementUpdateRequest, ElementCheckRequest, ElementCheckResponse, ElementStyleResponse, @@ -164,7 +164,7 @@ async def create_bausteine(req: BausteineCreateRequest): raise HTTPException(400, "Link muss mit http:// oder https:// beginnen.") qp.parent.mkdir(parents=True, exist_ok=True) atomic_write_json(qp, {"type": typ, "ort": ort, "spec": req.instructions.strip()}) - asyncio.create_task(generate_bausteine(topic, req.instructions.strip(), req.provider, ab_phase=req.ab_phase)) + asyncio.create_task(generate_bausteine(topic, req.instructions.strip(), req.provider, ab_phase=req.ab_phase, ab_step=req.ab_step)) return {"ok": True} @@ -182,6 +182,15 @@ async def remove_bausteine(topic: str): return {"ok": True} +@router.post("/bausteine/reset-step") +async def reset_bausteine_step(req: BausteineResetStepRequest): + topic = req.topic.strip() + if bausteine_status(topic)["generating"]: + return {"ok": True, "status": "generating"} # nicht in laufende Generierung eingreifen + await reset_bausteine_ab_step(topic, req.ab_step) + return {"ok": True} + + @router.delete("/bausteine/fortschritt") async def reset_baustein_fortschritt(topic: str, baustein: str): """Lern-Fortschritt EINES Bausteins auf null (Score/Streak/Flags/offene Frage).""" diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 1d6027a..0a03024 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,6 +1,6 @@