From 4f1f8d18d08da0037293d864df3f3225d8a07e92 Mon Sep 17 00:00:00 2001 From: team3 Date: Mon, 22 Jun 2026 10:13:30 +0200 Subject: [PATCH] update --- backend/config.py | 2 + backend/guide.py | 50 +++++++++++++++++------- backend/models.py | 2 + backend/routes.py | 2 +- frontend/src/App.vue | 2 +- frontend/src/components/TopicDetail.vue | 25 ++++++++---- frontend/src/components/TopicSidebar.vue | 2 + 7 files changed, 60 insertions(+), 25 deletions(-) diff --git a/backend/config.py b/backend/config.py index d5849f2..6f4e665 100644 --- a/backend/config.py +++ b/backend/config.py @@ -69,6 +69,8 @@ TIMEOUTS = { # Auswahl-Auftrag je Format: (Mindest-Anteil, Maximal-Anteil, Mindestanzahl, Zweck). FORMAT_ANTEIL = { "Guide": (0.0, 1.0, 0, "einen fokussierten Guide — alles Relevante ohne Randthemen"), + "FullGuide": (0.0, 1.0, 0, "einen Komplett-Guide — das ganze Thema inkl. Randthemen"), + "Rest": (0.0, 1.0, 0, "einen Ergänzungs-Guide — nur die Randthemen"), } # Provider-Stacks: komplett unabhängig, einer kann jederzeit entfernt werden. diff --git a/backend/guide.py b/backend/guide.py index bbfa565..4e53bd2 100644 --- a/backend/guide.py +++ b/backend/guide.py @@ -422,15 +422,21 @@ async def _generate_sections( "Wähle, was diesem Zweck dient — lass weg, was dafür nicht nötig ist." ) - # Subbausteine je Baustein (Sidecar) — früh geladen: „Guide" wählt darüber aus - # (Bausteine mit ≥1 relevantem Sub) und filtert später Rand-Subs. Fehlt sie → {} (Fallback). + # Subbausteine je Baustein (Sidecar) — früh geladen: steuert Auswahl + Sub-Filter je Format. + # Fehlt sie → {} (Fallback: Guide nimmt alles). subs_raw = _load_subbausteine(topic) - # Schritt 0: Auswahl. „Guide" ist deterministisch — alle Bausteine mit ≥1 - # relevantem Subbaustein. Fallback (keine Relevanz-Daten): alle Bausteine. - if format_name == "Guide": - auswahl = [num for num in entries - if any(s.get("relevanz") == "relevant" for s in subs_raw.get(_titel(entries[num]), []))] + def _hat_relevanz(num, art): + return any(isinstance(s, dict) and s.get("relevanz") == art for s in subs_raw.get(_titel(entries[num]), [])) + + # Schritt 0: Auswahl. Deterministisch je Format (Roadmap hat einen eigenen Weg): + # Guide=relevante Bausteine · FullGuide=alle · Rest=Rand-Bausteine. + if format_name == "FullGuide": + auswahl = list(entries) + elif format_name == "Rest": + auswahl = [num for num in entries if _hat_relevanz(num, "rand")] + elif format_name == "Guide": + auswahl = [num for num in entries if _hat_relevanz(num, "relevant")] if not auswahl: _log(topic, "Guide: keine Relevanz-Daten — alle Bausteine genommen") auswahl = list(entries) @@ -442,6 +448,9 @@ async def _generate_sections( ) if auswahl is None: return None + if not auswahl: + await _fail(guide_id, "Keine passenden Bausteine für dieses Format") + return None sel_entries = {num: entries[num] for num in auswahl} soll = len(sel_entries) @@ -523,13 +532,14 @@ async def _generate_sections( # dieselbe Aufteilung: 1 Agent je ~30 Bausteine (gedeckelt). total_sections = sum(len(c["nums"]) for c in plan) chunks = _split_chunks(plan, min(WRITER_MAX, max(1, math.ceil(total_sections / WRITER_SECTIONS)))) - # Subbausteine je Baustein. Rand-Subbausteine fliegen aus dem Guide (Fokus auf - # Relevantes). Subs ohne Relevanz-Feld (Alt-Themen) bleiben erhalten. - # Fehlt die Sidecar → leer → Inhalte/Writer laufen auf Baustein-Granularität. - subs_by_titel = { - t: [s for s in subs if s.get("relevanz") != "rand"] - for t, subs in subs_raw.items() - } + # Subbausteine je Baustein, je Format gefiltert: Guide=relevant · FullGuide=alle · Rest=Rand. + # Subs ohne Relevanz-Feld (Alt-Themen) zählen als „nicht Rand" (→ Guide/FullGuide). + if format_name == "FullGuide": + subs_by_titel = {t: list(subs) for t, subs in subs_raw.items()} + elif format_name == "Rest": + subs_by_titel = {t: [s for s in subs if s.get("relevanz") == "rand"] for t, subs in subs_raw.items()} + else: # Guide + subs_by_titel = {t: [s for s in subs if s.get("relevanz") != "rand"] for t, subs in subs_raw.items()} subs_by_titel = {t: subs for t, subs in subs_by_titel.items() if subs} zuteilungen = [_zuteilung_subs(chunk, entries, subs_by_titel) for chunk in chunks] chunk_sizes = [sum(len(c["nums"]) for c in chunk) for chunk in chunks] @@ -784,12 +794,22 @@ async def _generate_sections( break scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums] + # Prüfbar = Format hat Prüfung UND Baustein hat ≥1 relevanten Subbaustein. + # Guide ist immer prüfbar (auch ohne Relevanz-Daten, Fallback = alles). + def _pruefbar(num): + if format_name == "Guide": + return True + if format_name == "FullGuide": + return any(isinstance(s, dict) and s.get("relevanz") == "relevant" + for s in subs_raw.get(_titel(entries[num]), [])) + return False # Rest u.a. → reine Lese-Sections + await _set_progress(guide_id, "Setze zusammen…") chapters: list[dict] = [] for ch in plan: sections = [ {"num": num, "title": _titel(entries[num]), "md": by_num[num]["md"], - "kompakt": by_num[num].get("kompakt", "")} + "kompakt": by_num[num].get("kompakt", ""), "pruefbar": _pruefbar(num)} for num in ch["nums"] if num in by_num ] if sections: diff --git a/backend/models.py b/backend/models.py index b0424e2..239e918 100644 --- a/backend/models.py +++ b/backend/models.py @@ -4,6 +4,8 @@ from typing import Literal FormatType = Literal[ "Roadmap", "Guide", + "FullGuide", + "Rest", ] ProviderType = Literal["claude", "minimax", "lokal"] diff --git a/backend/routes.py b/backend/routes.py index cad2bed..98c2d61 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -478,7 +478,7 @@ async def list_all(): async def guide_locks(topic: str): """Sperr-Gründe pro Format für den ▶-Button — None = erstellbar.""" guides, progress, levels = await lade_lernstand() - return {fmt: guide_lock(topic, fmt, guides, progress, levels) for fmt in ("Roadmap", *FORMATE)} + return {fmt: guide_lock(topic, fmt, guides, progress, levels) for fmt in ("Roadmap", "FullGuide", "Rest", *FORMATE)} @router.get("/guides/{guide_id}", response_model=GuideResponse) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 1f4fb8c..2481d46 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -181,7 +181,7 @@ async function loadBausteine() { } } -const FORMAT_ORDER = ['Guide', 'Roadmap'] +const FORMAT_ORDER = ['Guide', 'FullGuide', 'Roadmap', 'Rest'] function autoPreview() { const map = doneByFormat.value diff --git a/frontend/src/components/TopicDetail.vue b/frontend/src/components/TopicDetail.vue index de63428..ef62f56 100644 --- a/frontend/src/components/TopicDetail.vue +++ b/frontend/src/components/TopicDetail.vue @@ -74,13 +74,19 @@ const bausteine = computed(() => const fokusBaustein = computed(() => (fokusIndex.value === null ? null : bausteine.value[fokusIndex.value])) function openFokus(baustein, tab) { + if (!baustein.pruefbar) return // reine Lese-Sections (Rest/Rand) öffnen keinen Prüfungs-Fokus const i = bausteine.value.findIndex((s) => s.title === baustein.title) if (i === -1) return fokusIndex.value = i fokusTab.value = tab || 'pruefung' } -function fokusPrev() { if (fokusIndex.value > 0) fokusIndex.value-- } -function fokusNext() { if (fokusIndex.value < bausteine.value.length - 1) fokusIndex.value++ } +// Nur prüfbare Sections im Fokus anspringen (FullGuide hat dazwischen reine Lese-Sections). +function fokusPrev() { + for (let i = fokusIndex.value - 1; i >= 0; i--) if (bausteine.value[i].pruefbar) { fokusIndex.value = i; return } +} +function fokusNext() { + for (let i = fokusIndex.value + 1; i < bausteine.value.length; i++) if (bausteine.value[i].pruefbar) { fokusIndex.value = i; return } +} // Erfahrungsleiste: Lernstand über alle Bausteine des Guides (sticky, absolviert ⊇ verstanden ⊇ gemeistert). const fortschritt = computed(() => { @@ -94,8 +100,8 @@ const fortschritt = computed(() => { return { total: bausteine.value.length, absolviert: a, verstanden: v, gemeistert: g } }) -// Ein Format „Guide" → Prüfung läuft voll 0–30. Ohne fertigen Guide kein Deckel relevant (6). -const cap = computed(() => (props.doneByFormat?.Guide ? 30 : 6)) +// Prüfung läuft voll 0–30, sobald Guide ODER FullGuide fertig ist (gleicher Baustein-Score). +const cap = computed(() => ((props.doneByFormat?.Guide || props.doneByFormat?.FullGuide) ? 30 : 6)) // --- Chat (Mechanik in useChat; Kontext-Extraktion bleibt hier) --- const chat = useChat((msgs) => { @@ -202,15 +208,18 @@ function extractContext() { :key="s.num" :class="['section-card', lernstand[s.title]?.gemeistert ? 'gemeistert' : (lernstand[s.title]?.verstanden ? 'verstanden' : (lernstand[s.title]?.absolviert ? 'absolviert' : ''))]" > -

+

{{ s.title }} - ✓✓✓ Gemeistert - ✓✓ Verstanden - ✓ Absolviert +

{ const formats = [ { key: 'Roadmap', label: 'Roadmap' }, { key: 'Guide', label: 'Guide' }, + { key: 'FullGuide', label: 'FullGuide' }, + { key: 'Rest', label: 'Rest' }, ] const bausteineState = computed(() => {