feat(VocabLessonView): enhance exercise access and vocab trainer experience

- Added conditional access to exercises based on user progress and previous vocab completion.
- Introduced visual indicators for exercise availability and review priorities to guide users.
- Updated vocab trainer descriptions and button labels to reflect the user's current state and encourage engagement.
- Implemented methods to manage exercise unlock conditions and improve user feedback on progress.
This commit is contained in:
Torsten Schulz (local)
2026-03-28 13:11:05 +01:00
parent 8a96951b50
commit a2c86247b6

View File

@@ -19,8 +19,9 @@
{{ $t('socialnetwork.vocab.courses.learn') }} {{ $t('socialnetwork.vocab.courses.learn') }}
</button> </button>
<button <button
:class="{ active: activeTab === 'exercises' }" :class="{ active: activeTab === 'exercises' }"
@click="activeTab = 'exercises'" :disabled="!canAccessExercises"
@click="openExercisesTab"
class="tab-button" class="tab-button"
> >
{{ $t('socialnetwork.vocab.courses.exercises') }} {{ $t('socialnetwork.vocab.courses.exercises') }}
@@ -190,10 +191,18 @@
<!-- 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>
<div v-if="hasPreviousVocab" class="review-priority-note">
<strong>Tageswiederholung zuerst</strong>
<p>Ältere Vokabeln stehen hier bewusst vor der Kapitel-Prüfung, damit Wiederholung täglich mitläuft.</p>
</div>
<div v-if="hasExercises && !canAccessExercises" class="exercise-lock-note">
<strong>Kapitel-Prüfung noch gesperrt</strong>
<p>{{ exerciseUnlockHint }}</p>
</div>
<div v-if="!vocabTrainerActive" class="vocab-trainer-start"> <div v-if="!vocabTrainerActive" class="vocab-trainer-start">
<p>{{ $t('socialnetwork.vocab.courses.vocabTrainerDescription') }}</p> <p>{{ hasPreviousVocab ? 'Starte mit der Wiederholung älterer Vokabeln und arbeite dich dann zur aktuellen Lektion vor.' : $t('socialnetwork.vocab.courses.vocabTrainerDescription') }}</p>
<button @click="startVocabTrainer" class="btn-start-trainer"> <button @click="startVocabTrainer" class="btn-start-trainer">
{{ $t('socialnetwork.vocab.courses.startVocabTrainer') }} {{ hasPreviousVocab ? 'Tageswiederholung starten' : $t('socialnetwork.vocab.courses.startVocabTrainer') }}
</button> </button>
</div> </div>
<div v-else class="vocab-trainer-active"> <div v-else class="vocab-trainer-active">
@@ -293,8 +302,8 @@
</div> </div>
<!-- Button um zu Übungen zu wechseln --> <!-- Button um zu Übungen zu wechseln -->
<div v-if="hasExercises" class="continue-to-exercises"> <div v-if="hasExercises && canAccessExercises" class="continue-to-exercises">
<button @click="activeTab = 'exercises'" class="btn-continue"> <button @click="openExercisesTab" class="btn-continue">
{{ $t('socialnetwork.vocab.courses.startExercises') }} {{ $t('socialnetwork.vocab.courses.startExercises') }}
</button> </button>
</div> </div>
@@ -692,6 +701,7 @@ export default {
vocabTrainerPhase: 'current', // 'current' = aktuelle Lektion, 'mixed' = gemischt mit alten vocabTrainerPhase: 'current', // 'current' = aktuelle Lektion, 'mixed' = gemischt mit alten
vocabTrainerMixedPool: [], // Pool aus alten Lektionsvokabeln vocabTrainerMixedPool: [], // Pool aus alten Lektionsvokabeln
vocabTrainerMixedAttempts: 0, // Zähler für Mixed-Phase vocabTrainerMixedAttempts: 0, // Zähler für Mixed-Phase
exercisePreparationCompleted: false,
currentVocabQuestion: null, currentVocabQuestion: null,
vocabTrainerAnswer: '', vocabTrainerAnswer: '',
vocabTrainerSelectedChoice: null, vocabTrainerSelectedChoice: null,
@@ -727,6 +737,20 @@ export default {
const exercises = this.effectiveExercises; const exercises = this.effectiveExercises;
return exercises && Array.isArray(exercises) && exercises.length > 0; return exercises && Array.isArray(exercises) && exercises.length > 0;
}, },
hasPreviousVocab() {
return Array.isArray(this.previousVocab) && this.previousVocab.length > 0;
},
canAccessExercises() {
if (!this.hasExercises) return false;
const isReview = this.lesson?.lessonType === 'review' || this.lesson?.lessonType === 'vocab_review';
return isReview || this.exercisePreparationCompleted;
},
exerciseUnlockHint() {
if (this.hasPreviousVocab) {
return 'Beantworte zuerst einige Wiederholungsfragen aus älteren Lektionen richtig. Danach wird die Kapitel-Prüfung freigeschaltet.';
}
return 'Arbeite zuerst kurz mit dem Vokabeltrainer der aktuellen Lektion. Danach wird die Kapitel-Prüfung freigeschaltet.';
},
/** Für Wiederholungslektionen: Übungen aus vorherigen Lektionen (Kapitelprüfung). Sonst: eigene Grammatik-Übungen. */ /** Für Wiederholungslektionen: Übungen aus vorherigen Lektionen (Kapitelprüfung). Sonst: eigene Grammatik-Übungen. */
effectiveExercises() { effectiveExercises() {
if (!this.lesson) return []; if (!this.lesson) return [];
@@ -836,6 +860,40 @@ export default {
} }
}, },
methods: { methods: {
openExercisesTab() {
if (!this.canAccessExercises) {
this.activeTab = 'learn';
this.showErrorDialog = true;
this.errorMessage = this.exerciseUnlockHint;
return;
}
this.activeTab = 'exercises';
},
updateExerciseUnlockState() {
if (this.exercisePreparationCompleted) {
return;
}
if (!this.hasExercises) {
this.exercisePreparationCompleted = true;
return;
}
const minimumAttempts = this.hasPreviousVocab ? 8 : 6;
const successRate = this.vocabTrainerTotalAttempts > 0
? (this.vocabTrainerCorrect / this.vocabTrainerTotalAttempts) * 100
: 0;
if (this.hasPreviousVocab) {
if (this.vocabTrainerPhase === 'mixed' && this.vocabTrainerTotalAttempts >= minimumAttempts && successRate >= 70) {
this.exercisePreparationCompleted = true;
}
return;
}
if (this.vocabTrainerTotalAttempts >= minimumAttempts && successRate >= 70) {
this.exercisePreparationCompleted = true;
}
},
_extractVocabFromExercises(exercises) { _extractVocabFromExercises(exercises) {
// Sicherstellen, dass exercises ein Array ist // Sicherstellen, dass exercises ein Array ist
if (!exercises) { if (!exercises) {
@@ -989,6 +1047,11 @@ export default {
this.assistantMessages = []; this.assistantMessages = [];
this.assistantInput = ''; this.assistantInput = '';
this.assistantError = ''; this.assistantError = '';
this.exercisePreparationCompleted = false;
this.vocabTrainerActive = false;
this.vocabTrainerPool = [];
this.vocabTrainerMixedPool = [];
this.vocabTrainerPhase = 'current';
// Reset Flags // Reset Flags
this.isCheckingLessonCompletion = false; this.isCheckingLessonCompletion = false;
this.isNavigatingToNext = false; this.isNavigatingToNext = false;
@@ -998,6 +1061,9 @@ export default {
if (tabParam === 'learn') { if (tabParam === 'learn') {
this.activeTab = 'learn'; this.activeTab = 'learn';
} }
if (tabParam === 'exercises') {
this.activeTab = 'learn';
}
if (this.$route.query.assistant) { if (this.$route.query.assistant) {
this.activeTab = 'learn'; this.activeTab = 'learn';
} }
@@ -1406,10 +1472,13 @@ export default {
this.vocabTrainerWrong = 0; this.vocabTrainerWrong = 0;
this.vocabTrainerTotalAttempts = 0; this.vocabTrainerTotalAttempts = 0;
this.vocabTrainerStats = {}; this.vocabTrainerStats = {};
this.vocabTrainerPhase = 'current';
this.vocabTrainerMixedAttempts = 0;
// Bereite Mixed-Pool aus alten Vokabeln vor (ohne Duplikate aus aktueller Lektion) // Bereite Mixed-Pool aus alten Vokabeln vor (ohne Duplikate aus aktueller Lektion)
this.vocabTrainerMixedPool = this._buildMixedPool(); this.vocabTrainerMixedPool = this._buildMixedPool();
this.vocabTrainerPhase = this.vocabTrainerMixedPool.length > 0 ? 'mixed' : 'current';
this.vocabTrainerMixedAttempts = 0;
this.vocabTrainerPool = this.vocabTrainerPhase === 'mixed'
? [...this.vocabTrainerMixedPool]
: [...this.importantVocab];
debugLog('[VocabLessonView] Mixed-Pool:', this.vocabTrainerMixedPool.length, 'Vokabeln'); debugLog('[VocabLessonView] Mixed-Pool:', this.vocabTrainerMixedPool.length, 'Vokabeln');
debugLog('[VocabLessonView] Rufe nextVocabQuestion auf'); debugLog('[VocabLessonView] Rufe nextVocabQuestion auf');
this.$nextTick(() => { this.$nextTick(() => {
@@ -1451,6 +1520,8 @@ export default {
const MC_THRESHOLD = 60; // Multiple Choice Versuche pro Phase const MC_THRESHOLD = 60; // Multiple Choice Versuche pro Phase
const MIXED_LIMIT = 40; // Anzahl gemischter Vokabeln aus alten Lektionen const MIXED_LIMIT = 40; // Anzahl gemischter Vokabeln aus alten Lektionen
this.updateExerciseUnlockState();
if (this.vocabTrainerPhase === 'current') { if (this.vocabTrainerPhase === 'current') {
// Phase 1: Aktuelle Lektion - nach MC_THRESHOLD Versuchen mit 80% → Wechsel zu Mixed oder Typing // Phase 1: Aktuelle Lektion - nach MC_THRESHOLD Versuchen mit 80% → Wechsel zu Mixed oder Typing
if (this.vocabTrainerMode === 'multiple_choice' && this.vocabTrainerTotalAttempts >= MC_THRESHOLD) { if (this.vocabTrainerMode === 'multiple_choice' && this.vocabTrainerTotalAttempts >= MC_THRESHOLD) {
@@ -2349,6 +2420,31 @@ export default {
color: #333; color: #333;
} }
.review-priority-note,
.exercise-lock-note {
margin-bottom: 12px;
padding: 12px;
border-radius: 4px;
}
.review-priority-note {
background: #eef7ff;
border: 1px solid #b9d8ff;
color: #234a72;
}
.exercise-lock-note {
background: #fff3cd;
border: 1px solid #ffc107;
color: #856404;
}
.review-priority-note strong,
.exercise-lock-note strong {
display: block;
margin-bottom: 4px;
}
.vocab-trainer-start { .vocab-trainer-start {
text-align: center; text-align: center;
} }