feat(bisaya): enhance situational response exercises with keyword hints and minimum match requirements
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
const OLD_QUESTION = 'Erzähle einen Tagesplan mit drei Schritten und einem Wunsch.';
|
||||
const REPAIRED_QUESTION = 'Schreibe vier kurze Sätze auf Bisaya. Verwende dabei alle diese Bausteine: Una, Pagkahuman, Unya und Gusto ko nga.';
|
||||
const KEYWORDS = ['una', 'pagkahuman', 'unya', 'gusto ko nga'];
|
||||
const MODEL_ANSWER = 'Una, moadto ko sa merkado. Pagkahuman, kuhaon nako ang bata. Unya, mouli mi sa balay. Gusto ko nga magkita ta ugma.';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.sequelize.transaction(async (transaction) => {
|
||||
// This was an open-ended production prompt, but the checker required four
|
||||
// hidden words. Make both the expected language and the four criteria
|
||||
// explicit, while still accepting every answer that fulfils them.
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE community.vocab_grammar_exercise AS exercise
|
||||
SET question_data = JSONB_SET(
|
||||
JSONB_SET(
|
||||
JSONB_SET(exercise.question_data, '{question}', TO_JSONB(CAST(:repairedQuestion AS text))),
|
||||
'{keywords}', CAST(:keywords AS jsonb)
|
||||
),
|
||||
'{minKeywordMatches}', '4'::jsonb
|
||||
),
|
||||
answer_data = JSONB_SET(
|
||||
JSONB_SET(
|
||||
JSONB_SET(COALESCE(exercise.answer_data, '{}'::jsonb), '{modelAnswer}', TO_JSONB(CAST(:modelAnswer AS text))),
|
||||
'{keywords}', CAST(:keywords AS jsonb)
|
||||
),
|
||||
'{minKeywordMatches}', '4'::jsonb
|
||||
)
|
||||
FROM community.vocab_course_lesson AS lesson
|
||||
WHERE exercise.lesson_id = lesson.id
|
||||
AND (lesson.id = 39 OR (lesson.course_id = 1 AND lesson.lesson_number = 39))
|
||||
AND exercise.question_data->>'type' = 'situational_response'
|
||||
AND exercise.question_data->>'question' = :oldQuestion;
|
||||
`, {
|
||||
replacements: {
|
||||
oldQuestion: OLD_QUESTION,
|
||||
repairedQuestion: REPAIRED_QUESTION,
|
||||
keywords: JSON.stringify(KEYWORDS),
|
||||
modelAnswer: MODEL_ANSWER
|
||||
},
|
||||
transaction
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async down() {
|
||||
// Keep the unambiguous exercise wording once deployed.
|
||||
}
|
||||
};
|
||||
@@ -761,18 +761,24 @@ function buildSituationalExercise(lessonTitle, didactics, fallbackPattern) {
|
||||
.split(' ')
|
||||
.filter((token) => token.length >= 4)
|
||||
.slice(0, 4);
|
||||
const requiredParts = keywords.length > 0 ? keywords : [modelAnswer];
|
||||
const question = speakingPrompt?.prompt
|
||||
? `${speakingPrompt.prompt} Schreibe auf Bisaya und verwende diese Bausteine: ${requiredParts.join(', ')}.`
|
||||
: `Reagiere passend mit einem Ausdruck aus der Lektion „${lessonTitle}“. Verwende: ${requiredParts.join(', ')}.`;
|
||||
|
||||
return withTypeName('situational_response', {
|
||||
title: `${lessonTitle}: Situativ reagieren`,
|
||||
instruction: 'Antworte kurz und passend auf die Situation.',
|
||||
instruction: 'Antworte auf Bisaya. Die angezeigten Bausteine müssen in deiner Antwort vorkommen.',
|
||||
questionData: {
|
||||
type: 'situational_response',
|
||||
question: speakingPrompt?.prompt || `Reagiere passend mit einem Ausdruck aus der Lektion "${lessonTitle}".`,
|
||||
keywords
|
||||
question,
|
||||
keywords: requiredParts,
|
||||
minKeywordMatches: requiredParts.length
|
||||
},
|
||||
answerData: {
|
||||
modelAnswer,
|
||||
keywords
|
||||
keywords: requiredParts,
|
||||
minKeywordMatches: requiredParts.length
|
||||
},
|
||||
explanation: `Das Kernmuster "${modelAnswer}" passt natürlich zu dieser Situation.`
|
||||
});
|
||||
|
||||
@@ -4593,7 +4593,17 @@ export default class VocabService {
|
||||
const keywords = parsedQuestionData.keywords || parsedAnswerData.keywords || [];
|
||||
if (keywords.length > 0) {
|
||||
const normalizedUser = this._normalizeTextAnswer(userAnswer);
|
||||
return keywords.every((keyword) => normalizedUser.includes(this._normalizeTextAnswer(keyword)));
|
||||
const matches = keywords.filter((keyword) => (
|
||||
normalizedUser.includes(this._normalizeTextAnswer(keyword))
|
||||
)).length;
|
||||
const configuredMinimum = Number(
|
||||
parsedQuestionData.minKeywordMatches ?? parsedAnswerData.minKeywordMatches
|
||||
);
|
||||
const requiredMatches = Number.isInteger(configuredMinimum)
|
||||
&& configuredMinimum > 0
|
||||
? Math.min(configuredMinimum, keywords.length)
|
||||
: keywords.length;
|
||||
return matches >= requiredMatches;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -784,6 +784,10 @@
|
||||
|
||||
<div v-else-if="getExerciseType(exercise) === 'situational_response'" class="situational-response-exercise">
|
||||
<p v-if="getVisibleQuestionText(exercise)" class="exercise-question">{{ getVisibleQuestionText(exercise) }}</p>
|
||||
<div v-if="getQuestionData(exercise)?.keywords?.length" class="keywords-hint">
|
||||
<strong>{{ $t('socialnetwork.vocab.courses.keywords') }}:</strong>
|
||||
<span v-for="(keyword, idx) in getQuestionData(exercise).keywords" :key="idx" class="keyword-tag">{{ keyword }}</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="exerciseAnswers[exercise.id]"
|
||||
:placeholder="$t('socialnetwork.vocab.courses.situationalResponsePlaceholder')"
|
||||
|
||||
Reference in New Issue
Block a user