Enhance VocabLessonView and VocabService with new learning features

- Added a tabbed interface in VocabLessonView for 'Learn' and 'Exercises' sections, improving user navigation.
- Implemented logic to display important vocabulary and cultural notes in the learning section.
- Updated exercise result display to include correct answers and alternatives for better user feedback.
- Enhanced VocabService to extract correct answers and alternatives from exercise data, supporting the new UI features.
- Added new translations for vocabulary-related terms in both English and German, ensuring consistency across the application.
This commit is contained in:
Torsten Schulz (local)
2026-01-19 16:41:10 +01:00
parent 305e137a1a
commit 196b74bebb
4 changed files with 278 additions and 7 deletions

View File

@@ -1310,8 +1310,20 @@ export default class VocabService {
await progress.save(); await progress.save();
} }
// Extrahiere richtige Antwort und Alternativen
const answerData = typeof exercise.answerData === 'string'
? JSON.parse(exercise.answerData)
: exercise.answerData;
const correctAnswer = Array.isArray(answerData.correct)
? answerData.correct[0]
: answerData.correct;
const alternatives = answerData.alternatives || [];
return { return {
correct: isCorrect, correct: isCorrect,
correctAnswer: correctAnswer,
alternatives: alternatives,
explanation: exercise.explanation, explanation: exercise.explanation,
progress: progress.get({ plain: true }) progress: progress.get({ plain: true })
}; };

View File

@@ -366,7 +366,16 @@
"checkAnswer": "Antwort prüfen", "checkAnswer": "Antwort prüfen",
"correct": "Richtig!", "correct": "Richtig!",
"wrong": "Falsch", "wrong": "Falsch",
"explanation": "Erklärung" "explanation": "Erklärung",
"learn": "Lernen",
"exercises": "Übungen",
"learnVocabulary": "Vokabeln lernen",
"culturalNotes": "Kulturelle Notizen",
"importantVocab": "Wichtige Begriffe",
"noVocabInfo": "Lies die Beschreibung oben und die Erklärungen in den Übungen, um die wichtigsten Begriffe zu lernen.",
"startExercises": "Zu den Übungen",
"correctAnswer": "Richtige Antwort",
"alternatives": "Alternative Antworten"
} }
} }
} }

View File

@@ -366,7 +366,16 @@
"checkAnswer": "Check Answer", "checkAnswer": "Check Answer",
"correct": "Correct!", "correct": "Correct!",
"wrong": "Wrong", "wrong": "Wrong",
"explanation": "Explanation" "explanation": "Explanation",
"learn": "Learn",
"exercises": "Exercises",
"learnVocabulary": "Learn Vocabulary",
"culturalNotes": "Cultural Notes",
"importantVocab": "Important Vocabulary",
"noVocabInfo": "Read the description above and the explanations in the exercises to learn the most important terms.",
"startExercises": "Start Exercises",
"correctAnswer": "Correct Answer",
"alternatives": "Alternative Answers"
} }
} }
} }

View File

@@ -9,7 +9,62 @@
<p v-if="lesson.description" class="lesson-description">{{ lesson.description }}</p> <p v-if="lesson.description" class="lesson-description">{{ lesson.description }}</p>
<div v-if="lesson.grammarExercises && lesson.grammarExercises.length > 0" class="grammar-exercises"> <!-- 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"
:disabled="!hasExercises"
>
{{ $t('socialnetwork.vocab.courses.exercises') }}
</button>
</div>
<!-- Lernen-Tab -->
<div v-if="activeTab === 'learn'" class="learn-section">
<h3>{{ $t('socialnetwork.vocab.courses.learnVocabulary') }}</h3>
<!-- Kulturelle Notizen -->
<div v-if="lesson.culturalNotes" class="cultural-notes">
<h4>{{ $t('socialnetwork.vocab.courses.culturalNotes') }}</h4>
<p>{{ lesson.culturalNotes }}</p>
</div>
<!-- Wichtige Begriffe aus den Übungen -->
<div v-if="importantVocab.length > 0" class="vocab-list">
<h4>{{ $t('socialnetwork.vocab.courses.importantVocab') }}</h4>
<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 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' && lesson.grammarExercises && lesson.grammarExercises.length > 0" class="grammar-exercises">
<h3>{{ $t('socialnetwork.vocab.courses.grammarExercises') }}</h3> <h3>{{ $t('socialnetwork.vocab.courses.grammarExercises') }}</h3>
<div v-for="exercise in lesson.grammarExercises" :key="exercise.id" class="exercise-item"> <div v-for="exercise in lesson.grammarExercises" :key="exercise.id" class="exercise-item">
<h4>{{ exercise.title }}</h4> <h4>{{ exercise.title }}</h4>
@@ -34,7 +89,13 @@
</button> </button>
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'"> <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> <strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
<p v-if="exercise.explanation" class="exercise-explanation">{{ exercise.explanation }}</p> <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>
</div> </div>
@@ -55,7 +116,13 @@
</button> </button>
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'"> <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> <strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
<p v-if="exercise.explanation" class="exercise-explanation">{{ exercise.explanation }}</p> <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>
</div> </div>
@@ -72,7 +139,13 @@
</button> </button>
<div v-if="exerciseResults[exercise.id]" class="exercise-result" :class="exerciseResults[exercise.id].correct ? 'correct' : 'wrong'"> <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> <strong>{{ exerciseResults[exercise.id].correct ? $t('socialnetwork.vocab.courses.correct') : $t('socialnetwork.vocab.courses.wrong') }}</strong>
<p v-if="exercise.explanation" class="exercise-explanation">{{ exercise.explanation }}</p> <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>
</div> </div>
@@ -112,9 +185,54 @@ export default {
loading: false, loading: false,
lesson: null, lesson: null,
exerciseAnswers: {}, exerciseAnswers: {},
exerciseResults: {} exerciseResults: {},
activeTab: 'learn' // Standardmäßig "Lernen"-Tab
}; };
}, },
computed: {
...mapGetters(['user']),
hasExercises() {
return this.lesson && this.lesson.grammarExercises && this.lesson.grammarExercises.length > 0;
},
importantVocab() {
// Extrahiere wichtige Begriffe aus den Übungen
if (!this.lesson || !this.lesson.grammarExercises) return [];
const vocabMap = new Map();
this.lesson.grammarExercises.forEach(exercise => {
// Extrahiere aus questionData
const qData = this.getQuestionData(exercise);
const aData = this.getAnswerData(exercise);
if (qData && aData) {
// Für Multiple Choice: Extrahiere Optionen und richtige Antwort
if (this.getExerciseType(exercise) === 'multiple_choice') {
const correct = Array.isArray(aData.correct) ? aData.correct[0] : aData.correct;
const question = qData.text || '';
// Versuche die Frage zu analysieren (z.B. "Wie sagt man X auf Bisaya?")
const match = question.match(/['"]([^'"]+)['"]/);
if (match) {
const germanWord = match[1];
vocabMap.set(correct, { learning: correct, reference: germanWord });
} else if (correct) {
// Fallback: Verwende die richtige Antwort als Lernwort
vocabMap.set(correct, { learning: correct, reference: correct });
}
}
// Für Gap Fill: Extrahiere richtige Antworten
if (this.getExerciseType(exercise) === 'gap_fill' && aData.correct) {
const correct = Array.isArray(aData.correct) ? aData.correct[0] : aData.correct;
vocabMap.set(correct, { learning: correct, reference: correct });
}
}
});
return Array.from(vocabMap.values());
}
},
computed: { computed: {
...mapGetters(['user']) ...mapGetters(['user'])
}, },
@@ -441,4 +559,127 @@ export default {
font-size: 0.85em; font-size: 0.85em;
overflow-x: auto; 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;
}
.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> </style>