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(' ')
|
.split(' ')
|
||||||
.filter((token) => token.length >= 4)
|
.filter((token) => token.length >= 4)
|
||||||
.slice(0, 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', {
|
return withTypeName('situational_response', {
|
||||||
title: `${lessonTitle}: Situativ reagieren`,
|
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: {
|
questionData: {
|
||||||
type: 'situational_response',
|
type: 'situational_response',
|
||||||
question: speakingPrompt?.prompt || `Reagiere passend mit einem Ausdruck aus der Lektion "${lessonTitle}".`,
|
question,
|
||||||
keywords
|
keywords: requiredParts,
|
||||||
|
minKeywordMatches: requiredParts.length
|
||||||
},
|
},
|
||||||
answerData: {
|
answerData: {
|
||||||
modelAnswer,
|
modelAnswer,
|
||||||
keywords
|
keywords: requiredParts,
|
||||||
|
minKeywordMatches: requiredParts.length
|
||||||
},
|
},
|
||||||
explanation: `Das Kernmuster "${modelAnswer}" passt natürlich zu dieser Situation.`
|
explanation: `Das Kernmuster "${modelAnswer}" passt natürlich zu dieser Situation.`
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4593,7 +4593,17 @@ export default class VocabService {
|
|||||||
const keywords = parsedQuestionData.keywords || parsedAnswerData.keywords || [];
|
const keywords = parsedQuestionData.keywords || parsedAnswerData.keywords || [];
|
||||||
if (keywords.length > 0) {
|
if (keywords.length > 0) {
|
||||||
const normalizedUser = this._normalizeTextAnswer(userAnswer);
|
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">
|
<div v-else-if="getExerciseType(exercise) === 'situational_response'" class="situational-response-exercise">
|
||||||
<p v-if="getVisibleQuestionText(exercise)" class="exercise-question">{{ getVisibleQuestionText(exercise) }}</p>
|
<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
|
<textarea
|
||||||
v-model="exerciseAnswers[exercise.id]"
|
v-model="exerciseAnswers[exercise.id]"
|
||||||
:placeholder="$t('socialnetwork.vocab.courses.situationalResponsePlaceholder')"
|
:placeholder="$t('socialnetwork.vocab.courses.situationalResponsePlaceholder')"
|
||||||
|
|||||||
Reference in New Issue
Block a user