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:
@@ -330,38 +330,72 @@ function createFamilyConversationExercises(nativeLanguageName) {
|
|||||||
|
|
||||||
let exerciseNum = 1;
|
let exerciseNum = 1;
|
||||||
|
|
||||||
// Multiple Choice: Übersetze Bisaya-Satz in Muttersprache
|
// Multiple Choice: Übersetze Bisaya-Satz in Muttersprache (alle Gespräche)
|
||||||
conversations.forEach((conv, idx) => {
|
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({
|
exercises.push({
|
||||||
exerciseTypeId: 2, // multiple_choice
|
exerciseTypeId: 2, // multiple_choice
|
||||||
exerciseNumber: exerciseNum++,
|
exerciseNumber: exerciseNum++,
|
||||||
title: `Familien-Gespräch ${idx + 1} - Übersetzung`,
|
title: `Familien-Gespräch ${idx + 1} - Was bedeutet dieser Satz?`,
|
||||||
instruction: 'Übersetze den Bisaya-Satz ins ' + nativeLanguageName,
|
instruction: 'Was bedeutet dieser Bisaya-Satz?',
|
||||||
questionData: JSON.stringify({
|
questionData: JSON.stringify({
|
||||||
type: 'multiple_choice',
|
type: 'multiple_choice',
|
||||||
question: `Wie sagt man "${conv.bisaya}" auf ${nativeLanguageName}?`,
|
question: `Was bedeutet "${conv.bisaya}"?`,
|
||||||
options: [
|
options: options
|
||||||
conv.native,
|
|
||||||
conversations[(idx + 1) % conversations.length].native,
|
|
||||||
conversations[(idx + 2) % conversations.length].native,
|
|
||||||
conversations[(idx + 3) % conversations.length].native
|
|
||||||
]
|
|
||||||
}),
|
}),
|
||||||
answerData: JSON.stringify({
|
answerData: JSON.stringify({
|
||||||
type: 'multiple_choice',
|
type: 'multiple_choice',
|
||||||
correctAnswer: 0
|
correctAnswer: correctIndex
|
||||||
}),
|
}),
|
||||||
explanation: conv.explanation
|
explanation: conv.explanation
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gap Fill: Vervollständige Familiengespräche
|
// Gap Fill: Vervollständige Familiengespräche (mehrere Varianten)
|
||||||
exercises.push({
|
exercises.push({
|
||||||
exerciseTypeId: 1, // gap_fill
|
exerciseTypeId: 1, // gap_fill
|
||||||
exerciseNumber: exerciseNum++,
|
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.',
|
instruction: 'Vervollständige das Gespräch mit den richtigen Bisaya-Wörtern.',
|
||||||
questionData: JSON.stringify({
|
questionData: JSON.stringify({
|
||||||
type: 'gap_fill',
|
type: 'gap_fill',
|
||||||
@@ -375,44 +409,40 @@ function createFamilyConversationExercises(nativeLanguageName) {
|
|||||||
explanation: '"Nanay" ist "Mama" und "Maayo ko" bedeutet "Mir geht es gut"'
|
explanation: '"Nanay" ist "Mama" und "Maayo ko" bedeutet "Mir geht es gut"'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Transformation: Übersetze Muttersprache-Satz nach Bisaya
|
|
||||||
exercises.push({
|
exercises.push({
|
||||||
exerciseTypeId: 3, // transformation
|
exerciseTypeId: 1, // gap_fill
|
||||||
exerciseNumber: exerciseNum++,
|
exerciseNumber: exerciseNum++,
|
||||||
title: 'Familien-Gespräch - Übersetzung nach Bisaya',
|
title: 'Familien-Gespräch 2 - Vervollständigen',
|
||||||
instruction: 'Übersetze den Satz ins Bisaya.',
|
instruction: 'Vervollständige das Gespräch mit den richtigen Bisaya-Wörtern.',
|
||||||
questionData: JSON.stringify({
|
questionData: JSON.stringify({
|
||||||
type: 'transformation',
|
type: 'gap_fill',
|
||||||
text: conversations[0].native
|
text: 'Person A: {gap} si Tatay? (Wo ist)\nPerson B: {gap} siya sa balay. (Er ist)',
|
||||||
|
gaps: 2
|
||||||
}),
|
}),
|
||||||
answerData: JSON.stringify({
|
answerData: JSON.stringify({
|
||||||
type: 'transformation',
|
type: 'gap_fill',
|
||||||
correctAnswer: conversations[0].bisaya
|
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
|
// Transformation: Übersetze Muttersprache-Satz nach Bisaya (mehrere Varianten)
|
||||||
exercises.push({
|
conversations.slice(0, 4).forEach((conv, idx) => {
|
||||||
exerciseTypeId: 2, // multiple_choice
|
exercises.push({
|
||||||
exerciseNumber: exerciseNum++,
|
exerciseTypeId: 3, // transformation
|
||||||
title: 'Familien-Gespräch - Was bedeutet dieser Satz?',
|
exerciseNumber: exerciseNum++,
|
||||||
instruction: 'Was bedeutet dieser Bisaya-Satz?',
|
title: `Familien-Gespräch ${idx + 1} - Übersetzung nach Bisaya`,
|
||||||
questionData: JSON.stringify({
|
instruction: 'Übersetze den Satz ins Bisaya.',
|
||||||
type: 'multiple_choice',
|
questionData: JSON.stringify({
|
||||||
question: `Was bedeutet "${conversations[2].bisaya}"?`,
|
type: 'transformation',
|
||||||
options: [
|
text: conv.native
|
||||||
conversations[2].native,
|
}),
|
||||||
conversations[3].native,
|
answerData: JSON.stringify({
|
||||||
conversations[4].native,
|
type: 'transformation',
|
||||||
conversations[5].native
|
correctAnswer: conv.bisaya
|
||||||
]
|
}),
|
||||||
}),
|
explanation: `"${conv.bisaya}" bedeutet "${conv.native}" auf Bisaya. ${conv.explanation}`
|
||||||
answerData: JSON.stringify({
|
});
|
||||||
type: 'multiple_choice',
|
|
||||||
correctAnswer: 0
|
|
||||||
}),
|
|
||||||
explanation: conversations[2].explanation
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return exercises;
|
return exercises;
|
||||||
|
|||||||
@@ -330,38 +330,72 @@ function createFeelingsAffectionExercises(nativeLanguageName) {
|
|||||||
|
|
||||||
let exerciseNum = 1;
|
let exerciseNum = 1;
|
||||||
|
|
||||||
// Multiple Choice: Übersetze Bisaya-Satz in Muttersprache
|
// Multiple Choice: Übersetze Bisaya-Satz in Muttersprache (alle Gespräche)
|
||||||
conversations.forEach((conv, idx) => {
|
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({
|
exercises.push({
|
||||||
exerciseTypeId: 2, // multiple_choice
|
exerciseTypeId: 2, // multiple_choice
|
||||||
exerciseNumber: exerciseNum++,
|
exerciseNumber: exerciseNum++,
|
||||||
title: `Gefühle & Zuneigung ${idx + 1} - Übersetzung`,
|
title: `Gefühle & Zuneigung ${idx + 1} - Was bedeutet dieser Satz?`,
|
||||||
instruction: 'Übersetze den Bisaya-Satz ins ' + nativeLanguageName,
|
instruction: 'Was bedeutet dieser Bisaya-Satz?',
|
||||||
questionData: JSON.stringify({
|
questionData: JSON.stringify({
|
||||||
type: 'multiple_choice',
|
type: 'multiple_choice',
|
||||||
question: `Wie sagt man "${conv.bisaya}" auf ${nativeLanguageName}?`,
|
question: `Was bedeutet "${conv.bisaya}"?`,
|
||||||
options: [
|
options: options
|
||||||
conv.native,
|
|
||||||
conversations[(idx + 1) % conversations.length].native,
|
|
||||||
conversations[(idx + 2) % conversations.length].native,
|
|
||||||
conversations[(idx + 3) % conversations.length].native
|
|
||||||
]
|
|
||||||
}),
|
}),
|
||||||
answerData: JSON.stringify({
|
answerData: JSON.stringify({
|
||||||
type: 'multiple_choice',
|
type: 'multiple_choice',
|
||||||
correctAnswer: 0
|
correctAnswer: correctIndex
|
||||||
}),
|
}),
|
||||||
explanation: conv.explanation
|
explanation: conv.explanation
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gap Fill: Vervollständige Gefühlsausdrücke
|
// Gap Fill: Vervollständige Gefühlsausdrücke (mehrere Varianten)
|
||||||
exercises.push({
|
exercises.push({
|
||||||
exerciseTypeId: 1, // gap_fill
|
exerciseTypeId: 1, // gap_fill
|
||||||
exerciseNumber: exerciseNum++,
|
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.',
|
instruction: 'Vervollständige den Satz mit den richtigen Bisaya-Wörtern.',
|
||||||
questionData: JSON.stringify({
|
questionData: JSON.stringify({
|
||||||
type: 'gap_fill',
|
type: 'gap_fill',
|
||||||
@@ -375,44 +409,40 @@ function createFeelingsAffectionExercises(nativeLanguageName) {
|
|||||||
explanation: '"Gihigugma" bedeutet "lieben" und wird wiederholt, um "auch" auszudrücken'
|
explanation: '"Gihigugma" bedeutet "lieben" und wird wiederholt, um "auch" auszudrücken'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Transformation: Übersetze Muttersprache-Satz nach Bisaya
|
|
||||||
exercises.push({
|
exercises.push({
|
||||||
exerciseTypeId: 3, // transformation
|
exerciseTypeId: 1, // gap_fill
|
||||||
exerciseNumber: exerciseNum++,
|
exerciseNumber: exerciseNum++,
|
||||||
title: 'Gefühle & Zuneigung - Übersetzung nach Bisaya',
|
title: 'Gefühle & Zuneigung 2 - Vervollständigen',
|
||||||
instruction: 'Übersetze den Satz ins Bisaya.',
|
instruction: 'Vervollständige den Satz mit den richtigen Bisaya-Wörtern.',
|
||||||
questionData: JSON.stringify({
|
questionData: JSON.stringify({
|
||||||
type: 'transformation',
|
type: 'gap_fill',
|
||||||
text: conversations[0].native
|
text: 'Person A: {gap} ko nga nakita ka. (Ich bin glücklich)\nPerson B: {gap} ko pud. (Ich auch)',
|
||||||
|
gaps: 2
|
||||||
}),
|
}),
|
||||||
answerData: JSON.stringify({
|
answerData: JSON.stringify({
|
||||||
type: 'transformation',
|
type: 'gap_fill',
|
||||||
correctAnswer: conversations[0].bisaya
|
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
|
// Transformation: Übersetze Muttersprache-Satz nach Bisaya (mehrere Varianten)
|
||||||
exercises.push({
|
conversations.slice(0, 4).forEach((conv, idx) => {
|
||||||
exerciseTypeId: 2, // multiple_choice
|
exercises.push({
|
||||||
exerciseNumber: exerciseNum++,
|
exerciseTypeId: 3, // transformation
|
||||||
title: 'Gefühle & Zuneigung - Was bedeutet dieser Satz?',
|
exerciseNumber: exerciseNum++,
|
||||||
instruction: 'Was bedeutet dieser Bisaya-Satz?',
|
title: `Gefühle & Zuneigung ${idx + 1} - Übersetzung nach Bisaya`,
|
||||||
questionData: JSON.stringify({
|
instruction: 'Übersetze den Satz ins Bisaya.',
|
||||||
type: 'multiple_choice',
|
questionData: JSON.stringify({
|
||||||
question: `Was bedeutet "${conversations[2].bisaya}"?`,
|
type: 'transformation',
|
||||||
options: [
|
text: conv.native
|
||||||
conversations[2].native,
|
}),
|
||||||
conversations[3].native,
|
answerData: JSON.stringify({
|
||||||
conversations[4].native,
|
type: 'transformation',
|
||||||
conversations[5].native
|
correctAnswer: conv.bisaya
|
||||||
]
|
}),
|
||||||
}),
|
explanation: `"${conv.bisaya}" bedeutet "${conv.native}" auf Bisaya. ${conv.explanation}`
|
||||||
answerData: JSON.stringify({
|
});
|
||||||
type: 'multiple_choice',
|
|
||||||
correctAnswer: 0
|
|
||||||
}),
|
|
||||||
explanation: conversations[2].explanation
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return exercises;
|
return exercises;
|
||||||
|
|||||||
@@ -1421,6 +1421,15 @@ export default class VocabService {
|
|||||||
? answerData.answers.join(', ')
|
? answerData.answers.join(', ')
|
||||||
: answerData.answers;
|
: 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
|
// Fallback: Versuche correct oder correctAnswer
|
||||||
else {
|
else {
|
||||||
correctAnswer = Array.isArray(answerData.correct)
|
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) {
|
_checkAnswer(answerData, questionData, userAnswer, exerciseTypeId) {
|
||||||
// Vereinfachte Antwortprüfung - kann je nach Übungstyp erweitert werden
|
// Vereinfachte Antwortprüfung - kann je nach Übungstyp erweitert werden
|
||||||
if (!answerData || userAnswer === undefined || userAnswer === null) return false;
|
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)
|
// Für andere Typen: einfacher String-Vergleich (kann später erweitert werden)
|
||||||
const normalize = (str) => String(str || '').trim().toLowerCase();
|
const normalize = (str) => String(str || '').trim().toLowerCase();
|
||||||
const correctAnswers = parsedAnswerData.correct || parsedAnswerData.correctAnswer || [];
|
const correctAnswers = parsedAnswerData.correct || parsedAnswerData.correctAnswer || [];
|
||||||
|
|||||||
16
backend/sql/add-speaking-exercise-types.sql
Normal file
16
backend/sql/add-speaking-exercise-types.sql
Normal file
@@ -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
|
||||||
|
-- ============================================
|
||||||
@@ -392,7 +392,20 @@
|
|||||||
"allLessonsCompleted": "Alle Lektionen abgeschlossen!",
|
"allLessonsCompleted": "Alle Lektionen abgeschlossen!",
|
||||||
"startExercises": "Zur Kapitel-Prüfung",
|
"startExercises": "Zur Kapitel-Prüfung",
|
||||||
"correctAnswer": "Richtige Antwort",
|
"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!",
|
"allLessonsCompleted": "All lessons completed!",
|
||||||
"startExercises": "Start Chapter Test",
|
"startExercises": "Start Chapter Test",
|
||||||
"correctAnswer": "Correct Answer",
|
"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">
|
<span v-if="getLessonProgress(lesson.id)?.completed" class="badge completed">
|
||||||
{{ $t('socialnetwork.vocab.courses.completed') }}
|
{{ $t('socialnetwork.vocab.courses.completed') }}
|
||||||
</span>
|
</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 }}%
|
{{ $t('socialnetwork.vocab.courses.score') }}: {{ getLessonProgress(lesson.id).score }}%
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="!getLessonProgress(lesson.id)" class="status-new">
|
<span v-else class="status-new">
|
||||||
Nicht begonnen
|
{{ $t('socialnetwork.vocab.courses.notStarted') }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="lesson-actions">
|
<td class="lesson-actions">
|
||||||
@@ -296,7 +296,7 @@ export default {
|
|||||||
|
|
||||||
.lessons-table td {
|
.lessons-table td {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
vertical-align: top;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-number {
|
.lesson-number {
|
||||||
@@ -328,6 +328,8 @@ export default {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge.completed {
|
.badge.completed {
|
||||||
@@ -356,37 +358,42 @@ export default {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-start {
|
.btn-start {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
background: #007bff;
|
background: #F9A22C;
|
||||||
color: white;
|
color: #000000;
|
||||||
border: none;
|
border: 1px solid #F9A22C;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: background-color 0.2s ease;
|
transition: background 0.05s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-start:hover {
|
.btn-start:hover {
|
||||||
background: #0056b3;
|
background: #fdf1db;
|
||||||
|
color: #7E471B;
|
||||||
|
border: 1px solid #7E471B;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-edit {
|
.btn-edit {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: #ffc107;
|
background: #F9A22C;
|
||||||
color: #333;
|
color: #000000;
|
||||||
border: none;
|
border: 1px solid #F9A22C;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
transition: background-color 0.2s ease;
|
transition: background 0.05s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-edit:hover {
|
.btn-edit:hover {
|
||||||
background: #e0a800;
|
background: #fdf1db;
|
||||||
|
color: #7E471B;
|
||||||
|
border: 1px solid #7E471B;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-delete {
|
.btn-delete {
|
||||||
|
|||||||
@@ -242,6 +242,103 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Fallback für unbekannte Typen -->
|
||||||
<div v-else class="unknown-exercise">
|
<div v-else class="unknown-exercise">
|
||||||
<p>Übungstyp: {{ getExerciseType(exercise) }}</p>
|
<p>Übungstyp: {{ getExerciseType(exercise) }}</p>
|
||||||
@@ -347,6 +444,12 @@ export default {
|
|||||||
isCheckingLessonCompletion: false, // Flag um Endlosschleife zu verhindern
|
isCheckingLessonCompletion: false, // Flag um Endlosschleife zu verhindern
|
||||||
isNavigatingToNext: false, // Flag um mehrfache Navigation zu verhindern
|
isNavigatingToNext: false, // Flag um mehrfache Navigation zu verhindern
|
||||||
showNextLessonDialog: false,
|
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,
|
nextLessonId: null,
|
||||||
showCompletionDialog: false,
|
showCompletionDialog: false,
|
||||||
showErrorDialog: false,
|
showErrorDialog: false,
|
||||||
@@ -633,7 +736,9 @@ export default {
|
|||||||
3: 'sentence_building',
|
3: 'sentence_building',
|
||||||
4: 'transformation',
|
4: 'transformation',
|
||||||
5: 'conjugation',
|
5: 'conjugation',
|
||||||
6: 'declension'
|
6: 'declension',
|
||||||
|
7: 'reading_aloud',
|
||||||
|
8: 'speaking_from_memory'
|
||||||
};
|
};
|
||||||
return typeMap[exercise.exerciseTypeId] || 'unknown';
|
return typeMap[exercise.exerciseTypeId] || 'unknown';
|
||||||
},
|
},
|
||||||
@@ -710,6 +815,9 @@ export default {
|
|||||||
} else if (exerciseType === 'transformation') {
|
} else if (exerciseType === 'transformation') {
|
||||||
// Transformation: String
|
// Transformation: String
|
||||||
answer = String(answer || '').trim();
|
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 });
|
const res = await apiClient.post(`/api/vocab/grammar-exercises/${exerciseId}/check`, { answer });
|
||||||
@@ -1066,10 +1174,124 @@ export default {
|
|||||||
this.vocabTrainerAnswer = '';
|
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() {
|
async mounted() {
|
||||||
|
// Prüfe Speech Recognition Support
|
||||||
|
this.initSpeechRecognition();
|
||||||
await this.loadLesson();
|
await this.loadLesson();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
// Stoppe alle aktiven Recognition-Instanzen
|
||||||
|
Object.keys(this.activeRecognition).forEach(exerciseId => {
|
||||||
|
this.stopRecognition(exerciseId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -1626,6 +1848,142 @@ export default {
|
|||||||
background: #0056b3;
|
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 Styles */
|
||||||
.dialog-overlay {
|
.dialog-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
Reference in New Issue
Block a user