+
-
-
+
+
@@ -854,6 +854,7 @@ import apiClient from '@/utils/axios.js';
const debugLog = () => {};
const LESSON_STATE_VERSION = 1;
+const VOCAB_REPEAT_INTERVALS = [1, 2, 4];
export default {
name: 'VocabLessonView',
@@ -883,6 +884,7 @@ export default {
vocabTrainerWrong: 0,
vocabTrainerTotalAttempts: 0,
vocabTrainerStats: {}, // { [vocabKey]: { attempts: 0, correct: 0, wrong: 0 } }
+ vocabTrainerRepeatQueue: [],
vocabTrainerChoiceOptions: [],
vocabTrainerPhase: 'current', // 'current' = aktuelle Lektion, 'mixed' = gemischt mit alten
vocabTrainerMixedPool: [], // Pool aus alten Lektionsvokabeln
@@ -1240,6 +1242,7 @@ export default {
vocabTrainerWrong: this.vocabTrainerWrong,
vocabTrainerTotalAttempts: this.vocabTrainerTotalAttempts,
vocabTrainerStats: this.vocabTrainerStats,
+ vocabTrainerRepeatQueue: this.vocabTrainerRepeatQueue,
vocabTrainerCurrentAttempts: this.vocabTrainerCurrentAttempts,
vocabTrainerReviewAttempts: this.vocabTrainerReviewAttempts,
exerciseRetryPending: this.exerciseRetryPending,
@@ -1474,6 +1477,9 @@ export default {
this.exerciseRetryPending = Boolean(parsedState.exerciseRetryPending);
this.exerciseRetryPendingSinceAttempts = Math.max(0, Number(parsedState.exerciseRetryPendingSinceAttempts) || 0);
this.vocabTrainerMixedPool = this._buildMixedPool();
+ const knownRepeatKeys = new Set([...this.trainableLessonVocab, ...this.vocabTrainerMixedPool].map((entry) => this.getVocabKey(entry)));
+ this.vocabTrainerRepeatQueue = this.normalizeRepeatQueue(parsedState.vocabTrainerRepeatQueue)
+ .filter((entry) => knownRepeatKeys.has(entry.key));
this.vocabTrainerMixedAttempts = 0;
this.vocabTrainerPhase = this.hasPreviousVocab && this.currentReviewShare > 0 ? 'mixed' : 'current';
this.currentVocabQuestion = null;
@@ -1756,6 +1762,7 @@ export default {
this.vocabTrainerActive = false;
this.vocabTrainerPool = [];
this.vocabTrainerMixedPool = [];
+ this.vocabTrainerRepeatQueue = [];
this.vocabTrainerPhase = 'current';
this.vocabTrainerCurrentAttempts = 0;
this.vocabTrainerReviewAttempts = 0;
@@ -2331,6 +2338,7 @@ export default {
this.vocabTrainerCurrentAttempts = 0;
this.vocabTrainerReviewAttempts = 0;
this.vocabTrainerStats = {};
+ this.vocabTrainerRepeatQueue = [];
// Bereite Mixed-Pool aus alten Vokabeln vor (ohne Duplikate aus aktueller Lektion)
this.vocabTrainerMixedPool = this._buildMixedPool();
this.vocabTrainerPhase = 'current';
@@ -2351,6 +2359,7 @@ export default {
this.vocabTrainerCurrentAttempts = 0;
this.vocabTrainerReviewAttempts = 0;
this.vocabTrainerMixedPool = [];
+ this.vocabTrainerRepeatQueue = [];
this.currentVocabQuestion = null;
this.vocabTrainerAnswer = '';
this.vocabTrainerSelectedChoice = null;
@@ -2375,6 +2384,79 @@ export default {
}
return this.vocabTrainerStats[key];
},
+ normalizeRepeatQueue(queue = []) {
+ if (!Array.isArray(queue)) {
+ return [];
+ }
+ return queue
+ .map((entry) => ({
+ key: String(entry?.key || '').trim(),
+ dueAfter: Math.max(0, Number(entry?.dueAfter) || 0),
+ stageIndex: Math.max(0, Math.min(VOCAB_REPEAT_INTERVALS.length - 1, Number(entry?.stageIndex) || 0))
+ }))
+ .filter((entry) => entry.key);
+ },
+ queueFailedVocab(vocab) {
+ const key = this.getVocabKey(vocab);
+ const existing = this.vocabTrainerRepeatQueue.find((entry) => entry.key === key);
+ if (existing) {
+ existing.dueAfter = VOCAB_REPEAT_INTERVALS[0];
+ existing.stageIndex = 0;
+ return;
+ }
+ this.vocabTrainerRepeatQueue.push({
+ key,
+ dueAfter: VOCAB_REPEAT_INTERVALS[0],
+ stageIndex: 0
+ });
+ },
+ resolveRepeatedVocab(vocab) {
+ const key = this.getVocabKey(vocab);
+ const entryIndex = this.vocabTrainerRepeatQueue.findIndex((entry) => entry.key === key && entry.dueAfter <= 0);
+ if (entryIndex === -1) {
+ return;
+ }
+ const entry = this.vocabTrainerRepeatQueue[entryIndex];
+ if (entry.stageIndex >= VOCAB_REPEAT_INTERVALS.length - 1) {
+ this.vocabTrainerRepeatQueue.splice(entryIndex, 1);
+ return;
+ }
+ entry.stageIndex += 1;
+ entry.dueAfter = VOCAB_REPEAT_INTERVALS[entry.stageIndex];
+ },
+ advanceRepeatQueue(completedKey = '') {
+ this.vocabTrainerRepeatQueue = this.vocabTrainerRepeatQueue
+ .map((entry) => {
+ if (entry.key === completedKey) {
+ return entry;
+ }
+ return {
+ ...entry,
+ dueAfter: Math.max(0, entry.dueAfter - 1)
+ };
+ })
+ .filter((entry) => entry.key);
+ },
+ getRepeatDueVocab() {
+ const dueEntry = this.vocabTrainerRepeatQueue.find((entry) => entry.dueAfter <= 0);
+ if (!dueEntry) {
+ return null;
+ }
+ const allVocabs = [...this.trainableLessonVocab, ...this.vocabTrainerMixedPool];
+ return allVocabs.find((vocab) => this.getVocabKey(vocab) === dueEntry.key) || null;
+ },
+ getPendingRepeatKeys() {
+ return new Set(
+ this.vocabTrainerRepeatQueue
+ .filter((entry) => entry.dueAfter > 0)
+ .map((entry) => entry.key)
+ );
+ },
+ continueAfterVocabAnswer() {
+ const completedKey = this.currentVocabQuestion?.key || '';
+ this.advanceRepeatQueue(completedKey);
+ this.nextVocabQuestion();
+ },
checkVocabModeSwitch() {
this.updateExerciseUnlockState();
@@ -2507,13 +2589,21 @@ export default {
let questionSource = 'current';
let sourcePool = this.trainableLessonVocab;
+ const dueRepeatVocab = this.getRepeatDueVocab();
- if (this.vocabTrainerMode === 'typing') {
+ if (dueRepeatVocab) {
+ sourcePool = [dueRepeatVocab];
+ questionSource = this.vocabTrainerMixedPool.some((entry) => this.getVocabKey(entry) === this.getVocabKey(dueRepeatVocab))
+ ? 'review'
+ : 'current';
+ }
+
+ if (!dueRepeatVocab && this.vocabTrainerMode === 'typing') {
sourcePool = this.vocabTrainerPool;
if (this.vocabTrainerMixedPool.length > 0 && Math.random() < 0.35) {
questionSource = 'review';
}
- } else if (this.vocabTrainerMixedPool.length > 0 && this.currentReviewShare > 0 && Math.random() < this.currentReviewShare) {
+ } else if (!dueRepeatVocab && this.vocabTrainerMixedPool.length > 0 && this.currentReviewShare > 0 && Math.random() < this.currentReviewShare) {
sourcePool = this.vocabTrainerMixedPool;
questionSource = 'review';
}
@@ -2523,6 +2613,14 @@ export default {
questionSource = 'current';
}
+ if (!dueRepeatVocab) {
+ const pendingRepeatKeys = this.getPendingRepeatKeys();
+ const filteredPool = sourcePool.filter((vocab) => !pendingRepeatKeys.has(this.getVocabKey(vocab)));
+ if (filteredPool.length > 0) {
+ sourcePool = filteredPool;
+ }
+ }
+
const randomIndex = Math.floor(Math.random() * sourcePool.length);
const vocab = sourcePool[randomIndex];
this.vocabTrainerDirection = Math.random() < 0.5 ? 'L2R' : 'R2L';
@@ -2615,9 +2713,11 @@ export default {
if (this.vocabTrainerLastCorrect) {
this.vocabTrainerCorrect++;
stats.correct++;
+ this.resolveRepeatedVocab(this.currentVocabQuestion.vocab);
} else {
this.vocabTrainerWrong++;
stats.wrong++;
+ this.queueFailedVocab(this.currentVocabQuestion.vocab);
}
this.vocabTrainerAnswered = true;
@@ -2630,7 +2730,7 @@ export default {
setTimeout(() => {
// Prüfe erneut, ob noch Fragen vorhanden sind (könnte sich geändert haben)
if (this.vocabTrainerPool && this.vocabTrainerPool.length > 0 && this.vocabTrainerActive) {
- this.nextVocabQuestion();
+ this.continueAfterVocabAnswer();
}
}, delay);
}