diff --git a/frontend/src/i18n/locales/de/socialnetwork.json b/frontend/src/i18n/locales/de/socialnetwork.json index 864ed52..02378c6 100644 --- a/frontend/src/i18n/locales/de/socialnetwork.json +++ b/frontend/src/i18n/locales/de/socialnetwork.json @@ -383,6 +383,13 @@ "translateTo": "Übersetze ins Deutsche", "translateFrom": "Übersetze ins Bisaya", "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", "correctAnswer": "Richtige Antwort", "alternatives": "Alternative Antworten" diff --git a/frontend/src/i18n/locales/en/socialnetwork.json b/frontend/src/i18n/locales/en/socialnetwork.json index a388125..a99260c 100644 --- a/frontend/src/i18n/locales/en/socialnetwork.json +++ b/frontend/src/i18n/locales/en/socialnetwork.json @@ -383,6 +383,13 @@ "translateTo": "Translate to English", "translateFrom": "Translate to Target Language", "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", "correctAnswer": "Correct Answer", "alternatives": "Alternative Answers" diff --git a/frontend/src/views/social/VocabLessonView.vue b/frontend/src/views/social/VocabLessonView.vue index 6d846f1..da1c06e 100644 --- a/frontend/src/views/social/VocabLessonView.vue +++ b/frontend/src/views/social/VocabLessonView.vue @@ -64,9 +64,23 @@
- {{ $t('socialnetwork.vocab.courses.correct') }}: {{ vocabTrainerCorrect }} - {{ $t('socialnetwork.vocab.courses.wrong') }}: {{ vocabTrainerWrong }} - +
+ {{ $t('socialnetwork.vocab.courses.correct') }}: {{ vocabTrainerCorrect }} + {{ $t('socialnetwork.vocab.courses.wrong') }}: {{ vocabTrainerWrong }} + {{ $t('socialnetwork.vocab.courses.totalAttempts') }}: {{ vocabTrainerTotalAttempts }} + + {{ $t('socialnetwork.vocab.courses.successRate') }}: {{ Math.round((vocabTrainerCorrect / vocabTrainerTotalAttempts) * 100) }}% + +
+
+ + {{ $t('socialnetwork.vocab.courses.modeMultipleChoice') }} + + + {{ $t('socialnetwork.vocab.courses.modeTyping') }} + + +
@@ -79,7 +93,25 @@ {{ $t('socialnetwork.vocab.courses.wrong') }}. {{ $t('socialnetwork.vocab.courses.correctAnswer') }}: {{ currentVocabQuestion.answer }}
-
+ +
+
+ +
+ +
+ +
-
-
+ +
@@ -252,10 +285,15 @@ export default { // Vokabeltrainer vocabTrainerActive: false, vocabTrainerPool: [], + vocabTrainerMode: 'multiple_choice', // 'multiple_choice' oder 'typing' vocabTrainerCorrect: 0, vocabTrainerWrong: 0, + vocabTrainerTotalAttempts: 0, + vocabTrainerStats: {}, // { [vocabKey]: { attempts: 0, correct: 0, wrong: 0 } } + vocabTrainerChoiceOptions: [], currentVocabQuestion: null, vocabTrainerAnswer: '', + vocabTrainerSelectedChoice: null, vocabTrainerAnswered: false, vocabTrainerLastCorrect: false, vocabTrainerDirection: 'L2R' // L2R: learning->reference, R2L: reference->learning @@ -299,17 +337,22 @@ export default { // Extrahiere wichtige Begriffe aus den Übungen try { if (!this.lesson || !this.lesson.grammarExercises || !Array.isArray(this.lesson.grammarExercises)) { + console.log('[importantVocab] Keine Übungen vorhanden'); return []; } const vocabMap = new Map(); - this.lesson.grammarExercises.forEach(exercise => { + this.lesson.grammarExercises.forEach((exercise, idx) => { try { + console.log(`[importantVocab] Verarbeite Übung ${idx + 1}:`, exercise.title); // Extrahiere aus questionData const qData = this.getQuestionData(exercise); const aData = this.getAnswerData(exercise); + console.log(`[importantVocab] qData:`, qData); + console.log(`[importantVocab] aData:`, aData); + if (qData && aData) { // Für Multiple Choice: Extrahiere Optionen und richtige Antwort if (this.getExerciseType(exercise) === 'multiple_choice') { @@ -317,22 +360,28 @@ export default { const correctIndex = aData.correctAnswer !== undefined ? aData.correctAnswer : (aData.correct || 0); const correctAnswer = options[correctIndex] || ''; + console.log(`[importantVocab] Multiple Choice - options:`, options, `correctIndex:`, correctIndex, `correctAnswer:`, correctAnswer); + if (correctAnswer) { // Versuche die Frage zu analysieren (z.B. "Wie sagt man 'X' auf Bisaya?" oder "Was bedeutet 'X'?") 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 let match = question.match(/['"]([^'"]+)['"]/); if (match) { const germanWord = match[1]; + console.log(`[importantVocab] Pattern 1 gefunden - Bisaya:`, correctAnswer, `Deutsch:`, germanWord); vocabMap.set(correctAnswer, { learning: correctAnswer, reference: germanWord }); } else { // Pattern 2: "Was bedeutet 'X'?" -> X ist Bisaya, correctAnswer ist Deutsch match = question.match(/Was bedeutet ['"]([^'"]+)['"]/); if (match) { const bisayaWord = match[1]; + console.log(`[importantVocab] Pattern 2 gefunden - Bisaya:`, bisayaWord, `Deutsch:`, correctAnswer); vocabMap.set(bisayaWord, { learning: bisayaWord, reference: correctAnswer }); } else { + console.log(`[importantVocab] Kein Pattern gefunden, Fallback`); // Fallback: Verwende die richtige Antwort als Lernwort vocabMap.set(correctAnswer, { learning: correctAnswer, reference: correctAnswer }); } @@ -343,6 +392,7 @@ export default { // Für Gap Fill: Extrahiere richtige Antworten if (this.getExerciseType(exercise) === 'gap_fill') { 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) { // Versuche aus dem Text Kontext zu extrahieren 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) { console.error('Fehler in importantVocab computed property:', e); return []; @@ -522,11 +574,72 @@ export default { const res = await apiClient.post(`/api/vocab/grammar-exercises/${exerciseId}/check`, { answer }); this.exerciseResults[exerciseId] = res.data; + + // Prüfe ob alle Übungen bestanden sind + await this.checkLessonCompletion(); } catch (e) { console.error('Fehler beim Prüfen der Antwort:', e); 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() { this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}`); }, @@ -535,52 +648,149 @@ export default { if (!this.importantVocab || this.importantVocab.length === 0) return; this.vocabTrainerActive = true; this.vocabTrainerPool = [...this.importantVocab]; + this.vocabTrainerMode = 'multiple_choice'; this.vocabTrainerCorrect = 0; this.vocabTrainerWrong = 0; + this.vocabTrainerTotalAttempts = 0; + this.vocabTrainerStats = {}; this.nextVocabQuestion(); - this.$nextTick(() => { - this.$refs.vocabInput?.focus?.(); - }); }, stopVocabTrainer() { this.vocabTrainerActive = false; this.currentVocabQuestion = null; this.vocabTrainerAnswer = ''; + this.vocabTrainerSelectedChoice = null; 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() { if (!this.vocabTrainerPool || this.vocabTrainerPool.length === 0) { this.currentVocabQuestion = null; 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 vocab = this.vocabTrainerPool[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 + answer: this.vocabTrainerDirection === 'L2R' ? vocab.reference : vocab.learning, + key: this.getVocabKey(vocab) }; + + // Reset UI this.vocabTrainerAnswer = ''; + this.vocabTrainerSelectedChoice = null; 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) { return String(s || '').trim().toLowerCase().replace(/\s+/g, ' '); }, checkVocabAnswer() { - if (!this.currentVocabQuestion || !this.vocabTrainerAnswer.trim()) return; - const userAnswer = this.normalizeVocab(this.vocabTrainerAnswer); - const correctAnswer = this.normalizeVocab(this.currentVocabQuestion.answer); - this.vocabTrainerLastCorrect = userAnswer === correctAnswer; + if (!this.currentVocabQuestion) return; + + let userAnswer = ''; + 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) { this.vocabTrainerCorrect++; + stats.correct++; } else { this.vocabTrainerWrong++; + stats.wrong++; } + 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() { @@ -921,15 +1131,48 @@ export default { } .vocab-trainer-stats { - display: flex; - justify-content: space-between; - align-items: center; margin-bottom: 15px; padding: 10px; background: #f5f5f5; 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 { padding: 5px 15px; background: #dc3545; @@ -969,9 +1212,46 @@ export default { } .vocab-answer-area { + margin-bottom: 15px; +} + +.vocab-answer-area.typing { display: flex; 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 { @@ -982,7 +1262,7 @@ export default { font-size: 1em; } -.vocab-answer-area button { +.btn-check { padding: 10px 20px; background: #4CAF50; color: white; @@ -992,11 +1272,11 @@ export default { font-size: 1em; } -.vocab-answer-area button:hover:not(:disabled) { +.btn-check:hover:not(:disabled) { background: #45a049; } -.vocab-answer-area button:disabled { +.btn-check:disabled { background: #ccc; cursor: not-allowed; }