feat(vocab): enhance vocabulary exercises and localization support
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s

- Updated core patterns in BISAYA_PHASE5_DIDACTICS to include gloss translations for better understanding.
- Refactored vocabulary exercise generation in update-food-care-exercises.js to improve randomization and user engagement.
- Added new exercise types and improved question structures for vocabulary lessons, enhancing the learning experience.
- Enhanced localization files for German, English, and Spanish to support new exercise features and improve user guidance.
- Updated VocabLessonView to incorporate sequential navigation for exercises, providing a more structured learning flow.
This commit is contained in:
Torsten Schulz (local)
2026-04-07 09:09:43 +02:00
parent d192bcae2d
commit e17f0cdce0
6 changed files with 348 additions and 38 deletions

View File

@@ -52,7 +52,12 @@ export const BISAYA_PHASE5_DIDACTICS = {
'Nahe Bedeutungen in stabilere Antworten überführen.', 'Nahe Bedeutungen in stabilere Antworten überführen.',
'Häufige Stolperstellen transparent machen.' 'Häufige Stolperstellen transparent machen.'
], ],
corePatterns: ['Palangga taka.', 'Mingaw ko nimo.', 'Magpahuway sa.', 'Andam na ka?'] corePatterns: [
{ target: 'Palangga taka.', gloss: 'Ich hab dich lieb.' },
{ target: 'Mingaw ko nimo.', gloss: 'Ich vermisse dich.' },
{ target: 'Magpahuway sa.', gloss: 'Ruh dich aus.' },
{ target: 'Andam na ka?', gloss: 'Bist du fertig?' }
]
}, },
'Freies Erzählen - Mein Alltag': { 'Freies Erzählen - Mein Alltag': {
learningGoals: [ learningGoals: [

View File

@@ -652,23 +652,43 @@ async function updateFoodCareExercises() {
totalExercisesCreated++; totalExercisesCreated++;
} }
} else if (lesson.title === 'Essen & Trinken') { } else if (lesson.title === 'Essen & Trinken') {
// Vokabular-Übungen für "Essen & Trinken" // Vokabular: keine strikte Paarung Deutsch→Bisaya direkt gefolgt von Rückrichtung (vermeidet Verräter-Effekt).
for (const vocab of conversations) { const n = conversations.length;
// Multiple Choice: Muttersprache -> Bisaya const offset = Math.max(1, Math.floor(n / 2));
const revIdx = (i) => (i + offset) % Math.max(n, 1);
const pickOtherBisaya = (correct, start) => {
for (let t = 1; t <= n; t++) {
const o = conversations[(start + t) % n]?.bisaya;
if (o && o !== correct) return o;
}
return 'Salamat';
};
const pickOtherNative = (correct, start) => {
for (let t = 1; t <= n; t++) {
const o = conversations[(start + t) % n]?.native;
if (o && o !== correct) return o;
}
return 'Danke';
};
for (let i = 0; i < n; i++) {
const vocab = conversations[i];
await VocabGrammarExercise.create({ await VocabGrammarExercise.create({
lessonId: lesson.id, lessonId: lesson.id,
exerciseTypeId: 2, // multiple_choice exerciseTypeId: 2,
exerciseNumber: exerciseNumber++, exerciseNumber: exerciseNumber++,
title: `Wie sagt man "${vocab.native}"?`, title: `Wie sagt man "${vocab.native}"?`,
instruction: 'Wähle die richtige Übersetzung.', instruction: 'Wähle die richtige Übersetzung.',
questionData: JSON.stringify({ questionData: JSON.stringify({
type: 'multiple_choice', type: 'multiple_choice',
answerLanguage: 'target',
question: `Wie sagt man "${vocab.native}" auf Bisaya?`, question: `Wie sagt man "${vocab.native}" auf Bisaya?`,
options: [ options: [
vocab.bisaya, vocab.bisaya,
conversations[(exerciseNumber - 2 + 1) % conversations.length]?.bisaya || 'Salamat', pickOtherBisaya(vocab.bisaya, i),
conversations[(exerciseNumber - 2 + 2) % conversations.length]?.bisaya || 'Maayo', pickOtherBisaya(vocab.bisaya, i + 3),
conversations[(exerciseNumber - 2 + 3) % conversations.length]?.bisaya || 'Palihug' pickOtherBisaya(vocab.bisaya, i + 6)
] ]
}), }),
answerData: JSON.stringify({ answerData: JSON.stringify({
@@ -679,22 +699,25 @@ async function updateFoodCareExercises() {
createdByUserId: course.owner_user_id || systemUser.id createdByUserId: course.owner_user_id || systemUser.id
}); });
totalExercisesCreated++; totalExercisesCreated++;
}
// Multiple Choice: Bisaya -> Muttersprache for (let i = 0; i < n; i++) {
const vocab = conversations[revIdx(i)];
await VocabGrammarExercise.create({ await VocabGrammarExercise.create({
lessonId: lesson.id, lessonId: lesson.id,
exerciseTypeId: 2, // multiple_choice exerciseTypeId: 2,
exerciseNumber: exerciseNumber++, exerciseNumber: exerciseNumber++,
title: `Was bedeutet "${vocab.bisaya}"?`, title: `Was bedeutet "${vocab.bisaya}"?`,
instruction: 'Wähle die richtige Übersetzung.', instruction: 'Wähle die richtige Übersetzung.',
questionData: JSON.stringify({ questionData: JSON.stringify({
type: 'multiple_choice', type: 'multiple_choice',
answerLanguage: 'native',
question: `Was bedeutet "${vocab.bisaya}"?`, question: `Was bedeutet "${vocab.bisaya}"?`,
options: [ options: [
vocab.native, vocab.native,
conversations[(exerciseNumber - 3 + 1) % conversations.length]?.native || 'Danke', pickOtherNative(vocab.native, i),
conversations[(exerciseNumber - 3 + 2) % conversations.length]?.native || 'Bitte', pickOtherNative(vocab.native, i + 4),
conversations[(exerciseNumber - 3 + 3) % conversations.length]?.native || 'Gut' pickOtherNative(vocab.native, i + 8)
] ]
}), }),
answerData: JSON.stringify({ answerData: JSON.stringify({
@@ -706,6 +729,31 @@ async function updateFoodCareExercises() {
}); });
totalExercisesCreated++; totalExercisesCreated++;
} }
for (let i = 0; i < n; i++) {
const vocab = conversations[i];
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId: 4,
exerciseNumber: exerciseNumber++,
title: `Tippe auf Bisaya: "${vocab.native}"`,
instruction: 'Übersetze das Wort (Schreibtest).',
questionData: JSON.stringify({
type: 'transformation',
text: vocab.native,
sourceLanguage: nativeLangName,
targetLanguage: 'Bisaya'
}),
answerData: JSON.stringify({
type: 'transformation',
correct: vocab.bisaya,
alternatives: []
}),
explanation: vocab.explanation,
createdByUserId: course.owner_user_id || systemUser.id
});
totalExercisesCreated++;
}
} }
} }

View File

@@ -517,6 +517,12 @@
"exerciseProgressLabel": "Fortschritt", "exerciseProgressLabel": "Fortschritt",
"exerciseTargetLabel": "Benötigt", "exerciseTargetLabel": "Benötigt",
"exerciseCardLabel": "Aufgabe {number}", "exerciseCardLabel": "Aufgabe {number}",
"exerciseSequentialProgress": "Frage {current} von {total}",
"exerciseSequentialBack": "Zurück",
"exerciseSequentialNext": "Weiter",
"exerciseWrongTitle": "Noch nicht richtig",
"exerciseReinforcementGoPractice": "Zum Üben wechseln",
"exerciseReinforcementStay": "Bei der Prüfung bleiben",
"exerciseStatusOpen": "Offen", "exerciseStatusOpen": "Offen",
"exerciseStatusCorrect": "Erledigt", "exerciseStatusCorrect": "Erledigt",
"exerciseStatusRetry": "Nochmal prüfen", "exerciseStatusRetry": "Nochmal prüfen",
@@ -548,6 +554,8 @@
"vocabPrepTitle": "Vorbereitung vor dem Vokabeltrainer", "vocabPrepTitle": "Vorbereitung vor dem Vokabeltrainer",
"vocabPrepStep1": "Lies Kernmuster und Wortliste (Deutsch ↔ Zielsprache) einmal in Ruhe durch.", "vocabPrepStep1": "Lies Kernmuster und Wortliste (Deutsch ↔ Zielsprache) einmal in Ruhe durch.",
"vocabPrepProgress": "Durchgang {pass}: Begriff {current} von {total}", "vocabPrepProgress": "Durchgang {pass}: Begriff {current} von {total}",
"vocabPrepTargetLabel": "Zielsprache",
"vocabPrepGlossLabel": "Deutsch",
"vocabPrepNextItem": "Nächster Begriff", "vocabPrepNextItem": "Nächster Begriff",
"vocabPrepConfirm1": "Erste Durchsicht erledigt", "vocabPrepConfirm1": "Erste Durchsicht erledigt",
"vocabPrepStep2": "Gehe die gleichen Begriffe noch einmal durch (aktive Wiederholung, ohne zu üben).", "vocabPrepStep2": "Gehe die gleichen Begriffe noch einmal durch (aktive Wiederholung, ohne zu üben).",

View File

@@ -517,6 +517,12 @@
"exerciseProgressLabel": "Progress", "exerciseProgressLabel": "Progress",
"exerciseTargetLabel": "Required", "exerciseTargetLabel": "Required",
"exerciseCardLabel": "Task {number}", "exerciseCardLabel": "Task {number}",
"exerciseSequentialProgress": "Question {current} of {total}",
"exerciseSequentialBack": "Back",
"exerciseSequentialNext": "Next",
"exerciseWrongTitle": "Not quite right",
"exerciseReinforcementGoPractice": "Go to practice",
"exerciseReinforcementStay": "Stay on the test",
"exerciseStatusOpen": "Open", "exerciseStatusOpen": "Open",
"exerciseStatusCorrect": "Done", "exerciseStatusCorrect": "Done",
"exerciseStatusRetry": "Try again", "exerciseStatusRetry": "Try again",
@@ -548,6 +554,8 @@
"vocabPrepTitle": "Preparation before the vocabulary trainer", "vocabPrepTitle": "Preparation before the vocabulary trainer",
"vocabPrepStep1": "Read through core patterns and the word list (native language ↔ target language) once.", "vocabPrepStep1": "Read through core patterns and the word list (native language ↔ target language) once.",
"vocabPrepProgress": "Pass {pass}: item {current} of {total}", "vocabPrepProgress": "Pass {pass}: item {current} of {total}",
"vocabPrepTargetLabel": "Target language",
"vocabPrepGlossLabel": "Meaning",
"vocabPrepNextItem": "Next item", "vocabPrepNextItem": "Next item",
"vocabPrepConfirm1": "First pass done", "vocabPrepConfirm1": "First pass done",
"vocabPrepStep2": "Go through the same items again (active review, not testing yet).", "vocabPrepStep2": "Go through the same items again (active review, not testing yet).",

View File

@@ -515,6 +515,12 @@
"exerciseProgressLabel": "Progreso", "exerciseProgressLabel": "Progreso",
"exerciseTargetLabel": "Necesario", "exerciseTargetLabel": "Necesario",
"exerciseCardLabel": "Tarea {number}", "exerciseCardLabel": "Tarea {number}",
"exerciseSequentialProgress": "Pregunta {current} de {total}",
"exerciseSequentialBack": "Atrás",
"exerciseSequentialNext": "Siguiente",
"exerciseWrongTitle": "Aún no es correcto",
"exerciseReinforcementGoPractice": "Ir a practicar",
"exerciseReinforcementStay": "Seguir en la prueba",
"exerciseStatusOpen": "Pendiente", "exerciseStatusOpen": "Pendiente",
"exerciseStatusCorrect": "Hecha", "exerciseStatusCorrect": "Hecha",
"exerciseStatusRetry": "Revisar otra vez", "exerciseStatusRetry": "Revisar otra vez",
@@ -546,6 +552,8 @@
"vocabPrepTitle": "Preparación antes del entrenador de vocabulario", "vocabPrepTitle": "Preparación antes del entrenador de vocabulario",
"vocabPrepStep1": "Lee una vez los patrones clave y la lista de palabras (idioma nativo ↔ lengua meta).", "vocabPrepStep1": "Lee una vez los patrones clave y la lista de palabras (idioma nativo ↔ lengua meta).",
"vocabPrepProgress": "Pasada {pass}: término {current} de {total}", "vocabPrepProgress": "Pasada {pass}: término {current} de {total}",
"vocabPrepTargetLabel": "Lengua meta",
"vocabPrepGlossLabel": "Significado",
"vocabPrepNextItem": "Siguiente término", "vocabPrepNextItem": "Siguiente término",
"vocabPrepConfirm1": "Primera lectura hecha", "vocabPrepConfirm1": "Primera lectura hecha",
"vocabPrepStep2": "Repasa los mismos elementos otra vez (repaso activo, aún sin practicar).", "vocabPrepStep2": "Repasa los mismos elementos otra vez (repaso activo, aún sin practicar).",

View File

@@ -125,8 +125,14 @@
: $t('socialnetwork.vocab.courses.vocabPrepStep2') }} : $t('socialnetwork.vocab.courses.vocabPrepStep2') }}
</p> </p>
<div class="vocab-prep-card"> <div class="vocab-prep-card">
<div class="vocab-prep-card__target">{{ currentPrepItem.target }}</div> <div class="vocab-prep-card__row">
<div v-if="currentPrepItem.gloss" class="vocab-prep-card__gloss">{{ currentPrepItem.gloss }}</div> <span class="vocab-prep-card__label">{{ $t('socialnetwork.vocab.courses.vocabPrepTargetLabel') }}</span>
<div class="vocab-prep-card__target">{{ currentPrepItem.target }}</div>
</div>
<div class="vocab-prep-card__row">
<span class="vocab-prep-card__label">{{ $t('socialnetwork.vocab.courses.vocabPrepGlossLabel') }}</span>
<div class="vocab-prep-card__gloss">{{ currentPrepItem.gloss || '—' }}</div>
</div>
</div> </div>
<button type="button" class="btn-prep-pass" @click="advancePrepPass"> <button type="button" class="btn-prep-pass" @click="advancePrepPass">
{{ isLastPrepItemInPass {{ isLastPrepItemInPass
@@ -487,9 +493,38 @@
</article> </article>
</div> </div>
</div> </div>
<div
v-if="sequentialPanelActive && scrambledChapterExamExercises.length > 0"
class="exercise-sequential-nav surface-card"
>
<p class="exercise-sequential-nav__progress">
{{ $t('socialnetwork.vocab.courses.exerciseSequentialProgress', {
current: exerciseSequentialIndex + 1,
total: scrambledChapterExamExercises.length
}) }}
</p>
<div class="exercise-sequential-nav__buttons">
<button
type="button"
class="btn-seq"
:disabled="exerciseSequentialIndex <= 0"
@click="stepExercisePanel(-1)"
>
{{ $t('socialnetwork.vocab.courses.exerciseSequentialBack') }}
</button>
<button
type="button"
class="btn-seq btn-seq--primary"
:disabled="!canStepExercisePanelForward"
@click="stepExercisePanel(1)"
>
{{ $t('socialnetwork.vocab.courses.exerciseSequentialNext') }}
</button>
</div>
</div>
<div class="exercise-flow-list"> <div class="exercise-flow-list">
<div <div
v-for="(exercise, index) in effectiveExercises" v-for="(exercise, index) in exercisesPanelExercises"
:key="exercise.id" :key="exercise.id"
class="exercise-item surface-card" class="exercise-item surface-card"
:class="{ :class="{
@@ -500,7 +535,7 @@
> >
<div class="exercise-item__header"> <div class="exercise-item__header">
<div> <div>
<span class="exercise-item__index">{{ $t('socialnetwork.vocab.courses.exerciseCardLabel', { number: index + 1 }) }}</span> <span class="exercise-item__index">{{ $t('socialnetwork.vocab.courses.exerciseCardLabel', { number: exercisePanelDisplayNumber(index) }) }}</span>
<h4>{{ exercise.title }}</h4> <h4>{{ exercise.title }}</h4>
</div> </div>
<span <span
@@ -865,6 +900,31 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Nach falscher Kapitel-Antwort: zuerst Lösung, dann optional zum Üben -->
<div v-if="showExerciseReinforcementDialog" class="dialog-overlay" @click.self="closeExerciseReinforcementDialog">
<div class="dialog" style="width: 440px; height: auto;">
<div class="dialog-header">
<span class="dialog-title">{{ $t('socialnetwork.vocab.courses.exerciseWrongTitle') }}</span>
<span class="dialog-close" @click="closeExerciseReinforcementDialog"></span>
</div>
<div class="dialog-body">
<p v-if="exerciseReinforcementCorrectAnswer" class="exercise-reinforcement-correct">
<strong>{{ $t('socialnetwork.vocab.courses.correctAnswer') }}:</strong>
{{ exerciseReinforcementCorrectAnswer }}
</p>
<p>{{ exerciseReinforcementMessage }}</p>
</div>
<div class="dialog-footer dialog-footer--stack">
<button type="button" class="dialog-button dialog-button--primary" @click="confirmExerciseReinforcement">
{{ $t('socialnetwork.vocab.courses.exerciseReinforcementGoPractice') }}
</button>
<button type="button" class="dialog-button" @click="closeExerciseReinforcementDialog">
{{ $t('socialnetwork.vocab.courses.exerciseReinforcementStay') }}
</button>
</div>
</div>
</div>
</template> </template>
<script> <script>
@@ -945,6 +1005,12 @@ export default {
showCompletionDialog: false, showCompletionDialog: false,
showErrorDialog: false, showErrorDialog: false,
errorMessage: '', errorMessage: '',
showExerciseReinforcementDialog: false,
exerciseReinforcementPrepMode: false,
exerciseReinforcementCorrectAnswer: '',
exerciseReinforcementMessage: '',
/** Index in scrambledChapterExamExercises bei Ein-Frage-Ansicht */
exerciseSequentialIndex: 0,
/** Aus vorherigen Lektionen (MC-Optionen nach Fragentyp Ziel-/Muttersprache) */ /** Aus vorherigen Lektionen (MC-Optionen nach Fragentyp Ziel-/Muttersprache) */
distractorPool: { target: [], native: [] }, distractorPool: { target: [], native: [] },
/** { [exerciseId]: { options: string[], useTextAnswer: boolean } } */ /** { [exerciseId]: { options: string[], useTextAnswer: boolean } } */
@@ -1058,6 +1124,32 @@ export default {
exerciseTargetScore() { exerciseTargetScore() {
return Number(this.lesson?.targetScorePercent) || 80; return Number(this.lesson?.targetScorePercent) || 80;
}, },
/** Kapitel-Prüfung: eine Frage pro Ansicht (Essen & Trinken: deterministisch gemischt). */
scrambledChapterExamExercises() {
const raw = this.effectiveExercises;
if (!raw.length) return [];
if ((this.lesson?.title || '').trim() === 'Essen & Trinken') {
return this._deterministicShuffle(raw.slice(), Number(this.lessonId) || 1);
}
return raw;
},
sequentialPanelActive() {
return (this.scrambledChapterExamExercises?.length || 0) > 1;
},
exercisesPanelExercises() {
const list = this.scrambledChapterExamExercises;
if (!list.length) return [];
if (!this.sequentialPanelActive) return list;
const idx = Math.max(0, Math.min(this.exerciseSequentialIndex, list.length - 1));
return [list[idx]];
},
canStepExercisePanelForward() {
const list = this.scrambledChapterExamExercises;
if (!list.length) return false;
const ex = list[this.exerciseSequentialIndex];
if (!ex) return false;
return Boolean(this.exerciseResults[ex.id]?.correct) && this.exerciseSequentialIndex < list.length - 1;
},
exerciseRetryUnlockAttempts() { exerciseRetryUnlockAttempts() {
return Math.min(8, Math.max(2, Math.ceil(this.trainerNewFocusTarget * 0.25))); return Math.min(8, Math.max(2, Math.ceil(this.trainerNewFocusTarget * 0.25)));
}, },
@@ -1324,7 +1416,8 @@ export default {
vocabTrainerCurrentAttempts: this.vocabTrainerCurrentAttempts, vocabTrainerCurrentAttempts: this.vocabTrainerCurrentAttempts,
vocabTrainerReviewAttempts: this.vocabTrainerReviewAttempts, vocabTrainerReviewAttempts: this.vocabTrainerReviewAttempts,
exerciseRetryPending: this.exerciseRetryPending, exerciseRetryPending: this.exerciseRetryPending,
exerciseRetryPendingSinceAttempts: this.exerciseRetryPendingSinceAttempts exerciseRetryPendingSinceAttempts: this.exerciseRetryPendingSinceAttempts,
exerciseSequentialIndex: this.exerciseSequentialIndex
}; };
} }
}, },
@@ -1592,6 +1685,8 @@ export default {
this.vocabTrainerReviewAttempts = Math.max(0, Number(parsedState.vocabTrainerReviewAttempts) || 0); this.vocabTrainerReviewAttempts = Math.max(0, Number(parsedState.vocabTrainerReviewAttempts) || 0);
this.exerciseRetryPending = Boolean(parsedState.exerciseRetryPending); this.exerciseRetryPending = Boolean(parsedState.exerciseRetryPending);
this.exerciseRetryPendingSinceAttempts = Math.max(0, Number(parsedState.exerciseRetryPendingSinceAttempts) || 0); this.exerciseRetryPendingSinceAttempts = Math.max(0, Number(parsedState.exerciseRetryPendingSinceAttempts) || 0);
const maxIdx = Math.max(0, this.scrambledChapterExamExercises.length - 1);
this.exerciseSequentialIndex = Math.max(0, Math.min(maxIdx, Number(parsedState.exerciseSequentialIndex) || 0));
this.vocabTrainerMixedPool = this._buildMixedPool(); this.vocabTrainerMixedPool = this._buildMixedPool();
const knownRepeatKeys = new Set([...this.trainableLessonVocab, ...this.vocabTrainerMixedPool].map((entry) => this.getVocabKey(entry))); const knownRepeatKeys = new Set([...this.trainableLessonVocab, ...this.vocabTrainerMixedPool].map((entry) => this.getVocabKey(entry)));
this.vocabTrainerRepeatQueue = this.normalizeRepeatQueue(parsedState.vocabTrainerRepeatQueue) this.vocabTrainerRepeatQueue = this.normalizeRepeatQueue(parsedState.vocabTrainerRepeatQueue)
@@ -1661,6 +1756,59 @@ export default {
if (!n) return ''; if (!n) return '';
return n.gloss ? `${n.target} (${n.gloss})` : n.target; return n.gloss ? `${n.target} (${n.gloss})` : n.target;
}, },
_deterministicShuffle(arr, seed) {
const out = arr.slice();
let s = Number(seed) || 1;
const rnd = () => {
s = (s * 1103515245 + 12345) & 0x7fffffff;
return s / 0x7fffffff;
};
for (let i = out.length - 1; i > 0; i--) {
const j = Math.floor(rnd() * (i + 1));
[out[i], out[j]] = [out[j], out[i]];
}
return out;
},
exercisePanelDisplayNumber(panelIndex) {
if (this.sequentialPanelActive) {
return this.exerciseSequentialIndex + 1;
}
return panelIndex + 1;
},
stepExercisePanel(delta) {
const list = this.scrambledChapterExamExercises;
if (!list.length) return;
const next = this.exerciseSequentialIndex + delta;
if (next < 0 || next >= list.length) return;
this.exerciseSequentialIndex = next;
this.persistLessonState();
},
confirmExerciseReinforcement() {
this.showExerciseReinforcementDialog = false;
if (this.exerciseReinforcementPrepMode) {
this.lessonPrepStage = 0;
this.lessonPrepIndex = 0;
} else {
this.exerciseRetryPending = true;
this.exerciseRetryPendingSinceAttempts = this.vocabTrainerTotalAttempts;
}
this.exerciseReinforcementPrepMode = false;
this.exerciseReinforcementCorrectAnswer = '';
this.exerciseReinforcementMessage = '';
this.activeTab = 'learn';
this.$nextTick(() => {
const scrollEl = document.querySelector('.app-content__scroll.contentscroll');
if (scrollEl) scrollEl.scrollTop = 0;
else window.scrollTo(0, 0);
});
this.persistLessonState();
},
closeExerciseReinforcementDialog() {
this.showExerciseReinforcementDialog = false;
this.exerciseReinforcementPrepMode = false;
this.exerciseReinforcementCorrectAnswer = '';
this.exerciseReinforcementMessage = '';
},
advancePrepPass() { advancePrepPass() {
if (this.lessonPrepStage >= 2 || this.prepItems.length === 0) { if (this.lessonPrepStage >= 2 || this.prepItems.length === 0) {
return; return;
@@ -1776,8 +1924,10 @@ export default {
const question = qData.question || qData.text || ''; const question = qData.question || qData.text || '';
debugLog(`[importantVocab] Frage:`, question); debugLog(`[importantVocab] Frage:`, question);
// Pattern 1: "Wie sagt man 'X' auf Bisaya?" -> X ist Muttersprache (z.B. "Großmutter"), correctAnswer ist Bisaya (z.B. "Lola") // Pattern 1: Muttersprache → Zielsprache (MC: richtige Option = Zielsprache)
let match = question.match(/Wie sagt man ['"]([^'"]+)['"]/i); let match = question.match(/Wie sagt man ['"]([^'"]+)['"]/i);
if (!match) match = question.match(/Wie heißt ['"]([^'"]+)['"]/i);
if (!match) match = question.match(/How do you say ['"]([^'"]+)['"]/i);
if (match) { if (match) {
const nativeWord = match[1]; // Das Wort in der Muttersprache const nativeWord = match[1]; // Das Wort in der Muttersprache
// Nur hinzufügen, wenn Muttersprache und Bisaya unterschiedlich sind (verhindert "ko" -> "ko") // Nur hinzufügen, wenn Muttersprache und Bisaya unterschiedlich sind (verhindert "ko" -> "ko")
@@ -1789,8 +1939,9 @@ export default {
debugLog(`[importantVocab] Pattern 1 übersprungen - Muttersprache und Bisaya sind gleich:`, nativeWord, correctAnswer); debugLog(`[importantVocab] Pattern 1 übersprungen - Muttersprache und Bisaya sind gleich:`, nativeWord, correctAnswer);
} }
} else { } else {
// Pattern 2: "Was bedeutet 'X'?" -> X ist Bisaya, correctAnswer ist Muttersprache // Pattern 2: Zielsprache im Satz → richtige Option = Muttersprache
match = question.match(/Was bedeutet ['"]([^'"]+)['"]/i); match = question.match(/Was bedeutet ['"]([^'"]+)['"]/i);
if (!match) match = question.match(/Was heißt ['"]([^'"]+)['"]/i);
if (match) { if (match) {
const bisayaWord = match[1]; const bisayaWord = match[1];
// Nur hinzufügen, wenn Bisaya und Muttersprache unterschiedlich sind (verhindert "ko" -> "ko") // Nur hinzufügen, wenn Bisaya und Muttersprache unterschiedlich sind (verhindert "ko" -> "ko")
@@ -1876,6 +2027,7 @@ export default {
this.assistantError = ''; this.assistantError = '';
this.exerciseRetryPending = false; this.exerciseRetryPending = false;
this.exerciseRetryPendingSinceAttempts = 0; this.exerciseRetryPendingSinceAttempts = 0;
this.exerciseSequentialIndex = 0;
this.exercisePreparationCompleted = false; this.exercisePreparationCompleted = false;
this.lessonPrepStage = 0; this.lessonPrepStage = 0;
this.lessonPrepIndex = 0; this.lessonPrepIndex = 0;
@@ -2319,32 +2471,33 @@ export default {
this.exerciseResults[exerciseId] = res.data; this.exerciseResults[exerciseId] = res.data;
if (!res.data?.correct) { if (!res.data?.correct) {
const correctText = res.data?.correctAnswer
? String(res.data.correctAnswer)
: '';
if (this.trainableLessonVocab.length === 0 && this.prepItems.length > 0) { if (this.trainableLessonVocab.length === 0 && this.prepItems.length > 0) {
this.lessonPrepStage = 0; this.exerciseReinforcementPrepMode = true;
this.lessonPrepIndex = 0; this.exerciseReinforcementCorrectAnswer = correctText;
this.errorMessage = this.$t('socialnetwork.vocab.courses.exercisePrepReinforcementHint'); this.exerciseReinforcementMessage = this.$t('socialnetwork.vocab.courses.exercisePrepReinforcementHint');
} else { } else {
this.exerciseRetryPending = true; this.exerciseReinforcementPrepMode = false;
this.exerciseRetryPendingSinceAttempts = this.vocabTrainerTotalAttempts; this.exerciseReinforcementCorrectAnswer = correctText;
this.errorMessage = this.$t('socialnetwork.vocab.courses.exerciseReinforcementHint', { this.exerciseReinforcementMessage = this.$t('socialnetwork.vocab.courses.exerciseReinforcementHint', {
count: this.exerciseRetryUnlockAttempts count: this.exerciseRetryUnlockAttempts
}); });
} }
this.activeTab = 'learn'; this.showExerciseReinforcementDialog = true;
this.showErrorDialog = true;
this.$nextTick(() => {
const scrollEl = document.querySelector('.app-content__scroll.contentscroll');
if (scrollEl) {
scrollEl.scrollTop = 0;
} else {
window.scrollTo(0, 0);
}
});
return; return;
} }
// Prüfe ob alle Übungen bestanden sind (mit Verzögerung, um mehrfache Aufrufe zu vermeiden) // Nächste Frage im Einzel-Panel / Abschluss prüfen
this.$nextTick(() => { this.$nextTick(() => {
if (this.sequentialPanelActive) {
const list = this.scrambledChapterExamExercises;
const idx = list.findIndex((e) => e.id === exerciseId);
if (idx >= 0 && idx < list.length - 1) {
this.exerciseSequentialIndex = idx + 1;
}
}
this.checkLessonCompletion(); this.checkLessonCompletion();
}); });
} catch (e) { } catch (e) {
@@ -3435,6 +3588,26 @@ export default {
border-radius: 10px; border-radius: 10px;
} }
.vocab-prep-card__row {
display: grid;
grid-template-columns: minmax(0, 110px) 1fr;
gap: 10px 14px;
align-items: baseline;
margin-top: 10px;
}
.vocab-prep-card__row:first-of-type {
margin-top: 0;
}
.vocab-prep-card__label {
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #8a7658;
}
.vocab-prep-card__target { .vocab-prep-card__target {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 700; font-weight: 700;
@@ -3442,7 +3615,6 @@ export default {
} }
.vocab-prep-card__gloss { .vocab-prep-card__gloss {
margin-top: 8px;
font-size: 1rem; font-size: 1rem;
color: #6a5a44; color: #6a5a44;
} }
@@ -3610,6 +3782,67 @@ export default {
margin-top: 30px; margin-top: 30px;
} }
.exercise-sequential-nav {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 16px;
margin-bottom: 14px;
border: 1px solid rgba(80, 118, 178, 0.14);
background: rgba(255, 255, 255, 0.85);
}
.exercise-sequential-nav__progress {
margin: 0;
font-weight: 600;
color: #2a3f5f;
}
.exercise-sequential-nav__buttons {
display: flex;
gap: 10px;
}
.btn-seq {
padding: 8px 14px;
border-radius: 8px;
border: 1px solid rgba(58, 117, 196, 0.35);
background: #fff;
color: #27528f;
font-weight: 600;
cursor: pointer;
}
.btn-seq:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn-seq--primary {
background: rgba(58, 117, 196, 0.12);
border-color: rgba(58, 117, 196, 0.45);
}
.exercise-reinforcement-correct {
padding: 10px 12px;
margin: 0 0 12px;
border-radius: 8px;
background: rgba(45, 106, 62, 0.08);
color: #1f3d2a;
}
.dialog-footer--stack {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.dialog-button--primary {
font-weight: 700;
}
.exercise-flow-header { .exercise-flow-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;