From ebb2283646ab08391c8518624884a9d6e949032d Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 7 Apr 2026 14:32:44 +0200 Subject: [PATCH] refactor(exercises): standardize answer language handling across exercise scripts - Introduced a mechanism to infer answer language based on question phrasing in multiple exercise scripts, enhancing consistency in exercise data. - Updated question formats to clarify the intent of exercises, improving user understanding and engagement. - Streamlined the code for better maintainability and clarity in exercise generation processes. --- .../update-family-conversations-exercises.js | 4 +- .../scripts/update-family-words-exercises.js | 1 + .../update-feelings-affection-exercises.js | 4 +- backend/scripts/update-food-care-exercises.js | 2 + .../update-survival-sentences-exercises.js | 27 ++++++++++- .../scripts/update-week1-bisaya-exercises.js | 27 ++++++++++- frontend/src/views/social/VocabLessonView.vue | 48 ++++++++++++++++++- 7 files changed, 107 insertions(+), 6 deletions(-) diff --git a/backend/scripts/update-family-conversations-exercises.js b/backend/scripts/update-family-conversations-exercises.js index 173e5e5..9f6df9a 100755 --- a/backend/scripts/update-family-conversations-exercises.js +++ b/backend/scripts/update-family-conversations-exercises.js @@ -349,7 +349,8 @@ function createFamilyConversationExercises(nativeLanguageName) { instruction: 'Übersetze den Bisaya-Satz ins ' + nativeLanguageName, questionData: JSON.stringify({ type: 'multiple_choice', - question: `Wie sagt man "${conv.bisaya}" auf ${nativeLanguageName}?`, + answerLanguage: 'native', + question: `Was bedeutet "${conv.bisaya}"?`, options: options }), answerData: JSON.stringify({ @@ -379,6 +380,7 @@ function createFamilyConversationExercises(nativeLanguageName) { instruction: 'Was bedeutet dieser Bisaya-Satz?', questionData: JSON.stringify({ type: 'multiple_choice', + answerLanguage: 'native', question: `Was bedeutet "${conv.bisaya}"?`, options: options }), diff --git a/backend/scripts/update-family-words-exercises.js b/backend/scripts/update-family-words-exercises.js index e95c470..e95ee2c 100755 --- a/backend/scripts/update-family-words-exercises.js +++ b/backend/scripts/update-family-words-exercises.js @@ -134,6 +134,7 @@ function createFamilyWordsExercises(nativeLanguageName) { instruction: 'Wähle die richtige Übersetzung.', questionData: { type: 'multiple_choice', + answerLanguage: 'target', question: `Wie sagt man "${nativeWord}" auf Bisaya?`, options: options }, diff --git a/backend/scripts/update-feelings-affection-exercises.js b/backend/scripts/update-feelings-affection-exercises.js index a6f8877..8a8388f 100755 --- a/backend/scripts/update-feelings-affection-exercises.js +++ b/backend/scripts/update-feelings-affection-exercises.js @@ -349,7 +349,8 @@ function createFeelingsAffectionExercises(nativeLanguageName) { instruction: 'Übersetze den Bisaya-Satz ins ' + nativeLanguageName, questionData: JSON.stringify({ type: 'multiple_choice', - question: `Wie sagt man "${conv.bisaya}" auf ${nativeLanguageName}?`, + answerLanguage: 'native', + question: `Was bedeutet "${conv.bisaya}"?`, options: options }), answerData: JSON.stringify({ @@ -379,6 +380,7 @@ function createFeelingsAffectionExercises(nativeLanguageName) { instruction: 'Was bedeutet dieser Bisaya-Satz?', questionData: JSON.stringify({ type: 'multiple_choice', + answerLanguage: 'native', question: `Was bedeutet "${conv.bisaya}"?`, options: options }), diff --git a/backend/scripts/update-food-care-exercises.js b/backend/scripts/update-food-care-exercises.js index 3355c6a..c91d939 100755 --- a/backend/scripts/update-food-care-exercises.js +++ b/backend/scripts/update-food-care-exercises.js @@ -582,6 +582,7 @@ async function updateFoodCareExercises() { instruction: 'Wähle die richtige Übersetzung.', questionData: JSON.stringify({ type: 'multiple_choice', + answerLanguage: 'target', question: `Wie sagt man "${conv.native}" auf Bisaya?`, options: [ conv.bisaya, @@ -608,6 +609,7 @@ async function updateFoodCareExercises() { instruction: 'Wähle die richtige Übersetzung.', questionData: JSON.stringify({ type: 'multiple_choice', + answerLanguage: 'native', question: `Was bedeutet "${conv.bisaya}"?`, options: [ conv.native, diff --git a/backend/scripts/update-survival-sentences-exercises.js b/backend/scripts/update-survival-sentences-exercises.js index 71e520c..1155204 100755 --- a/backend/scripts/update-survival-sentences-exercises.js +++ b/backend/scripts/update-survival-sentences-exercises.js @@ -13,6 +13,31 @@ import VocabCourseLesson from '../models/community/vocab_course_lesson.js'; import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js'; import User from '../models/community/user.js'; +function normalizeMcAnswerLanguage(question = '') { + const q = String(question || '').trim(); + if (!q) return null; + if (/Wie sagt man/i.test(q) || /auf Bisaya\?/i.test(q)) return 'target'; + if (/Was bedeutet/i.test(q) || /Was heißt/i.test(q)) return 'native'; + return null; +} + +function enrichAnswerLanguage(exercises = []) { + return exercises.map((exercise) => { + const qd = exercise?.questionData; + if (!qd || qd.type !== 'multiple_choice') return exercise; + if (qd.answerLanguage || qd.answerLanguageId) return exercise; + const inferred = normalizeMcAnswerLanguage(qd.question); + if (!inferred) return exercise; + return { + ...exercise, + questionData: { + ...qd, + answerLanguage: inferred + } + }; + }); +} + // Spezifische Übungen für Überlebenssätze const SURVIVAL_EXERCISES = { 'Überlebenssätze - Teil 1': [ @@ -399,7 +424,7 @@ async function updateSurvivalExercises() { console.log(` ${lessons.length} "Überlebenssätze"-Lektionen gefunden\n`); for (const lesson of lessons) { - const exercises = SURVIVAL_EXERCISES[lesson.title]; + const exercises = enrichAnswerLanguage(SURVIVAL_EXERCISES[lesson.title]); if (!exercises || exercises.length === 0) { console.log(` ⚠️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - keine Übungen definiert`); diff --git a/backend/scripts/update-week1-bisaya-exercises.js b/backend/scripts/update-week1-bisaya-exercises.js index b637c21..286534c 100644 --- a/backend/scripts/update-week1-bisaya-exercises.js +++ b/backend/scripts/update-week1-bisaya-exercises.js @@ -23,6 +23,31 @@ function withTypeName(exerciseTypeName, exercise) { const LESSON_TITLES = ['Woche 1 - Wiederholung', 'Woche 1 - Vokabeltest']; +function normalizeMcAnswerLanguage(question = '') { + const q = String(question || '').trim(); + if (!q) return null; + if (/Wie sagt man/i.test(q) || /auf Bisaya\?/i.test(q)) return 'target'; + if (/Was bedeutet/i.test(q) || /Was heißt/i.test(q)) return 'native'; + return null; +} + +function enrichAnswerLanguage(exercises = []) { + return exercises.map((exercise) => { + const qd = exercise?.questionData; + if (!qd || qd.type !== 'multiple_choice') return exercise; + if (qd.answerLanguage || qd.answerLanguageId) return exercise; + const inferred = normalizeMcAnswerLanguage(qd.question); + if (!inferred) return exercise; + return { + ...exercise, + questionData: { + ...qd, + answerLanguage: inferred + } + }; + }); +} + const BISAYA_EXERCISES = { 'Woche 1 - Wiederholung': [ { exerciseTypeId: 2, title: 'Wiederholung: Wie sagt man "Wie geht es dir?"?', instruction: 'Wähle die richtige Begrüßung aus.', questionData: { type: 'multiple_choice', question: 'Wie sagt man "Wie geht es dir?" auf Bisaya?', options: ['Kumusta ka?', 'Maayo', 'Salamat', 'Palihug'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Kumusta ka?" ist die Standard-Begrüßung auf Bisaya.' }, @@ -111,7 +136,7 @@ async function updateWeek1BisayaExercises() { console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`); for (const lessonTitle of LESSON_TITLES) { - const exercises = BISAYA_EXERCISES[lessonTitle]; + const exercises = enrichAnswerLanguage(BISAYA_EXERCISES[lessonTitle]); if (!exercises || exercises.length === 0) continue; const lessons = await VocabCourseLesson.findAll({ diff --git a/frontend/src/views/social/VocabLessonView.vue b/frontend/src/views/social/VocabLessonView.vue index 72651b7..8079608 100644 --- a/frontend/src/views/social/VocabLessonView.vue +++ b/frontend/src/views/social/VocabLessonView.vue @@ -1251,9 +1251,53 @@ export default { }, lessonVocab() { const vocabByReference = new Map(); + const targetTokenWeight = new Map(); + const nativeTokenWeight = new Map(); + const addTokens = (text, map, weight = 1) => { + const tokens = String(text || '') + .toLowerCase() + .normalize('NFKC') + .replace(/[\p{P}\p{S}]+/gu, ' ') + .split(/\s+/) + .map((t) => t.trim()) + .filter((t) => t.length >= 2); + tokens.forEach((token) => { + map.set(token, (map.get(token) || 0) + weight); + }); + }; + const sideScore = (text, map) => { + const tokens = String(text || '') + .toLowerCase() + .normalize('NFKC') + .replace(/[\p{P}\p{S}]+/gu, ' ') + .split(/\s+/) + .map((t) => t.trim()) + .filter((t) => t.length >= 2); + return tokens.reduce((sum, token) => sum + (map.get(token) || 0), 0); + }; + (this.normalizedCorePatterns || []).forEach((p) => { + addTokens(p?.target, targetTokenWeight, 3); + addTokens(p?.gloss, nativeTokenWeight, 3); + }); + (this.importantVocab || []).forEach((v) => { + addTokens(v?.reference, targetTokenWeight, 1); + addTokens(v?.learning, nativeTokenWeight, 1); + }); + const orientPair = (learning, reference) => { + const l = String(learning || '').trim(); + const r = String(reference || '').trim(); + if (!l || !r) return { learning: l, reference: r }; + const directScore = sideScore(r, targetTokenWeight) + sideScore(l, nativeTokenWeight); + const swappedScore = sideScore(l, targetTokenWeight) + sideScore(r, nativeTokenWeight); + if (swappedScore > directScore) { + return { learning: r, reference: l }; + } + return { learning: l, reference: r }; + }; const addEntry = (entry) => { - const reference = String(entry?.reference || '').trim(); - const learning = String(entry?.learning || '').trim(); + const oriented = orientPair(entry?.learning, entry?.reference); + const reference = String(oriented.reference || '').trim(); + const learning = String(oriented.learning || '').trim(); if (!reference) return; const key = this.normalizeLessonVocabTerm(reference); if (!vocabByReference.has(key)) {