feat: erweitere VocabService um Unterstützung für Grammatikübungen aus vorherigen Lektionen
All checks were successful
Deploy to production / deploy (push) Successful in 2m6s
All checks were successful
Deploy to production / deploy (push) Successful in 2m6s
This commit is contained in:
@@ -2664,28 +2664,99 @@ export default class VocabService {
|
||||
if (plainLesson.lessonType === 'review' || plainLesson.lessonType === 'vocab_review') {
|
||||
return list;
|
||||
}
|
||||
const rows = await this._fetchChapterLexemeRowsForMc(plainLesson.chapterId);
|
||||
let rows = [];
|
||||
|
||||
// If this lesson belongs to a week, prefer vocab from previous lessons of the same week
|
||||
if (plainLesson.weekNumber) {
|
||||
try {
|
||||
const weekExercises = await this._getWeekVocabExercises(plainLesson.courseId, plainLesson.weekNumber, plainLesson.lessonNumber);
|
||||
const extracted = this._extractTrainerVocabsFromExercises(weekExercises || []);
|
||||
if (extracted && extracted.length) {
|
||||
// Map extracted pairs to row-like objects with numeric ids
|
||||
rows = extracted.map((item, idx) => ({ id: 2000000 + idx, learning: item.learning, reference: item.reference }));
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore and fallback to chapter lexemes
|
||||
rows = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
rows = await this._fetchChapterLexemeRowsForMc(plainLesson.chapterId);
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
plainLesson.chapterLexemeExamCount = 0;
|
||||
plainLesson.chapterLexemeTrainingCount = 0;
|
||||
plainLesson.chapterLexemeTraining = [];
|
||||
return list;
|
||||
}
|
||||
|
||||
const allReferences = rows.map((r) => r.reference).filter(Boolean);
|
||||
let maxNum = list.reduce((m, ex) => Math.max(m, Number(ex.exerciseNumber) || 0), 0);
|
||||
const augmentedRows = rows.map((r) => ({ ...r, allReferences }));
|
||||
for (const row of augmentedRows) {
|
||||
// Skip multi-word learning items (they are sentences, not single lexemes)
|
||||
|
||||
// Sampling parameters
|
||||
const EXAM_MAX = 15; // max items in the chapter exam
|
||||
const TRAINING_RATIO = 0.67; // fraction of chapter lexemes to include in training pool
|
||||
|
||||
const seed = (Number(plainLesson.id) * 100003) >>> 0;
|
||||
const shuffled = this._seededShuffle(augmentedRows.slice(), seed);
|
||||
|
||||
const totalRows = shuffled.length;
|
||||
const trainingTarget = Math.max(0, Math.min(totalRows, Math.ceil(totalRows * TRAINING_RATIO)));
|
||||
const examTarget = Math.max(0, Math.min(EXAM_MAX, totalRows));
|
||||
|
||||
const examSelected = [];
|
||||
const trainingSelected = [];
|
||||
|
||||
for (const row of shuffled) {
|
||||
const learningText = String(row.learning || '').trim();
|
||||
if (!learningText || learningText.split(/\s+/).length > 1) {
|
||||
// skip multi-word learning items for both exam and training
|
||||
continue;
|
||||
}
|
||||
if (this._lexemePairCoveredByMultipleChoice(list, row.learning, row.reference)) {
|
||||
continue;
|
||||
|
||||
// Fill training pool up to target (allow duplicates between exam and training)
|
||||
if (trainingSelected.length < trainingTarget) {
|
||||
trainingSelected.push(row);
|
||||
}
|
||||
|
||||
// For exam, also enforce that the pair is not already covered by an existing MC
|
||||
if (examSelected.length < examTarget && !this._lexemePairCoveredByMultipleChoice(list, row.learning, row.reference)) {
|
||||
examSelected.push(row);
|
||||
}
|
||||
|
||||
if (examSelected.length >= examTarget && trainingSelected.length >= trainingTarget) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if examSelected is smaller than examTarget, try to relax duplicate check
|
||||
if (examSelected.length < examTarget) {
|
||||
for (const row of shuffled) {
|
||||
if (examSelected.find((r) => Number(r.id) === Number(row.id))) continue;
|
||||
const learningText = String(row.learning || '').trim();
|
||||
if (!learningText || learningText.split(/\s+/).length > 1) continue;
|
||||
examSelected.push(row);
|
||||
if (examSelected.length >= examTarget) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Attach metadata for frontend/training use
|
||||
plainLesson.chapterLexemeExamCount = examSelected.length;
|
||||
plainLesson.chapterLexemeTrainingCount = trainingSelected.length;
|
||||
plainLesson.chapterLexemeTraining = trainingSelected.map((r) => ({ id: r.id, learning: r.learning, reference: r.reference }));
|
||||
|
||||
// Build synthetic exercises only for examSelected
|
||||
for (const row of examSelected) {
|
||||
maxNum += 1;
|
||||
const ex = this._buildSyntheticLexemeMcExercisePlain(plainLesson.id, row, maxNum);
|
||||
if (ex) {
|
||||
list.push(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -3066,6 +3137,35 @@ export default class VocabService {
|
||||
return exercises.map(e => e.get({ plain: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sammelt Grammatik‑Übungen aus vorherigen Lektionen derselben Woche
|
||||
*/
|
||||
async _getWeekVocabExercises(courseId, weekNumber, currentLessonNumber) {
|
||||
if (weekNumber == null) return [];
|
||||
const previousLessons = await VocabCourseLesson.findAll({
|
||||
where: {
|
||||
courseId: courseId,
|
||||
weekNumber: weekNumber,
|
||||
lessonNumber: { [Op.lt]: currentLessonNumber },
|
||||
lessonType: { [Op.notIn]: ['review', 'vocab_review'] }
|
||||
},
|
||||
attributes: ['id']
|
||||
});
|
||||
|
||||
if (previousLessons.length === 0) return [];
|
||||
const lessonIds = previousLessons.map(l => l.id);
|
||||
const exercises = await VocabGrammarExercise.findAll({
|
||||
where: { lessonId: { [Op.in]: lessonIds } },
|
||||
include: [
|
||||
{ model: VocabGrammarExerciseType, as: 'exerciseType' },
|
||||
{ model: VocabCourseLesson, as: 'lesson', attributes: ['id', 'lessonNumber', 'title'] }
|
||||
],
|
||||
order: [[{ model: VocabCourseLesson, as: 'lesson' }, 'lessonNumber', 'ASC'], ['exerciseNumber', 'ASC']]
|
||||
});
|
||||
|
||||
return exercises.map(e => e.get({ plain: true }));
|
||||
}
|
||||
|
||||
async addLessonToCourse(hashedUserId, courseId, { chapterId, lessonNumber, title, description, weekNumber, dayNumber, lessonType, didacticMode, phaseLabel, blockNumber, difficultyWeight, newUnitTarget, reviewWeight, isIntensiveReview, audioUrl, culturalNotes, learningGoals, corePatterns, grammarFocus, speakingPrompts, practicalTasks, targetMinutes, targetScorePercent, requiresReview }) {
|
||||
const user = await this._getUserByHashedId(hashedUserId);
|
||||
const course = await VocabCourse.findByPk(courseId);
|
||||
|
||||
Reference in New Issue
Block a user