feat(vocab): add support for answer alternatives in core patterns and update related logic
This commit is contained in:
@@ -0,0 +1,142 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const SHORT_ANSWER = 'Ich ruhe mich später kurz aus.';
|
||||||
|
const ALTERNATIVE_ANSWER = 'Ich ruhe mich später aus.';
|
||||||
|
const COMBINED_ANSWER = `${SHORT_ANSWER} / ${ALTERNATIVE_ANSWER}`;
|
||||||
|
const TARGET = 'Magpahuway ko gamay unya.';
|
||||||
|
const NORMALIZED_COMBINED_ANSWER = 'ich ruhe mich später kurz aus ich ruhe mich später aus';
|
||||||
|
const NORMALIZED_TARGET = 'magpahuway ko gamay unya';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
await queryInterface.sequelize.transaction(async (transaction) => {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE community.vocab_course_lesson
|
||||||
|
SET core_patterns = (
|
||||||
|
SELECT JSONB_AGG(
|
||||||
|
CASE
|
||||||
|
WHEN pattern.value->>'target' = :target AND pattern.value->>'gloss' = :shortAnswer
|
||||||
|
THEN pattern.value || JSONB_BUILD_OBJECT('alternatives', JSONB_BUILD_ARRAY(:alternativeAnswer))
|
||||||
|
ELSE pattern.value
|
||||||
|
END
|
||||||
|
)
|
||||||
|
FROM JSONB_ARRAY_ELEMENTS(core_patterns) AS pattern(value)
|
||||||
|
)
|
||||||
|
WHERE JSONB_TYPEOF(core_patterns) = 'array'
|
||||||
|
AND core_patterns @> JSONB_BUILD_ARRAY(JSONB_BUILD_OBJECT('target', :target, 'gloss', :shortAnswer));
|
||||||
|
`, {
|
||||||
|
replacements: { target: TARGET, shortAnswer: SHORT_ANSWER, alternativeAnswer: ALTERNATIVE_ANSWER },
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
WITH stale AS (
|
||||||
|
SELECT id, user_id, course_id, lesson_id, direction, correct_count, wrong_count, lapse_count,
|
||||||
|
stage, interval_days, last_reviewed_at, next_due_at
|
||||||
|
FROM community.vocab_srs_item
|
||||||
|
WHERE learning = :shortAnswer
|
||||||
|
AND reference = :target
|
||||||
|
)
|
||||||
|
UPDATE community.vocab_srs_item AS canonical
|
||||||
|
SET correct_count = canonical.correct_count + stale.correct_count,
|
||||||
|
wrong_count = canonical.wrong_count + stale.wrong_count,
|
||||||
|
lapse_count = canonical.lapse_count + stale.lapse_count,
|
||||||
|
stage = GREATEST(canonical.stage, stale.stage),
|
||||||
|
interval_days = GREATEST(canonical.interval_days, stale.interval_days),
|
||||||
|
last_reviewed_at = GREATEST(canonical.last_reviewed_at, stale.last_reviewed_at),
|
||||||
|
next_due_at = LEAST(canonical.next_due_at, stale.next_due_at),
|
||||||
|
updated_at = NOW()
|
||||||
|
FROM stale
|
||||||
|
WHERE canonical.user_id = stale.user_id
|
||||||
|
AND canonical.id <> stale.id
|
||||||
|
AND canonical.item_key = ENCODE(DIGEST(CONCAT_WS(
|
||||||
|
'|',
|
||||||
|
stale.course_id::text,
|
||||||
|
COALESCE(stale.lesson_id::text, 'course'),
|
||||||
|
UPPER(stale.direction),
|
||||||
|
:normalizedCombinedAnswer,
|
||||||
|
:normalizedTarget
|
||||||
|
), 'sha1'), 'hex');
|
||||||
|
`, {
|
||||||
|
replacements: {
|
||||||
|
target: TARGET,
|
||||||
|
shortAnswer: SHORT_ANSWER,
|
||||||
|
normalizedCombinedAnswer: NORMALIZED_COMBINED_ANSWER,
|
||||||
|
normalizedTarget: NORMALIZED_TARGET
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DELETE FROM community.vocab_srs_item AS stale
|
||||||
|
WHERE stale.learning = :shortAnswer
|
||||||
|
AND stale.reference = :target
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM community.vocab_srs_item canonical
|
||||||
|
WHERE canonical.user_id = stale.user_id
|
||||||
|
AND canonical.id <> stale.id
|
||||||
|
AND canonical.item_key = ENCODE(DIGEST(CONCAT_WS(
|
||||||
|
'|',
|
||||||
|
stale.course_id::text,
|
||||||
|
COALESCE(stale.lesson_id::text, 'course'),
|
||||||
|
UPPER(stale.direction),
|
||||||
|
:normalizedCombinedAnswer,
|
||||||
|
:normalizedTarget
|
||||||
|
), 'sha1'), 'hex')
|
||||||
|
);
|
||||||
|
`, {
|
||||||
|
replacements: {
|
||||||
|
target: TARGET,
|
||||||
|
shortAnswer: SHORT_ANSWER,
|
||||||
|
normalizedCombinedAnswer: NORMALIZED_COMBINED_ANSWER,
|
||||||
|
normalizedTarget: NORMALIZED_TARGET
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE community.vocab_srs_item
|
||||||
|
SET learning = :combinedAnswer,
|
||||||
|
item_key = ENCODE(DIGEST(CONCAT_WS(
|
||||||
|
'|',
|
||||||
|
course_id::text,
|
||||||
|
COALESCE(lesson_id::text, 'course'),
|
||||||
|
UPPER(direction),
|
||||||
|
:normalizedCombinedAnswer,
|
||||||
|
:normalizedTarget
|
||||||
|
), 'sha1'), 'hex'),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE learning = :shortAnswer
|
||||||
|
AND reference = :target
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM community.vocab_srs_item duplicate
|
||||||
|
WHERE duplicate.user_id = vocab_srs_item.user_id
|
||||||
|
AND duplicate.item_key = ENCODE(DIGEST(CONCAT_WS(
|
||||||
|
'|',
|
||||||
|
vocab_srs_item.course_id::text,
|
||||||
|
COALESCE(vocab_srs_item.lesson_id::text, 'course'),
|
||||||
|
UPPER(vocab_srs_item.direction),
|
||||||
|
:normalizedCombinedAnswer,
|
||||||
|
:normalizedTarget
|
||||||
|
), 'sha1'), 'hex')
|
||||||
|
AND duplicate.id <> vocab_srs_item.id
|
||||||
|
);
|
||||||
|
`, {
|
||||||
|
replacements: {
|
||||||
|
target: TARGET,
|
||||||
|
shortAnswer: SHORT_ANSWER,
|
||||||
|
combinedAnswer: COMBINED_ANSWER,
|
||||||
|
normalizedCombinedAnswer: NORMALIZED_COMBINED_ANSWER,
|
||||||
|
normalizedTarget: NORMALIZED_TARGET
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
// The alternative is valid course content and should not be removed.
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -242,7 +242,7 @@ const LESSON_DIDACTICS = {
|
|||||||
{ target: 'Human na ka sa trabaho?', gloss: 'Bist du mit der Arbeit fertig?' },
|
{ target: 'Human na ka sa trabaho?', gloss: 'Bist du mit der Arbeit fertig?' },
|
||||||
{ target: 'Dali lang ko mubalik.', gloss: 'Ich komme gleich wieder.' },
|
{ target: 'Dali lang ko mubalik.', gloss: 'Ich komme gleich wieder.' },
|
||||||
{ target: 'Naa koy lakaw karong hapon.', gloss: 'Ich habe heute Nachmittag etwas zu erledigen.' },
|
{ target: 'Naa koy lakaw karong hapon.', gloss: 'Ich habe heute Nachmittag etwas zu erledigen.' },
|
||||||
{ target: 'Magpahuway ko gamay unya.', gloss: 'Ich ruhe mich später kurz aus.' },
|
{ target: 'Magpahuway ko gamay unya.', gloss: 'Ich ruhe mich später kurz aus.', alternatives: ['Ich ruhe mich später aus.'] },
|
||||||
{ target: 'Tawagi ko kung mahuman ka.', gloss: 'Ruf mich an, wenn du fertig bist.' }
|
{ target: 'Tawagi ko kung mahuman ka.', gloss: 'Ruf mich an, wenn du fertig bist.' }
|
||||||
],
|
],
|
||||||
speakingPrompts: [
|
speakingPrompts: [
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const BISAYA_DIDACTICS_FRAGMENTS = {
|
|||||||
{ target: 'Human na ka sa trabaho?', gloss: 'Bist du mit der Arbeit fertig?' },
|
{ target: 'Human na ka sa trabaho?', gloss: 'Bist du mit der Arbeit fertig?' },
|
||||||
{ target: 'Dali lang ko mubalik.', gloss: 'Ich komme gleich wieder.' },
|
{ target: 'Dali lang ko mubalik.', gloss: 'Ich komme gleich wieder.' },
|
||||||
{ target: 'Naa koy lakaw karong hapon.', gloss: 'Ich habe heute Nachmittag etwas zu erledigen.' },
|
{ target: 'Naa koy lakaw karong hapon.', gloss: 'Ich habe heute Nachmittag etwas zu erledigen.' },
|
||||||
{ target: 'Magpahuway ko gamay unya.', gloss: 'Ich ruhe mich später kurz aus.' },
|
{ target: 'Magpahuway ko gamay unya.', gloss: 'Ich ruhe mich später kurz aus.', alternatives: ['Ich ruhe mich später aus.'] },
|
||||||
{ target: 'Tawagi ko kung mahuman ka.', gloss: 'Ruf mich an, wenn du fertig bist.' }
|
{ target: 'Tawagi ko kung mahuman ka.', gloss: 'Ruf mich an, wenn du fertig bist.' }
|
||||||
],
|
],
|
||||||
speakingPrompts: [
|
speakingPrompts: [
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ const LESSON_DIDACTICS = {
|
|||||||
{ target: 'Human na ka sa trabaho?', gloss: 'Bist du mit der Arbeit fertig?' },
|
{ target: 'Human na ka sa trabaho?', gloss: 'Bist du mit der Arbeit fertig?' },
|
||||||
{ target: 'Dali lang ko mubalik.', gloss: 'Ich komme gleich wieder.' },
|
{ target: 'Dali lang ko mubalik.', gloss: 'Ich komme gleich wieder.' },
|
||||||
{ target: 'Naa koy lakaw karong hapon.', gloss: 'Ich habe heute Nachmittag etwas zu erledigen.' },
|
{ target: 'Naa koy lakaw karong hapon.', gloss: 'Ich habe heute Nachmittag etwas zu erledigen.' },
|
||||||
{ target: 'Magpahuway ko gamay unya.', gloss: 'Ich ruhe mich später kurz aus.' },
|
{ target: 'Magpahuway ko gamay unya.', gloss: 'Ich ruhe mich später kurz aus.', alternatives: ['Ich ruhe mich später aus.'] },
|
||||||
{ target: 'Tawagi ko kung mahuman ka.', gloss: 'Ruf mich an, wenn du fertig bist.' }
|
{ target: 'Tawagi ko kung mahuman ka.', gloss: 'Ruf mich an, wenn du fertig bist.' }
|
||||||
],
|
],
|
||||||
speakingPrompts: [
|
speakingPrompts: [
|
||||||
|
|||||||
@@ -984,7 +984,10 @@ export default class VocabService {
|
|||||||
corePatterns.forEach((entry) => {
|
corePatterns.forEach((entry) => {
|
||||||
const pattern = this._normalizeCorePatternEntry(entry);
|
const pattern = this._normalizeCorePatternEntry(entry);
|
||||||
const reference = String(pattern?.target || '').trim();
|
const reference = String(pattern?.target || '').trim();
|
||||||
const learning = String(pattern?.gloss || '').trim();
|
const answers = [pattern?.gloss, ...(pattern?.alternatives || [])]
|
||||||
|
.map((answer) => String(answer || '').trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const learning = [...new Set(answers)].join(' / ');
|
||||||
if (!this._isTrainableSrsPair({ learning, reference })) return;
|
if (!this._isTrainableSrsPair({ learning, reference })) return;
|
||||||
vocabMap.set(`${learning}-${reference}`, { learning, reference });
|
vocabMap.set(`${learning}-${reference}`, { learning, reference });
|
||||||
});
|
});
|
||||||
@@ -1009,8 +1012,9 @@ export default class VocabService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kernmuster: Zielsprachen-Phrase + optionale Glossierung (z. B. Deutsch).
|
* Kernmuster: Zielsprachen-Phrase + optionale Glossierung und Antwortalternativen.
|
||||||
* Unterstützt Legacy-Strings, "Phrase|Gloss" und Objekte { target, gloss } / { ceb, de }.
|
* Unterstützt Legacy-Strings, "Phrase|Gloss" und Objekte
|
||||||
|
* { target, gloss, alternatives } / { ceb, de }.
|
||||||
*/
|
*/
|
||||||
_normalizeCorePatternEntry(entry) {
|
_normalizeCorePatternEntry(entry) {
|
||||||
if (entry === null || entry === undefined || entry === '') {
|
if (entry === null || entry === undefined || entry === '') {
|
||||||
@@ -1020,7 +1024,9 @@ export default class VocabService {
|
|||||||
const target = String(entry.target ?? entry.ceb ?? entry.phrase ?? '').trim();
|
const target = String(entry.target ?? entry.ceb ?? entry.phrase ?? '').trim();
|
||||||
const gloss = String(entry.gloss ?? entry.de ?? entry.translation ?? '').trim();
|
const gloss = String(entry.gloss ?? entry.de ?? entry.translation ?? '').trim();
|
||||||
if (!target) return null;
|
if (!target) return null;
|
||||||
return { target, gloss };
|
const alternatives = this._normalizeStringList(entry.alternatives ?? entry.glossAlternatives)
|
||||||
|
.filter((alternative) => alternative !== gloss);
|
||||||
|
return { target, gloss, alternatives };
|
||||||
}
|
}
|
||||||
const s = String(entry).trim();
|
const s = String(entry).trim();
|
||||||
if (!s) return null;
|
if (!s) return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user