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;
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || [];
|
||||
|
||||
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
|
||||
-- ============================================
|
||||
Reference in New Issue
Block a user