Files
yourpart3/frontend/src/views/social/VocabLessonView.vue

2158 lines
73 KiB
Vue

<template>
<div class="vocab-lesson-view">
<div v-if="loading">{{ $t('general.loading') }}</div>
<div v-else-if="lesson">
<div class="lesson-header">
<button @click="back" class="btn-back">{{ $t('general.back') }}</button>
<h2>{{ lesson.title }}</h2>
</div>
<p v-if="lesson.description" class="lesson-description">{{ lesson.description }}</p>
<!-- Tabs für Lernen und Übungen -->
<div class="lesson-tabs">
<button
:class="{ active: activeTab === 'learn' }"
@click="activeTab = 'learn'"
class="tab-button"
>
{{ $t('socialnetwork.vocab.courses.learn') }}
</button>
<button
:class="{ active: activeTab === 'exercises' }"
@click="activeTab = 'exercises'"
class="tab-button"
>
{{ $t('socialnetwork.vocab.courses.exercises') }}
<span v-if="hasExercises" class="exercise-count">({{ effectiveExercises?.length || 0 }})</span>
</button>
</div>
<!-- Lernen-Tab -->
<div v-if="activeTab === 'learn'" class="learn-section">
<h3>{{ $t('socialnetwork.vocab.courses.learnVocabulary') }}</h3>
<!-- Lektions-Beschreibung -->
<div v-if="lesson && lesson.description" class="lesson-description-box">
<h4>{{ $t('socialnetwork.vocab.courses.lessonDescription') }}</h4>
<p>{{ lesson.description }}</p>
</div>
<!-- Kulturelle Notizen -->
<div v-if="lesson && lesson.culturalNotes" class="cultural-notes">
<h4>{{ $t('socialnetwork.vocab.courses.culturalNotes') }}</h4>
<p>{{ lesson.culturalNotes }}</p>
</div>
<!-- Grammatik-Erklärungen -->
<div v-if="grammarExplanations && grammarExplanations.length > 0" class="grammar-explanations">
<h4>{{ $t('socialnetwork.vocab.courses.grammarExplanations') }}</h4>
<div v-for="(explanation, index) in grammarExplanations" :key="index" class="grammar-explanation-item">
<strong>{{ explanation.title }}</strong>
<p>{{ explanation.text }}</p>
</div>
</div>
<!-- Vokabeltrainer -->
<div v-if="importantVocab && importantVocab.length > 0" class="vocab-trainer-section">
<h4>{{ $t('socialnetwork.vocab.courses.vocabTrainer') }}</h4>
<div v-if="!vocabTrainerActive" class="vocab-trainer-start">
<p>{{ $t('socialnetwork.vocab.courses.vocabTrainerDescription') }}</p>
<button @click="startVocabTrainer" class="btn-start-trainer">
{{ $t('socialnetwork.vocab.courses.startVocabTrainer') }}
</button>
</div>
<div v-else class="vocab-trainer-active">
<div class="vocab-trainer-stats">
<div class="stats-row">
<span>{{ $t('socialnetwork.vocab.courses.correct') }}: {{ vocabTrainerCorrect }}</span>
<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 v-if="currentVocabQuestion" class="vocab-question">
<div class="vocab-prompt">
<div class="vocab-direction">{{ vocabTrainerDirection === 'L2R' ? $t('socialnetwork.vocab.courses.translateTo') : $t('socialnetwork.vocab.courses.translateFrom') }}</div>
<div class="vocab-word">{{ currentVocabQuestion.prompt }}</div>
</div>
<div v-if="vocabTrainerAnswered" class="vocab-feedback" :class="{ correct: vocabTrainerLastCorrect, wrong: !vocabTrainerLastCorrect }">
<div v-if="vocabTrainerLastCorrect">{{ $t('socialnetwork.vocab.courses.correct') }}!</div>
<div v-else>
{{ $t('socialnetwork.vocab.courses.wrong') }}. {{ $t('socialnetwork.vocab.courses.correctAnswer') }}: {{ currentVocabQuestion.answer }}
</div>
</div>
<!-- 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 entfernt: Prüfung erfolgt automatisch beim Klick auf Option -->
</div>
<!-- Texteingabe Modus -->
<div v-else class="vocab-answer-area typing">
<div v-if="vocabTrainerAutoSwitchedToTyping" class="mode-switch-notice">
<button @click="switchBackToMultipleChoice" class="btn-switch-mode">
{{ $t('socialnetwork.vocab.courses.switchBackToMultipleChoice') }}
</button>
</div>
<input
v-model="vocabTrainerAnswer"
@keydown.enter.prevent="checkVocabAnswer"
:placeholder="$t('socialnetwork.vocab.courses.enterAnswer')"
class="vocab-input"
ref="vocabInput"
/>
<button @click="checkVocabAnswer" :disabled="!vocabTrainerAnswer.trim()" class="btn-check">
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
</button>
</div>
<!-- "Weiter"-Button nur bei falscher Antwort (bei richtiger Antwort wird automatisch weiter gemacht) -->
<div v-if="vocabTrainerAnswered && !vocabTrainerLastCorrect" class="vocab-next">
<button @click="nextVocabQuestion">{{ $t('socialnetwork.vocab.courses.next') }}</button>
</div>
</div>
</div>
</div>
<!-- Wichtige Begriffe Liste (nur Anzeige) -->
<div v-if="lesson && importantVocab && importantVocab.length > 0 && !vocabTrainerActive" class="vocab-list">
<h4>{{ $t('socialnetwork.vocab.courses.importantVocab') }}</h4>
<p class="vocab-info-text">{{ $t('socialnetwork.vocab.courses.vocabInfoText') }}</p>
<div class="vocab-items">
<div v-for="(vocab, index) in importantVocab" :key="index" class="vocab-item">
<strong>{{ vocab.learning }}</strong>
<span class="separator"></span>
<span>{{ vocab.reference }}</span>
</div>
</div>
</div>
<!-- Hinweis wenn keine Vokabeln vorhanden -->
<div v-else-if="lesson && (!importantVocab || importantVocab.length === 0)" class="no-vocab-info">
<p>{{ $t('socialnetwork.vocab.courses.noVocabInfo') }}</p>
</div>
<!-- Button um zu Übungen zu wechseln -->
<div v-if="hasExercises" class="continue-to-exercises">
<button @click="activeTab = 'exercises'" class="btn-continue">
{{ $t('socialnetwork.vocab.courses.startExercises') }}
</button>
</div>
</div>
<!-- Übungen-Tab (Kapitel-Prüfung) -->
<div v-if="activeTab === 'exercises'" class="grammar-exercises">
<div v-if="lesson && effectiveExercises && effectiveExercises.length > 0">
<h3>{{ $t('socialnetwork.vocab.courses.grammarExercises') }}</h3>
<div v-for="exercise in effectiveExercises" :key="exercise.id" class="exercise-item">
<h4>{{ exercise.title }}</h4>
<p v-if="exercise.instruction" class="exercise-instruction">{{ exercise.instruction }}</p>
<!-- Multiple Choice Übung -->
<div v-if="getExerciseType(exercise) === 'multiple_choice'" class="multiple-choice-exercise">
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
<div class="options">
<label v-for="(option, index) in getOptions(exercise)" :key="index" class="option-label">
<input
type="radio"
:name="'exercise-' + exercise.id"
:value="index"
v-model="exerciseAnswers[exercise.id]"
/>
<span>{{ option }}</span>
</label>
</div>
<button @click="checkAnswer(exercise.id)" :disabled="!exerciseAnswers[exercise.id] && exerciseAnswers[exercise.id] !== 0">
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
</button>
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
<strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
<p v-if="!exerciseResults[exercise.id].correct && exerciseResults[exercise.id].correctAnswer" class="correct-answer">
{{ $t('socialnetwork.vocab.courses.correctAnswer') }}: {{ exerciseResults[exercise.id].correctAnswer }}
</p>
<p v-if="!exerciseResults[exercise.id].correct && exerciseResults[exercise.id].alternatives && exerciseResults[exercise.id].alternatives.length > 0" class="alternatives">
{{ $t('socialnetwork.vocab.courses.alternatives') }}: {{ exerciseResults[exercise.id].alternatives.join(', ') }}
</p>
<p v-if="exerciseResults[exercise.id].explanation" class="exercise-explanation">{{ exerciseResults[exercise.id].explanation }}</p>
</div>
</div>
<!-- Gap Fill Übung -->
<div v-else-if="getExerciseType(exercise) === 'gap_fill'" class="gap-fill-exercise">
<p class="exercise-text" v-html="formatGapFill(exercise)"></p>
<div class="gap-inputs">
<input
v-for="(gap, index) in getGapCount(exercise)"
:key="index"
v-model="exerciseAnswers[exercise.id][index]"
:placeholder="$t('socialnetwork.vocab.courses.enterAnswer')"
class="gap-input"
/>
</div>
<button @click="checkAnswer(exercise.id)" :disabled="!hasAllGapsFilled(exercise)">
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
</button>
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
<strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
<p v-if="!exerciseResults[exercise.id].correct && exerciseResults[exercise.id].correctAnswer" class="correct-answer">
{{ $t('socialnetwork.vocab.courses.correctAnswer') }}: {{ exerciseResults[exercise.id].correctAnswer }}
</p>
<p v-if="!exerciseResults[exercise.id].correct && exerciseResults[exercise.id].alternatives && exerciseResults[exercise.id].alternatives.length > 0" class="alternatives">
{{ $t('socialnetwork.vocab.courses.alternatives') }}: {{ exerciseResults[exercise.id].alternatives.join(', ') }}
</p>
<p v-if="exerciseResults[exercise.id].explanation" class="exercise-explanation">{{ exerciseResults[exercise.id].explanation }}</p>
</div>
</div>
<!-- Transformation Übung -->
<div v-else-if="getExerciseType(exercise) === 'transformation'" class="transformation-exercise">
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
<input
v-model="exerciseAnswers[exercise.id]"
:placeholder="$t('socialnetwork.vocab.courses.enterAnswer')"
class="transformation-input"
/>
<button @click="checkAnswer(exercise.id)" :disabled="!exerciseAnswers[exercise.id]">
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
</button>
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
<strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
<p v-if="!exerciseResults[exercise.id].correct && exerciseResults[exercise.id].correctAnswer" class="correct-answer">
{{ $t('socialnetwork.vocab.courses.correctAnswer') }}: {{ exerciseResults[exercise.id].correctAnswer }}
</p>
<p v-if="!exerciseResults[exercise.id].correct && exerciseResults[exercise.id].alternatives && exerciseResults[exercise.id].alternatives.length > 0" class="alternatives">
{{ $t('socialnetwork.vocab.courses.alternatives') }}: {{ exerciseResults[exercise.id].alternatives.join(', ') }}
</p>
<p v-if="exerciseResults[exercise.id].explanation" class="exercise-explanation">{{ exerciseResults[exercise.id].explanation }}</p>
</div>
</div>
<!-- Reading Aloud Übung -->
<div v-else-if="getExerciseType(exercise) === 'reading_aloud'" class="reading-aloud-exercise">
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
<p class="exercise-instruction">{{ $t('socialnetwork.vocab.courses.readingAloudInstruction') }}</p>
<div class="reading-aloud-controls">
<button
v-if="!isRecording(exercise.id)"
@click="startReadingAloud(exercise.id)"
class="btn-record"
:disabled="!isSpeechRecognitionSupported"
>
{{ $t('socialnetwork.vocab.courses.startRecording') }}
</button>
<button
v-else
@click="stopReadingAloud(exercise.id)"
class="btn-stop-record"
>
{{ $t('socialnetwork.vocab.courses.stopRecording') }}
</button>
</div>
<div v-if="recordingStatus[exercise.id]" class="recording-status" :class="{ 'recording': isRecording(exercise.id) }">
<span v-if="isRecording(exercise.id)">{{ $t('socialnetwork.vocab.courses.recording') }}...</span>
<span v-else>{{ recordingStatus[exercise.id] }}</span>
</div>
<div v-if="recognizedText[exercise.id]" class="recognized-text">
<strong>{{ $t('socialnetwork.vocab.courses.recognizedText') }}:</strong>
<p>{{ recognizedText[exercise.id] }}</p>
</div>
<button
v-if="recognizedText[exercise.id] && !isRecording(exercise.id)"
@click="checkAnswer(exercise.id)"
class="btn-check"
>
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
</button>
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
<strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
<p v-if="!exerciseResults[exercise.id].correct && exerciseResults[exercise.id].correctAnswer" class="correct-answer">
{{ $t('socialnetwork.vocab.courses.correctAnswer') }}: {{ exerciseResults[exercise.id].correctAnswer }}
</p>
<p v-if="exerciseResults[exercise.id].explanation" class="exercise-explanation">{{ exerciseResults[exercise.id].explanation }}</p>
</div>
<div v-if="!isSpeechRecognitionSupported" class="speech-not-supported">
<p>{{ $t('socialnetwork.vocab.courses.speechRecognitionNotSupported') }}</p>
</div>
</div>
<!-- Speaking From Memory Übung -->
<div v-else-if="getExerciseType(exercise) === 'speaking_from_memory'" class="speaking-from-memory-exercise">
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
<p class="exercise-instruction">{{ $t('socialnetwork.vocab.courses.speakingFromMemoryInstruction') }}</p>
<div v-if="getQuestionData(exercise)?.keywords" class="keywords-hint">
<strong>{{ $t('socialnetwork.vocab.courses.keywords') }}:</strong>
<span v-for="(keyword, idx) in getQuestionData(exercise).keywords" :key="idx" class="keyword-tag">{{ keyword }}</span>
</div>
<div class="speaking-controls">
<button
v-if="!isRecording(exercise.id)"
@click="startSpeakingFromMemory(exercise.id)"
class="btn-record"
:disabled="!isSpeechRecognitionSupported"
>
{{ $t('socialnetwork.vocab.courses.startSpeaking') }}
</button>
<button
v-else
@click="stopSpeakingFromMemory(exercise.id)"
class="btn-stop-record"
>
{{ $t('socialnetwork.vocab.courses.stopRecording') }}
</button>
</div>
<div v-if="recordingStatus[exercise.id]" class="recording-status" :class="{ 'recording': isRecording(exercise.id) }">
<span v-if="isRecording(exercise.id)">{{ $t('socialnetwork.vocab.courses.recording') }}...</span>
<span v-else>{{ recordingStatus[exercise.id] }}</span>
</div>
<div v-if="recognizedText[exercise.id]" class="recognized-text">
<strong>{{ $t('socialnetwork.vocab.courses.recognizedText') }}:</strong>
<p>{{ recognizedText[exercise.id] }}</p>
</div>
<button
v-if="recognizedText[exercise.id] && !isRecording(exercise.id)"
@click="checkAnswer(exercise.id)"
class="btn-check"
>
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
</button>
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'">
<strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
<p v-if="exerciseResults[exercise.id].explanation" class="exercise-explanation">{{ exerciseResults[exercise.id].explanation }}</p>
</div>
<div v-if="!isSpeechRecognitionSupported" class="speech-not-supported">
<p>{{ $t('socialnetwork.vocab.courses.speechRecognitionNotSupported') }}</p>
</div>
</div>
<!-- Fallback für unbekannte Typen -->
<div v-else class="unknown-exercise">
<p>Übungstyp: {{ getExerciseType(exercise) }}</p>
<pre>{{ JSON.stringify(exercise, null, 2) }}</pre>
</div>
</div>
</div>
<div v-else-if="lesson && (!effectiveExercises || effectiveExercises.length === 0)">
<p>{{ $t('socialnetwork.vocab.courses.noExercises') }}</p>
</div>
</div>
</div>
</div>
<!-- Dialog für Navigation zur nächsten Lektion -->
<div v-if="showNextLessonDialog" class="dialog-overlay" @click.self="cancelNavigateToNextLesson">
<div class="dialog" style="width: 400px; height: auto;">
<div class="dialog-header">
<span class="dialog-title">{{ $t('socialnetwork.vocab.courses.lessonCompleted') }}</span>
<span class="dialog-close" @click="cancelNavigateToNextLesson"></span>
</div>
<div class="dialog-body">
<p>{{ $t('socialnetwork.vocab.courses.goToNextLesson') }}</p>
</div>
<div class="dialog-footer">
<button @click="confirmNavigateToNextLesson" class="dialog-button">{{ $t('general.yes') }}</button>
<button @click="cancelNavigateToNextLesson" class="dialog-button">{{ $t('general.no') }}</button>
</div>
</div>
</div>
<!-- Dialog für Kurs-Abschluss -->
<div v-if="showCompletionDialog" class="dialog-overlay" @click.self="closeCompletionDialog">
<div class="dialog" style="width: 400px; height: auto;">
<div class="dialog-header">
<span class="dialog-title">{{ $t('socialnetwork.vocab.courses.allLessonsCompleted') }}</span>
<span class="dialog-close" @click="closeCompletionDialog"></span>
</div>
<div class="dialog-body">
<p>{{ $t('socialnetwork.vocab.courses.allLessonsCompleted') }}</p>
</div>
<div class="dialog-footer">
<button @click="closeCompletionDialog" class="dialog-button">{{ $t('general.ok') }}</button>
</div>
</div>
</div>
<!-- Dialog für Fehler -->
<div v-if="showErrorDialog" class="dialog-overlay" @click.self="closeErrorDialog">
<div class="dialog" style="width: 400px; height: auto;">
<div class="dialog-header">
<span class="dialog-title">{{ $t('error-title') }}</span>
<span class="dialog-close" @click="closeErrorDialog"></span>
</div>
<div class="dialog-body">
<p>{{ errorMessage }}</p>
</div>
<div class="dialog-footer">
<button @click="closeErrorDialog" class="dialog-button">{{ $t('general.ok') }}</button>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import apiClient from '@/utils/axios.js';
export default {
name: 'VocabLessonView',
props: {
courseId: {
type: String,
required: true
},
lessonId: {
type: String,
required: true
}
},
data() {
return {
loading: false,
lesson: null,
exerciseAnswers: {},
exerciseResults: {},
activeTab: 'learn', // Standardmäßig "Lernen"-Tab
// Vokabeltrainer
vocabTrainerActive: false,
vocabTrainerPool: [],
vocabTrainerMode: 'multiple_choice', // 'multiple_choice' oder 'typing'
vocabTrainerAutoSwitchedToTyping: false, // Track ob automatisch zu Typing gewechselt wurde
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
isCheckingLessonCompletion: false, // Flag um Endlosschleife zu verhindern
isNavigatingToNext: false, // Flag um mehrfache Navigation zu verhindern
showNextLessonDialog: false,
// Speech Recognition für Reading Aloud und Speaking From Memory
speechRecognition: null,
activeRecognition: {}, // { [exerciseId]: SpeechRecognition instance }
recognizedText: {}, // { [exerciseId]: string }
recordingStatus: {}, // { [exerciseId]: string }
isSpeechRecognitionSupported: false,
nextLessonId: null,
showCompletionDialog: false,
showErrorDialog: false,
errorMessage: ''
};
},
computed: {
...mapGetters(['user']),
hasExercises() {
const exercises = this.effectiveExercises;
return exercises && Array.isArray(exercises) && exercises.length > 0;
},
/** Für Wiederholungslektionen: Übungen aus vorherigen Lektionen (Kapitelprüfung). Sonst: eigene Grammatik-Übungen. */
effectiveExercises() {
if (!this.lesson) return [];
const isReview = this.lesson.lessonType === 'review' || this.lesson.lessonType === 'vocab_review';
if (isReview && this.lesson.reviewVocabExercises && Array.isArray(this.lesson.reviewVocabExercises) && this.lesson.reviewVocabExercises.length > 0) {
return this.lesson.reviewVocabExercises;
}
if (this.lesson.grammarExercises && Array.isArray(this.lesson.grammarExercises)) {
return this.lesson.grammarExercises;
}
return [];
},
grammarExplanations() {
// Extrahiere Grammatik-Erklärungen aus den Übungen
try {
const exercises = this.effectiveExercises;
if (!exercises || !Array.isArray(exercises) || exercises.length === 0) {
return [];
}
const explanations = [];
const seen = new Set();
exercises.forEach(exercise => {
if (exercise.explanation && !seen.has(exercise.explanation)) {
seen.add(exercise.explanation);
explanations.push({
title: exercise.title || '',
text: exercise.explanation
});
}
});
return explanations;
} catch (e) {
console.error('Fehler beim Extrahieren der Grammatik-Erklärungen:', e);
return [];
}
},
importantVocab() {
// Extrahiere wichtige Begriffe aus den Übungen
try {
// Bei Wiederholungslektionen: Verwende Vokabeln aus vorherigen Lektionen (effectiveExercises = reviewVocabExercises)
// Normale Lektion: Verwende effectiveExercises (grammarExercises)
const exercises = this.effectiveExercises;
if (!exercises || !Array.isArray(exercises) || exercises.length === 0) {
console.log('[importantVocab] Keine Übungen vorhanden');
return [];
}
return this._extractVocabFromExercises(exercises);
} catch (e) {
console.error('Fehler in importantVocab computed property:', e);
return [];
}
}
},
watch: {
courseId(newVal, oldVal) {
if (newVal !== oldVal) {
// Reset Flags beim Kurswechsel
this.isCheckingLessonCompletion = false;
this.isNavigatingToNext = false;
this.loadLesson();
}
},
lessonId(newVal, oldVal) {
if (newVal !== oldVal) {
// Reset Flags beim Lektionswechsel
this.isCheckingLessonCompletion = false;
this.isNavigatingToNext = false;
this.loadLesson();
}
}
},
methods: {
_extractVocabFromExercises(exercises) {
// Sicherstellen, dass exercises ein Array ist
if (!exercises) {
console.warn('[_extractVocabFromExercises] exercises ist null/undefined:', exercises);
return [];
}
// Konvertiere Vue Proxy oder Objekt zu Array
let exercisesArray = exercises;
// Prüfe ob es bereits ein Array ist (auch Vue Proxy-Arrays)
const isArrayLike = exercises.length !== undefined && typeof exercises.length === 'number';
if (!Array.isArray(exercises) && isArrayLike) {
console.log('[_extractVocabFromExercises] exercises ist kein Array, aber array-like, konvertiere...');
// Versuche Array.from (funktioniert mit iterierbaren Objekten und Array-like Objekten)
try {
exercisesArray = Array.from(exercises);
} catch (e) {
// Fallback: Manuelle Konvertierung
try {
exercisesArray = [];
for (let i = 0; i < exercises.length; i++) {
exercisesArray.push(exercises[i]);
}
} catch (e2) {
console.error('[_extractVocabFromExercises] Kann exercises nicht zu Array konvertieren:', exercises, e2);
return [];
}
}
} else if (!Array.isArray(exercises)) {
console.error('[_extractVocabFromExercises] exercises ist weder Array noch array-like:', exercises);
return [];
}
const vocabMap = new Map();
exercisesArray.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') {
const options = qData.options || [];
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 Muttersprache (z.B. "Großmutter"), correctAnswer ist Bisaya (z.B. "Lola")
let match = question.match(/Wie sagt man ['"]([^'"]+)['"]/i);
if (match) {
const nativeWord = match[1]; // Das Wort in der Muttersprache
// Nur hinzufügen, wenn Muttersprache und Bisaya unterschiedlich sind (verhindert "ko" -> "ko")
if (nativeWord && correctAnswer && nativeWord.trim() !== correctAnswer.trim()) {
console.log(`[importantVocab] Pattern 1 gefunden - Muttersprache:`, nativeWord, `Bisaya:`, correctAnswer);
// learning = Muttersprache (was man lernt), reference = Bisaya (Zielsprache)
vocabMap.set(`${nativeWord}-${correctAnswer}`, { learning: nativeWord, reference: correctAnswer });
} else {
console.log(`[importantVocab] Pattern 1 übersprungen - Muttersprache und Bisaya sind gleich:`, nativeWord, correctAnswer);
}
} else {
// Pattern 2: "Was bedeutet 'X'?" -> X ist Bisaya, correctAnswer ist Muttersprache
match = question.match(/Was bedeutet ['"]([^'"]+)['"]/i);
if (match) {
const bisayaWord = match[1];
// Nur hinzufügen, wenn Bisaya und Muttersprache unterschiedlich sind (verhindert "ko" -> "ko")
if (bisayaWord && correctAnswer && bisayaWord.trim() !== correctAnswer.trim()) {
console.log(`[importantVocab] Pattern 2 gefunden - Bisaya:`, bisayaWord, `Muttersprache:`, correctAnswer);
// learning = Muttersprache (was man lernt), reference = Bisaya (Zielsprache)
vocabMap.set(`${correctAnswer}-${bisayaWord}`, { learning: correctAnswer, reference: bisayaWord });
} else {
console.log(`[importantVocab] Pattern 2 übersprungen - Bisaya und Muttersprache sind gleich:`, bisayaWord, correctAnswer);
}
} else {
console.log(`[importantVocab] Kein Pattern gefunden, Überspringe diese Übung`);
// Überspringe, wenn wir die Richtung nicht erkennen können
}
}
}
}
// 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
// Gap Fill hat normalerweise Format: "{gap} (Muttersprache) | {gap} (Muttersprache) | ..."
const text = qData.text || '';
// Extrahiere Wörter in Klammern als Muttersprache
const matches = text.matchAll(/\(([^)]+)\)/g);
const nativeWords = Array.from(matches, m => m[1]);
console.log(`[importantVocab] Gap Fill - text:`, text, `nativeWords:`, nativeWords);
// Nur extrahieren, wenn Muttersprache-Hinweise (Klammern) vorhanden sind
if (nativeWords.length > 0) {
answers.forEach((answer, index) => {
if (answer && answer.trim()) {
const nativeWord = nativeWords[index];
if (nativeWord && nativeWord.trim() && nativeWord !== answer) {
// Die answer ist normalerweise Bisaya, nativeWord ist Muttersprache
// Nur hinzufügen, wenn sie unterschiedlich sind (verhindert "ko" -> "ko")
vocabMap.set(`${nativeWord}-${answer}`, { learning: nativeWord, reference: answer });
console.log(`[importantVocab] Gap Fill extrahiert - Muttersprache:`, nativeWord, `Bisaya:`, answer);
} else {
console.log(`[importantVocab] Gap Fill übersprungen - keine Muttersprache oder gleich:`, nativeWord, answer);
}
}
});
} else {
console.log(`[importantVocab] Gap Fill übersprungen - keine Muttersprache-Hinweise (Klammern) gefunden`);
}
}
}
}
} catch (e) {
console.warn('Fehler beim Extrahieren von Vokabeln aus Übung:', e, exercise);
}
});
const result = Array.from(vocabMap.values());
console.log(`[_extractVocabFromExercises] Ergebnis:`, result.length, 'Vokabeln');
return result;
},
async loadLesson() {
// Verhindere mehrfaches Laden
if (this.loading) {
console.log('[VocabLessonView] loadLesson übersprungen - bereits am Laden');
return;
}
console.log('[VocabLessonView] loadLesson gestartet für lessonId:', this.lessonId);
this.loading = true;
// Setze Antworten und Ergebnisse zurück
this.exerciseAnswers = {};
this.exerciseResults = {};
// Reset Flags
this.isCheckingLessonCompletion = false;
this.isNavigatingToNext = false;
// Prüfe ob 'tab' Query-Parameter vorhanden ist (für Navigation zur nächsten Lektion)
const tabParam = this.$route.query.tab;
if (tabParam === 'learn') {
this.activeTab = 'learn';
}
try {
const res = await apiClient.get(`/api/vocab/lessons/${this.lessonId}`);
this.lesson = res.data;
console.log('[VocabLessonView] Geladene Lektion:', this.lesson?.id, this.lesson?.title);
// Initialisiere mit effectiveExercises (für Review: reviewVocabExercises, sonst: grammarExercises)
this.$nextTick(async () => {
const exercises = this.effectiveExercises;
if (exercises && exercises.length > 0) {
console.log('[VocabLessonView] Übungen für Kapitel-Prüfung:', exercises.length);
this.initializeExercises(exercises);
} else {
console.log('[VocabLessonView] Lade Übungen separat...');
await this.loadGrammarExercises();
}
});
console.log('[VocabLessonView] loadLesson abgeschlossen');
} catch (e) {
console.error('[VocabLessonView] Fehler beim Laden der Lektion:', e);
} finally {
this.loading = false;
}
},
initializeExercises(exercises) {
// Initialisiere Antwort-Arrays für Gap Fill Übungen
exercises.forEach(exercise => {
const exerciseType = this.getExerciseType(exercise);
if (exerciseType === 'gap_fill') {
const gapCount = this.getGapCount(exercise);
this.exerciseAnswers[exercise.id] = new Array(gapCount).fill('');
} else {
this.exerciseAnswers[exercise.id] = '';
}
this.exerciseResults[exercise.id] = null;
});
},
async loadGrammarExercises() {
try {
const res = await apiClient.get(`/api/vocab/lessons/${this.lessonId}/grammar-exercises`);
const exercises = res.data || [];
this.lesson.grammarExercises = exercises;
this.initializeExercises(exercises);
} catch (e) {
console.error('Konnte Grammatik-Übungen nicht laden:', e);
this.lesson.grammarExercises = [];
}
},
getExerciseType(exercise) {
// Hole den Typ aus questionData oder exerciseType
if (exercise.questionData) {
const qData = typeof exercise.questionData === 'string'
? JSON.parse(exercise.questionData)
: exercise.questionData;
return qData.type;
}
// Fallback: Prüfe exerciseTypeId
const typeMap = {
1: 'gap_fill',
2: 'multiple_choice',
3: 'sentence_building',
4: 'transformation',
5: 'conjugation',
6: 'declension',
7: 'reading_aloud',
8: 'speaking_from_memory'
};
return typeMap[exercise.exerciseTypeId] || 'unknown';
},
getQuestionData(exercise) {
if (!exercise.questionData) return null;
return typeof exercise.questionData === 'string'
? JSON.parse(exercise.questionData)
: exercise.questionData;
},
getAnswerData(exercise) {
if (!exercise.answerData) return null;
return typeof exercise.answerData === 'string'
? JSON.parse(exercise.answerData)
: exercise.answerData;
},
getQuestionText(exercise) {
const qData = this.getQuestionData(exercise);
if (!qData) return exercise.title;
if (qData.question) return qData.question;
// Für Transformation-Übungen: Formatiere als "Übersetze 'X' ins Bisaya"
if (qData.type === 'transformation' && qData.text) {
const sourceLang = qData.sourceLanguage || 'Deutsch';
const targetLang = qData.targetLanguage || 'Bisaya';
return `Übersetze "${qData.text}" ins ${targetLang}`;
}
if (qData.text) return qData.text;
return exercise.title;
},
getOptions(exercise) {
const qData = this.getQuestionData(exercise);
return qData?.options || [];
},
formatGapFill(exercise) {
const qData = this.getQuestionData(exercise);
if (!qData || !qData.text) return '';
// Ersetze {gap} mit Platzhaltern
return qData.text.replace(/\{gap\}/g, '<span class="gap">_____</span>');
},
getGapCount(exercise) {
const qData = this.getQuestionData(exercise);
if (!qData) return 0;
// Zähle {gap} im Text
const matches = qData.text?.match(/\{gap\}/g);
return matches ? matches.length : (qData.gaps || 1);
},
hasAllGapsFilled(exercise) {
const answers = this.exerciseAnswers[exercise.id];
if (!answers || !Array.isArray(answers)) return false;
const gapCount = this.getGapCount(exercise);
return answers.length === gapCount && answers.every(a => a && a.trim());
},
async checkAnswer(exerciseId) {
try {
const exercise = this.effectiveExercises.find(e => e.id === exerciseId);
if (!exercise) return;
const exerciseType = this.getExerciseType(exercise);
let answer = this.exerciseAnswers[exerciseId];
// Formatiere Antwort je nach Typ
if (exerciseType === 'gap_fill') {
// Gap Fill: Array von Antworten
if (!Array.isArray(answer)) {
answer = [answer];
}
} else if (exerciseType === 'multiple_choice') {
// Multiple Choice: Index als Zahl
answer = Number(answer);
} else if (exerciseType === 'transformation') {
// Transformation: String
answer = String(answer || '').trim();
} else if (exerciseType === 'reading_aloud' || exerciseType === 'speaking_from_memory') {
// Reading Aloud / Speaking From Memory: Verwende erkannten Text
answer = this.recognizedText[exerciseId] || String(answer || '').trim();
}
const res = await apiClient.post(`/api/vocab/grammar-exercises/${exerciseId}/check`, { answer });
this.exerciseResults[exerciseId] = res.data;
// Prüfe ob alle Übungen bestanden sind (mit Verzögerung, um mehrfache Aufrufe zu vermeiden)
this.$nextTick(() => {
this.checkLessonCompletion();
});
} catch (e) {
console.error('Fehler beim Prüfen der Antwort:', e);
this.showErrorDialog = true;
this.errorMessage = e.response?.data?.error || 'Fehler beim Prüfen der Antwort';
}
},
async checkLessonCompletion() {
// Verhindere mehrfache Ausführung
if (this.isCheckingLessonCompletion || this.isNavigatingToNext) {
console.log('[VocabLessonView] checkLessonCompletion übersprungen - bereits in Ausführung');
return;
}
// Prüfe ob alle Übungen korrekt beantwortet wurden (effectiveExercises = Kapitel-Prüfung)
const allExercises = this.effectiveExercises;
if (!this.lesson || !allExercises || allExercises.length === 0) {
console.log('[VocabLessonView] checkLessonCompletion übersprungen - keine Lektion/Übungen');
return;
}
if (allExercises.length === 0) {
console.log('[VocabLessonView] checkLessonCompletion übersprungen - keine Übungen');
return;
}
// Prüfe ob alle Übungen korrekt beantwortet wurden
const allCompleted = allExercises.every(exercise => {
const result = this.exerciseResults[exercise.id];
return result && result.correct;
});
console.log('[VocabLessonView] checkLessonCompletion - allCompleted:', allCompleted, 'Übungen:', allExercises.length, 'Korrekt:', allExercises.filter(ex => this.exerciseResults[ex.id]?.correct).length);
if (allCompleted && !this.isCheckingLessonCompletion) {
this.isCheckingLessonCompletion = true;
console.log('[VocabLessonView] Alle Übungen abgeschlossen - starte Fortschritts-Update');
try {
// 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);
console.log('[VocabLessonView] Score berechnet:', score, '%');
// Aktualisiere Fortschritt
await apiClient.put(`/api/vocab/lessons/${this.lessonId}/progress`, {
completed: true,
score: score,
timeSpentMinutes: 0 // TODO: Zeit tracken
});
console.log('[VocabLessonView] Fortschritt aktualisiert - starte Navigation');
// Weiterleitung zur nächsten Lektion
await this.navigateToNextLesson();
} catch (e) {
console.error('[VocabLessonView] Fehler beim Aktualisieren des Fortschritts:', e);
this.isCheckingLessonCompletion = false;
}
}
},
async navigateToNextLesson() {
// Verhindere mehrfache Navigation
if (this.isNavigatingToNext) {
console.log('[VocabLessonView] Navigation bereits in Gang, überspringe...');
return;
}
this.isNavigatingToNext = true;
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) {
console.log('[VocabLessonView] Keine Lektionen gefunden');
this.isNavigatingToNext = false;
this.isCheckingLessonCompletion = false;
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];
console.log('[VocabLessonView] Nächste Lektion gefunden:', nextLesson.id);
// Zeige Erfolgs-Meldung und leite weiter
// Verwende Dialog statt confirm
this.showNextLessonDialog = true;
this.nextLessonId = nextLesson.id;
} else {
// Letzte Lektion - zeige Abschluss-Meldung
console.log('[VocabLessonView] Letzte Lektion erreicht');
this.showCompletionDialog = true;
this.isNavigatingToNext = false;
this.isCheckingLessonCompletion = false;
}
} catch (e) {
console.error('Fehler beim Laden der nächsten Lektion:', e);
this.isNavigatingToNext = false;
this.isCheckingLessonCompletion = false;
}
},
back() {
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}`);
},
confirmNavigateToNextLesson() {
if (this.nextLessonId) {
console.log('[VocabLessonView] Navigiere zur nächsten Lektion:', this.nextLessonId);
// Setze Flags zurück BEVOR die Navigation stattfindet
this.isNavigatingToNext = false;
this.isCheckingLessonCompletion = false;
this.showNextLessonDialog = false;
// Verwende replace statt push, um die History nicht zu belasten
// Setze activeTab auf 'learn' für die nächste Lektion via Query-Parameter
this.$router.replace(`/socialnetwork/vocab/courses/${this.courseId}/lessons/${this.nextLessonId}?tab=learn`);
}
},
cancelNavigateToNextLesson() {
console.log('[VocabLessonView] Navigation abgebrochen');
this.isNavigatingToNext = false;
this.isCheckingLessonCompletion = false;
this.showNextLessonDialog = false;
this.nextLessonId = null;
},
closeCompletionDialog() {
this.showCompletionDialog = false;
},
closeErrorDialog() {
this.showErrorDialog = false;
this.errorMessage = '';
},
// Vokabeltrainer-Methoden
startVocabTrainer() {
console.log('[VocabLessonView] startVocabTrainer aufgerufen');
if (!this.importantVocab || this.importantVocab.length === 0) {
console.log('[VocabLessonView] Keine Vokabeln vorhanden');
return;
}
console.log('[VocabLessonView] Vokabeln gefunden:', this.importantVocab.length);
this.vocabTrainerActive = true;
this.vocabTrainerPool = [...this.importantVocab];
this.vocabTrainerMode = 'multiple_choice';
this.vocabTrainerAutoSwitchedToTyping = false;
this.vocabTrainerCorrect = 0;
this.vocabTrainerWrong = 0;
this.vocabTrainerTotalAttempts = 0;
this.vocabTrainerStats = {};
console.log('[VocabLessonView] Rufe nextVocabQuestion auf');
this.$nextTick(() => {
this.nextVocabQuestion();
});
},
stopVocabTrainer() {
this.vocabTrainerActive = false;
this.vocabTrainerMode = 'multiple_choice';
this.vocabTrainerAutoSwitchedToTyping = 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';
this.vocabTrainerAutoSwitchedToTyping = true; // Markiere als automatisch gewechselt
// Reset Stats für Texteingabe-Modus
this.vocabTrainerCorrect = 0;
this.vocabTrainerWrong = 0;
this.vocabTrainerTotalAttempts = 0;
}
}
},
switchBackToMultipleChoice() {
// Wechsle zurück zu Multiple Choice
this.vocabTrainerMode = 'multiple_choice';
this.vocabTrainerAutoSwitchedToTyping = false;
// Reset Stats für Multiple Choice Modus
this.vocabTrainerCorrect = 0;
this.vocabTrainerWrong = 0;
this.vocabTrainerTotalAttempts = 0;
// Starte neue Frage im Multiple Choice Modus
this.nextVocabQuestion();
},
buildChoiceOptions(correctAnswer, allVocabs, excludePrompt = null) {
const options = new Set([correctAnswer]);
// Normalisiere alle Werte für den Vergleich (trim + lowercase)
const normalizedExcludeSet = new Set();
normalizedExcludeSet.add(this.normalizeVocab(correctAnswer));
// Wichtig: Der Prompt (die Frage) darf nicht in den Optionen erscheinen
if (excludePrompt) {
normalizedExcludeSet.add(this.normalizeVocab(excludePrompt));
}
// Sammle alle verfügbaren Distraktoren aus beiden Richtungen
const allDistractors = [];
allVocabs.forEach(vocab => {
// Füge beide Richtungen hinzu (learning und reference)
if (vocab.learning && vocab.learning.trim()) {
const normalized = this.normalizeVocab(vocab.learning);
if (!normalizedExcludeSet.has(normalized)) {
allDistractors.push(vocab.learning);
}
}
if (vocab.reference && vocab.reference.trim()) {
const normalized = this.normalizeVocab(vocab.reference);
if (!normalizedExcludeSet.has(normalized)) {
allDistractors.push(vocab.reference);
}
}
});
// Entferne Duplikate
const uniqueDistractors = [...new Set(allDistractors)];
// Füge Distraktoren hinzu, bis wir 4 Optionen haben
let attempts = 0;
const maxAttempts = 200;
while (options.size < 4 && uniqueDistractors.length > 0 && attempts < maxAttempts) {
attempts++;
const randomIndex = Math.floor(Math.random() * uniqueDistractors.length);
const distractor = uniqueDistractors[randomIndex];
if (distractor && distractor.trim()) {
const normalizedDistractor = this.normalizeVocab(distractor);
if (!normalizedExcludeSet.has(normalizedDistractor) && !options.has(distractor)) {
options.add(distractor);
}
}
}
// Falls immer noch nicht genug Optionen: Verwende auch die andere Richtung der aktuellen Vokabeln
if (options.size < 4 && allVocabs.length > 0) {
allVocabs.forEach(vocab => {
if (options.size >= 4) return;
// Verwende die andere Richtung als Distraktor
const otherDirection = this.vocabTrainerDirection === 'L2R' ? vocab.learning : vocab.reference;
if (otherDirection && otherDirection.trim()) {
const normalized = this.normalizeVocab(otherDirection);
if (!normalizedExcludeSet.has(normalized) && !options.has(otherDirection)) {
options.add(otherDirection);
}
}
});
}
// Falls immer noch nicht genug Optionen: Verwende weniger Optionen (mindestens 2)
if (options.size < 2) {
console.warn('[buildChoiceOptions] Nicht genug Optionen gefunden, verwende nur', options.size, 'Optionen');
}
// 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() {
console.log('[VocabLessonView] nextVocabQuestion aufgerufen');
if (!this.vocabTrainerPool || this.vocabTrainerPool.length === 0) {
console.log('[VocabLessonView] Keine Vokabeln im Pool');
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,
key: this.getVocabKey(vocab)
};
console.log('[VocabLessonView] Neue Frage erstellt:', this.currentVocabQuestion.prompt);
// Reset UI
this.vocabTrainerAnswer = '';
this.vocabTrainerSelectedChoice = null;
this.vocabTrainerAnswered = false;
// Erstelle Choice-Optionen für Multiple Choice
if (this.vocabTrainerMode === 'multiple_choice') {
console.log('[VocabLessonView] Erstelle Choice-Optionen...');
console.log('[VocabLessonView] Prompt:', this.currentVocabQuestion.prompt);
console.log('[VocabLessonView] Answer:', this.currentVocabQuestion.answer);
// Wichtig: Der Prompt (die Frage) darf nicht in den Optionen erscheinen
this.vocabTrainerChoiceOptions = this.buildChoiceOptions(
this.currentVocabQuestion.answer,
this.vocabTrainerPool,
this.currentVocabQuestion.prompt // Exkludiere den Prompt
);
console.log('[VocabLessonView] Choice-Optionen erstellt:', this.vocabTrainerChoiceOptions);
}
// Fokussiere Eingabefeld im Typing-Modus
if (this.vocabTrainerMode === 'typing') {
this.$nextTick(() => {
this.$refs.vocabInput?.focus?.();
});
}
},
selectVocabChoice(option) {
this.vocabTrainerSelectedChoice = option;
// Bei Multiple Choice: Sofort prüfen
if (this.vocabTrainerMode === 'multiple_choice') {
this.checkVocabAnswer();
}
},
normalizeVocab(s) {
return String(s || '').trim().toLowerCase().replace(/\s+/g, ' ');
},
checkVocabAnswer() {
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;
// Automatisch zur nächsten Frage nach kurzer Pause (nur bei richtiger Antwort)
if (this.vocabTrainerLastCorrect) {
// Prüfe ob noch Fragen vorhanden sind
if (this.vocabTrainerPool && this.vocabTrainerPool.length > 0) {
const delay = this.vocabTrainerMode === 'multiple_choice' ? 1000 : 500; // 1 Sekunde für Multiple Choice, 500ms für Typing
setTimeout(() => {
// Prüfe erneut, ob noch Fragen vorhanden sind (könnte sich geändert haben)
if (this.vocabTrainerPool && this.vocabTrainerPool.length > 0 && this.vocabTrainerActive) {
this.nextVocabQuestion();
}
}, delay);
}
}
// 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 = '';
});
}
},
initSpeechRecognition() {
// Prüfe Browser-Support für Speech Recognition
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
this.isSpeechRecognitionSupported = false;
console.warn('Speech Recognition wird von diesem Browser nicht unterstützt');
return;
}
this.isSpeechRecognitionSupported = true;
},
isRecording(exerciseId) {
return !!this.activeRecognition[exerciseId];
},
startReadingAloud(exerciseId) {
const exercise = this.effectiveExercises.find(e => e.id === exerciseId);
if (!exercise) return;
const qData = this.getQuestionData(exercise);
const expectedText = qData.text || '';
this.startRecognition(exerciseId, expectedText);
},
stopReadingAloud(exerciseId) {
this.stopRecognition(exerciseId);
},
startSpeakingFromMemory(exerciseId) {
const exercise = this.effectiveExercises.find(e => e.id === exerciseId);
if (!exercise) return;
const qData = this.getQuestionData(exercise);
const expectedText = qData.expectedText || qData.text || '';
this.startRecognition(exerciseId, expectedText);
},
stopSpeakingFromMemory(exerciseId) {
this.stopRecognition(exerciseId);
},
startRecognition(exerciseId, expectedText) {
if (!this.isSpeechRecognitionSupported) {
this.recordingStatus[exerciseId] = this.$t('socialnetwork.vocab.courses.speechRecognitionNotSupported');
return;
}
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
// Konfiguriere Recognition
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = 'de-DE'; // Kann später dynamisch basierend auf Kurs-Sprache gesetzt werden
let finalTranscript = '';
recognition.onresult = (event) => {
let interimTranscript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
finalTranscript += transcript + ' ';
} else {
interimTranscript += transcript;
}
}
// Aktualisiere erkannten Text
this.recognizedText[exerciseId] = finalTranscript.trim() || interimTranscript;
this.recordingStatus[exerciseId] = this.$t('socialnetwork.vocab.courses.listening');
};
recognition.onerror = (event) => {
console.error('Speech Recognition Fehler:', event.error);
this.recordingStatus[exerciseId] = this.$t('socialnetwork.vocab.courses.recordingError') + ': ' + event.error;
this.stopRecognition(exerciseId);
};
recognition.onend = () => {
// Speichere finalen Text in exerciseAnswers
if (finalTranscript.trim()) {
this.exerciseAnswers[exerciseId] = finalTranscript.trim();
}
this.activeRecognition[exerciseId] = null;
this.recordingStatus[exerciseId] = this.$t('socialnetwork.vocab.courses.recordingStopped');
};
// Starte Recognition
try {
recognition.start();
this.activeRecognition[exerciseId] = recognition;
this.recordingStatus[exerciseId] = this.$t('socialnetwork.vocab.courses.recording');
this.recognizedText[exerciseId] = '';
} catch (error) {
console.error('Fehler beim Starten der Speech Recognition:', error);
this.recordingStatus[exerciseId] = this.$t('socialnetwork.vocab.courses.recordingError') + ': ' + error.message;
}
},
stopRecognition(exerciseId) {
if (this.activeRecognition[exerciseId]) {
try {
this.activeRecognition[exerciseId].stop();
} catch (error) {
console.error('Fehler beim Stoppen der Speech Recognition:', error);
}
this.activeRecognition[exerciseId] = null;
}
}
},
async mounted() {
// Prüfe Speech Recognition Support
this.initSpeechRecognition();
await this.loadLesson();
},
beforeUnmount() {
// Stoppe alle aktiven Recognition-Instanzen
Object.keys(this.activeRecognition).forEach(exerciseId => {
this.stopRecognition(exerciseId);
});
}
};
</script>
<style scoped>
.vocab-lesson-view {
padding: 20px;
}
.lesson-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.btn-back {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
}
.lesson-description {
color: #666;
margin-bottom: 20px;
}
.grammar-exercises {
margin-top: 30px;
}
.exercise-item {
background: white;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 15px;
}
.exercise-instruction {
color: #666;
font-style: italic;
margin-bottom: 15px;
}
.exercise-question {
font-weight: 600;
margin-bottom: 15px;
font-size: 1.1em;
}
.exercise-text {
margin-bottom: 15px;
line-height: 1.6;
}
.gap {
display: inline-block;
min-width: 100px;
border-bottom: 2px solid #333;
margin: 0 5px;
padding: 0 5px;
}
.multiple-choice-exercise .options {
margin: 15px 0;
}
.option-label {
display: block;
padding: 10px;
margin: 8px 0;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.option-label:hover {
background-color: #f5f5f5;
}
.option-label input[type="radio"] {
margin-right: 10px;
}
.gap-fill-exercise .gap-inputs {
display: flex;
flex-direction: column;
gap: 10px;
margin: 15px 0;
}
.gap-input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.transformation-input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
margin: 15px 0;
}
.exercise-item button {
padding: 10px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
margin-top: 10px;
transition: background-color 0.2s;
}
.exercise-item button:hover:not(:disabled) {
background: #45a049;
}
.exercise-item button:disabled {
background: #ccc;
cursor: not-allowed;
}
.exercise-result {
margin-top: 15px;
padding: 15px;
border-radius: 4px;
}
.exercise-result.correct {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.exercise-result.wrong {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.exercise-explanation {
margin-top: 10px;
font-style: italic;
}
.unknown-exercise {
padding: 15px;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
margin-top: 10px;
}
.unknown-exercise pre {
margin-top: 10px;
font-size: 0.85em;
overflow-x: auto;
}
/* Tabs */
.lesson-tabs {
display: flex;
gap: 10px;
margin: 20px 0;
border-bottom: 2px solid #ddd;
}
.tab-button {
padding: 10px 20px;
border: none;
background: transparent;
cursor: pointer;
font-size: 1em;
color: #666;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: all 0.2s;
}
.tab-button:hover:not(:disabled) {
color: #333;
background: #f5f5f5;
}
.tab-button.active {
color: #007bff;
border-bottom-color: #007bff;
font-weight: bold;
}
.tab-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Lernen-Sektion */
.learn-section {
margin-top: 20px;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
}
.learn-section h3 {
margin-top: 0;
color: #333;
}
.cultural-notes {
margin: 20px 0;
padding: 15px;
background: #e7f3ff;
border-left: 4px solid #007bff;
border-radius: 4px;
}
.cultural-notes h4 {
margin-top: 0;
color: #007bff;
}
.vocab-list {
margin: 20px 0;
}
.vocab-list h4 {
margin-bottom: 15px;
color: #333;
}
.vocab-items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 10px;
}
.vocab-item {
padding: 10px;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
display: flex;
align-items: center;
gap: 10px;
}
.vocab-item strong {
color: #007bff;
}
.separator {
color: #999;
margin: 0 10px;
}
.lesson-description-box {
margin: 20px 0;
padding: 15px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
}
.lesson-description-box h4 {
margin-top: 0;
color: #333;
}
.grammar-explanations {
margin: 20px 0;
padding: 15px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
}
.grammar-explanations h4 {
margin-top: 0;
color: #856404;
}
.grammar-explanation-item {
margin: 15px 0;
padding: 10px;
background: white;
border-radius: 4px;
}
.grammar-explanation-item strong {
display: block;
margin-bottom: 5px;
color: #856404;
}
.vocab-trainer-section {
margin: 20px 0;
padding: 15px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
}
.vocab-trainer-section h4 {
margin-top: 0;
color: #333;
}
.vocab-trainer-start {
text-align: center;
}
.btn-start-trainer {
padding: 10px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
margin-top: 10px;
}
.btn-start-trainer:hover {
background: #45a049;
}
.vocab-trainer-stats {
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;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.btn-stop-trainer:hover {
background: #c82333;
}
.vocab-question {
margin-top: 15px;
}
.vocab-prompt {
padding: 15px;
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 15px;
}
.vocab-direction {
color: #666;
font-size: 0.9em;
margin-bottom: 5px;
}
.vocab-word {
font-size: 1.5em;
font-weight: bold;
color: #333;
}
.vocab-answer-area {
margin-bottom: 15px;
}
.vocab-answer-area.typing {
display: flex;
flex-direction: column;
gap: 10px;
}
.mode-switch-notice {
margin-bottom: 10px;
padding: 10px;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
text-align: center;
}
.btn-switch-mode {
padding: 8px 16px;
background: var(--color-primary-orange);
color: #000000;
border: 1px solid var(--color-primary-orange);
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
font-weight: 500;
transition: background 0.05s;
}
.btn-switch-mode:hover {
background: var(--color-primary-orange-light);
color: var(--color-text-secondary);
border: 1px solid var(--color-text-secondary);
}
.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 {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1em;
}
.btn-check {
padding: 10px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
}
.btn-check:hover:not(:disabled) {
background: #45a049;
}
.btn-check:disabled {
background: #ccc;
cursor: not-allowed;
}
.vocab-feedback {
padding: 15px;
border-radius: 4px;
margin-bottom: 15px;
}
.vocab-feedback.correct {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.vocab-feedback.wrong {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.vocab-next {
margin-top: 15px;
}
.vocab-next button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
}
.vocab-next button:hover {
background: #0056b3;
}
.vocab-info-text {
color: #666;
font-size: 0.9em;
margin-bottom: 10px;
}
.no-vocab-info {
padding: 15px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
color: #856404;
}
.continue-to-exercises {
margin-top: 30px;
text-align: center;
}
.btn-continue {
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 1.1em;
cursor: pointer;
transition: background 0.2s;
}
.btn-continue:hover {
background: #0056b3;
}
/* Reading Aloud & Speaking From Memory Styles */
.reading-aloud-exercise,
.speaking-from-memory-exercise {
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 20px;
}
.reading-aloud-controls,
.speaking-controls {
margin: 20px 0;
display: flex;
gap: 10px;
align-items: center;
}
.btn-record,
.btn-stop-record {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 1em;
cursor: pointer;
transition: all 0.2s;
}
.btn-record {
background: #28a745;
color: white;
}
.btn-record:hover:not(:disabled) {
background: #218838;
}
.btn-record:disabled {
background: #6c757d;
cursor: not-allowed;
}
.btn-stop-record {
background: #dc3545;
color: white;
}
.btn-stop-record:hover {
background: #c82333;
}
.btn-check {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
margin-top: 15px;
}
.btn-check:hover {
background: #0056b3;
}
.recording-status {
margin: 15px 0;
padding: 10px;
border-radius: 4px;
font-weight: 500;
}
.recording-status.recording {
background: #fff3cd;
color: #856404;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.recognized-text {
margin: 20px 0;
padding: 15px;
background: white;
border: 2px solid #dee2e6;
border-radius: 6px;
}
.recognized-text strong {
display: block;
margin-bottom: 10px;
color: #495057;
}
.recognized-text p {
margin: 0;
font-size: 1.1em;
color: #212529;
line-height: 1.6;
}
.keywords-hint {
margin: 15px 0;
padding: 12px;
background: #e7f3ff;
border-left: 4px solid #007bff;
border-radius: 4px;
}
.keywords-hint strong {
display: block;
margin-bottom: 8px;
color: #0056b3;
}
.keyword-tag {
display: inline-block;
padding: 4px 10px;
margin: 4px 4px 0 0;
background: #007bff;
color: white;
border-radius: 12px;
font-size: 0.9em;
}
.speech-not-supported {
margin-top: 15px;
padding: 12px;
background: #f8d7da;
border-left: 4px solid #dc3545;
border-radius: 4px;
color: #721c24;
}
/* Dialog Styles */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
}
.dialog {
background: white;
display: flex;
flex-direction: column;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
max-width: 90%;
max-height: 90%;
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 20px;
border-bottom: 1px solid #ddd;
background-color: var(--color-primary-orange);
}
.dialog-title {
flex-grow: 1;
font-size: 1.2em;
font-weight: bold;
}
.dialog-close {
cursor: pointer;
font-size: 1.5em;
margin-left: 10px;
color: #000;
}
.dialog-body {
padding: 20px;
overflow-y: auto;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 10px 20px;
border-top: 1px solid #ddd;
}
.dialog-button {
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
}
.dialog-button:hover {
background: #0056b3;
}
</style>