- Added formatting for transformation exercises to display prompts as "Übersetze 'X' ins Bisaya", improving clarity for users. - Updated the buildChoiceOptions method to exclude the prompt from the choice options, ensuring a more relevant selection for multiple choice questions. - Enhanced console logging to provide better insights into the prompt and answer during choice option creation, aiding in debugging and user feedback.
1496 lines
49 KiB
Vue
1496 lines
49 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 @click="checkVocabAnswer" :disabled="!vocabTrainerSelectedChoice" class="btn-check">
|
|
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
|
|
</button>
|
|
</div>
|
|
<!-- Texteingabe Modus -->
|
|
<div v-else class="vocab-answer-area typing">
|
|
<input
|
|
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>
|
|
</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
|
|
};
|
|
},
|
|
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 {
|
|
if (!this.lesson || !this.lesson.grammarExercises || !Array.isArray(this.lesson.grammarExercises)) {
|
|
console.log('[importantVocab] Keine Übungen vorhanden');
|
|
return [];
|
|
}
|
|
|
|
const vocabMap = new Map();
|
|
|
|
this.lesson.grammarExercises.forEach((exercise, 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 Deutsch, correctAnswer ist Bisaya
|
|
let match = question.match(/['"]([^'"]+)['"]/);
|
|
if (match) {
|
|
const germanWord = match[1];
|
|
console.log(`[importantVocab] Pattern 1 gefunden - Bisaya:`, correctAnswer, `Deutsch:`, germanWord);
|
|
vocabMap.set(correctAnswer, { learning: correctAnswer, reference: germanWord });
|
|
} else {
|
|
// Pattern 2: "Was bedeutet 'X'?" -> X ist Bisaya, correctAnswer ist Deutsch
|
|
match = question.match(/Was bedeutet ['"]([^'"]+)['"]/);
|
|
if (match) {
|
|
const bisayaWord = match[1];
|
|
console.log(`[importantVocab] Pattern 2 gefunden - Bisaya:`, bisayaWord, `Deutsch:`, correctAnswer);
|
|
vocabMap.set(bisayaWord, { learning: bisayaWord, reference: correctAnswer });
|
|
} else {
|
|
console.log(`[importantVocab] Kein Pattern gefunden, Fallback`);
|
|
// Fallback: Verwende die richtige Antwort als Lernwort
|
|
vocabMap.set(correctAnswer, { learning: correctAnswer, reference: correctAnswer });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Für Gap Fill: Extrahiere richtige Antworten
|
|
if (this.getExerciseType(exercise) === 'gap_fill') {
|
|
const answers = aData.answers || (aData.correct ? (Array.isArray(aData.correct) ? aData.correct : [aData.correct]) : []);
|
|
console.log(`[importantVocab] Gap Fill - answers:`, answers);
|
|
if (answers.length > 0) {
|
|
// Versuche aus dem Text Kontext zu extrahieren
|
|
const text = qData.text || '';
|
|
answers.forEach((answer, index) => {
|
|
if (answer && answer.trim()) {
|
|
// Erstelle einen eindeutigen Key
|
|
const key = `${answer}-${index}`;
|
|
vocabMap.set(key, { learning: answer, reference: answer });
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Fehler beim Extrahieren von Vokabeln aus Übung:', e, exercise);
|
|
}
|
|
});
|
|
|
|
const result = Array.from(vocabMap.values());
|
|
console.log(`[importantVocab] Ergebnis:`, result);
|
|
return result;
|
|
} catch (e) {
|
|
console.error('Fehler in importantVocab computed property:', e);
|
|
return [];
|
|
}
|
|
}
|
|
},
|
|
watch: {
|
|
courseId(newVal, oldVal) {
|
|
if (newVal !== oldVal) {
|
|
// Reset Flags beim Kurswechsel
|
|
this.isCheckingLessonCompletion = false;
|
|
this.isNavigatingToNext = false;
|
|
this.loadLesson();
|
|
}
|
|
},
|
|
lessonId(newVal, oldVal) {
|
|
if (newVal !== oldVal) {
|
|
// Reset Flags beim Lektionswechsel
|
|
this.isCheckingLessonCompletion = false;
|
|
this.isNavigatingToNext = false;
|
|
this.loadLesson();
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
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;
|
|
|
|
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);
|
|
alert(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
|
|
const shouldNavigate = confirm(this.$t('socialnetwork.vocab.courses.lessonCompleted') + '\n' + this.$t('socialnetwork.vocab.courses.goToNextLesson'));
|
|
|
|
if (shouldNavigate) {
|
|
console.log('[VocabLessonView] Navigiere zur nächsten Lektion:', nextLesson.id);
|
|
// Setze Flags zurück BEVOR die Navigation stattfindet
|
|
this.isNavigatingToNext = false;
|
|
this.isCheckingLessonCompletion = false;
|
|
// Verwende replace statt push, um die History nicht zu belasten
|
|
this.$router.replace(`/socialnetwork/vocab/courses/${this.courseId}/lessons/${nextLesson.id}`);
|
|
} else {
|
|
// Benutzer hat abgebrochen - Flag zurücksetzen
|
|
console.log('[VocabLessonView] Navigation abgebrochen');
|
|
this.isNavigatingToNext = false;
|
|
this.isCheckingLessonCompletion = false;
|
|
}
|
|
} else {
|
|
// Letzte Lektion - zeige Abschluss-Meldung
|
|
console.log('[VocabLessonView] Letzte Lektion erreicht');
|
|
alert(this.$t('socialnetwork.vocab.courses.allLessonsCompleted'));
|
|
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}`);
|
|
},
|
|
// 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;
|
|
},
|
|
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;
|
|
|
|
// Im Typing-Modus: Automatisch zur nächsten Frage nach kurzer Pause (nur bei richtiger Antwort)
|
|
if (this.vocabTrainerMode === 'typing' && this.vocabTrainerLastCorrect) {
|
|
// Prüfe ob noch Fragen vorhanden sind
|
|
if (this.vocabTrainerPool && this.vocabTrainerPool.length > 0) {
|
|
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();
|
|
}
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
// Im Typing-Modus bei falscher Antwort: Eingabefeld fokussieren für erneuten Versuch
|
|
if (this.vocabTrainerMode === 'typing' && !this.vocabTrainerLastCorrect) {
|
|
this.$nextTick(() => {
|
|
this.$refs.vocabInput?.focus?.();
|
|
this.vocabTrainerAnswer = '';
|
|
});
|
|
}
|
|
}
|
|
},
|
|
async mounted() {
|
|
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;
|
|
}
|
|
</style>
|