feat(exercises): enhance context in gap fill exercises and update migration for route descriptions
All checks were successful
Deploy to production / deploy (push) Successful in 3m21s
All checks were successful
Deploy to production / deploy (push) Successful in 3m21s
This commit is contained in:
@@ -3617,7 +3617,7 @@ export default class VocabService {
|
||||
const seed = (Number(lessonId) * 100003) >>> 0;
|
||||
const percentage = 40 + (seed % 21);
|
||||
const targetCount = Math.max(1, Math.ceil((list.length * percentage) / 100));
|
||||
return this._seededShuffle(list.slice(), seed).slice(0, targetCount);
|
||||
return this._selectConcreteExamExercises(list, targetCount, seed);
|
||||
}
|
||||
|
||||
_selectCheckpointExamExercises(exercises = [], lessonId) {
|
||||
@@ -3628,7 +3628,73 @@ export default class VocabService {
|
||||
// Checkpoints: smaller sample ~10-30%
|
||||
const percentage = 10 + (seed % 21);
|
||||
const targetCount = Math.max(1, Math.ceil((list.length * percentage) / 100));
|
||||
return this._seededShuffle(list.slice(), seed).slice(0, targetCount);
|
||||
return this._selectConcreteExamExercises(list, targetCount, seed);
|
||||
}
|
||||
|
||||
_getExamExerciseQuestionType(exercise) {
|
||||
const questionData = typeof exercise?.questionData === 'string'
|
||||
? JSON.parse(exercise.questionData)
|
||||
: (exercise?.questionData || {});
|
||||
return String(questionData.type || exercise?.exerciseType?.name || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
_isGenericPatternPrompt(exercise) {
|
||||
const questionData = typeof exercise?.questionData === 'string'
|
||||
? JSON.parse(exercise.questionData)
|
||||
: (exercise?.questionData || {});
|
||||
const prompt = String(questionData.question || questionData.text || exercise?.instruction || '');
|
||||
return /(?:kernmuster|zentrales? muster)/i.test(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Review and checkpoint exams must be concrete and reliably checkable.
|
||||
* Prefer gap fills (one is included whenever available), then multiple
|
||||
* choice, and use open "core pattern" prompts only when there is no other
|
||||
* material available at all.
|
||||
*/
|
||||
_selectConcreteExamExercises(exercises = [], targetCount, seed) {
|
||||
const list = Array.isArray(exercises) ? exercises : [];
|
||||
const groups = {
|
||||
gap: [],
|
||||
multipleChoice: [],
|
||||
concreteOther: [],
|
||||
fallback: []
|
||||
};
|
||||
|
||||
for (const exercise of list) {
|
||||
const type = this._getExamExerciseQuestionType(exercise);
|
||||
if (type === 'gap_fill') {
|
||||
groups.gap.push(exercise);
|
||||
} else if (type === 'multiple_choice') {
|
||||
groups.multipleChoice.push(exercise);
|
||||
} else if (!this._isGenericPatternPrompt(exercise)
|
||||
&& !['speaking_from_memory', 'reading_aloud'].includes(type)) {
|
||||
groups.concreteOther.push(exercise);
|
||||
} else {
|
||||
groups.fallback.push(exercise);
|
||||
}
|
||||
}
|
||||
|
||||
const selected = [];
|
||||
const addUntilFull = (items, groupSeed) => {
|
||||
for (const exercise of this._seededShuffle(items, groupSeed)) {
|
||||
if (selected.length >= targetCount) break;
|
||||
selected.push(exercise);
|
||||
}
|
||||
};
|
||||
|
||||
// A gap fill is a good, unambiguous opening question and avoids exams
|
||||
// consisting only of abstract sentence-building prompts. Keep the rest
|
||||
// balanced so a larger exam does not become a list of gaps only.
|
||||
const shuffledGaps = this._seededShuffle(groups.gap, (seed ^ 0x1f123bb5) >>> 0);
|
||||
if (shuffledGaps.length > 0) {
|
||||
selected.push(shuffledGaps.shift());
|
||||
}
|
||||
addUntilFull(groups.multipleChoice, (seed ^ 0x39a1f8c7) >>> 0);
|
||||
addUntilFull(shuffledGaps, (seed ^ 0x2d98c6f1) >>> 0);
|
||||
addUntilFull(groups.concreteOther, (seed ^ 0x5bd1e995) >>> 0);
|
||||
addUntilFull(groups.fallback, (seed ^ 0x7f4a7c15) >>> 0);
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user