feat(bisaya-course): restructure core patterns and enhance vocabulary preparation
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:
Torsten Schulz (local)
2026-04-01 08:12:57 +02:00
parent 7e45049e94
commit 0c89c48e68
9 changed files with 312 additions and 59 deletions

View File

@@ -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 {