feat: erweitere VocabLessonView mit Glossar-Optionen und verbessere die Lückentextformatierung
All checks were successful
Deploy to production / deploy (push) Successful in 2m8s
All checks were successful
Deploy to production / deploy (push) Successful in 2m8s
feat: füge Skript hinzu, um doppelte Muster in Lektionen zu identifizieren feat: implementiere Skript zur Suche nach Übungen anhand von Text feat: erstelle Skript zur Reparatur von Multiple-Choice-Antworten feat: implementiere Skript zum Drucken von Lehrmusterinformationen
This commit is contained in:
42
backend/scripts/find-duplicate-patterns.mjs
Normal file
42
backend/scripts/find-duplicate-patterns.mjs
Normal file
@@ -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(', ')}`);
|
||||
31
backend/scripts/find-exercise-by-text.cjs
Normal file
31
backend/scripts/find-exercise-by-text.cjs
Normal file
@@ -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); });
|
||||
14
backend/scripts/print-lesson-patterns.mjs
Normal file
14
backend/scripts/print-lesson-patterns.mjs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
86
backend/scripts/repair-fix-body-mc.cjs
Normal file
86
backend/scripts/repair-fix-body-mc.cjs
Normal file
@@ -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 <id...> [--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); });
|
||||
@@ -3216,7 +3216,13 @@ export default {
|
||||
|
||||
// Bevorzugt: expliziter Lückentext
|
||||
if (qData && qData.text) {
|
||||
return qData.text.replace(/\{gap\}/g, '<span class="gap">_____</span>');
|
||||
// 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, '<span class="gap">_____</span>');
|
||||
return gloss ? `${filled}<div class="gap-gloss">(${this.$sanitize ? this.$sanitize(gloss) : gloss})</div>` : 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);
|
||||
|
||||
Reference in New Issue
Block a user