From 381483dcf33c0bcc8cb83aeba02ee6571596ef27 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 14 Sep 2026 15:14:32 +0200 Subject: [PATCH] fix: a question slide's options are lettered once, and a bare-letter answer names its option DeepSeek flash wrote 'A. Start IVIG' into option text the renderer already letters, so the slide read 'A A. Start IVIG'; and an answer of just 'A' sat alone on the answer card, sending the room back a slide to see what A was. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP --- src/utils/deckSchema.js | 22 ++++++++++++++++++++-- test/deck-formats.test.js | 12 ++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/utils/deckSchema.js b/src/utils/deckSchema.js index 0699fa7a..799df34d 100644 --- a/src/utils/deckSchema.js +++ b/src/utils/deckSchema.js @@ -97,6 +97,20 @@ function text(value, max) { .slice(0, max || 400).trim(); } +var OPTION_LETTER = /^\(?([A-Ha-h])[.):]\s+/; + +function unletter(option) { + return option.replace(OPTION_LETTER, ''); +} + +function answerWithOption(answer, options) { + var letter = /^\(?([A-Ha-h])[.):]?$/.exec(answer.trim()); + if (!letter) return answer; + var at = 'ABCDEFGH'.indexOf(letter[1].toUpperCase()); + if (at < 0 || !options[at]) return letter[1].toUpperCase(); + return letter[1].toUpperCase() + ' — ' + options[at]; +} + function bullets(list) { if (!Array.isArray(list)) return []; return list.slice(0, 14).map(function (item) { @@ -167,9 +181,13 @@ function normalise(raw, gaps) { if (!out.text) return; } else if (type === 'question') { out.question = text(slide.question || slide.text, 400); + // The renderer letters the options itself, so a letter the model wrote + // into the text ("A. Start IVIG") would be drawn twice. out.options = (Array.isArray(slide.options) ? slide.options : []).slice(0, 6) - .map(function (o) { return text(typeof o === 'string' ? o : (o && o.text), 160); }).filter(Boolean); - out.answer = text(slide.answer, 300); + .map(function (o) { return unletter(text(typeof o === 'string' ? o : (o && o.text), 160)); }).filter(Boolean); + // An answer given as a bare letter is shown with the option it names: a + // card reading "A" alone made the room look back at the previous slide. + out.answer = answerWithOption(text(slide.answer, 300), out.options); out.explanation = text(slide.explanation, 500); if (!out.question) return; } else if (type === 'title') { diff --git a/test/deck-formats.test.js b/test/deck-formats.test.js index 2a29baed..51a38d65 100644 --- a/test/deck-formats.test.js +++ b/test/deck-formats.test.js @@ -68,3 +68,15 @@ test('the picker is told what a format is for, never the recipe itself', () => { assert.deepEqual(Object.keys(entry).sort(), ['audience', 'description', 'id', 'length', 'name']); } }); + +test('a question slide never letters its options twice, and a bare-letter answer names its option', () => { + const deck = deckSchema.normalise({ slides: [{ type: 'question', heading: 'Case 1', question: 'Next step?', + options: ['A. Start IVIG plus aspirin', 'B) Antibiotics', '(C) Observe', 'Give NSAIDs'], answer: 'A', explanation: 'x' }] }); + const q = deck.slides[0]; + assert.deepEqual(q.options, ['Start IVIG plus aspirin', 'Antibiotics', 'Observe', 'Give NSAIDs']); + assert.equal(q.answer, 'A — Start IVIG plus aspirin'); + const prose = deckSchema.normalise({ slides: [{ type: 'question', question: 'q', options: ['x', 'y'], answer: 'Start IVIG' }] }); + assert.equal(prose.slides[0].answer, 'Start IVIG', 'a written-out answer is left alone'); + const stray = deckSchema.normalise({ slides: [{ type: 'question', question: 'q', options: ['x'], answer: 'D.' }] }); + assert.equal(stray.slides[0].answer, 'D', 'a letter with no such option is shown as the letter'); +});