From cee4492daec91859ce4c6524d3457d46257e5b05 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 22 May 2026 09:43:39 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20erweitere=20VocabLessonView=20mit=20Glo?= =?UTF-8?q?ssar-Optionen=20und=20verbessere=20die=20L=C3=BCckentextformati?= =?UTF-8?q?erung=20feat:=20f=C3=BCge=20Skript=20hinzu,=20um=20doppelte=20M?= =?UTF-8?q?uster=20in=20Lektionen=20zu=20identifizieren=20feat:=20implemen?= =?UTF-8?q?tiere=20Skript=20zur=20Suche=20nach=20=C3=9Cbungen=20anhand=20v?= =?UTF-8?q?on=20Text=20feat:=20erstelle=20Skript=20zur=20Reparatur=20von?= =?UTF-8?q?=20Multiple-Choice-Antworten=20feat:=20implementiere=20Skript?= =?UTF-8?q?=20zum=20Drucken=20von=20Lehrmusterinformationen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/scripts/find-duplicate-patterns.mjs | 42 +++++++++ backend/scripts/find-exercise-by-text.cjs | 31 +++++++ backend/scripts/print-lesson-patterns.mjs | 14 +++ backend/scripts/repair-fix-body-mc.cjs | 86 +++++++++++++++++++ frontend/src/views/social/VocabLessonView.vue | 14 ++- 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 backend/scripts/find-duplicate-patterns.mjs create mode 100644 backend/scripts/find-exercise-by-text.cjs create mode 100644 backend/scripts/print-lesson-patterns.mjs create mode 100644 backend/scripts/repair-fix-body-mc.cjs diff --git a/backend/scripts/find-duplicate-patterns.mjs b/backend/scripts/find-duplicate-patterns.mjs new file mode 100644 index 0000000..c43f4fe --- /dev/null +++ b/backend/scripts/find-duplicate-patterns.mjs @@ -0,0 +1,42 @@ +import { BISAYA_LESSONS_24_43_BY_NUMBER, BISAYA_DIDACTICS_24_43 } from './bisaya-course-plan-24-43.js'; +import { BISAYA_PHASE3_LESSONS, BISAYA_PHASE3_DIDACTICS } from './bisaya-course-phase3-extension.js'; + +// Build lesson->patterns for 24..63 using available didactics +const lessonPatterns = {}; +for (let n = 24; n <= 63; n++) { + const lesson = BISAYA_LESSONS_24_43_BY_NUMBER[n] || (BISAYA_PHASE3_LESSONS && BISAYA_PHASE3_LESSONS.find(l=>l.num===n)); + if (!lesson) continue; + const title = lesson.title; + let didactic = BISAYA_DIDACTICS_24_43[title] || (BISAYA_PHASE3_DIDACTICS && BISAYA_PHASE3_DIDACTICS[title]); + if (!didactic) continue; + const pats = (didactic.corePatterns || []).map(p => typeof p === 'string' ? p : p.target).filter(Boolean).map(s=>s.trim()); + lessonPatterns[n] = pats; +} + +// Build reverse map from pattern -> list of lessons +const patternMap = {}; +for (const [n, pats] of Object.entries(lessonPatterns)) { + for (const p of pats) { + patternMap[p] = patternMap[p] || []; + patternMap[p].push(Number(n)); + } +} + +// For lesson 26, list patterns that also appear in lessons <26 +const dupes = []; +const pats26 = lessonPatterns[26] || []; +for (const p of pats26) { + const appears = patternMap[p] || []; + const earlier = appears.filter(x=>x < 26); + if (earlier.length) dupes.push({pattern: p, earlier}); +} + +console.log('Patterns in lesson 26 that also appear in earlier lessons:'); +if (dupes.length === 0) console.log(' (none)'); +for (const d of dupes) console.log(` - ${d.pattern} (also in lessons: ${d.earlier.join(', ')})`); + +// Also list patterns in lesson 24-26 that are duplicates across 24-26 +console.log('\nPatterns repeated within 24..26:'); +const localMap = {}; +for (let n=24;n<=26;n++){ (lessonPatterns[n]||[]).forEach(p=>{ localMap[p]=localMap[p]||[]; localMap[p].push(n); }); } +for (const [p, arr] of Object.entries(localMap)) if (arr.length>1) console.log(` - ${p}: in lessons ${arr.join(', ')}`); diff --git a/backend/scripts/find-exercise-by-text.cjs b/backend/scripts/find-exercise-by-text.cjs new file mode 100644 index 0000000..a298aca --- /dev/null +++ b/backend/scripts/find-exercise-by-text.cjs @@ -0,0 +1,31 @@ +const { Op } = require('sequelize'); +const { sequelize } = require('../utils/sequelize.js'); +const VocabGrammarExercise = require('../models/community/vocab_grammar_exercise.js'); + +async function main() { + await sequelize.authenticate(); + const where = { + [Op.or]: [ + { questionData: { [Op.iLike]: '%Hals%' } }, + { questionData: { [Op.iLike]: '%Kehle%' } }, + { questionData: { [Op.iLike]: '%tutunlan%' } }, + { answerData: { [Op.iLike]: '%tutunlan%' } }, + { answerData: { [Op.iLike]: '%Sakit akong tutunlan%' } } + ] + }; + + const rows = await VocabGrammarExercise.findAll({ where, limit: 50 }); + if (!rows.length) { + console.log('No matching exercises found'); + process.exit(0); + } + for (const r of rows) { + console.log('---'); + console.log('id:', r.id, 'lessonId:', r.lessonId, 'exerciseTypeId:', r.exerciseTypeId, 'title:', r.title); + console.log('questionData:', r.questionData); + console.log('answerData:', r.answerData); + } + process.exit(0); +} + +main().catch((err) => { console.error(err); process.exit(2); }); diff --git a/backend/scripts/print-lesson-patterns.mjs b/backend/scripts/print-lesson-patterns.mjs new file mode 100644 index 0000000..78c7f19 --- /dev/null +++ b/backend/scripts/print-lesson-patterns.mjs @@ -0,0 +1,14 @@ +import { BISAYA_LESSONS_24_43_BY_NUMBER, BISAYA_DIDACTICS_24_43 } from './bisaya-course-plan-24-43.js'; + +for (const n of [24,25,26]) { + const lesson = BISAYA_LESSONS_24_43_BY_NUMBER[n]; + const title = lesson?.title || '[missing]'; + const didactic = BISAYA_DIDACTICS_24_43[title]; + console.log(`\nLektion ${n} — ${title}`); + if (!didactic) { console.log(' (keine Didaktik gefunden)'); continue; } + const patterns = didactic.corePatterns || []; + for (const p of patterns) { + if (typeof p === 'string') console.log(' -', p); + else console.log(' -', p.target || JSON.stringify(p)); + } +} diff --git a/backend/scripts/repair-fix-body-mc.cjs b/backend/scripts/repair-fix-body-mc.cjs new file mode 100644 index 0000000..f674c07 --- /dev/null +++ b/backend/scripts/repair-fix-body-mc.cjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +const { Op } = require('sequelize'); +const { sequelize } = require('../utils/sequelize.js'); +const VocabGrammarExercise = require('../models/community/vocab_grammar_exercise.js'); + +function parseArgs(argv) { + const args = { apply: false, ids: [] }; + argv.forEach((a) => { + if (a === '--apply') args.apply = true; + if (/^\d+$/.test(a)) args.ids.push(Number(a)); + }); + return args; +} + +function tryParse(json) { + if (!json) return null; + try { return typeof json === 'string' ? JSON.parse(json) : json; } catch (_) { return null; } +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + if (!args.ids.length) { + console.error('Usage: node repair-fix-body-mc.cjs [--apply]'); + process.exit(2); + } + + await sequelize.authenticate(); + + const rows = await VocabGrammarExercise.findAll({ where: { id: { [Op.in]: args.ids } } }); + if (!rows.length) { + console.log('No exercises found for ids', args.ids); + process.exit(0); + } + + const fixes = []; + for (const r of rows) { + const q = tryParse(r.questionData) || {}; + const a = tryParse(r.answerData) || {}; + const type = String(q.type || a.type || '').trim(); + if (type !== 'multiple_choice') { + console.log('skipping id', r.id, 'type', type); + continue; + } + const options = Array.isArray(q.options) ? q.options.map((o) => String(o).trim()) : []; + const correctIndex = Number(a.correctAnswer); + const target = 'tutunlan'; + const foundIndex = options.findIndex((opt) => opt.toLowerCase() === target.toLowerCase()); + if (foundIndex === -1) { + console.log('id', r.id, 'options do not contain', target, '->', options.join(', ')); + continue; + } + if (correctIndex === foundIndex) { + console.log('id', r.id, 'already correct (index', correctIndex, ')'); + continue; + } + fixes.push({ id: r.id, lessonId: r.lessonId, old: correctIndex, next: foundIndex, options }); + } + + if (!fixes.length) { + console.log('No fixes necessary'); + process.exit(0); + } + + console.log('Planned fixes:'); + fixes.forEach((f) => console.log(` id ${f.id} (lesson ${f.lessonId}): ${f.old} -> ${f.next} options: [${f.options.join(', ')}]`)); + + if (!args.apply) { + console.log('\nDry run complete. Re-run with --apply to persist changes.'); + process.exit(0); + } + + for (const f of fixes) { + const r = await VocabGrammarExercise.findByPk(f.id); + if (!r) continue; + const a = tryParse(r.answerData) || {}; + a.correctAnswer = f.next; + r.answerData = JSON.stringify(a); + await r.save(); + console.log('Updated id', f.id, '-> correctAnswer', f.next); + } + + console.log('All done'); + process.exit(0); +} + +main().catch((err) => { console.error(err); process.exit(2); }); diff --git a/frontend/src/views/social/VocabLessonView.vue b/frontend/src/views/social/VocabLessonView.vue index 8254864..c27ad4c 100644 --- a/frontend/src/views/social/VocabLessonView.vue +++ b/frontend/src/views/social/VocabLessonView.vue @@ -3216,7 +3216,13 @@ export default { // Bevorzugt: expliziter Lückentext if (qData && qData.text) { - return qData.text.replace(/\{gap\}/g, '_____'); + // Extrahiere eine eventuell angehängte Gloss‑Klammer am Ende, z.B. "... (Hals / Kehle)" + const raw = String(qData.text || ''); + const glossMatch = raw.match(/\(([^)]+)\)\s*$/); + const gloss = glossMatch ? glossMatch[1] : ''; + const base = glossMatch ? raw.replace(/\s*\([^)]+\)\s*$/, '') : raw; + const filled = base.replace(/\{gap\}/g, '_____'); + return gloss ? `${filled}
(${this.$sanitize ? this.$sanitize(gloss) : gloss})
` : filled; } // Fallbacks: Frage-Text, Erklärung oder Titel anzeigen @@ -4556,6 +4562,12 @@ export default { gap: 10px; } +.gap-gloss { + margin-top: 6px; + color: var(--color-text-secondary); + font-size: 0.9em; +} + .assistant-message { padding: 12px 14px; border-radius: var(--radius-md);