diff --git a/assets/deck-formats.json b/assets/deck-formats.json new file mode 100644 index 00000000..e3185d22 --- /dev/null +++ b/assets/deck-formats.json @@ -0,0 +1,239 @@ +{ + "_comment": "The shape of a teaching session, as distinct from its look (deck-themes.json). Distilled from how departments actually run each session: residency presenter guides (UW GIM, UAMS, U Colorado morning-report blueprint, UChicago IM, Rush EM M&M, Northwestern Surgery), CORD and TAMU journal-club guides, the ACTIVE interactive-conference study, Harvard CMS and INACSL debriefing standards, IHI/UNC IHQI/MN DOH quality-improvement guidance, AAP and AHRQ plain-language guidance. Each part names the layouts it is usually drawn with, from deckSchema's twelve, and whether the model may use it more than once. Read by src/utils/deckFormats.js.", + "formats": [ + { + "id": "grand-rounds", + "name": "Grand Rounds", + "audience": "residents, fellows and faculty at a formal departmental lecture, often CME-accredited", + "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." } + ], + "pitfalls": [ + "trying to cover diagnosis, pathophysiology, treatment and follow-up in one talk — zoom in on one aspect", + "jargon and unexpanded abbreviations for a departmental audience", + "slides that are the script — summarize on the slide, elaborate aloud", + "sections that run past ten minutes" + ] + }, + { + "id": "journal-club", + "name": "Journal Club", + "audience": "residents presenting one paper to peers and faculty", + "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." } + ], + "pitfalls": [ + "rushing the methods, which is where the audience is lost", + "reporting p values without absolute effects", + "an appraisal that is an opinion rather than a structured tool", + "ending on the results with no bottom line" + ] + }, + { + "id": "morning-report", + "name": "Morning Report", + "audience": "residents and students at daily case conference, a chief resident or attending facilitating", + "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": "" } + ], + "pitfalls": [ + "showing all the history, examination and results at once", + "discussing management before the diagnosis is established", + "leaving out the relevant negatives", + "a zebra case — a common diagnosis with strong teaching points works better" + ] + }, + { + "id": "morbidity-mortality", + "name": "Morbidity and Mortality Conference", + "audience": "residents, faculty and nursing at a patient-safety conference", + "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 to fill in during discussion." }, + { "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", + "hindsight bias — judging with what was learned afterwards", + "blame language left standing ('X should have') instead of 'how do we design so one bad night cannot break safety'", + "patient-identifying detail on a slide" + ] + }, + { + "id": "board-review", + "name": "Board Review", + "audience": "residents preparing for the in-training or certifying examination", + "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": "" } + ], + "pitfalls": [ + "a lecture of facts with no question-first structure — that format shows no gain in examination scores", + "explanations that confirm the right answer and skip why each distractor is wrong", + "artificially confusing stems" + ] + }, + { + "id": "noon-conference", + "name": "Noon Conference", + "audience": "residents at a lunchtime didactic session", + "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": "" } + ], + "pitfalls": [ + "a straight lecture with no case and no question — residents ask for the alternative, and it retains better", + "dense text slides used as cue cards", + "take-home messages that are only spoken" + ] + }, + { + "id": "sim-debrief", + "name": "Simulation Debrief", + "audience": "learners immediately after a simulation, facilitator-led", + "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." } + ], + "pitfalls": [ + "spending the time on deltas and skipping why the good moments worked", + "double-barrelled plus-and-delta questions", + "questions the facilitator already knows the answer to", + "judging performance instead of describing behaviour; forgetting teamwork and communication" + ] + }, + { + "id": "patient-education", + "name": "Parent and Patient Education Talk", + "audience": "parents and caregivers, in plain language", + "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?'" } + ], + "pitfalls": [ + "more than five pieces of information on one slide", + "any undefined jargon — it is a comprehension failure, not a style issue", + "closed 'do you understand?' questions", + "teaching back everything instead of the few details that matter" + ] + }, + { + "id": "qi-project", + "name": "Quality Improvement Project", + "audience": "the quality committee, hospital leadership and peer residents", + "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": "" } + ], + "pitfalls": [ + "a vague aim", + "confusing outcome, process and balancing measures", + "a Study step that only describes what was done", + "visual complexity that overshadows the story" + ] + }, + { + "id": "abstract-presentation", + "name": "Research Abstract Presentation", + "audience": "judges and peer researchers at a research day, strictly time-boxed", + "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." } + ], + "pitfalls": [ + "text the audience reads instead of listening", + "figures whose axes need explaining", + "jargon and acronyms for a mixed audience", + "anything not tied to the conclusion" + ] + } + ] +} diff --git a/docs/my-resources.md b/docs/my-resources.md index ddd5c66d..af50e003 100644 --- a/docs/my-resources.md +++ b/docs/my-resources.md @@ -200,6 +200,30 @@ body where the template allows it and carried into the Word and PDF exports as a bold closing line either way. The sample deck under every theme shows all of it. +## Formats + +A format is the shape of a session, as distinct from its look: a journal club +runs PICO question → methods → critical appraisal → bottom line; a board +review runs in cycles of case → question → answer; a morning report walks the +history before it reveals anything. Left to itself the model wrote one lecture +shape for all of them, however the layouts varied. + +`assets/deck-formats.json` holds the catalogue, distilled from how real +departments run each session; `src/utils/deckFormats.js` reads it. Each format +is a sequence of parts, and each part names the layouts it is usually drawn +with — from the same twelve the schema offers, never a slide type of its own — +and whether it may be repeated. The brief given to the model says so in prose, +after the layout vocabulary: the parts in order, which ones repeat, and what +the format goes wrong on. A part is a unit the model may reuse as many times as +the material needs (one case-question-answer run per topic, one timeline +segment per event) and may leave out when the material has none. + +The author picks the format beside the theme; "Free" is the default and is +what every deck was before. The choice is stored on the deck (`deck.format`, +validated against the catalogue the same way `deck.theme` is) so a later +modification keeps the shape it was written in — the revision prompt carries +the same brief. + ## Illustrations `resourceImages.js`, **not** the shared `imageTool.dispatch` — that one permits diff --git a/public/components/my-resources.html b/public/components/my-resources.html index 8a492e95..2d9c9729 100644 --- a/public/components/my-resources.html +++ b/public/components/my-resources.html @@ -56,6 +56,16 @@ + + +
Draw on
diff --git a/public/js/myResources.js b/public/js/myResources.js index f5f41b6a..33ef64e9 100644 --- a/public/js/myResources.js +++ b/public/js/myResources.js @@ -170,6 +170,31 @@ } if (themeRow) themeRow.hidden = themes.length < 2; + // The shape of the session. "Free" is the default: the model lays the + // deck out as it sees fit, which is what every deck was until now. + var formatSelect = document.getElementById('mr-format'); + var formatRow = document.getElementById('mr-format-row'); + var formats = Array.isArray(data.formats) ? data.formats : []; + if (formatSelect && formats.length) { + var chosenFormat = formatSelect.value; + formatSelect.textContent = ''; + var free = document.createElement('option'); + free.value = ''; free.textContent = 'Free — let the model choose'; + formatSelect.appendChild(free); + formats.forEach(function (f) { + var option = document.createElement('option'); + option.value = f.id; + option.textContent = f.name + (f.length ? ' · ' + f.length : ''); + formatSelect.appendChild(option); + }); + if (chosenFormat && formats.some(function (f) { return f.id === chosenFormat; })) { + formatSelect.value = chosenFormat; + } + describeFormat(formats); + formatSelect.onchange = function () { describeFormat(formats); }; + } + if (formatRow) formatRow.hidden = !formats.length; + // Hidden entirely unless an administrator enabled it, so an option // never appears that someone could tick and then be refused. Both // groups are driven from the same answer: Generate and Modify offer @@ -240,6 +265,17 @@ box.hidden = false; } + // Says who the chosen format is for and how long it runs. + function describeFormat(formats) { + var select = document.getElementById('mr-format'); + var hint = document.getElementById('mr-format-hint'); + if (!select || !hint) return; + var chosen = formats.filter(function (f) { return f.id === select.value; })[0]; + hint.textContent = chosen + ? [chosen.audience ? 'For ' + chosen.audience : '', chosen.description || ''].filter(Boolean).join('. ') + : 'The layouts are chosen slide by slide with no fixed order of parts.'; + } + // Says what the chosen theme is for, which is the part a name cannot carry. function describeTheme(themes) { var select = document.getElementById('mr-theme'); @@ -281,6 +317,7 @@ useCorpus: corpusBox && corpusBox.checked === false ? 'false' : 'true', model: (document.getElementById('mr-model') || {}).value || '', theme: (document.getElementById('mr-theme') || {}).value || '', + format: (document.getElementById('mr-format') || {}).value || '', withImages: (document.getElementById('mr-with-images') || {}).checked ? 'true' : 'false', withWebSearch: (document.getElementById('mr-web-search') || {}).checked ? 'true' : 'false', withPubmed: (document.getElementById('mr-pubmed') || {}).checked ? 'true' : 'false' diff --git a/src/routes/myResources.js b/src/routes/myResources.js index c37805a8..4b7c8592 100644 --- a/src/routes/myResources.js +++ b/src/routes/myResources.js @@ -24,6 +24,7 @@ var { callAI } = require('../utils/ai'); var learningRetrieval = require('../utils/learningRetrieval'); var resourceImages = require('../utils/resourceImages'); var deckSchema = require('../utils/deckSchema'); +var deckFormats = require('../utils/deckFormats'); var deckSample = require('../utils/deckSample'); var deckBuild = require('../utils/deckBuild'); var deckReview = require('../utils/deckReview'); @@ -165,6 +166,10 @@ function buildPrompt(opts) { 'audience (pediatrics / primary care).\n\nTOPIC: ' + opts.topic + '\n' + detailsBlock(opts) + grounding + findings + '\n' + deckSchema.instructions(opts.slideCount, opts.figureCount) + + // The shape of the session, after the vocabulary it is built from: a + // journal club and a board review are not the same deck with different + // colours, and without this the model writes one lecture shape for both. + (opts.format ? '\n\n' + deckFormats.instructions(opts.format) : '') + (opts.refinement ? '\n\nAdditional instructions: ' + opts.refinement + '\n' : '') + figures; } @@ -253,6 +258,9 @@ router.get('/my-resources/options', async function (req, res) { themes: deckSchema.themes().map(function (t) { return { id: t.id, name: t.name, description: t.description || '' }; }), + // The shape of the session — journal club, morning report, board + // review — as distinct from its look. + formats: deckFormats.catalogue(), webSearchAvailable: await webSearch.isAvailable(), pubmedAvailable: await pubmedSearch.isAvailable() }); @@ -346,6 +354,7 @@ router.post('/my-resources/generate', async function (req, res) { literature: sources.literature, webFindings: sources.webFindings, searchedAndFoundNothing: sources.searchedAndFoundNothing, wantsImages: wantsImages, deckMode: deckMode, + format: deckMode ? deckFormats.formatId(req.body.format) : '', figureCount: wantsImages ? (resourceImages.requestedCount(refinement) || 0) : 0, slideCount: clampInt(req.body.slideCount, 3, 30, 8), wordCount: clampInt(req.body.wordCount, 200, 3000, 800) @@ -399,6 +408,9 @@ router.post('/my-resources/generate', async function (req, res) { // Chosen by the author, not the model: a theme is a look, and the person // making the deck is the one who knows the room it will be shown in. if (deck) deck.theme = deckSchema.themeId(req.body.theme); + // The format travels with the deck so a later revision keeps the shape it + // was written in. + if (deck) deck.format = deckFormats.formatId(req.body.format); reportVocabularyGaps(vocabularyGaps, topic); // Why a deck became plain slides, when it did. Reported to the caller as // well as logged: the fallback produces a usable but plainer deck, and @@ -997,6 +1009,7 @@ router.post('/my-resources/:id/refine', async function (req, res) { 'INSTRUCTION: ' + instructions + illustration + material + sight + '\n\nThe layouts available are:\n' + deckSchema.instructions( (existingDeck.slides || []).length, 0) + + (existingDeck.format ? '\n\n' + deckFormats.instructions(existingDeck.format) : '') + '\n\nDECK JSON:\n' + JSON.stringify({ slides: existingDeck.slides }) : 'Revise the following Pandoc markdown according to the instruction. ' + 'Return ONLY the complete revised markdown, no commentary, no code fences. ' + @@ -1045,6 +1058,8 @@ router.post('/my-resources/:id/refine', async function (req, res) { // The look is the author's, not the model's: a modification never // resets the theme. The column keeps it too, but the two should agree. revisedDeck.theme = existingDeck.theme || existing.theme || revisedDeck.theme; + // And the shape it was written in. + revisedDeck.format = existingDeck.format || revisedDeck.format; // Draw whatever the revised deck asked for, the same way generating does, // so each new figure belongs to the slide that wanted it. if (sources.wantsImages) { diff --git a/src/utils/deckFormats.js b/src/utils/deckFormats.js new file mode 100644 index 00000000..a8003210 --- /dev/null +++ b/src/utils/deckFormats.js @@ -0,0 +1,88 @@ +// ============================================================ +// DECK FORMATS +// ============================================================ +// The shape of a teaching session, as distinct from its look. +// +// A theme says what a deck looks like; the slide vocabulary says what one slide +// can be. Neither says what a journal club *is* — a PICO question, then the +// methods, then the appraisal, then the bottom line — or that a board-review +// session runs in cycles of case, question, answer. Left to itself the model +// writes the same lecture shape for everything, however the layouts vary. +// +// A format is that sequence, read from `assets/deck-formats.json`, distilled +// from how real departments run each session. Its parts name the layouts they +// are usually drawn with, from the same vocabulary the schema offers, and a +// part marked repeatable may be used as many times as the material needs — the +// format is a recipe, not a slide count. The model still writes the deck in +// the same JSON the renderer reads; the format only tells it what order the +// parts come in and what good looks like at each. +// ============================================================ + +var fs = require('fs'); +var path = require('path'); + +var cache = null; +function formats() { + if (cache) return cache; + try { + var file = path.join(__dirname, '..', '..', 'assets', 'deck-formats.json'); + cache = JSON.parse(fs.readFileSync(file, 'utf8')).formats || []; + } catch (e) { cache = []; } + return cache; +} + +// An id the catalogue knows, or '' — a deck must never claim a format that +// does not exist. +function formatId(value) { + var id = String(value || '').trim(); + if (!id) return ''; + return formats().some(function (f) { return f.id === id; }) ? id : ''; +} + +function byId(id) { + return formats().filter(function (f) { return f.id === id; })[0] || null; +} + +// The block given to the model, after the layout vocabulary. Prose, for the +// same reason the vocabulary is prose: the failure to avoid is a dull deck +// that follows the letter of a schema. +function instructions(id) { + var format = byId(formatId(id)); + if (!format) return ''; + var lines = [ + 'FORMAT: this is a ' + format.name + (format.audience ? ' for ' + format.audience : '') + '.', + format.length ? 'Typical length: ' + format.length + '.' : '', + 'Its parts, in this order:' + ]; + (format.sequence || []).forEach(function (part, i) { + var layouts = Array.isArray(part.layouts) && part.layouts.length + ? ' Usually "' + part.layouts.join('" or "') + '" slides.' : ''; + lines.push(' ' + (i + 1) + '. ' + part.part + (part.purpose ? ' — ' + part.purpose : '') + '.' + + layouts + (part.guidance ? ' ' + part.guidance : '') + + (part.repeat ? ' (repeatable)' : '')); + }); + 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.'); + if (Array.isArray(format.pitfalls) && format.pitfalls.length) { + lines.push('What this format goes wrong on: ' + format.pitfalls.join('; ') + '.'); + } + return lines.filter(Boolean).join('\n'); +} + +// What the picker shows: never the sequence itself, which is the model's +// brief, only what the format is for. +function catalogue() { + return formats().map(function (f) { + return { + id: f.id, + name: f.name, + audience: f.audience || '', + description: f.description || '', + length: f.length || '' + }; + }); +} + +module.exports = { formats: formats, formatId: formatId, instructions: instructions, catalogue: catalogue }; diff --git a/src/utils/deckSchema.js b/src/utils/deckSchema.js index c631a878..84779746 100644 --- a/src/utils/deckSchema.js +++ b/src/utils/deckSchema.js @@ -226,6 +226,9 @@ function normalise(raw, gaps) { // does not know would silently fall back, and a stored deck would then // claim a theme it never had. theme: themeId(deck.theme), + // The shape it was written in, kept the same way and for the same reason: + // a revision must not lose it, and it must name a format that exists. + format: require('./deckFormats').formatId(deck.format), slides: slides }; } diff --git a/test/deck-formats.test.js b/test/deck-formats.test.js new file mode 100644 index 00000000..2a29baed --- /dev/null +++ b/test/deck-formats.test.js @@ -0,0 +1,70 @@ +// A deck has a shape as well as a look: a journal club runs PICO → methods → +// appraisal → bottom line, a board review runs in case → question → answer +// cycles. The format is a recipe the model follows and may repeat, never a +// slide type of its own. +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const read = f => fs.readFileSync(path.join(__dirname, '..', f), 'utf8'); +const deckFormats = require('../src/utils/deckFormats'); +const deckSchema = require('../src/utils/deckSchema'); +const catalogue = JSON.parse(read('assets/deck-formats.json')).formats; +const LAYOUTS = ['title', 'section', 'bullets', 'two', 'compare', 'table', 'callout', 'question', 'figure', 'image', 'flow', 'custom']; + +test('every format is complete and speaks only the slide vocabulary', () => { + assert.ok(catalogue.length >= 5, 'a picker of a few is not a choice'); + for (const format of catalogue) { + for (const key of ['id', 'name', 'audience', 'description', 'sequence']) { + assert.ok(format[key], format.id + ' is missing ' + key); + } + assert.ok(format.sequence.length >= 3, format.id + ' has too few parts'); + for (const part of format.sequence) { + assert.ok(part.part && part.purpose, format.id + ' has a part without a name or purpose'); + for (const layout of part.layouts || []) { + assert.ok(LAYOUTS.includes(layout), format.id + ' names a layout the renderer cannot draw: ' + layout); + } + } + assert.ok(format.sequence.some(p => p.repeat), format.id + ' has no part the model may reuse'); + } + const ids = catalogue.map(f => f.id); + assert.equal(ids.length, new Set(ids).size, 'format ids must be unique'); +}); + +test('the brief lists the parts in order, says which repeat, and never invents a slide type', () => { + const format = catalogue[0]; + const brief = deckFormats.instructions(format.id); + assert.match(brief, new RegExp('FORMAT: this is a ' + format.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + format.sequence.forEach((part, i) => { + assert.ok(brief.indexOf((i + 1) + '. ' + part.part) !== -1, 'part ' + part.part + ' is listed in order'); + }); + assert.match(brief, /\(repeatable\)/); + assert.match(brief, /may be used more than once/); + assert.match(brief, /Every slide is still one of the types above/); + assert.equal(deckFormats.instructions('not-a-format'), ''); + assert.equal(deckFormats.instructions(''), ''); +}); + +test('an unknown format is dropped rather than stored, and a known one survives normalisation', () => { + const slides = [{ type: 'bullets', heading: 'x', bullets: ['a'] }]; + assert.equal(deckSchema.normalise({ format: catalogue[0].id, slides }).format, catalogue[0].id); + assert.equal(deckSchema.normalise({ format: 'not-a-format', slides }).format, ''); + assert.equal(deckSchema.normalise({ slides }).format, ''); +}); + +test('the format reaches the model after the layouts, on generation and on revision', () => { + const route = read('src/routes/myResources.js'); + assert.match(route, /deckSchema\.instructions\(opts\.slideCount, opts\.figureCount\) \+[\s\S]{0,400}deckFormats\.instructions\(opts\.format\)/); + assert.match(route, /deckFormats\.instructions\(existingDeck\.format\)/); + assert.match(route, /revisedDeck\.format = existingDeck\.format/); + assert.match(route, /formats: deckFormats\.catalogue\(\)/); + const ui = read('public/js/myResources.js'); + assert.match(ui, /format: \(document\.getElementById\('mr-format'\) \|\| \{\}\)\.value/); +}); + +test('the picker is told what a format is for, never the recipe itself', () => { + for (const entry of deckFormats.catalogue()) { + assert.deepEqual(Object.keys(entry).sort(), ['audience', 'description', 'id', 'length', 'name']); + } +}); diff --git a/test/my-resources-refine.test.js b/test/my-resources-refine.test.js index 7d507f4a..29e5bd3a 100644 --- a/test/my-resources-refine.test.js +++ b/test/my-resources-refine.test.js @@ -70,6 +70,7 @@ function router(t, overrides = {}) { }, '../utils/deckBuild': deckBuild, '../utils/deckSchema': require('../src/utils/deckSchema'), + '../utils/deckFormats': require('../src/utils/deckFormats'), '../utils/deckSample': require('../src/utils/deckSample'), '../utils/deckReview': { slideImages: async () => (overrides.slideImages || []),