update
This commit is contained in:
@@ -414,17 +414,22 @@ async def set_baustein_absolviert(topic: str, baustein: str) -> bool:
|
|||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
async def list_baustein_absolviert_all() -> dict[str, set[str]]:
|
async def list_baustein_levels_all() -> dict[str, dict[str, set[str]]]:
|
||||||
"""Alle absolvierten Bausteine in einem Query: topic → Baustein-Titel."""
|
"""Bausteine je Meilenstein in EINER Query: {"absolviert"/"verstanden"/"gemeistert": {topic → {Titel}}}."""
|
||||||
db = await get_db()
|
db = await get_db()
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT topic, baustein FROM baustein_progress WHERE absolviert IS NOT NULL"
|
"SELECT topic, baustein, absolviert, verstanden, gemeistert FROM baustein_progress"
|
||||||
)
|
)
|
||||||
rows = await cursor.fetchall()
|
rows = await cursor.fetchall()
|
||||||
out: dict[str, set[str]] = {}
|
levels: dict[str, dict[str, set[str]]] = {"absolviert": {}, "verstanden": {}, "gemeistert": {}}
|
||||||
for topic, baustein in rows:
|
for topic, baustein, absolviert, verstanden, gemeistert in rows:
|
||||||
out.setdefault(topic, set()).add(baustein)
|
if absolviert is not None:
|
||||||
return out
|
levels["absolviert"].setdefault(topic, set()).add(baustein)
|
||||||
|
if verstanden is not None:
|
||||||
|
levels["verstanden"].setdefault(topic, set()).add(baustein)
|
||||||
|
if gemeistert is not None:
|
||||||
|
levels["gemeistert"].setdefault(topic, set()).add(baustein)
|
||||||
|
return levels
|
||||||
|
|
||||||
|
|
||||||
async def delete_baustein_daten(topic: str) -> None:
|
async def delete_baustein_daten(topic: str) -> None:
|
||||||
|
|||||||
@@ -12,26 +12,29 @@ Query-Schleifen mehr pro Guide.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from database import list_baustein_absolviert_all, list_guides, list_progress_all
|
from database import list_baustein_levels_all, list_guides, list_progress_all
|
||||||
from guide import guide_slot_dateien
|
from guide import guide_slot_dateien
|
||||||
from paths import bausteine_path, guide_content_path
|
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
|
||||||
VORSTUFE = {"Guide": "MiniGuide", "FullGuide": "Guide"}
|
VORSTUFE = {"Guide": "MiniGuide", "FullGuide": "Guide"}
|
||||||
|
# Welches Niveau die Vorstufe erreichen muss, um das Format freizuschalten.
|
||||||
|
FREISCHALT_LEVEL = {"Guide": "absolviert", "FullGuide": "verstanden"}
|
||||||
FORMATE = ("MiniGuide", "Guide", "FullGuide")
|
FORMATE = ("MiniGuide", "Guide", "FullGuide")
|
||||||
|
|
||||||
|
|
||||||
async def lade_lernstand() -> tuple[list[dict], dict[str, set[str]], dict[str, set[str]]]:
|
async def lade_lernstand() -> tuple[list[dict], dict[str, set[str]], dict[str, dict[str, set[str]]]]:
|
||||||
"""Guides + Kapitel-Fortschritt + absolvierte Bausteine in drei Queries.
|
"""Guides + Kapitel-Fortschritt + Bausteine je Meilenstein.
|
||||||
|
|
||||||
bausteine_done: topic → normalisierte Titel der Bausteine mit bestandener Prüfung.
|
levels: {"absolviert"/"verstanden"/"gemeistert": {topic → normalisierte Titel}}.
|
||||||
"""
|
"""
|
||||||
bausteine_done = {
|
roh = await list_baustein_levels_all()
|
||||||
topic: {_norm_titel(b) for b in titel}
|
levels = {
|
||||||
for topic, titel in (await list_baustein_absolviert_all()).items()
|
stufe: {topic: {_norm_titel(b) for b in titel} for topic, titel in pro_topic.items()}
|
||||||
|
for stufe, pro_topic in roh.items()
|
||||||
}
|
}
|
||||||
return await list_guides(), await list_progress_all(), bausteine_done
|
return await list_guides(), await list_progress_all(), levels
|
||||||
|
|
||||||
|
|
||||||
def _content_json(topic: str, fmt: str) -> dict | None:
|
def _content_json(topic: str, fmt: str) -> dict | None:
|
||||||
@@ -73,31 +76,42 @@ def _neueste_done(guides: list[dict], fmt: str) -> dict[str, dict]:
|
|||||||
return neueste
|
return neueste
|
||||||
|
|
||||||
|
|
||||||
def _guide_absolviert(g: dict, progress: dict[str, set[str]], bausteine_done: dict[str, set[str]]) -> bool:
|
def _guide_alle(g: dict, progress: dict[str, set[str]], levelset: dict[str, set[str]]) -> bool:
|
||||||
|
"""Sind ALLE Bausteine (bzw. OnePager-Kapitel) des Guides auf dem geforderten Niveau?"""
|
||||||
if g["format"] == "OnePager":
|
if g["format"] == "OnePager":
|
||||||
titles = _kapitel_titel(g["topic"], g["format"])
|
titles = _kapitel_titel(g["topic"], g["format"])
|
||||||
return bool(titles) and titles <= progress.get(g["id"], set())
|
return bool(titles) and titles <= progress.get(g["id"], set())
|
||||||
sections = _section_titel(g["topic"], g["format"])
|
sections = _section_titel(g["topic"], g["format"])
|
||||||
return bool(sections) and sections <= bausteine_done.get(g["topic"], set())
|
return bool(sections) and sections <= levelset.get(g["topic"], set())
|
||||||
|
|
||||||
|
|
||||||
def ist_absolviert(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], bausteine_done: dict[str, set[str]]) -> bool:
|
def ist_level(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], levelset: dict[str, set[str]]) -> bool:
|
||||||
"""Alle Bausteine des neuesten fertigen Guides (Thema+Format) per Prüfung absolviert?"""
|
"""Neuester fertiger Guide (Thema+Format): alle Bausteine auf dem Niveau von levelset?"""
|
||||||
g = _neueste_done(guides, fmt).get(topic)
|
g = _neueste_done(guides, fmt).get(topic)
|
||||||
return g is not None and _guide_absolviert(g, progress, bausteine_done)
|
return g is not None and _guide_alle(g, progress, levelset)
|
||||||
|
|
||||||
|
|
||||||
def formate_stats(guides: list[dict], progress: dict[str, set[str]], bausteine_done: dict[str, set[str]]) -> dict:
|
def ist_absolviert(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> bool:
|
||||||
|
"""Alle Bausteine des neuesten fertigen Guides absolviert (≥3)?"""
|
||||||
|
return ist_level(topic, fmt, guides, progress, levels["absolviert"])
|
||||||
|
|
||||||
|
|
||||||
|
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)?"""
|
||||||
|
return ist_level(topic, "FullGuide", guides, progress, levels["gemeistert"])
|
||||||
|
|
||||||
|
|
||||||
|
def formate_stats(guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> dict:
|
||||||
"""Pro Format erstellt/absolviert — pro Thema zählt nur der neueste fertige Guide."""
|
"""Pro Format erstellt/absolviert — pro Thema zählt nur der neueste fertige Guide."""
|
||||||
formate = {}
|
formate = {}
|
||||||
for fmt in FORMATE:
|
for fmt in FORMATE:
|
||||||
neueste = _neueste_done(guides, fmt)
|
neueste = _neueste_done(guides, fmt)
|
||||||
absolviert = sum(1 for g in neueste.values() if _guide_absolviert(g, progress, bausteine_done))
|
absolviert = sum(1 for g in neueste.values() if _guide_alle(g, progress, levels["absolviert"]))
|
||||||
formate[fmt] = {"erstellt": len(neueste), "absolviert": absolviert}
|
formate[fmt] = {"erstellt": len(neueste), "absolviert": absolviert}
|
||||||
return formate
|
return formate
|
||||||
|
|
||||||
|
|
||||||
def guide_lock(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], bausteine_done: dict[str, set[str]]) -> str | None:
|
def guide_lock(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> str | None:
|
||||||
"""Grund, warum ein Neu-Start für Thema+Format gesperrt ist — None = erlaubt.
|
"""Grund, warum ein Neu-Start für Thema+Format gesperrt ist — None = erlaubt.
|
||||||
|
|
||||||
Exakt die Regeln aus POST /guides: Bausteine nötig, kein Duplikat-Start,
|
Exakt die Regeln aus POST /guides: Bausteine nötig, kein Duplikat-Start,
|
||||||
@@ -111,9 +125,12 @@ def guide_lock(topic: str, fmt: str, guides: list[dict], progress: dict[str, set
|
|||||||
content = guide_content_path(topic, fmt)
|
content = guide_content_path(topic, fmt)
|
||||||
if fmt != "OnePager" and not content.exists() and not guide_slot_dateien(content):
|
if fmt != "OnePager" and not content.exists() and not guide_slot_dateien(content):
|
||||||
vorstufe = VORSTUFE.get(fmt)
|
vorstufe = VORSTUFE.get(fmt)
|
||||||
if vorstufe and not ist_absolviert(topic, vorstufe, guides, progress, bausteine_done):
|
if vorstufe:
|
||||||
return f"Erst den {vorstufe} dieses Themas absolvieren (alle Bausteine prüfen)"
|
stufe = FREISCHALT_LEVEL[fmt] # "absolviert" (alle 3) oder "verstanden" (alle 10)
|
||||||
stat = formate_stats(guides, progress, bausteine_done).get(fmt, {"erstellt": 0, "absolviert": 0})
|
if not ist_level(topic, vorstufe, guides, progress, levels[stufe]):
|
||||||
|
wort = "verstehen (alle Bausteine auf 10)" if stufe == "verstanden" else "absolvieren (alle Bausteine prüfen)"
|
||||||
|
return f"Erst den {vorstufe} dieses Themas {wort}"
|
||||||
|
stat = formate_stats(guides, progress, levels).get(fmt, {"erstellt": 0, "absolviert": 0})
|
||||||
offen = stat["erstellt"] - stat["absolviert"]
|
offen = stat["erstellt"] - stat["absolviert"]
|
||||||
if offen >= MAX_OFFENE_GUIDES:
|
if offen >= MAX_OFFENE_GUIDES:
|
||||||
return f"Erst {fmt}s absolvieren — maximal {MAX_OFFENE_GUIDES} offene erlaubt ({offen} offen)"
|
return f"Erst {fmt}s absolvieren — maximal {MAX_OFFENE_GUIDES} offene erlaubt ({offen} offen)"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from elements import generate_element, chat_with_guide, chat_with_element, check
|
|||||||
from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_frage, score_berechnen, vertiefung_generieren
|
from lernen import NOETIG, MASTERY, MEISTERN, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_frage, score_berechnen, vertiefung_generieren
|
||||||
from guide import generate_guide, guide_slot_dateien
|
from guide import generate_guide, guide_slot_dateien
|
||||||
from pipeline import cancel_guide
|
from pipeline import cancel_guide
|
||||||
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand
|
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
||||||
from models import (
|
from models import (
|
||||||
GuideCreateRequest, GuideResponse,
|
GuideCreateRequest, GuideResponse,
|
||||||
TopicCreateRequest,
|
TopicCreateRequest,
|
||||||
@@ -59,18 +59,20 @@ async def get_topics():
|
|||||||
@router.get("/stats")
|
@router.get("/stats")
|
||||||
async def get_stats():
|
async def get_stats():
|
||||||
"""Tracker: Themen-Anzahl + pro Format erstellt/absolviert."""
|
"""Tracker: Themen-Anzahl + pro Format erstellt/absolviert."""
|
||||||
guides, progress, bausteine_done = await lade_lernstand()
|
guides, progress, levels = await lade_lernstand()
|
||||||
themen = set(await db_list_topics()) | {g["topic"] for g in guides} | set(bausteine_topics())
|
themen = set(await db_list_topics()) | {g["topic"] for g in guides} | set(bausteine_topics())
|
||||||
if PROJECTS_DIR.is_dir():
|
if PROJECTS_DIR.is_dir():
|
||||||
themen |= {e.name for e in PROJECTS_DIR.iterdir() if e.is_dir()}
|
themen |= {e.name for e in PROJECTS_DIR.iterdir() if e.is_dir()}
|
||||||
return {"themen": len(themen), "formate": formate_stats(guides, progress, bausteine_done)}
|
return {"themen": len(themen), "formate": formate_stats(guides, progress, levels)}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/topics/fortschritt")
|
@router.get("/topics/fortschritt")
|
||||||
async def topic_fortschritt(topic: str):
|
async def topic_fortschritt(topic: str):
|
||||||
"""Absolviert-Status pro Format — fürs Freischalten der nächsten Ausbaustufe."""
|
"""Absolviert-Status pro Format + Themen-Abschluss — fürs Freischalten der nächsten Ausbaustufe."""
|
||||||
guides, progress, bausteine_done = await lade_lernstand()
|
guides, progress, levels = await lade_lernstand()
|
||||||
return {fmt: ist_absolviert(topic, fmt, guides, progress, bausteine_done) for fmt in FORMATE}
|
status = {fmt: ist_absolviert(topic, fmt, guides, progress, levels) for fmt in FORMATE}
|
||||||
|
status["abgeschlossen"] = thema_abgeschlossen(topic, guides, progress, levels)
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
@router.post("/topics")
|
@router.post("/topics")
|
||||||
@@ -270,8 +272,8 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest):
|
|||||||
|
|
||||||
@router.post("/guides", response_model=GuideResponse)
|
@router.post("/guides", response_model=GuideResponse)
|
||||||
async def create(req: GuideCreateRequest):
|
async def create(req: GuideCreateRequest):
|
||||||
guides, progress, bausteine_done = await lade_lernstand()
|
guides, progress, levels = await lade_lernstand()
|
||||||
grund = guide_lock(req.topic.strip(), req.format, guides, progress, bausteine_done)
|
grund = guide_lock(req.topic.strip(), req.format, guides, progress, levels)
|
||||||
if grund:
|
if grund:
|
||||||
raise HTTPException(400 if grund == "Erst Bausteine erstellen" else 409, grund)
|
raise HTTPException(400 if grund == "Erst Bausteine erstellen" else 409, grund)
|
||||||
await create_topic(req.topic.strip())
|
await create_topic(req.topic.strip())
|
||||||
@@ -299,8 +301,8 @@ async def list_all():
|
|||||||
@router.get("/guides/locks")
|
@router.get("/guides/locks")
|
||||||
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, bausteine_done = await lade_lernstand()
|
guides, progress, levels = await lade_lernstand()
|
||||||
return {fmt: guide_lock(topic, fmt, guides, progress, bausteine_done) for fmt in ("OnePager", *FORMATE)}
|
return {fmt: guide_lock(topic, fmt, guides, progress, levels) for fmt in ("OnePager", *FORMATE)}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/guides/{guide_id}", response_model=GuideResponse)
|
@router.get("/guides/{guide_id}", response_model=GuideResponse)
|
||||||
|
|||||||
@@ -392,6 +392,8 @@ onMounted(async () => {
|
|||||||
:dark="darkMode"
|
:dark="darkMode"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
:elementsOpen="elementsOpen"
|
:elementsOpen="elementsOpen"
|
||||||
|
:doneByFormat="doneByFormat"
|
||||||
|
:themaAbgeschlossen="!!fortschritt.abgeschlossen"
|
||||||
@progressChanged="loadStats(); loadBausteine()"
|
@progressChanged="loadStats(); loadBausteine()"
|
||||||
/>
|
/>
|
||||||
<div v-else class="empty-main">
|
<div v-else class="empty-main">
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const props = defineProps({
|
|||||||
dark: { type: Boolean, default: false },
|
dark: { type: Boolean, default: false },
|
||||||
provider: { type: String, default: 'claude' },
|
provider: { type: String, default: 'claude' },
|
||||||
elementsOpen: { type: Boolean, default: false }, // Element-Sidebar offen → Chat nach links
|
elementsOpen: { type: Boolean, default: false }, // Element-Sidebar offen → Chat nach links
|
||||||
|
doneByFormat: { type: Object, default: () => ({}) }, // Format → fertiger Guide (Themen-bezogen)
|
||||||
|
themaAbgeschlossen: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['progressChanged'])
|
const emit = defineEmits(['progressChanged'])
|
||||||
@@ -55,16 +57,9 @@ function onBausteinStatus(baustein, status) {
|
|||||||
if (status.absolviert && !warAbsolviert) emit('progressChanged') // Locks/Stats neu laden
|
if (status.absolviert && !warAbsolviert) emit('progressChanged') // Locks/Stats neu laden
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 2 (Score 3→10) frei, sobald ALLE Bausteine absolviert; Tier 3 (Meisterpfad 10→25) frei, sobald ALLE verstanden.
|
// Cap pro Baustein folgt den erstellten Formaten: Guide vorhanden → bis 10, FullGuide → bis 25.
|
||||||
const guideSections = computed(() => (content.value?.chapters || []).flatMap((ch) => ch.sections))
|
const tier2 = computed(() => !!props.doneByFormat?.Guide)
|
||||||
const guideAbsolviert = computed(() => {
|
const tier3 = computed(() => !!props.doneByFormat?.FullGuide)
|
||||||
const secs = guideSections.value
|
|
||||||
return secs.length > 0 && secs.every((s) => lernstand.value[s.title]?.absolviert)
|
|
||||||
})
|
|
||||||
const guideVerstanden = computed(() => {
|
|
||||||
const secs = guideSections.value
|
|
||||||
return secs.length > 0 && secs.every((s) => lernstand.value[s.title]?.verstanden)
|
|
||||||
})
|
|
||||||
|
|
||||||
// --- Chat (Mechanik in useChat; Kontext-Extraktion bleibt hier) ---
|
// --- Chat (Mechanik in useChat; Kontext-Extraktion bleibt hier) ---
|
||||||
const chat = useChat((msgs) => {
|
const chat = useChat((msgs) => {
|
||||||
@@ -152,6 +147,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>
|
||||||
</header>
|
</header>
|
||||||
<section
|
<section
|
||||||
v-for="(ch, ci) in content.chapters"
|
v-for="(ch, ci) in content.chapters"
|
||||||
@@ -181,8 +177,8 @@ function extractContext() {
|
|||||||
:section="s.md"
|
:section="s.md"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
:status="lernstand[s.title]"
|
:status="lernstand[s.title]"
|
||||||
:tier2="guideAbsolviert"
|
:tier2="tier2"
|
||||||
:tier3="guideVerstanden"
|
:tier3="tier3"
|
||||||
@status-changed="(st) => onBausteinStatus(s.title, st)"
|
@status-changed="(st) => onBausteinStatus(s.title, st)"
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
@@ -283,6 +279,16 @@ function extractContext() {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thema-done {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.15rem 0.6rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in srgb, #d4af37 20%, var(--panel));
|
||||||
|
border: 1px solid #d4af37;
|
||||||
|
color: #8a6d12;
|
||||||
|
}
|
||||||
|
|
||||||
.chapter {
|
.chapter {
|
||||||
margin-bottom: 2.5rem;
|
margin-bottom: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user