This commit is contained in:
team3
2026-06-24 09:25:14 +02:00
parent 3b1e84e17d
commit d389584b58
10 changed files with 251 additions and 212 deletions

View File

@@ -3,6 +3,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { chatBaustein, pruefeBaustein, fetchFrageMuster } from '../api.js'
import { usePruefSlot } from '../pruefungCache.js'
import { renderMarkdown, renderMarkdownInline } from '../markdown.js'
import { stufeFuer, malusRegel } from '../stufen.js'
import { useChat, istUnten } from '../composables/useChat.js'
const props = defineProps({
@@ -22,74 +23,40 @@ const props = defineProps({
const emit = defineEmits(['statusChanged', 'openFokus', 'setAnsicht'])
// Meilensteine (Score-Bänder): Quiz→6, Lückentext→12, Erklären→18, FullGuide-Max 30.
const ABSOLVIERT = 6
const VERSTANDEN = 12
const GEMEISTERT = 18
const VOLL = 30
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, absolviert: false, verstanden: false, gemeistert: false })
// Volle Prüfung (cap 30): durchgehend zufällig aus allen 5 Formen. Kurze Prüfung (cap 6):
// Score-Band + Schwierigkeit (ALT+E). Ohne Frage-Muster (alter Baustein) nur Erklären.
// Lernstufen relativ zum cap (stufen.js): grün/blau/lila/gold @ 20/40/60/100 %.
// Prüfung IMMER zufällig aus 5 Formen (mit Muster); cap = 2×relevante Subbausteine.
const MODI = ['quiz_leicht', 'quiz_schwer', 'lueck_leicht', 'lueck_schwer', 'erklaeren']
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, cap: props.cap })
const score = computed(() => st.value.gute_antworten || 0)
const schwerWahl = ref(false) // leicht/schwer in den Bändern unter 19 (ALT+E)
// Band aus einem Score: Lückentext ab 6, Erklären ab 12, Zufall ab 18 (Meilenstein-inklusiv).
function bandFuer(s) {
if (s >= GEMEISTERT) return 'zufall'
if (s >= VERSTANDEN) return 'erklaeren'
if (s >= ABSOLVIERT) return 'lueckentext'
return 'quiz'
}
// Am Format-Max: Score eingefroren („Max"). Gilt auch bei cap 30 (FullGuide, endgültig).
const amDeckel = computed(() => score.value >= props.cap)
// Aktuelles Band: volle Prüfung (cap 30) ist DURCHGEHEND zufällig aus allen 5 Formen.
// Sonst score-basiert (am Deckel das Band des erreichten Formats, cap1).
// Ohne Frage-Muster (alter Baustein) gibt es nur Erklären.
const aktuellesBand = computed(() =>
!musterModus.value ? 'erklaeren'
: props.cap >= VOLL ? 'zufall'
: bandFuer(amDeckel.value ? props.cap - 1 : score.value))
// 'zufall' = Platzhalter; das gezogene Objekt trägt die echte Form/Schwierigkeit.
const aktiverModus = computed(() => {
const b = aktuellesBand.value
if (b === 'lueckentext') return schwerWahl.value ? 'lueck_schwer' : 'lueck_leicht'
if (b === 'quiz') return schwerWahl.value ? 'quiz_schwer' : 'quiz_leicht'
return b // 'erklaeren' | 'zufall'
})
const cap = computed(() => st.value.cap || props.cap)
const scoreAnzeige = computed(() => Math.min(score.value, cap.value)) // Altbestand kann cap übersteigen
const stufe = computed(() => stufeFuer(score.value, cap.value)) // Objekt {key,label,kurz,farbe} | null
const schwerWahl = ref(false) // ungenutzt im Zufallsmodus (immer zufällig)
const amDeckel = computed(() => score.value >= cap.value)
// Mit Frage-Muster zufällig aus 5 Formen; ohne Muster (alter Baustein) nur Erklären.
const aktuellesBand = computed(() => musterModus.value ? 'zufall' : 'erklaeren')
const imZufall = computed(() => aktuellesBand.value === 'zufall')
const aktiveForm = computed(() => {
const m = aktiverModus.value
if (m === 'zufall') return 'quiz' // Platzhalter fürs idle-Widget; Objekt überschreibt
return m.startsWith('quiz') ? 'quiz' : m.startsWith('lueck') ? 'lueckentext' : 'erklaeren'
})
const schwer = computed(() => aktiverModus.value.endsWith('_schwer'))
// Angezeigte Form: folgt der aktiven Form, friert aber ein, solange eine gezogene Frage
// sichtbar ist (sonst verschwindet das Ergebnis, wenn die Antwort eine Bandgrenze überschreitet).
const aktiverModus = computed(() => aktuellesBand.value) // 'zufall' | 'erklaeren'
const aktiveForm = computed(() => imZufall.value ? 'quiz' : 'erklaeren') // Platzhalter; gezogenes Objekt überschreibt
const schwer = computed(() => false)
// Angezeigte Form: folgt der aktiven Form, friert ein, solange eine gezogene Frage sichtbar ist.
const anzeigeForm = ref(null)
const zeigeForm = computed(() => anzeigeForm.value || aktiveForm.value)
// Band, in dem die gezeigte Frage gezogen wurde. Weicht es vom aktuellen Band ab, hat die
// letzte Antwort einen Meilenstein überschritten → Button „Zum nächsten Abschnitt".
const anzeigeBand = ref(null)
const abschnittWechsel = computed(() => anzeigeBand.value && anzeigeBand.value !== aktuellesBand.value)
// Schwierigkeit des gerade gezeigten Widgets (eingefroren) bzw. der aktiven Wahl.
// Schwierigkeit des gerade gezeigten Widgets (vom gezogenen Objekt).
const aktSchwer = computed(() => {
if (zeigeForm.value === 'quiz' && quizAktuell.value) return quizAktuell.value.schwer
if (zeigeForm.value === 'lueckentext' && lueckAktuell.value) return lueckAktuell.value.schwer
return schwer.value
return false
})
const modusRegel = computed(() => {
const m = score.value >= GEMEISTERT ? '3' : '1' // Fehler kostet erst ab Score 18 3
const m = malusRegel(score.value, cap.value) // '0' | '1' | '2' | '3' nach Fortschritt
if (zeigeForm.value === 'quiz') return aktSchwer.value ? `x von 4 · +3/${m}` : `1 von 4 · +1/${m}`
if (zeigeForm.value === 'lueckentext') return aktSchwer.value ? `frei tippen · +3/${m}` : `Begriff aus 4 · +1/${m}`
return score.value >= GEMEISTERT ? '+1/+2/+3 · Fehler 3' : '+1/+2/+3 · Nachfragen max +1 · schwach 1'
})
// Ende des aktuellen Bands (nächster Meilenstein).
const bandZiel = computed(() => {
if (score.value >= GEMEISTERT) return VOLL
if (score.value >= VERSTANDEN) return GEMEISTERT
if (score.value >= ABSOLVIERT) return VERSTANDEN
return ABSOLVIERT
return `+1/+2/+3 · Fehler ${m}`
})
const bandZiel = computed(() => cap.value)
const FORM_NAME = { quiz: 'Quiz', lueckentext: 'Lückentext', erklaeren: 'Erklären' }
// --- Toggle-Bereich ---
@@ -141,7 +108,7 @@ function onPruefScroll() {
function applyPruefung(res) {
// res.streak nur bei antwort_pruefen gesetzt (verbindlich, persistiert); sonst Streak unverändert lassen.
emit('statusChanged', { baustein: props.baustein, ...st.value, gute_antworten: res.gute_antworten, streak: res.streak ?? st.value.streak, absolviert: res.absolviert, verstanden: res.verstanden, gemeistert: res.gemeistert })
emit('statusChanged', { baustein: props.baustein, ...st.value, gute_antworten: res.gute_antworten, streak: res.streak ?? st.value.streak, cap: res.cap ?? cap.value })
}
async function pruefScroll() {
@@ -310,8 +277,8 @@ function poolFormPruefen() {
function poolFuellen() {
poolFormPruefen()
const keyBeginn = poolKey.value
const cap = musterModus.value ? Infinity : 3
while (pool.value.length + inflight.value < POOL_ZIEL && poolMiss < cap) {
const missCap = musterModus.value ? Infinity : 3
while (pool.value.length + inflight.value < POOL_ZIEL && poolMiss < missCap) {
inflight.value++
frageEinzeln()
.then((obj) => {
@@ -326,10 +293,10 @@ function poolFuellen() {
// Ein Frage-Objekt aus dem Pool nehmen; ist er leer, auf das erste eintreffende warten.
async function poolNehmen() {
if (!pool.value.length) {
const cap = musterModus.value ? Infinity : 3
const missCap = musterModus.value ? Infinity : 3
pruefLoading.value = true
try {
while (!pool.value.length && (inflight.value > 0 || poolMiss < cap)) {
while (!pool.value.length && (inflight.value > 0 || poolMiss < missCap)) {
if (!inflight.value) poolFuellen()
await new Promise((r) => setTimeout(r, 120))
}
@@ -657,10 +624,8 @@ function onPruefKey(e) {
</button>
<button :class="{ active: activeTab === 'pruefung' }" @click="tabKlick('pruefung')">
Prüfung
<span v-if="st.gemeistert" class="bp-chip gold" :title="`Gemeistert (${GEMEISTERT})`"> {{ score }}/{{ cap }}</span>
<span v-else-if="st.verstanden" class="bp-chip lila" :title="`Verstanden (${VERSTANDEN})`"> {{ score }}/{{ cap }}</span>
<span v-else-if="st.absolviert" class="bp-chip done" :title="`Absolviert (${ABSOLVIERT})`"> {{ score }}/{{ cap }}</span>
<span v-else-if="score" class="bp-chip">{{ score }}/{{ cap }}</span>
<span v-if="stufe" class="bp-chip" :style="{ borderColor: stufe.farbe, color: stufe.farbe }" :title="`${stufe.label} (${cap})`">{{ stufe.kurz }} {{ scoreAnzeige }}/{{ cap }}</span>
<span v-else-if="score" class="bp-chip">{{ scoreAnzeige }}/{{ cap }}</span>
</button>
</div>
@@ -692,9 +657,9 @@ function onPruefKey(e) {
<!-- Prüfung: gesteuerter Dialog -->
<div v-else>
<p class="bp-hint">
<template v-if="amDeckel">{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} <strong>Max</strong>.</template>
<template v-else-if="imZufall && !anzeigeForm">Zufällig · {{ score }}/{{ cap }}</template>
<template v-else-if="imZufall">{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} · {{ modusRegel }}</template>
<template v-if="amDeckel">{{ FORM_NAME[zeigeForm] }} · {{ scoreAnzeige }}/{{ cap }} <strong>Max</strong>.</template>
<template v-else-if="imZufall && !anzeigeForm">Zufällig · {{ scoreAnzeige }}/{{ cap }}</template>
<template v-else-if="imZufall">{{ FORM_NAME[zeigeForm] }} · {{ scoreAnzeige }}/{{ cap }} · {{ modusRegel }}</template>
<template v-else>{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ bandZiel }} · {{ modusRegel }}</template>
</p>
<button