feat(vocab): enhance vocabulary exercises and localization support
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
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:
@@ -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: [
|
||||||
|
|||||||
@@ -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++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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).",
|
||||||
|
|||||||
@@ -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).",
|
||||||
|
|||||||
@@ -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).",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user