diff --git a/backend/controllers/vocabController.js b/backend/controllers/vocabController.js index db5591f..d8d7979 100755 --- a/backend/controllers/vocabController.js +++ b/backend/controllers/vocabController.js @@ -43,10 +43,16 @@ class VocabController { this.getCourseSrsDue = this._wrapWithUser((userId, req) => this.service.getCourseSrsDue(userId, req.params.courseId, req.query) ); + this.getCourseHardSrsItems = this._wrapWithUser((userId, req) => + this.service.getCourseHardSrsItems(userId, req.params.courseId) + ); this.reviewSrsItem = this._wrapWithUser((userId, req) => this.service.reviewSrsItem(userId, req.body), { successStatus: 201 } ); + this.clearSrsItemHardStatus = this._wrapWithUser((userId, req) => + this.service.clearSrsItemHardStatus(userId, req.body) + ); this.getCourseByShareCode = this._wrapWithUser((userId, req) => this.service.getCourseByShareCode(userId, req.body.shareCode)); this.updateCourse = this._wrapWithUser((userId, req) => this.service.updateCourse(userId, req.params.courseId, req.body)); this.deleteCourse = this._wrapWithUser((userId, req) => this.service.deleteCourse(userId, req.params.courseId)); diff --git a/backend/migrations-active/20260820000000-add-vocab-srs-hard-status.cjs b/backend/migrations-active/20260820000000-add-vocab-srs-hard-status.cjs new file mode 100644 index 0000000..e3de9f2 --- /dev/null +++ b/backend/migrations-active/20260820000000-add-vocab-srs-hard-status.cjs @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = { + async up(queryInterface) { + await queryInterface.sequelize.query(` + ALTER TABLE community.vocab_srs_item + ADD COLUMN IF NOT EXISTS is_hard BOOLEAN NOT NULL DEFAULT FALSE; + `); + await queryInterface.sequelize.query(` + CREATE INDEX IF NOT EXISTS idx_vocab_srs_item_hard + ON community.vocab_srs_item (user_id, course_id, is_hard, next_due_at); + `); + }, + + async down(queryInterface) { + await queryInterface.sequelize.query(` + DROP INDEX IF EXISTS community.idx_vocab_srs_item_hard; + `); + await queryInterface.sequelize.query(` + ALTER TABLE community.vocab_srs_item + DROP COLUMN IF EXISTS is_hard; + `); + } +}; diff --git a/backend/models/community/vocab_srs_item.js b/backend/models/community/vocab_srs_item.js index b5c2bbd..54114ea 100755 --- a/backend/models/community/vocab_srs_item.js +++ b/backend/models/community/vocab_srs_item.js @@ -81,6 +81,12 @@ VocabSrsItem.init({ allowNull: false, defaultValue: 0, field: 'lapse_count' + }, + isHard: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + field: 'is_hard' } }, { sequelize, diff --git a/backend/routers/vocabRouter.js b/backend/routers/vocabRouter.js index 349f990..cac65ff 100755 --- a/backend/routers/vocabRouter.js +++ b/backend/routers/vocabRouter.js @@ -36,10 +36,12 @@ router.get('/courses/:courseId/completed-lesson-vocabs', vocabController.getComp router.get('/courses/:courseId/dictionary', vocabController.getCourseDictionary); router.get('/courses/:courseId/distractor-pool', vocabController.getVocabDistractorPool); router.get('/courses/:courseId/srs/due', vocabController.getCourseSrsDue); +router.get('/courses/:courseId/srs/hard', vocabController.getCourseHardSrsItems); router.get('/courses/:courseId', vocabController.getCourse); router.put('/courses/:courseId', vocabController.updateCourse); router.delete('/courses/:courseId', vocabController.deleteCourse); router.post('/srs/review', vocabController.reviewSrsItem); +router.patch('/srs/hard', vocabController.clearSrsItemHardStatus); // Lessons router.post('/courses/:courseId/lessons', vocabController.addLessonToCourse); diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index 41ec97b..6eb3dc6 100755 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -15,6 +15,8 @@ import { notifyUser } from '../utils/socket.js'; import { Op } from 'sequelize'; import { BISAYA_PHASE1_DIDACTICS, BISAYA_DIDACTICS_FRAGMENTS } from '../scripts/bisaya-course-phase1.js'; +const DAILY_SRS_LIMIT = 50; + export default class VocabService { _stripGermanNumberSeparators(value) { return String(value || '').replace(/[\s.-]+/g, ''); @@ -202,35 +204,23 @@ export default class VocabService { return (leftLooksShortFragment && rightLooksSentence) || (rightLooksShortFragment && leftLooksSentence); } - _calculateSrsSchedule(item, { correct, rating = null } = {}) { + _calculateSrsSchedule(item, { correct } = {}) { const now = new Date(); - const previousStage = Math.max(0, Number(item?.stage) || 0); - const previousInterval = Math.max(0, Number(item?.intervalDays) || 0); - const normalizedRating = String(rating || '').toLowerCase(); - const isCorrect = Boolean(correct) && normalizedRating !== 'again'; - if (!isCorrect) { + if (!correct) { return { - stage: Math.max(0, previousStage - 1), + stage: 0, intervalDays: 0, nextDueAt: new Date(now.getTime() + 10 * 60 * 1000), lapseDelta: 1 }; } - // Neue einfache Policy: - // - 'easy' -> 7 Tage - // - 'good'/'normal' -> 4 Tage - // - 'hard' -> 1 Tag - // Außerdem: nextDueAt darf nicht mehr am gleichen Kalendertag liegen. - let intervalDays; - if (normalizedRating === 'easy') { - intervalDays = 7; - } else if (normalizedRating === 'hard') { - intervalDays = 1; - } else { - // default / 'good' / unspecified - intervalDays = 4; - } + // Intervals are based only on successful answers in the daily SRS review. + // There is deliberately no user-selected difficulty rating: each successful + // recall moves the term to the next, less frequent interval. + const intervals = [1, 3, 7, 14, 30, 60, 120]; + const previousCorrectCount = Math.max(0, Number(item?.correctCount) || 0); + const intervalDays = intervals[Math.min(previousCorrectCount, intervals.length - 1)]; // Bestimme nextDueAt als Start des Tages (00:00) nach intervalDays const nextDueAt = new Date(now); @@ -239,8 +229,7 @@ export default class VocabService { // Gehe vorwärts: morgen + (intervalDays - 1) nextDueAt.setDate(nextDueAt.getDate() + 1 + Math.max(0, intervalDays - 1)); - // Stage-Logik: einfache Fortschrittsstufe basierend auf intervalDays - let nextStage = Math.min(8, Math.max(0, Math.floor(Math.log2(intervalDays + 1)))); + const nextStage = Math.min(intervals.length, previousCorrectCount + 1); return { stage: nextStage, @@ -2012,7 +2001,10 @@ export default class VocabService { throw err; } - const limit = this._clampInteger(query?.limit, { min: 1, max: 100, fallback: 30 }); + // A daily SRS batch is intentionally capped. Excess due items remain due + // and are shown on the following day rather than silently overloading today. + const requestedLimit = this._clampInteger(query?.limit, { min: 1, max: DAILY_SRS_LIMIT, fallback: DAILY_SRS_LIMIT }); + const limit = Math.min(DAILY_SRS_LIMIT, requestedLimit); const now = new Date(); const dueWhere = { userId: user.id, @@ -2052,6 +2044,7 @@ export default class VocabService { dueAt: now.toISOString(), count: rows.length, totalDueCount, + dailyLimit: DAILY_SRS_LIMIT, limit, items: rows.map((item) => ({ itemKey: item.itemKey, @@ -2066,7 +2059,42 @@ export default class VocabService { nextDueAt: this._normalizeIsoDate(item.nextDueAt), correctCount: item.correctCount, wrongCount: item.wrongCount, - lapseCount: item.lapseCount + lapseCount: item.lapseCount, + isHard: item.isHard + })) + }; + } + + async getCourseHardSrsItems(hashedUserId, courseId) { + const user = await this._getUserByHashedId(hashedUserId); + const course = await VocabCourse.findByPk(courseId); + if (!course) { + const err = new Error('Course not found'); + err.status = 404; + throw err; + } + if (course.ownerUserId !== user.id && !course.isPublic) { + const err = new Error('Access denied'); + err.status = 403; + throw err; + } + + const items = await VocabSrsItem.findAll({ + where: { userId: user.id, courseId: Number(course.id), isHard: true }, + order: [['nextDueAt', 'ASC'], ['wrongCount', 'DESC']] + }); + return { + courseId: course.id, + items: items.filter((item) => this._isTrainableSrsPair(item)).map((item) => ({ + itemKey: item.itemKey, + courseId: item.courseId, + lessonId: item.lessonId, + learning: item.learning, + reference: item.reference, + direction: item.direction, + correctCount: item.correctCount, + wrongCount: item.wrongCount, + isHard: true })) }; } @@ -2143,8 +2171,7 @@ export default class VocabService { const correct = Boolean(payload?.correct); const schedule = this._calculateSrsSchedule(item, { - correct, - rating: payload?.rating + correct }); item.stage = schedule.stage; @@ -2156,6 +2183,7 @@ export default class VocabService { } else { item.wrongCount += 1; item.lapseCount += schedule.lapseDelta; + item.isHard = true; } await item.save(); @@ -2180,10 +2208,31 @@ export default class VocabService { nextDueAt: this._normalizeIsoDate(item.nextDueAt), correctCount: item.correctCount, wrongCount: item.wrongCount, - lapseCount: item.lapseCount + lapseCount: item.lapseCount, + isHard: item.isHard }; } + async clearSrsItemHardStatus(hashedUserId, payload = {}) { + const user = await this._getUserByHashedId(hashedUserId); + const courseId = this._clampInteger(payload?.courseId, { min: 1, max: 1_000_000, fallback: 0 }); + const itemKey = this._sanitizeShortString(payload?.itemKey, 80); + if (!courseId || !itemKey) { + const err = new Error('Missing SRS item'); + err.status = 400; + throw err; + } + const item = await VocabSrsItem.findOne({ where: { userId: user.id, courseId, itemKey } }); + if (!item) { + const err = new Error('SRS item not found'); + err.status = 404; + throw err; + } + item.isHard = false; + await item.save(); + return { itemKey: item.itemKey, isHard: false }; + } + async searchVocabs(hashedUserId, languageId, { q = '', learning = '', motherTongue = '' } = {}) { const user = await this._getUserByHashedId(hashedUserId); const access = await this._getLanguageAccess(user.id, languageId); diff --git a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue index d5c6424..bf6a9e3 100755 --- a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue +++ b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue @@ -45,22 +45,6 @@ -
-
{{ $t('socialnetwork.vocab.practice.srsRateTitle') }}
- -
-
- -
@@ -162,7 +140,6 @@ 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', @@ -171,7 +148,6 @@ export default { return { openParams: null, // { languageId, chapterId, lessonId, courseId } onClose: null, - onDailyHardLimitReached: null, loading: false, allVocabs: false, srsMode: false, @@ -258,9 +234,6 @@ export default { showSkipButton() { return !this.answered; }, - showSrsRatingButtons() { - return this.srsMode && this.answered && !this.locked; - }, visibleCorrectAnswers() { const answers = Array.isArray(this.acceptableAnswers) ? this.acceptableAnswers.filter(Boolean) : []; if (answers.length > 0) { @@ -272,37 +245,6 @@ export default { const fallback = this.direction === 'L2R' ? this.current.reference : this.current.learning; return this.expandAnswerVariants(fallback); }, - srsRatingOptions() { - if (!this.answered) { - return []; - } - if (!this.lastCorrect) { - return [ - { - value: 'again', - label: this.$t('socialnetwork.vocab.practice.srsAgain'), - hint: this.$t('socialnetwork.vocab.practice.srsAgainHint') - } - ]; - } - return [ - { - value: 'hard', - label: this.$t('socialnetwork.vocab.practice.srsHard'), - hint: this.$t('socialnetwork.vocab.practice.srsHardHint') - }, - { - value: 'good', - label: this.$t('socialnetwork.vocab.practice.srsGood'), - hint: this.$t('socialnetwork.vocab.practice.srsGoodHint') - }, - { - value: 'easy', - label: this.$t('socialnetwork.vocab.practice.srsEasy'), - hint: this.$t('socialnetwork.vocab.practice.srsEasyHint') - } - ]; - }, srsTotalDue() { return Number(this.srsSession?.initialTotalDue || 0) || 0; }, @@ -446,21 +388,6 @@ export default { this.hardMasteryByKey[key] = 0; } this.saveHardVocabMap(); - if (this.srsMode) { - this.maybeStartDailyHardPractice(); - } - }, - maybeStartDailyHardPractice() { - if (this.hardCount < MAX_DAILY_HARD_VOCABS) return; - const callback = this.onDailyHardLimitReached; - this.close(); - this.$nextTick(() => { - try { - callback?.(); - } catch (_) { - // A missing parent callback must not interrupt the dialog close. - } - }); }, unmarkCurrentAsHard() { if (!this.current) return; @@ -604,7 +531,7 @@ export default { } this.saveSrsSession(); }, - open({ languageId, chapterId, lessonId, courseId, initialPool = null, srsMode = false, closeOnHardCompletion = false, onDailyHardLimitReached = null, onClose = null }) { + open({ languageId, chapterId, lessonId, courseId, initialPool = null, initialHardPool = null, srsMode = false, closeOnHardCompletion = false, onClose = null }) { if (this.autoAdvanceTimer) { clearTimeout(this.autoAdvanceTimer); this.autoAdvanceTimer = null; @@ -615,7 +542,6 @@ 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); @@ -639,9 +565,15 @@ export default { this.$refs.dialog.open(); this.$nextTick(() => { document.addEventListener('keydown', this.handleKeyDown); - if (this.srsMode) this.maybeStartDailyHardPractice(); }); this.loadHardVocabMap(); + if (Array.isArray(initialHardPool)) { + initialHardPool.forEach((item) => { + const key = this.getHardKey(item); + if (!key || !item?.learning || !item?.reference) return; + this.hardVocabMap[key] = { learning: item.learning, reference: item.reference, itemKey: item.itemKey || item.id || null }; + }); + } this.reloadPool(); }, close() { @@ -1198,7 +1130,7 @@ export default { // ignore autoplay issues } }, - reportSrsReview(isCorrect, rating = null) { + reportSrsReview(isCorrect) { if (!this.current || !this.openParams?.courseId) { return Promise.resolve(); } @@ -1209,8 +1141,7 @@ export default { learning: this.current.learning, reference: this.current.reference, direction: this.direction, - correct: Boolean(isCorrect), - rating + correct: Boolean(isCorrect) }).catch((error) => { console.warn('[VocabPracticeDialog] SRS review could not be saved:', error); }); @@ -1239,9 +1170,7 @@ export default { }; } } - if (!this.srsMode) { - this.reportSrsReview(isCorrect); - } + if (this.srsMode) this.finishSrsAnswer(isCorrect); const id = this.current?.id; if (!id) return; @@ -1259,6 +1188,7 @@ export default { this.hardMasteryByKey[key] = Math.max(0, Number(this.hardMasteryByKey[key]) || 0) + 1; const mapKey = this.findMatchingHardKey(this.current); if ((Number(this.hardMasteryByKey[key]) || 0) >= HARD_REQUIRED_CONSECUTIVE_CORRECT && mapKey) { + this.clearSrsHardStatus(this.current); const next = { ...this.hardVocabMap }; delete next[mapKey]; this.hardVocabMap = next; @@ -1282,87 +1212,38 @@ export default { this.lastIds.unshift(id); this.lastIds = this.lastIds.slice(0, 3); }, - async submitSrsRating(rating) { - if (!this.srsMode || !this.answered || this.locked) { - return; - } + clearSrsHardStatus(item) { + const itemKey = String(item?.itemKey || item?.id || '').trim(); + if (!itemKey || !this.openParams?.courseId) return; + apiClient.patch('/api/vocab/srs/hard', { + courseId: this.openParams.courseId, + itemKey + }).catch((error) => { + console.warn('[VocabPracticeDialog] hard status could not be cleared:', error); + }); + }, + async finishSrsAnswer(isCorrect) { + if (!this.srsMode || !this.answered || this.locked) return; const id = this.current?.id; - const treatAsAgain = rating === 'again' || !this.lastCorrect; - const moveToHardPractice = treatAsAgain && this.isItemMarkedHard(this.current); - const retryPendingIds = Array.isArray(this.srsSession?.retryPendingIds) - ? this.srsSession.retryPendingIds - : []; - const needsFinalRepeat = this.lastCorrect && retryPendingIds.includes(id); - this.locked = true; - // The immediate correction after a wrong answer is a rehearsal, not the - // SRS result. Only the later repetition at the end is recorded as right. - if (!needsFinalRepeat) { - await this.reportSrsReview(this.lastCorrect, rating); - } + if (!isCorrect) this.markCurrentAsHard(); + await this.reportSrsReview(isCorrect); if (id && this.srsSession) { - 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); - } - if (!retryPendingIds.includes(id)) { - this.srsSession.retryPendingIds = [...retryPendingIds, id]; - } - } else if (needsFinalRepeat) { - // The correction was right, but the item must be recalled once more - // after the rest of today's cards before it counts as complete. - this.srsSession.retryPendingIds = retryPendingIds.filter((x) => x !== id); - const finalReviewIds = Array.isArray(this.srsSession.finalReviewIds) - ? this.srsSession.finalReviewIds - : []; - if (!finalReviewIds.includes(id)) { - this.srsSession.finalReviewIds = [...finalReviewIds, id]; - } - const remaining = Array.isArray(this.srsQueueIds) - ? this.srsQueueIds.filter((x) => x !== id) - : []; - remaining.push(id); - this.srsQueueIds = remaining; - } 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); - this.srsSession.doneIds = done; - } - this.srsSession.retryPendingIds = retryPendingIds.filter((x) => x !== id); - this.srsSession.finalReviewIds = (Array.isArray(this.srsSession.finalReviewIds) - ? this.srsSession.finalReviewIds - : []).filter((x) => x !== id); - if (Array.isArray(this.srsQueueIds) && this.srsQueueIds[0] === id) { - this.srsQueueIds = this.srsQueueIds.slice(1); - } else if (Array.isArray(this.srsQueueIds)) { - this.srsQueueIds = this.srsQueueIds.filter((x) => x !== id); - } + const done = Array.isArray(this.srsSession.doneIds) ? this.srsSession.doneIds : []; + if (!done.includes(id)) { + done.push(id); + this.srsSession.doneIds = done; } + this.srsQueueIds = Array.isArray(this.srsQueueIds) + ? this.srsQueueIds.filter((x) => x !== id) + : []; 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(); + this.autoAdvanceTimer = setTimeout(() => { + this.autoAdvanceTimer = null; + this.next(); + }, isCorrect ? 450 : 1300); }, submitChoice(opt) { if (this.locked) return; diff --git a/frontend/src/i18n/locales/de/socialnetwork.json b/frontend/src/i18n/locales/de/socialnetwork.json index a46e7fb..fdd5c33 100755 --- a/frontend/src/i18n/locales/de/socialnetwork.json +++ b/frontend/src/i18n/locales/de/socialnetwork.json @@ -870,10 +870,10 @@ "reviewTimeNow": "jetzt", "reviewTimeTomorrow": "morgen", "reviewTimeInDays": "in {count} Tagen", - "srsDueStat": "SRS fällig: {count}", + "srsDueStat": "Tageswiederholung: {scheduled} von {total}", "srsEyebrow": "Langzeitgedächtnis", - "srsTitle": "{count} Begriffe sind heute fällig", - "srsIntro": "Diese Wiederholung kommt aus dem SRS-Plan einzelner Begriffe. Sie hat Vorrang vor neuem Stoff, weil sie kurz vor dem Vergessen stabilisiert.", + "srsTitle": "Heute: {scheduled} von {total} fälligen Begriffen", + "srsIntro": "Diese Wiederholung kommt aus dem SRS-Plan einzelner Begriffe. Pro Tag werden höchstens 50 Begriffe eingeplant; weitere fällige Begriffe bleiben für die nächste Tageswiederholung erhalten.", "srsStart": "Tageswiederholung starten", "courseTodayPlanIntroSrs": "Didaktische Reihenfolge: Zuerst die fällige SRS-Tageswiederholung einzelner Begriffe. Danach kommen Kurz-Wiederholungen, Blockfortschritt und ggf. intensive Wiederholung. So wird altes Material stabilisiert, bevor neues Material dazukommt." } diff --git a/frontend/src/i18n/locales/en/socialnetwork.json b/frontend/src/i18n/locales/en/socialnetwork.json index 349f605..935c5c1 100755 --- a/frontend/src/i18n/locales/en/socialnetwork.json +++ b/frontend/src/i18n/locales/en/socialnetwork.json @@ -870,10 +870,10 @@ "reviewTimeNow": "now", "reviewTimeTomorrow": "tomorrow", "reviewTimeInDays": "in {count} days", - "srsDueStat": "SRS due: {count}", + "srsDueStat": "Daily review: {scheduled} of {total}", "srsEyebrow": "Long-term memory", - "srsTitle": "{count} terms are due today", - "srsIntro": "This review comes from the SRS schedule of individual terms. It should come before new material because it stabilizes items close to forgetting.", + "srsTitle": "Today: {scheduled} of {total} due terms", + "srsIntro": "This review comes from the SRS schedule of individual terms. At most 50 terms are planned per day; any additional due terms remain for the next daily review.", "srsStart": "Start daily review", "courseTodayPlanIntroSrs": "Pedagogical order: start with the due SRS daily review for individual terms. Then quick reviews, block progress, and intensive review if needed. This stabilizes old material before new material is added." } diff --git a/frontend/src/views/social/VocabCourseView.vue b/frontend/src/views/social/VocabCourseView.vue index fc28fed..c5c380f 100755 --- a/frontend/src/views/social/VocabCourseView.vue +++ b/frontend/src/views/social/VocabCourseView.vue @@ -46,13 +46,6 @@ {{ entry.learning }} {{ entry.reference }}
-
@@ -85,7 +78,7 @@

{{ $t('socialnetwork.vocab.courses.courseFlowIntro') }}

- {{ $t('socialnetwork.vocab.courses.srsDueStat', { count: srsDueCount }) }} + {{ $t('socialnetwork.vocab.courses.srsDueStat', { scheduled: srsDailyCount, total: srsDueCount }) }} {{ $t('socialnetwork.vocab.courses.courseFlowReviewStat', { count: dueReviewLessons.length }) }} {{ $t('socialnetwork.vocab.courses.courseFlowBlockStat', { block: currentBlockNumber || '—' }) }}
@@ -94,7 +87,7 @@
{{ $t('socialnetwork.vocab.courses.srsEyebrow') }} -

{{ $t('socialnetwork.vocab.courses.srsTitle', { count: srsDueCount }) }}

+

{{ $t('socialnetwork.vocab.courses.srsTitle', { scheduled: srsDailyCount, total: srsDueCount }) }}

{{ $t('socialnetwork.vocab.courses.srsIntro') }}

-
- - -