Enhance exercise generation for family conversations and feelings & affection
- Updated multiple choice exercises to include randomized wrong options for improved engagement and challenge. - Added new exercise types for reading aloud and speaking from memory, enhancing interactive learning experiences. - Improved gap fill exercises with clearer instructions and multiple variants for better user understanding. - Enhanced the vocabulary service to support new exercise types, ensuring robust answer checking and feedback mechanisms. - Updated localization files to include new instructions and messages related to the new exercise types.
This commit is contained in:
@@ -242,6 +242,103 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reading Aloud Übung -->
|
||||
<div v-else-if="getExerciseType(exercise) === 'reading_aloud'" class="reading-aloud-exercise">
|
||||
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
|
||||
<p class="exercise-instruction">{{ $t('socialnetwork.vocab.courses.readingAloudInstruction') }}</p>
|
||||
<div class="reading-aloud-controls">
|
||||
<button
|
||||
v-if="!isRecording(exercise.id)"
|
||||
@click="startReadingAloud(exercise.id)"
|
||||
class="btn-record"
|
||||
:disabled="!isSpeechRecognitionSupported"
|
||||
>
|
||||
{{ $t('socialnetwork.vocab.courses.startRecording') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
@click="stopReadingAloud(exercise.id)"
|
||||
class="btn-stop-record"
|
||||
>
|
||||
{{ $t('socialnetwork.vocab.courses.stopRecording') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="recordingStatus[exercise.id]" class="recording-status" :class="{ 'recording': isRecording(exercise.id) }">
|
||||
<span v-if="isRecording(exercise.id)">{{ $t('socialnetwork.vocab.courses.recording') }}...</span>
|
||||
<span v-else>{{ recordingStatus[exercise.id] }}</span>
|
||||
</div>
|
||||
<div v-if="recognizedText[exercise.id]" class="recognized-text">
|
||||
<strong>{{ $t('socialnetwork.vocab.courses.recognizedText') }}:</strong>
|
||||
<p>{{ recognizedText[exercise.id] }}</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="recognizedText[exercise.id] && !isRecording(exercise.id)"
|
||||
@click="checkAnswer(exercise.id)"
|
||||
class="btn-check"
|
||||
>
|
||||
{{ $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].explanation" class="exercise-explanation">{{ exerciseResults[exercise.id].explanation }}</p>
|
||||
</div>
|
||||
<div v-if="!isSpeechRecognitionSupported" class="speech-not-supported">
|
||||
<p>{{ $t('socialnetwork.vocab.courses.speechRecognitionNotSupported') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Speaking From Memory Übung -->
|
||||
<div v-else-if="getExerciseType(exercise) === 'speaking_from_memory'" class="speaking-from-memory-exercise">
|
||||
<p class="exercise-question">{{ getQuestionText(exercise) }}</p>
|
||||
<p class="exercise-instruction">{{ $t('socialnetwork.vocab.courses.speakingFromMemoryInstruction') }}</p>
|
||||
<div v-if="getQuestionData(exercise)?.keywords" class="keywords-hint">
|
||||
<strong>{{ $t('socialnetwork.vocab.courses.keywords') }}:</strong>
|
||||
<span v-for="(keyword, idx) in getQuestionData(exercise).keywords" :key="idx" class="keyword-tag">{{ keyword }}</span>
|
||||
</div>
|
||||
<div class="speaking-controls">
|
||||
<button
|
||||
v-if="!isRecording(exercise.id)"
|
||||
@click="startSpeakingFromMemory(exercise.id)"
|
||||
class="btn-record"
|
||||
:disabled="!isSpeechRecognitionSupported"
|
||||
>
|
||||
{{ $t('socialnetwork.vocab.courses.startSpeaking') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
@click="stopSpeakingFromMemory(exercise.id)"
|
||||
class="btn-stop-record"
|
||||
>
|
||||
{{ $t('socialnetwork.vocab.courses.stopRecording') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="recordingStatus[exercise.id]" class="recording-status" :class="{ 'recording': isRecording(exercise.id) }">
|
||||
<span v-if="isRecording(exercise.id)">{{ $t('socialnetwork.vocab.courses.recording') }}...</span>
|
||||
<span v-else>{{ recordingStatus[exercise.id] }}</span>
|
||||
</div>
|
||||
<div v-if="recognizedText[exercise.id]" class="recognized-text">
|
||||
<strong>{{ $t('socialnetwork.vocab.courses.recognizedText') }}:</strong>
|
||||
<p>{{ recognizedText[exercise.id] }}</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="recognizedText[exercise.id] && !isRecording(exercise.id)"
|
||||
@click="checkAnswer(exercise.id)"
|
||||
class="btn-check"
|
||||
>
|
||||
{{ $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].explanation" class="exercise-explanation">{{ exerciseResults[exercise.id].explanation }}</p>
|
||||
</div>
|
||||
<div v-if="!isSpeechRecognitionSupported" class="speech-not-supported">
|
||||
<p>{{ $t('socialnetwork.vocab.courses.speechRecognitionNotSupported') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fallback für unbekannte Typen -->
|
||||
<div v-else class="unknown-exercise">
|
||||
<p>Übungstyp: {{ getExerciseType(exercise) }}</p>
|
||||
@@ -347,6 +444,12 @@ export default {
|
||||
isCheckingLessonCompletion: false, // Flag um Endlosschleife zu verhindern
|
||||
isNavigatingToNext: false, // Flag um mehrfache Navigation zu verhindern
|
||||
showNextLessonDialog: false,
|
||||
// Speech Recognition für Reading Aloud und Speaking From Memory
|
||||
speechRecognition: null,
|
||||
activeRecognition: {}, // { [exerciseId]: SpeechRecognition instance }
|
||||
recognizedText: {}, // { [exerciseId]: string }
|
||||
recordingStatus: {}, // { [exerciseId]: string }
|
||||
isSpeechRecognitionSupported: false,
|
||||
nextLessonId: null,
|
||||
showCompletionDialog: false,
|
||||
showErrorDialog: false,
|
||||
@@ -633,7 +736,9 @@ export default {
|
||||
3: 'sentence_building',
|
||||
4: 'transformation',
|
||||
5: 'conjugation',
|
||||
6: 'declension'
|
||||
6: 'declension',
|
||||
7: 'reading_aloud',
|
||||
8: 'speaking_from_memory'
|
||||
};
|
||||
return typeMap[exercise.exerciseTypeId] || 'unknown';
|
||||
},
|
||||
@@ -710,6 +815,9 @@ export default {
|
||||
} else if (exerciseType === 'transformation') {
|
||||
// Transformation: String
|
||||
answer = String(answer || '').trim();
|
||||
} else if (exerciseType === 'reading_aloud' || exerciseType === 'speaking_from_memory') {
|
||||
// Reading Aloud / Speaking From Memory: Verwende erkannten Text
|
||||
answer = this.recognizedText[exerciseId] || String(answer || '').trim();
|
||||
}
|
||||
|
||||
const res = await apiClient.post(`/api/vocab/grammar-exercises/${exerciseId}/check`, { answer });
|
||||
@@ -1066,10 +1174,124 @@ export default {
|
||||
this.vocabTrainerAnswer = '';
|
||||
});
|
||||
}
|
||||
},
|
||||
initSpeechRecognition() {
|
||||
// Prüfe Browser-Support für Speech Recognition
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
if (!SpeechRecognition) {
|
||||
this.isSpeechRecognitionSupported = false;
|
||||
console.warn('Speech Recognition wird von diesem Browser nicht unterstützt');
|
||||
return;
|
||||
}
|
||||
this.isSpeechRecognitionSupported = true;
|
||||
},
|
||||
isRecording(exerciseId) {
|
||||
return !!this.activeRecognition[exerciseId];
|
||||
},
|
||||
startReadingAloud(exerciseId) {
|
||||
const exercise = this.lesson.grammarExercises.find(e => e.id === exerciseId);
|
||||
if (!exercise) return;
|
||||
|
||||
const qData = this.getQuestionData(exercise);
|
||||
const expectedText = qData.text || '';
|
||||
|
||||
this.startRecognition(exerciseId, expectedText);
|
||||
},
|
||||
stopReadingAloud(exerciseId) {
|
||||
this.stopRecognition(exerciseId);
|
||||
},
|
||||
startSpeakingFromMemory(exerciseId) {
|
||||
const exercise = this.lesson.grammarExercises.find(e => e.id === exerciseId);
|
||||
if (!exercise) return;
|
||||
|
||||
const qData = this.getQuestionData(exercise);
|
||||
const expectedText = qData.expectedText || qData.text || '';
|
||||
|
||||
this.startRecognition(exerciseId, expectedText);
|
||||
},
|
||||
stopSpeakingFromMemory(exerciseId) {
|
||||
this.stopRecognition(exerciseId);
|
||||
},
|
||||
startRecognition(exerciseId, expectedText) {
|
||||
if (!this.isSpeechRecognitionSupported) {
|
||||
this.recordingStatus[exerciseId] = this.$t('socialnetwork.vocab.courses.speechRecognitionNotSupported');
|
||||
return;
|
||||
}
|
||||
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
const recognition = new SpeechRecognition();
|
||||
|
||||
// Konfiguriere Recognition
|
||||
recognition.continuous = true;
|
||||
recognition.interimResults = true;
|
||||
recognition.lang = 'de-DE'; // Kann später dynamisch basierend auf Kurs-Sprache gesetzt werden
|
||||
|
||||
let finalTranscript = '';
|
||||
|
||||
recognition.onresult = (event) => {
|
||||
let interimTranscript = '';
|
||||
|
||||
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||
const transcript = event.results[i][0].transcript;
|
||||
if (event.results[i].isFinal) {
|
||||
finalTranscript += transcript + ' ';
|
||||
} else {
|
||||
interimTranscript += transcript;
|
||||
}
|
||||
}
|
||||
|
||||
// Aktualisiere erkannten Text
|
||||
this.recognizedText[exerciseId] = finalTranscript.trim() || interimTranscript;
|
||||
this.recordingStatus[exerciseId] = this.$t('socialnetwork.vocab.courses.listening');
|
||||
};
|
||||
|
||||
recognition.onerror = (event) => {
|
||||
console.error('Speech Recognition Fehler:', event.error);
|
||||
this.recordingStatus[exerciseId] = this.$t('socialnetwork.vocab.courses.recordingError') + ': ' + event.error;
|
||||
this.stopRecognition(exerciseId);
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
// Speichere finalen Text in exerciseAnswers
|
||||
if (finalTranscript.trim()) {
|
||||
this.exerciseAnswers[exerciseId] = finalTranscript.trim();
|
||||
}
|
||||
this.activeRecognition[exerciseId] = null;
|
||||
this.recordingStatus[exerciseId] = this.$t('socialnetwork.vocab.courses.recordingStopped');
|
||||
};
|
||||
|
||||
// Starte Recognition
|
||||
try {
|
||||
recognition.start();
|
||||
this.activeRecognition[exerciseId] = recognition;
|
||||
this.recordingStatus[exerciseId] = this.$t('socialnetwork.vocab.courses.recording');
|
||||
this.recognizedText[exerciseId] = '';
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Starten der Speech Recognition:', error);
|
||||
this.recordingStatus[exerciseId] = this.$t('socialnetwork.vocab.courses.recordingError') + ': ' + error.message;
|
||||
}
|
||||
},
|
||||
stopRecognition(exerciseId) {
|
||||
if (this.activeRecognition[exerciseId]) {
|
||||
try {
|
||||
this.activeRecognition[exerciseId].stop();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Stoppen der Speech Recognition:', error);
|
||||
}
|
||||
this.activeRecognition[exerciseId] = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// Prüfe Speech Recognition Support
|
||||
this.initSpeechRecognition();
|
||||
await this.loadLesson();
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Stoppe alle aktiven Recognition-Instanzen
|
||||
Object.keys(this.activeRecognition).forEach(exerciseId => {
|
||||
this.stopRecognition(exerciseId);
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1626,6 +1848,142 @@ export default {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
/* Reading Aloud & Speaking From Memory Styles */
|
||||
.reading-aloud-exercise,
|
||||
.speaking-from-memory-exercise {
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.reading-aloud-controls,
|
||||
.speaking-controls {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-record,
|
||||
.btn-stop-record {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-record {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-record:hover:not(:disabled) {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-record:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-stop-record {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-stop-record:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.btn-check {
|
||||
padding: 10px 20px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.btn-check:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.recording-status {
|
||||
margin: 15px 0;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recording-status.recording {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.recognized-text {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.recognized-text strong {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.recognized-text p {
|
||||
margin: 0;
|
||||
font-size: 1.1em;
|
||||
color: #212529;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.keywords-hint {
|
||||
margin: 15px 0;
|
||||
padding: 12px;
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #007bff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.keywords-hint strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
.keyword-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
margin: 4px 4px 0 0;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.speech-not-supported {
|
||||
margin-top: 15px;
|
||||
padding: 12px;
|
||||
background: #f8d7da;
|
||||
border-left: 4px solid #dc3545;
|
||||
border-radius: 4px;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* Dialog Styles */
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
|
||||
Reference in New Issue
Block a user