Enhance VocabLessonView with vocabulary trainer and grammar explanations

- Introduced a vocabulary trainer feature allowing users to practice important vocabulary interactively, with options to start and stop the trainer.
- Added sections for grammar explanations and lesson descriptions to improve user understanding of the content.
- Updated translations in both English and German to reflect changes in vocabulary and exercise terminology.
- Enhanced conditional rendering to ensure proper display of vocabulary and grammar information based on lesson data.
This commit is contained in:
Torsten Schulz (local)
2026-01-19 20:58:39 +01:00
parent dacf6cb7f8
commit 95ba8f0b33
3 changed files with 377 additions and 16 deletions

View File

@@ -360,21 +360,30 @@
"optional": "Optional",
"invalidCode": "Ungültiger Code",
"courseNotFound": "Kurs nicht gefunden",
"grammarExercises": "Grammatik-Übungen",
"noExercises": "Keine Übungen verfügbar",
"grammarExercises": "Grammatik-Prüfung",
"noExercises": "Keine Prüfung verfügbar",
"enterAnswer": "Antwort eingeben",
"checkAnswer": "Antwort prüfen",
"correct": "Richtig!",
"wrong": "Falsch",
"explanation": "Erklärung",
"learn": "Lernen",
"exercises": "Übungen",
"exercises": "Kapitel-Prüfung",
"learnVocabulary": "Vokabeln lernen",
"lessonDescription": "Lektions-Beschreibung",
"culturalNotes": "Kulturelle Notizen",
"grammarExplanations": "Grammatik-Erklärungen",
"importantVocab": "Wichtige Begriffe",
"vocabInfoText": "Diese Begriffe werden in den Übungen verwendet. Lerne sie hier passiv, bevor du zu den interaktiven Übungen wechselst.",
"noVocabInfo": "Lies die Beschreibung oben und die Erklärungen in den Übungen, um die wichtigsten Begriffe zu lernen.",
"startExercises": "Zu den Übungen",
"vocabInfoText": "Diese Begriffe werden in der Prüfung verwendet. Lerne sie hier passiv, bevor du zur Kapitel-Prüfung wechselst.",
"noVocabInfo": "Lies die Beschreibung oben und die Erklärungen in der Prüfung, um die wichtigsten Begriffe zu lernen.",
"vocabTrainer": "Vokabeltrainer",
"vocabTrainerDescription": "Übe die wichtigsten Begriffe dieser Lektion interaktiv.",
"startVocabTrainer": "Vokabeltrainer starten",
"stopTrainer": "Trainer beenden",
"translateTo": "Übersetze ins Deutsche",
"translateFrom": "Übersetze ins Bisaya",
"next": "Weiter",
"startExercises": "Zur Kapitel-Prüfung",
"correctAnswer": "Richtige Antwort",
"alternatives": "Alternative Antworten"
}

View File

@@ -360,21 +360,30 @@
"optional": "Optional",
"invalidCode": "Invalid code",
"courseNotFound": "Course not found",
"grammarExercises": "Grammar Exercises",
"noExercises": "No exercises available",
"grammarExercises": "Chapter Test",
"noExercises": "No test available",
"enterAnswer": "Enter answer",
"checkAnswer": "Check Answer",
"correct": "Correct!",
"wrong": "Wrong",
"explanation": "Explanation",
"learn": "Learn",
"exercises": "Exercises",
"exercises": "Chapter Test",
"learnVocabulary": "Learn Vocabulary",
"lessonDescription": "Lesson Description",
"culturalNotes": "Cultural Notes",
"grammarExplanations": "Grammar Explanations",
"importantVocab": "Important Vocabulary",
"vocabInfoText": "These terms are used in the exercises. Learn them here passively before switching to the interactive exercises.",
"noVocabInfo": "Read the description above and the explanations in the exercises to learn the most important terms.",
"startExercises": "Start Exercises",
"vocabInfoText": "These terms are used in the test. Learn them here passively before switching to the chapter test.",
"noVocabInfo": "Read the description above and the explanations in the test to learn the most important terms.",
"vocabTrainer": "Vocabulary Trainer",
"vocabTrainerDescription": "Practice the most important terms of this lesson interactively.",
"startVocabTrainer": "Start Vocabulary Trainer",
"stopTrainer": "Stop Trainer",
"translateTo": "Translate to English",
"translateFrom": "Translate to Target Language",
"next": "Next",
"startExercises": "Start Chapter Test",
"correctAnswer": "Correct Answer",
"alternatives": "Alternative Answers"
}

View File

@@ -32,14 +32,74 @@
<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>
<!-- Wichtige Begriffe aus den Übungen (nur Anzeige, keine Interaktion) -->
<div v-if="lesson && importantVocab && importantVocab.length > 0" class="vocab-list">
<!-- 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">
<span>{{ $t('socialnetwork.vocab.courses.correct') }}: {{ vocabTrainerCorrect }}</span>
<span>{{ $t('socialnetwork.vocab.courses.wrong') }}: {{ vocabTrainerWrong }}</span>
<button @click="stopVocabTrainer" class="btn-stop-trainer">{{ $t('socialnetwork.vocab.courses.stopTrainer') }}</button>
</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>
<div v-else class="vocab-answer-area">
<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()">
{{ $t('socialnetwork.vocab.courses.checkAnswer') }}
</button>
</div>
<div v-if="vocabTrainerAnswered" 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">
@@ -52,7 +112,7 @@
</div>
<!-- Hinweis wenn keine Vokabeln vorhanden -->
<div v-else-if="lesson" class="no-vocab-info">
<div v-else-if="lesson && (!importantVocab || importantVocab.length === 0)" class="no-vocab-info">
<p>{{ $t('socialnetwork.vocab.courses.noVocabInfo') }}</p>
</div>
@@ -188,7 +248,17 @@ export default {
lesson: null,
exerciseAnswers: {},
exerciseResults: {},
activeTab: 'learn' // Standardmäßig "Lernen"-Tab
activeTab: 'learn', // Standardmäßig "Lernen"-Tab
// Vokabeltrainer
vocabTrainerActive: false,
vocabTrainerPool: [],
vocabTrainerCorrect: 0,
vocabTrainerWrong: 0,
currentVocabQuestion: null,
vocabTrainerAnswer: '',
vocabTrainerAnswered: false,
vocabTrainerLastCorrect: false,
vocabTrainerDirection: 'L2R' // L2R: learning->reference, R2L: reference->learning
};
},
computed: {
@@ -196,6 +266,32 @@ export default {
hasExercises() {
return this.lesson && this.lesson.grammarExercises && 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 {
@@ -408,6 +504,58 @@ export default {
},
back() {
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}`);
},
// Vokabeltrainer-Methoden
startVocabTrainer() {
if (!this.importantVocab || this.importantVocab.length === 0) return;
this.vocabTrainerActive = true;
this.vocabTrainerPool = [...this.importantVocab];
this.vocabTrainerCorrect = 0;
this.vocabTrainerWrong = 0;
this.nextVocabQuestion();
this.$nextTick(() => {
this.$refs.vocabInput?.focus?.();
});
},
stopVocabTrainer() {
this.vocabTrainerActive = false;
this.currentVocabQuestion = null;
this.vocabTrainerAnswer = '';
this.vocabTrainerAnswered = false;
},
nextVocabQuestion() {
if (!this.vocabTrainerPool || this.vocabTrainerPool.length === 0) {
this.currentVocabQuestion = null;
return;
}
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
};
this.vocabTrainerAnswer = '';
this.vocabTrainerAnswered = false;
this.$nextTick(() => {
this.$refs.vocabInput?.focus?.();
});
},
normalizeVocab(s) {
return String(s || '').trim().toLowerCase().replace(/\s+/g, ' ');
},
checkVocabAnswer() {
if (!this.currentVocabQuestion || !this.vocabTrainerAnswer.trim()) return;
const userAnswer = this.normalizeVocab(this.vocabTrainerAnswer);
const correctAnswer = this.normalizeVocab(this.currentVocabQuestion.answer);
this.vocabTrainerLastCorrect = userAnswer === correctAnswer;
if (this.vocabTrainerLastCorrect) {
this.vocabTrainerCorrect++;
} else {
this.vocabTrainerWrong++;
}
this.vocabTrainerAnswered = true;
}
},
async mounted() {
@@ -673,6 +821,201 @@ export default {
.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 {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
.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 {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.vocab-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1em;
}
.vocab-answer-area button {
padding: 10px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
}
.vocab-answer-area button:hover:not(:disabled) {
background: #45a049;
}
.vocab-answer-area button: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 {