update
This commit is contained in:
@@ -69,6 +69,8 @@ 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 = {
|
||||||
"Guide": (0.0, 1.0, 0, "einen fokussierten Guide — alles Relevante ohne Randthemen"),
|
"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.
|
# Provider-Stacks: komplett unabhängig, einer kann jederzeit entfernt werden.
|
||||||
|
|||||||
@@ -422,15 +422,21 @@ 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: „Guide" wählt darüber aus
|
# Subbausteine je Baustein (Sidecar) — früh geladen: steuert Auswahl + Sub-Filter je Format.
|
||||||
# (Bausteine mit ≥1 relevantem Sub) und filtert später Rand-Subs. Fehlt sie → {} (Fallback).
|
# Fehlt sie → {} (Fallback: Guide nimmt alles).
|
||||||
subs_raw = _load_subbausteine(topic)
|
subs_raw = _load_subbausteine(topic)
|
||||||
|
|
||||||
# Schritt 0: Auswahl. „Guide" ist deterministisch — alle Bausteine mit ≥1
|
def _hat_relevanz(num, art):
|
||||||
# relevantem Subbaustein. Fallback (keine Relevanz-Daten): alle Bausteine.
|
return any(isinstance(s, dict) and s.get("relevanz") == art for s in subs_raw.get(_titel(entries[num]), []))
|
||||||
if format_name == "Guide":
|
|
||||||
auswahl = [num for num in entries
|
# Schritt 0: Auswahl. Deterministisch je Format (Roadmap hat einen eigenen Weg):
|
||||||
if any(s.get("relevanz") == "relevant" for s in subs_raw.get(_titel(entries[num]), []))]
|
# 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:
|
if not auswahl:
|
||||||
_log(topic, "Guide: keine Relevanz-Daten — alle Bausteine genommen")
|
_log(topic, "Guide: keine Relevanz-Daten — alle Bausteine genommen")
|
||||||
auswahl = list(entries)
|
auswahl = list(entries)
|
||||||
@@ -442,6 +448,9 @@ async def _generate_sections(
|
|||||||
)
|
)
|
||||||
if auswahl is None:
|
if auswahl is None:
|
||||||
return 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}
|
sel_entries = {num: entries[num] for num in auswahl}
|
||||||
soll = len(sel_entries)
|
soll = len(sel_entries)
|
||||||
@@ -523,13 +532,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 dem Guide (Fokus auf
|
# Subbausteine je Baustein, je Format gefiltert: Guide=relevant · FullGuide=alle · Rest=Rand.
|
||||||
# Relevantes). Subs ohne Relevanz-Feld (Alt-Themen) bleiben erhalten.
|
# Subs ohne Relevanz-Feld (Alt-Themen) zählen als „nicht Rand" (→ Guide/FullGuide).
|
||||||
# Fehlt die Sidecar → leer → Inhalte/Writer laufen auf Baustein-Granularität.
|
if format_name == "FullGuide":
|
||||||
subs_by_titel = {
|
subs_by_titel = {t: list(subs) for t, subs in subs_raw.items()}
|
||||||
t: [s for s in subs if s.get("relevanz") != "rand"]
|
elif format_name == "Rest":
|
||||||
for t, subs in subs_raw.items()
|
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}
|
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]
|
||||||
@@ -784,12 +794,22 @@ async def _generate_sections(
|
|||||||
break
|
break
|
||||||
scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums]
|
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…")
|
await _set_progress(guide_id, "Setze zusammen…")
|
||||||
chapters: list[dict] = []
|
chapters: list[dict] = []
|
||||||
for ch in plan:
|
for ch in plan:
|
||||||
sections = [
|
sections = [
|
||||||
{"num": num, "title": _titel(entries[num]), "md": by_num[num]["md"],
|
{"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
|
for num in ch["nums"] if num in by_num
|
||||||
]
|
]
|
||||||
if sections:
|
if sections:
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from typing import Literal
|
|||||||
FormatType = Literal[
|
FormatType = Literal[
|
||||||
"Roadmap",
|
"Roadmap",
|
||||||
"Guide",
|
"Guide",
|
||||||
|
"FullGuide",
|
||||||
|
"Rest",
|
||||||
]
|
]
|
||||||
|
|
||||||
ProviderType = Literal["claude", "minimax", "lokal"]
|
ProviderType = Literal["claude", "minimax", "lokal"]
|
||||||
|
|||||||
@@ -478,7 +478,7 @@ async def list_all():
|
|||||||
async def guide_locks(topic: str):
|
async def guide_locks(topic: str):
|
||||||
"""Sperr-Gründe pro Format für den ▶-Button — None = erstellbar."""
|
"""Sperr-Gründe pro Format für den ▶-Button — None = erstellbar."""
|
||||||
guides, progress, levels = await lade_lernstand()
|
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)
|
@router.get("/guides/{guide_id}", response_model=GuideResponse)
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ async function loadBausteine() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const FORMAT_ORDER = ['Guide', 'Roadmap']
|
const FORMAT_ORDER = ['Guide', 'FullGuide', 'Roadmap', 'Rest']
|
||||||
|
|
||||||
function autoPreview() {
|
function autoPreview() {
|
||||||
const map = doneByFormat.value
|
const map = doneByFormat.value
|
||||||
|
|||||||
@@ -74,13 +74,19 @@ const bausteine = computed(() =>
|
|||||||
const fokusBaustein = computed(() => (fokusIndex.value === null ? null : bausteine.value[fokusIndex.value]))
|
const fokusBaustein = computed(() => (fokusIndex.value === null ? null : bausteine.value[fokusIndex.value]))
|
||||||
|
|
||||||
function openFokus(baustein, tab) {
|
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)
|
const i = bausteine.value.findIndex((s) => s.title === baustein.title)
|
||||||
if (i === -1) return
|
if (i === -1) return
|
||||||
fokusIndex.value = i
|
fokusIndex.value = i
|
||||||
fokusTab.value = tab || 'pruefung'
|
fokusTab.value = tab || 'pruefung'
|
||||||
}
|
}
|
||||||
function fokusPrev() { if (fokusIndex.value > 0) fokusIndex.value-- }
|
// Nur prüfbare Sections im Fokus anspringen (FullGuide hat dazwischen reine Lese-Sections).
|
||||||
function fokusNext() { if (fokusIndex.value < bausteine.value.length - 1) fokusIndex.value++ }
|
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).
|
// Erfahrungsleiste: Lernstand über alle Bausteine des Guides (sticky, absolviert ⊇ verstanden ⊇ gemeistert).
|
||||||
const fortschritt = computed(() => {
|
const fortschritt = computed(() => {
|
||||||
@@ -94,8 +100,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 }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ein Format „Guide" → Prüfung läuft voll 0–30. Ohne fertigen Guide kein Deckel relevant (6).
|
// Prüfung läuft voll 0–30, sobald Guide ODER FullGuide fertig ist (gleicher Baustein-Score).
|
||||||
const cap = computed(() => (props.doneByFormat?.Guide ? 30 : 6))
|
const cap = computed(() => ((props.doneByFormat?.Guide || props.doneByFormat?.FullGuide) ? 30 : 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) => {
|
||||||
@@ -202,15 +208,18 @@ function extractContext() {
|
|||||||
:key="s.num"
|
:key="s.num"
|
||||||
:class="['section-card', lernstand[s.title]?.gemeistert ? 'gemeistert' : (lernstand[s.title]?.verstanden ? 'verstanden' : (lernstand[s.title]?.absolviert ? 'absolviert' : ''))]"
|
:class="['section-card', lernstand[s.title]?.gemeistert ? 'gemeistert' : (lernstand[s.title]?.verstanden ? 'verstanden' : (lernstand[s.title]?.absolviert ? 'absolviert' : ''))]"
|
||||||
>
|
>
|
||||||
<h3 class="baustein-klick" @click="openFokus(s, 'pruefung')">
|
<h3 :class="{ 'baustein-klick': s.pruefbar }" @click="s.pruefbar && openFokus(s, 'pruefung')">
|
||||||
{{ s.title }}
|
{{ s.title }}
|
||||||
|
<template v-if="s.pruefbar">
|
||||||
<span v-if="lernstand[s.title]?.gemeistert" class="baustein-done gemeistert" title="Gemeistert (25/25)">✓✓✓ Gemeistert</span>
|
<span v-if="lernstand[s.title]?.gemeistert" class="baustein-done gemeistert" title="Gemeistert (25/25)">✓✓✓ Gemeistert</span>
|
||||||
<span v-else-if="lernstand[s.title]?.verstanden" class="baustein-done verstanden" title="Vollständig verstanden (10/10)">✓✓ Verstanden</span>
|
<span v-else-if="lernstand[s.title]?.verstanden" class="baustein-done verstanden" title="Vollständig verstanden (10/10)">✓✓ Verstanden</span>
|
||||||
<span v-else-if="lernstand[s.title]?.absolviert" class="baustein-done" title="Prüfung bestanden">✓ Absolviert</span>
|
<span v-else-if="lernstand[s.title]?.absolviert" class="baustein-done" title="Prüfung bestanden">✓ Absolviert</span>
|
||||||
|
</template>
|
||||||
</h3>
|
</h3>
|
||||||
<GraphView v-if="ansichtModus === 'grafik'" :graph="grafiken[s.title] || null" />
|
<GraphView v-if="ansichtModus === 'grafik'" :graph="grafiken[s.title] || null" />
|
||||||
<div v-else class="section-body markdown" v-html="renderMarkdown(ansichtModus === 'kompakt' ? (s.kompakt || s.md) : s.md)"></div>
|
<div v-else class="section-body markdown" v-html="renderMarkdown(ansichtModus === 'kompakt' ? (s.kompakt || s.md) : s.md)"></div>
|
||||||
<BausteinPanel
|
<BausteinPanel
|
||||||
|
v-if="s.pruefbar"
|
||||||
mode="trigger"
|
mode="trigger"
|
||||||
:baustein="s.title"
|
:baustein="s.title"
|
||||||
:status="lernstand[s.title]"
|
:status="lernstand[s.title]"
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ const trackerItems = computed(() => {
|
|||||||
const formats = [
|
const formats = [
|
||||||
{ key: 'Roadmap', label: 'Roadmap' },
|
{ key: 'Roadmap', label: 'Roadmap' },
|
||||||
{ key: 'Guide', label: 'Guide' },
|
{ key: 'Guide', label: 'Guide' },
|
||||||
|
{ key: 'FullGuide', label: 'FullGuide' },
|
||||||
|
{ key: 'Rest', label: 'Rest' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const bausteineState = computed(() => {
|
const bausteineState = computed(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user