feat(VocabLessonView): refine vocab trainer experience and improve user guidance
- Updated vocab trainer descriptions and button labels to better reflect user progress and encourage engagement. - Enhanced visual indicators for exercise availability and review priorities, clarifying the learning path for users. - Introduced new methods to calculate trainer targets and review blending, optimizing the learning experience based on user performance.
This commit is contained in:
@@ -192,17 +192,17 @@
|
||||
<div v-if="importantVocab && importantVocab.length > 0" class="vocab-trainer-section">
|
||||
<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>
|
||||
<strong>Wiederholung läuft schrittweise mit</strong>
|
||||
<p>Zuerst liegt der Fokus auf den neuen Begriffen dieser Lektion. Mit deinem Fortschritt fließen ältere Vokabeln dann zunehmend mit ein.</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">
|
||||
<p>{{ hasPreviousVocab ? 'Starte mit der Wiederholung älterer Vokabeln und arbeite dich dann zur aktuellen Lektion vor.' : $t('socialnetwork.vocab.courses.vocabTrainerDescription') }}</p>
|
||||
<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>
|
||||
<button @click="startVocabTrainer" class="btn-start-trainer">
|
||||
{{ hasPreviousVocab ? 'Tageswiederholung starten' : $t('socialnetwork.vocab.courses.startVocabTrainer') }}
|
||||
{{ hasPreviousVocab ? 'Lektion starten' : $t('socialnetwork.vocab.courses.startVocabTrainer') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="vocab-trainer-active">
|
||||
@@ -220,7 +220,7 @@
|
||||
{{ $t('socialnetwork.vocab.courses.currentLesson') || 'Aktuelle Lektion' }}
|
||||
</span>
|
||||
<span v-if="previousVocab && previousVocab.length > 0" class="mode-badge" :class="{ 'mode-active': vocabTrainerPhase === 'mixed' }">
|
||||
{{ $t('socialnetwork.vocab.courses.mixedReview') || 'Wiederholung' }}
|
||||
{{ $t('socialnetwork.vocab.courses.mixedReview') || 'Gemischt' }}
|
||||
</span>
|
||||
<span class="mode-badge" :class="{ 'mode-active': vocabTrainerMode === 'multiple_choice', 'mode-completed': vocabTrainerMode === 'typing' }">
|
||||
{{ $t('socialnetwork.vocab.courses.modeMultipleChoice') }}
|
||||
@@ -230,6 +230,11 @@
|
||||
</span>
|
||||
<button @click="stopVocabTrainer" class="btn-stop-trainer">{{ $t('socialnetwork.vocab.courses.stopTrainer') }}</button>
|
||||
</div>
|
||||
<div v-if="hasPreviousVocab" class="stats-row trainer-progress-row">
|
||||
<span>Neue Inhalte: {{ vocabTrainerCurrentAttempts }}/{{ trainerNewFocusTarget }}</span>
|
||||
<span>Wiederholung: {{ vocabTrainerReviewAttempts }}</span>
|
||||
<span>Mischanteil: {{ Math.round(currentReviewShare * 100) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentVocabQuestion" class="vocab-question">
|
||||
<div class="vocab-prompt">
|
||||
@@ -701,6 +706,8 @@ export default {
|
||||
vocabTrainerPhase: 'current', // 'current' = aktuelle Lektion, 'mixed' = gemischt mit alten
|
||||
vocabTrainerMixedPool: [], // Pool aus alten Lektionsvokabeln
|
||||
vocabTrainerMixedAttempts: 0, // Zähler für Mixed-Phase
|
||||
vocabTrainerCurrentAttempts: 0,
|
||||
vocabTrainerReviewAttempts: 0,
|
||||
exercisePreparationCompleted: false,
|
||||
currentVocabQuestion: null,
|
||||
vocabTrainerAnswer: '',
|
||||
@@ -740,6 +747,42 @@ export default {
|
||||
hasPreviousVocab() {
|
||||
return Array.isArray(this.previousVocab) && this.previousVocab.length > 0;
|
||||
},
|
||||
lessonComplexityWeight() {
|
||||
const lessonType = this.lesson?.lessonType;
|
||||
if (['dialogue', 'phrases', 'survival', 'grammar'].includes(lessonType)) {
|
||||
return 1.2;
|
||||
}
|
||||
if (lessonType === 'review' || lessonType === 'vocab_review') {
|
||||
return 0.9;
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
trainerNewFocusTarget() {
|
||||
const vocabCount = this.importantVocab?.length || 0;
|
||||
const exerciseCount = this.effectiveExercises?.length || 0;
|
||||
const durationBonus = Math.max(0, Math.round((this.lesson?.targetMinutes || 0) / 5) - 1);
|
||||
const baseTarget = Math.ceil((Math.max(vocabCount, 4) * 1.35) + (exerciseCount * 0.35) + durationBonus);
|
||||
const weightedTarget = Math.ceil(baseTarget * this.lessonComplexityWeight);
|
||||
return Math.min(24, Math.max(6, weightedTarget));
|
||||
},
|
||||
trainerReviewBlendStart() {
|
||||
return Math.max(3, Math.ceil(this.trainerNewFocusTarget * 0.4));
|
||||
},
|
||||
trainerReviewRampWindow() {
|
||||
return Math.max(4, this.trainerNewFocusTarget - this.trainerReviewBlendStart);
|
||||
},
|
||||
trainerExerciseUnlockAttempts() {
|
||||
const unlockTarget = this.trainerNewFocusTarget + Math.ceil((this.effectiveExercises?.length || 0) * 0.25);
|
||||
return Math.min(28, Math.max(6, unlockTarget));
|
||||
},
|
||||
currentReviewShare() {
|
||||
if (!this.hasPreviousVocab) {
|
||||
return 0;
|
||||
}
|
||||
const progressPastBlendStart = Math.max(0, this.vocabTrainerCurrentAttempts - this.trainerReviewBlendStart);
|
||||
const normalizedRamp = Math.min(1, progressPastBlendStart / this.trainerReviewRampWindow);
|
||||
return Math.min(0.55, normalizedRamp * 0.55);
|
||||
},
|
||||
canAccessExercises() {
|
||||
if (!this.hasExercises) return false;
|
||||
const isReview = this.lesson?.lessonType === 'review' || this.lesson?.lessonType === 'vocab_review';
|
||||
@@ -747,9 +790,9 @@ export default {
|
||||
},
|
||||
exerciseUnlockHint() {
|
||||
if (this.hasPreviousVocab) {
|
||||
return 'Beantworte zuerst einige Wiederholungsfragen aus älteren Lektionen richtig. Danach wird die Kapitel-Prüfung freigeschaltet.';
|
||||
return `Lerne zuerst die neuen Inhalte der Lektion und arbeite dich durch ungefähr ${this.trainerExerciseUnlockAttempts} Trainerfragen. Ältere Vokabeln werden dabei nach und nach zugemischt.`;
|
||||
}
|
||||
return 'Arbeite zuerst kurz mit dem Vokabeltrainer der aktuellen Lektion. Danach wird die Kapitel-Prüfung freigeschaltet.';
|
||||
return `Arbeite zuerst durch ungefähr ${this.trainerExerciseUnlockAttempts} Trainerfragen aus dieser Lektion. Danach wird die Kapitel-Prüfung freigeschaltet.`;
|
||||
},
|
||||
/** Für Wiederholungslektionen: Übungen aus vorherigen Lektionen (Kapitelprüfung). Sonst: eigene Grammatik-Übungen. */
|
||||
effectiveExercises() {
|
||||
@@ -878,19 +921,13 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const minimumAttempts = this.hasPreviousVocab ? 8 : 6;
|
||||
const minimumAttempts = this.trainerExerciseUnlockAttempts;
|
||||
const successRate = this.vocabTrainerTotalAttempts > 0
|
||||
? (this.vocabTrainerCorrect / this.vocabTrainerTotalAttempts) * 100
|
||||
: 0;
|
||||
const currentLessonReady = this.vocabTrainerCurrentAttempts >= this.trainerNewFocusTarget;
|
||||
|
||||
if (this.hasPreviousVocab) {
|
||||
if (this.vocabTrainerPhase === 'mixed' && this.vocabTrainerTotalAttempts >= minimumAttempts && successRate >= 70) {
|
||||
this.exercisePreparationCompleted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.vocabTrainerTotalAttempts >= minimumAttempts && successRate >= 70) {
|
||||
if (currentLessonReady && this.vocabTrainerTotalAttempts >= minimumAttempts && successRate >= 70) {
|
||||
this.exercisePreparationCompleted = true;
|
||||
}
|
||||
},
|
||||
@@ -1052,6 +1089,8 @@ export default {
|
||||
this.vocabTrainerPool = [];
|
||||
this.vocabTrainerMixedPool = [];
|
||||
this.vocabTrainerPhase = 'current';
|
||||
this.vocabTrainerCurrentAttempts = 0;
|
||||
this.vocabTrainerReviewAttempts = 0;
|
||||
// Reset Flags
|
||||
this.isCheckingLessonCompletion = false;
|
||||
this.isNavigatingToNext = false;
|
||||
@@ -1471,14 +1510,14 @@ export default {
|
||||
this.vocabTrainerCorrect = 0;
|
||||
this.vocabTrainerWrong = 0;
|
||||
this.vocabTrainerTotalAttempts = 0;
|
||||
this.vocabTrainerCurrentAttempts = 0;
|
||||
this.vocabTrainerReviewAttempts = 0;
|
||||
this.vocabTrainerStats = {};
|
||||
// Bereite Mixed-Pool aus alten Vokabeln vor (ohne Duplikate aus aktueller Lektion)
|
||||
this.vocabTrainerMixedPool = this._buildMixedPool();
|
||||
this.vocabTrainerPhase = this.vocabTrainerMixedPool.length > 0 ? 'mixed' : 'current';
|
||||
this.vocabTrainerPhase = 'current';
|
||||
this.vocabTrainerMixedAttempts = 0;
|
||||
this.vocabTrainerPool = this.vocabTrainerPhase === 'mixed'
|
||||
? [...this.vocabTrainerMixedPool]
|
||||
: [...this.importantVocab];
|
||||
this.vocabTrainerPool = [...this.importantVocab];
|
||||
debugLog('[VocabLessonView] Mixed-Pool:', this.vocabTrainerMixedPool.length, 'Vokabeln');
|
||||
debugLog('[VocabLessonView] Rufe nextVocabQuestion auf');
|
||||
this.$nextTick(() => {
|
||||
@@ -1491,6 +1530,8 @@ export default {
|
||||
this.vocabTrainerAutoSwitchedToTyping = false;
|
||||
this.vocabTrainerPhase = 'current';
|
||||
this.vocabTrainerMixedAttempts = 0;
|
||||
this.vocabTrainerCurrentAttempts = 0;
|
||||
this.vocabTrainerReviewAttempts = 0;
|
||||
this.vocabTrainerMixedPool = [];
|
||||
this.currentVocabQuestion = null;
|
||||
this.vocabTrainerAnswer = '';
|
||||
@@ -1517,44 +1558,16 @@ export default {
|
||||
return this.vocabTrainerStats[key];
|
||||
},
|
||||
checkVocabModeSwitch() {
|
||||
const MC_THRESHOLD = 60; // Multiple Choice Versuche pro Phase
|
||||
const MIXED_LIMIT = 40; // Anzahl gemischter Vokabeln aus alten Lektionen
|
||||
|
||||
this.updateExerciseUnlockState();
|
||||
|
||||
if (this.vocabTrainerPhase === 'current') {
|
||||
// Phase 1: Aktuelle Lektion - nach MC_THRESHOLD Versuchen mit 80% → Wechsel zu Mixed oder Typing
|
||||
if (this.vocabTrainerMode === 'multiple_choice' && this.vocabTrainerTotalAttempts >= MC_THRESHOLD) {
|
||||
this.vocabTrainerPhase = this.hasPreviousVocab && this.currentReviewShare > 0 ? 'mixed' : 'current';
|
||||
|
||||
if (this.vocabTrainerMode === 'multiple_choice' && this.vocabTrainerTotalAttempts >= this.trainerExerciseUnlockAttempts) {
|
||||
const successRate = (this.vocabTrainerCorrect / this.vocabTrainerTotalAttempts) * 100;
|
||||
if (successRate >= 80) {
|
||||
// Wechsel zur Mixed-Phase (falls alte Vokabeln vorhanden)
|
||||
if (this.vocabTrainerMixedPool.length > 0) {
|
||||
debugLog('[VocabLessonView] Wechsel zu Mixed-Phase mit', this.vocabTrainerMixedPool.length, 'alten Vokabeln');
|
||||
this.vocabTrainerPhase = 'mixed';
|
||||
this.vocabTrainerPool = [...this.vocabTrainerMixedPool];
|
||||
this.vocabTrainerMixedAttempts = 0;
|
||||
// Stats zurücksetzen für neue Phase
|
||||
this.vocabTrainerCorrect = 0;
|
||||
this.vocabTrainerWrong = 0;
|
||||
this.vocabTrainerTotalAttempts = 0;
|
||||
} else {
|
||||
// Kein Mixed-Pool → direkt zu Typing
|
||||
this.vocabTrainerMode = 'typing';
|
||||
this.vocabTrainerAutoSwitchedToTyping = true;
|
||||
this.vocabTrainerPool = [...this.importantVocab];
|
||||
this.vocabTrainerCorrect = 0;
|
||||
this.vocabTrainerWrong = 0;
|
||||
this.vocabTrainerTotalAttempts = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (this.vocabTrainerPhase === 'mixed') {
|
||||
// Phase 2: Gemischte Wiederholung - nach MIXED_LIMIT Versuchen → Wechsel zu Typing mit allen Vokabeln
|
||||
if (this.vocabTrainerMode === 'multiple_choice' && this.vocabTrainerTotalAttempts >= MIXED_LIMIT) {
|
||||
debugLog('[VocabLessonView] Mixed-Phase abgeschlossen, wechsle zu Typing');
|
||||
this.vocabTrainerMode = 'typing';
|
||||
this.vocabTrainerAutoSwitchedToTyping = true;
|
||||
// Im Typing: Pool aus aktuellen + alten Vokabeln kombinieren
|
||||
this.vocabTrainerPool = [...this.importantVocab, ...this.vocabTrainerMixedPool];
|
||||
this.vocabTrainerCorrect = 0;
|
||||
this.vocabTrainerWrong = 0;
|
||||
@@ -1566,12 +1579,9 @@ export default {
|
||||
// Wechsle zurück zu Multiple Choice
|
||||
this.vocabTrainerMode = 'multiple_choice';
|
||||
this.vocabTrainerAutoSwitchedToTyping = false;
|
||||
// Zurück zur aktuellen Phase mit passendem Pool
|
||||
if (this.vocabTrainerPhase === 'mixed') {
|
||||
this.vocabTrainerPool = [...this.vocabTrainerMixedPool];
|
||||
} else {
|
||||
this.vocabTrainerPool = [...this.importantVocab];
|
||||
}
|
||||
this.vocabTrainerPool = this.vocabTrainerPhase === 'mixed'
|
||||
? [...this.importantVocab, ...this.vocabTrainerMixedPool]
|
||||
: [...this.importantVocab];
|
||||
// Reset Stats für Multiple Choice Modus
|
||||
this.vocabTrainerCorrect = 0;
|
||||
this.vocabTrainerWrong = 0;
|
||||
@@ -1667,15 +1677,33 @@ export default {
|
||||
// Prüfe ob Modus-Wechsel nötig ist
|
||||
this.checkVocabModeSwitch();
|
||||
|
||||
// Wähle zufällige Vokabel
|
||||
const randomIndex = Math.floor(Math.random() * this.vocabTrainerPool.length);
|
||||
const vocab = this.vocabTrainerPool[randomIndex];
|
||||
let questionSource = 'current';
|
||||
let sourcePool = this.importantVocab;
|
||||
|
||||
if (this.vocabTrainerMode === 'typing') {
|
||||
sourcePool = this.vocabTrainerPool;
|
||||
if (this.vocabTrainerMixedPool.length > 0 && Math.random() < 0.35) {
|
||||
questionSource = 'review';
|
||||
}
|
||||
} else if (this.vocabTrainerMixedPool.length > 0 && this.currentReviewShare > 0 && Math.random() < this.currentReviewShare) {
|
||||
sourcePool = this.vocabTrainerMixedPool;
|
||||
questionSource = 'review';
|
||||
}
|
||||
|
||||
if (!sourcePool || sourcePool.length === 0) {
|
||||
sourcePool = this.importantVocab;
|
||||
questionSource = 'current';
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * sourcePool.length);
|
||||
const vocab = sourcePool[randomIndex];
|
||||
this.vocabTrainerDirection = Math.random() < 0.5 ? 'L2R' : 'R2L';
|
||||
this.currentVocabQuestion = {
|
||||
vocab: vocab,
|
||||
prompt: this.vocabTrainerDirection === 'L2R' ? vocab.learning : vocab.reference,
|
||||
answer: this.vocabTrainerDirection === 'L2R' ? vocab.reference : vocab.learning,
|
||||
key: this.getVocabKey(vocab)
|
||||
key: this.getVocabKey(vocab),
|
||||
source: questionSource
|
||||
};
|
||||
|
||||
debugLog('[VocabLessonView] Neue Frage erstellt:', this.currentVocabQuestion.prompt);
|
||||
@@ -1693,7 +1721,7 @@ export default {
|
||||
// Wichtig: Der Prompt (die Frage) darf nicht in den Optionen erscheinen
|
||||
this.vocabTrainerChoiceOptions = this.buildChoiceOptions(
|
||||
this.currentVocabQuestion.answer,
|
||||
this.vocabTrainerPool,
|
||||
[...this.importantVocab, ...this.vocabTrainerMixedPool],
|
||||
this.currentVocabQuestion.prompt // Exkludiere den Prompt
|
||||
);
|
||||
debugLog('[VocabLessonView] Choice-Optionen erstellt:', this.vocabTrainerChoiceOptions);
|
||||
@@ -1736,6 +1764,11 @@ export default {
|
||||
const stats = this.getVocabStats(this.currentVocabQuestion.vocab);
|
||||
stats.attempts++;
|
||||
this.vocabTrainerTotalAttempts++;
|
||||
if (this.currentVocabQuestion.source === 'review') {
|
||||
this.vocabTrainerReviewAttempts++;
|
||||
} else {
|
||||
this.vocabTrainerCurrentAttempts++;
|
||||
}
|
||||
|
||||
if (this.vocabTrainerLastCorrect) {
|
||||
this.vocabTrainerCorrect++;
|
||||
@@ -2445,6 +2478,11 @@ export default {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.trainer-progress-row {
|
||||
font-size: 0.92em;
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
.vocab-trainer-start {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user