This commit is contained in:
team3
2026-07-13 20:04:30 +02:00
parent c16ef5415f
commit 7d31467498
5 changed files with 83 additions and 17 deletions

View File

@@ -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"
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' ORDER BY a.ord, ar.id", (topic, baustein_id))
" 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]

View File

@@ -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: '{}' }),

View File

@@ -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: 14 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(() => {
<div class="lm-frage markdown" v-html="render(stapel[0].inhalt.frage)"></div>
<div v-if="zeigeAntwort" class="lm-antwort markdown" v-html="render(stapel[0].inhalt.antwort)"></div>
<div class="lm-karte-akt">
<button v-if="!zeigeAntwort" @click="zeigeAntwort = true">Antwort zeigen</button>
<button v-if="!zeigeAntwort" @click="zeigeAntwort = true">
Antwort zeigen <kbd class="kurz">_</kbd></button>
<template v-else>
<button class="lm-falsch" @click="antworten(false)">Falsch</button>
<button class="lm-richtig" @click="antworten(true)">Richtig</button>
<button class="lm-falsch" @click="antworten(false)">
Falsch <kbd class="kurz">1</kbd></button>
<button class="lm-richtig" @click="antworten(true)">
Richtig <kbd class="kurz">2</kbd></button>
</template>
</div>
<div class="lm-rest">noch {{ stapel.length }} · Box {{ stapel[0].box }}</div>
</div>
<div v-else-if="alleKarten.length" class="lm-bestanden">
Alle Karten absolviert Wiedervorlage folgt automatisch.<br />
<button style="margin-top: 12px" @click="nochmalAlle">Alle erneut üben</button>
</div>
<div v-else class="lm-bestanden"> Abruf durch weiter mit </div>
</section>
@@ -231,11 +266,12 @@ onUnmounted(() => {
<template v-if="panel.length">
<h3 class="pf-frage">Was gilt? <span class="pf-hinweis">(0{{ panel.length }} Aussagen können wahr sein)</span></h3>
<div class="pf-panel">
<label v-for="x in panel" :key="x.id" class="pf-aussage"
<label v-for="(x, i) in panel" :key="x.id" class="pf-aussage"
:class="{ gewaehlt: auswahl.has(x.id),
'pf-wahr': urteil(x.id) === true,
'pf-falsch2': urteil(x.id) === false }"
@click.prevent="toggle(x.id)">
<kbd class="kurz">{{ i + 1 }}</kbd>
<span class="markdown" v-html="render(x.text)"></span>
</label>
</div>
@@ -243,8 +279,8 @@ onUnmounted(() => {
{{ warRichtig ? ' Panel korrekt (+1)' : ' Nicht exakt (1)' }}
</div>
<div class="pf-akt">
<button v-if="!aufloesung" @click="pruefen">Prüfen</button>
<button v-else @click="naechstesPanel">Nächstes Panel </button>
<button v-if="!aufloesung" @click="pruefen">Prüfen <kbd class="kurz">_</kbd></button>
<button v-else @click="naechstesPanel">Nächstes Panel <kbd class="kurz">_</kbd></button>
<span v-if="fertig.has(screen.baustein)" class="lm-bestanden" style="margin: 0"> bestanden</span>
</div>
</template>

View File

@@ -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);

View File

@@ -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,