update
This commit is contained in:
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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: '{}' }),
|
||||
|
||||
@@ -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(() => {
|
||||
<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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user