Files
yourpart3/frontend/src/views/social/VocabLessonView.vue
Torsten Schulz (local) cf97a3ba5e Enhance _extractVocabFromExercises method for robust input handling
- 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.
2026-01-20 00:00:57 +01:00

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>