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

@@ -22,7 +22,26 @@ const LESSON_DIDACTICS = {
'Höfliche Reaktionen wie Danke und Bitte passend einsetzen.',
'Ein kurzes Begrüßungs-Mini-Gespräch laut üben.'
],
corePatterns: ['Kumusta ka?', 'Maayong buntag.', 'Maayong adlaw.', 'Maayong gabii.', 'Maayong gabii, matulog na ta.', 'Katulog og maayo.', 'Kapoy na ka?', 'Matulog na ta.', 'Inom sa og tubig.', 'Patya ang suga.', 'Tabuni ang imong kaugalingon.', 'Ugma nasad.', 'Damgo og nindot.', 'Amping.', 'Babay.', 'Maayo ko.', 'Salamat.', 'Palihug.'],
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.' }
],
grammarFocus: [
{ title: 'Kurzantworten mit ko', text: 'Mit "ko" sprichst du über dich selbst: "Maayo ko."', example: 'Maayo ko. = Mir geht es gut.' },
{ title: 'Maayong + Tageszeit', text: 'Mit "Maayong" kannst du Grüße für verschiedene Tageszeiten bilden.', example: 'Maayong buntag. / Maayong gabii.' },

View File

@@ -30,19 +30,46 @@ const GENERATED_BISAYA_DIDACTICS = {
...BISAYA_PHASE5_DIDACTICS
};
const GENERIC_DISTRACTOR_PATTERNS = Array.from(new Set(
Object.values(GENERATED_BISAYA_DIDACTICS)
.flatMap((entry) => Array.isArray(entry?.corePatterns) ? entry.corePatterns : [])
.map((pattern) => String(pattern || '').trim())
.filter(Boolean)
)).slice(0, 200);
function normalizeText(value) {
return String(value || '')
.trim()
.replace(/\s+/g, ' ');
}
function normalizeCorePatternEntry(entry) {
if (entry === null || entry === undefined || entry === '') {
return null;
}
if (typeof entry === 'object' && !Array.isArray(entry)) {
const target = normalizeText(entry.target ?? entry.ceb ?? entry.phrase ?? '');
const gloss = normalizeText(entry.gloss ?? entry.de ?? entry.translation ?? '');
if (!target) return null;
return { target, gloss };
}
const s = normalizeText(entry);
if (!s) return null;
const pipe = s.indexOf('|');
if (pipe !== -1) {
const target = normalizeText(s.slice(0, pipe));
const gloss = normalizeText(s.slice(pipe + 1));
if (!target) return null;
return { target, gloss };
}
return { target: s, gloss: '' };
}
function corePatternTarget(entry) {
const n = normalizeCorePatternEntry(entry);
return n ? n.target : '';
}
const GENERIC_DISTRACTOR_PATTERNS = Array.from(new Set(
Object.values(GENERATED_BISAYA_DIDACTICS)
.flatMap((entry) => Array.isArray(entry?.corePatterns) ? entry.corePatterns : [])
.map((pattern) => corePatternTarget(pattern))
.filter(Boolean)
)).slice(0, 200);
function simpleHash(value) {
return Array.from(String(value || '')).reduce((sum, char) => sum + char.charCodeAt(0), 0);
}
@@ -63,7 +90,9 @@ function getLessonDidactics(lesson) {
return {
learningGoals,
corePatterns: corePatterns.map((entry) => normalizeText(entry)).filter(Boolean),
corePatterns: corePatterns
.map((entry) => normalizeCorePatternEntry(entry))
.filter(Boolean),
grammarFocus,
speakingPrompts,
practicalTasks
@@ -323,10 +352,10 @@ function generateExercisesFromDidactics(lesson) {
return [];
}
const patternA = corePatterns[0];
const patternB = corePatterns[1] || corePatterns[0];
const patternA = corePatternTarget(corePatterns[0]);
const patternB = corePatternTarget(corePatterns[1] || corePatterns[0]);
const lessonPool = Array.from(new Set([
...corePatterns,
...corePatterns.map((p) => corePatternTarget(p)),
...GENERIC_DISTRACTOR_PATTERNS
]));
let generated = [];

View File

@@ -26,24 +26,24 @@ const LESSON_DIDACTICS = {
'Eine kurze Abend- und Schlafensroutine im Familienalltag sprechen.'
],
corePatterns: [
'Kumusta ka?',
'Maayong buntag.',
'Maayong adlaw.',
'Maayong gabii.',
'Maayong gabii, matulog na ta.',
'Katulog og maayo.',
'Kapoy na ka?',
'Matulog na ta.',
'Inom sa og tubig.',
'Patya ang suga.',
'Tabuni ang imong kaugalingon.',
'Ugma nasad.',
'Damgo og nindot.',
'Amping.',
'Babay.',
'Maayo ko.',
'Salamat.',
'Palihug.'
{ 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.' }
],
grammarFocus: [
{

View File

@@ -18,7 +18,26 @@ const LESSON_DIDACTICS = {
'Höfliche Reaktionen wie Danke und Bitte passend einsetzen.',
'Ein kurzes Begrüßungs-Mini-Gespräch laut üben.'
],
corePatterns: ['Kumusta ka?', 'Maayong buntag.', 'Maayong adlaw.', 'Maayong gabii.', 'Maayong gabii, matulog na ta.', 'Katulog og maayo.', 'Kapoy na ka?', 'Matulog na ta.', 'Inom sa og tubig.', 'Patya ang suga.', 'Tabuni ang imong kaugalingon.', 'Ugma nasad.', 'Damgo og nindot.', 'Amping.', 'Babay.', 'Maayo ko.', 'Salamat.', 'Palihug.'],
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.' }
],
grammarFocus: [
{ title: 'Kurzantworten mit ko', text: 'Mit "ko" sprichst du über dich selbst: "Maayo ko."', example: 'Maayo ko. = Mir geht es gut.' },
{ title: 'Maayong + Tageszeit', text: 'Mit "Maayong" kannst du Grüße für verschiedene Tageszeiten bilden.', example: 'Maayong buntag. / Maayong gabii.' },

View File

@@ -106,7 +106,13 @@ export default class VocabService {
`Lektion: ${lesson?.title || 'Unbekannte Lektion'}`,
lesson?.description ? `Beschreibung: ${lesson.description}` : '',
learningGoals.length ? `Lernziele: ${learningGoals.join(' | ')}` : '',
corePatterns.length ? `Kernmuster: ${corePatterns.join(' | ')}` : '',
corePatterns.length
? `Kernmuster: ${corePatterns.map((p) => {
const n = this._normalizeCorePatternEntry(p);
if (!n) return '';
return n.gloss ? `${n.target} (${n.gloss})` : n.target;
}).filter(Boolean).join(' | ')}`
: '',
speakingPrompts.length
? `Sprechaufträge: ${speakingPrompts.map((item) => item.prompt || item.title || '').filter(Boolean).join(' | ')}`
: '',
@@ -239,14 +245,16 @@ export default class VocabService {
speakingPrompts.forEach((prompt, index) => {
const learning = String(prompt?.prompt || prompt?.title || '').trim();
const reference = String(prompt?.cue || corePatterns[index] || corePatterns[0] || '').trim();
const refEntry = corePatterns[index] ?? corePatterns[0];
const reference = String(prompt?.cue || this._corePatternTarget(refEntry) || '').trim();
if (!learning || !reference || learning === reference) return;
vocabMap.set(`${learning}-${reference}`, { learning, reference });
});
practicalTasks.forEach((task, index) => {
const learning = String(task?.text || task?.title || '').trim();
const reference = String(corePatterns[index] || corePatterns[0] || '').trim();
const refEntry = corePatterns[index] ?? corePatterns[0];
const reference = String(this._corePatternTarget(refEntry) || '').trim();
if (!learning || !reference || learning === reference) return;
vocabMap.set(`${learning}-${reference}`, { learning, reference });
});
@@ -270,6 +278,49 @@ export default class VocabService {
return [];
}
/**
* Kernmuster: Zielsprachen-Phrase + optionale Glossierung (z. B. Deutsch).
* Unterstützt Legacy-Strings, "Phrase|Gloss" und Objekte { target, gloss } / { ceb, de }.
*/
_normalizeCorePatternEntry(entry) {
if (entry === null || entry === undefined || entry === '') {
return null;
}
if (typeof entry === 'object' && !Array.isArray(entry)) {
const target = String(entry.target ?? entry.ceb ?? entry.phrase ?? '').trim();
const gloss = String(entry.gloss ?? entry.de ?? entry.translation ?? '').trim();
if (!target) return null;
return { target, gloss };
}
const s = String(entry).trim();
if (!s) return null;
const pipe = s.indexOf('|');
if (pipe !== -1) {
const target = s.slice(0, pipe).trim();
const gloss = s.slice(pipe + 1).trim();
if (!target) return null;
return { target, gloss };
}
return { target: s, gloss: '' };
}
_normalizeCorePatternList(value) {
if (!value) return [];
const raw = Array.isArray(value)
? value
: (typeof value === 'string'
? value.split(/\r?\n|;/).map((entry) => entry.trim()).filter(Boolean)
: []);
return raw
.map((entry) => this._normalizeCorePatternEntry(entry))
.filter(Boolean);
}
_corePatternTarget(entry) {
const n = this._normalizeCorePatternEntry(entry);
return n ? n.target : '';
}
_normalizeStructuredList(value, keys = ['title', 'text']) {
if (!value) return [];
if (Array.isArray(value)) {
@@ -482,7 +533,7 @@ export default class VocabService {
const uniquePatterns = [...new Set(patterns.map((item) => String(item || '').trim()).filter(Boolean))];
const learningGoals = this._normalizeStringList(plainLesson.learningGoals);
const corePatterns = this._normalizeStringList(plainLesson.corePatterns);
const corePatterns = this._normalizeCorePatternList(plainLesson.corePatterns);
const grammarFocus = this._normalizeStructuredList(plainLesson.grammarFocus, ['title', 'text', 'example']);
const explicitSpeakingPrompts = this._normalizeStructuredList(plainLesson.speakingPrompts, ['title', 'prompt', 'cue']);
const practicalTasks = this._normalizeStructuredList(plainLesson.practicalTasks, ['title', 'text']);
@@ -495,7 +546,9 @@ export default class VocabService {
'Ein bis zwei Satzmuster aktiv anwenden.',
'Kurze Sätze oder Mini-Dialoge zum Thema selbst bilden.'
],
corePatterns: corePatterns.length > 0 ? corePatterns : uniquePatterns.slice(0, 5),
corePatterns: corePatterns.length > 0
? corePatterns
: uniquePatterns.slice(0, 5).map((s) => ({ target: String(s || '').trim(), gloss: '' })).filter((p) => p.target),
grammarFocus: grammarFocus.length > 0 ? grammarFocus : uniqueGrammarExplanations.slice(0, 4),
speakingPrompts: explicitSpeakingPrompts.length > 0 ? explicitSpeakingPrompts : speakingPrompts.slice(0, 4),
practicalTasks: practicalTasks.length > 0
@@ -1775,7 +1828,7 @@ export default class VocabService {
audioUrl: audioUrl || null,
culturalNotes: culturalNotes || null,
learningGoals: this._normalizeStringList(learningGoals),
corePatterns: this._normalizeStringList(corePatterns),
corePatterns: this._normalizeCorePatternList(corePatterns),
grammarFocus: this._normalizeStructuredList(grammarFocus, ['title', 'text', 'example']),
speakingPrompts: this._normalizeStructuredList(speakingPrompts, ['title', 'prompt', 'cue']),
practicalTasks: this._normalizeStructuredList(practicalTasks, ['title', 'text']),
@@ -1822,7 +1875,7 @@ export default class VocabService {
if (audioUrl !== undefined) updates.audioUrl = audioUrl;
if (culturalNotes !== undefined) updates.culturalNotes = culturalNotes;
if (learningGoals !== undefined) updates.learningGoals = this._normalizeStringList(learningGoals);
if (corePatterns !== undefined) updates.corePatterns = this._normalizeStringList(corePatterns);
if (corePatterns !== undefined) updates.corePatterns = this._normalizeCorePatternList(corePatterns);
if (grammarFocus !== undefined) updates.grammarFocus = this._normalizeStructuredList(grammarFocus, ['title', 'text', 'example']);
if (speakingPrompts !== undefined) updates.speakingPrompts = this._normalizeStructuredList(speakingPrompts, ['title', 'prompt', 'cue']);
if (practicalTasks !== undefined) updates.practicalTasks = this._normalizeStructuredList(practicalTasks, ['title', 'text']);

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

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 {