~/ecg-practice-deck/build.js
const pptxgen = require("pptxgenjs");
const { execSync } = require("child_process");
// βββ Color Palette ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const BG_DARK = "0D1B2A"; // deep navy - title / answer slides
const BG_CASE = "112233"; // slightly lighter navy - case slides
const BG_LIGHT = "F0F4F8"; // off-white - ECG slides
const ACCENT = "E63946"; // medical red
const GOLD = "F4A261"; // warm gold for highlights
const WHITE = "FFFFFF";
const LIGHT_TXT = "B8C8D8";
const GREEN = "2EC4B6"; // teal-green for correct answers
// βββ ECG Cases Data βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const cases = [
{
number: 1,
clinicalContext: "65-year-old man with hypertension presents with palpitations and mild dyspnea for 2 days. HR 110, BP 130/85.",
clues: ["Note the ventricular rhythm carefully", "Look for P waves", "Is every RR interval equal?"],
imageUrl: "https://cdn.orris.care/cdss_images/c85541618159f7da9530493cfb243ab6826c7ea2c50edece5d0bd203ac7f6bb6.png",
diagnosis: "Atrial Fibrillation (AF)",
keyFindings: [
"Irregularly irregular RR intervals",
"No discrete P waves β chaotic baseline",
"Narrow QRS complexes (AV node conducting normally)",
"Ventricular rate ~110 bpm (uncontrolled)"
],
teaching: "AF is the most common sustained arrhythmia. The chaotic atrial activity (>600 impulses/min) produces no organised P waves. The AV node acts as a 'gatekeeper', allowing only some impulses through β resulting in an irregularly irregular rhythm. Stroke risk must be assessed (CHAβDSβ-VASc score).",
management: "Rate control (beta-blocker/diltiazem) β Anticoagulation (CHAβDSβ-VASc β₯2) β Consider rhythm control"
},
{
number: 2,
clinicalContext: "80-year-old man with sudden onset chest pain radiating to the jaw. Diaphoretic, BP 95/60, HR 88.",
clues: ["Focus on leads II, III, and aVF", "Check leads I and aVL", "Look for reciprocal changes"],
imageUrl: "https://cdn.orris.care/cdss_images/e30f337203edbe720805bc753caca4b6ae448f7e006d83ca604f83e144d2f782.png",
diagnosis: "Inferior STEMI (RCA Occlusion)",
keyFindings: [
"ST elevation in leads II, III, aVF (inferior leads)",
"ST elevation in lead III > lead II β favours RCA over circumflex",
"Reciprocal ST depression in lead I and aVL",
"Must get right-sided leads to rule out RV infarction"
],
teaching: "Inferior STEMI is caused by RCA occlusion in ~80% of cases. The hallmark is ST elevation in II, III, and aVF. Reciprocal ST depression in I and aVL confirms it. Always obtain lead V4R β ST elevation in V4R indicates co-existing RV infarction and means the patient will NOT tolerate nitrates.",
management: "Aspirin + P2Y12 inhibitor β Emergency PCI (door-to-balloon <90 min) β Avoid nitrates if RV infarction suspected"
},
{
number: 3,
clinicalContext: "52-year-old man with 1 hour of crushing central chest pain, sweating. BP 140/90, HR 92. No prior cardiac history.",
clues: ["Look at precordial leads V1-V4", "Check leads I and aVL", "Note the pattern of ST elevation"],
imageUrl: "https://cdn.orris.care/cdss_images/dc6d5cd22f8672f0b27b85a097c90de9ee666d76b0d0e89b4b46115b0fe5f154.png",
diagnosis: "Anterior STEMI (Distal LAD Occlusion)",
keyFindings: [
"ST elevation in V1, V2, V3 and lead I",
"No ST depression in II, III, aVF (distal LAD β inferior wall spared)",
"Anterior territory involvement: septum + anterior wall",
"Distal LAD: less territory than proximal occlusion"
],
teaching: "Anterior STEMI from LAD occlusion is the most haemodynamically dangerous MI. ST elevation in V1-V4 + I, aVL indicates anterior/lateral territory. Occlusion proximal to the first septal perforator affects a larger territory and carries higher mortality. Look for ST elevation in aVR (main stem equivalent).",
management: "Dual antiplatelet + Emergency PCI β Monitor for LBBB, VT, VF, acute MR, cardiogenic shock"
},
{
number: 4,
clinicalContext: "65-year-old woman with chest pain for 45 minutes, shortness of breath. Known COPD. BP 105/70, HR 115.",
clues: ["Compare ST elevation across all leads", "Look for any lead with ST depression", "Check leads aVR and V1"],
imageUrl: "https://cdn.orris.care/cdss_images/2892df61f6a4e33f8814ab62d11c2ad4ffb8a3cb8023eeeb775ea3cf982cde82.png",
diagnosis: "Anterior STEMI (Proximal LAD Occlusion)",
keyFindings: [
"ST elevation V1, V2, V3, V4 with associated aVL involvement",
"Extensive anterior territory = proximal LAD",
"Reciprocal ST depression in inferior leads",
"Risk of cardiogenic shock β large territory at risk"
],
teaching: "Proximal LAD occlusion (before the first diagonal or septal branch) produces massive anterior STEMI affecting the anterior wall, septum, apex, and often the lateral wall. These patients have the highest in-hospital mortality among all STEMIs. Expect complications: VT, VF, AV block, LV failure.",
management: "Fastest possible PCI β ICU monitoring β Inotropic support if cardiogenic shock"
},
{
number: 5,
clinicalContext: "33-year-old man post-long-haul flight, sudden onset pleuritic chest pain and breathlessness. HR 105, Oβ sat 91% on air. Leg swollen.",
clues: ["Look at lead I for S waves", "Look at lead III for Q waves and T-wave changes", "Check right precordial leads for RBBB pattern"],
imageUrl: "https://cdn.orris.care/cdss_images/28043f05c25edcd2ef040a7618f8d8af42facf9c194292f826202ab262a09da3.png",
diagnosis: "Pulmonary Embolism (S1Q3T3 Pattern)",
keyFindings: [
"S wave in lead I",
"Q wave in lead III",
"T-wave inversion in lead III (S1Q3T3)",
"Incomplete RBBB with T-wave inversion V1βV4 (right heart strain)",
"Sinus tachycardia"
],
teaching: "The S1Q3T3 pattern reflects acute right ventricular strain from PE. The most common ECG finding is sinus tachycardia. S1Q3T3 is specific but not sensitive (~20%). T-wave inversion V1-V4 indicates RV pressure overload. The ECG is most useful for excluding MI and supporting PE diagnosis.",
management: "CT pulmonary angiogram to confirm β Anticoagulation (LMWH/DOAC) β Thrombolysis if massive PE + haemodynamic collapse"
},
{
number: 6,
clinicalContext: "Patient with regular narrow-complex tachycardia at HR 150 bpm. Asymptomatic except mild palpitations. BP 118/75.",
clues: ["A rate of exactly 150 bpm should trigger a specific suspicion", "Look at the baseline between QRS complexes in leads II, III, aVF", "Is there any sawtooth pattern?"],
imageUrl: "https://cdn.orris.care/cdss_images/012b4fbfc7465f18d6c3598dd526531fd9e11dcda15d8da6c0faae3de3d1520f.png",
diagnosis: "Atrial Flutter (2:1 AV Block)",
keyFindings: [
"Sawtooth flutter waves at ~300/min in inferior leads",
"Positive flutter waves in V1",
"Regular ventricular rate ~150 bpm (300 Γ· 2 = 150)",
"2:1 AV conduction ratio",
"Narrow QRS complexes"
],
teaching: "Atrial flutter arises from a single reentry circuit in the right atrium cycling at ~300/min. The AV node blocks every other impulse, giving a ventricular rate of ~150 bpm. RULE: any regular narrow-complex tachycardia at exactly 150 bpm β think flutter until proven otherwise. Vagal manoeuvres/adenosine will slow the rate transiently, unmasking the flutter waves.",
management: "Rate control β Anticoagulation (same as AF) β Rhythm control: cardioversion or catheter ablation (>95% curative)"
},
{
number: 7,
clinicalContext: "73-year-old man with dizziness and near-syncope. HR 38, BP 90/60. Permanent pacemaker not fitted.",
clues: ["Count the P waves and QRS complexes separately", "Are P waves and QRS complexes related to each other?", "What is the ventricular escape rate?"],
imageUrl: "https://cdn.orris.care/cdss_images/261b1c6b1574e2e999daf13b011387a0122373806c4f1abfa30243401b33c870.png",
diagnosis: "High-Grade AV Block",
keyFindings: [
"P waves and QRS complexes not always related (intermittent dissociation)",
"Variable RR intervals β some P waves conduct, others are blocked",
"Sinus rate 82 bpm; slower ventricular rate",
"Beats 4 and 5 have shorter preceding RR β these conduct",
"This is high-grade (not complete) AV block"
],
teaching: "High-grade AV block (also called advanced second-degree) means most P waves are blocked. In complete (third-degree) block, NO P waves conduct β the atria and ventricles beat completely independently. The ventricular escape rate is <40/min with wide QRS. Both are pacemaker indications. Causes: inferior MI, drugs (digoxin, beta-blockers), degenerative conduction disease.",
management: "IV atropine (temporary) β Transcutaneous pacing if unstable β Permanent pacemaker"
},
{
number: 8,
clinicalContext: "55-year-old woman with progressive exertional dyspnoea and fatigue. History of hypertension for 15 years. BP 165/95.",
clues: ["Look at QRS voltage in precordial leads", "Check for ST and T-wave changes in lateral leads (V5, V6, I, aVL)", "Calculate: S in V1 + R in V5 or V6"],
imageUrl: "https://cdn.orris.care/cdss_images/17684e22a76b3488c70edf5d6e238d7feaf1c716f9e05f1e73b4e1deb5429cb4.png",
diagnosis: "Normal Sinus Rhythm with PAC Initiating AF",
keyFindings: [
"Two normal sinus beats at the start",
"A blocked premature atrial contraction (PAC) after the 2nd QRS",
"A subsequent PAC then triggers atrial fibrillation",
"Demonstrates how most AF is triggered by atrial ectopy from pulmonary veins"
],
teaching: "Most episodes of AF are triggered by premature atrial contractions (PACs) originating from the pulmonary veins. A PAC fires at a critical moment when part of the atrium is still partially refractory, initiating re-entry and AF. This is the rationale for pulmonary vein isolation (PVI) catheter ablation. PACs appear on the ECG as early P waves with different morphology.",
management: "Identify triggers β Treat underlying cause β Antiarrhythmic therapy or PVI ablation for paroxysmal AF"
},
{
number: 9,
clinicalContext: "58-year-old man presents with chest pain. ECG shows ST abnormalities in left bundle branch block. How do you interpret this?",
clues: ["In LBBB, the ST segment is normally discordant to the QRS", "Concordant = ST goes SAME direction as QRS dominant deflection", "Excessive discordance >5mm is also abnormal"],
imageUrl: "https://cdn.orris.care/cdss_images/ce8f5f11790c18bb6281489bc410aada31d4b45782ce90e05721fccf4be3822e.png",
diagnosis: "LBBB with Modified Sgarbossa Criteria",
keyFindings: [
"A: Discordant ST depression (normal in LBBB)",
"B: Discordant ST elevation (normal in LBBB)",
"C: CONCORDANT ST elevation β strongly suggests acute MI",
"D: Concordant ST depression β suggests acute MI",
"E: Excessive discordant ST elevation >5mm β weakly suggestive"
],
teaching: "In LBBB, the ST segment is normally discordant (opposite direction to main QRS deflection) because repolarisation is abnormal. Finding concordant ST changes breaks this rule and suggests acute ischaemia on top of LBBB (Sgarbossa criteria). A new LBBB in chest pain was historically treated as STEMI equivalent, but now the modified Sgarbossa criteria are more specific.",
management: "Concordant ST changes in LBBB + chest pain β treat as STEMI β Emergency PCI"
},
{
number: 10,
clinicalContext: "A 47-year-old man with right ventricular infarction complicating inferior STEMI. Note the classic findings.",
clues: ["Look at the inferior leads II, III, aVF", "Check for ST elevation in right-sided leads V3R-V6R", "Observe lead V1 carefully"],
imageUrl: "https://cdn.orris.care/cdss_images/591d741e2ea55e526228f79f8dd6a2cf080b0626906cbf806282e2992df06b17.png",
diagnosis: "RV Infarction with Inferior STEMI",
keyFindings: [
"ST elevation in II, III, aVF (inferior STEMI)",
"ST elevation in V1 on standard ECG",
"Right-sided leads (V3R-V6R): ST elevation confirms RV infarction",
"This pattern indicates proximal RCA occlusion",
"RV infarction in ~30-50% of inferior STEMIs"
],
teaching: "RV infarction complicates proximal RCA occlusion. On the standard 12-lead, ST elevation in V1 alongside inferior STEMI should trigger right-sided leads. Haemodynamically, these patients are volume-dependent β their RV needs preload. AVOID nitrates (cause massive BP drop), diuretics, and morphine. Give IV fluids. Management is different from standard STEMI.",
management: "IV fluid boluses (cautious) β No nitrates/diuretics β Urgent PCI of proximal RCA β Dobutamine if refractory"
}
];
// βββ Fetch all ECG images via skill script βββββββββββββββββββββββββββββββββββ
const imageUrls = cases.map(c => c.imageUrl);
console.log("Fetching ECG images...");
const fetchResult = execSync(
`node /home/daytona/skills/shared/scripts/fetch_images.js ${imageUrls.map(u => `"${u}"`).join(" ")}`
).toString();
const images = JSON.parse(fetchResult);
console.log(`Fetched ${images.filter(i => !i.error).length}/${images.length} images successfully`);
// βββ Build Presentation ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
let pres = new pptxgen();
pres.layout = "LAYOUT_16x9";
pres.title = "ECG Practice Cases";
pres.author = "Orris Medical Education";
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// TITLE SLIDE
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
{
let slide = pres.addSlide();
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 5.625, fill: { color: BG_DARK } });
// Red accent bar
slide.addShape(pres.ShapeType.rect, { x: 0, y: 2.2, w: 10, h: 0.05, fill: { color: ACCENT } });
// ECG-like decorative lines (horizontal)
for (let i = 0; i < 5; i++) {
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0.3 + i * 0.28, w: 10, h: 0.01, fill: { color: "1A2E45" } });
}
slide.addText("ECG Practice Cases", {
x: 0.6, y: 0.7, w: 8.8, h: 1.2,
fontSize: 44, bold: true, color: WHITE, fontFace: "Calibri", align: "center"
});
slide.addText("10 Clinical Scenarios β Test Your Interpretation Skills", {
x: 0.6, y: 1.85, w: 8.8, h: 0.55,
fontSize: 18, color: LIGHT_TXT, fontFace: "Calibri", align: "center"
});
slide.addShape(pres.ShapeType.rect, { x: 3.5, y: 2.35, w: 3, h: 0.06, fill: { color: ACCENT } });
const topics = ["Arrhythmias", "STEMI Localization", "AV Blocks", "Bundle Branch Blocks", "PE & RV Strain"];
topics.forEach((t, i) => {
const col = i < 3 ? 0 : 1;
const row = i < 3 ? i : i - 3;
slide.addShape(pres.ShapeType.rect, {
x: 1.5 + col * 4.5, y: 2.6 + row * 0.52, w: 3.5, h: 0.42,
fill: { color: "162840" }, line: { color: ACCENT, width: 1 }
});
slide.addText(t, {
x: 1.5 + col * 4.5, y: 2.6 + row * 0.52, w: 3.5, h: 0.42,
fontSize: 15, color: GOLD, bold: true, align: "center", valign: "middle"
});
});
slide.addText("Each case: Clinical Context β ECG β Answer & Teaching", {
x: 0.5, y: 5.1, w: 9, h: 0.35,
fontSize: 13, color: "607890", align: "center", italic: true
});
}
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// HELPER: HOW TO USE SLIDE
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
{
let slide = pres.addSlide();
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 5.625, fill: { color: BG_DARK } });
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 0.12, h: 5.625, fill: { color: ACCENT } });
slide.addText("How to Use This Deck", {
x: 0.4, y: 0.2, w: 9.2, h: 0.7,
fontSize: 28, bold: true, color: WHITE, fontFace: "Calibri"
});
const steps = [
["STEP 1 β Read the Clinical Context", "Age, symptoms, vitals β form your differential before seeing the ECG."],
["STEP 2 β Interpret the ECG", "Use your 7-step system: Rate β Rhythm β PR β QRS β Axis β ST β QT. Read the clues for hints."],
["STEP 3 β Commit to a Diagnosis", "Write it down or say it out loud before advancing to the answer slide."],
["STEP 4 β Check the Answer", "Review key findings, teaching points, and management pearls."],
];
steps.forEach(([title, desc], i) => {
slide.addShape(pres.ShapeType.rect, {
x: 0.4, y: 1.0 + i * 1.1, w: 9.2, h: 0.95,
fill: { color: "0F2035" }, line: { color: GOLD, width: 1.5 }
});
slide.addText(title, {
x: 0.6, y: 1.05 + i * 1.1, w: 8.8, h: 0.35,
fontSize: 14, bold: true, color: GOLD
});
slide.addText(desc, {
x: 0.6, y: 1.38 + i * 1.1, w: 8.8, h: 0.5,
fontSize: 12, color: LIGHT_TXT
});
});
}
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// CASE SLIDES (Case + ECG + Answer) Γ 10
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
cases.forEach((c, idx) => {
const img = images[idx];
// ββ CASE CONTEXT SLIDE βββββββββββββββββββββββββββββββββββββββββββββββββββββ
{
let slide = pres.addSlide();
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 5.625, fill: { color: BG_CASE } });
// Case number badge
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 0.72, fill: { color: ACCENT } });
slide.addText(`CASE ${c.number} / 10`, {
x: 0.4, y: 0.08, w: 5, h: 0.56,
fontSize: 22, bold: true, color: WHITE, fontFace: "Calibri"
});
slide.addText("CLINICAL CONTEXT", {
x: 5, y: 0.08, w: 4.6, h: 0.56,
fontSize: 13, color: "FFCCD0", align: "right", italic: true
});
// Clinical scenario box
slide.addShape(pres.ShapeType.rect, { x: 0.4, y: 0.85, w: 9.2, h: 1.35, fill: { color: "0A1929" }, line: { color: GOLD, width: 1.5 } });
slide.addText("Patient Presentation", {
x: 0.6, y: 0.9, w: 8.8, h: 0.3,
fontSize: 12, color: GOLD, bold: true, italic: true
});
slide.addText(c.clinicalContext, {
x: 0.6, y: 1.18, w: 8.8, h: 0.9,
fontSize: 14, color: WHITE, fontFace: "Calibri"
});
// Systematic approach reminder
slide.addText("Apply Your 7-Step ECG System:", {
x: 0.4, y: 2.3, w: 9.2, h: 0.35,
fontSize: 13, color: LIGHT_TXT, bold: true
});
const steps7 = ["Rate", "Rhythm", "PR Interval", "QRS Width", "Axis", "ST Segment", "T Waves / QTc"];
steps7.forEach((s, i) => {
const col = i < 4 ? 0 : 1;
const row = i < 4 ? i : i - 4;
slide.addShape(pres.ShapeType.roundRect, {
x: 0.4 + col * 4.8, y: 2.7 + row * 0.48, w: 4.3, h: 0.4,
fill: { color: "162840" }, line: { color: "2A4A6A", width: 1 }, rectRadius: 0.05
});
slide.addText(`${i + 1}. ${s}`, {
x: 0.4 + col * 4.8, y: 2.7 + row * 0.48, w: 4.3, h: 0.4,
fontSize: 12, color: LIGHT_TXT, align: "center", valign: "middle"
});
});
// Hint clues
slide.addShape(pres.ShapeType.rect, { x: 0.4, y: 4.65, w: 9.2, h: 0.8, fill: { color: "1A3A1A" }, line: { color: GREEN, width: 1 } });
slide.addText("π‘ Clues: " + c.clues.join(" | "), {
x: 0.5, y: 4.68, w: 9.0, h: 0.74,
fontSize: 11.5, color: GREEN, italic: true, valign: "middle"
});
}
// ββ ECG SLIDE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
{
let slide = pres.addSlide();
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 5.625, fill: { color: BG_LIGHT } });
// Top bar
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 0.5, fill: { color: BG_DARK } });
slide.addText(`CASE ${c.number} β ECG`, {
x: 0.4, y: 0.05, w: 6, h: 0.4,
fontSize: 16, bold: true, color: WHITE
});
slide.addText("What is your diagnosis?", {
x: 5, y: 0.05, w: 4.6, h: 0.4,
fontSize: 14, color: GOLD, align: "right", italic: true
});
if (img && !img.error) {
slide.addImage({
data: img.base64,
x: 0.3, y: 0.6, w: 9.4, h: 4.9
});
} else {
slide.addText("ECG image could not be loaded", {
x: 2, y: 2.5, w: 6, h: 1,
fontSize: 18, color: ACCENT, align: "center"
});
}
}
// ββ ANSWER SLIDE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
{
let slide = pres.addSlide();
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 5.625, fill: { color: BG_DARK } });
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 0.15, h: 5.625, fill: { color: GREEN } });
// Header
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 0.65, fill: { color: "051020" } });
slide.addText(`CASE ${c.number} β ANSWER`, {
x: 0.4, y: 0.1, w: 4, h: 0.45,
fontSize: 15, bold: true, color: LIGHT_TXT
});
// Diagnosis banner
slide.addShape(pres.ShapeType.rect, { x: 0.25, y: 0.75, w: 9.5, h: 0.75, fill: { color: ACCENT }, line: { color: ACCENT } });
slide.addText(`β ${c.diagnosis}`, {
x: 0.35, y: 0.75, w: 9.3, h: 0.75,
fontSize: 22, bold: true, color: WHITE, valign: "middle", fontFace: "Calibri"
});
// Key Findings
slide.addText("KEY ECG FINDINGS", {
x: 0.3, y: 1.62, w: 4.4, h: 0.3,
fontSize: 11, bold: true, color: GOLD, charSpacing: 2
});
slide.addShape(pres.ShapeType.rect, { x: 0.25, y: 1.93, w: 4.55, h: 2.3, fill: { color: "0A1929" }, line: { color: GOLD, width: 1 } });
c.keyFindings.forEach((f, i) => {
slide.addText(`βΈ ${f}`, {
x: 0.35, y: 1.98 + i * 0.47, w: 4.35, h: 0.42,
fontSize: 11.5, color: WHITE
});
});
// Teaching point
slide.addText("TEACHING POINT", {
x: 5.05, y: 1.62, w: 4.6, h: 0.3,
fontSize: 11, bold: true, color: GOLD, charSpacing: 2
});
slide.addShape(pres.ShapeType.rect, { x: 5.0, y: 1.93, w: 4.75, h: 1.55, fill: { color: "0A1929" }, line: { color: LIGHT_TXT, width: 1 } });
slide.addText(c.teaching, {
x: 5.1, y: 1.98, w: 4.55, h: 1.45,
fontSize: 10.5, color: LIGHT_TXT
});
// Management pearl
slide.addText("MANAGEMENT PEARL", {
x: 5.05, y: 3.57, w: 4.6, h: 0.3,
fontSize: 11, bold: true, color: GOLD, charSpacing: 2
});
slide.addShape(pres.ShapeType.rect, { x: 5.0, y: 3.87, w: 4.75, h: 1.45, fill: { color: "0F2530" }, line: { color: GREEN, width: 1 } });
slide.addText(c.management, {
x: 5.1, y: 3.92, w: 4.55, h: 1.3,
fontSize: 11, color: GREEN
});
// Mini ECG thumbnail on answer slide
if (img && !img.error) {
slide.addImage({
data: img.base64,
x: 0.25, y: 4.28, w: 4.55, h: 1.2
});
}
}
});
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// SUMMARY / QUICK REFERENCE SLIDE
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
{
let slide = pres.addSlide();
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 5.625, fill: { color: BG_DARK } });
slide.addShape(pres.ShapeType.rect, { x: 0, y: 0, w: 10, h: 0.65, fill: { color: ACCENT } });
slide.addText("ECG Quick Reference Card", {
x: 0.4, y: 0.08, w: 9.2, h: 0.5,
fontSize: 22, bold: true, color: WHITE, align: "center"
});
const diagnoses = [
["AF", "Irregularly irregular, no P waves, narrow QRS"],
["A. Flutter", "Sawtooth ~300/min, regular rate ~150 (2:1)"],
["Inf. STEMI", "STβ II, III, aVF + reciprocal depression I, aVL"],
["Ant. STEMI", "STβ V1-V4 Β± I, aVL (LAD)"],
["PE", "S1Q3T3, sinus tachy, T inv V1-V4, RBBB pattern"],
["3Β° AV Block", "P waves + QRS independent, escape rate <40"],
["LBBB", "QRS β₯120ms, broad R V5/V6, QS in V1"],
["RBBB", "QRS β₯120ms, rSR' V1 (rabbit ears), wide S V6"],
["LVH", "SV1 + RV5/V6 >35mm, strain pattern I/aVL"],
["WPW", "Short PR, delta wave, wide QRS"],
];
diagnoses.forEach(([dx, features], i) => {
const col = i < 5 ? 0 : 1;
const row = i < 5 ? i : i - 5;
slide.addShape(pres.ShapeType.rect, {
x: 0.2 + col * 4.9, y: 0.78 + row * 0.93, w: 4.65, h: 0.86,
fill: { color: "0A1929" }, line: { color: "2A4A6A", width: 1 }
});
slide.addText(dx, {
x: 0.3 + col * 4.9, y: 0.82 + row * 0.93, w: 1.1, h: 0.35,
fontSize: 12, bold: true, color: GOLD
});
slide.addText(features, {
x: 0.3 + col * 4.9, y: 1.14 + row * 0.93, w: 4.45, h: 0.4,
fontSize: 10.5, color: LIGHT_TXT
});
});
}
// ββ WRITE FILE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const outputPath = "/home/daytona/workspace/ecg-practice-deck/ECG_Practice_Cases.pptx";
pres.writeFile({ fileName: outputPath }).then(() => {
console.log(`β
Presentation saved to: ${outputPath}`);
}).catch(err => {
console.error("Error writing file:", err);
process.exit(1);
});