diff --git a/backend/scripts/repair-bisaya-children-lessons.js b/backend/scripts/repair-bisaya-children-lessons.js new file mode 100644 index 0000000..e2aec17 --- /dev/null +++ b/backend/scripts/repair-bisaya-children-lessons.js @@ -0,0 +1,171 @@ +#!/usr/bin/env node +/** + * Repariert alte/platzhalterhafte Prüfungen für Kinder‑Lektionen (z.B. Lektion 28). + * + * Nutzung: + * node backend/scripts/repair-bisaya-children-lessons.js + * + * Optional: + * VOCAB_FORCE_REBUILD_ALL=1 node backend/scripts/repair-bisaya-children-lessons.js + * + * Das Script sucht in der DB nach Kurslektionen mit lesson_number=28 oder + * mit typischen alten Titeln und ersetzt Platzhalter‑Exercises ("Option A/B/C/D") + * durch didaktisch passende Übungen. + */ + +import { sequelize } from '../utils/sequelize.js'; +import VocabCourseLesson from '../models/community/vocab_course_lesson.js'; +import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js'; + +const CONVERSATION_PROFILE = { + title: 'Kinder, Spiel & Routine', + description: 'Kinderalltag: kurze Anweisungen, Routinen und spielerische Aufforderungen', + learningGoals: [ + 'Kurze Aufforderungen und Routinen klar und freundlich formulieren', + 'Wichtige Alltagswörter (Tasche, Hände, Schlaf) sicher abrufen', + 'Routinen in der richtigen Reihenfolge ausdrücken' + ], + corePatterns: [ + { target: 'Hugas sa kamot.', gloss: 'Wasch dir die Hände.' }, + { target: 'Kuhaa imong bag.', gloss: 'Hol deine Tasche.' }, + { target: 'Matulog na ta.', gloss: 'Jetzt schlafen wir.' }, + { target: 'Patya ang suga.', gloss: 'Mach das Licht aus.' } + ], + speakingPrompts: [ + { title: 'Morgenroutine', prompt: 'Sprich eine kurze Morgenroutine: Tasche holen, Hände waschen, Essen.', cue: 'Kuhaa imong bag. Hugas sa kamot. Nikaon na ka?' } + ], + practicalTasks: [ + { title: 'Routinekette', text: 'Bilde eine Abfolge von vier kurzen Handlungsanweisungen für Kinder.' } + ], + exercises: [ + { + exerciseTypeId: 2, + title: 'Routinebefehl erkennen', + instruction: 'Wähle die richtige Bedeutung.', + questionData: { + type: 'multiple_choice', + question: 'Was bedeutet "Hugas sa kamot"?', + options: ['Wasch dir die Hände.', 'Hol deine Tasche.', 'Geh schlafen.', 'Iss zuerst.'] + }, + answerData: { type: 'multiple_choice', correctAnswer: 0 }, + explanation: '"Hugas sa kamot" ist eine typische Aufforderung zur Hygiene.' + }, + { + exerciseTypeId: 1, + title: 'Lücken: Morgenroutine', + instruction: 'Fülle die Lücken.', + questionData: { type: 'gap_fill', text: 'Kuhaa imong {gap}. Hugas sa {gap}.', gaps: 2 }, + answerData: { type: 'gap_fill', answers: ['bag', 'kamot'] }, + explanation: 'Tasche und Hände sind Kernwörter in Morgenroutinen.' + }, + { + exerciseTypeId: 3, + title: 'Routine reihenfolge', + instruction: 'Ordne die Sätze zu einer natürlichen Reihenfolge.', + questionData: { + type: 'sentence_building', + question: 'Ordne: Hände waschen, Tasche holen, Licht aus, Schlafen.', + tokens: ['Kuhaa imong bag.', 'Hugas sa kamot.', 'Patya ang suga.', 'Matulog na ta.'] + }, + answerData: { correct: ['Kuhaa imong bag. Hugas sa kamot. Patya ang suga. Matulog na ta.'] }, + explanation: 'Typische Abfolge für Abend-/Morgenroutinen.' + }, + { + exerciseTypeId: 10, + title: 'Kind-Ansage', + instruction: 'Gib eine kurze, freundliche Anweisung.', + questionData: { + type: 'situational_response', + question: 'Du möchtest, dass ein Kind seine Tasche holt und sich die Hände wäscht.', + keywords: ['kuhaa', 'bag', 'hugas', 'kamot'] + }, + answerData: { + modelAnswer: 'Kuhaa imong bag. Hugas sa kamot.', + keywords: ['kuhaa', 'bag', 'hugas', 'kamot'] + }, + explanation: 'Kurze klare Sätze funktionieren bei Kindern am besten.' + } + ] +}; + +function isPlaceholderExercise(exercise) { + const q = exercise?.questionData || {}; + const questionData = typeof q === 'string' ? JSON.parse(q) : q; + return Array.isArray(questionData?.options) + && questionData.options.some((option) => /^Option [A-D]$/i.test(String(option || '').trim())); +} + +async function repairLesson(lesson, ownerUserId, { force = false } = {}) { + const profile = CONVERSATION_PROFILE; + const existing = await VocabGrammarExercise.findAll({ where: { lessonId: lesson.id }, order: [['exerciseNumber', 'ASC']] }); + const hasPlaceholder = existing.some((exercise) => isPlaceholderExercise(exercise)); + const shouldReplaceExercises = force || hasPlaceholder || existing.length < profile.exercises.length; + + await lesson.update({ + title: profile.title, + description: profile.description, + learningGoals: profile.learningGoals, + corePatterns: profile.corePatterns, + speakingPrompts: profile.speakingPrompts, + practicalTasks: profile.practicalTasks, + targetScorePercent: Math.max(Number(lesson.targetScorePercent) || 0, 80) + }); + + if (!shouldReplaceExercises) return { updated: true, exercisesReplaced: false, exerciseCount: existing.length }; + + await VocabGrammarExercise.destroy({ where: { lessonId: lesson.id } }); + let exerciseNumber = 1; + for (const exercise of profile.exercises) { + await VocabGrammarExercise.create({ + lessonId: lesson.id, + exerciseTypeId: exercise.exerciseTypeId, + exerciseNumber, + title: exercise.title, + instruction: exercise.instruction, + questionData: exercise.questionData, + answerData: exercise.answerData, + explanation: exercise.explanation, + createdByUserId: ownerUserId + }); + exerciseNumber += 1; + } + + return { updated: true, exercisesReplaced: true, exerciseCount: profile.exercises.length }; +} + +async function main() { + await sequelize.authenticate(); + + const [bisayaLanguage] = await sequelize.query( + `SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`, + { type: sequelize.QueryTypes.SELECT } + ); + if (!bisayaLanguage) throw new Error('Bisaya language not found'); + + const rows = await sequelize.query( + `SELECT l.id, l.course_id, l.lesson_number, l.title, l.lesson_type, c.owner_user_id + FROM community.vocab_course_lesson l + JOIN community.vocab_course c ON c.id = l.course_id + WHERE c.language_id = :languageId + AND (l.lesson_number = 28 OR l.title IN ('Kinder & Spiel', 'Kinder, Spiel & Routine', 'Kinder & Familie')) + ORDER BY c.id, l.lesson_number`, + { replacements: { languageId: bisayaLanguage.id }, type: sequelize.QueryTypes.SELECT } + ); + + console.log(`Gefundene Kinderlektionen: ${rows.length}`); + let replaced = 0; + for (const row of rows) { + const lesson = await VocabCourseLesson.findByPk(row.id); + const result = await repairLesson(lesson, row.owner_user_id, { + force: process.env.VOCAB_FORCE_REBUILD_ALL === '1' + }); + if (result.exercisesReplaced) replaced += 1; + console.log(`✅ Kurs ${row.course_id}, Lektion ${row.lesson_number}: ${lesson.title} (${result.exerciseCount} Prüfung(en))`); + } + + console.log(`Fertig. Aktualisiert: ${rows.length}, Prüfungen ersetzt/erstellt: ${replaced}`); +} + +main() + .then(async () => { await sequelize.close(); }) + .catch(async (error) => { console.error('❌ Reparatur fehlgeschlagen:', error); await sequelize.close(); process.exit(1); });