feat: a deck has a format as well as a theme — the shape of the session, whose parts the model may reuse

A theme is a look and the slide vocabulary is what one slide can be; neither
said what a journal club is (PICO, methods, appraisal, bottom line) or that a
board review runs in case-question-answer cycles, so every deck came out as
the same lecture. assets/deck-formats.json holds ten formats distilled from
how departments run each session — grand rounds, journal club, morning
report, M&M, board review, noon conference, simulation debrief, parent
education, QI project, research abstract — each a sequence of parts naming
the layouts it is usually drawn with and whether it may repeat. The brief is
prose after the layout vocabulary; a part is a unit the model reuses as many
times as the material needs, and every slide stays one of the twelve types.
Chosen beside the theme, 'Free' by default, stored on the deck and carried
through modifications.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
This commit is contained in:
Daniel 2026-09-14 14:13:22 +02:00
parent f762e91af7
commit cd00fae460
9 changed files with 487 additions and 0 deletions

239
assets/deck-formats.json Normal file
View file

@ -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": "3040 slides, 4560 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": "1216 slides, 1520 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": "2025 slides, 2545 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": "1520 slides, 1520 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": "1525 cases, three or four slides each, 6090 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": "2025 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": "812 slides, 2030 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": "710 slides, 1015 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": "1012 slides, 1015 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; 810 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"
]
}
]
}

View file

@ -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

View file

@ -56,6 +56,16 @@
</div>
</div>
<!-- The shape of the session — journal club, morning report, board
review — as distinct from its look. Hidden until the catalogue loads. -->
<div class="admin-row" id="mr-format-row" hidden>
<label for="mr-format" class="admin-row-label">Format</label>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<select id="mr-format" class="admin-control" style="max-width:320px;"></select>
<p id="mr-format-hint" style="margin:0;font-size:12px;color:var(--g500);"></p>
</div>
</div>
<div class="admin-row" style="align-items:flex-start;">
<strong class="admin-row-label">Draw on</strong>
<div style="flex:1;display:flex;flex-direction:column;gap:8px;min-width:0;">

View file

@ -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'

View file

@ -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) {

88
src/utils/deckFormats.js Normal file
View file

@ -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 };

View file

@ -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
};
}

70
test/deck-formats.test.js Normal file
View file

@ -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']);
}
});

View file

@ -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 || []),