diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index 4da83b3..e7df88d 100644 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -42,7 +42,7 @@ export default class VocabService { .map((entry) => { const learning = String(entry?.learning || '').trim(); const reference = String(entry?.reference || '').trim(); - if (!learning || !reference || this._normalizeSrsText(learning) === this._normalizeSrsText(reference)) { + if (!this._isTrainableSrsPair({ learning, reference })) { return null; } const direction = String(entry?.direction || 'BOTH').toUpperCase(); @@ -61,6 +61,30 @@ export default class VocabService { .filter(Boolean); } + _isInstructionLikeText(value) { + const text = String(value || '').trim(); + if (!text) { + return false; + } + + const wordCount = text.split(/\s+/).filter(Boolean).length; + if (wordCount < 3) { + return false; + } + + return /^(sage|sag|frage|frag|bitte|stelle|sprich|erzähle|beschreibe|bilde|wähle|ordne|übersetze|nenne|nenn|beginne|verwende|reagiere|kombiniere|spiele|löse|beantworte|ergänze|formuliere)\b/i.test(text); + } + + _isTrainableSrsPair(entry) { + const learning = String(entry?.learning || '').trim(); + const reference = String(entry?.reference || '').trim(); + if (!learning || !reference || this._normalizeSrsText(learning) === this._normalizeSrsText(reference)) { + return false; + } + + return !this._isInstructionLikeText(learning) && !this._isInstructionLikeText(reference); + } + _calculateSrsSchedule(item, { correct, rating = null } = {}) { const now = new Date(); const previousStage = Math.max(0, Number(item?.stage) || 0); @@ -459,16 +483,17 @@ export default class VocabService { } async _courseHasDueSrsItems(userId, courseId) { - const due = await VocabSrsItem.count({ + const dueItems = await VocabSrsItem.findAll({ where: { userId, courseId: Number(courseId), nextDueAt: { [Op.lte]: new Date() } - } + }, + attributes: ['learning', 'reference'] }); - return due > 0; + return dueItems.some((item) => this._isTrainableSrsPair(item)); } async _getUserByHashedId(hashedUserId) { @@ -713,7 +738,7 @@ export default class VocabService { const question = String(qData.question || qData.text || ''); let match = question.match(/Wie sagt man ['"]([^'"]+)['"]/i); - if (match && match[1] && correctAnswer && match[1].trim() !== String(correctAnswer).trim()) { + if (match && this._isTrainableSrsPair({ learning: match[1], reference: String(correctAnswer) })) { vocabMap.set(`${match[1]}-${correctAnswer}`, { learning: match[1], reference: String(correctAnswer) @@ -722,7 +747,7 @@ export default class VocabService { } match = question.match(/Was bedeutet ['"]([^'"]+)['"]/i); - if (match && match[1] && correctAnswer && match[1].trim() !== String(correctAnswer).trim()) { + if (match && this._isTrainableSrsPair({ learning: String(correctAnswer), reference: match[1] })) { vocabMap.set(`${correctAnswer}-${match[1]}`, { learning: String(correctAnswer), reference: match[1] @@ -745,7 +770,7 @@ export default class VocabService { answers.forEach((answer, index) => { const nativeWord = nativeWords[index]; const normalizedAnswer = String(answer || '').trim(); - if (!nativeWord || !normalizedAnswer || nativeWord === normalizedAnswer) { + if (!this._isTrainableSrsPair({ learning: nativeWord, reference: normalizedAnswer })) { return; } vocabMap.set(`${nativeWord}-${normalizedAnswer}`, { @@ -764,31 +789,13 @@ export default class VocabService { _extractTrainerVocabsFromLessonDidactics(lesson) { const vocabMap = new Map(); - const speakingPrompts = Array.isArray(lesson?.speakingPrompts) ? lesson.speakingPrompts : []; - const practicalTasks = Array.isArray(lesson?.practicalTasks) ? lesson.practicalTasks : []; const corePatterns = Array.isArray(lesson?.corePatterns) ? lesson.corePatterns : []; corePatterns.forEach((entry) => { const pattern = this._normalizeCorePatternEntry(entry); const reference = String(pattern?.target || '').trim(); const learning = String(pattern?.gloss || '').trim(); - if (!learning || !reference || learning === reference) return; - vocabMap.set(`${learning}-${reference}`, { learning, reference }); - }); - - speakingPrompts.forEach((prompt, index) => { - const learning = String(prompt?.prompt || prompt?.title || '').trim(); - const refEntry = corePatterns[index] ?? corePatterns[0]; - const reference = String(prompt?.cue || this._corePatternTarget(refEntry) || '').trim(); - if (!learning || !reference || learning === reference) return; - vocabMap.set(`${learning}-${reference}`, { learning, reference }); - }); - - practicalTasks.forEach((task, index) => { - const learning = String(task?.text || task?.title || '').trim(); - const refEntry = corePatterns[index] ?? corePatterns[0]; - const reference = String(this._corePatternTarget(refEntry) || '').trim(); - if (!learning || !reference || learning === reference) return; + if (!this._isTrainableSrsPair({ learning, reference })) return; vocabMap.set(`${learning}-${reference}`, { learning, reference }); }); @@ -1808,16 +1815,17 @@ export default class VocabService { [Op.lte]: now } }; - const totalDueCount = await VocabSrsItem.count({ where: dueWhere }); - const rows = await VocabSrsItem.findAll({ + const dueRows = await VocabSrsItem.findAll({ where: dueWhere, order: [ ['nextDueAt', 'ASC'], ['wrongCount', 'DESC'], ['stage', 'ASC'] - ], - limit + ] }); + const validDueRows = dueRows.filter((item) => this._isTrainableSrsPair(item)); + const rows = validDueRows.slice(0, limit); + const totalDueCount = validDueRows.length; return { courseId: course.id,