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') {
|
if (plainLesson.lessonType === 'review' || plainLesson.lessonType === 'vocab_review') {
|
||||||
return list;
|
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) {
|
if (!rows.length) {
|
||||||
|
rows = await this._fetchChapterLexemeRowsForMc(plainLesson.chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
plainLesson.chapterLexemeExamCount = 0;
|
||||||
|
plainLesson.chapterLexemeTrainingCount = 0;
|
||||||
|
plainLesson.chapterLexemeTraining = [];
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allReferences = rows.map((r) => r.reference).filter(Boolean);
|
const allReferences = rows.map((r) => r.reference).filter(Boolean);
|
||||||
let maxNum = list.reduce((m, ex) => Math.max(m, Number(ex.exerciseNumber) || 0), 0);
|
let maxNum = list.reduce((m, ex) => Math.max(m, Number(ex.exerciseNumber) || 0), 0);
|
||||||
const augmentedRows = rows.map((r) => ({ ...r, allReferences }));
|
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();
|
const learningText = String(row.learning || '').trim();
|
||||||
if (!learningText || learningText.split(/\s+/).length > 1) {
|
if (!learningText || learningText.split(/\s+/).length > 1) {
|
||||||
|
// skip multi-word learning items for both exam and training
|
||||||
continue;
|
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;
|
maxNum += 1;
|
||||||
const ex = this._buildSyntheticLexemeMcExercisePlain(plainLesson.id, row, maxNum);
|
const ex = this._buildSyntheticLexemeMcExercisePlain(plainLesson.id, row, maxNum);
|
||||||
if (ex) {
|
if (ex) {
|
||||||
list.push(ex);
|
list.push(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3066,6 +3137,35 @@ export default class VocabService {
|
|||||||
return exercises.map(e => e.get({ plain: true }));
|
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 }) {
|
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 user = await this._getUserByHashedId(hashedUserId);
|
||||||
const course = await VocabCourse.findByPk(courseId);
|
const course = await VocabCourse.findByPk(courseId);
|
||||||
|
|||||||
Reference in New Issue
Block a user