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:
@@ -392,7 +392,20 @@
|
||||
"allLessonsCompleted": "Alle Lektionen abgeschlossen!",
|
||||
"startExercises": "Zur Kapitel-Prüfung",
|
||||
"correctAnswer": "Richtige Antwort",
|
||||
"alternatives": "Alternative Antworten"
|
||||
"alternatives": "Alternative Antworten",
|
||||
"notStarted": "Nicht begonnen",
|
||||
"readingAloudInstruction": "Lies den Text laut vor. Klicke auf 'Aufnahme starten' und beginne zu sprechen.",
|
||||
"speakingFromMemoryInstruction": "Sprich frei aus dem Kopf. Verwende die angezeigten Schlüsselwörter.",
|
||||
"startRecording": "Aufnahme starten",
|
||||
"stopRecording": "Aufnahme stoppen",
|
||||
"startSpeaking": "Sprechen starten",
|
||||
"recording": "Aufnahme läuft",
|
||||
"listening": "Höre zu...",
|
||||
"recordingStopped": "Aufnahme beendet",
|
||||
"recordingError": "Aufnahme-Fehler",
|
||||
"recognizedText": "Erkannter Text",
|
||||
"speechRecognitionNotSupported": "Speech Recognition wird von diesem Browser nicht unterstützt. Bitte verwende Chrome oder Edge.",
|
||||
"keywords": "Schlüsselwörter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,7 +392,20 @@
|
||||
"allLessonsCompleted": "All lessons completed!",
|
||||
"startExercises": "Start Chapter Test",
|
||||
"correctAnswer": "Correct Answer",
|
||||
"alternatives": "Alternative Answers"
|
||||
"alternatives": "Alternative Answers",
|
||||
"notStarted": "Not Started",
|
||||
"readingAloudInstruction": "Read the text aloud. Click 'Start Recording' and begin speaking.",
|
||||
"speakingFromMemoryInstruction": "Speak freely from memory. Use the displayed keywords.",
|
||||
"startRecording": "Start Recording",
|
||||
"stopRecording": "Stop Recording",
|
||||
"startSpeaking": "Start Speaking",
|
||||
"recording": "Recording...",
|
||||
"listening": "Listening...",
|
||||
"recordingStopped": "Recording stopped",
|
||||
"recordingError": "Recording error",
|
||||
"recognizedText": "Recognized Text",
|
||||
"speechRecognitionNotSupported": "Speech Recognition is not supported by this browser. Please use Chrome or Edge.",
|
||||
"keywords": "Keywords"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,11 @@
|
||||
<span v-if="getLessonProgress(lesson.id)?.completed" class="badge completed">
|
||||
{{ $t('socialnetwork.vocab.courses.completed') }}
|
||||
</span>
|
||||
<span v-if="getLessonProgress(lesson.id)?.score" class="score">
|
||||
<span v-else-if="getLessonProgress(lesson.id)?.score" class="score">
|
||||
{{ $t('socialnetwork.vocab.courses.score') }}: {{ getLessonProgress(lesson.id).score }}%
|
||||
</span>
|
||||
<span v-else-if="!getLessonProgress(lesson.id)" class="status-new">
|
||||
Nicht begonnen
|
||||
<span v-else class="status-new">
|
||||
{{ $t('socialnetwork.vocab.courses.notStarted') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="lesson-actions">
|
||||
@@ -296,7 +296,7 @@ export default {
|
||||
|
||||
.lessons-table td {
|
||||
padding: 15px;
|
||||
vertical-align: top;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.lesson-number {
|
||||
@@ -328,6 +328,8 @@ export default {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.badge.completed {
|
||||
@@ -356,37 +358,42 @@ export default {
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.btn-start {
|
||||
padding: 8px 16px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
background: #F9A22C;
|
||||
color: #000000;
|
||||
border: 1px solid #F9A22C;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s ease;
|
||||
transition: background 0.05s;
|
||||
}
|
||||
|
||||
.btn-start:hover {
|
||||
background: #0056b3;
|
||||
background: #fdf1db;
|
||||
color: #7E471B;
|
||||
border: 1px solid #7E471B;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
padding: 6px 12px;
|
||||
background: #ffc107;
|
||||
color: #333;
|
||||
border: none;
|
||||
background: #F9A22C;
|
||||
color: #000000;
|
||||
border: 1px solid #F9A22C;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85em;
|
||||
transition: background-color 0.2s ease;
|
||||
transition: background 0.05s;
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: #e0a800;
|
||||
background: #fdf1db;
|
||||
color: #7E471B;
|
||||
border: 1px solid #7E471B;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
|
||||
@@ -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