From 2f4657c0fb52b47cf556fc9b5900af6ceaed1925 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 21 Aug 2026 09:03:40 +0200 Subject: [PATCH] feat(vocab): implement daily hard vocab limit handling in practice dialog --- .../socialnetwork/VocabPracticeDialog.vue | 20 ++++++++++++++++++- frontend/src/views/social/VocabCourseView.vue | 5 +++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue index bf6a9e3..e951436 100755 --- a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue +++ b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue @@ -140,6 +140,7 @@ const PRACTICE_MIN_EXPOSURES = 3; const SRS_SESSION_STORAGE_VERSION = 2; const HARD_REQUIRED_CONSECUTIVE_CORRECT = 5; const MAX_DAILY_DUE = 50; +const MAX_DAILY_HARD_VOCABS = 7; export default { name: 'VocabPracticeDialog', @@ -148,6 +149,7 @@ export default { return { openParams: null, // { languageId, chapterId, lessonId, courseId } onClose: null, + onDailyHardLimitReached: null, loading: false, allVocabs: false, srsMode: false, @@ -389,6 +391,19 @@ export default { } this.saveHardVocabMap(); }, + maybeStartDailyHardPractice() { + if (!this.srsMode || this.hardCount < MAX_DAILY_HARD_VOCABS) return false; + const callback = this.onDailyHardLimitReached; + this.close(); + this.$nextTick(() => { + try { + callback?.(); + } catch (_) { + // A missing parent callback must not interrupt the dialog close. + } + }); + return true; + }, unmarkCurrentAsHard() { if (!this.current) return; const matchingKey = this.findMatchingHardKey(this.current); @@ -531,7 +546,7 @@ export default { } this.saveSrsSession(); }, - open({ languageId, chapterId, lessonId, courseId, initialPool = null, initialHardPool = null, srsMode = false, closeOnHardCompletion = false, onClose = null }) { + open({ languageId, chapterId, lessonId, courseId, initialPool = null, initialHardPool = null, srsMode = false, closeOnHardCompletion = false, onDailyHardLimitReached = null, onClose = null }) { if (this.autoAdvanceTimer) { clearTimeout(this.autoAdvanceTimer); this.autoAdvanceTimer = null; @@ -542,6 +557,7 @@ export default { console.debug('[VocabPracticeDialog] open called with', { languageId, chapterId, lessonId, courseId, srsMode, hasInitialPool: Array.isArray(initialPool) }); } catch (_) {} this.onClose = typeof onClose === 'function' ? onClose : null; + this.onDailyHardLimitReached = typeof onDailyHardLimitReached === 'function' ? onDailyHardLimitReached : null; this.srsMode = Boolean(srsMode); this.initialPool = Array.isArray(initialPool) ? initialPool : null; this.closeOnHardCompletion = Boolean(closeOnHardCompletion); @@ -574,6 +590,7 @@ export default { this.hardVocabMap[key] = { learning: item.learning, reference: item.reference, itemKey: item.itemKey || item.id || null }; }); } + if (this.maybeStartDailyHardPractice()) return; this.reloadPool(); }, close() { @@ -1240,6 +1257,7 @@ export default { : []; this.saveSrsSession(); } + if (!isCorrect && this.maybeStartDailyHardPractice()) return; this.autoAdvanceTimer = setTimeout(() => { this.autoAdvanceTimer = null; this.next(); diff --git a/frontend/src/views/social/VocabCourseView.vue b/frontend/src/views/social/VocabCourseView.vue index c5c380f..ff3ea15 100755 --- a/frontend/src/views/social/VocabCourseView.vue +++ b/frontend/src/views/social/VocabCourseView.vue @@ -946,7 +946,12 @@ export default { this.$refs.practiceDialog?.open?.({ courseId: this.courseId, initialPool: this.srsDueItems, + initialHardPool: this.hardVocabList, srsMode: true, + onDailyHardLimitReached: async () => { + await this.refreshHardVocabList(); + this.openHardPractice(); + }, onClose: () => this.loadSrsDueItems() }); },