From 6ca278c0e6d507d93f822f80b9544e3e60f91f8f Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 14 Sep 2026 15:35:10 +0200 Subject: [PATCH] feat: deck cards fit their content, callouts use the theme's ink, questions can carry a film, formats end with references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compare cards were as tall as the body whatever they held, so two columns of four short bullets sat in cards 60% empty; a one-sentence callout sat in a card two thirds of the slide. Both now size to the text. The callout card's fixed brown ink was the worst contrast in the catalogue on Board Review's mint card; it takes the theme's ink. A question slide may carry image_prompt — a film beside the stem, drawn or left as a labelled frame like a figure. The evidence-based formats (board review, journal club, QI, abstract, debrief) gain a References part and the brief says the deck ends with it: DeepSeek skipped it when it was only suggested. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP --- assets/deck-formats.json | 970 ++++++++++++++++++++++++--- scripts/render_pptx.py | 41 +- src/utils/deckFormats.js | 2 +- src/utils/deckSchema.js | 11 +- src/utils/docSpec.js | 2 +- test/deck-image-placeholders.test.js | 16 +- 6 files changed, 944 insertions(+), 98 deletions(-) diff --git a/assets/deck-formats.json b/assets/deck-formats.json index 452e9a56..616f76f1 100644 --- a/assets/deck-formats.json +++ b/assets/deck-formats.json @@ -8,16 +8,107 @@ "description": "Objectives, background, a case that makes the problem concrete, the evidence, a management pathway, and a few take-home points. One aspect of the topic in depth rather than everything about it.", "length": "30–40 slides, 45–60 minutes with questions", "sequence": [ - { "part": "Title", "purpose": "the talk, the presenter and the department", "layouts": ["title"], "repeat": false, "guidance": "" }, - { "part": "Disclosures", "purpose": "conflicts of interest and accreditation, before any content", "layouts": ["callout"], "repeat": false, "guidance": "One line; 'no relevant financial relationships' is a complete slide." }, - { "part": "Learning objectives", "purpose": "what the audience will be able to do afterwards", "layouts": ["bullets"], "repeat": false, "guidance": "Three objectives, each with an explicit clinical relevance to a general pediatric audience." }, - { "part": "Background and burden", "purpose": "define the topic and its epidemiology", "layouts": ["section", "bullets", "table"], "repeat": false, "guidance": "Open the block with a divider slide. Keep this to a few minutes of material; it is the frame, not the talk." }, - { "part": "The case", "purpose": "a patient who makes the problem concrete", "layouts": ["section", "bullets", "question"], "repeat": true, "guidance": "Present the history and examination as a clinician met them, and stop with a question for the room before the answer. One case per aspect of the topic." }, - { "part": "Diagnostic workup", "purpose": "the differential and the data that narrowed it", "layouts": ["table", "image", "figure"], "repeat": false, "guidance": "Criteria, imaging and laboratory findings as a table or a figure, with the reasoning beside them." }, - { "part": "The evidence", "purpose": "the trials and guidelines behind current practice", "layouts": ["section", "table", "figure", "compare"], "repeat": true, "guidance": "Open with a divider. One slide per key study or guideline: design, population, the number that matters. Where two approaches compete, compare them side by side." }, - { "part": "Management pathway", "purpose": "how the evidence becomes a decision", "layouts": ["flow"], "repeat": false, "guidance": "A flowchart of the if-then decisions, not a list of options." }, - { "part": "Take-home points", "purpose": "the three to five things to remember", "layouts": ["callout", "bullets"], "repeat": false, "guidance": "Written on the slide, tied back to the objectives." }, - { "part": "References and acknowledgements", "purpose": "the literature cited and the people to thank", "layouts": ["table", "bullets"], "repeat": false, "guidance": "References last." } + { + "part": "Title", + "purpose": "the talk, the presenter and the department", + "layouts": [ + "title" + ], + "repeat": false, + "guidance": "" + }, + { + "part": "Disclosures", + "purpose": "conflicts of interest and accreditation, before any content", + "layouts": [ + "callout" + ], + "repeat": false, + "guidance": "One line; 'no relevant financial relationships' is a complete slide." + }, + { + "part": "Learning objectives", + "purpose": "what the audience will be able to do afterwards", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "Three objectives, each with an explicit clinical relevance to a general pediatric audience." + }, + { + "part": "Background and burden", + "purpose": "define the topic and its epidemiology", + "layouts": [ + "section", + "bullets", + "table" + ], + "repeat": false, + "guidance": "Open the block with a divider slide. Keep this to a few minutes of material; it is the frame, not the talk." + }, + { + "part": "The case", + "purpose": "a patient who makes the problem concrete", + "layouts": [ + "section", + "bullets", + "question" + ], + "repeat": true, + "guidance": "Present the history and examination as a clinician met them, and stop with a question for the room before the answer. One case per aspect of the topic." + }, + { + "part": "Diagnostic workup", + "purpose": "the differential and the data that narrowed it", + "layouts": [ + "table", + "image", + "figure" + ], + "repeat": false, + "guidance": "Criteria, imaging and laboratory findings as a table or a figure, with the reasoning beside them." + }, + { + "part": "The evidence", + "purpose": "the trials and guidelines behind current practice", + "layouts": [ + "section", + "table", + "figure", + "compare" + ], + "repeat": true, + "guidance": "Open with a divider. One slide per key study or guideline: design, population, the number that matters. Where two approaches compete, compare them side by side." + }, + { + "part": "Management pathway", + "purpose": "how the evidence becomes a decision", + "layouts": [ + "flow" + ], + "repeat": false, + "guidance": "A flowchart of the if-then decisions, not a list of options." + }, + { + "part": "Take-home points", + "purpose": "the three to five things to remember", + "layouts": [ + "callout", + "bullets" + ], + "repeat": false, + "guidance": "Written on the slide, tied back to the objectives." + }, + { + "part": "References and acknowledgements", + "purpose": "the literature cited and the people to thank", + "layouts": [ + "table", + "bullets" + ], + "repeat": false, + "guidance": "References last." + } ], "pitfalls": [ "trying to cover diagnosis, pathophysiology, treatment and follow-up in one talk — zoom in on one aspect", @@ -33,18 +124,127 @@ "description": "Why this paper, the PICO question, the methods step by step, the results with effect sizes, a structured critical appraisal, and whether it should change practice.", "length": "12–16 slides, 15–20 minutes plus discussion", "sequence": [ - { "part": "Title and full citation", "purpose": "the paper, authors, journal and year", "layouts": ["title"], "repeat": false, "guidance": "The full citation is the subtitle." }, - { "part": "Why this paper", "purpose": "the clinical situation that made it worth reading", "layouts": ["bullets"], "repeat": false, "guidance": "A short case or a real decision the team faced; one slide." }, - { "part": "The question in PICO", "purpose": "population, intervention, comparison, outcome", "layouts": ["table"], "repeat": false, "guidance": "A four-row table so the audience orients before the methods." }, - { "part": "Background", "purpose": "what was known before this paper", "layouts": ["bullets"], "repeat": false, "guidance": "Only what is needed to judge whether the question mattered." }, - { "part": "Methods", "purpose": "design, setting, population, intervention, comparator, outcomes, bias", "layouts": ["bullets", "table"], "repeat": true, "guidance": "Do not rush this: design and population on one slide, intervention, comparator and outcomes on another, risk of bias on a third. Flag pre-specified versus post-hoc analyses." }, - { "part": "Results", "purpose": "the key findings with their statistics", "layouts": ["table", "figure"], "repeat": true, "guidance": "Pair every p value with an absolute risk reduction or number needed to treat. One slide per primary outcome; secondary outcomes in a table." }, - { "part": "Critical appraisal", "purpose": "validity for and against the conclusion", "layouts": ["compare"], "repeat": false, "guidance": "Two columns: strengths and weaknesses of the internal validity, using a structured appraisal tool." }, - { "part": "Limitations", "purpose": "what the authors admit and what they did not", "layouts": ["compare"], "repeat": false, "guidance": "Acknowledged versus unacknowledged, side by side." }, - { "part": "Statistical versus clinical significance", "purpose": "the effect size in terms a clinician can act on", "layouts": ["callout"], "repeat": false, "guidance": "One sentence: how many patients treated, how many helped." }, - { "part": "Bottom line", "purpose": "whether and how this should change practice", "layouts": ["callout"], "repeat": false, "guidance": "A yes, a no, or a 'not yet, because'." }, - { "part": "Questions for discussion", "purpose": "what the group should argue about", "layouts": ["bullets", "question"], "repeat": true, "guidance": "Two or three open questions, or a single-best-answer question that exposes the paper's key judgment call." }, - { "part": "Next step", "purpose": "a concrete action for the group", "layouts": ["bullets"], "repeat": false, "guidance": "An audit, a guideline review, a protocol change to propose." } + { + "part": "Title and full citation", + "purpose": "the paper, authors, journal and year", + "layouts": [ + "title" + ], + "repeat": false, + "guidance": "The full citation is the subtitle." + }, + { + "part": "Why this paper", + "purpose": "the clinical situation that made it worth reading", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "A short case or a real decision the team faced; one slide." + }, + { + "part": "The question in PICO", + "purpose": "population, intervention, comparison, outcome", + "layouts": [ + "table" + ], + "repeat": false, + "guidance": "A four-row table so the audience orients before the methods." + }, + { + "part": "Background", + "purpose": "what was known before this paper", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "Only what is needed to judge whether the question mattered." + }, + { + "part": "Methods", + "purpose": "design, setting, population, intervention, comparator, outcomes, bias", + "layouts": [ + "bullets", + "table" + ], + "repeat": true, + "guidance": "Do not rush this: design and population on one slide, intervention, comparator and outcomes on another, risk of bias on a third. Flag pre-specified versus post-hoc analyses." + }, + { + "part": "Results", + "purpose": "the key findings with their statistics", + "layouts": [ + "table", + "figure" + ], + "repeat": true, + "guidance": "Pair every p value with an absolute risk reduction or number needed to treat. One slide per primary outcome; secondary outcomes in a table." + }, + { + "part": "Critical appraisal", + "purpose": "validity for and against the conclusion", + "layouts": [ + "compare" + ], + "repeat": false, + "guidance": "Two columns: strengths and weaknesses of the internal validity, using a structured appraisal tool." + }, + { + "part": "Limitations", + "purpose": "what the authors admit and what they did not", + "layouts": [ + "compare" + ], + "repeat": false, + "guidance": "Acknowledged versus unacknowledged, side by side." + }, + { + "part": "Statistical versus clinical significance", + "purpose": "the effect size in terms a clinician can act on", + "layouts": [ + "callout" + ], + "repeat": false, + "guidance": "One sentence: how many patients treated, how many helped." + }, + { + "part": "Bottom line", + "purpose": "whether and how this should change practice", + "layouts": [ + "callout" + ], + "repeat": false, + "guidance": "A yes, a no, or a 'not yet, because'." + }, + { + "part": "Questions for discussion", + "purpose": "what the group should argue about", + "layouts": [ + "bullets", + "question" + ], + "repeat": true, + "guidance": "Two or three open questions, or a single-best-answer question that exposes the paper's key judgment call." + }, + { + "part": "Next step", + "purpose": "a concrete action for the group", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "An audit, a guideline review, a protocol change to propose." + }, + { + "part": "References", + "purpose": "the paper and the guidelines or studies compared with it", + "layouts": [ + "table", + "bullets" + ], + "repeat": false, + "guidance": "Always last, even when short." + } ], "pitfalls": [ "rushing the methods, which is where the audience is lost", @@ -60,16 +260,106 @@ "description": "A real case revealed in stages — history, then examination, then results — with the differential re-ranked after each reveal, the diagnosis withheld until the reasoning is done, then the illness script and teaching points.", "length": "20–25 slides, 25–45 minutes", "sequence": [ - { "part": "Case opener", "purpose": "non-identifying demographics and the chief complaint", "layouts": ["title"], "repeat": false, "guidance": "One line of demographics and the complaint in the patient's words; nothing that identifies." }, - { "part": "Learning objectives", "purpose": "the three things the case will teach", "layouts": ["bullets"], "repeat": false, "guidance": "" }, - { "part": "Reveal", "purpose": "one category of data at a time: history, then past history and medications, then examination, then results", "layouts": ["bullets", "question", "image"], "repeat": true, "guidance": "Reveal one category per slide and stop with a question for the room before moving on. Include the relevant negatives that shaped the reasoning." }, - { "part": "Differential, narrowed", "purpose": "what the new data rules in or out", "layouts": ["table", "compare"], "repeat": true, "guidance": "Re-rank the differential after each reveal, with the findings for and against each diagnosis. This part alternates with the reveals." }, - { "part": "The clinching finding", "purpose": "the result, image or sign that resolves it", "layouts": ["image", "figure", "callout"], "repeat": false, "guidance": "Invite predictions on the slide before this one." }, - { "part": "Diagnosis", "purpose": "the confirmed diagnosis, plainly", "layouts": ["callout"], "repeat": false, "guidance": "Only after the differential has run its course." }, - { "part": "Illness script", "purpose": "epidemiology, pathophysiology and the typical presentation of the diagnosis", "layouts": ["table", "two", "flow"], "repeat": false, "guidance": "The three elements of an illness script, briefly; the mechanism as a flow if it teaches." }, - { "part": "Management and course", "purpose": "what was done and what happened", "layouts": ["flow", "bullets"], "repeat": false, "guidance": "Management follows the diagnosis, never precedes it." }, - { "part": "Teaching points", "purpose": "two to four durable lessons", "layouts": ["callout", "bullets"], "repeat": false, "guidance": "Tied back to the objectives." }, - { "part": "References", "purpose": "the literature behind the differential and the teaching", "layouts": ["table", "bullets"], "repeat": false, "guidance": "" } + { + "part": "Case opener", + "purpose": "non-identifying demographics and the chief complaint", + "layouts": [ + "title" + ], + "repeat": false, + "guidance": "One line of demographics and the complaint in the patient's words; nothing that identifies." + }, + { + "part": "Learning objectives", + "purpose": "the three things the case will teach", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "" + }, + { + "part": "Reveal", + "purpose": "one category of data at a time: history, then past history and medications, then examination, then results", + "layouts": [ + "bullets", + "question", + "image" + ], + "repeat": true, + "guidance": "Reveal one category per slide and stop with a question for the room before moving on. Include the relevant negatives that shaped the reasoning." + }, + { + "part": "Differential, narrowed", + "purpose": "what the new data rules in or out", + "layouts": [ + "table", + "compare" + ], + "repeat": true, + "guidance": "Re-rank the differential after each reveal, with the findings for and against each diagnosis. This part alternates with the reveals." + }, + { + "part": "The clinching finding", + "purpose": "the result, image or sign that resolves it", + "layouts": [ + "image", + "figure", + "callout" + ], + "repeat": false, + "guidance": "Invite predictions on the slide before this one." + }, + { + "part": "Diagnosis", + "purpose": "the confirmed diagnosis, plainly", + "layouts": [ + "callout" + ], + "repeat": false, + "guidance": "Only after the differential has run its course." + }, + { + "part": "Illness script", + "purpose": "epidemiology, pathophysiology and the typical presentation of the diagnosis", + "layouts": [ + "table", + "two", + "flow" + ], + "repeat": false, + "guidance": "The three elements of an illness script, briefly; the mechanism as a flow if it teaches." + }, + { + "part": "Management and course", + "purpose": "what was done and what happened", + "layouts": [ + "flow", + "bullets" + ], + "repeat": false, + "guidance": "Management follows the diagnosis, never precedes it." + }, + { + "part": "Teaching points", + "purpose": "two to four durable lessons", + "layouts": [ + "callout", + "bullets" + ], + "repeat": false, + "guidance": "Tied back to the objectives." + }, + { + "part": "References", + "purpose": "the literature behind the differential and the teaching", + "layouts": [ + "table", + "bullets" + ], + "repeat": false, + "guidance": "" + } ], "pitfalls": [ "showing all the history, examination and results at once", @@ -85,15 +375,92 @@ "description": "A no-blame framing, a lean case summary, a timeline of what was known when, a second discipline's view, a systems analysis by domain, and named, dated commitments. Systems learning, not clinical reasoning.", "length": "15–20 slides, 15–20 minutes per case plus discussion", "sequence": [ - { "part": "Ground rules", "purpose": "psychological safety before any detail", "layouts": ["title", "callout"], "repeat": false, "guidance": "Say that the forum is for systems learning, not blame, and that discussion stays in the room." }, - { "part": "Case summary", "purpose": "the minimum facts needed to follow what happened", "layouts": ["bullets"], "repeat": false, "guidance": "One slide, six or seven bullets, only the pertinent data. No identifying detail." }, - { "part": "Timeline", "purpose": "the sequence of decisions and changes in status", "layouts": ["flow", "bullets"], "repeat": true, "guidance": "One segment per phase of care, as bare chart facts and times; the context is spoken. Freeze each step at what was known then, not what is known now." }, - { "part": "Another team's view", "purpose": "the case as nursing or another discipline saw it", "layouts": ["bullets", "compare"], "repeat": false, "guidance": "Their account of the same hours, beside the physician account where they differ." }, - { "part": "Systems analysis", "purpose": "contributing causes across domains", "layouts": ["figure", "table", "flow"], "repeat": true, "guidance": "One slide per domain — communication, protocols, staffing, equipment, cognitive bias — each with three or four guiding questions for the room." }, - { "part": "What I would do differently", "purpose": "the presenter's own safeguards", "layouts": ["bullets"], "repeat": false, "guidance": "Specific and personal, not generic." }, - { "part": "Commitments", "purpose": "named, owned, dated changes", "layouts": ["table"], "repeat": false, "guidance": "Columns for action, owner and deadline; rows may be left blank to fill in by hand." }, - { "part": "Questions for the room", "purpose": "discussion after the presentation, not during", "layouts": ["bullets"], "repeat": false, "guidance": "" }, - { "part": "Previous commitments", "purpose": "closing the loop on the last conference", "layouts": ["table"], "repeat": false, "guidance": "Status of each earlier commitment; put it first if the session opens with it." } + { + "part": "Ground rules", + "purpose": "psychological safety before any detail", + "layouts": [ + "title", + "callout" + ], + "repeat": false, + "guidance": "Say that the forum is for systems learning, not blame, and that discussion stays in the room." + }, + { + "part": "Case summary", + "purpose": "the minimum facts needed to follow what happened", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "One slide, six or seven bullets, only the pertinent data. No identifying detail." + }, + { + "part": "Timeline", + "purpose": "the sequence of decisions and changes in status", + "layouts": [ + "flow", + "bullets" + ], + "repeat": true, + "guidance": "One segment per phase of care, as bare chart facts and times; the context is spoken. Freeze each step at what was known then, not what is known now." + }, + { + "part": "Another team's view", + "purpose": "the case as nursing or another discipline saw it", + "layouts": [ + "bullets", + "compare" + ], + "repeat": false, + "guidance": "Their account of the same hours, beside the physician account where they differ." + }, + { + "part": "Systems analysis", + "purpose": "contributing causes across domains", + "layouts": [ + "figure", + "table", + "flow" + ], + "repeat": true, + "guidance": "One slide per domain — communication, protocols, staffing, equipment, cognitive bias — each with three or four guiding questions for the room." + }, + { + "part": "What I would do differently", + "purpose": "the presenter's own safeguards", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "Specific and personal, not generic." + }, + { + "part": "Commitments", + "purpose": "named, owned, dated changes", + "layouts": [ + "table" + ], + "repeat": false, + "guidance": "Columns for action, owner and deadline; rows may be left blank to fill in by hand." + }, + { + "part": "Questions for the room", + "purpose": "discussion after the presentation, not during", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "" + }, + { + "part": "Previous commitments", + "purpose": "closing the loop on the last conference", + "layouts": [ + "table" + ], + "repeat": false, + "guidance": "Status of each earlier commitment; put it first if the session opens with it." + } ], "pitfalls": [ "letting the case slide become a differential-diagnosis exercise", @@ -109,11 +476,63 @@ "description": "Cycles of case stem and single-best-answer question, the answer, why each distractor is wrong, and one durable teaching point — then a recap. Question first, facts second.", "length": "15–25 cases, three or four slides each, 60–90 minutes", "sequence": [ - { "part": "Session title", "purpose": "the content area this block covers", "layouts": ["title"], "repeat": false, "guidance": "" }, - { "part": "Case and question", "purpose": "a vignette with one pivot point and a single-best-answer question", "layouts": ["question"], "repeat": true, "guidance": "Build the stem around one cue — age, duration, a vital sign, a laboratory value — that decides the answer; difficulty comes from subtle cues, never trick wording." }, - { "part": "Why the answer, why not the others", "purpose": "the reasoning for the correct option and the trap in each distractor", "layouts": ["compare", "bullets"], "repeat": true, "guidance": "Distractors are built from common reasoning errors and outdated recommendations; naming the trap is the teaching." }, - { "part": "Teaching point", "purpose": "one durable pearl beyond this question", "layouts": ["callout"], "repeat": true, "guidance": "One sentence, general enough to answer the next question on the topic." }, - { "part": "Recap", "purpose": "the pearls from every case in the block", "layouts": ["bullets", "table"], "repeat": false, "guidance": "" } + { + "part": "Session title", + "purpose": "the content area this block covers", + "layouts": [ + "title" + ], + "repeat": false, + "guidance": "" + }, + { + "part": "Case and question", + "purpose": "a vignette with one pivot point and a single-best-answer question", + "layouts": [ + "question" + ], + "repeat": true, + "guidance": "Build the stem around one cue — age, duration, a vital sign, a laboratory value — that decides the answer; difficulty comes from subtle cues, never trick wording." + }, + { + "part": "Why the answer, why not the others", + "purpose": "the reasoning for the correct option and the trap in each distractor", + "layouts": [ + "compare", + "bullets" + ], + "repeat": true, + "guidance": "Distractors are built from common reasoning errors and outdated recommendations; naming the trap is the teaching." + }, + { + "part": "Teaching point", + "purpose": "one durable pearl beyond this question", + "layouts": [ + "callout" + ], + "repeat": true, + "guidance": "One sentence, general enough to answer the next question on the topic." + }, + { + "part": "Recap", + "purpose": "the pearls from every case in the block", + "layouts": [ + "bullets", + "table" + ], + "repeat": false, + "guidance": "" + }, + { + "part": "References", + "purpose": "the sources behind the cases and teaching points", + "layouts": [ + "table", + "bullets" + ], + "repeat": false, + "guidance": "Always last, even when short." + } ], "pitfalls": [ "a lecture of facts with no question-first structure — that format shows no gain in examination scores", @@ -128,17 +547,114 @@ "description": "A topic lecture built to stay interactive: objectives, a case hook, tight background, the mechanism as a flow, presentation, diagnosis, management, the case resolved, and written take-home messages.", "length": "20–25 slides, 45 minutes", "sequence": [ - { "part": "Title", "purpose": "topic, presenter, date", "layouts": ["title"], "repeat": false, "guidance": "" }, - { "part": "Learning objectives", "purpose": "specific, measurable objectives", "layouts": ["bullets"], "repeat": false, "guidance": "Decide the one to three central messages first; the objectives follow from them." }, - { "part": "Opening case", "purpose": "a patient who poses the question the talk answers", "layouts": ["question", "bullets"], "repeat": true, "guidance": "Pose it as a question for the room and leave it open until the resolution. A long talk may run one case per section." }, - { "part": "Background", "purpose": "incidence, risk factors, burden", "layouts": ["bullets", "table"], "repeat": false, "guidance": "Three to five minutes of material at most." }, - { "part": "Mechanism", "purpose": "the pathophysiology that explains the picture", "layouts": ["flow"], "repeat": false, "guidance": "Insult to cascade to clinical effect, as a flow rather than prose." }, - { "part": "Presentation", "purpose": "how the disease shows up", "layouts": ["bullets", "image", "figure"], "repeat": false, "guidance": "A picture where a picture says it." }, - { "part": "Diagnosis", "purpose": "the differential and how to narrow it", "layouts": ["compare", "table"], "repeat": false, "guidance": "" }, - { "part": "Management", "purpose": "the treatment approach", "layouts": ["flow", "bullets", "callout"], "repeat": false, "guidance": "A stepwise algorithm as a flow; a dose or a rule of thumb as a callout." }, - { "part": "Case resolved", "purpose": "the opening case answered", "layouts": ["callout"], "repeat": true, "guidance": "One per opening case." }, - { "part": "Take-home messages", "purpose": "the three to five points, written down", "layouts": ["bullets", "callout"], "repeat": false, "guidance": "Spoken take-home messages are not remembered; written ones are." }, - { "part": "References", "purpose": "sources for the claims made", "layouts": ["table", "bullets"], "repeat": false, "guidance": "" } + { + "part": "Title", + "purpose": "topic, presenter, date", + "layouts": [ + "title" + ], + "repeat": false, + "guidance": "" + }, + { + "part": "Learning objectives", + "purpose": "specific, measurable objectives", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "Decide the one to three central messages first; the objectives follow from them." + }, + { + "part": "Opening case", + "purpose": "a patient who poses the question the talk answers", + "layouts": [ + "question", + "bullets" + ], + "repeat": true, + "guidance": "Pose it as a question for the room and leave it open until the resolution. A long talk may run one case per section." + }, + { + "part": "Background", + "purpose": "incidence, risk factors, burden", + "layouts": [ + "bullets", + "table" + ], + "repeat": false, + "guidance": "Three to five minutes of material at most." + }, + { + "part": "Mechanism", + "purpose": "the pathophysiology that explains the picture", + "layouts": [ + "flow" + ], + "repeat": false, + "guidance": "Insult to cascade to clinical effect, as a flow rather than prose." + }, + { + "part": "Presentation", + "purpose": "how the disease shows up", + "layouts": [ + "bullets", + "image", + "figure" + ], + "repeat": false, + "guidance": "A picture where a picture says it." + }, + { + "part": "Diagnosis", + "purpose": "the differential and how to narrow it", + "layouts": [ + "compare", + "table" + ], + "repeat": false, + "guidance": "" + }, + { + "part": "Management", + "purpose": "the treatment approach", + "layouts": [ + "flow", + "bullets", + "callout" + ], + "repeat": false, + "guidance": "A stepwise algorithm as a flow; a dose or a rule of thumb as a callout." + }, + { + "part": "Case resolved", + "purpose": "the opening case answered", + "layouts": [ + "callout" + ], + "repeat": true, + "guidance": "One per opening case." + }, + { + "part": "Take-home messages", + "purpose": "the three to five points, written down", + "layouts": [ + "bullets", + "callout" + ], + "repeat": false, + "guidance": "Spoken take-home messages are not remembered; written ones are." + }, + { + "part": "References", + "purpose": "sources for the claims made", + "layouts": [ + "table", + "bullets" + ], + "repeat": false, + "guidance": "" + } ], "pitfalls": [ "a straight lecture with no case and no question — residents ask for the alternative, and it retains better", @@ -153,11 +669,66 @@ "description": "Objectives, a reactions phase, a shared account of what happened, then plus-and-delta analysis against each objective, and a summary with commitments. A short deck that structures a conversation.", "length": "8–12 slides, 20–30 minutes — as long as the simulation itself or longer", "sequence": [ - { "part": "Scenario and objectives", "purpose": "what the scenario was designed to teach", "layouts": ["title", "bullets"], "repeat": false, "guidance": "The objectives set before the simulation, restated." }, - { "part": "Reactions", "purpose": "room for the emotional response first", "layouts": ["callout"], "repeat": false, "guidance": "One open question: how did that feel? Nothing else on the slide." }, - { "part": "What happened", "purpose": "a shared factual account before any judgment", "layouts": ["bullets", "image", "flow"], "repeat": false, "guidance": "Events in order, as facts, so everyone is analysing the same scenario." }, - { "part": "Plus and delta", "purpose": "what went well and what should change, per objective", "layouts": ["two", "compare"], "repeat": true, "guidance": "One slide per objective. Ask and discuss the plus before the delta; never both at once, or the positive never gets its turn." }, - { "part": "Summary and commitments", "purpose": "teaching points and what each person will do differently", "layouts": ["callout", "bullets"], "repeat": false, "guidance": "Confirm each objective was reviewed; commitments are behaviour-specific." } + { + "part": "Scenario and objectives", + "purpose": "what the scenario was designed to teach", + "layouts": [ + "title", + "bullets" + ], + "repeat": false, + "guidance": "The objectives set before the simulation, restated." + }, + { + "part": "Reactions", + "purpose": "room for the emotional response first", + "layouts": [ + "callout" + ], + "repeat": false, + "guidance": "One open question: how did that feel? Nothing else on the slide." + }, + { + "part": "What happened", + "purpose": "a shared factual account before any judgment", + "layouts": [ + "bullets", + "image", + "flow" + ], + "repeat": false, + "guidance": "Events in order, as facts, so everyone is analysing the same scenario." + }, + { + "part": "Plus and delta", + "purpose": "what went well and what should change, per objective", + "layouts": [ + "two", + "compare" + ], + "repeat": true, + "guidance": "One slide per objective. Ask and discuss the plus before the delta; never both at once, or the positive never gets its turn." + }, + { + "part": "Summary and commitments", + "purpose": "teaching points and what each person will do differently", + "layouts": [ + "callout", + "bullets" + ], + "repeat": false, + "guidance": "Confirm each objective was reviewed; commitments are behaviour-specific." + }, + { + "part": "References", + "purpose": "the guideline or protocol the scenario tested", + "layouts": [ + "table", + "bullets" + ], + "repeat": false, + "guidance": "Always last, even when short." + } ], "pitfalls": [ "spending the time on deltas and skipping why the good moments worked", @@ -173,14 +744,82 @@ "description": "What it is, why it happens, the warning signs, the plan, what to do at home, when to seek help, and a teach-back check. Two or three concepts, plain words, active voice.", "length": "7–10 slides, 10–15 minutes", "sequence": [ - { "part": "Title", "purpose": "the condition in the words a family would use", "layouts": ["title"], "repeat": false, "guidance": "The everyday name, not the clinical term." }, - { "part": "What it is", "purpose": "two or three core ideas", "layouts": ["bullets"], "repeat": false, "guidance": "Define any technical word the moment it appears." }, - { "part": "Why it happens", "purpose": "a simple cause", "layouts": ["bullets", "figure"], "repeat": false, "guidance": "Short statements or one simple picture; not a pathophysiology lecture." }, - { "part": "What to watch for", "purpose": "the signs that change the plan", "layouts": ["callout"], "repeat": false, "guidance": "A numbered checklist of two or three signs." }, - { "part": "What we will do", "purpose": "the treatment plan in active voice", "layouts": ["bullets", "table"], "repeat": false, "guidance": "Concrete instructions: 'give one spoon after breakfast', not 'medication will be administered'." }, - { "part": "What you can do at home", "purpose": "the family's part", "layouts": ["bullets"], "repeat": true, "guidance": "One slide per situation if there are several — at home, at school, at night." }, - { "part": "When to seek help", "purpose": "routine follow-up versus emergency", "layouts": ["compare", "callout"], "repeat": false, "guidance": "'Call us' beside 'go to the emergency department', as two columns." }, - { "part": "Teach-back", "purpose": "checking the family understood", "layouts": ["question", "bullets"], "repeat": false, "guidance": "Open questions — 'how will you give the medicine tonight?' — never 'do you understand?'" } + { + "part": "Title", + "purpose": "the condition in the words a family would use", + "layouts": [ + "title" + ], + "repeat": false, + "guidance": "The everyday name, not the clinical term." + }, + { + "part": "What it is", + "purpose": "two or three core ideas", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "Define any technical word the moment it appears." + }, + { + "part": "Why it happens", + "purpose": "a simple cause", + "layouts": [ + "bullets", + "figure" + ], + "repeat": false, + "guidance": "Short statements or one simple picture; not a pathophysiology lecture." + }, + { + "part": "What to watch for", + "purpose": "the signs that change the plan", + "layouts": [ + "callout" + ], + "repeat": false, + "guidance": "A numbered checklist of two or three signs." + }, + { + "part": "What we will do", + "purpose": "the treatment plan in active voice", + "layouts": [ + "bullets", + "table" + ], + "repeat": false, + "guidance": "Concrete instructions: 'give one spoon after breakfast', not 'medication will be administered'." + }, + { + "part": "What you can do at home", + "purpose": "the family's part", + "layouts": [ + "bullets" + ], + "repeat": true, + "guidance": "One slide per situation if there are several — at home, at school, at night." + }, + { + "part": "When to seek help", + "purpose": "routine follow-up versus emergency", + "layouts": [ + "compare", + "callout" + ], + "repeat": false, + "guidance": "'Call us' beside 'go to the emergency department', as two columns." + }, + { + "part": "Teach-back", + "purpose": "checking the family understood", + "layouts": [ + "question", + "bullets" + ], + "repeat": false, + "guidance": "Open questions — 'how will you give the medicine tonight?' — never 'do you understand?'" + } ], "pitfalls": [ "more than five pieces of information on one slide", @@ -196,16 +835,111 @@ "description": "The problem in a story, a SMART aim, the theory of change, the measures, one slide per PDSA cycle, the run chart, honest lessons, and the plan to sustain and spread.", "length": "10–12 slides, 10–15 minutes plus discussion", "sequence": [ - { "part": "Title and team", "purpose": "project, team, institution, dates", "layouts": ["title"], "repeat": false, "guidance": "" }, - { "part": "The problem", "purpose": "why it matters here, in a real story", "layouts": ["bullets", "callout"], "repeat": false, "guidance": "Local context and one patient or staff story that lands." }, - { "part": "Aim", "purpose": "what, by how much, by when, for whom", "layouts": ["callout"], "repeat": false, "guidance": "One SMART sentence: 'By June, reduce X from 40% to 20% in Y'. 'Improve care' is not an aim." }, - { "part": "Theory of change", "purpose": "the drivers and change ideas", "layouts": ["flow", "table"], "repeat": false, "guidance": "Aim to primary drivers to change ideas, as a flow or a three-column table." }, - { "part": "Measures", "purpose": "outcome, process and balancing measures", "layouts": ["table"], "repeat": false, "guidance": "Three rows, each with its definition and data source; the balancing measure is what catches unintended harm." }, - { "part": "PDSA cycle", "purpose": "one test of change: plan, do, study, act", "layouts": ["bullets", "table"], "repeat": true, "guidance": "One slide per cycle. Study must compare what happened with what was predicted, or there is no evidence of improvement." }, - { "part": "Results", "purpose": "the measure over time", "layouts": ["figure", "image"], "repeat": false, "guidance": "A run chart or control chart, annotated with when each cycle started." }, - { "part": "Lessons", "purpose": "what worked, what did not, and why", "layouts": ["bullets", "compare"], "repeat": false, "guidance": "Honest; the failures teach more than the successes." }, - { "part": "Sustain and spread", "purpose": "how the gain will hold and where it goes next", "layouts": ["bullets"], "repeat": false, "guidance": "" }, - { "part": "Questions for discussion", "purpose": "room for challenge", "layouts": ["bullets"], "repeat": false, "guidance": "" } + { + "part": "Title and team", + "purpose": "project, team, institution, dates", + "layouts": [ + "title" + ], + "repeat": false, + "guidance": "" + }, + { + "part": "The problem", + "purpose": "why it matters here, in a real story", + "layouts": [ + "bullets", + "callout" + ], + "repeat": false, + "guidance": "Local context and one patient or staff story that lands." + }, + { + "part": "Aim", + "purpose": "what, by how much, by when, for whom", + "layouts": [ + "callout" + ], + "repeat": false, + "guidance": "One SMART sentence: 'By June, reduce X from 40% to 20% in Y'. 'Improve care' is not an aim." + }, + { + "part": "Theory of change", + "purpose": "the drivers and change ideas", + "layouts": [ + "flow", + "table" + ], + "repeat": false, + "guidance": "Aim to primary drivers to change ideas, as a flow or a three-column table." + }, + { + "part": "Measures", + "purpose": "outcome, process and balancing measures", + "layouts": [ + "table" + ], + "repeat": false, + "guidance": "Three rows, each with its definition and data source; the balancing measure is what catches unintended harm." + }, + { + "part": "PDSA cycle", + "purpose": "one test of change: plan, do, study, act", + "layouts": [ + "bullets", + "table" + ], + "repeat": true, + "guidance": "One slide per cycle. Study must compare what happened with what was predicted, or there is no evidence of improvement." + }, + { + "part": "Results", + "purpose": "the measure over time", + "layouts": [ + "figure", + "image" + ], + "repeat": false, + "guidance": "A run chart or control chart, annotated with when each cycle started." + }, + { + "part": "Lessons", + "purpose": "what worked, what did not, and why", + "layouts": [ + "bullets", + "compare" + ], + "repeat": false, + "guidance": "Honest; the failures teach more than the successes." + }, + { + "part": "Sustain and spread", + "purpose": "how the gain will hold and where it goes next", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "" + }, + { + "part": "Questions for discussion", + "purpose": "room for challenge", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "" + }, + { + "part": "References", + "purpose": "the guidance and evidence the change ideas rest on", + "layouts": [ + "table", + "bullets" + ], + "repeat": false, + "guidance": "Always last, even when short." + } ], "pitfalls": [ "a vague aim", @@ -221,12 +955,74 @@ "description": "Six slides in five minutes: title, the question, methods in three bullets, the key result as a figure, one conclusion, and questions. Every word budgeted.", "length": "6 slides, 5 minutes; 8–10 slides for a 10-minute slot", "sequence": [ - { "part": "Title and authors", "purpose": "the study, authors and affiliations", "layouts": ["title"], "repeat": false, "guidance": "A title of about six words." }, - { "part": "Question", "purpose": "the clinical problem and the objective", "layouts": ["bullets", "figure"], "repeat": false, "guidance": "A visual overview of the context; as little text as possible." }, - { "part": "Methods", "purpose": "design and key measures", "layouts": ["bullets", "flow"], "repeat": false, "guidance": "Three bullets; no figure that needs its axes explained." }, - { "part": "Results", "purpose": "the key finding, shown", "layouts": ["figure", "table"], "repeat": true, "guidance": "One finding per slide, and only findings the conclusion depends on." }, - { "part": "Conclusion", "purpose": "one take-home message tied to the objective", "layouts": ["callout", "bullets"], "repeat": false, "guidance": "One sentence the audience could repeat." }, - { "part": "Limitations and questions", "purpose": "what to say when asked", "layouts": ["bullets"], "repeat": false, "guidance": "Name the limitations before the judges do." } + { + "part": "Title and authors", + "purpose": "the study, authors and affiliations", + "layouts": [ + "title" + ], + "repeat": false, + "guidance": "A title of about six words." + }, + { + "part": "Question", + "purpose": "the clinical problem and the objective", + "layouts": [ + "bullets", + "figure" + ], + "repeat": false, + "guidance": "A visual overview of the context; as little text as possible." + }, + { + "part": "Methods", + "purpose": "design and key measures", + "layouts": [ + "bullets", + "flow" + ], + "repeat": false, + "guidance": "Three bullets; no figure that needs its axes explained." + }, + { + "part": "Results", + "purpose": "the key finding, shown", + "layouts": [ + "figure", + "table" + ], + "repeat": true, + "guidance": "One finding per slide, and only findings the conclusion depends on." + }, + { + "part": "Conclusion", + "purpose": "one take-home message tied to the objective", + "layouts": [ + "callout", + "bullets" + ], + "repeat": false, + "guidance": "One sentence the audience could repeat." + }, + { + "part": "Limitations and questions", + "purpose": "what to say when asked", + "layouts": [ + "bullets" + ], + "repeat": false, + "guidance": "Name the limitations before the judges do." + }, + { + "part": "References", + "purpose": "the key references, on one slide", + "layouts": [ + "table", + "bullets" + ], + "repeat": false, + "guidance": "Always last, even when short." + } ], "pitfalls": [ "text the audience reads instead of listening", @@ -236,4 +1032,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/scripts/render_pptx.py b/scripts/render_pptx.py index 35b720d0..0a40804e 100644 --- a/scripts/render_pptx.py +++ b/scripts/render_pptx.py @@ -550,9 +550,18 @@ def slide_compare(prs, spec): sizes = [_fit_size(c.get("bullets") or [], 0.44, int(BODY_H) - int(Emu(548640))) for c in columns] or [BULLET_SIZES[0]] size = min(sizes) + # The cards are as tall as the fuller column needs, not the whole body: two + # columns of four short bullets used to sit in cards that were 60% empty. + line_emu, gap_emu = Pt(size * 1.35).emu, Pt(size * 0.55).emu + need = 0 + for column in columns: + total = sum(_estimate_lines(item.get("text", ""), size, 0.44) * line_emu + gap_emu + for item in (column.get("bullets") or [])) + need = max(need, total) + card_h = Emu(max(int(BODY_H * 0.5), min(int(BODY_H), need + 822960 + 365760))) for index, column in enumerate(columns): left = MARGIN + (col_w + gutter) * index - card = slide.shapes.add_shape(5, left, BODY_TOP, col_w, BODY_H) # rounded rect + card = slide.shapes.add_shape(5, left, BODY_TOP, col_w, card_h) # rounded rect card.fill.solid() card.fill.fore_color.rgb = tints[index % 2] card.line.color.rgb = edges[index % 2] @@ -566,7 +575,7 @@ def slide_compare(prs, spec): bold=True, color=edges[index % 2]) frame = _textbox(slide, left + Emu(228600), BODY_TOP + Emu(640080), - Emu(int(col_w) - 457200), Emu(int(BODY_H) - 822960)) + Emu(int(col_w) - 457200), Emu(int(card_h) - 822960)) _bullets(frame, column.get("bullets") or [], size, width_frac=0.44) _notes(slide, spec.get("notes")) return slide @@ -578,9 +587,11 @@ def slide_callout(prs, spec): _heading(slide, spec.get("heading") or "") text = (spec.get("text") or "").strip() size = 28 if len(text) <= 90 else (22 if len(text) <= 180 else 18) + # One sentence does not need a card two thirds of the slide tall. + card_frac = 0.42 if len(text) <= 90 else (0.52 if len(text) <= 180 else 0.62) if (STYLE.get("callout") or "card") == "stripe": # A white card with a thick accent stripe and a mark in the margin. - card_h = Emu(int(BODY_H * 0.62)) + card_h = Emu(int(BODY_H * card_frac)) _rect(slide, MARGIN, BODY_TOP, BODY_W, card_h, TINT, shape=5) _rect(slide, MARGIN, BODY_TOP, Emu(182880), card_h, ACCENT_ALT) mark = _rect(slide, MARGIN + Emu(457200), BODY_TOP + Emu(int(card_h / 2)) - Emu(320040), Emu(640080), Emu(640080), ACCENT_ALT, shape=9) @@ -595,7 +606,7 @@ def slide_callout(prs, spec): _run(para, chunk, size, bold=True, color=INK) _notes(slide, spec.get("notes")) return slide - card = slide.shapes.add_shape(5, MARGIN, BODY_TOP, BODY_W, Emu(int(BODY_H * 0.62))) + card = slide.shapes.add_shape(5, MARGIN, BODY_TOP, BODY_W, Emu(int(BODY_H * card_frac))) card.fill.solid() card.fill.fore_color.rgb = TINT_ALT card.line.color.rgb = ACCENT_ALT @@ -604,12 +615,14 @@ def slide_callout(prs, spec): card.text_frame.text = "" frame = _textbox(slide, MARGIN + Emu(457200), BODY_TOP + Emu(365760), - Emu(int(BODY_W) - 914400), Emu(int(BODY_H * 0.62) - 731520)) + Emu(int(BODY_W) - 914400), Emu(int(BODY_H * card_frac) - 731520)) frame.vertical_anchor = MSO_ANCHOR.MIDDLE para = frame.paragraphs[0] para.alignment = PP_ALIGN.CENTER + # The theme's ink, not a fixed brown: brown on a mint card was the worst + # contrast in the catalogue. for chunk, bold in _split_bold(text): - _run(para, chunk, size, bold=bold or True, color=RGBColor(0x78, 0x35, 0x0F)) + _run(para, chunk, size, bold=bold or True, color=INK) _notes(slide, spec.get("notes")) return slide @@ -642,7 +655,21 @@ def slide_question(prs, spec): mp.alignment = PP_ALIGN.CENTER mark.text_frame.vertical_anchor = MSO_ANCHOR.MIDDLE _run(mp, "?", 80, bold=True, color=PAPER if style == "spotlight" else ACCENT) - frame = _textbox(slide, MARGIN + mark_w + Emu(457200), BODY_TOP, BODY_W - mark_w - Emu(457200), BODY_H) + # A film or a photograph beside the stem, when the question carries one: + # the text takes the left part and the picture (or its empty frame) the + # right third, as on a figure slide. + path = spec.get("image") + has_picture = bool(path and os.path.exists(path)) or bool(spec.get("placeholder")) + pic_w = Emu(int(BODY_W * 0.34)) if has_picture else Emu(0) + text_w = BODY_W - mark_w - Emu(457200) - pic_w - (Emu(365760) if has_picture else Emu(0)) + frame = _textbox(slide, MARGIN + mark_w + Emu(457200), BODY_TOP, text_w, BODY_H) + if has_picture: + pic_left = MARGIN + BODY_W - pic_w + if path and os.path.exists(path): + draw_image(slide, {"image": path, "x": 100.0 * int(pic_left) / int(SLIDE_W), "y": 100.0 * int(BODY_TOP) / int(SLIDE_H), + "w": 100.0 * int(pic_w) / int(SLIDE_W), "h": 100.0 * int(BODY_H) / int(SLIDE_H)}) + else: + _placeholder(slide, pic_left, BODY_TOP, pic_w, BODY_H, spec.get("image_prompt") or spec.get("caption")) para = frame.paragraphs[0] _run(para, question, 26 if len(question) <= 120 else (22 if len(question) <= 220 else 18), bold=True, color=fg) letters = "ABCDEFGH" diff --git a/src/utils/deckFormats.js b/src/utils/deckFormats.js index a8003210..88568def 100644 --- a/src/utils/deckFormats.js +++ b/src/utils/deckFormats.js @@ -64,7 +64,7 @@ function instructions(id) { lines.push('A part marked repeatable may be used more than once — once per case, event or question —'); lines.push('and a run of repeatable parts repeats as a unit, as many times as the material needs.'); lines.push('Leave out a part the material does not have. Keep the order between parts.'); - lines.push('Every slide is still one of the types above.'); + lines.push('Every slide is still one of the types above. If the format lists a References part, the deck ends with it.'); if (Array.isArray(format.pitfalls) && format.pitfalls.length) { lines.push('What this format goes wrong on: ' + format.pitfalls.join('; ') + '.'); } diff --git a/src/utils/deckSchema.js b/src/utils/deckSchema.js index 799df34d..cb9059df 100644 --- a/src/utils/deckSchema.js +++ b/src/utils/deckSchema.js @@ -48,6 +48,8 @@ function instructions(slideCount, figureCount) { ' A question for the room, drawn with a question mark and lettered options;', ' the answer and its one-line explanation go on the slide after, so people', ' commit first. One or two per deck, at the end of a part, is about right.', + ' Options are plain text — the letters are drawn for you. A question may', + ' carry "image_prompt" for a film or photograph shown beside the stem.', '', 'Any bullets, table, compare, flow or figure slide may carry "takeaway": one', 'sentence the audience should leave with; it is drawn as a key-point strip.', @@ -190,6 +192,13 @@ function normalise(raw, gaps) { out.answer = answerWithOption(text(slide.answer, 300), out.options); out.explanation = text(slide.explanation, 500); if (!out.question) return; + // A film or photograph beside the stem: drawn when illustration is on, + // an empty labelled frame otherwise, like a figure slide. + if (slide.image_prompt || slide.imagePrompt) { + out.image_prompt = text(slide.image_prompt || slide.imagePrompt, 1200); + out.caption = text(slide.caption, 200); + if (slide.placeholder === true) out.placeholder = true; + } } else if (type === 'title') { out.subtitle = text(slide.subtitle, 200); out.date = text(slide.date, 60); @@ -219,7 +228,7 @@ function normalise(raw, gaps) { // no one found out. The intent is honoured instead: a slide with words // becomes a figure, one without becomes a full-slide image. if (slide.image_prompt && type !== 'figure' && type !== 'image' - && type !== 'title' && type !== 'custom') { + && type !== 'title' && type !== 'custom' && type !== 'question') { out.type = type = (out.bullets && out.bullets.length) || (slide.bullets || []).length ? 'figure' : 'image'; if (type === 'figure' && !(out.bullets || []).length) out.bullets = bullets(slide.bullets); diff --git a/src/utils/docSpec.js b/src/utils/docSpec.js index e818efce..4710dd78 100644 --- a/src/utils/docSpec.js +++ b/src/utils/docSpec.js @@ -82,7 +82,7 @@ function fromDeck(deck, images) { if (slide.image_job && byJob[slide.image_job]) { blocks.push({ type: 'image', path: byJob[slide.image_job], caption: text(slide.caption, 300) }); - } else if (slide.placeholder && (slide.type === 'image' || slide.type === 'figure')) { + } else if (slide.placeholder && (slide.type === 'image' || slide.type === 'figure' || slide.type === 'question')) { // The frame the deck keeps for a picture added by hand, as a line that // says what belongs there. blocks.push({ type: 'para', text: '[Add an image here' + (slide.image_prompt ? ': ' + text(slide.image_prompt, 300) : '') + ']', muted: true }); diff --git a/test/deck-image-placeholders.test.js b/test/deck-image-placeholders.test.js index d89a53e1..a95e8e0c 100644 --- a/test/deck-image-placeholders.test.js +++ b/test/deck-image-placeholders.test.js @@ -61,7 +61,7 @@ test('the renderer draws the frame on figure, image and custom image shapes', () const py = read('scripts/render_pptx.py'); assert.match(py, /def _placeholder\(slide, left, top, width, height, label\)/); assert.match(py, /MSO_LINE_DASH_STYLE\.DASH/); - assert.equal((py.match(/\n\s+_placeholder\(slide, /g) || []).length, 3, 'figure, image and draw_image each draw it'); + assert.equal((py.match(/\n\s+_placeholder\(slide, /g) || []).length, 4, 'figure, image, question and draw_image each draw it'); assert.match(py, /"Add an image here"/); }); @@ -70,3 +70,17 @@ test('the model is told a picture it names is drawn or left as a frame, never de assert.match(brief, /keeps an empty labelled/); assert.match(brief, /Never write an image placeholder into any text/); }); + +test('a question may carry a picture beside its stem, drawn or left as a frame', async () => { + const d = deckSchema.normalise({ slides: [ + { type: 'question', heading: 'Case 2', question: 'What does the film show?', options: ['a', 'b'], answer: 'a', image_prompt: 'the admission chest radiograph' } + ] }); + assert.equal(d.slides[0].type, 'question', 'not converted into a figure slide'); + assert.equal(d.slides[0].image_prompt, 'the admission chest radiograph'); + await deckBuild.drawFigures(d, { imageModel: '' }); + assert.equal(d.slides[0].placeholder, true); + const blocks = docSpec.fromDeck(d, {}); + assert.match(JSON.stringify(blocks), /\[Add an image here: the admission chest radiograph\]/); + const py = read('scripts/render_pptx.py'); + assert.match(py, /has_picture = bool\(path and os\.path\.exists\(path\)\) or bool\(spec\.get\("placeholder"\)\)/); +});