feat(vocab): add vocab distractor pool functionality
All checks were successful
Deploy to production / deploy (push) Successful in 3m9s
All checks were successful
Deploy to production / deploy (push) Successful in 3m9s
- Implemented a new endpoint to retrieve a pool of distractors for vocabulary exercises based on prior lessons. - Updated the VocabController and VocabRouter to include the new getVocabDistractorPool method. - Enhanced VocabService to classify questions and gather distractors from previous lessons. - Modified VocabLessonView to fetch and utilize the distractor pool for multiple-choice exercises, improving the learning experience.
This commit is contained in:
@@ -735,7 +735,11 @@ export default {
|
||||
nextLessonId: null,
|
||||
showCompletionDialog: false,
|
||||
showErrorDialog: false,
|
||||
errorMessage: ''
|
||||
errorMessage: '',
|
||||
/** Aus vorherigen Lektionen (MC-Optionen nach Fragentyp Ziel-/Muttersprache) */
|
||||
distractorPool: { target: [], native: [] },
|
||||
/** { [exerciseId]: { options: string[], useTextAnswer: boolean } } */
|
||||
mcRandomizedOptions: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -1091,6 +1095,8 @@ export default {
|
||||
this.vocabTrainerPhase = 'current';
|
||||
this.vocabTrainerCurrentAttempts = 0;
|
||||
this.vocabTrainerReviewAttempts = 0;
|
||||
this.distractorPool = { target: [], native: [] };
|
||||
this.mcRandomizedOptions = {};
|
||||
// Reset Flags
|
||||
this.isCheckingLessonCompletion = false;
|
||||
this.isNavigatingToNext = false;
|
||||
@@ -1116,17 +1122,27 @@ export default {
|
||||
this.focusAssistantCard();
|
||||
});
|
||||
}
|
||||
// Initialisiere mit effectiveExercises (für Review: reviewVocabExercises, sonst: grammarExercises)
|
||||
this.$nextTick(async () => {
|
||||
const exercises = this.effectiveExercises;
|
||||
if (exercises && exercises.length > 0) {
|
||||
debugLog('[VocabLessonView] Übungen für Kapitel-Prüfung:', exercises.length);
|
||||
this.initializeExercises(exercises);
|
||||
} else {
|
||||
debugLog('[VocabLessonView] Lade Übungen separat...');
|
||||
await this.loadGrammarExercises();
|
||||
}
|
||||
});
|
||||
try {
|
||||
const poolRes = await apiClient.get(`/api/vocab/courses/${this.courseId}/distractor-pool`, {
|
||||
params: { beforeLessonId: this.lessonId }
|
||||
});
|
||||
this.distractorPool = poolRes.data || { target: [], native: [] };
|
||||
} catch (poolErr) {
|
||||
console.warn('[VocabLessonView] Distraktor-Pool nicht geladen:', poolErr);
|
||||
this.distractorPool = { target: [], native: [] };
|
||||
}
|
||||
await this.$nextTick();
|
||||
let exercises = this.effectiveExercises;
|
||||
if (!exercises || exercises.length === 0) {
|
||||
debugLog('[VocabLessonView] Lade Übungen separat...');
|
||||
await this.loadGrammarExercises();
|
||||
exercises = this.effectiveExercises;
|
||||
}
|
||||
if (exercises && exercises.length > 0) {
|
||||
debugLog('[VocabLessonView] Übungen für Kapitel-Prüfung:', exercises.length);
|
||||
this.initializeExercises(exercises);
|
||||
this.buildMcRandomizedOptions();
|
||||
}
|
||||
debugLog('[VocabLessonView] loadLesson abgeschlossen');
|
||||
} catch (e) {
|
||||
console.error('[VocabLessonView] Fehler beim Laden der Lektion:', e);
|
||||
@@ -1224,7 +1240,6 @@ export default {
|
||||
const res = await apiClient.get(`/api/vocab/lessons/${this.lessonId}/grammar-exercises`);
|
||||
const exercises = res.data || [];
|
||||
this.lesson.grammarExercises = exercises;
|
||||
this.initializeExercises(exercises);
|
||||
} catch (e) {
|
||||
console.error('Konnte Grammatik-Übungen nicht laden:', e);
|
||||
this.lesson.grammarExercises = [];
|
||||
@@ -1284,6 +1299,74 @@ export default {
|
||||
? JSON.parse(exercise.answerData)
|
||||
: exercise.answerData;
|
||||
},
|
||||
_shuffleArray(arr) {
|
||||
const a = [...arr];
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
},
|
||||
/** Muss zu backend/services/vocabService _classifyMcQuestionSide passen */
|
||||
_classifyMcQuestionSide(question) {
|
||||
const q = String(question || '');
|
||||
if (/Wie sagt man\s/i.test(q) || /Übersetze/i.test(q)) return 'target';
|
||||
if (/Was bedeutet/i.test(q)) return 'native';
|
||||
return 'unknown';
|
||||
},
|
||||
/**
|
||||
* Zufällige Distraktoren aus gelernten Vokabeln (vorherige Lektionen), gleiche Sprache wie die richtigen Optionen.
|
||||
* Deaktivierbar pro Übung: questionData.randomizeDistractors === false
|
||||
*/
|
||||
randomizeMcOptionsIfPossible(exercise) {
|
||||
const q = this.getQuestionData(exercise);
|
||||
const a = this.getAnswerData(exercise);
|
||||
if (!q || !a || q.type !== 'multiple_choice' || q.randomizeDistractors === false) return null;
|
||||
const qtext = q.question || '';
|
||||
const side = this._classifyMcQuestionSide(qtext);
|
||||
if (side === 'unknown') return null;
|
||||
const options = q.options || [];
|
||||
let correctIndices = [];
|
||||
if (a.correctAnswer !== undefined) {
|
||||
correctIndices = Array.isArray(a.correctAnswer) ? a.correctAnswer.map(Number) : [Number(a.correctAnswer)];
|
||||
} else if (a.correct !== undefined) {
|
||||
correctIndices = Array.isArray(a.correct) ? a.correct.map(Number) : [Number(a.correct)];
|
||||
}
|
||||
const correctTexts = correctIndices.map((i) => options[i]).filter((t) => t !== undefined && t !== null);
|
||||
if (!correctTexts.length) return null;
|
||||
const totalSlots = options.length;
|
||||
const need = totalSlots - correctTexts.length;
|
||||
if (need <= 0) return null;
|
||||
const norm = (s) => String(s).trim().toLowerCase();
|
||||
const correctSet = new Set(correctTexts.map(norm));
|
||||
const poolRaw = side === 'target' ? this.distractorPool.target : this.distractorPool.native;
|
||||
let poolFiltered = poolRaw.filter((w) => w && !correctSet.has(norm(w)));
|
||||
poolFiltered = this._shuffleArray(poolFiltered);
|
||||
let picked = poolFiltered.slice(0, need);
|
||||
if (picked.length < need) {
|
||||
const wrongFromDb = options.filter((_, i) => !correctIndices.includes(i));
|
||||
const shuffledWrong = this._shuffleArray(wrongFromDb);
|
||||
for (const w of shuffledWrong) {
|
||||
if (picked.length >= need) break;
|
||||
if (!picked.some((p) => norm(p) === norm(w))) picked.push(w);
|
||||
}
|
||||
}
|
||||
if (picked.length < need) return null;
|
||||
const all = this._shuffleArray([...correctTexts, ...picked]);
|
||||
return { options: all, useTextAnswer: true };
|
||||
},
|
||||
buildMcRandomizedOptions() {
|
||||
this.mcRandomizedOptions = {};
|
||||
const exercises = this.effectiveExercises;
|
||||
if (!exercises) return;
|
||||
exercises.forEach((ex) => {
|
||||
if (this.getExerciseType(ex) !== 'multiple_choice') return;
|
||||
const built = this.randomizeMcOptionsIfPossible(ex);
|
||||
if (built) {
|
||||
this.mcRandomizedOptions[ex.id] = built;
|
||||
}
|
||||
});
|
||||
},
|
||||
getQuestionText(exercise) {
|
||||
const qData = this.getQuestionData(exercise);
|
||||
if (!qData) return exercise.title;
|
||||
@@ -1301,6 +1384,10 @@ export default {
|
||||
return exercise.title;
|
||||
},
|
||||
getOptions(exercise) {
|
||||
const custom = this.mcRandomizedOptions[exercise.id];
|
||||
if (custom && Array.isArray(custom.options) && custom.options.length > 0) {
|
||||
return custom.options;
|
||||
}
|
||||
const qData = this.getQuestionData(exercise);
|
||||
return qData?.options || [];
|
||||
},
|
||||
@@ -1340,8 +1427,15 @@ export default {
|
||||
answer = [answer];
|
||||
}
|
||||
} else if (exerciseType === 'multiple_choice') {
|
||||
// Multiple Choice: Index als Zahl
|
||||
answer = Number(answer);
|
||||
const ro = this.mcRandomizedOptions[exercise.id];
|
||||
if (ro && ro.useTextAnswer) {
|
||||
const opts = this.getOptions(exercise);
|
||||
const idx = Number(answer);
|
||||
const pick = opts[idx];
|
||||
answer = pick !== undefined && pick !== null ? String(pick) : '';
|
||||
} else {
|
||||
answer = Number(answer);
|
||||
}
|
||||
} else if (exerciseType === 'transformation' || exerciseType === 'sentence_building' || exerciseType === 'dialog_completion' || exerciseType === 'pattern_drill') {
|
||||
// Transformation: String
|
||||
answer = String(answer || '').trim();
|
||||
|
||||
Reference in New Issue
Block a user