diff --git a/backend/scripts/update-family-conversations-exercises.js b/backend/scripts/update-family-conversations-exercises.js index bac468f..173e5e5 100755 --- a/backend/scripts/update-family-conversations-exercises.js +++ b/backend/scripts/update-family-conversations-exercises.js @@ -330,38 +330,72 @@ function createFamilyConversationExercises(nativeLanguageName) { let exerciseNum = 1; - // Multiple Choice: Übersetze Bisaya-Satz in Muttersprache + // Multiple Choice: Übersetze Bisaya-Satz in Muttersprache (alle Gespräche) conversations.forEach((conv, idx) => { - if (idx < 4) { // Erste 4 als Multiple Choice + // Erstelle für jedes Gespräch eine Multiple Choice Übung + const wrongOptions = conversations + .filter((c, i) => i !== idx) + .sort(() => Math.random() - 0.5) + .slice(0, 3) + .map(c => c.native); + + const options = [conv.native, ...wrongOptions].sort(() => Math.random() - 0.5); + const correctIndex = options.indexOf(conv.native); + + exercises.push({ + exerciseTypeId: 2, // multiple_choice + exerciseNumber: exerciseNum++, + title: `Familien-Gespräch ${idx + 1} - Übersetzung`, + instruction: 'Übersetze den Bisaya-Satz ins ' + nativeLanguageName, + questionData: JSON.stringify({ + type: 'multiple_choice', + question: `Wie sagt man "${conv.bisaya}" auf ${nativeLanguageName}?`, + options: options + }), + answerData: JSON.stringify({ + type: 'multiple_choice', + correctAnswer: correctIndex + }), + explanation: conv.explanation + }); + }); + + // Multiple Choice: Rückwärts-Übersetzung (Was bedeutet dieser Satz?) + conversations.forEach((conv, idx) => { + if (idx < 6) { // Erste 6 als Rückwärts-Übersetzung + const wrongOptions = conversations + .filter((c, i) => i !== idx) + .sort(() => Math.random() - 0.5) + .slice(0, 3) + .map(c => c.native); + + const options = [conv.native, ...wrongOptions].sort(() => Math.random() - 0.5); + const correctIndex = options.indexOf(conv.native); + exercises.push({ exerciseTypeId: 2, // multiple_choice exerciseNumber: exerciseNum++, - title: `Familien-Gespräch ${idx + 1} - Übersetzung`, - instruction: 'Übersetze den Bisaya-Satz ins ' + nativeLanguageName, + title: `Familien-Gespräch ${idx + 1} - Was bedeutet dieser Satz?`, + instruction: 'Was bedeutet dieser Bisaya-Satz?', questionData: JSON.stringify({ type: 'multiple_choice', - question: `Wie sagt man "${conv.bisaya}" auf ${nativeLanguageName}?`, - options: [ - conv.native, - conversations[(idx + 1) % conversations.length].native, - conversations[(idx + 2) % conversations.length].native, - conversations[(idx + 3) % conversations.length].native - ] + question: `Was bedeutet "${conv.bisaya}"?`, + options: options }), answerData: JSON.stringify({ type: 'multiple_choice', - correctAnswer: 0 + correctAnswer: correctIndex }), explanation: conv.explanation }); } }); - // Gap Fill: Vervollständige Familiengespräche + // Gap Fill: Vervollständige Familiengespräche (mehrere Varianten) exercises.push({ exerciseTypeId: 1, // gap_fill exerciseNumber: exerciseNum++, - title: 'Familien-Gespräch vervollständigen', + title: 'Familien-Gespräch 1 - Vervollständigen', instruction: 'Vervollständige das Gespräch mit den richtigen Bisaya-Wörtern.', questionData: JSON.stringify({ type: 'gap_fill', @@ -375,44 +409,40 @@ function createFamilyConversationExercises(nativeLanguageName) { explanation: '"Nanay" ist "Mama" und "Maayo ko" bedeutet "Mir geht es gut"' }); - // Transformation: Übersetze Muttersprache-Satz nach Bisaya exercises.push({ - exerciseTypeId: 3, // transformation + exerciseTypeId: 1, // gap_fill exerciseNumber: exerciseNum++, - title: 'Familien-Gespräch - Übersetzung nach Bisaya', - instruction: 'Übersetze den Satz ins Bisaya.', + title: 'Familien-Gespräch 2 - Vervollständigen', + instruction: 'Vervollständige das Gespräch mit den richtigen Bisaya-Wörtern.', questionData: JSON.stringify({ - type: 'transformation', - text: conversations[0].native + type: 'gap_fill', + text: 'Person A: {gap} si Tatay? (Wo ist)\nPerson B: {gap} siya sa balay. (Er ist)', + gaps: 2 }), answerData: JSON.stringify({ - type: 'transformation', - correctAnswer: conversations[0].bisaya + type: 'gap_fill', + answers: ['Asa', 'Naa'] }), - explanation: `"${conversations[0].bisaya}" bedeutet "${conversations[0].native}" auf Bisaya. ${conversations[0].explanation}` + explanation: '"Asa" bedeutet "wo" und "Naa" bedeutet "ist/sein"' }); - // Weitere Multiple Choice: Rückwärts-Übersetzung - exercises.push({ - exerciseTypeId: 2, // multiple_choice - exerciseNumber: exerciseNum++, - title: 'Familien-Gespräch - Was bedeutet dieser Satz?', - instruction: 'Was bedeutet dieser Bisaya-Satz?', - questionData: JSON.stringify({ - type: 'multiple_choice', - question: `Was bedeutet "${conversations[2].bisaya}"?`, - options: [ - conversations[2].native, - conversations[3].native, - conversations[4].native, - conversations[5].native - ] - }), - answerData: JSON.stringify({ - type: 'multiple_choice', - correctAnswer: 0 - }), - explanation: conversations[2].explanation + // Transformation: Übersetze Muttersprache-Satz nach Bisaya (mehrere Varianten) + conversations.slice(0, 4).forEach((conv, idx) => { + exercises.push({ + exerciseTypeId: 3, // transformation + exerciseNumber: exerciseNum++, + title: `Familien-Gespräch ${idx + 1} - Übersetzung nach Bisaya`, + instruction: 'Übersetze den Satz ins Bisaya.', + questionData: JSON.stringify({ + type: 'transformation', + text: conv.native + }), + answerData: JSON.stringify({ + type: 'transformation', + correctAnswer: conv.bisaya + }), + explanation: `"${conv.bisaya}" bedeutet "${conv.native}" auf Bisaya. ${conv.explanation}` + }); }); return exercises; diff --git a/backend/scripts/update-feelings-affection-exercises.js b/backend/scripts/update-feelings-affection-exercises.js index 6df1365..a6f8877 100755 --- a/backend/scripts/update-feelings-affection-exercises.js +++ b/backend/scripts/update-feelings-affection-exercises.js @@ -330,38 +330,72 @@ function createFeelingsAffectionExercises(nativeLanguageName) { let exerciseNum = 1; - // Multiple Choice: Übersetze Bisaya-Satz in Muttersprache + // Multiple Choice: Übersetze Bisaya-Satz in Muttersprache (alle Gespräche) conversations.forEach((conv, idx) => { - if (idx < 4) { // Erste 4 als Multiple Choice + // Erstelle für jedes Gespräch eine Multiple Choice Übung + const wrongOptions = conversations + .filter((c, i) => i !== idx) + .sort(() => Math.random() - 0.5) + .slice(0, 3) + .map(c => c.native); + + const options = [conv.native, ...wrongOptions].sort(() => Math.random() - 0.5); + const correctIndex = options.indexOf(conv.native); + + exercises.push({ + exerciseTypeId: 2, // multiple_choice + exerciseNumber: exerciseNum++, + title: `Gefühle & Zuneigung ${idx + 1} - Übersetzung`, + instruction: 'Übersetze den Bisaya-Satz ins ' + nativeLanguageName, + questionData: JSON.stringify({ + type: 'multiple_choice', + question: `Wie sagt man "${conv.bisaya}" auf ${nativeLanguageName}?`, + options: options + }), + answerData: JSON.stringify({ + type: 'multiple_choice', + correctAnswer: correctIndex + }), + explanation: conv.explanation + }); + }); + + // Multiple Choice: Rückwärts-Übersetzung (Was bedeutet dieser Satz?) + conversations.forEach((conv, idx) => { + if (idx < 6) { // Erste 6 als Rückwärts-Übersetzung + const wrongOptions = conversations + .filter((c, i) => i !== idx) + .sort(() => Math.random() - 0.5) + .slice(0, 3) + .map(c => c.native); + + const options = [conv.native, ...wrongOptions].sort(() => Math.random() - 0.5); + const correctIndex = options.indexOf(conv.native); + exercises.push({ exerciseTypeId: 2, // multiple_choice exerciseNumber: exerciseNum++, - title: `Gefühle & Zuneigung ${idx + 1} - Übersetzung`, - instruction: 'Übersetze den Bisaya-Satz ins ' + nativeLanguageName, + title: `Gefühle & Zuneigung ${idx + 1} - Was bedeutet dieser Satz?`, + instruction: 'Was bedeutet dieser Bisaya-Satz?', questionData: JSON.stringify({ type: 'multiple_choice', - question: `Wie sagt man "${conv.bisaya}" auf ${nativeLanguageName}?`, - options: [ - conv.native, - conversations[(idx + 1) % conversations.length].native, - conversations[(idx + 2) % conversations.length].native, - conversations[(idx + 3) % conversations.length].native - ] + question: `Was bedeutet "${conv.bisaya}"?`, + options: options }), answerData: JSON.stringify({ type: 'multiple_choice', - correctAnswer: 0 + correctAnswer: correctIndex }), explanation: conv.explanation }); } }); - // Gap Fill: Vervollständige Gefühlsausdrücke + // Gap Fill: Vervollständige Gefühlsausdrücke (mehrere Varianten) exercises.push({ exerciseTypeId: 1, // gap_fill exerciseNumber: exerciseNum++, - title: 'Gefühle & Zuneigung vervollständigen', + title: 'Gefühle & Zuneigung 1 - Vervollständigen', instruction: 'Vervollständige den Satz mit den richtigen Bisaya-Wörtern.', questionData: JSON.stringify({ type: 'gap_fill', @@ -375,44 +409,40 @@ function createFeelingsAffectionExercises(nativeLanguageName) { explanation: '"Gihigugma" bedeutet "lieben" und wird wiederholt, um "auch" auszudrücken' }); - // Transformation: Übersetze Muttersprache-Satz nach Bisaya exercises.push({ - exerciseTypeId: 3, // transformation + exerciseTypeId: 1, // gap_fill exerciseNumber: exerciseNum++, - title: 'Gefühle & Zuneigung - Übersetzung nach Bisaya', - instruction: 'Übersetze den Satz ins Bisaya.', + title: 'Gefühle & Zuneigung 2 - Vervollständigen', + instruction: 'Vervollständige den Satz mit den richtigen Bisaya-Wörtern.', questionData: JSON.stringify({ - type: 'transformation', - text: conversations[0].native + type: 'gap_fill', + text: 'Person A: {gap} ko nga nakita ka. (Ich bin glücklich)\nPerson B: {gap} ko pud. (Ich auch)', + gaps: 2 }), answerData: JSON.stringify({ - type: 'transformation', - correctAnswer: conversations[0].bisaya + type: 'gap_fill', + answers: ['Nalipay', 'Nalipay'] }), - explanation: `"${conversations[0].bisaya}" bedeutet "${conversations[0].native}" auf Bisaya. ${conversations[0].explanation}` + explanation: '"Nalipay" bedeutet "glücklich sein"' }); - // Weitere Multiple Choice: Rückwärts-Übersetzung - exercises.push({ - exerciseTypeId: 2, // multiple_choice - exerciseNumber: exerciseNum++, - title: 'Gefühle & Zuneigung - Was bedeutet dieser Satz?', - instruction: 'Was bedeutet dieser Bisaya-Satz?', - questionData: JSON.stringify({ - type: 'multiple_choice', - question: `Was bedeutet "${conversations[2].bisaya}"?`, - options: [ - conversations[2].native, - conversations[3].native, - conversations[4].native, - conversations[5].native - ] - }), - answerData: JSON.stringify({ - type: 'multiple_choice', - correctAnswer: 0 - }), - explanation: conversations[2].explanation + // Transformation: Übersetze Muttersprache-Satz nach Bisaya (mehrere Varianten) + conversations.slice(0, 4).forEach((conv, idx) => { + exercises.push({ + exerciseTypeId: 3, // transformation + exerciseNumber: exerciseNum++, + title: `Gefühle & Zuneigung ${idx + 1} - Übersetzung nach Bisaya`, + instruction: 'Übersetze den Satz ins Bisaya.', + questionData: JSON.stringify({ + type: 'transformation', + text: conv.native + }), + answerData: JSON.stringify({ + type: 'transformation', + correctAnswer: conv.bisaya + }), + explanation: `"${conv.bisaya}" bedeutet "${conv.native}" auf Bisaya. ${conv.explanation}` + }); }); return exercises; diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index e1e62d7..5ae6de7 100644 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -1421,6 +1421,15 @@ export default class VocabService { ? answerData.answers.join(', ') : answerData.answers; } + // Für Reading Aloud: Extrahiere den erwarteten Text + else if (questionData.type === 'reading_aloud') { + correctAnswer = questionData.text || answerData.expectedText || ''; + } + // Für Speaking From Memory: Extrahiere erwarteten Text oder Schlüsselwörter + else if (questionData.type === 'speaking_from_memory') { + correctAnswer = questionData.expectedText || questionData.text || ''; + alternatives = questionData.keywords || []; + } // Fallback: Versuche correct oder correctAnswer else { correctAnswer = Array.isArray(answerData.correct) @@ -1438,6 +1447,11 @@ export default class VocabService { }; } + async _getExerciseTypeIdByName(typeName) { + const type = await VocabGrammarExerciseType.findOne({ where: { name: typeName } }); + return type ? type.id : null; + } + _checkAnswer(answerData, questionData, userAnswer, exerciseTypeId) { // Vereinfachte Antwortprüfung - kann je nach Übungstyp erweitert werden if (!answerData || userAnswer === undefined || userAnswer === null) return false; @@ -1476,6 +1490,32 @@ export default class VocabService { } } + // Für Reading Aloud: userAnswer ist der erkannte Text (String) + // Vergleiche mit dem erwarteten Text aus questionData.text + if (parsedQuestionData.type === 'reading_aloud' || parsedQuestionData.type === 'speaking_from_memory') { + const normalize = (str) => String(str || '').trim().toLowerCase().replace(/[.,!?;:]/g, ''); + const expectedText = parsedQuestionData.text || parsedQuestionData.expectedText || ''; + const normalizedExpected = normalize(expectedText); + const normalizedUser = normalize(userAnswer); + + // Für reading_aloud: Exakter Vergleich oder Levenshtein-Distanz + if (parsedQuestionData.type === 'reading_aloud') { + // Exakter Vergleich (kann später mit Levenshtein erweitert werden) + return normalizedUser === normalizedExpected; + } + + // Für speaking_from_memory: Flexibler Vergleich (Schlüsselwörter) + if (parsedQuestionData.type === 'speaking_from_memory') { + const keywords = parsedQuestionData.keywords || []; + if (keywords.length === 0) { + // Fallback: Exakter Vergleich + return normalizedUser === normalizedExpected; + } + // Prüfe ob alle Schlüsselwörter vorhanden sind + return keywords.every(keyword => normalizedUser.includes(normalize(keyword))); + } + } + // Für andere Typen: einfacher String-Vergleich (kann später erweitert werden) const normalize = (str) => String(str || '').trim().toLowerCase(); const correctAnswers = parsedAnswerData.correct || parsedAnswerData.correctAnswer || []; diff --git a/backend/sql/add-speaking-exercise-types.sql b/backend/sql/add-speaking-exercise-types.sql new file mode 100644 index 0000000..d19209f --- /dev/null +++ b/backend/sql/add-speaking-exercise-types.sql @@ -0,0 +1,16 @@ +-- ============================================ +-- Neue Übungstypen für Sprachproduktion hinzufügen +-- ============================================ +-- Führe diese Queries direkt auf dem Server aus + +-- Neue Übungstypen hinzufügen +INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES + ('reading_aloud', 'Laut vorlesen - Übung zur Verbesserung der Aussprache'), + ('speaking_from_memory', 'Aus dem Kopf sprechen - Übung zur aktiven Sprachproduktion') +ON CONFLICT (name) DO NOTHING; + +-- ============================================ +-- Hinweis: +-- - reading_aloud: Text wird angezeigt, User liest vor, Speech Recognition prüft +-- - speaking_from_memory: Prompt wird angezeigt, User spricht frei, manuelle/automatische Bewertung +-- ============================================ diff --git a/frontend/src/i18n/locales/de/socialnetwork.json b/frontend/src/i18n/locales/de/socialnetwork.json index 02378c6..57b244d 100644 --- a/frontend/src/i18n/locales/de/socialnetwork.json +++ b/frontend/src/i18n/locales/de/socialnetwork.json @@ -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" } } } diff --git a/frontend/src/i18n/locales/en/socialnetwork.json b/frontend/src/i18n/locales/en/socialnetwork.json index a99260c..b36b620 100644 --- a/frontend/src/i18n/locales/en/socialnetwork.json +++ b/frontend/src/i18n/locales/en/socialnetwork.json @@ -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" } } } diff --git a/frontend/src/views/social/VocabCourseView.vue b/frontend/src/views/social/VocabCourseView.vue index 35d57d0..a2216e8 100644 --- a/frontend/src/views/social/VocabCourseView.vue +++ b/frontend/src/views/social/VocabCourseView.vue @@ -40,11 +40,11 @@ {{ $t('socialnetwork.vocab.courses.completed') }} - + {{ $t('socialnetwork.vocab.courses.score') }}: {{ getLessonProgress(lesson.id).score }}% - - Nicht begonnen + + {{ $t('socialnetwork.vocab.courses.notStarted') }}
{{ getQuestionText(exercise) }}
+{{ $t('socialnetwork.vocab.courses.readingAloudInstruction') }}
+{{ recognizedText[exercise.id] }}
++ {{ $t('socialnetwork.vocab.courses.correctAnswer') }}: {{ exerciseResults[exercise.id].correctAnswer }} +
+{{ exerciseResults[exercise.id].explanation }}
+{{ $t('socialnetwork.vocab.courses.speechRecognitionNotSupported') }}
+{{ getQuestionText(exercise) }}
+{{ $t('socialnetwork.vocab.courses.speakingFromMemoryInstruction') }}
+{{ recognizedText[exercise.id] }}
+{{ exerciseResults[exercise.id].explanation }}
+{{ $t('socialnetwork.vocab.courses.speechRecognitionNotSupported') }}
+Übungstyp: {{ getExerciseType(exercise) }}
@@ -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); + }); } }; @@ -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;