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:
@@ -22,7 +22,26 @@ const LESSON_DIDACTICS = {
|
|||||||
'Höfliche Reaktionen wie Danke und Bitte passend einsetzen.',
|
'Höfliche Reaktionen wie Danke und Bitte passend einsetzen.',
|
||||||
'Ein kurzes Begrüßungs-Mini-Gespräch laut üben.'
|
'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: [
|
grammarFocus: [
|
||||||
{ title: 'Kurzantworten mit ko', text: 'Mit "ko" sprichst du über dich selbst: "Maayo ko."', example: 'Maayo ko. = Mir geht es gut.' },
|
{ 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.' },
|
{ title: 'Maayong + Tageszeit', text: 'Mit "Maayong" kannst du Grüße für verschiedene Tageszeiten bilden.', example: 'Maayong buntag. / Maayong gabii.' },
|
||||||
|
|||||||
@@ -30,19 +30,46 @@ const GENERATED_BISAYA_DIDACTICS = {
|
|||||||
...BISAYA_PHASE5_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) {
|
function normalizeText(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/\s+/g, ' ');
|
.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) {
|
function simpleHash(value) {
|
||||||
return Array.from(String(value || '')).reduce((sum, char) => sum + char.charCodeAt(0), 0);
|
return Array.from(String(value || '')).reduce((sum, char) => sum + char.charCodeAt(0), 0);
|
||||||
}
|
}
|
||||||
@@ -63,7 +90,9 @@ function getLessonDidactics(lesson) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
learningGoals,
|
learningGoals,
|
||||||
corePatterns: corePatterns.map((entry) => normalizeText(entry)).filter(Boolean),
|
corePatterns: corePatterns
|
||||||
|
.map((entry) => normalizeCorePatternEntry(entry))
|
||||||
|
.filter(Boolean),
|
||||||
grammarFocus,
|
grammarFocus,
|
||||||
speakingPrompts,
|
speakingPrompts,
|
||||||
practicalTasks
|
practicalTasks
|
||||||
@@ -323,10 +352,10 @@ function generateExercisesFromDidactics(lesson) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const patternA = corePatterns[0];
|
const patternA = corePatternTarget(corePatterns[0]);
|
||||||
const patternB = corePatterns[1] || corePatterns[0];
|
const patternB = corePatternTarget(corePatterns[1] || corePatterns[0]);
|
||||||
const lessonPool = Array.from(new Set([
|
const lessonPool = Array.from(new Set([
|
||||||
...corePatterns,
|
...corePatterns.map((p) => corePatternTarget(p)),
|
||||||
...GENERIC_DISTRACTOR_PATTERNS
|
...GENERIC_DISTRACTOR_PATTERNS
|
||||||
]));
|
]));
|
||||||
let generated = [];
|
let generated = [];
|
||||||
|
|||||||
@@ -26,24 +26,24 @@ const LESSON_DIDACTICS = {
|
|||||||
'Eine kurze Abend- und Schlafensroutine im Familienalltag sprechen.'
|
'Eine kurze Abend- und Schlafensroutine im Familienalltag sprechen.'
|
||||||
],
|
],
|
||||||
corePatterns: [
|
corePatterns: [
|
||||||
'Kumusta ka?',
|
{ target: 'Kumusta ka?', gloss: 'Wie geht es dir?' },
|
||||||
'Maayong buntag.',
|
{ target: 'Maayong buntag.', gloss: 'Guten Morgen.' },
|
||||||
'Maayong adlaw.',
|
{ target: 'Maayong adlaw.', gloss: 'Guten Tag.' },
|
||||||
'Maayong gabii.',
|
{ target: 'Maayong gabii.', gloss: 'Guten Abend.' },
|
||||||
'Maayong gabii, matulog na ta.',
|
{ target: 'Maayong gabii, matulog na ta.', gloss: 'Guten Abend, wir legen uns schlafen.' },
|
||||||
'Katulog og maayo.',
|
{ target: 'Katulog og maayo.', gloss: 'Schlaf gut.' },
|
||||||
'Kapoy na ka?',
|
{ target: 'Kapoy na ka?', gloss: 'Bist du müde?' },
|
||||||
'Matulog na ta.',
|
{ target: 'Matulog na ta.', gloss: 'Lass uns schlafen gehen.' },
|
||||||
'Inom sa og tubig.',
|
{ target: 'Inom sa og tubig.', gloss: 'Trink Wasser.' },
|
||||||
'Patya ang suga.',
|
{ target: 'Patya ang suga.', gloss: 'Mach das Licht aus.' },
|
||||||
'Tabuni ang imong kaugalingon.',
|
{ target: 'Tabuni ang imong kaugalingon.', gloss: 'Deck dich zu.' },
|
||||||
'Ugma nasad.',
|
{ target: 'Ugma nasad.', gloss: 'Bis morgen wieder.' },
|
||||||
'Damgo og nindot.',
|
{ target: 'Damgo og nindot.', gloss: 'Träum schön.' },
|
||||||
'Amping.',
|
{ target: 'Amping.', gloss: 'Pass auf dich auf.' },
|
||||||
'Babay.',
|
{ target: 'Babay.', gloss: 'Tschüss.' },
|
||||||
'Maayo ko.',
|
{ target: 'Maayo ko.', gloss: 'Mir geht es gut.' },
|
||||||
'Salamat.',
|
{ target: 'Salamat.', gloss: 'Danke.' },
|
||||||
'Palihug.'
|
{ target: 'Palihug.', gloss: 'Bitte.' }
|
||||||
],
|
],
|
||||||
grammarFocus: [
|
grammarFocus: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,26 @@ const LESSON_DIDACTICS = {
|
|||||||
'Höfliche Reaktionen wie Danke und Bitte passend einsetzen.',
|
'Höfliche Reaktionen wie Danke und Bitte passend einsetzen.',
|
||||||
'Ein kurzes Begrüßungs-Mini-Gespräch laut üben.'
|
'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: [
|
grammarFocus: [
|
||||||
{ title: 'Kurzantworten mit ko', text: 'Mit "ko" sprichst du über dich selbst: "Maayo ko."', example: 'Maayo ko. = Mir geht es gut.' },
|
{ 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.' },
|
{ title: 'Maayong + Tageszeit', text: 'Mit "Maayong" kannst du Grüße für verschiedene Tageszeiten bilden.', example: 'Maayong buntag. / Maayong gabii.' },
|
||||||
|
|||||||
@@ -106,7 +106,13 @@ export default class VocabService {
|
|||||||
`Lektion: ${lesson?.title || 'Unbekannte Lektion'}`,
|
`Lektion: ${lesson?.title || 'Unbekannte Lektion'}`,
|
||||||
lesson?.description ? `Beschreibung: ${lesson.description}` : '',
|
lesson?.description ? `Beschreibung: ${lesson.description}` : '',
|
||||||
learningGoals.length ? `Lernziele: ${learningGoals.join(' | ')}` : '',
|
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
|
speakingPrompts.length
|
||||||
? `Sprechaufträge: ${speakingPrompts.map((item) => item.prompt || item.title || '').filter(Boolean).join(' | ')}`
|
? `Sprechaufträge: ${speakingPrompts.map((item) => item.prompt || item.title || '').filter(Boolean).join(' | ')}`
|
||||||
: '',
|
: '',
|
||||||
@@ -239,14 +245,16 @@ export default class VocabService {
|
|||||||
|
|
||||||
speakingPrompts.forEach((prompt, index) => {
|
speakingPrompts.forEach((prompt, index) => {
|
||||||
const learning = String(prompt?.prompt || prompt?.title || '').trim();
|
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;
|
if (!learning || !reference || learning === reference) return;
|
||||||
vocabMap.set(`${learning}-${reference}`, { learning, reference });
|
vocabMap.set(`${learning}-${reference}`, { learning, reference });
|
||||||
});
|
});
|
||||||
|
|
||||||
practicalTasks.forEach((task, index) => {
|
practicalTasks.forEach((task, index) => {
|
||||||
const learning = String(task?.text || task?.title || '').trim();
|
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;
|
if (!learning || !reference || learning === reference) return;
|
||||||
vocabMap.set(`${learning}-${reference}`, { learning, reference });
|
vocabMap.set(`${learning}-${reference}`, { learning, reference });
|
||||||
});
|
});
|
||||||
@@ -270,6 +278,49 @@ export default class VocabService {
|
|||||||
return [];
|
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']) {
|
_normalizeStructuredList(value, keys = ['title', 'text']) {
|
||||||
if (!value) return [];
|
if (!value) return [];
|
||||||
if (Array.isArray(value)) {
|
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 uniquePatterns = [...new Set(patterns.map((item) => String(item || '').trim()).filter(Boolean))];
|
||||||
|
|
||||||
const learningGoals = this._normalizeStringList(plainLesson.learningGoals);
|
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 grammarFocus = this._normalizeStructuredList(plainLesson.grammarFocus, ['title', 'text', 'example']);
|
||||||
const explicitSpeakingPrompts = this._normalizeStructuredList(plainLesson.speakingPrompts, ['title', 'prompt', 'cue']);
|
const explicitSpeakingPrompts = this._normalizeStructuredList(plainLesson.speakingPrompts, ['title', 'prompt', 'cue']);
|
||||||
const practicalTasks = this._normalizeStructuredList(plainLesson.practicalTasks, ['title', 'text']);
|
const practicalTasks = this._normalizeStructuredList(plainLesson.practicalTasks, ['title', 'text']);
|
||||||
@@ -495,7 +546,9 @@ export default class VocabService {
|
|||||||
'Ein bis zwei Satzmuster aktiv anwenden.',
|
'Ein bis zwei Satzmuster aktiv anwenden.',
|
||||||
'Kurze Sätze oder Mini-Dialoge zum Thema selbst bilden.'
|
'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),
|
grammarFocus: grammarFocus.length > 0 ? grammarFocus : uniqueGrammarExplanations.slice(0, 4),
|
||||||
speakingPrompts: explicitSpeakingPrompts.length > 0 ? explicitSpeakingPrompts : speakingPrompts.slice(0, 4),
|
speakingPrompts: explicitSpeakingPrompts.length > 0 ? explicitSpeakingPrompts : speakingPrompts.slice(0, 4),
|
||||||
practicalTasks: practicalTasks.length > 0
|
practicalTasks: practicalTasks.length > 0
|
||||||
@@ -1775,7 +1828,7 @@ export default class VocabService {
|
|||||||
audioUrl: audioUrl || null,
|
audioUrl: audioUrl || null,
|
||||||
culturalNotes: culturalNotes || null,
|
culturalNotes: culturalNotes || null,
|
||||||
learningGoals: this._normalizeStringList(learningGoals),
|
learningGoals: this._normalizeStringList(learningGoals),
|
||||||
corePatterns: this._normalizeStringList(corePatterns),
|
corePatterns: this._normalizeCorePatternList(corePatterns),
|
||||||
grammarFocus: this._normalizeStructuredList(grammarFocus, ['title', 'text', 'example']),
|
grammarFocus: this._normalizeStructuredList(grammarFocus, ['title', 'text', 'example']),
|
||||||
speakingPrompts: this._normalizeStructuredList(speakingPrompts, ['title', 'prompt', 'cue']),
|
speakingPrompts: this._normalizeStructuredList(speakingPrompts, ['title', 'prompt', 'cue']),
|
||||||
practicalTasks: this._normalizeStructuredList(practicalTasks, ['title', 'text']),
|
practicalTasks: this._normalizeStructuredList(practicalTasks, ['title', 'text']),
|
||||||
@@ -1822,7 +1875,7 @@ export default class VocabService {
|
|||||||
if (audioUrl !== undefined) updates.audioUrl = audioUrl;
|
if (audioUrl !== undefined) updates.audioUrl = audioUrl;
|
||||||
if (culturalNotes !== undefined) updates.culturalNotes = culturalNotes;
|
if (culturalNotes !== undefined) updates.culturalNotes = culturalNotes;
|
||||||
if (learningGoals !== undefined) updates.learningGoals = this._normalizeStringList(learningGoals);
|
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 (grammarFocus !== undefined) updates.grammarFocus = this._normalizeStructuredList(grammarFocus, ['title', 'text', 'example']);
|
||||||
if (speakingPrompts !== undefined) updates.speakingPrompts = this._normalizeStructuredList(speakingPrompts, ['title', 'prompt', 'cue']);
|
if (speakingPrompts !== undefined) updates.speakingPrompts = this._normalizeStructuredList(speakingPrompts, ['title', 'prompt', 'cue']);
|
||||||
if (practicalTasks !== undefined) updates.practicalTasks = this._normalizeStructuredList(practicalTasks, ['title', 'text']);
|
if (practicalTasks !== undefined) updates.practicalTasks = this._normalizeStructuredList(practicalTasks, ['title', 'text']);
|
||||||
|
|||||||
@@ -447,6 +447,14 @@
|
|||||||
"grammarImpulse": "Grammatik-Impuls",
|
"grammarImpulse": "Grammatik-Impuls",
|
||||||
"learningGoals": "Lernziele",
|
"learningGoals": "Lernziele",
|
||||||
"corePatterns": "Kernmuster",
|
"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",
|
"speakingTasks": "Sprechaufträge",
|
||||||
"speakingPrompt": "Sprechauftrag",
|
"speakingPrompt": "Sprechauftrag",
|
||||||
"practicalTasks": "Praxisaufgaben",
|
"practicalTasks": "Praxisaufgaben",
|
||||||
|
|||||||
@@ -447,6 +447,14 @@
|
|||||||
"grammarImpulse": "Grammar Focus",
|
"grammarImpulse": "Grammar Focus",
|
||||||
"learningGoals": "Learning Goals",
|
"learningGoals": "Learning Goals",
|
||||||
"corePatterns": "Core Patterns",
|
"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",
|
"speakingTasks": "Speaking Tasks",
|
||||||
"speakingPrompt": "Speaking Prompt",
|
"speakingPrompt": "Speaking Prompt",
|
||||||
"practicalTasks": "Practical Tasks",
|
"practicalTasks": "Practical Tasks",
|
||||||
|
|||||||
@@ -445,6 +445,14 @@
|
|||||||
"grammarImpulse": "Impulso gramatical",
|
"grammarImpulse": "Impulso gramatical",
|
||||||
"learningGoals": "Objetivos",
|
"learningGoals": "Objetivos",
|
||||||
"corePatterns": "Patrones básicos",
|
"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",
|
"speakingTasks": "Tareas orales",
|
||||||
"speakingPrompt": "Tarea oral",
|
"speakingPrompt": "Tarea oral",
|
||||||
"practicalTasks": "Tareas prácticas",
|
"practicalTasks": "Tareas prácticas",
|
||||||
|
|||||||
@@ -92,11 +92,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<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 class="pattern-list">
|
||||||
<div v-for="(pattern, index) in lessonDidactics.corePatterns" :key="'pattern-' + index" class="pattern-item">
|
<div v-for="(pattern, index) in normalizedCorePatterns" :key="'pattern-' + index" class="pattern-item">
|
||||||
{{ pattern }}
|
<div class="pattern-target">{{ pattern.target }}</div>
|
||||||
|
<div v-if="pattern.gloss" class="pattern-gloss">{{ pattern.gloss }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,6 +217,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Vokabeltrainer -->
|
||||||
<div v-if="importantVocab && importantVocab.length > 0" class="vocab-trainer-section">
|
<div v-if="importantVocab && importantVocab.length > 0" class="vocab-trainer-section">
|
||||||
<h4>{{ $t('socialnetwork.vocab.courses.vocabTrainer') }}</h4>
|
<h4>{{ $t('socialnetwork.vocab.courses.vocabTrainer') }}</h4>
|
||||||
@@ -225,10 +263,13 @@
|
|||||||
<p>{{ exerciseUnlockHint }}</p>
|
<p>{{ exerciseUnlockHint }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!vocabTrainerActive" class="vocab-trainer-start">
|
<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>
|
<template v-if="canStartVocabTrainerPrep">
|
||||||
<button @click="startVocabTrainer" class="btn-start-trainer">
|
<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>
|
||||||
{{ hasPreviousVocab ? 'Lektion starten' : $t('socialnetwork.vocab.courses.startVocabTrainer') }}
|
<button @click="startVocabTrainer" class="btn-start-trainer">
|
||||||
</button>
|
{{ 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>
|
||||||
<div v-else class="vocab-trainer-active">
|
<div v-else class="vocab-trainer-active">
|
||||||
<div class="vocab-trainer-stats">
|
<div class="vocab-trainer-stats">
|
||||||
@@ -313,19 +354,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Hinweis wenn keine Vokabeln vorhanden -->
|
||||||
<div v-else-if="lesson && (!importantVocab || importantVocab.length === 0)" class="no-vocab-info">
|
<div v-else-if="lesson && (!importantVocab || importantVocab.length === 0)" class="no-vocab-info">
|
||||||
<p>{{ $t('socialnetwork.vocab.courses.noVocabInfo') }}</p>
|
<p>{{ $t('socialnetwork.vocab.courses.noVocabInfo') }}</p>
|
||||||
@@ -734,6 +762,8 @@ export default {
|
|||||||
vocabTrainerCurrentAttempts: 0,
|
vocabTrainerCurrentAttempts: 0,
|
||||||
vocabTrainerReviewAttempts: 0,
|
vocabTrainerReviewAttempts: 0,
|
||||||
exercisePreparationCompleted: false,
|
exercisePreparationCompleted: false,
|
||||||
|
/** 0 = noch kein Durchgang, 1 = erste Durchsicht, 2 = zweite Durchsicht — dann Vokabeltrainer */
|
||||||
|
lessonPrepStage: 0,
|
||||||
currentVocabQuestion: null,
|
currentVocabQuestion: null,
|
||||||
vocabTrainerAnswer: '',
|
vocabTrainerAnswer: '',
|
||||||
vocabTrainerSelectedChoice: null,
|
vocabTrainerSelectedChoice: null,
|
||||||
@@ -897,6 +927,21 @@ export default {
|
|||||||
practicalTasks: []
|
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() {
|
lessonPedagogy() {
|
||||||
return this.lesson?.pedagogy || {
|
return this.lesson?.pedagogy || {
|
||||||
didacticMode: null,
|
didacticMode: null,
|
||||||
@@ -943,6 +988,29 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
openExercisesTab() {
|
||||||
if (!this.canAccessExercises) {
|
if (!this.canAccessExercises) {
|
||||||
this.activeTab = 'learn';
|
this.activeTab = 'learn';
|
||||||
@@ -1133,6 +1201,7 @@ export default {
|
|||||||
this.assistantInput = '';
|
this.assistantInput = '';
|
||||||
this.assistantError = '';
|
this.assistantError = '';
|
||||||
this.exercisePreparationCompleted = false;
|
this.exercisePreparationCompleted = false;
|
||||||
|
this.lessonPrepStage = 0;
|
||||||
this.vocabTrainerActive = false;
|
this.vocabTrainerActive = false;
|
||||||
this.vocabTrainerPool = [];
|
this.vocabTrainerPool = [];
|
||||||
this.vocabTrainerMixedPool = [];
|
this.vocabTrainerMixedPool = [];
|
||||||
@@ -1222,10 +1291,11 @@ export default {
|
|||||||
buildAssistantPrompt(preset) {
|
buildAssistantPrompt(preset) {
|
||||||
const lessonTitle = this.lesson?.title || this.$t('socialnetwork.vocab.courses.thisLesson');
|
const lessonTitle = this.lesson?.title || this.$t('socialnetwork.vocab.courses.thisLesson');
|
||||||
const firstPattern = this.lessonDidactics.corePatterns?.[0];
|
const firstPattern = this.lessonDidactics.corePatterns?.[0];
|
||||||
|
const firstPatternStr = firstPattern ? this.corePatternToDisplayString(firstPattern) : '';
|
||||||
const firstGrammar = this.lessonDidactics.grammarFocus?.[0]?.text;
|
const firstGrammar = this.lessonDidactics.grammarFocus?.[0]?.text;
|
||||||
|
|
||||||
if (preset === 'explain') {
|
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') {
|
if (preset === 'correct') {
|
||||||
return this.$t('socialnetwork.vocab.courses.languageAssistantPresetCorrectStart', { lesson: lessonTitle });
|
return this.$t('socialnetwork.vocab.courses.languageAssistantPresetCorrectStart', { lesson: lessonTitle });
|
||||||
@@ -1669,6 +1739,9 @@ export default {
|
|||||||
debugLog('[VocabLessonView] Keine Vokabeln vorhanden');
|
debugLog('[VocabLessonView] Keine Vokabeln vorhanden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!this.canStartVocabTrainerPrep) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
debugLog('[VocabLessonView] Vokabeln gefunden:', this.importantVocab.length);
|
debugLog('[VocabLessonView] Vokabeln gefunden:', this.importantVocab.length);
|
||||||
debugLog('[VocabLessonView] Alte Vokabeln:', this.previousVocab?.length || 0);
|
debugLog('[VocabLessonView] Alte Vokabeln:', this.previousVocab?.length || 0);
|
||||||
this.vocabTrainerActive = true;
|
this.vocabTrainerActive = true;
|
||||||
@@ -2329,6 +2402,42 @@ export default {
|
|||||||
border-radius: 8px;
|
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,
|
.grammar-example,
|
||||||
.speaking-cue,
|
.speaking-cue,
|
||||||
.pattern-drill-hint {
|
.pattern-drill-hint {
|
||||||
|
|||||||
Reference in New Issue
Block a user