diff --git a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue index fc2fa2e..9d274ab 100755 --- a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue +++ b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue @@ -161,7 +161,6 @@ import { normalizeComparableWithNumberWords } from '@/utils/numberAnswerVariants const PRACTICE_MIN_EXPOSURES = 3; const SRS_SESSION_STORAGE_VERSION = 2; const HARD_REQUIRED_CONSECUTIVE_CORRECT = 5; -const SRS_AGAIN_REINSERT_OFFSET = 3; const MAX_DAILY_DUE = 50; export default { @@ -175,6 +174,7 @@ export default { allVocabs: false, srsMode: false, initialPool: null, + closeOnHardCompletion: false, simpleMode: false, pool: [], @@ -450,10 +450,8 @@ export default { const matchingKey = this.findMatchingHardKey(this.current); if (!matchingKey) return; this.removeHardEntriesForItem(this.current); - if (this.hardPhaseActive && this.hardRemainingCount <= 0) { - this.hardPhaseActive = false; - } this.saveHardVocabMap(); + this.maybeFinishHardPhase(); }, addCurrentToCycle() { if (!this.current?.id) return; @@ -467,10 +465,16 @@ export default { this.hardPhaseActive = true; }, maybeFinishHardPhase() { - if (!this.hardPhaseActive) return; - if (this.hardRemainingCount > 0) return; + if (!this.hardPhaseActive && !this.closeOnHardCompletion) return false; + if (this.hardRemainingCount > 0) return false; + if (this.hardCount <= 0 && this.closeOnHardCompletion) { + this.close(); + return true; + } + if (!this.hardPhaseActive) return false; this.hardPhaseActive = false; this.cycleAskedIds = []; + return false; }, getLocalDateKey() { const d = new Date(); @@ -575,7 +579,7 @@ export default { } this.saveSrsSession(); }, - open({ languageId, chapterId, lessonId, courseId, initialPool = null, srsMode = false, onClose = null }) { + open({ languageId, chapterId, lessonId, courseId, initialPool = null, srsMode = false, closeOnHardCompletion = false, onClose = null }) { if (this.autoAdvanceTimer) { clearTimeout(this.autoAdvanceTimer); this.autoAdvanceTimer = null; @@ -588,6 +592,7 @@ export default { this.onClose = typeof onClose === 'function' ? onClose : null; this.srsMode = Boolean(srsMode); this.initialPool = Array.isArray(initialPool) ? initialPool : null; + this.closeOnHardCompletion = Boolean(closeOnHardCompletion); this.allVocabs = false; this.simpleMode = false; this.srsSession = null; @@ -1231,6 +1236,9 @@ export default { delete next[mapKey]; this.hardVocabMap = next; this.saveHardVocabMap(); + if (this.maybeFinishHardPhase()) { + return; + } } } } else { @@ -1256,21 +1264,18 @@ export default { const id = this.current?.id; const treatAsAgain = rating === 'again' || !this.lastCorrect; + const moveToHardPractice = treatAsAgain && this.isItemMarkedHard(this.current); if (id && this.srsSession) { - if (treatAsAgain) { - // Wrong answers are not done yet: requeue near the end so the user - // sees them again before the session finishes. - if (Array.isArray(this.srsQueueIds)) { - const remaining = this.srsQueueIds.filter((x) => x !== id); - const insertAt = Math.min(remaining.length, SRS_AGAIN_REINSERT_OFFSET); - remaining.splice(insertAt, 0, id); - this.srsQueueIds = remaining; - } + if (treatAsAgain && !moveToHardPractice) { + // Keep the card at the front of the queue. After the solution has + // been shown, the user must type it correctly before moving on. if (Array.isArray(this.srsSession.doneIds)) { this.srsSession.doneIds = this.srsSession.doneIds.filter((x) => x !== id); } } else { + // Hard cards leave the daily review after a wrong answer. They are + // practiced separately through the hard-vocab drill. const done = Array.isArray(this.srsSession.doneIds) ? this.srsSession.doneIds : []; if (!done.includes(id)) { done.push(id); @@ -1285,6 +1290,21 @@ export default { this.saveSrsSession(); } + if (treatAsAgain && !moveToHardPractice) { + const retryItem = this.pool.find((item) => item.id === id) || this.current; + const retryDirection = this.direction; + this.resetQuestion(); + this.current = retryItem; + this.direction = retryDirection; + const prompt = this.currentAnswerPrompt; + this.acceptableAnswers = this.getAnswersForPrompt(prompt, this.direction); + if (this.simpleMode) this.buildChoices(); + this.$nextTick(() => { + if (!this.simpleMode) this.$refs.answerInput?.focus?.(); + }); + return; + } + this.next(); }, submitChoice(opt) { @@ -1338,7 +1358,7 @@ export default { this.addCurrentToCycle(); } this.maybeStartHardPhase(); - this.maybeFinishHardPhase(); + if (this.maybeFinishHardPhase()) return; const retryDirection = this.pendingRetry?.direction || null; this.resetQuestion(); if (retryDirection) { @@ -1346,7 +1366,7 @@ export default { } this.current = this.pickNextItem(); if (!this.current) { - this.maybeFinishHardPhase(); + if (this.maybeFinishHardPhase()) return; if (!this.hardPhaseActive) { this.current = this.pickNextItem(); } diff --git a/frontend/src/views/social/VocabCourseView.vue b/frontend/src/views/social/VocabCourseView.vue index e39301d..b0c096f 100755 --- a/frontend/src/views/social/VocabCourseView.vue +++ b/frontend/src/views/social/VocabCourseView.vue @@ -966,6 +966,7 @@ export default { this.$refs.practiceDialog?.open?.({ courseId: this.courseId, initialPool: this.hardVocabList, + closeOnHardCompletion: true, onClose: () => this.refreshHardVocabList() }); },