- Added checks to ensure the exercises parameter is an array, with console warnings for null, undefined, or non-array inputs. - Implemented logic to convert non-array objects with a length property into arrays, improving flexibility in handling various input types. - Enhanced error logging to provide clearer insights when input conversion fails, aiding in debugging and user feedback.
1687 lines
56 KiB
Vue
1687 lines
56 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">({{ lesson?.grammarExercises?.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">
|
|
<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 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>
|
|
</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 -->
|
|
<div v-if="activeTab === 'exercises'" class="grammar-exercises">
|
|
<div v-if="lesson && lesson.grammarExercises && lesson.grammarExercises.length > 0">
|
|
<h3>{{ $t('socialnetwork.vocab.courses.grammarExercises') }}</h3>
|
|
<div v-for="exercise in lesson.grammarExercises" :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>
|
|
|
|
<!-- 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 && (!lesson.grammarExercises || lesson.grammarExercises.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'
|
|
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,
|
|
nextLessonId: null,
|
|
showCompletionDialog: false,
|
|
showErrorDialog: false,
|
|
errorMessage: ''
|
|
};
|
|
},
|
|
computed: {
|
|
...mapGetters(['user']),
|
|
hasExercises() {
|
|
if (!this.lesson) return false;
|
|
if (!this.lesson.grammarExercises) return false;
|
|
if (!Array.isArray(this.lesson.grammarExercises)) return false;
|
|
return this.lesson.grammarExercises.length > 0;
|
|
},
|
|
grammarExplanations() {
|
|
// Extrahiere Grammatik-Erklärungen aus den Übungen
|
|
try {
|
|
if (!this.lesson || !this.lesson.grammarExercises || !Array.isArray(this.lesson.grammarExercises)) {
|
|
return [];
|
|
}
|
|
|
|
const explanations = [];
|
|
const seen = new Set();
|
|
|
|
this.lesson.grammarExercises.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
|
|
if (this.lesson && (this.lesson.lessonType === 'review' || this.lesson.lessonType === 'vocab_review')) {
|
|
if (this.lesson.reviewVocabExercises && Array.isArray(this.lesson.reviewVocabExercises)) {
|
|
console.log('[importantVocab] Wiederholungslektion - verwende reviewVocabExercises:', this.lesson.reviewVocabExercises.length);
|
|
return this._extractVocabFromExercises(this.lesson.reviewVocabExercises);
|
|
} else {
|
|
console.log('[importantVocab] Wiederholungslektion aber keine reviewVocabExercises vorhanden');
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Normale Lektion: Verwende Übungen aus der aktuellen Lektion
|
|
if (!this.lesson || !this.lesson.grammarExercises || !Array.isArray(this.lesson.grammarExercises)) {
|
|
console.log('[importantVocab] Keine Übungen vorhanden');
|
|
return [];
|
|
}
|
|
|
|
return this._extractVocabFromExercises(this.lesson.grammarExercises);
|
|
} catch (e) {
|
|
console.error('Fehler in importantVocab computed property:', e);
|
|
return [];
|
|
}
|
|
},
|
|
_extractVocabFromExercises(exercises) {
|
|
// Sicherstellen, dass exercises ein Array ist
|
|
if (!exercises) {
|
|
console.warn('[_extractVocabFromExercises] exercises ist null/undefined:', exercises);
|
|
return [];
|
|
}
|
|
|
|
// Konvertiere zu Array falls nötig
|
|
let exercisesArray = exercises;
|
|
if (!Array.isArray(exercises)) {
|
|
console.warn('[_extractVocabFromExercises] exercises ist kein Array, versuche zu konvertieren:', exercises);
|
|
// Falls es ein Objekt mit length ist, versuche es zu konvertieren
|
|
if (typeof exercises === 'object' && exercises.length !== undefined) {
|
|
exercisesArray = Array.from(exercises);
|
|
} else {
|
|
console.error('[_extractVocabFromExercises] Kann exercises nicht zu Array konvertieren:', 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(/['"]([^'"]+)['"]/);
|
|
if (match) {
|
|
const nativeWord = match[1]; // Das Wort in der Muttersprache
|
|
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 {
|
|
// Pattern 2: "Was bedeutet 'X'?" -> X ist Bisaya, correctAnswer ist Muttersprache
|
|
match = question.match(/Was bedeutet ['"]([^'"]+)['"]/);
|
|
if (match) {
|
|
const bisayaWord = match[1];
|
|
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] 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]);
|
|
|
|
answers.forEach((answer, index) => {
|
|
if (answer && answer.trim()) {
|
|
const nativeWord = nativeWords[index] || answer; // Falls keine Klammer, verwende answer
|
|
// Die answer ist normalerweise Bisaya, nativeWord ist Muttersprache
|
|
vocabMap.set(`${nativeWord}-${answer}`, { learning: nativeWord, reference: answer });
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} 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;
|
|
}
|
|
},
|
|
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: {
|
|
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);
|
|
console.log('[VocabLessonView] Übungen:', this.lesson?.grammarExercises?.length || 0);
|
|
// Initialisiere Übungen aus der Lektion oder lade sie separat
|
|
if (this.lesson && this.lesson.id) {
|
|
if (this.lesson.grammarExercises && this.lesson.grammarExercises.length > 0) {
|
|
// Übungen sind bereits in der Lektion enthalten
|
|
console.log('[VocabLessonView] Übungen bereits in Lektion enthalten:', this.lesson.grammarExercises.length);
|
|
this.initializeExercises(this.lesson.grammarExercises);
|
|
} else {
|
|
// Lade Übungen separat (Fallback)
|
|
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'
|
|
};
|
|
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.lesson.grammarExercises.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();
|
|
}
|
|
|
|
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
|
|
if (!this.lesson || !this.lesson.grammarExercises) {
|
|
console.log('[VocabLessonView] checkLessonCompletion übersprungen - keine Lektion/Übungen');
|
|
return;
|
|
}
|
|
|
|
const allExercises = this.lesson.grammarExercises;
|
|
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.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.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, 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));
|
|
}
|
|
|
|
// Füge 3 Distraktoren hinzu
|
|
let attempts = 0;
|
|
const maxAttempts = 100; // Erhöht, da wir mehr Filter haben
|
|
|
|
while (options.size < 4 && allVocabs.length > 1 && attempts < maxAttempts) {
|
|
attempts++;
|
|
const randomVocab = allVocabs[Math.floor(Math.random() * allVocabs.length)];
|
|
const distractor = this.vocabTrainerDirection === 'L2R' ? randomVocab.reference : randomVocab.learning;
|
|
|
|
// Prüfe: Distraktor darf nicht gleich correctAnswer oder excludePrompt sein (mit Normalisierung)
|
|
if (distractor && distractor.trim()) {
|
|
const normalizedDistractor = this.normalizeVocab(distractor);
|
|
if (!normalizedExcludeSet.has(normalizedDistractor) && !options.has(distractor)) {
|
|
options.add(distractor);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Falls nicht genug Optionen gefunden wurden, füge generische Optionen hinzu
|
|
if (options.size < 4) {
|
|
const genericOptions = ['Option A', 'Option B', 'Option C', 'Option D'];
|
|
let index = 0;
|
|
while (options.size < 4 && index < genericOptions.length) {
|
|
const option = genericOptions[index];
|
|
if (!excludeSet.has(option)) {
|
|
options.add(option);
|
|
}
|
|
index++;
|
|
}
|
|
}
|
|
|
|
// 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' ? 2000 : 500; // 2 Sekunden 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 = '';
|
|
});
|
|
}
|
|
}
|
|
},
|
|
async mounted() {
|
|
await this.loadLesson();
|
|
}
|
|
};
|
|
</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;
|
|
gap: 10px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* 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: #F9A22C;
|
|
}
|
|
|
|
.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>
|