From 2ca008806f606c0caa2832c30c4456f17c68d99a Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 26 May 2026 16:24:20 +0200 Subject: [PATCH] feat: normalize questionData for gap-fill exercises and add placeholders if necessary --- backend/services/vocabService.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index 81e31af..b02246c 100644 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -3873,6 +3873,35 @@ export default class VocabService { const plainLesson = lesson.get({ plain: true }); const list = exercises.map((e) => e.get({ plain: true })); + // Normalize questionData for gap-fill hints: if text contains placeholders + list.forEach((ex) => { + try { + const qData = typeof ex.questionData === 'string' ? JSON.parse(ex.questionData || '{}') : (ex.questionData || {}); + const text = String(qData.text || qData.question || '').trim(); + const hasUnderscoreGap = /_{3,}/.test(text); + const hasBraceGap = /\{\s*gap\s*\}/i.test(text); + const hasParenPlaceholders = /\(\s*_{0,}\s*\)/.test(text); + // If answerData suggests a gap-like exercise and question text contains gap markers, ensure type is gap_fill + if ((hasUnderscoreGap || hasBraceGap || hasParenPlaceholders) && qData.type !== 'gap_fill') { + qData.type = 'gap_fill'; + ex.questionData = qData; + } + // If answerData has explicit answers but there's no question text, and exerciseTypeId indicates input, add a placeholder + const aData = typeof ex.answerData === 'string' ? JSON.parse(ex.answerData || '{}') : (ex.answerData || {}); + const answers = Array.isArray(aData.answers) ? aData.answers : (aData.correct ? (Array.isArray(aData.correct) ? aData.correct : [aData.correct]) : []); + if ((!qData.text && (!qData.question || String(qData.question).trim() === '')) && answers.length && qData.type !== 'multiple_choice') { + // build a minimal gap_fill text using learning hint if available in ex.title + const learningHintMatch = String(ex.title || '').match(/:(.*)$/); + const hint = learningHintMatch ? learningHintMatch[1].trim() : ''; + qData.type = 'gap_fill'; + qData.text = hint ? `{gap} (${hint})` : '{gap}'; + ex.questionData = qData; + } + } catch (err) { + // ignore parse errors + } + }); + return await this._mergeSyntheticChapterLexemeMcExercises(plainLesson, list); }