feat(bisaya-course): restructure core patterns and enhance vocabulary preparation
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
- Updated core patterns in various scripts to use an object format with target phrases and glosses, improving clarity and usability for learners. - Enhanced the VocabService to normalize core pattern entries, ensuring consistent handling of vocabulary data. - Introduced new vocabulary preparation steps in the VocabLessonView, guiding users through active review processes before engaging with the vocabulary trainer. - Added localization support for new vocabulary preparation hints and instructions in multiple languages, enhancing user experience across the application.
This commit is contained in:
@@ -447,6 +447,14 @@
|
||||
"grammarImpulse": "Grammatik-Impuls",
|
||||
"learningGoals": "Lernziele",
|
||||
"corePatterns": "Kernmuster",
|
||||
"corePatternsHint": "Zuerst die Zielsprache lesen, darunter die deutsche Bedeutung — so lernst du jedes Muster bewusst in beiden Richtungen.",
|
||||
"vocabPrepTitle": "Vorbereitung vor dem Vokabeltrainer",
|
||||
"vocabPrepStep1": "Lies Kernmuster und Wortliste (Deutsch ↔ Zielsprache) einmal in Ruhe durch.",
|
||||
"vocabPrepConfirm1": "Erste Durchsicht erledigt",
|
||||
"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.",
|
||||
"vocabTrainerLockedHint": "Bitte bestätige zuerst zwei Lern-Durchgänge bei „Vorbereitung vor dem Vokabeltrainer“.",
|
||||
"speakingTasks": "Sprechaufträge",
|
||||
"speakingPrompt": "Sprechauftrag",
|
||||
"practicalTasks": "Praxisaufgaben",
|
||||
|
||||
@@ -447,6 +447,14 @@
|
||||
"grammarImpulse": "Grammar Focus",
|
||||
"learningGoals": "Learning Goals",
|
||||
"corePatterns": "Core Patterns",
|
||||
"corePatternsHint": "Read the target language first, then the meaning below — you learn each pattern both ways.",
|
||||
"vocabPrepTitle": "Preparation before the vocabulary trainer",
|
||||
"vocabPrepStep1": "Read through core patterns and the word list (native language ↔ target language) once.",
|
||||
"vocabPrepConfirm1": "First pass done",
|
||||
"vocabPrepStep2": "Go through the same items again (active review, not testing yet).",
|
||||
"vocabPrepConfirm2": "Second pass done",
|
||||
"vocabPrepReady": "You can start the vocabulary trainer now.",
|
||||
"vocabTrainerLockedHint": "Please confirm two preparation steps under “Preparation before the vocabulary trainer” first.",
|
||||
"speakingTasks": "Speaking Tasks",
|
||||
"speakingPrompt": "Speaking Prompt",
|
||||
"practicalTasks": "Practical Tasks",
|
||||
|
||||
@@ -445,6 +445,14 @@
|
||||
"grammarImpulse": "Impulso gramatical",
|
||||
"learningGoals": "Objetivos",
|
||||
"corePatterns": "Patrones básicos",
|
||||
"corePatternsHint": "Primero la lengua meta; debajo, el significado en tu idioma.",
|
||||
"vocabPrepTitle": "Preparación antes del entrenador de vocabulario",
|
||||
"vocabPrepStep1": "Lee una vez los patrones clave y la lista de palabras (idioma nativo ↔ lengua meta).",
|
||||
"vocabPrepConfirm1": "Primera lectura hecha",
|
||||
"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.",
|
||||
"vocabTrainerLockedHint": "Confirma primero los dos pasos de preparación arriba.",
|
||||
"speakingTasks": "Tareas orales",
|
||||
"speakingPrompt": "Tarea oral",
|
||||
"practicalTasks": "Tareas prácticas",
|
||||
|
||||
@@ -92,11 +92,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="lessonDidactics.corePatterns.length > 0" class="didactic-card">
|
||||
<div v-if="normalizedCorePatterns.length > 0" class="didactic-card">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.corePatterns') }}</h4>
|
||||
<p v-if="corePatternsHaveGloss" class="core-patterns-hint">
|
||||
{{ $t('socialnetwork.vocab.courses.corePatternsHint') }}
|
||||
</p>
|
||||
<div class="pattern-list">
|
||||
<div v-for="(pattern, index) in lessonDidactics.corePatterns" :key="'pattern-' + index" class="pattern-item">
|
||||
{{ pattern }}
|
||||
<div v-for="(pattern, index) in normalizedCorePatterns" :key="'pattern-' + index" class="pattern-item">
|
||||
<div class="pattern-target">{{ pattern.target }}</div>
|
||||
<div v-if="pattern.gloss" class="pattern-gloss">{{ pattern.gloss }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,6 +217,40 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wichtige Begriffe (vor dem Trainer: passiv, mit DE ↔ Zielsprache) -->
|
||||
<div v-if="lesson && importantVocab && importantVocab.length > 0 && !vocabTrainerActive" class="vocab-list">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.importantVocab') }}</h4>
|
||||
<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>
|
||||
<span class="separator">→</span>
|
||||
<span>{{ vocab.reference }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zwei Durchgänge vor dem aktiven Üben -->
|
||||
<div
|
||||
v-if="importantVocab && importantVocab.length > 0 && !vocabTrainerActive"
|
||||
class="vocab-prep-pass didactic-card"
|
||||
>
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.vocabPrepTitle') }}</h4>
|
||||
<template v-if="lessonPrepStage === 0">
|
||||
<p>{{ $t('socialnetwork.vocab.courses.vocabPrepStep1') }}</p>
|
||||
<button type="button" class="btn-prep-pass" @click="lessonPrepStage = 1">
|
||||
{{ $t('socialnetwork.vocab.courses.vocabPrepConfirm1') }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="lessonPrepStage === 1">
|
||||
<p>{{ $t('socialnetwork.vocab.courses.vocabPrepStep2') }}</p>
|
||||
<button type="button" class="btn-prep-pass" @click="lessonPrepStage = 2">
|
||||
{{ $t('socialnetwork.vocab.courses.vocabPrepConfirm2') }}
|
||||
</button>
|
||||
</template>
|
||||
<p v-else class="vocab-prep-pass__ready">{{ $t('socialnetwork.vocab.courses.vocabPrepReady') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Vokabeltrainer -->
|
||||
<div v-if="importantVocab && importantVocab.length > 0" class="vocab-trainer-section">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.vocabTrainer') }}</h4>
|
||||
@@ -225,10 +263,13 @@
|
||||
<p>{{ exerciseUnlockHint }}</p>
|
||||
</div>
|
||||
<div v-if="!vocabTrainerActive" class="vocab-trainer-start">
|
||||
<p>{{ hasPreviousVocab ? 'Starte mit den neuen Vokabeln dieser Lektion. Mit fortschreitendem Üben mischt der Trainer automatisch passende Wiederholungen ein.' : $t('socialnetwork.vocab.courses.vocabTrainerDescription') }}</p>
|
||||
<button @click="startVocabTrainer" class="btn-start-trainer">
|
||||
{{ hasPreviousVocab ? 'Lektion starten' : $t('socialnetwork.vocab.courses.startVocabTrainer') }}
|
||||
</button>
|
||||
<template v-if="canStartVocabTrainerPrep">
|
||||
<p>{{ hasPreviousVocab ? 'Starte mit den neuen Vokabeln dieser Lektion. Mit fortschreitendem Üben mischt der Trainer automatisch passende Wiederholungen ein.' : $t('socialnetwork.vocab.courses.vocabTrainerDescription') }}</p>
|
||||
<button @click="startVocabTrainer" class="btn-start-trainer">
|
||||
{{ hasPreviousVocab ? 'Lektion starten' : $t('socialnetwork.vocab.courses.startVocabTrainer') }}
|
||||
</button>
|
||||
</template>
|
||||
<p v-else class="vocab-trainer-locked-hint">{{ $t('socialnetwork.vocab.courses.vocabTrainerLockedHint') }}</p>
|
||||
</div>
|
||||
<div v-else class="vocab-trainer-active">
|
||||
<div class="vocab-trainer-stats">
|
||||
@@ -313,19 +354,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wichtige Begriffe Liste (nur Anzeige) -->
|
||||
<div v-if="lesson && importantVocab && importantVocab.length > 0 && !vocabTrainerActive" class="vocab-list">
|
||||
<h4>{{ $t('socialnetwork.vocab.courses.importantVocab') }}</h4>
|
||||
<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>
|
||||
<span class="separator">→</span>
|
||||
<span>{{ vocab.reference }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hinweis wenn keine Vokabeln vorhanden -->
|
||||
<div v-else-if="lesson && (!importantVocab || importantVocab.length === 0)" class="no-vocab-info">
|
||||
<p>{{ $t('socialnetwork.vocab.courses.noVocabInfo') }}</p>
|
||||
@@ -734,6 +762,8 @@ export default {
|
||||
vocabTrainerCurrentAttempts: 0,
|
||||
vocabTrainerReviewAttempts: 0,
|
||||
exercisePreparationCompleted: false,
|
||||
/** 0 = noch kein Durchgang, 1 = erste Durchsicht, 2 = zweite Durchsicht — dann Vokabeltrainer */
|
||||
lessonPrepStage: 0,
|
||||
currentVocabQuestion: null,
|
||||
vocabTrainerAnswer: '',
|
||||
vocabTrainerSelectedChoice: null,
|
||||
@@ -897,6 +927,21 @@ export default {
|
||||
practicalTasks: []
|
||||
};
|
||||
},
|
||||
normalizedCorePatterns() {
|
||||
const raw = this.lessonDidactics.corePatterns || [];
|
||||
return raw
|
||||
.map((p) => this.normalizeCorePatternEntry(p))
|
||||
.filter(Boolean);
|
||||
},
|
||||
corePatternsHaveGloss() {
|
||||
return this.normalizedCorePatterns.some((p) => p.gloss);
|
||||
},
|
||||
canStartVocabTrainerPrep() {
|
||||
if (!this.importantVocab || this.lessonPrepStage < 2) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
lessonPedagogy() {
|
||||
return this.lesson?.pedagogy || {
|
||||
didacticMode: null,
|
||||
@@ -943,6 +988,29 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
normalizeCorePatternEntry(p) {
|
||||
if (p && typeof p === 'object' && p.target) {
|
||||
return {
|
||||
target: String(p.target).trim(),
|
||||
gloss: String(p.gloss || '').trim()
|
||||
};
|
||||
}
|
||||
const s = String(p || '').trim();
|
||||
if (!s) return null;
|
||||
const i = s.indexOf('|');
|
||||
if (i !== -1) {
|
||||
return {
|
||||
target: s.slice(0, i).trim(),
|
||||
gloss: s.slice(i + 1).trim()
|
||||
};
|
||||
}
|
||||
return { target: s, gloss: '' };
|
||||
},
|
||||
corePatternToDisplayString(p) {
|
||||
const n = this.normalizeCorePatternEntry(p);
|
||||
if (!n) return '';
|
||||
return n.gloss ? `${n.target} (${n.gloss})` : n.target;
|
||||
},
|
||||
openExercisesTab() {
|
||||
if (!this.canAccessExercises) {
|
||||
this.activeTab = 'learn';
|
||||
@@ -1133,6 +1201,7 @@ export default {
|
||||
this.assistantInput = '';
|
||||
this.assistantError = '';
|
||||
this.exercisePreparationCompleted = false;
|
||||
this.lessonPrepStage = 0;
|
||||
this.vocabTrainerActive = false;
|
||||
this.vocabTrainerPool = [];
|
||||
this.vocabTrainerMixedPool = [];
|
||||
@@ -1222,10 +1291,11 @@ export default {
|
||||
buildAssistantPrompt(preset) {
|
||||
const lessonTitle = this.lesson?.title || this.$t('socialnetwork.vocab.courses.thisLesson');
|
||||
const firstPattern = this.lessonDidactics.corePatterns?.[0];
|
||||
const firstPatternStr = firstPattern ? this.corePatternToDisplayString(firstPattern) : '';
|
||||
const firstGrammar = this.lessonDidactics.grammarFocus?.[0]?.text;
|
||||
|
||||
if (preset === 'explain') {
|
||||
return `${this.$t('socialnetwork.vocab.courses.languageAssistantPresetExplainStart')} "${lessonTitle}". ${firstPattern ? `${this.$t('socialnetwork.vocab.courses.languageAssistantPatternHint')} ${firstPattern}.` : ''} ${firstGrammar || ''}`.trim();
|
||||
return `${this.$t('socialnetwork.vocab.courses.languageAssistantPresetExplainStart')} "${lessonTitle}". ${firstPatternStr ? `${this.$t('socialnetwork.vocab.courses.languageAssistantPatternHint')} ${firstPatternStr}.` : ''} ${firstGrammar || ''}`.trim();
|
||||
}
|
||||
if (preset === 'correct') {
|
||||
return this.$t('socialnetwork.vocab.courses.languageAssistantPresetCorrectStart', { lesson: lessonTitle });
|
||||
@@ -1669,6 +1739,9 @@ export default {
|
||||
debugLog('[VocabLessonView] Keine Vokabeln vorhanden');
|
||||
return;
|
||||
}
|
||||
if (!this.canStartVocabTrainerPrep) {
|
||||
return;
|
||||
}
|
||||
debugLog('[VocabLessonView] Vokabeln gefunden:', this.importantVocab.length);
|
||||
debugLog('[VocabLessonView] Alte Vokabeln:', this.previousVocab?.length || 0);
|
||||
this.vocabTrainerActive = true;
|
||||
@@ -2329,6 +2402,42 @@ export default {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.pattern-target {
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.pattern-gloss {
|
||||
margin-top: 6px;
|
||||
font-size: 0.92rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.core-patterns-hint {
|
||||
margin: 0 0 12px;
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.vocab-prep-pass {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.vocab-prep-pass .btn-prep-pass {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.vocab-prep-pass__ready {
|
||||
margin: 0;
|
||||
color: #2d6a3e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vocab-trainer-locked-hint {
|
||||
margin: 0;
|
||||
color: #8a5a00;
|
||||
}
|
||||
|
||||
.grammar-example,
|
||||
.speaking-cue,
|
||||
.pattern-drill-hint {
|
||||
|
||||
Reference in New Issue
Block a user