From c9a7619737448a3db6de62efe372830ad4c6805a Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sat, 28 Mar 2026 23:41:25 +0100 Subject: [PATCH] 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. --- frontend/src/views/social/VocabLessonView.vue | 166 +++++++++++------- 1 file changed, 102 insertions(+), 64 deletions(-) diff --git a/frontend/src/views/social/VocabLessonView.vue b/frontend/src/views/social/VocabLessonView.vue index 2756f82..d0a3953 100644 --- a/frontend/src/views/social/VocabLessonView.vue +++ b/frontend/src/views/social/VocabLessonView.vue @@ -192,17 +192,17 @@

{{ $t('socialnetwork.vocab.courses.vocabTrainer') }}

- Tageswiederholung zuerst -

Ältere Vokabeln stehen hier bewusst vor der Kapitel-Prüfung, damit Wiederholung täglich mitläuft.

+ Wiederholung läuft schrittweise mit +

Zuerst liegt der Fokus auf den neuen Begriffen dieser Lektion. Mit deinem Fortschritt fließen ältere Vokabeln dann zunehmend mit ein.

Kapitel-Prüfung noch gesperrt

{{ exerciseUnlockHint }}

-

{{ hasPreviousVocab ? 'Starte mit der Wiederholung älterer Vokabeln und arbeite dich dann zur aktuellen Lektion vor.' : $t('socialnetwork.vocab.courses.vocabTrainerDescription') }}

+

{{ hasPreviousVocab ? 'Starte mit den neuen Vokabeln dieser Lektion. Mit fortschreitendem Üben mischt der Trainer automatisch passende Wiederholungen ein.' : $t('socialnetwork.vocab.courses.vocabTrainerDescription') }}

@@ -220,7 +220,7 @@ {{ $t('socialnetwork.vocab.courses.currentLesson') || 'Aktuelle Lektion' }} - {{ $t('socialnetwork.vocab.courses.mixedReview') || 'Wiederholung' }} + {{ $t('socialnetwork.vocab.courses.mixedReview') || 'Gemischt' }} {{ $t('socialnetwork.vocab.courses.modeMultipleChoice') }} @@ -230,6 +230,11 @@
+
+ Neue Inhalte: {{ vocabTrainerCurrentAttempts }}/{{ trainerNewFocusTarget }} + Wiederholung: {{ vocabTrainerReviewAttempts }} + Mischanteil: {{ Math.round(currentReviewShare * 100) }}% +
@@ -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) { - 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) { + 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) { 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; }