feat(vocab): integrate fallback core patterns and enhance vocabulary display in VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m44s
All checks were successful
Deploy to production / deploy (push) Successful in 2m44s
- Added a new method in VocabService to merge core pattern glosses with fallback patterns, improving vocabulary clarity and consistency. - Updated VocabLessonView to utilize the merged core patterns, ensuring a comprehensive vocabulary overview for users. - Refactored vocabulary handling logic to enhance user experience during vocabulary lessons, including improved display of lesson vocabulary.
This commit is contained in:
24
backend/scripts/bisaya-course-phase1.js
Normal file
24
backend/scripts/bisaya-course-phase1.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export const BISAYA_PHASE1_DIDACTICS = {
|
||||
'Begrüßungen & Höflichkeit': {
|
||||
corePatterns: [
|
||||
{ target: 'Kumusta ka?', gloss: 'Wie geht es dir?' },
|
||||
{ target: 'Maayong buntag.', gloss: 'Guten Morgen.' },
|
||||
{ target: 'Maayong adlaw.', gloss: 'Guten Tag.' },
|
||||
{ target: 'Maayong gabii.', gloss: 'Guten Abend.' },
|
||||
{ target: 'Maayong gabii, matulog na ta.', gloss: 'Guten Abend, wir legen uns schlafen.' },
|
||||
{ target: 'Katulog og maayo.', gloss: 'Schlaf gut.' },
|
||||
{ target: 'Kapoy na ka?', gloss: 'Bist du müde?' },
|
||||
{ target: 'Matulog na ta.', gloss: 'Lass uns schlafen gehen.' },
|
||||
{ target: 'Inom sa og tubig.', gloss: 'Trink Wasser.' },
|
||||
{ target: 'Patya ang suga.', gloss: 'Mach das Licht aus.' },
|
||||
{ target: 'Tabuni ang imong kaugalingon.', gloss: 'Deck dich zu.' },
|
||||
{ target: 'Ugma nasad.', gloss: 'Bis morgen wieder.' },
|
||||
{ target: 'Damgo og nindot.', gloss: 'Träum schön.' },
|
||||
{ target: 'Amping.', gloss: 'Pass auf dich auf.' },
|
||||
{ target: 'Babay.', gloss: 'Tschüss.' },
|
||||
{ target: 'Maayo ko.', gloss: 'Mir geht es gut.' },
|
||||
{ target: 'Salamat.', gloss: 'Danke.' },
|
||||
{ target: 'Palihug.', gloss: 'Bitte.' }
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import UserParam from '../models/community/user_param.js';
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import { notifyUser } from '../utils/socket.js';
|
||||
import { Op } from 'sequelize';
|
||||
import { BISAYA_PHASE1_DIDACTICS } from '../scripts/bisaya-course-phase1.js';
|
||||
|
||||
export default class VocabService {
|
||||
async _getUserByHashedId(hashedUserId) {
|
||||
@@ -347,6 +348,27 @@ export default class VocabService {
|
||||
});
|
||||
}
|
||||
|
||||
_mergeCorePatternGlosses(primaryPatterns = [], fallbackPatterns = []) {
|
||||
const fallbackByTarget = new Map(
|
||||
fallbackPatterns
|
||||
.map((entry) => this._normalizeCorePatternEntry(entry))
|
||||
.filter(Boolean)
|
||||
.map((entry) => [this._normalizeLexeme(entry.target), entry.gloss || ''])
|
||||
);
|
||||
|
||||
return primaryPatterns.map((entry) => {
|
||||
const normalized = this._normalizeCorePatternEntry(entry);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
if (normalized.gloss) {
|
||||
return normalized;
|
||||
}
|
||||
const gloss = fallbackByTarget.get(this._normalizeLexeme(normalized.target)) || '';
|
||||
return gloss ? { ...normalized, gloss } : normalized;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
_normalizeStructuredList(value, keys = ['title', 'text']) {
|
||||
if (!value) return [];
|
||||
if (Array.isArray(value)) {
|
||||
@@ -560,9 +582,13 @@ export default class VocabService {
|
||||
|
||||
const learningGoals = this._normalizeStringList(plainLesson.learningGoals);
|
||||
const extractedTrainerVocabs = this._extractTrainerVocabsFromExercises(grammarExercises);
|
||||
const corePatterns = this._enrichCorePatternsWithGloss(
|
||||
this._normalizeCorePatternList(plainLesson.corePatterns),
|
||||
extractedTrainerVocabs
|
||||
const phase1FallbackCorePatterns = BISAYA_PHASE1_DIDACTICS[plainLesson.title]?.corePatterns || [];
|
||||
const corePatterns = this._mergeCorePatternGlosses(
|
||||
this._enrichCorePatternsWithGloss(
|
||||
this._normalizeCorePatternList(plainLesson.corePatterns),
|
||||
extractedTrainerVocabs
|
||||
),
|
||||
phase1FallbackCorePatterns
|
||||
);
|
||||
const grammarFocus = this._normalizeStructuredList(plainLesson.grammarFocus, ['title', 'text', 'example']);
|
||||
const explicitSpeakingPrompts = this._normalizeStructuredList(plainLesson.speakingPrompts, ['title', 'prompt', 'cue']);
|
||||
@@ -578,9 +604,12 @@ export default class VocabService {
|
||||
],
|
||||
corePatterns: corePatterns.length > 0
|
||||
? corePatterns
|
||||
: this._enrichCorePatternsWithGloss(
|
||||
uniquePatterns.slice(0, 5).map((s) => ({ target: String(s || '').trim(), gloss: '' })).filter((p) => p.target),
|
||||
extractedTrainerVocabs
|
||||
: this._mergeCorePatternGlosses(
|
||||
this._enrichCorePatternsWithGloss(
|
||||
uniquePatterns.slice(0, 5).map((s) => ({ target: String(s || '').trim(), gloss: '' })).filter((p) => p.target),
|
||||
extractedTrainerVocabs
|
||||
),
|
||||
phase1FallbackCorePatterns
|
||||
),
|
||||
grammarFocus: grammarFocus.length > 0 ? grammarFocus : uniqueGrammarExplanations.slice(0, 4),
|
||||
speakingPrompts: explicitSpeakingPrompts.length > 0 ? explicitSpeakingPrompts : speakingPrompts.slice(0, 4),
|
||||
|
||||
@@ -456,7 +456,9 @@
|
||||
"vocabPrepStep2": "Gehe die gleichen Begriffe noch einmal durch (aktive Wiederholung, ohne zu üben).",
|
||||
"vocabPrepConfirm2": "Zweite Durchsicht erledigt",
|
||||
"vocabPrepReady": "Du kannst jetzt mit dem Vokabeltrainer starten.",
|
||||
"vocabOverviewToggle": "Gesamtübersicht der Begriffe anzeigen",
|
||||
"vocabTrainerLockedHint": "Bitte bestätige zuerst zwei Lern-Durchgänge bei „Vorbereitung vor dem Vokabeltrainer“.",
|
||||
"exerciseUnlockHintAfterPrep": "Arbeite zuerst die vorbereiteten Begriffe durch. Danach wird die Kapitel-Prüfung freigeschaltet.",
|
||||
"speakingTasks": "Sprechaufträge",
|
||||
"speakingPrompt": "Sprechauftrag",
|
||||
"practicalTasks": "Praxisaufgaben",
|
||||
|
||||
@@ -456,7 +456,9 @@
|
||||
"vocabPrepStep2": "Go through the same items again (active review, not testing yet).",
|
||||
"vocabPrepConfirm2": "Second pass done",
|
||||
"vocabPrepReady": "You can start the vocabulary trainer now.",
|
||||
"vocabOverviewToggle": "Show full overview of terms",
|
||||
"vocabTrainerLockedHint": "Please confirm two preparation steps under “Preparation before the vocabulary trainer” first.",
|
||||
"exerciseUnlockHintAfterPrep": "Work through the prepared terms first. The chapter test will unlock afterwards.",
|
||||
"speakingTasks": "Speaking Tasks",
|
||||
"speakingPrompt": "Speaking Prompt",
|
||||
"practicalTasks": "Practical Tasks",
|
||||
|
||||
@@ -454,7 +454,9 @@
|
||||
"vocabPrepStep2": "Repasa los mismos elementos otra vez (repaso activo, aún sin practicar).",
|
||||
"vocabPrepConfirm2": "Segunda lectura hecha",
|
||||
"vocabPrepReady": "Ya puedes iniciar el entrenador de vocabulario.",
|
||||
"vocabOverviewToggle": "Mostrar vista general completa de los términos",
|
||||
"vocabTrainerLockedHint": "Confirma primero los dos pasos de preparación arriba.",
|
||||
"exerciseUnlockHintAfterPrep": "Primero recorre los términos preparados. Después se desbloqueará la prueba del capítulo.",
|
||||
"speakingTasks": "Tareas orales",
|
||||
"speakingPrompt": "Tarea oral",
|
||||
"practicalTasks": "Tareas prácticas",
|
||||
|
||||
@@ -247,24 +247,27 @@
|
||||
<p v-else class="vocab-prep-pass__ready">{{ $t('socialnetwork.vocab.courses.vocabPrepReady') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Wichtige Begriffe erst nach den zwei Durchgängen gesammelt anzeigen -->
|
||||
<div
|
||||
v-if="lesson && importantVocab && importantVocab.length > 0 && !vocabTrainerActive && lessonPrepStage >= 2"
|
||||
class="vocab-list"
|
||||
<!-- Gesamtübersicht: gleicher Lernsatz wie Vorbereitung und Trainer -->
|
||||
<details
|
||||
v-if="lesson && lessonVocab.length > 0 && !vocabTrainerActive"
|
||||
class="vocab-list vocab-list--overview"
|
||||
:open="lessonPrepStage >= 2"
|
||||
>
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.importantVocab') }}</h4>
|
||||
<summary class="vocab-list__summary">
|
||||
{{ $t('socialnetwork.vocab.courses.vocabOverviewToggle') }}
|
||||
</summary>
|
||||
<p class="vocab-info-text">{{ $t('socialnetwork.vocab.courses.vocabInfoText') }}</p>
|
||||
<div class="vocab-items">
|
||||
<div v-for="(vocab, index) in importantVocab" :key="index" class="vocab-item">
|
||||
<strong>{{ vocab.learning }}</strong>
|
||||
<div v-for="(vocab, index) in lessonVocab" :key="index" class="vocab-item">
|
||||
<strong>{{ vocab.learning || '—' }}</strong>
|
||||
<span class="separator">→</span>
|
||||
<span>{{ vocab.reference }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Vokabeltrainer -->
|
||||
<div v-if="importantVocab && importantVocab.length > 0" class="vocab-trainer-section">
|
||||
<div v-if="trainableLessonVocab.length > 0" class="vocab-trainer-section">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.vocabTrainer') }}</h4>
|
||||
<div v-if="hasPreviousVocab" class="review-priority-note">
|
||||
<strong>Wiederholung läuft schrittweise mit</strong>
|
||||
@@ -367,7 +370,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hinweis wenn keine Vokabeln vorhanden -->
|
||||
<div v-else-if="lesson && (!importantVocab || importantVocab.length === 0)" class="no-vocab-info">
|
||||
<div v-else-if="lesson && lessonVocab.length === 0" class="no-vocab-info">
|
||||
<p>{{ $t('socialnetwork.vocab.courses.noVocabInfo') }}</p>
|
||||
</div>
|
||||
|
||||
@@ -830,7 +833,7 @@ export default {
|
||||
return 1;
|
||||
},
|
||||
trainerNewFocusTarget() {
|
||||
const vocabCount = this.importantVocab?.length || 0;
|
||||
const vocabCount = this.trainableLessonVocab?.length || 0;
|
||||
const exerciseCount = this.effectiveExercises?.length || 0;
|
||||
const durationBonus = Math.max(0, Math.round((this.lesson?.targetMinutes || 0) / 5) - 1);
|
||||
const baseTarget = Math.ceil((Math.max(vocabCount, 4) * 1.35) + (exerciseCount * 0.35) + durationBonus);
|
||||
@@ -858,9 +861,16 @@ export default {
|
||||
canAccessExercises() {
|
||||
if (!this.hasExercises) return false;
|
||||
const isReview = this.lesson?.lessonType === 'review' || this.lesson?.lessonType === 'vocab_review';
|
||||
return isReview || this.exercisePreparationCompleted;
|
||||
if (isReview) return true;
|
||||
if (this.trainableLessonVocab.length === 0 && this.prepItems.length > 0) {
|
||||
return this.lessonPrepStage >= 2;
|
||||
}
|
||||
return this.exercisePreparationCompleted;
|
||||
},
|
||||
exerciseUnlockHint() {
|
||||
if (this.trainableLessonVocab.length === 0 && this.prepItems.length > 0) {
|
||||
return this.$t('socialnetwork.vocab.courses.exerciseUnlockHintAfterPrep');
|
||||
}
|
||||
if (this.hasPreviousVocab) {
|
||||
return `Lerne zuerst die neuen Inhalte der Lektion und arbeite dich durch ungefähr ${this.trainerExerciseUnlockAttempts} Trainerfragen. Ältere Vokabeln werden dabei nach und nach zugemischt.`;
|
||||
}
|
||||
@@ -931,6 +941,39 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
lessonVocab() {
|
||||
const vocabByReference = new Map();
|
||||
const addEntry = (entry) => {
|
||||
const reference = String(entry?.reference || '').trim();
|
||||
const learning = String(entry?.learning || '').trim();
|
||||
if (!reference) return;
|
||||
const key = reference.toLowerCase();
|
||||
if (!vocabByReference.has(key)) {
|
||||
vocabByReference.set(key, { learning, reference });
|
||||
return;
|
||||
}
|
||||
const existing = vocabByReference.get(key);
|
||||
if (!existing.learning && learning) {
|
||||
existing.learning = learning;
|
||||
}
|
||||
};
|
||||
|
||||
this.normalizedCorePatterns.forEach((item) => {
|
||||
addEntry({
|
||||
learning: item.gloss || '',
|
||||
reference: item.target || ''
|
||||
});
|
||||
});
|
||||
|
||||
this.importantVocab.forEach((item) => {
|
||||
addEntry(item);
|
||||
});
|
||||
|
||||
return Array.from(vocabByReference.values());
|
||||
},
|
||||
trainableLessonVocab() {
|
||||
return this.lessonVocab.filter((entry) => entry.learning && entry.reference && entry.learning !== entry.reference);
|
||||
},
|
||||
lessonDidactics() {
|
||||
return this.lesson?.didactics || {
|
||||
learningGoals: [],
|
||||
@@ -947,21 +990,7 @@ export default {
|
||||
.filter(Boolean);
|
||||
},
|
||||
prepItems() {
|
||||
if (this.normalizedCorePatterns.length > 0) {
|
||||
const glossByReference = new Map(
|
||||
(this.importantVocab || [])
|
||||
.map((item) => [String(item?.reference || '').trim().toLowerCase(), String(item?.learning || '').trim()])
|
||||
.filter(([reference, learning]) => reference && learning)
|
||||
);
|
||||
return this.normalizedCorePatterns.map((item) => {
|
||||
if (item.gloss) {
|
||||
return item;
|
||||
}
|
||||
const gloss = glossByReference.get(String(item.target || '').trim().toLowerCase()) || '';
|
||||
return gloss ? { ...item, gloss } : item;
|
||||
});
|
||||
}
|
||||
return (this.importantVocab || [])
|
||||
return this.lessonVocab
|
||||
.map((item) => ({
|
||||
target: String(item?.reference || '').trim(),
|
||||
gloss: String(item?.learning || '').trim()
|
||||
@@ -981,7 +1010,7 @@ export default {
|
||||
return this.normalizedCorePatterns.some((p) => p.gloss);
|
||||
},
|
||||
canStartVocabTrainerPrep() {
|
||||
if (!this.importantVocab || this.importantVocab.length === 0) {
|
||||
if (!this.trainableLessonVocab || this.trainableLessonVocab.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (this.prepItems.length > 0 && this.lessonPrepStage < 2) {
|
||||
@@ -1794,17 +1823,17 @@ export default {
|
||||
// Vokabeltrainer-Methoden
|
||||
startVocabTrainer() {
|
||||
debugLog('[VocabLessonView] startVocabTrainer aufgerufen');
|
||||
if (!this.importantVocab || this.importantVocab.length === 0) {
|
||||
if (!this.trainableLessonVocab || this.trainableLessonVocab.length === 0) {
|
||||
debugLog('[VocabLessonView] Keine Vokabeln vorhanden');
|
||||
return;
|
||||
}
|
||||
if (!this.canStartVocabTrainerPrep) {
|
||||
return;
|
||||
}
|
||||
debugLog('[VocabLessonView] Vokabeln gefunden:', this.importantVocab.length);
|
||||
debugLog('[VocabLessonView] Vokabeln gefunden:', this.trainableLessonVocab.length);
|
||||
debugLog('[VocabLessonView] Alte Vokabeln:', this.previousVocab?.length || 0);
|
||||
this.vocabTrainerActive = true;
|
||||
this.vocabTrainerPool = [...this.importantVocab];
|
||||
this.vocabTrainerPool = [...this.trainableLessonVocab];
|
||||
this.vocabTrainerMode = 'multiple_choice';
|
||||
this.vocabTrainerAutoSwitchedToTyping = false;
|
||||
this.vocabTrainerCorrect = 0;
|
||||
@@ -1817,7 +1846,7 @@ export default {
|
||||
this.vocabTrainerMixedPool = this._buildMixedPool();
|
||||
this.vocabTrainerPhase = 'current';
|
||||
this.vocabTrainerMixedAttempts = 0;
|
||||
this.vocabTrainerPool = [...this.importantVocab];
|
||||
this.vocabTrainerPool = [...this.trainableLessonVocab];
|
||||
debugLog('[VocabLessonView] Mixed-Pool:', this.vocabTrainerMixedPool.length, 'Vokabeln');
|
||||
debugLog('[VocabLessonView] Rufe nextVocabQuestion auf');
|
||||
this.$nextTick(() => {
|
||||
@@ -1841,7 +1870,7 @@ export default {
|
||||
/** Erstellt den Mixed-Pool aus vorherigen Lektions-Vokabeln (ohne Duplikate der aktuellen Lektion) */
|
||||
_buildMixedPool() {
|
||||
if (!this.previousVocab || this.previousVocab.length === 0) return [];
|
||||
const currentKeys = new Set(this.importantVocab.map(v => this.getVocabKey(v)));
|
||||
const currentKeys = new Set(this.trainableLessonVocab.map(v => this.getVocabKey(v)));
|
||||
const filtered = this.previousVocab.filter(v => !currentKeys.has(this.getVocabKey(v)));
|
||||
// Zufällig mischen und auf 40 begrenzen
|
||||
const shuffled = [...filtered].sort(() => Math.random() - 0.5);
|
||||
@@ -1868,7 +1897,7 @@ export default {
|
||||
debugLog('[VocabLessonView] Mixed-Phase abgeschlossen, wechsle zu Typing');
|
||||
this.vocabTrainerMode = 'typing';
|
||||
this.vocabTrainerAutoSwitchedToTyping = true;
|
||||
this.vocabTrainerPool = [...this.importantVocab, ...this.vocabTrainerMixedPool];
|
||||
this.vocabTrainerPool = [...this.trainableLessonVocab, ...this.vocabTrainerMixedPool];
|
||||
this.vocabTrainerCorrect = 0;
|
||||
this.vocabTrainerWrong = 0;
|
||||
this.vocabTrainerTotalAttempts = 0;
|
||||
@@ -1880,8 +1909,8 @@ export default {
|
||||
this.vocabTrainerMode = 'multiple_choice';
|
||||
this.vocabTrainerAutoSwitchedToTyping = false;
|
||||
this.vocabTrainerPool = this.vocabTrainerPhase === 'mixed'
|
||||
? [...this.importantVocab, ...this.vocabTrainerMixedPool]
|
||||
: [...this.importantVocab];
|
||||
? [...this.trainableLessonVocab, ...this.vocabTrainerMixedPool]
|
||||
: [...this.trainableLessonVocab];
|
||||
// Reset Stats für Multiple Choice Modus
|
||||
this.vocabTrainerCorrect = 0;
|
||||
this.vocabTrainerWrong = 0;
|
||||
@@ -1988,7 +2017,7 @@ export default {
|
||||
this.checkVocabModeSwitch();
|
||||
|
||||
let questionSource = 'current';
|
||||
let sourcePool = this.importantVocab;
|
||||
let sourcePool = this.trainableLessonVocab;
|
||||
|
||||
if (this.vocabTrainerMode === 'typing') {
|
||||
sourcePool = this.vocabTrainerPool;
|
||||
@@ -2001,14 +2030,14 @@ export default {
|
||||
}
|
||||
|
||||
if (!sourcePool || sourcePool.length === 0) {
|
||||
sourcePool = this.importantVocab;
|
||||
sourcePool = this.trainableLessonVocab;
|
||||
questionSource = 'current';
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * sourcePool.length);
|
||||
const vocab = sourcePool[randomIndex];
|
||||
this.vocabTrainerDirection = Math.random() < 0.5 ? 'L2R' : 'R2L';
|
||||
const allTrainerVocabs = [...this.importantVocab, ...this.vocabTrainerMixedPool];
|
||||
const allTrainerVocabs = [...this.trainableLessonVocab, ...this.vocabTrainerMixedPool];
|
||||
const prompt = this.vocabTrainerDirection === 'L2R' ? vocab.learning : vocab.reference;
|
||||
const acceptableAnswers = this.getEquivalentVocabAnswers(
|
||||
prompt,
|
||||
@@ -2795,6 +2824,17 @@ export default {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.vocab-list__summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #5f4313;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.vocab-list[open] .vocab-list__summary {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.vocab-list h4 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
|
||||
Reference in New Issue
Block a user