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:
@@ -125,8 +125,14 @@
|
||||
: $t('socialnetwork.vocab.courses.vocabPrepStep2') }}
|
||||
</p>
|
||||
<div class="vocab-prep-card">
|
||||
<div class="vocab-prep-card__target">{{ currentPrepItem.target }}</div>
|
||||
<div v-if="currentPrepItem.gloss" class="vocab-prep-card__gloss">{{ currentPrepItem.gloss }}</div>
|
||||
<div class="vocab-prep-card__row">
|
||||
<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>
|
||||
<button type="button" class="btn-prep-pass" @click="advancePrepPass">
|
||||
{{ isLastPrepItemInPass
|
||||
@@ -487,9 +493,38 @@
|
||||
</article>
|
||||
</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
|
||||
v-for="(exercise, index) in effectiveExercises"
|
||||
v-for="(exercise, index) in exercisesPanelExercises"
|
||||
:key="exercise.id"
|
||||
class="exercise-item surface-card"
|
||||
:class="{
|
||||
@@ -500,7 +535,7 @@
|
||||
>
|
||||
<div class="exercise-item__header">
|
||||
<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>
|
||||
</div>
|
||||
<span
|
||||
@@ -865,6 +900,31 @@
|
||||
</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>
|
||||
|
||||
<script>
|
||||
@@ -945,6 +1005,12 @@ export default {
|
||||
showCompletionDialog: false,
|
||||
showErrorDialog: false,
|
||||
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) */
|
||||
distractorPool: { target: [], native: [] },
|
||||
/** { [exerciseId]: { options: string[], useTextAnswer: boolean } } */
|
||||
@@ -1058,6 +1124,32 @@ export default {
|
||||
exerciseTargetScore() {
|
||||
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() {
|
||||
return Math.min(8, Math.max(2, Math.ceil(this.trainerNewFocusTarget * 0.25)));
|
||||
},
|
||||
@@ -1324,7 +1416,8 @@ export default {
|
||||
vocabTrainerCurrentAttempts: this.vocabTrainerCurrentAttempts,
|
||||
vocabTrainerReviewAttempts: this.vocabTrainerReviewAttempts,
|
||||
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.exerciseRetryPending = Boolean(parsedState.exerciseRetryPending);
|
||||
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();
|
||||
const knownRepeatKeys = new Set([...this.trainableLessonVocab, ...this.vocabTrainerMixedPool].map((entry) => this.getVocabKey(entry)));
|
||||
this.vocabTrainerRepeatQueue = this.normalizeRepeatQueue(parsedState.vocabTrainerRepeatQueue)
|
||||
@@ -1661,6 +1756,59 @@ export default {
|
||||
if (!n) return '';
|
||||
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() {
|
||||
if (this.lessonPrepStage >= 2 || this.prepItems.length === 0) {
|
||||
return;
|
||||
@@ -1776,8 +1924,10 @@ export default {
|
||||
const question = qData.question || qData.text || '';
|
||||
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);
|
||||
if (!match) match = question.match(/Wie heißt ['"]([^'"]+)['"]/i);
|
||||
if (!match) match = question.match(/How do you say ['"]([^'"]+)['"]/i);
|
||||
if (match) {
|
||||
const nativeWord = match[1]; // Das Wort in der Muttersprache
|
||||
// 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);
|
||||
}
|
||||
} 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);
|
||||
if (!match) match = question.match(/Was heißt ['"]([^'"]+)['"]/i);
|
||||
if (match) {
|
||||
const bisayaWord = match[1];
|
||||
// Nur hinzufügen, wenn Bisaya und Muttersprache unterschiedlich sind (verhindert "ko" -> "ko")
|
||||
@@ -1876,6 +2027,7 @@ export default {
|
||||
this.assistantError = '';
|
||||
this.exerciseRetryPending = false;
|
||||
this.exerciseRetryPendingSinceAttempts = 0;
|
||||
this.exerciseSequentialIndex = 0;
|
||||
this.exercisePreparationCompleted = false;
|
||||
this.lessonPrepStage = 0;
|
||||
this.lessonPrepIndex = 0;
|
||||
@@ -2319,32 +2471,33 @@ export default {
|
||||
this.exerciseResults[exerciseId] = res.data;
|
||||
|
||||
if (!res.data?.correct) {
|
||||
const correctText = res.data?.correctAnswer
|
||||
? String(res.data.correctAnswer)
|
||||
: '';
|
||||
if (this.trainableLessonVocab.length === 0 && this.prepItems.length > 0) {
|
||||
this.lessonPrepStage = 0;
|
||||
this.lessonPrepIndex = 0;
|
||||
this.errorMessage = this.$t('socialnetwork.vocab.courses.exercisePrepReinforcementHint');
|
||||
this.exerciseReinforcementPrepMode = true;
|
||||
this.exerciseReinforcementCorrectAnswer = correctText;
|
||||
this.exerciseReinforcementMessage = this.$t('socialnetwork.vocab.courses.exercisePrepReinforcementHint');
|
||||
} else {
|
||||
this.exerciseRetryPending = true;
|
||||
this.exerciseRetryPendingSinceAttempts = this.vocabTrainerTotalAttempts;
|
||||
this.errorMessage = this.$t('socialnetwork.vocab.courses.exerciseReinforcementHint', {
|
||||
this.exerciseReinforcementPrepMode = false;
|
||||
this.exerciseReinforcementCorrectAnswer = correctText;
|
||||
this.exerciseReinforcementMessage = this.$t('socialnetwork.vocab.courses.exerciseReinforcementHint', {
|
||||
count: this.exerciseRetryUnlockAttempts
|
||||
});
|
||||
}
|
||||
this.activeTab = 'learn';
|
||||
this.showErrorDialog = true;
|
||||
this.$nextTick(() => {
|
||||
const scrollEl = document.querySelector('.app-content__scroll.contentscroll');
|
||||
if (scrollEl) {
|
||||
scrollEl.scrollTop = 0;
|
||||
} else {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
});
|
||||
this.showExerciseReinforcementDialog = true;
|
||||
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(() => {
|
||||
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();
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -3435,6 +3588,26 @@ export default {
|
||||
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 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
@@ -3442,7 +3615,6 @@ export default {
|
||||
}
|
||||
|
||||
.vocab-prep-card__gloss {
|
||||
margin-top: 8px;
|
||||
font-size: 1rem;
|
||||
color: #6a5a44;
|
||||
}
|
||||
@@ -3610,6 +3782,67 @@ export default {
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
Reference in New Issue
Block a user