- MultipleChoiceCardForm: optionales `explanation`-Feld (Erklärung wird
nach Auswahl angezeigt); `field-optional`-Style ergänzt
- MultipleChoiceView: `explanation`-Prop; zeigt Erklärungsbox nach
Auswahl (grün bei richtig, neutral bei falsch); `{#key card_id}`-Block
erzwingt Remount bei Kartenwechsel — behebt State-Leak zwischen Karten
- edit/+page.svelte: MC-Edit-Bug behoben — Karten wurden fälschlich mit
`{front, back}` gespeichert und haben `answer`/`distractor_pool`
überschrieben; `MultipleChoiceCardForm` importiert und verdrahtet;
`canSave` und `onSubmit` handhaben MC korrekt; lädt `answer` +
`distractor_pool` beim Öffnen zurück in `mcOptions`-Array
- new/+page.svelte: `mcExplanation`-State an Form gebunden und beim
Speichern als `fields.explanation` gesetzt
- study/+page.svelte: `explanation` aus Card-Fields extrahiert und
an MultipleChoiceView durchgereicht
- scripts/migrate-factfulness-to-mc.ts: einmalige Migration — 13
Factfulness-Quiz-Karten von `basic` (A/B/C in Freitext) auf
`multiple-choice` mit strukturierten Feldern konvertiert; Deck auf
`visibility=public` gesetzt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
116 lines
3 KiB
TypeScript
116 lines
3 KiB
TypeScript
/**
|
|
* Migrates the 13 Factfulness quiz cards from type=basic (A/B/C embedded in
|
|
* front text) to type=multiple-choice with proper field structure.
|
|
* Also sets the deck to visibility=public.
|
|
*
|
|
* Run from repo root: bun --cwd apps/api run ../../scripts/migrate-factfulness-to-mc.ts
|
|
*/
|
|
|
|
import postgres from 'postgres';
|
|
|
|
const DECK_ID = '01KR96NH892HBP1TV5M3XJTB39';
|
|
const DB_URL = 'postgresql://cards:cards@localhost:5435/cards';
|
|
|
|
const sql = postgres(DB_URL);
|
|
|
|
interface CardRow {
|
|
id: string;
|
|
fields: { front?: string; back?: string };
|
|
}
|
|
|
|
function parseQuizCard(front: string, back: string): {
|
|
question: string;
|
|
answer: string;
|
|
distractorPool: string;
|
|
explanation: string;
|
|
} | null {
|
|
// front: "<question>\n\nA: opt1\nB: opt2\nC: opt3"
|
|
const splitIdx = front.indexOf('\n\nA:');
|
|
if (splitIdx === -1) return null;
|
|
|
|
const question = front.slice(0, splitIdx).trim();
|
|
const optionsBlock = front.slice(splitIdx + 2).trim(); // starts with "A: …"
|
|
|
|
const options: Record<string, string> = {};
|
|
for (const line of optionsBlock.split('\n')) {
|
|
const m = line.match(/^([A-D]):\s*(.+)$/);
|
|
if (m) options[m[1]] = m[2].trim();
|
|
}
|
|
|
|
// back: "LETTER — answer_text\n\nexplanation"
|
|
const backMatch = back.match(/^([A-D])\s*[—–-]+\s*(.+?)(?:\n\n([\s\S]*))?$/);
|
|
if (!backMatch) return null;
|
|
|
|
const correctLetter = backMatch[1];
|
|
const answer = options[correctLetter] ?? backMatch[2].trim();
|
|
const explanation = (backMatch[3] ?? '').trim();
|
|
|
|
const distractorPool = Object.entries(options)
|
|
.filter(([l]) => l !== correctLetter)
|
|
.map(([, v]) => v)
|
|
.join('\n');
|
|
|
|
return { question, answer, distractorPool, explanation };
|
|
}
|
|
|
|
async function run() {
|
|
console.log('Fetching basic cards from Factfulness deck…\n');
|
|
|
|
const rows = await sql<CardRow[]>`
|
|
SELECT id, fields
|
|
FROM cards.cards
|
|
WHERE deck_id = ${DECK_ID}
|
|
AND type = 'basic'
|
|
`;
|
|
|
|
let migrated = 0;
|
|
let skipped = 0;
|
|
|
|
for (const row of rows) {
|
|
const front = row.fields.front ?? '';
|
|
const back = row.fields.back ?? '';
|
|
|
|
if (!front.includes('\n\nA:')) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
const parsed = parseQuizCard(front, back);
|
|
if (!parsed) {
|
|
console.warn(` ⚠ Cannot parse ${row.id.slice(-8)}: "${front.slice(0, 60)}"`);
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
const { question, answer, distractorPool, explanation } = parsed;
|
|
|
|
const newFields: Record<string, string> = { front: question, answer };
|
|
if (distractorPool) newFields.distractor_pool = distractorPool;
|
|
if (explanation) newFields.explanation = explanation;
|
|
|
|
await sql`
|
|
UPDATE cards.cards
|
|
SET type = 'multiple-choice',
|
|
fields = ${sql.json(newFields)}
|
|
WHERE id = ${row.id}
|
|
`;
|
|
|
|
console.log(` ✓ ${row.id.slice(-8)} Q: "${question.slice(0, 55)}…"`);
|
|
migrated++;
|
|
}
|
|
|
|
await sql`
|
|
UPDATE cards.decks
|
|
SET visibility = 'public'
|
|
WHERE id = ${DECK_ID}
|
|
`;
|
|
console.log(`\n ✓ Deck → visibility=public`);
|
|
|
|
console.log(`\nDone: ${migrated} migrated, ${skipped} basic cards kept as-is.`);
|
|
await sql.end();
|
|
}
|
|
|
|
run().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|