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:
@@ -27,6 +27,9 @@ class VocabController {
|
||||
this.createCourse = this._wrapWithUser((userId, req) => this.service.createCourse(userId, req.body), { successStatus: 201 });
|
||||
this.getCourses = this._wrapWithUser((userId, req) => this.service.getCourses(userId, req.query));
|
||||
this.getCourse = this._wrapWithUser((userId, req) => this.service.getCourse(userId, req.params.courseId));
|
||||
this.getVocabDistractorPool = this._wrapWithUser((userId, req) =>
|
||||
this.service.getVocabDistractorPool(userId, req.params.courseId, req.query.beforeLessonId)
|
||||
);
|
||||
this.getCourseByShareCode = this._wrapWithUser((userId, req) => this.service.getCourseByShareCode(userId, req.body.shareCode));
|
||||
this.updateCourse = this._wrapWithUser((userId, req) => this.service.updateCourse(userId, req.params.courseId, req.body));
|
||||
this.deleteCourse = this._wrapWithUser((userId, req) => this.service.deleteCourse(userId, req.params.courseId));
|
||||
|
||||
@@ -28,6 +28,7 @@ router.post('/courses', vocabController.createCourse);
|
||||
router.get('/courses', vocabController.getCourses);
|
||||
router.get('/courses/my', vocabController.getMyCourses);
|
||||
router.post('/courses/find-by-code', vocabController.getCourseByShareCode);
|
||||
router.get('/courses/:courseId/distractor-pool', vocabController.getVocabDistractorPool);
|
||||
router.get('/courses/:courseId', vocabController.getCourse);
|
||||
router.put('/courses/:courseId', vocabController.updateCourse);
|
||||
router.delete('/courses/:courseId', vocabController.deleteCourse);
|
||||
|
||||
@@ -107,13 +107,14 @@ const BISAYA_EXERCISES = {
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Babay"?',
|
||||
options: ['Tschüss / Auf Wiedersehen', 'Wie geht es dir?', 'Guten Tag', 'Ich bin müde']
|
||||
options: ['Tschüss', 'Auf Wiedersehen', 'Wie geht es dir?', 'Guten Tag']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
// Beide gelten als richtig (Lehnwort von „bye-bye“)
|
||||
correctAnswer: [0, 1]
|
||||
},
|
||||
explanation: '"Babay" ist eine einfache alltägliche Verabschiedung.'
|
||||
explanation: '"Babay" ist eine einfache Verabschiedung — vergleichbar mit „Tschüss“ oder „Auf Wiedersehen“.'
|
||||
},
|
||||
withTypeName('dialog_completion', {
|
||||
title: 'Begrüßungsdialog ergänzen',
|
||||
|
||||
@@ -1932,9 +1932,22 @@ export default class VocabService {
|
||||
}
|
||||
|
||||
if (correctIndices.length === 0) return false;
|
||||
|
||||
// userAnswer ist der Index (0, 1, 2, ...)
|
||||
|
||||
const correctTexts = correctIndices
|
||||
.map((i) => options[i])
|
||||
.filter((opt) => opt !== undefined && opt !== null);
|
||||
const norm = (s) => String(s).trim().toLowerCase();
|
||||
|
||||
// Nach zufälligen Distraktoren: Client sendet gewählten Optionstext statt Index
|
||||
if (typeof userAnswer === 'string') {
|
||||
const u = norm(userAnswer);
|
||||
if (!u) return false;
|
||||
return correctTexts.some((t) => norm(t) === u);
|
||||
}
|
||||
|
||||
// Legacy: Index in die gespeicherten (nicht gemischten) Optionen
|
||||
const userIndex = Number(userAnswer);
|
||||
if (Number.isNaN(userIndex)) return false;
|
||||
return correctIndices.includes(userIndex);
|
||||
}
|
||||
|
||||
@@ -2083,4 +2096,88 @@ export default class VocabService {
|
||||
await exercise.destroy();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordnet eine Multiple-Choice-Frage der Zielsprache (zu lernen) oder Muttersprache (Erklärung) zu,
|
||||
* damit Distraktoren aus dem passenden Wortpool gewählt werden können.
|
||||
* @returns {'target'|'native'|'unknown'}
|
||||
*/
|
||||
_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';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sammelt Vokabeln aus allen Multiple-Choice-Übungen von Lektionen **vor** der angegebenen Lektion
|
||||
* (gleicher Kurs), getrennt nach Ziel- vs. Muttersprache anhand der Frageformulierung.
|
||||
*/
|
||||
async getVocabDistractorPool(hashedUserId, courseId, beforeLessonId) {
|
||||
if (!beforeLessonId) {
|
||||
const err = new Error('beforeLessonId is required');
|
||||
err.status = 400;
|
||||
throw err;
|
||||
}
|
||||
const user = await this._getUserByHashedId(hashedUserId);
|
||||
const enrollment = await VocabCourseEnrollment.findOne({
|
||||
where: { userId: user.id, courseId: Number(courseId) },
|
||||
});
|
||||
if (!enrollment) {
|
||||
const err = new Error('Not enrolled in this course');
|
||||
err.status = 403;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const currentLesson = await VocabCourseLesson.findByPk(beforeLessonId);
|
||||
if (!currentLesson || currentLesson.courseId !== Number(courseId)) {
|
||||
const err = new Error('Lesson not found');
|
||||
err.status = 404;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const priorLessons = await VocabCourseLesson.findAll({
|
||||
where: {
|
||||
courseId: Number(courseId),
|
||||
lessonNumber: { [Op.lt]: currentLesson.lessonNumber },
|
||||
},
|
||||
attributes: ['id'],
|
||||
order: [['lessonNumber', 'ASC']],
|
||||
});
|
||||
|
||||
const lessonIds = priorLessons.map((l) => l.id);
|
||||
if (lessonIds.length === 0) {
|
||||
return { target: [], native: [] };
|
||||
}
|
||||
|
||||
const exercises = await VocabGrammarExercise.findAll({
|
||||
where: {
|
||||
lessonId: { [Op.in]: lessonIds },
|
||||
exerciseTypeId: 2,
|
||||
},
|
||||
attributes: ['questionData'],
|
||||
});
|
||||
|
||||
const target = new Set();
|
||||
const native = new Set();
|
||||
|
||||
for (const ex of exercises) {
|
||||
const qd =
|
||||
typeof ex.questionData === 'string' ? JSON.parse(ex.questionData) : ex.questionData;
|
||||
const question = qd?.question || '';
|
||||
const opts = qd?.options;
|
||||
if (!Array.isArray(opts)) continue;
|
||||
const side = this._classifyMcQuestionSide(question);
|
||||
if (side === 'target') {
|
||||
opts.forEach((o) => target.add(String(o).trim()));
|
||||
} else if (side === 'native') {
|
||||
opts.forEach((o) => native.add(String(o).trim()));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
target: [...target],
|
||||
native: [...native],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
33
backend/sql/update-bisaya-babay-multiple-choice.sql
Normal file
33
backend/sql/update-bisaya-babay-multiple-choice.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- Bisaya-Kurs: Übung „Abschiedsform erkennen“ (Was bedeutet „Babay“?)
|
||||
-- Statt einer kombinierten Option „Tschüss / Auf Wiedersehen“: zwei gültige Antworten
|
||||
-- (correctAnswer [0, 1]), passend zu backend/services/vocabService.js (multiple_choice).
|
||||
--
|
||||
-- Nach dem Einspielen: Frontend deployen, falls der Vokabeltrainer aus Übungen
|
||||
-- mehrere korrekte MC-Indizes verarbeiten soll (VocabLessonView importantVocab).
|
||||
|
||||
UPDATE community.vocab_grammar_exercise AS e
|
||||
SET
|
||||
question_data = jsonb_build_object(
|
||||
'type', 'multiple_choice',
|
||||
'question', 'Was bedeutet "Babay"?',
|
||||
'options', jsonb_build_array(
|
||||
'Tschüss',
|
||||
'Auf Wiedersehen',
|
||||
'Wie geht es dir?',
|
||||
'Guten Tag'
|
||||
)
|
||||
),
|
||||
answer_data = jsonb_build_object(
|
||||
'type', 'multiple_choice',
|
||||
'correctAnswer', jsonb_build_array(0, 1)
|
||||
),
|
||||
explanation = '"Babay" ist eine einfache Verabschiedung — vergleichbar mit „Tschüss“ oder „Auf Wiedersehen“.'
|
||||
WHERE e.exercise_type_id = 2
|
||||
AND e.title = 'Abschiedsform erkennen'
|
||||
AND (
|
||||
e.question_data->>'question' = 'Was bedeutet "Babay"?'
|
||||
OR (e.question_data->'options'->>0) = 'Tschüss / Auf Wiedersehen'
|
||||
);
|
||||
|
||||
-- Erwartet: mindestens eine Zeile pro betroffener Übung (pro Kurs/Lektion).
|
||||
-- SELECT COUNT(*) vorher/nachher bei Bedarf prüfen.
|
||||
Reference in New Issue
Block a user