This commit is contained in:
team3
2026-06-21 22:06:02 +02:00
parent 50b1c81bca
commit b4c686f74f
13 changed files with 41 additions and 57 deletions

View File

@@ -71,10 +71,7 @@ TIMEOUTS = {
# Auswahl-Auftrag je Format: (Mindest-Anteil, Maximal-Anteil, Mindestanzahl, Zweck). # Auswahl-Auftrag je Format: (Mindest-Anteil, Maximal-Anteil, Mindestanzahl, Zweck).
FORMAT_ANTEIL = { FORMAT_ANTEIL = {
"MiniGuide": (0.05, 0.10, 8, "einen kompakten Anfänger-Guide — der schnelle Einstieg ins Thema"), "Guide": (0.0, 1.0, 0, "einen fokussierten Guide — alles Relevante ohne Randthemen"),
"Guide": (0.25, 0.35, 20, "einen ausführlichen Anfänger-Guide — ein solides Fundament im Thema"),
"ProGuide": (0.0, 1.0, 0, "einen fokussierten Guide — alles Relevante ohne Randthemen"),
"FullGuide": (0.90, 1.00, 0, "einen Komplett-Guide — das ganze Thema"),
} }
# Provider-Stacks: komplett unabhängig, einer kann jederzeit entfernt werden. # Provider-Stacks: komplett unabhängig, einer kann jederzeit entfernt werden.

View File

@@ -71,7 +71,7 @@ def _topic_context(topic: str, limit: int = 12000) -> str:
bp = bausteine_path(topic) bp = bausteine_path(topic)
if bp.exists(): if bp.exists():
parts.append(bp.read_text(encoding="utf-8")) parts.append(bp.read_text(encoding="utf-8"))
for fmt in ("FullGuide", "ProGuide", "Guide", "MiniGuide", "OnePager"): for fmt in ("Guide", "OnePager"):
content = _json_datei(guide_content_path(topic, fmt)) content = _json_datei(guide_content_path(topic, fmt))
if content: if content:
for ch in content.get("chapters", []): for ch in content.get("chapters", []):

View File

@@ -408,17 +408,17 @@ async def _generate_sections(
"Wähle, was diesem Zweck dient — lass weg, was dafür nicht nötig ist." "Wähle, was diesem Zweck dient — lass weg, was dafür nicht nötig ist."
) )
# Subbausteine je Baustein (Sidecar) — früh geladen: ProGuide wählt darüber aus, # Subbausteine je Baustein (Sidecar) — früh geladen: Guide" wählt darüber aus
# Nicht-FullGuide filtert später Rand-Subbausteine. Fehlt sie → {} (Fallback). # (Bausteine mit ≥1 relevantem Sub) und filtert später Rand-Subs. Fehlt sie → {} (Fallback).
subs_raw = _load_subbausteine(topic) subs_raw = _load_subbausteine(topic)
# Schritt 0: Auswahl. ProGuide ist deterministisch — alle Bausteine mit ≥1 # Schritt 0: Auswahl. Guide" ist deterministisch — alle Bausteine mit ≥1
# relevantem Subbaustein. Fallback (keine Relevanz-Daten): alle Bausteine. # relevantem Subbaustein. Fallback (keine Relevanz-Daten): alle Bausteine.
if format_name == "ProGuide": if format_name == "Guide":
auswahl = [num for num in entries auswahl = [num for num in entries
if any(s.get("relevanz") == "relevant" for s in subs_raw.get(_titel(entries[num]), []))] if any(s.get("relevanz") == "relevant" for s in subs_raw.get(_titel(entries[num]), []))]
if not auswahl: if not auswahl:
_log(topic, "ProGuide: keine Relevanz-Daten — alle Bausteine genommen") _log(topic, "Guide: keine Relevanz-Daten — alle Bausteine genommen")
auswahl = list(entries) auswahl = list(entries)
else: else:
# Auswahl-Konsens (5 Agenten → Voting → Mapping → Klärungs-Loop) # Auswahl-Konsens (5 Agenten → Voting → Mapping → Klärungs-Loop)
@@ -501,16 +501,14 @@ async def _generate_sections(
# dieselbe Aufteilung: 1 Agent je ~30 Bausteine (gedeckelt). # dieselbe Aufteilung: 1 Agent je ~30 Bausteine (gedeckelt).
total_sections = sum(len(c["nums"]) for c in plan) 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)))) chunks = _split_chunks(plan, min(WRITER_MAX, max(1, math.ceil(total_sections / WRITER_SECTIONS))))
# Subbausteine je Baustein. Rand-Subbausteine fliegen aus allen Guides AUSSER # Subbausteine je Baustein. Rand-Subbausteine fliegen aus dem Guide (Fokus auf
# FullGuide (Fokus). Subs ohne Relevanz-Feld (Alt-Themen) bleiben erhalten. # Relevantes). Subs ohne Relevanz-Feld (Alt-Themen) bleiben erhalten.
# Fehlt die Sidecar → leer → Inhalte/Writer laufen auf Baustein-Granularität. # Fehlt die Sidecar → leer → Inhalte/Writer laufen auf Baustein-Granularität.
subs_by_titel = subs_raw subs_by_titel = {
if format_name != "FullGuide": t: [s for s in subs if s.get("relevanz") != "rand"]
subs_by_titel = { for t, subs in subs_raw.items()
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}
}
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] 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] chunk_sizes = [sum(len(c["nums"]) for c in chunk) for chunk in chunks]
writer_count = len(chunks) writer_count = len(chunks)

View File

@@ -3,10 +3,7 @@ from typing import Literal
FormatType = Literal[ FormatType = Literal[
"OnePager", "OnePager",
"MiniGuide",
"Guide", "Guide",
"ProGuide",
"FullGuide",
] ]
ProviderType = Literal["claude", "minimax", "lokal"] ProviderType = Literal["claude", "minimax", "lokal"]

View File

@@ -18,12 +18,11 @@ from paths import bausteine_path, guide_content_path
from textkit import _norm_titel from textkit import _norm_titel
MAX_OFFENE_GUIDES = 3 MAX_OFFENE_GUIDES = 3
# Lineare Progression: jedes Format schaltet das nächste frei, sobald ALLE Bausteine # Nur noch EIN Format „Guide" (alle relevanten Bausteine, Prüfung 030). Keine Progression,
# der Vorstufe das geforderte Niveau (Score-Meilenstein 10/20/30) erreicht haben. # keine Vorstufe → der Guide ist immer freischaltbar. „Rest"/OnePager separat.
VORSTUFE = {"Guide": "MiniGuide", "ProGuide": "Guide", "FullGuide": "ProGuide"} VORSTUFE: dict[str, str] = {}
# absolviert=6 · verstanden=12 · gemeistert=18 (siehe lernen.NOETIG/MASTERY/MEISTERN). FREISCHALT_LEVEL: dict[str, str] = {}
FREISCHALT_LEVEL = {"Guide": "absolviert", "ProGuide": "verstanden", "FullGuide": "gemeistert"} FORMATE = ("Guide",)
FORMATE = ("MiniGuide", "Guide", "ProGuide", "FullGuide")
# Geforderter Score je Freischalt-Niveau — nur für die Sperr-Meldung. # Geforderter Score je Freischalt-Niveau — nur für die Sperr-Meldung.
_LEVEL_WORT = { _LEVEL_WORT = {
@@ -106,8 +105,8 @@ def ist_absolviert(topic: str, fmt: str, guides: list[dict], progress: dict[str,
def thema_abgeschlossen(topic: str, guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> bool: def thema_abgeschlossen(topic: str, guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> bool:
"""Thema fertig: neuester fertiger FullGuide, alle Bausteine gemeistert (25)?""" """Thema fertig: neuester fertiger Guide, alle Bausteine gemeistert (18)?"""
return ist_level(topic, "FullGuide", guides, progress, levels["gemeistert"]) return ist_level(topic, "Guide", guides, progress, levels["gemeistert"])
def formate_stats(guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> dict: def formate_stats(guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> dict:

View File

@@ -181,7 +181,7 @@ async function loadBausteine() {
} }
} }
const FORMAT_ORDER = ['OnePager', 'MiniGuide', 'Guide', 'ProGuide', 'FullGuide'] const FORMAT_ORDER = ['Guide', 'OnePager']
function autoPreview() { function autoPreview() {
const map = doneByFormat.value const map = doneByFormat.value

View File

@@ -87,7 +87,6 @@ const bandZiel = computed(() => {
return ABSOLVIERT return ABSOLVIERT
}) })
const FORM_NAME = { quiz: 'Quiz', lueckentext: 'Lückentext', erklaeren: 'Erklären' } const FORM_NAME = { quiz: 'Quiz', lueckentext: 'Lückentext', erklaeren: 'Erklären' }
const NAECHSTES_FORMAT = { 6: 'Guide', 12: 'ProGuide', 18: 'FullGuide' }
// --- Toggle-Bereich --- // --- Toggle-Bereich ---
const activeTab = ref(props.mode === 'full' ? props.initialTab : null) // null | 'chat' | 'pruefung' const activeTab = ref(props.mode === 'full' ? props.initialTab : null) // null | 'chat' | 'pruefung'
@@ -702,7 +701,7 @@ function onPruefKey(e) {
<!-- Prüfung: gesteuerter Dialog --> <!-- Prüfung: gesteuerter Dialog -->
<div v-else> <div v-else>
<p class="bp-hint"> <p class="bp-hint">
<template v-if="amDeckel">{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} — <strong>Max</strong>.<template v-if="cap < VOLL"> Erstelle den {{ NAECHSTES_FORMAT[cap] }} für mehr.</template></template> <template v-if="amDeckel">{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} <strong>Max</strong>.</template>
<template v-else>{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ bandZiel }} · {{ modusRegel }}<span v-if="imZufall" class="bp-hint-key"> · zufällig</span></template> <template v-else>{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ bandZiel }} · {{ modusRegel }}<span v-if="imZufall" class="bp-hint-key"> · zufällig</span></template>
</p> </p>
<button <button

View File

@@ -33,7 +33,7 @@ async function load() {
} }
} }
// Baustein relevant = hat ≥1 relevanten Subbaustein (gleiche Regel wie ProGuide). // Baustein relevant = hat ≥1 relevanten Subbaustein (gleiche Regel wie der Guide).
// Ohne Relevanz-Daten (Alt-Themen) nicht dimmen. // Ohne Relevanz-Daten (Alt-Themen) nicht dimmen.
function relevant(b) { function relevant(b) {
const mitRelevanz = (b.subbausteine || []).filter((s) => s.relevanz) const mitRelevanz = (b.subbausteine || []).filter((s) => s.relevanz)
@@ -70,7 +70,7 @@ const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subbausteine
:key="b.num" :key="b.num"
class="bk-card" class="bk-card"
:class="{ 'bk-irrelevant': !relevant(b) }" :class="{ 'bk-irrelevant': !relevant(b) }"
:title="relevant(b) ? null : 'Nicht relevant — nur im FullGuide'" :title="relevant(b) ? null : 'Nicht relevant — kommt später in den „Rest“'"
> >
<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>
@@ -79,7 +79,7 @@ const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subbausteine
<span class="bk-stufe-label">{{ g.label }}</span> <span class="bk-stufe-label">{{ g.label }}</span>
<ul> <ul>
<li v-for="s in g.subs" :key="s.titel" :class="{ rand: s.relevanz === 'rand' }"> <li v-for="s in g.subs" :key="s.titel" :class="{ rand: s.relevanz === 'rand' }">
{{ s.titel }}<span v-if="s.relevanz === 'rand'" class="rand-tag" title="Randthema — nur im FullGuide">Rand</span> {{ s.titel }}<span v-if="s.relevanz === 'rand'" class="rand-tag" title="Randthema — kommt später in den „Rest“">Rand</span>
</li> </li>
</ul> </ul>
</div> </div>

View File

@@ -122,12 +122,16 @@ watch(() => props.graph, build, { immediate: true })
background: var(--panel, #fff); background: var(--panel, #fff);
color: var(--text, #111); color: var(--text, #111);
font-size: 0.8rem; font-size: 0.8rem;
line-height: 1.3; line-height: 1.35;
text-align: center; text-align: center;
max-width: 240px; max-width: 300px;
/* Langer Text/Formel umbricht, statt im Knoten abgeschnitten zu werden. */
white-space: normal;
overflow-wrap: anywhere;
} }
/* KaTeX im Knoten nicht umbrechen lassen */ /* KaTeX im Knoten umbrechen lassen + geerbtes Klippen (markdown .katex-display) neutralisieren. */
.gv-node :deep(.katex) { white-space: nowrap; } .gv-node :deep(.katex) { white-space: normal; }
.gv-node :deep(.katex-display) { overflow: visible; margin: 0; }
.graph-view :deep(.vue-flow__edge-text) { font-size: 0.72rem; fill: var(--text-muted, #666); } .graph-view :deep(.vue-flow__edge-text) { font-size: 0.72rem; fill: var(--text-muted, #666); }
.graph-view :deep(.vue-flow__edge-textbg) { fill: var(--panel, #fff); } .graph-view :deep(.vue-flow__edge-textbg) { fill: var(--panel, #fff); }
</style> </style>

View File

@@ -93,14 +93,8 @@ const fortschritt = computed(() => {
return { total: bausteine.value.length, absolviert: a, verstanden: v, gemeistert: g } return { total: bausteine.value.length, absolviert: a, verstanden: v, gemeistert: g }
}) })
// Score-Deckel folgt dem höchsten erstellten Format: Mini→6, Guide→12, ProGuide→18, FullGuide→30. // Ein Format „Guide" → Prüfung läuft voll 030. Ohne fertigen Guide kein Deckel relevant (6).
const cap = computed(() => { const cap = computed(() => (props.doneByFormat?.Guide ? 30 : 6))
const d = props.doneByFormat || {}
if (d.FullGuide) return 30
if (d.ProGuide) return 18
if (d.Guide) return 12
return 6
})
// --- Chat (Mechanik in useChat; Kontext-Extraktion bleibt hier) --- // --- Chat (Mechanik in useChat; Kontext-Extraktion bleibt hier) ---
const chat = useChat((msgs) => { const chat = useChat((msgs) => {
@@ -188,7 +182,7 @@ function extractContext() {
<header class="guide-head"> <header class="guide-head">
<h1>{{ previewGuide.topic }}</h1> <h1>{{ previewGuide.topic }}</h1>
<span class="guide-format">{{ previewGuide.format }}</span> <span class="guide-format">{{ previewGuide.format }}</span>
<span v-if="themaAbgeschlossen" class="thema-done" title="Alle FullGuide-Bausteine gemeistert (25/25)"> Thema abgeschlossen</span> <span v-if="themaAbgeschlossen" class="thema-done" title="Alle Bausteine gemeistert (18/18)"> Thema abgeschlossen</span>
</header> </header>
<section <section
v-for="(ch, ci) in content.chapters" v-for="(ch, ci) in content.chapters"

View File

@@ -47,19 +47,13 @@ const trackerItems = computed(() => {
const fmt = (k) => `${f[k]?.absolviert ?? 0}/${f[k]?.erstellt ?? 0}` const fmt = (k) => `${f[k]?.absolviert ?? 0}/${f[k]?.erstellt ?? 0}`
return [ return [
{ label: 'Themen', value: String(props.stats.themen ?? 0), title: 'Themen inkl. Projekte' }, { label: 'Themen', value: String(props.stats.themen ?? 0), title: 'Themen inkl. Projekte' },
{ label: 'MiniGuides', value: fmt('MiniGuide'), title: 'absolviert/erstellt' },
{ label: 'Guides', value: fmt('Guide'), title: 'absolviert/erstellt' }, { label: 'Guides', value: fmt('Guide'), title: 'absolviert/erstellt' },
{ label: 'ProGuides', value: fmt('ProGuide'), title: 'absolviert/erstellt' },
{ label: 'FullGuides', value: fmt('FullGuide'), title: 'absolviert/erstellt' },
] ]
}) })
const formats = [ const formats = [
{ key: 'OnePager', label: 'OnePager' }, { key: 'OnePager', label: 'OnePager' },
{ key: 'MiniGuide', label: 'MiniGuide' },
{ key: 'Guide', label: 'Guide' }, { key: 'Guide', label: 'Guide' },
{ key: 'ProGuide', label: 'ProGuide' },
{ key: 'FullGuide', label: 'FullGuide' },
] ]
const bausteineState = computed(() => { const bausteineState = computed(() => {

View File

@@ -11,7 +11,7 @@ PRÜFE auf:
- **Inhalt:** die relevanten Subbausteine kommen vor; Richtung/Kanten geben die Beziehungen korrekt wieder; nichts Erfundenes. - **Inhalt:** die relevanten Subbausteine kommen vor; Richtung/Kanten geben die Beziehungen korrekt wieder; nichts Erfundenes.
- **Mathe:** Symbole stehen als echte KaTeX-Notation in Dollarzeichen (`$\Sigma$`, `$w \in L$`), nicht als Wort ausgeschrieben. Korrigiere Verstöße. - **Mathe:** Symbole stehen als echte KaTeX-Notation in Dollarzeichen (`$\Sigma$`, `$w \in L$`), nicht als Wort ausgeschrieben. Korrigiere Verstöße.
- **Kanten-Label:** nur ein kurzes Wort (ja/nein/kodiert), **keine Formeln/Dollarzeichen** in Kanten — Formeln gehören in die Knoten. - **Kanten-Label:** nur ein kurzes Wort (ja/nein/kodiert), **keine Formeln/Dollarzeichen** in Kanten — Formeln gehören in die Knoten.
- **Knappheit:** Knotentexte kurz. - **Knappheit:** Knotentexte SEHR kurz (≤ 6 Wörter / eine kompakte Inline-Formel). **Verboten:** `\\`, `\text{…}`, `$$…$$`, mehrteilige Formeln. Kürze lange Knoten (z.B. volle Formaldefinitionen) auf den Kern ein.
Korrigiere alle Mängel. Ist die Grafik bereits gut, gib sie unverändert zurück. Korrigiere alle Mängel. Ist die Grafik bereits gut, gib sie unverändert zurück.
{extra} {extra}

View File

@@ -10,7 +10,9 @@ AUFGABE:
- Baue EINEN gerichteten Graphen: **Knoten** = die Subbausteine (plus ggf. Verbindungs-Knoten wie Start/Ende/Verzweigung), **Kanten** = ihre Beziehungen (Reihenfolge, Abhängigkeit, Ober-/Unterbegriff, Ja/Nein). - Baue EINEN gerichteten Graphen: **Knoten** = die Subbausteine (plus ggf. Verbindungs-Knoten wie Start/Ende/Verzweigung), **Kanten** = ihre Beziehungen (Reihenfolge, Abhängigkeit, Ober-/Unterbegriff, Ja/Nein).
- Decke die relevanten Subbausteine ab. Erfinde keine Inhalte außerhalb des Bausteins. - Decke die relevanten Subbausteine ab. Erfinde keine Inhalte außerhalb des Bausteins.
- Wähle die Richtung passend: `TB` (oben→unten, für Abläufe/Hierarchien) oder `LR` (links→rechts, für Ketten). - Wähle die Richtung passend: `TB` (oben→unten, für Abläufe/Hierarchien) oder `LR` (links→rechts, für Ketten).
- **Knotentext kurz** (Stichwort/kurzer Satz). Mathematik schreibst du als **echte Notation** mit KaTeX in Dollarzeichen: `$\Sigma$`, `$(L, \Sigma)$`, `$w \in L$`, `$2^n$`. Niemals Symbole als Wort ausschreiben (nicht „Sigma", nicht „Vereinigung"). - **Knotentext SEHR kurz** (ein Stichwort oder eine **kompakte** Inline-Formel, Richtwert ≤ 6 Wörter). Lange Formaldefinitionen gehören NICHT in den Graph — die stehen im Guide-Text.
- Mathematik als **echte Notation** mit KaTeX in Dollarzeichen: `$\Sigma$`, `$(L, \Sigma)$`, `$w \in L$`, `$2^n$`. Niemals Symbole als Wort ausschreiben (nicht „Sigma", nicht „Vereinigung").
- **Verboten im Knotentext:** `\\` (Zeilenumbruch), `\text{…}`, `$$…$$` (Display-Mathe), mehrteilige/verkettete Formeln. Eine Formel pro Knoten, knapp.
- Kanten-Label (`text`) ist optional und **nur ein kurzes Wort** (z.B. „kodiert", „ja", „nein") — **keine Formeln/Dollarzeichen** in Kanten. Formeln gehören in die Knoten. - Kanten-Label (`text`) ist optional und **nur ein kurzes Wort** (z.B. „kodiert", „ja", „nein") — **keine Formeln/Dollarzeichen** in Kanten. Formeln gehören in die Knoten.
- Jede `id` ist ein kurzes Kürzel (az, 09), eindeutig. Jede Kante verweist mit `von`/`nach` auf existierende Knoten-`id`s. - Jede `id` ist ein kurzes Kürzel (az, 09), eindeutig. Jede Kante verweist mit `von`/`nach` auf existierende Knoten-`id`s.
{extra} {extra}