diff --git a/backend/main.py b/backend/main.py index dd80b87..62e037b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -323,15 +323,18 @@ def ueben_antwort(a: UebenAntwort): @app.get("/api/topics/{topic}/baustein/{baustein_id}/karten") -def baustein_karten(topic: str, baustein_id: int): - # Flashcards genau dieses Bausteins — NICHT faellig-gefiltert (gerade gelesen → jetzt - # prüfen). Beantwortet wird über /api/ueben/antwort, Leitner-Spacing bleibt intakt. - rows = db.query( - "SELECT ar.id, ar.inhalt, a.titel, a.level, COALESCE(l.box,1) AS box" - " FROM artefakte ar JOIN atome a ON ar.atom_id=a.id" - " LEFT JOIN leitner l ON l.artefakt_id=ar.id" - " WHERE a.topic=? AND a.baustein_id=? AND ar.typ='flashcard'" - " AND ar.status='verifiziert' ORDER BY a.ord, ar.id", (topic, baustein_id)) +def baustein_karten(topic: str, baustein_id: int, alle: bool = False): + # Absolvierte Karten (richtig beantwortet → Leitner-faellig in der Zukunft) + # bleiben draußen — der Abruf startet nicht bei jedem Besuch von vorn. + # alle=True zeigt den vollen Stapel (Button „Alle erneut üben"). + sql = ("SELECT ar.id, ar.inhalt, a.titel, a.level, COALESCE(l.box,1) AS box" + " FROM artefakte ar JOIN atome a ON ar.atom_id=a.id" + " LEFT JOIN leitner l ON l.artefakt_id=ar.id" + " WHERE a.topic=? AND a.baustein_id=? AND ar.typ='flashcard'" + " AND ar.status='verifiziert'") + if not alle: + sql += " AND COALESCE(l.faellig, date('now')) <= date('now')" + rows = db.query(sql + " ORDER BY a.ord, ar.id", (topic, baustein_id)) return [{**r, "inhalt": db.uj(r["inhalt"], {})} for r in rows] diff --git a/frontend/src/api.js b/frontend/src/api.js index a2897da..b13a5bd 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -35,8 +35,8 @@ export const api = { ueben: (topic) => req(`/api/topics/${e(topic)}/ueben`), antwort: (artefakt_id, richtig) => req('/api/ueben/antwort', { method: 'POST', body: JSON.stringify({ artefakt_id, richtig }) }), - bausteinKarten: (topic, bausteinId) => - req(`/api/topics/${e(topic)}/baustein/${e(bausteinId)}/karten`), + bausteinKarten: (topic, bausteinId, alle = false) => + req(`/api/topics/${e(topic)}/baustein/${e(bausteinId)}/karten${alle ? '?alle=1' : ''}`), lernstand: (topic) => req(`/api/topics/${e(topic)}/lernstand`), bausteinFertig: (topic, bausteinId) => req(`/api/topics/${e(topic)}/baustein/${e(bausteinId)}/fertig`, { method: 'POST', body: '{}' }), diff --git a/frontend/src/components/Lesemodus.vue b/frontend/src/components/Lesemodus.vue index 6996109..7e0ba58 100644 --- a/frontend/src/components/Lesemodus.vue +++ b/frontend/src/components/Lesemodus.vue @@ -66,6 +66,22 @@ function taste(e) { e.preventDefault() buehne.value?.scrollBy({ top: e.key === 'ArrowDown' ? 140 : -140, behavior: 'smooth' }) } + // Abruf: Leertaste deckt auf, 1 = falsch, 2 = richtig + else if (screen.value.typ === 'abruf' && stapel.value.length) { + if (e.key === ' ') { e.preventDefault(); zeigeAntwort.value = true } + else if (zeigeAntwort.value && e.key === '1') antworten(false) + else if (zeigeAntwort.value && e.key === '2') antworten(true) + } + // Prüfung: 1–4 toggeln die Aussagen, Leertaste prüft / blättert zum nächsten Panel + else if (screen.value.typ === 'pruefung' && panel.value.length) { + if (e.key === ' ') { + e.preventDefault() + aufloesung.value ? naechstesPanel() : pruefen() + } else if (['1', '2', '3', '4'].includes(e.key)) { + const x = panel.value[Number(e.key) - 1] + if (x) toggle(x.id) + } + } } // Wisch-Navigation (mobil): klar horizontal + kurz genug = blättern. @@ -92,12 +108,23 @@ function wischEnde(e) { } // ── Abrufprüfung (Karteikarten) ──────────────────────────────────────────────── +// Persistenz über Leitner (DB): richtig beantwortete Karten sind bis zur +// Wiedervorlage nicht fällig und erscheinen beim nächsten Besuch nicht mehr. const stapel = ref([]) +const alleKarten = ref([]) // gefüllt, wenn alles absolviert → „Alle erneut üben" const zeigeAntwort = ref(false) const sende = ref(false) async function abrufLaden(bausteinId) { + zeigeAntwort.value = false + alleKarten.value = [] stapel.value = await api.bausteinKarten(props.topic, bausteinId) + if (!stapel.value.length) + alleKarten.value = await api.bausteinKarten(props.topic, bausteinId, true) +} +function nochmalAlle() { + stapel.value = alleKarten.value + alleKarten.value = [] zeigeAntwort.value = false } async function antworten(richtig) { @@ -108,6 +135,7 @@ async function antworten(richtig) { await api.antwort(karte.id, richtig) stapel.value = richtig ? stapel.value.slice(1) : [...stapel.value.slice(1), karte] zeigeAntwort.value = false + if (!stapel.value.length) await abrufLaden(screen.value.baustein) } finally { sende.value = false } } @@ -215,14 +243,21 @@ onUnmounted(() => {
- +
noch {{ stapel.length }} · Box {{ stapel[0].box }}
+
+ ✓ Alle Karten absolviert — Wiedervorlage folgt automatisch.
+ +
✓ Abruf durch — weiter mit →
@@ -231,11 +266,12 @@ onUnmounted(() => { diff --git a/frontend/src/style.css b/frontend/src/style.css index 537c9dd..4a4b928 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -340,6 +340,19 @@ table.kennzahlen th { color: var(--dim); } padding: 12px 14px; border: 1px solid var(--rand); border-radius: 10px; background: var(--karte); } +/* Shortcut-Badge: echte Tastenkappe — Monospace-Ziffer, dickere Unterkante + als 3D-Hinweis. „_" steht für die Leertaste. */ +kbd.kurz { + display: inline-block; padding: 0 5px; margin-left: 8px; + font: 600 10px/16px ui-monospace, 'SF Mono', Menlo, monospace; + color: var(--dim); text-align: center; + background: var(--bg); border: 1px solid var(--rand); + border-bottom-width: 2px; border-radius: 5px; +} +/* Zahlen-Badge der Aussagen: absolut rechts oben, aus dem Textfluss raus */ +.pf-aussage { position: relative; } +.pf-aussage kbd.kurz { position: absolute; top: 7px; right: 7px; margin: 0; } + /* Auswahl ohne Checkbox: die ganze Karte ist der Klickbereich */ .pf-aussage.gewaehlt { border-color: var(--akzent); diff --git a/tests/test_lesemodus.py b/tests/test_lesemodus.py index a2af220..69c8b18 100644 --- a/tests/test_lesemodus.py +++ b/tests/test_lesemodus.py @@ -27,6 +27,20 @@ def test_baustein_karten_nur_verifizierte_flashcards(): assert karten[0]["box"] == 1 # Default ohne Leitner-Eintrag +def test_baustein_karten_absolvierte_bleiben_draussen(): + """Richtig beantwortet (faellig in Zukunft) → nicht im Stapel; alle=True zeigt sie.""" + topic = topic_anlegen("lm-leitner") + b, _ = _setup(topic) + kid = main.baustein_karten(topic, b)[0]["id"] + db.execute("INSERT INTO leitner(artefakt_id, box, faellig)" + " VALUES(?, 2, date('now', '+1 day'))", (kid,)) + assert main.baustein_karten(topic, b) == [] + assert len(main.baustein_karten(topic, b, alle=True)) == 1 + # faellig heute (falsch beantwortet) → wieder im Stapel + db.execute("UPDATE leitner SET faellig=date('now') WHERE artefakt_id=?", (kid,)) + assert len(main.baustein_karten(topic, b)) == 1 + + def _aussage(topic, atom_id, paar, wahr, text): return db.insert("artefakte", atom_id=atom_id, typ="aussage", status="verifiziert", inhalt=db.j({"text": text, "wahr": wahr, "paar": paar,