From e7af2b1150e9e86956bccdf2d114d6675004f176 Mon Sep 17 00:00:00 2001 From: Team3 Date: Mon, 1 Jun 2026 18:22:13 +0200 Subject: [PATCH] update --- backend/database.py | 42 +++++++++++ backend/models.py | 9 +++ backend/routes.py | 20 ++++++ frontend/src/api.js | 14 ++++ frontend/src/components/TopicDetail.vue | 92 +++++++++++++++++++++---- 5 files changed, 165 insertions(+), 12 deletions(-) diff --git a/backend/database.py b/backend/database.py index 8810ed4..90c3be1 100644 --- a/backend/database.py +++ b/backend/database.py @@ -43,6 +43,15 @@ CREATE TABLE IF NOT EXISTS baustein_suggestions ( ) """ +CREATE_PROGRESS = """ +CREATE TABLE IF NOT EXISTS guide_progress ( + guide_id TEXT NOT NULL, + chapter TEXT NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (guide_id, chapter) +) +""" + _db: aiosqlite.Connection | None = None @@ -59,6 +68,7 @@ async def init_db(): await db.execute(CREATE_GUIDES) await db.execute(CREATE_BAUSTEINE) await db.execute(CREATE_SUGGESTIONS) + await db.execute(CREATE_PROGRESS) cursor = await db.execute("PRAGMA table_info(guides)") columns = {row[1] for row in await cursor.fetchall()} if "instructions" not in columns: @@ -249,3 +259,35 @@ async def delete_pending_suggestions(topic: str) -> None: "DELETE FROM baustein_suggestions WHERE topic = ? AND status = 'pending'", (topic,) ) await db.commit() + + +# --- Kapitel-Fortschritt --- + +async def list_progress(guide_id: str) -> list[str]: + db = await get_db() + cursor = await db.execute( + "SELECT chapter FROM guide_progress WHERE guide_id = ?", (guide_id,) + ) + rows = await cursor.fetchall() + return [row[0] for row in rows] + + +async def set_progress(guide_id: str, chapter: str, done: bool) -> None: + from datetime import datetime, timezone + db = await get_db() + if done: + await db.execute( + "INSERT OR IGNORE INTO guide_progress (guide_id, chapter, created_at) VALUES (?, ?, ?)", + (guide_id, chapter, datetime.now(timezone.utc).isoformat()), + ) + else: + await db.execute( + "DELETE FROM guide_progress WHERE guide_id = ? AND chapter = ?", (guide_id, chapter) + ) + await db.commit() + + +async def delete_progress(guide_id: str) -> None: + db = await get_db() + await db.execute("DELETE FROM guide_progress WHERE guide_id = ?", (guide_id,)) + await db.commit() diff --git a/backend/models.py b/backend/models.py index 6b79d3a..8592e16 100644 --- a/backend/models.py +++ b/backend/models.py @@ -90,3 +90,12 @@ class GuideChatRequest(BaseModel): class GuideChatResponse(BaseModel): reply: str + + +class ProgressUpdate(BaseModel): + chapter: str = Field(min_length=1, max_length=100) + done: bool + + +class ProgressResponse(BaseModel): + chapters: list[str] diff --git a/backend/routes.py b/backend/routes.py index e37878f..22ab8db 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -10,6 +10,7 @@ from database import ( create_guide, delete_guide, get_guide, list_guides, create_baustein as db_create_baustein, list_bausteine, get_baustein, delete_baustein as db_delete_baustein, list_suggestions, get_suggestion, update_suggestion, delete_suggestion, + list_progress, set_progress, delete_progress, ) from generator import generate_guide, rework_guide, cancel_guide, generate_suggestions, generate_baustein_detail, rework_baustein, sort_bausteine, suggest_topics, chat_with_guide, is_suggestions_generating, is_sorting from models import ( @@ -17,6 +18,7 @@ from models import ( BausteinCreateRequest, BausteinReworkRequest, BausteinSortRequest, BausteinResponse, SuggestionResponse, TopicSuggestRequest, TopicSuggestion, GuideChatRequest, GuideChatResponse, + ProgressUpdate, ProgressResponse, ) from paths import final_paths @@ -131,10 +133,28 @@ async def remove(guide_id: str): html_path, pdf_path = final_paths(guide["topic"], guide["format"]) html_path.unlink(missing_ok=True) pdf_path.unlink(missing_ok=True) + await delete_progress(guide_id) await delete_guide(guide_id) return {"ok": True} +@router.get("/guides/{guide_id}/progress", response_model=ProgressResponse) +async def get_progress(guide_id: str): + guide = await get_guide(guide_id) + if guide is None: + raise HTTPException(404, "Guide nicht gefunden") + return {"chapters": await list_progress(guide_id)} + + +@router.post("/guides/{guide_id}/progress", response_model=ProgressResponse) +async def update_progress(guide_id: str, req: ProgressUpdate): + guide = await get_guide(guide_id) + if guide is None: + raise HTTPException(404, "Guide nicht gefunden") + await set_progress(guide_id, req.chapter, req.done) + return {"chapters": await list_progress(guide_id)} + + # --- Bausteine --- @router.get("/bausteine", response_model=list[BausteinResponse]) diff --git a/frontend/src/api.js b/frontend/src/api.js index a033adf..8547ac9 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -44,6 +44,20 @@ export function htmlUrl(id) { return `${BASE}/guides/${id}/html` } +export async function fetchProgress(id) { + const res = await fetch(`${BASE}/guides/${id}/progress`) + return res.json() +} + +export async function setProgress(id, chapter, done) { + const res = await fetch(`${BASE}/guides/${id}/progress`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ chapter, done }), + }) + return res.json() +} + export async function chatGuide(id, { section, outline, messages }) { const res = await fetch(`${BASE}/guides/${id}/chat`, { method: 'POST', diff --git a/frontend/src/components/TopicDetail.vue b/frontend/src/components/TopicDetail.vue index 909418a..49956a3 100644 --- a/frontend/src/components/TopicDetail.vue +++ b/frontend/src/components/TopicDetail.vue @@ -2,7 +2,7 @@ import { computed, ref, nextTick } from 'vue' import { marked } from 'marked' import DOMPurify from 'dompurify' -import { htmlUrl, chatGuide } from '../api.js' +import { htmlUrl, chatGuide, fetchProgress, setProgress } from '../api.js' marked.setOptions({ breaks: true, gfm: true }) @@ -19,25 +19,93 @@ const isLandscape = computed(() => LANDSCAPE_FORMATS.includes(props.previewGuide const frameEl = ref(null) -function injectPadding(e) { - if (isLandscape.value) return +// --- Kapitel-Fortschritt --- +function onFrameLoad(e) { const doc = e.target.contentDocument if (!doc) return + injectStyles(doc) + setupProgress(doc) +} + +function injectStyles(doc) { const style = doc.createElement('style') + const portrait = !isLandscape.value + ? `html { background: #f0f1f4; } + body { + zoom: 1.15; + max-width: 1000px; + margin: 0 auto; + padding: 2.5rem 3.5rem; + background: #fff; + box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08); + }` + : '' style.textContent = `@media screen { - html { background: #f0f1f4; } - body { - zoom: 1.15; - max-width: 1000px; - margin: 0 auto; - padding: 2.5rem 3.5rem; - background: #fff; - box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08); + ${portrait} + .ch-toggle { + display: block; + width: 100%; + margin: 2.5rem 0 0.5rem; + padding: 0.9rem 1rem; + border: 1.5px dashed #c7ccd6; + border-radius: 10px; + background: #f8f9fb; + color: #4b5563; + font: 600 0.95rem/1.2 -apple-system, "Segoe UI", Roboto, sans-serif; + text-align: center; + cursor: pointer; + transition: all 0.12s; } + .ch-toggle:hover { border-color: #6366f1; color: #4f46e5; background: #ede9fe; } + .ch-toggle.is-done { + border-style: solid; + border-color: #34d399; + background: #d1fae5; + color: #065f46; + } + section.chapter.ch-complete > :not(.ch-toggle) { opacity: 0.4; } }` doc.head?.appendChild(style) } +async function setupProgress(doc) { + const guideId = props.previewGuide?.id + const chapters = Array.from(doc.querySelectorAll('section.chapter')) + if (!guideId || !chapters.length) return + + let done = new Set() + try { + const res = await fetchProgress(guideId) + done = new Set(res.chapters || []) + } catch { /* offline → leer */ } + + chapters.forEach((section, i) => { + if (section.querySelector(':scope > .ch-toggle')) return // Guard gegen Doppel-Inject + const numEl = section.querySelector('.chapter-num') + const key = (numEl?.textContent.match(/\d+/)?.[0]) || String(i + 1) + + const toggle = doc.createElement('div') + toggle.className = 'ch-toggle' + const apply = (isDone) => { + toggle.classList.toggle('is-done', isDone) + section.classList.toggle('ch-complete', isDone) + toggle.textContent = isDone ? '✓ Erledigt – rückgängig' : 'Kapitel als erledigt markieren' + } + apply(done.has(key)) + + toggle.addEventListener('click', async () => { + const newState = !section.classList.contains('ch-complete') + apply(newState) + try { + await setProgress(guideId, key, newState) + } catch { + apply(!newState) // Rollback bei Fehler + } + }) + section.appendChild(toggle) + }) +} + // --- Chat --- const chatOpen = ref(false) const messages = ref([]) @@ -122,7 +190,7 @@ async function send() {