feat(vocabService, VocabPracticeDialog, VocabLessonView): enhance vocabulary handling and exposure tracking
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
- Updated vocabService to merge extracted vocabularies and improve handling of learning and reference pairs. - Introduced normalization and exposure tracking in VocabPracticeDialog to ensure diverse and underexposed vocabulary practice. - Enhanced VocabLessonView with methods to identify underexposed vocabularies and adjust selection logic for improved learning outcomes. - Implemented new constants for minimum exposure requirements to optimize vocabulary training sessions.
This commit is contained in:
@@ -1001,6 +1001,7 @@ import apiClient from '@/utils/axios.js';
|
||||
const debugLog = () => {};
|
||||
const LESSON_STATE_VERSION = 1;
|
||||
const VOCAB_REPEAT_INTERVALS = [1, 2, 4];
|
||||
const VOCAB_MIN_CURRENT_EXPOSURES = 3;
|
||||
/** Mindest-Erfolgsquote im Vokabeltrainer (gesamt), damit die Kapitel-Prüfung freigeschaltet wird. */
|
||||
const EXERCISE_UNLOCK_MIN_SUCCESS_PERCENT = 70;
|
||||
/** Max. Zeilen pro Seite in der einklappbaren Vokabel-Gesamtübersicht */
|
||||
@@ -1123,9 +1124,20 @@ export default {
|
||||
const vocabCount = this.trainableLessonVocab?.length || 0;
|
||||
const exerciseCount = this.effectiveExercises?.length || 0;
|
||||
const durationBonus = Math.max(0, Math.round((this.lesson?.targetMinutes || 0) / 5) - 1);
|
||||
const coverageTarget = vocabCount * this.trainerMinimumCurrentExposures;
|
||||
const baseTarget = Math.ceil((Math.max(vocabCount, 4) * 1.35) + (exerciseCount * 0.35) + durationBonus);
|
||||
const weightedTarget = Math.ceil(baseTarget * this.lessonComplexityWeight);
|
||||
return Math.max(6, Math.min(120, weightedTarget));
|
||||
return Math.max(6, Math.min(160, Math.max(weightedTarget, coverageTarget)));
|
||||
},
|
||||
trainerMinimumCurrentExposures() {
|
||||
const mode = this.lessonPedagogy?.didacticMode || this.lesson?.lessonType || '';
|
||||
if (mode === 'intensive_review' || mode === 'review' || mode === 'vocab_review') {
|
||||
return 2;
|
||||
}
|
||||
if (['grammar', 'dialogue', 'phrases', 'survival'].includes(this.lesson?.lessonType)) {
|
||||
return 4;
|
||||
}
|
||||
return VOCAB_MIN_CURRENT_EXPOSURES;
|
||||
},
|
||||
trainerReviewBlendStart() {
|
||||
return Math.max(3, Math.ceil(this.trainerNewFocusTarget * 0.4));
|
||||
@@ -3270,6 +3282,53 @@ export default {
|
||||
.map((entry) => entry.key)
|
||||
);
|
||||
},
|
||||
getUnderexposedCurrentVocabs(pool = this.trainableLessonVocab) {
|
||||
const uniqueByKey = new Map();
|
||||
(pool || []).forEach((vocab) => {
|
||||
const key = this.getVocabKey(vocab);
|
||||
if (!uniqueByKey.has(key)) {
|
||||
uniqueByKey.set(key, vocab);
|
||||
}
|
||||
});
|
||||
|
||||
const underexposed = Array.from(uniqueByKey.values())
|
||||
.map((vocab) => {
|
||||
const stats = this.getVocabStats(vocab);
|
||||
return {
|
||||
vocab,
|
||||
attempts: Number(stats.attempts) || 0,
|
||||
wrong: Number(stats.wrong) || 0
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry.attempts < this.trainerMinimumCurrentExposures)
|
||||
.sort((a, b) => {
|
||||
if (a.attempts !== b.attempts) return a.attempts - b.attempts;
|
||||
if (a.wrong !== b.wrong) return b.wrong - a.wrong;
|
||||
return this.getVocabKey(a.vocab).localeCompare(this.getVocabKey(b.vocab));
|
||||
});
|
||||
|
||||
return underexposed.map((entry) => entry.vocab);
|
||||
},
|
||||
chooseVocabFromPool(pool = []) {
|
||||
if (!pool.length) return null;
|
||||
const ranked = pool
|
||||
.map((vocab) => {
|
||||
const stats = this.getVocabStats(vocab);
|
||||
return {
|
||||
vocab,
|
||||
attempts: Number(stats.attempts) || 0,
|
||||
wrong: Number(stats.wrong) || 0,
|
||||
correct: Number(stats.correct) || 0
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.attempts !== b.attempts) return a.attempts - b.attempts;
|
||||
if (a.wrong !== b.wrong) return b.wrong - a.wrong;
|
||||
if (a.correct !== b.correct) return a.correct - b.correct;
|
||||
return Math.random() - 0.5;
|
||||
});
|
||||
return ranked[0]?.vocab || pool[Math.floor(Math.random() * pool.length)];
|
||||
},
|
||||
continueAfterVocabAnswer() {
|
||||
const completedKey = this.currentVocabQuestion?.key || '';
|
||||
this.advanceRepeatQueue(completedKey);
|
||||
@@ -3461,8 +3520,18 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * sourcePool.length);
|
||||
const vocab = sourcePool[randomIndex];
|
||||
if (!dueRepeatVocab && questionSource === 'current' && this.vocabTrainerMode === 'multiple_choice') {
|
||||
const underexposed = this.getUnderexposedCurrentVocabs(sourcePool);
|
||||
if (underexposed.length > 0) {
|
||||
sourcePool = underexposed;
|
||||
}
|
||||
}
|
||||
|
||||
const vocab = this.chooseVocabFromPool(sourcePool);
|
||||
if (!vocab) {
|
||||
this.currentVocabQuestion = null;
|
||||
return;
|
||||
}
|
||||
this.vocabTrainerDirection = Math.random() < 0.5 ? 'L2R' : 'R2L';
|
||||
const allTrainerVocabs = [...this.trainableLessonVocab, ...this.vocabTrainerMixedPool];
|
||||
const prompt = this.vocabTrainerDirection === 'L2R' ? vocab.learning : vocab.reference;
|
||||
|
||||
Reference in New Issue
Block a user