Enhance VocabLessonView with new vocabulary trainer features and improved statistics
- Added functionality for tracking total attempts and success rates in the vocabulary trainer, enhancing user feedback on performance. - Introduced multiple choice and typing modes for vocabulary practice, allowing users to switch between different learning styles. - Updated translations in both English and German to include new vocabulary terms and exercise instructions, ensuring consistency across languages. - Improved UI layout for displaying vocabulary statistics and answer options, enhancing overall user experience.
This commit is contained in:
@@ -383,6 +383,13 @@
|
|||||||
"translateTo": "Übersetze ins Deutsche",
|
"translateTo": "Übersetze ins Deutsche",
|
||||||
"translateFrom": "Übersetze ins Bisaya",
|
"translateFrom": "Übersetze ins Bisaya",
|
||||||
"next": "Weiter",
|
"next": "Weiter",
|
||||||
|
"totalAttempts": "Versuche",
|
||||||
|
"successRate": "Erfolgsrate",
|
||||||
|
"modeMultipleChoice": "Multiple Choice",
|
||||||
|
"modeTyping": "Texteingabe",
|
||||||
|
"lessonCompleted": "Lektion abgeschlossen!",
|
||||||
|
"goToNextLesson": "Zur nächsten Lektion wechseln?",
|
||||||
|
"allLessonsCompleted": "Alle Lektionen abgeschlossen!",
|
||||||
"startExercises": "Zur Kapitel-Prüfung",
|
"startExercises": "Zur Kapitel-Prüfung",
|
||||||
"correctAnswer": "Richtige Antwort",
|
"correctAnswer": "Richtige Antwort",
|
||||||
"alternatives": "Alternative Antworten"
|
"alternatives": "Alternative Antworten"
|
||||||
|
|||||||
@@ -383,6 +383,13 @@
|
|||||||
"translateTo": "Translate to English",
|
"translateTo": "Translate to English",
|
||||||
"translateFrom": "Translate to Target Language",
|
"translateFrom": "Translate to Target Language",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
|
"totalAttempts": "Attempts",
|
||||||
|
"successRate": "Success Rate",
|
||||||
|
"modeMultipleChoice": "Multiple Choice",
|
||||||
|
"modeTyping": "Text Input",
|
||||||
|
"lessonCompleted": "Lesson completed!",
|
||||||
|
"goToNextLesson": "Go to next lesson?",
|
||||||
|
"allLessonsCompleted": "All lessons completed!",
|
||||||
"startExercises": "Start Chapter Test",
|
"startExercises": "Start Chapter Test",
|
||||||
"correctAnswer": "Correct Answer",
|
"correctAnswer": "Correct Answer",
|
||||||
"alternatives": "Alternative Answers"
|
"alternatives": "Alternative Answers"
|
||||||
|
|||||||
@@ -64,9 +64,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="vocab-trainer-active">
|
<div v-else class="vocab-trainer-active">
|
||||||
<div class="vocab-trainer-stats">
|
<div class="vocab-trainer-stats">
|
||||||
<span>{{ $t('socialnetwork.vocab.courses.correct') }}: {{ vocabTrainerCorrect }}</span>
|
<div class="stats-row">
|
||||||
<span>{{ $t('socialnetwork.vocab.courses.wrong') }}: {{ vocabTrainerWrong }}</span>
|
<span>{{ $t('socialnetwork.vocab.courses.correct') }}: {{ vocabTrainerCorrect }}</span>
|
||||||
<button @click="stopVocabTrainer" class="btn-stop-trainer">{{ $t('socialnetwork.vocab.courses.stopTrainer') }}</button>
|
<span>{{ $t('socialnetwork.vocab.courses.wrong') }}: {{ vocabTrainerWrong }}</span>
|
||||||
|
<span>{{ $t('socialnetwork.vocab.courses.totalAttempts') }}: {{ vocabTrainerTotalAttempts }}</span>
|
||||||
|
<span v-if="vocabTrainerTotalAttempts > 0" class="success-rate">
|
||||||
|
{{ $t('socialnetwork.vocab.courses.successRate') }}: {{ Math.round((vocabTrainerCorrect / vocabTrainerTotalAttempts) * 100) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="stats-row">
|
||||||
|
<span class="mode-badge" :class="{ 'mode-active': vocabTrainerMode === 'multiple_choice', 'mode-completed': vocabTrainerMode === 'typing' }">
|
||||||
|
{{ $t('socialnetwork.vocab.courses.modeMultipleChoice') }}
|
||||||
|
</span>
|
||||||
|
<span class="mode-badge" :class="{ 'mode-active': vocabTrainerMode === 'typing' }">
|
||||||
|
{{ $t('socialnetwork.vocab.courses.modeTyping') }}
|
||||||
|
</span>
|
||||||
|
<button @click="stopVocabTrainer" class="btn-stop-trainer">{{ $t('socialnetwork.vocab.courses.stopTrainer') }}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="currentVocabQuestion" class="vocab-question">
|
<div v-if="currentVocabQuestion" class="vocab-question">
|
||||||
<div class="vocab-prompt">
|
<div class="vocab-prompt">
|
||||||
@@ -79,7 +93,25 @@
|
|||||||
{{ $t('socialnetwork.vocab.courses.wrong') }}. {{ $t('socialnetwork.vocab.courses.correctAnswer') }}: {{ currentVocabQuestion.answer }}
|
{{ $t('socialnetwork.vocab.courses.wrong') }}. {{ $t('socialnetwork.vocab.courses.correctAnswer') }}: {{ currentVocabQuestion.answer }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="vocab-answer-area">
|
<!-- Multiple Choice Modus -->
|
||||||
|
<div v-else-if="vocabTrainerMode === 'multiple_choice'" class="vocab-answer-area multiple-choice">
|
||||||
|
<div class="choice-buttons">
|
||||||
|
<button
|
||||||
|
v-for="(option, index) in vocabTrainerChoiceOptions"
|
||||||
|
:key="index"
|
||||||
|
@click="selectVocabChoice(option)"
|
||||||
|
class="choice-button"
|
||||||
|
:class="{ selected: vocabTrainerSelectedChoice === option }"
|
||||||
|
>
|
||||||
|
{{ option }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="checkVocabAnswer" :disabled="!vocabTrainerSelectedChoice" class="btn-check">
|
||||||
|
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Texteingabe Modus -->
|
||||||
|
<div v-else class="vocab-answer-area typing">
|
||||||
<input
|
<input
|
||||||
v-model="vocabTrainerAnswer"
|
v-model="vocabTrainerAnswer"
|
||||||
@keydown.enter.prevent="checkVocabAnswer"
|
@keydown.enter.prevent="checkVocabAnswer"
|
||||||
@@ -87,11 +119,12 @@
|
|||||||
class="vocab-input"
|
class="vocab-input"
|
||||||
ref="vocabInput"
|
ref="vocabInput"
|
||||||
/>
|
/>
|
||||||
<button @click="checkVocabAnswer" :disabled="!vocabTrainerAnswer.trim()">
|
<button @click="checkVocabAnswer" :disabled="!vocabTrainerAnswer.trim()" class="btn-check">
|
||||||
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
|
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="vocabTrainerAnswered" class="vocab-next">
|
<!-- "Weiter"-Button nur im Multiple Choice Modus oder bei falscher Antwort im Typing Modus -->
|
||||||
|
<div v-if="vocabTrainerAnswered && (vocabTrainerMode === 'multiple_choice' || !vocabTrainerLastCorrect)" class="vocab-next">
|
||||||
<button @click="nextVocabQuestion">{{ $t('socialnetwork.vocab.courses.next') }}</button>
|
<button @click="nextVocabQuestion">{{ $t('socialnetwork.vocab.courses.next') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,10 +285,15 @@ export default {
|
|||||||
// Vokabeltrainer
|
// Vokabeltrainer
|
||||||
vocabTrainerActive: false,
|
vocabTrainerActive: false,
|
||||||
vocabTrainerPool: [],
|
vocabTrainerPool: [],
|
||||||
|
vocabTrainerMode: 'multiple_choice', // 'multiple_choice' oder 'typing'
|
||||||
vocabTrainerCorrect: 0,
|
vocabTrainerCorrect: 0,
|
||||||
vocabTrainerWrong: 0,
|
vocabTrainerWrong: 0,
|
||||||
|
vocabTrainerTotalAttempts: 0,
|
||||||
|
vocabTrainerStats: {}, // { [vocabKey]: { attempts: 0, correct: 0, wrong: 0 } }
|
||||||
|
vocabTrainerChoiceOptions: [],
|
||||||
currentVocabQuestion: null,
|
currentVocabQuestion: null,
|
||||||
vocabTrainerAnswer: '',
|
vocabTrainerAnswer: '',
|
||||||
|
vocabTrainerSelectedChoice: null,
|
||||||
vocabTrainerAnswered: false,
|
vocabTrainerAnswered: false,
|
||||||
vocabTrainerLastCorrect: false,
|
vocabTrainerLastCorrect: false,
|
||||||
vocabTrainerDirection: 'L2R' // L2R: learning->reference, R2L: reference->learning
|
vocabTrainerDirection: 'L2R' // L2R: learning->reference, R2L: reference->learning
|
||||||
@@ -299,17 +337,22 @@ export default {
|
|||||||
// Extrahiere wichtige Begriffe aus den Übungen
|
// Extrahiere wichtige Begriffe aus den Übungen
|
||||||
try {
|
try {
|
||||||
if (!this.lesson || !this.lesson.grammarExercises || !Array.isArray(this.lesson.grammarExercises)) {
|
if (!this.lesson || !this.lesson.grammarExercises || !Array.isArray(this.lesson.grammarExercises)) {
|
||||||
|
console.log('[importantVocab] Keine Übungen vorhanden');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const vocabMap = new Map();
|
const vocabMap = new Map();
|
||||||
|
|
||||||
this.lesson.grammarExercises.forEach(exercise => {
|
this.lesson.grammarExercises.forEach((exercise, idx) => {
|
||||||
try {
|
try {
|
||||||
|
console.log(`[importantVocab] Verarbeite Übung ${idx + 1}:`, exercise.title);
|
||||||
// Extrahiere aus questionData
|
// Extrahiere aus questionData
|
||||||
const qData = this.getQuestionData(exercise);
|
const qData = this.getQuestionData(exercise);
|
||||||
const aData = this.getAnswerData(exercise);
|
const aData = this.getAnswerData(exercise);
|
||||||
|
|
||||||
|
console.log(`[importantVocab] qData:`, qData);
|
||||||
|
console.log(`[importantVocab] aData:`, aData);
|
||||||
|
|
||||||
if (qData && aData) {
|
if (qData && aData) {
|
||||||
// Für Multiple Choice: Extrahiere Optionen und richtige Antwort
|
// Für Multiple Choice: Extrahiere Optionen und richtige Antwort
|
||||||
if (this.getExerciseType(exercise) === 'multiple_choice') {
|
if (this.getExerciseType(exercise) === 'multiple_choice') {
|
||||||
@@ -317,22 +360,28 @@ export default {
|
|||||||
const correctIndex = aData.correctAnswer !== undefined ? aData.correctAnswer : (aData.correct || 0);
|
const correctIndex = aData.correctAnswer !== undefined ? aData.correctAnswer : (aData.correct || 0);
|
||||||
const correctAnswer = options[correctIndex] || '';
|
const correctAnswer = options[correctIndex] || '';
|
||||||
|
|
||||||
|
console.log(`[importantVocab] Multiple Choice - options:`, options, `correctIndex:`, correctIndex, `correctAnswer:`, correctAnswer);
|
||||||
|
|
||||||
if (correctAnswer) {
|
if (correctAnswer) {
|
||||||
// Versuche die Frage zu analysieren (z.B. "Wie sagt man 'X' auf Bisaya?" oder "Was bedeutet 'X'?")
|
// Versuche die Frage zu analysieren (z.B. "Wie sagt man 'X' auf Bisaya?" oder "Was bedeutet 'X'?")
|
||||||
const question = qData.question || qData.text || '';
|
const question = qData.question || qData.text || '';
|
||||||
|
console.log(`[importantVocab] Frage:`, question);
|
||||||
|
|
||||||
// Pattern 1: "Wie sagt man 'X' auf Bisaya?" -> X ist Deutsch, correctAnswer ist Bisaya
|
// Pattern 1: "Wie sagt man 'X' auf Bisaya?" -> X ist Deutsch, correctAnswer ist Bisaya
|
||||||
let match = question.match(/['"]([^'"]+)['"]/);
|
let match = question.match(/['"]([^'"]+)['"]/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const germanWord = match[1];
|
const germanWord = match[1];
|
||||||
|
console.log(`[importantVocab] Pattern 1 gefunden - Bisaya:`, correctAnswer, `Deutsch:`, germanWord);
|
||||||
vocabMap.set(correctAnswer, { learning: correctAnswer, reference: germanWord });
|
vocabMap.set(correctAnswer, { learning: correctAnswer, reference: germanWord });
|
||||||
} else {
|
} else {
|
||||||
// Pattern 2: "Was bedeutet 'X'?" -> X ist Bisaya, correctAnswer ist Deutsch
|
// Pattern 2: "Was bedeutet 'X'?" -> X ist Bisaya, correctAnswer ist Deutsch
|
||||||
match = question.match(/Was bedeutet ['"]([^'"]+)['"]/);
|
match = question.match(/Was bedeutet ['"]([^'"]+)['"]/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const bisayaWord = match[1];
|
const bisayaWord = match[1];
|
||||||
|
console.log(`[importantVocab] Pattern 2 gefunden - Bisaya:`, bisayaWord, `Deutsch:`, correctAnswer);
|
||||||
vocabMap.set(bisayaWord, { learning: bisayaWord, reference: correctAnswer });
|
vocabMap.set(bisayaWord, { learning: bisayaWord, reference: correctAnswer });
|
||||||
} else {
|
} else {
|
||||||
|
console.log(`[importantVocab] Kein Pattern gefunden, Fallback`);
|
||||||
// Fallback: Verwende die richtige Antwort als Lernwort
|
// Fallback: Verwende die richtige Antwort als Lernwort
|
||||||
vocabMap.set(correctAnswer, { learning: correctAnswer, reference: correctAnswer });
|
vocabMap.set(correctAnswer, { learning: correctAnswer, reference: correctAnswer });
|
||||||
}
|
}
|
||||||
@@ -343,6 +392,7 @@ export default {
|
|||||||
// Für Gap Fill: Extrahiere richtige Antworten
|
// Für Gap Fill: Extrahiere richtige Antworten
|
||||||
if (this.getExerciseType(exercise) === 'gap_fill') {
|
if (this.getExerciseType(exercise) === 'gap_fill') {
|
||||||
const answers = aData.answers || (aData.correct ? (Array.isArray(aData.correct) ? aData.correct : [aData.correct]) : []);
|
const answers = aData.answers || (aData.correct ? (Array.isArray(aData.correct) ? aData.correct : [aData.correct]) : []);
|
||||||
|
console.log(`[importantVocab] Gap Fill - answers:`, answers);
|
||||||
if (answers.length > 0) {
|
if (answers.length > 0) {
|
||||||
// Versuche aus dem Text Kontext zu extrahieren
|
// Versuche aus dem Text Kontext zu extrahieren
|
||||||
const text = qData.text || '';
|
const text = qData.text || '';
|
||||||
@@ -361,7 +411,9 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(vocabMap.values());
|
const result = Array.from(vocabMap.values());
|
||||||
|
console.log(`[importantVocab] Ergebnis:`, result);
|
||||||
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fehler in importantVocab computed property:', e);
|
console.error('Fehler in importantVocab computed property:', e);
|
||||||
return [];
|
return [];
|
||||||
@@ -522,11 +574,72 @@ export default {
|
|||||||
|
|
||||||
const res = await apiClient.post(`/api/vocab/grammar-exercises/${exerciseId}/check`, { answer });
|
const res = await apiClient.post(`/api/vocab/grammar-exercises/${exerciseId}/check`, { answer });
|
||||||
this.exerciseResults[exerciseId] = res.data;
|
this.exerciseResults[exerciseId] = res.data;
|
||||||
|
|
||||||
|
// Prüfe ob alle Übungen bestanden sind
|
||||||
|
await this.checkLessonCompletion();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fehler beim Prüfen der Antwort:', e);
|
console.error('Fehler beim Prüfen der Antwort:', e);
|
||||||
alert(e.response?.data?.error || 'Fehler beim Prüfen der Antwort');
|
alert(e.response?.data?.error || 'Fehler beim Prüfen der Antwort');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async checkLessonCompletion() {
|
||||||
|
// Prüfe ob alle Übungen korrekt beantwortet wurden
|
||||||
|
if (!this.lesson || !this.lesson.grammarExercises) return;
|
||||||
|
|
||||||
|
const allExercises = this.lesson.grammarExercises;
|
||||||
|
const allCompleted = allExercises.every(exercise => {
|
||||||
|
const result = this.exerciseResults[exercise.id];
|
||||||
|
return result && result.correct;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allCompleted) {
|
||||||
|
// Berechne Gesamt-Score
|
||||||
|
const totalExercises = allExercises.length;
|
||||||
|
const correctExercises = allExercises.filter(ex => this.exerciseResults[ex.id]?.correct).length;
|
||||||
|
const score = Math.round((correctExercises / totalExercises) * 100);
|
||||||
|
|
||||||
|
// Aktualisiere Fortschritt
|
||||||
|
try {
|
||||||
|
await apiClient.put(`/api/vocab/lessons/${this.lessonId}/progress`, {
|
||||||
|
completed: true,
|
||||||
|
score: score,
|
||||||
|
timeSpentMinutes: 0 // TODO: Zeit tracken
|
||||||
|
});
|
||||||
|
|
||||||
|
// Weiterleitung zur nächsten Lektion
|
||||||
|
await this.navigateToNextLesson();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Aktualisieren des Fortschritts:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async navigateToNextLesson() {
|
||||||
|
try {
|
||||||
|
// Lade Kurs mit allen Lektionen
|
||||||
|
const courseRes = await apiClient.get(`/api/vocab/courses/${this.courseId}`);
|
||||||
|
const course = courseRes.data;
|
||||||
|
|
||||||
|
if (!course.lessons || course.lessons.length === 0) return;
|
||||||
|
|
||||||
|
// Finde aktuelle Lektion
|
||||||
|
const currentLessonIndex = course.lessons.findIndex(l => l.id === parseInt(this.lessonId));
|
||||||
|
|
||||||
|
if (currentLessonIndex >= 0 && currentLessonIndex < course.lessons.length - 1) {
|
||||||
|
// Nächste Lektion gefunden
|
||||||
|
const nextLesson = course.lessons[currentLessonIndex + 1];
|
||||||
|
|
||||||
|
// Zeige Erfolgs-Meldung und leite weiter
|
||||||
|
if (confirm(this.$t('socialnetwork.vocab.courses.lessonCompleted') + '\n' + this.$t('socialnetwork.vocab.courses.goToNextLesson'))) {
|
||||||
|
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/lessons/${nextLesson.id}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Letzte Lektion - zeige Abschluss-Meldung
|
||||||
|
alert(this.$t('socialnetwork.vocab.courses.allLessonsCompleted'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Laden der nächsten Lektion:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
back() {
|
back() {
|
||||||
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}`);
|
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}`);
|
||||||
},
|
},
|
||||||
@@ -535,52 +648,149 @@ export default {
|
|||||||
if (!this.importantVocab || this.importantVocab.length === 0) return;
|
if (!this.importantVocab || this.importantVocab.length === 0) return;
|
||||||
this.vocabTrainerActive = true;
|
this.vocabTrainerActive = true;
|
||||||
this.vocabTrainerPool = [...this.importantVocab];
|
this.vocabTrainerPool = [...this.importantVocab];
|
||||||
|
this.vocabTrainerMode = 'multiple_choice';
|
||||||
this.vocabTrainerCorrect = 0;
|
this.vocabTrainerCorrect = 0;
|
||||||
this.vocabTrainerWrong = 0;
|
this.vocabTrainerWrong = 0;
|
||||||
|
this.vocabTrainerTotalAttempts = 0;
|
||||||
|
this.vocabTrainerStats = {};
|
||||||
this.nextVocabQuestion();
|
this.nextVocabQuestion();
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$refs.vocabInput?.focus?.();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
stopVocabTrainer() {
|
stopVocabTrainer() {
|
||||||
this.vocabTrainerActive = false;
|
this.vocabTrainerActive = false;
|
||||||
this.currentVocabQuestion = null;
|
this.currentVocabQuestion = null;
|
||||||
this.vocabTrainerAnswer = '';
|
this.vocabTrainerAnswer = '';
|
||||||
|
this.vocabTrainerSelectedChoice = null;
|
||||||
this.vocabTrainerAnswered = false;
|
this.vocabTrainerAnswered = false;
|
||||||
},
|
},
|
||||||
|
getVocabKey(vocab) {
|
||||||
|
return `${vocab.learning}|${vocab.reference}`;
|
||||||
|
},
|
||||||
|
getVocabStats(vocab) {
|
||||||
|
const key = this.getVocabKey(vocab);
|
||||||
|
if (!this.vocabTrainerStats[key]) {
|
||||||
|
this.vocabTrainerStats[key] = { attempts: 0, correct: 0, wrong: 0 };
|
||||||
|
}
|
||||||
|
return this.vocabTrainerStats[key];
|
||||||
|
},
|
||||||
|
checkVocabModeSwitch() {
|
||||||
|
// Wechsle zu Texteingabe wenn 80% erreicht und mindestens 20 Versuche
|
||||||
|
if (this.vocabTrainerMode === 'multiple_choice' && this.vocabTrainerTotalAttempts >= 20) {
|
||||||
|
const successRate = (this.vocabTrainerCorrect / this.vocabTrainerTotalAttempts) * 100;
|
||||||
|
if (successRate >= 80) {
|
||||||
|
this.vocabTrainerMode = 'typing';
|
||||||
|
// Reset Stats für Texteingabe-Modus
|
||||||
|
this.vocabTrainerCorrect = 0;
|
||||||
|
this.vocabTrainerWrong = 0;
|
||||||
|
this.vocabTrainerTotalAttempts = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildChoiceOptions(correctAnswer, allVocabs) {
|
||||||
|
const options = new Set([correctAnswer]);
|
||||||
|
// Füge 3 Distraktoren hinzu
|
||||||
|
while (options.size < 4 && allVocabs.length > 1) {
|
||||||
|
const randomVocab = allVocabs[Math.floor(Math.random() * allVocabs.length)];
|
||||||
|
const distractor = this.vocabTrainerDirection === 'L2R' ? randomVocab.reference : randomVocab.learning;
|
||||||
|
if (distractor !== correctAnswer) {
|
||||||
|
options.add(distractor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Shuffle
|
||||||
|
const arr = Array.from(options);
|
||||||
|
for (let i = arr.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
},
|
||||||
nextVocabQuestion() {
|
nextVocabQuestion() {
|
||||||
if (!this.vocabTrainerPool || this.vocabTrainerPool.length === 0) {
|
if (!this.vocabTrainerPool || this.vocabTrainerPool.length === 0) {
|
||||||
this.currentVocabQuestion = null;
|
this.currentVocabQuestion = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 randomIndex = Math.floor(Math.random() * this.vocabTrainerPool.length);
|
||||||
const vocab = this.vocabTrainerPool[randomIndex];
|
const vocab = this.vocabTrainerPool[randomIndex];
|
||||||
this.vocabTrainerDirection = Math.random() < 0.5 ? 'L2R' : 'R2L';
|
this.vocabTrainerDirection = Math.random() < 0.5 ? 'L2R' : 'R2L';
|
||||||
this.currentVocabQuestion = {
|
this.currentVocabQuestion = {
|
||||||
vocab: vocab,
|
vocab: vocab,
|
||||||
prompt: this.vocabTrainerDirection === 'L2R' ? vocab.learning : vocab.reference,
|
prompt: this.vocabTrainerDirection === 'L2R' ? vocab.learning : vocab.reference,
|
||||||
answer: this.vocabTrainerDirection === 'L2R' ? vocab.reference : vocab.learning
|
answer: this.vocabTrainerDirection === 'L2R' ? vocab.reference : vocab.learning,
|
||||||
|
key: this.getVocabKey(vocab)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reset UI
|
||||||
this.vocabTrainerAnswer = '';
|
this.vocabTrainerAnswer = '';
|
||||||
|
this.vocabTrainerSelectedChoice = null;
|
||||||
this.vocabTrainerAnswered = false;
|
this.vocabTrainerAnswered = false;
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$refs.vocabInput?.focus?.();
|
// Erstelle Choice-Optionen für Multiple Choice
|
||||||
});
|
if (this.vocabTrainerMode === 'multiple_choice') {
|
||||||
|
this.vocabTrainerChoiceOptions = this.buildChoiceOptions(this.currentVocabQuestion.answer, this.vocabTrainerPool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fokussiere Eingabefeld im Typing-Modus
|
||||||
|
if (this.vocabTrainerMode === 'typing') {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.vocabInput?.focus?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectVocabChoice(option) {
|
||||||
|
this.vocabTrainerSelectedChoice = option;
|
||||||
},
|
},
|
||||||
normalizeVocab(s) {
|
normalizeVocab(s) {
|
||||||
return String(s || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
return String(s || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
||||||
},
|
},
|
||||||
checkVocabAnswer() {
|
checkVocabAnswer() {
|
||||||
if (!this.currentVocabQuestion || !this.vocabTrainerAnswer.trim()) return;
|
if (!this.currentVocabQuestion) return;
|
||||||
const userAnswer = this.normalizeVocab(this.vocabTrainerAnswer);
|
|
||||||
const correctAnswer = this.normalizeVocab(this.currentVocabQuestion.answer);
|
let userAnswer = '';
|
||||||
this.vocabTrainerLastCorrect = userAnswer === correctAnswer;
|
if (this.vocabTrainerMode === 'multiple_choice') {
|
||||||
|
if (!this.vocabTrainerSelectedChoice) return;
|
||||||
|
userAnswer = this.vocabTrainerSelectedChoice;
|
||||||
|
} else {
|
||||||
|
if (!this.vocabTrainerAnswer.trim()) return;
|
||||||
|
userAnswer = this.vocabTrainerAnswer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedUser = this.normalizeVocab(userAnswer);
|
||||||
|
const normalizedCorrect = this.normalizeVocab(this.currentVocabQuestion.answer);
|
||||||
|
this.vocabTrainerLastCorrect = normalizedUser === normalizedCorrect;
|
||||||
|
|
||||||
|
// Update Stats
|
||||||
|
const stats = this.getVocabStats(this.currentVocabQuestion.vocab);
|
||||||
|
stats.attempts++;
|
||||||
|
this.vocabTrainerTotalAttempts++;
|
||||||
|
|
||||||
if (this.vocabTrainerLastCorrect) {
|
if (this.vocabTrainerLastCorrect) {
|
||||||
this.vocabTrainerCorrect++;
|
this.vocabTrainerCorrect++;
|
||||||
|
stats.correct++;
|
||||||
} else {
|
} else {
|
||||||
this.vocabTrainerWrong++;
|
this.vocabTrainerWrong++;
|
||||||
|
stats.wrong++;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.vocabTrainerAnswered = true;
|
this.vocabTrainerAnswered = true;
|
||||||
|
|
||||||
|
// Im Typing-Modus: Automatisch zur nächsten Frage nach kurzer Pause (nur bei richtiger Antwort)
|
||||||
|
if (this.vocabTrainerMode === 'typing' && this.vocabTrainerLastCorrect) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.nextVocabQuestion();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Im Typing-Modus bei falscher Antwort: Eingabefeld fokussieren für erneuten Versuch
|
||||||
|
if (this.vocabTrainerMode === 'typing' && !this.vocabTrainerLastCorrect) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.vocabInput?.focus?.();
|
||||||
|
this.vocabTrainerAnswer = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
@@ -921,15 +1131,48 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vocab-trainer-stats {
|
.vocab-trainer-stats {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-rate {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge.mode-active {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge.mode-completed {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-stop-trainer {
|
.btn-stop-trainer {
|
||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
background: #dc3545;
|
background: #dc3545;
|
||||||
@@ -969,9 +1212,46 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vocab-answer-area {
|
.vocab-answer-area {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vocab-answer-area.typing {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 15px;
|
}
|
||||||
|
|
||||||
|
.vocab-answer-area.multiple-choice {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-button {
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-button:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
background: #f0f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-button.selected {
|
||||||
|
border-color: #007bff;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vocab-input {
|
.vocab-input {
|
||||||
@@ -982,7 +1262,7 @@ export default {
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vocab-answer-area button {
|
.btn-check {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: #4CAF50;
|
background: #4CAF50;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -992,11 +1272,11 @@ export default {
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vocab-answer-area button:hover:not(:disabled) {
|
.btn-check:hover:not(:disabled) {
|
||||||
background: #45a049;
|
background: #45a049;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vocab-answer-area button:disabled {
|
.btn-check:disabled {
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user