feat(vocabService, VocabPracticeDialog, VocabLessonView): enhance vocabulary handling and exposure tracking
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:
Torsten Schulz (local)
2026-04-17 08:58:50 +02:00
parent d119869750
commit 54a77c2e08
5 changed files with 139 additions and 14 deletions

View File

@@ -105,6 +105,8 @@
import DialogWidget from '@/components/DialogWidget.vue';
import apiClient from '@/utils/axios.js';
const PRACTICE_MIN_EXPOSURES = 3;
export default {
name: 'VocabPracticeDialog',
components: { DialogWidget },
@@ -233,6 +235,29 @@ export default {
.trim();
return normalized.replace(/\s+/g, '');
},
normalizePool(items = []) {
const seen = new Set();
return (Array.isArray(items) ? items : [])
.map((item, index) => {
const learning = String(item?.learning || '').trim();
const reference = String(item?.reference || '').trim();
if (!learning || !reference || this.normalize(learning) === this.normalize(reference)) {
return null;
}
const key = `${this.normalize(learning)}|${this.normalize(reference)}`;
if (seen.has(key)) {
return null;
}
seen.add(key);
return {
...item,
id: item?.id || item?.key || `${key}|${index}`,
learning,
reference
};
})
.filter(Boolean);
},
resetQuestion() {
this.current = null;
this.direction = this.openParams?.lessonId ? 'L2R' : (Math.random() < 0.5 ? 'L2R' : 'R2L');
@@ -283,17 +308,17 @@ export default {
untilLessonId: this.openParams.lessonId
}
});
this.pool = res.data?.vocabs || [];
this.pool = this.normalizePool(res.data?.vocabs || []);
} else {
res = await apiClient.get(`/api/vocab/lessons/${this.openParams.lessonId}/vocab-pool`);
this.pool = res.data?.vocabs || [];
this.pool = this.normalizePool(res.data?.vocabs || []);
}
} else if (this.allVocabs) {
res = await apiClient.get(`/api/vocab/languages/${this.openParams.languageId}/vocabs`);
this.pool = res.data?.vocabs || [];
this.pool = this.normalizePool(res.data?.vocabs || []);
} else {
res = await apiClient.get(`/api/vocab/chapters/${this.openParams.chapterId}/vocabs`);
this.pool = res.data?.vocabs || [];
this.pool = this.normalizePool(res.data?.vocabs || []);
}
} catch (e) {
console.error('Reload pool failed:', e);
@@ -331,6 +356,26 @@ export default {
pickNextItem() {
const items = this.pool;
if (!items || items.length === 0) return null;
const recent = new Set(this.lastIds);
const underexposed = items
.map((item) => {
const st = this.perId[item.id] || { c: 0, w: 0, streak: 0, lastAsked: 0 };
return {
item,
attempts: (Number(st.c) || 0) + (Number(st.w) || 0),
wrong: Number(st.w) || 0
};
})
.filter((entry) => entry.attempts < PRACTICE_MIN_EXPOSURES && !recent.has(entry.item.id))
.sort((a, b) => {
if (a.attempts !== b.attempts) return a.attempts - b.attempts;
if (a.wrong !== b.wrong) return b.wrong - a.wrong;
return String(a.item.id).localeCompare(String(b.item.id));
});
if (underexposed.length > 0) {
return underexposed[0].item;
}
const weights = items.map((it) => this.computeWeight(it));
const sum = weights.reduce((a, b) => a + b, 0);
let r = Math.random() * sum;