feat: don't-miss tooltip after notes + bedside suture selector

#2 — Don't-miss tooltip (encounters HPI + sick visit, max 5)
- New POST /api/dont-miss returning {points: [{point, why}]} capped at 5
  (cap defended both in the prompt and server-side .slice(0,5))
- New dontMissTooltip prompt in prompts.js
- New suggestDontMiss() helper in app.js mirroring suggestBillingCodes;
  inserts an orange-bordered card next to the note output, silent on empty
- Wired into liveEncounter.js (encounter HPI) and sickVisit.js. Not added
  to wellvisit/soap/hospital/chart per spec.

#1 — Bedside suture selector
- New ES module public/js/bedside/sutures.js (~300L) following the
  burns.js pattern: site × age × tension × cosmetic × contamination ×
  hours-since-injury → material, size, technique, removal day range,
  glue/Steri-strip alternative, warnings, tetanus reminder.
- 15 anatomic sites covered (face, eyelid, lip vermilion, intraoral,
  ear, scalp, neck, trunk, upper/lower ext, hand, foot, joint surface,
  genitalia, fingertip).
- Bites: cat/human → don't-close-primarily warning; dog bite to hand →
  loose-approximation note. Heavy contamination → delayed primary
  closure. >12h non-face/scalp → judgment call note.
- Removal days shown as ranges (3–5, 7–10, 10–14) per source norms,
  not single midpoints.
- Subungual hematoma trephination guidance corrected: any painful
  hematoma with intact nail and no displaced fracture (especially if
  25–50% or more), per current UpToDate guidance.
- Inline citation: Roberts & Hedges 7e (2019), Fleisher & Ludwig 8e,
  AAP Section on EM, UpToDate (Pope JV).
- Pill registered in sub-nav SECTIONS + bedside/index.js. Persists
  active state via existing UIState helper.

All 46 tests pass.
This commit is contained in:
Daniel 2026-04-26 20:26:11 +02:00
parent f8e883e969
commit 7e93f7c629
10 changed files with 573 additions and 2 deletions

View file

@ -65,6 +65,7 @@
<button class="calc-pill" data-em="burns"><i class="fas fa-fire"></i> Burns</button>
<button class="calc-pill" data-em="toxicology"><i class="fas fa-skull-crossbones"></i> Toxicology</button>
<button class="calc-pill" data-em="trauma"><i class="fas fa-user-injured" style="transform:rotate(-45deg);"></i> Trauma</button>
<button class="calc-pill" data-em="sutures"><i class="fas fa-staff-snake"></i> Sutures</button>
</div>
<!-- ── NEONATAL ── -->
@ -519,5 +520,88 @@
<div id="trauma-result"></div>
</div>
<!-- ── SUTURES ── -->
<div id="em-sutures" class="em-section" style="display:none;">
<h4 style="font-size:15px;font-weight:700;color:var(--g800);margin:0 0 4px;">Suture Selector</h4>
<p style="font-size:12px;color:var(--g500);margin:0 0 14px;">Pick a site and modifiers — get material, size, technique, removal day, and warnings. Recommendation only; clinical judgment overrides.</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:12px;">
<div class="calc-field">
<label>Site</label>
<select id="sut-site">
<option value="">— Select site —</option>
<option value="face">Face (non-cosmetic-critical)</option>
<option value="eyelid_eyebrow">Eyelid / eyebrow</option>
<option value="lip_vermilion">Lip — crossing vermilion</option>
<option value="intraoral">Intraoral / tongue / mucosa</option>
<option value="ear">Ear</option>
<option value="scalp">Scalp</option>
<option value="neck">Neck</option>
<option value="trunk">Trunk</option>
<option value="upper_ext">Upper extremity (arm/forearm)</option>
<option value="lower_ext">Lower extremity (thigh/leg)</option>
<option value="hand">Hand / fingers</option>
<option value="foot">Foot / toes</option>
<option value="joint_surface">Over a joint</option>
<option value="genitalia">Genitalia / perineum</option>
<option value="fingertip">Fingertip / nail bed</option>
</select>
</div>
<div class="calc-field">
<label>Age</label>
<select id="sut-age">
<option value="6to12" selected>612 y</option>
<option value="lt1">&lt; 1 y</option>
<option value="1to5">15 y</option>
<option value="gt12">&gt; 12 y</option>
</select>
</div>
<div class="calc-field">
<label>Wound tension</label>
<select id="sut-tension">
<option value="low" selected>Low</option>
<option value="moderate">Moderate</option>
<option value="high">High</option>
</select>
</div>
<div class="calc-field">
<label>Cosmetic priority</label>
<select id="sut-cosmetic">
<option value="no" selected>No</option>
<option value="yes">Yes</option>
</select>
</div>
<div class="calc-field">
<label>Contamination</label>
<select id="sut-contam">
<option value="clean" selected>Clean</option>
<option value="mild">Mildly contaminated</option>
<option value="heavy">Heavily contaminated</option>
<option value="bite_dog">Dog bite</option>
<option value="bite_cat">Cat bite</option>
<option value="bite_human">Human bite</option>
<option value="bite_other">Other animal bite</option>
</select>
</div>
<div class="calc-field">
<label>Time since injury</label>
<select id="sut-hours">
<option value="lt6" selected>&lt; 6 h</option>
<option value="6to12">612 h</option>
<option value=">12">&gt; 12 h</option>
</select>
</div>
<div class="calc-field">
<label>Avoid suture removal?</label>
<select id="sut-removal-avoid" title="Use absorbable for young children to skip the removal visit">
<option value="no" selected>No</option>
<option value="yes">Yes (use absorbable)</option>
</select>
</div>
</div>
<div id="sut-result"></div>
</div>
</div>
</div>

View file

@ -888,6 +888,61 @@ function suggestBillingCodes(outputElementId, noteText, noteType, patientAge, vi
function escHtml(s) { return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
// ── Don't-miss tooltip (called after sick visit / encounter HPI generates) ──
// Same insertion pattern as suggestBillingCodes — a side card next to the note
// output. Hard-capped at 5 items by the server prompt. Silent on empty / failure.
function suggestDontMiss(outputElementId, noteText, noteType, patientAge, chiefComplaint) {
var outputEl = document.getElementById(outputElementId);
if (!outputEl || !noteText) return;
var card = outputEl.closest('.card, .output-card');
if (!card) return;
var containerId = outputElementId.replace('-text', '') + '-dont-miss';
var container = document.getElementById(containerId);
if (!container) {
container = document.createElement('div');
container.id = containerId;
container.className = 'card dont-miss-card';
container.style.cssText = 'margin-top:10px;border-left:3px solid #f59e0b;';
outputEl.parentNode.insertBefore(container, outputEl.nextSibling);
}
container.innerHTML = '<div style="padding:10px 16px;font-size:12px;color:var(--g500);"><i class="fas fa-spinner fa-spin"></i> Reviewing note for high-yield items...</div>';
fetch('/api/dont-miss', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
noteText: noteText,
noteType: noteType || '',
patientAge: patientAge || '',
chiefComplaint: chiefComplaint || ''
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success || !data.points || data.points.length === 0) {
container.classList.add('hidden');
return;
}
var html = '<div class="card-header"><h3><i class="fas fa-triangle-exclamation" style="color:#f59e0b;"></i> Don\'t Miss</h3>' +
'<span style="font-size:11px;color:var(--g500);">High-yield items. Suggestions only.</span></div>' +
'<div style="padding:8px 16px;">';
data.points.forEach(function(p) {
var why = p.why ? '<div style="font-size:11px;color:var(--g500);margin-top:2px;">' + escHtml(p.why) + '</div>' : '';
html += '<div style="padding:6px 0;border-bottom:1px solid var(--g100);">' +
'<div style="font-size:13px;color:var(--g800);"><i class="fas fa-circle-exclamation" style="color:#f59e0b;font-size:11px;margin-right:6px;"></i>' + escHtml(p.point) + '</div>' +
why + '</div>';
});
html += '</div>';
container.classList.remove('hidden');
container.innerHTML = html;
})
.catch(function() {
container.classList.add('hidden');
});
}
function refineDocument(outputElementId, inputElementId) {
var doc = document.getElementById(outputElementId);
var input = document.getElementById(inputElementId);

View file

@ -23,6 +23,7 @@ import * as antimicrobials from './antimicrobials.js';
import * as burns from './burns.js';
import * as toxicology from './toxicology.js';
import * as trauma from './trauma.js';
import * as sutures from './sutures.js';
[
ageWeight,
@ -43,4 +44,5 @@ import * as trauma from './trauma.js';
burns,
toxicology,
trauma,
sutures,
].forEach(function(m) { if (m && typeof m.init === 'function') m.init(); });

View file

@ -5,7 +5,7 @@
// same sub-section the user was last looking at.
// ============================================================
var SECTIONS = ['neonatal','airway','cardiac','respiratory','ventilation','seizure','sepsis','anaphylaxis','sedation','agitation','antiemetics','antimicrobials','burns','toxicology','trauma'];
var SECTIONS = ['neonatal','airway','cardiac','respiratory','ventilation','seizure','sepsis','anaphylaxis','sedation','agitation','antiemetics','antimicrobials','burns','toxicology','trauma','sutures'];
function activate(emKey) {
var pill = document.querySelector('[data-em="' + emKey + '"]');

View file

@ -0,0 +1,320 @@
// ============================================================
// bedside/sutures.js
// SUTURE SELECTOR — site × age × tension × wound state →
// material, size, technique, removal day, alternatives, and
// contraindication warnings.
//
// Sources cited inline in the result via S.ref():
// Roberts & Hedges' Clinical Procedures in Emergency Medicine, 7e (2019)
// Fleisher & Ludwig's Textbook of Pediatric Emergency Medicine, 8e (2021)
// AAP Section on Emergency Medicine — Pediatric laceration repair
// UpToDate: Pope JV — Skin laceration repair with sutures
// ============================================================
import { S } from './shared.js';
// ── Site table ──────────────────────────────────────────────────────
// sutureSize/removalDays are the "moderate tension, clean, school-age" defaults.
// recommendFn() below adjusts for tension, age, contamination.
var SITES = {
face: {
label: 'Face (non-cosmetic-critical)',
materialFamily: 'monofilament-nonabsorbable', // nylon / polypropylene
sutureSize: '6-0',
removalDays: '35',
glueOK: true,
glueNote: 'Tissue adhesive (Dermabond) excellent for clean linear lacs ≤4 cm with low tension and good apposition. Avoid near eyes, lips, joints, mucosa.',
techniqueDefault: 'Simple interrupted',
notes: 'Smallest practical size to minimize scar. Approximate edges precisely — eversion is more important than knot count.'
},
eyelid_eyebrow: {
label: 'Eyelid / eyebrow',
materialFamily: 'monofilament-nonabsorbable',
sutureSize: '6-0',
removalDays: '35',
glueOK: false,
glueNote: 'No tissue adhesive — risk of ocular irritation.',
techniqueDefault: 'Simple interrupted',
notes: 'Do NOT shave eyebrow (regrowth unpredictable). Refer full-thickness eyelid, lid-margin, or lacrimal-system lacerations to ophthalmology / plastics.'
},
lip_vermilion: {
label: 'Lip — crossing vermilion border',
materialFamily: 'monofilament-nonabsorbable',
sutureSize: '6-0',
removalDays: '35',
glueOK: false,
glueNote: 'No glue across vermilion.',
techniqueDefault: 'Simple interrupted, FIRST stitch precisely aligning the vermilion border',
notes: 'Vermilion misalignment as small as 1 mm is visible. If a deep layer (orbicularis oris) is involved, close it with 5-0 absorbable first. Intraoral side: 4-0 chromic or Vicryl Rapide.'
},
intraoral: {
label: 'Intraoral / tongue / mucosa',
materialFamily: 'absorbable-chromic-or-vicryl',
sutureSize: '4-0 or 5-0',
removalDays: null, // absorbable
glueOK: false,
glueNote: 'No glue on mucosa.',
techniqueDefault: 'Simple interrupted, generous bites (mucosa is friable)',
notes: 'Many intraoral lacs heal well without closure. Suture if: through-and-through, gaping >1 cm, flap, or interfering with feeding/airway. Tongue: sedation often required; absorbable only.'
},
ear: {
label: 'Ear',
materialFamily: 'monofilament-nonabsorbable',
sutureSize: '5-0',
removalDays: '45',
glueOK: false,
glueNote: 'No glue on ear.',
techniqueDefault: 'Simple interrupted',
notes: 'Cover exposed cartilage with skin (do not suture cartilage in ED). Hematoma → drain to prevent cauliflower ear. Auricular block before repair.'
},
scalp: {
label: 'Scalp',
materialFamily: 'monofilament-nonabsorbable',
sutureSize: '3-0 or 4-0',
removalDays: '710',
glueOK: true,
glueNote: 'Hair apposition technique (HAT) — twist hair across the wound and secure with a drop of glue — reasonable for clean linear scalp lacs ≤10 cm with hair ≥3 cm.',
techniqueDefault: 'Simple interrupted, or staples (faster, equivalent cosmesis under hair)',
notes: 'Highly vascular — pressure controls bleeding. Galea aponeurotica may need closure with 3-0 absorbable for hemostasis. Staples acceptable in non-visible scalp.'
},
neck: {
label: 'Neck',
materialFamily: 'monofilament-nonabsorbable',
sutureSize: '5-0',
removalDays: '57',
glueOK: true,
glueNote: 'Glue acceptable for superficial low-tension lacs only.',
techniqueDefault: 'Simple interrupted',
notes: 'Rule out deep neck-structure injury (zone-based assessment) before closing. Posterior neck heals well; anterior neck — be alert for vascular/airway involvement.'
},
trunk: {
label: 'Trunk',
materialFamily: 'monofilament-nonabsorbable',
sutureSize: '4-0 or 3-0',
removalDays: '710',
glueOK: false,
glueNote: 'Glue not first-line for trunk; tension is too variable.',
techniqueDefault: 'Simple interrupted; deep dermal 4-0 absorbable for wounds >0.5 cm deep',
notes: 'Layered closure for any wound deeper than mid-dermis. Watch for through-and-through abdominal/chest injury; image if mechanism warrants.'
},
upper_ext: {
label: 'Upper extremity (arm / forearm)',
materialFamily: 'monofilament-nonabsorbable',
sutureSize: '4-0',
removalDays: '710',
glueOK: true,
glueNote: 'Glue OK for clean linear lacs ≤5 cm with low tension; not over joints.',
techniqueDefault: 'Simple interrupted',
notes: 'Examine neurovascular function distally before and after repair.'
},
lower_ext: {
label: 'Lower extremity (thigh / leg)',
materialFamily: 'monofilament-nonabsorbable',
sutureSize: '4-0 or 3-0',
removalDays: '1014',
glueOK: true,
glueNote: 'Glue OK for clean linear lacs ≤5 cm with low tension; not over joints; not on shin where tension is high.',
techniqueDefault: 'Simple interrupted',
notes: 'Pre-tibial skin is thin and fragile — handle gently, avoid horizontal mattress. Leave in longer than upper extremity.'
},
hand: {
label: 'Hand / fingers',
materialFamily: 'monofilament-nonabsorbable',
sutureSize: '5-0',
removalDays: '1014',
glueOK: false,
glueNote: 'Generally avoid glue on hand (high tension, frequent movement).',
techniqueDefault: 'Simple interrupted',
notes: 'Examine tendon function (extensor and flexor) and 2-point discrimination BEFORE local anesthetic. Image if mechanism suggests fracture or foreign body. Bite to hand → consider not closing.'
},
foot: {
label: 'Foot / toes',
materialFamily: 'monofilament-nonabsorbable',
sutureSize: '4-0 or 5-0',
removalDays: '1014',
glueOK: false,
glueNote: 'Generally avoid glue on plantar surface.',
techniqueDefault: 'Simple interrupted',
notes: 'Plantar punctures: high infection risk — irrigate, do not close primarily. Consider Pseudomonas coverage in shoe-puncture.'
},
joint_surface: {
label: 'Over a joint (knee, elbow, etc.)',
materialFamily: 'monofilament-nonabsorbable',
sutureSize: '4-0',
removalDays: '1014',
glueOK: false,
glueNote: 'No glue across joint surfaces — repeated flexion separates the bond.',
techniqueDefault: 'Simple interrupted; consider vertical mattress for high tension',
notes: 'Splint joint in slight flexion for 57 d to reduce wound tension. Rule out joint capsule penetration (saline-load test if uncertain).'
},
genitalia: {
label: 'Genitalia / perineum',
materialFamily: 'absorbable-chromic-or-vicryl',
sutureSize: '5-0 or 4-0',
removalDays: null, // absorbable
glueOK: false,
glueNote: 'No glue on genitalia.',
techniqueDefault: 'Simple interrupted',
notes: 'Significant labial / scrotal / penile lacs — consider OR repair under sedation. Always consider non-accidental trauma evaluation in pediatric genital injuries.'
},
fingertip: {
label: 'Fingertip / nail bed',
materialFamily: 'absorbable-chromic-or-vicryl',
sutureSize: '6-0',
removalDays: null,
glueOK: false,
glueNote: 'No glue on nail bed.',
techniqueDefault: 'Simple interrupted with 6-0 absorbable for nail bed; replace and secure nail as a stent',
notes: 'Painful subungual hematoma — trephinate (especially if 2550% or more of the nail), provided the nail is intact and there is no displaced distal-phalanx fracture. Open nail-bed lac → repair with absorbable, suture nail back as biologic dressing or use Coban. X-ray any crush mechanism (tuft fracture is common).'
}
};
// ── Recommendation engine ───────────────────────────────────────────
function recommend(input) {
var s = SITES[input.site];
if (!s) return null;
var notes = [];
var warnings = [];
// Closure decision (contamination + age of wound)
var closure = 'Primary closure';
if (input.contamination === 'heavy' || input.contamination === 'bite_other') {
closure = 'Consider DELAYED primary closure (cleanse, pack, reassess in 35 days) or healing by secondary intention';
warnings.push('Heavily contaminated wound — primary closure increases infection risk. Irrigate copiously before any closure decision.');
}
if (input.contamination === 'bite_human' || input.contamination === 'bite_cat') {
closure = 'Generally DO NOT close primarily';
warnings.push((input.contamination === 'bite_cat' ? 'Cat bite' : 'Human bite') + ' — leave open or use loose approximation. High infection risk (Pasteurella for cat, Eikenella for human). Irrigate, debride, give amox-clavulanate prophylaxis. Consider hand-surgery consult for hand bites.');
}
if (input.contamination === 'bite_dog' && input.site === 'hand') {
warnings.push('Dog bite to the hand — many EPs do not close primarily. If closing, loose approximation only.');
}
if (input.hours === '>12' && input.site !== 'face' && input.site !== 'scalp') {
closure = closure === 'Primary closure' ? 'Delayed primary closure (>12 h since injury, non-face/scalp)' : closure;
notes.push('>12 h since injury for a non-face/scalp wound — closure decision is judgment-based; clean linear wounds in well-perfused areas can still be closed.');
}
// Material — switch to absorbable for very young if non-mucosal default
var material = s.materialFamily;
if (s.materialFamily === 'monofilament-nonabsorbable' && (input.age === 'lt1' || input.age === '1to5')) {
if (input.removalAvoid === 'yes') {
material = 'fast-absorbing-gut';
notes.push('Young child — fast-absorbing gut suture (or Vicryl Rapide) avoids the trauma of suture removal. Slightly more reactive than nylon but cosmetic outcome at 3 months is similar in low-tension wounds.');
}
}
var materialDisplay = ({
'monofilament-nonabsorbable': 'Monofilament non-absorbable (nylon — Ethilon, OR polypropylene — Prolene)',
'absorbable-chromic-or-vicryl': 'Absorbable (chromic gut OR Vicryl / Vicryl Rapide)',
'fast-absorbing-gut': 'Fast-absorbing plain gut (or Vicryl Rapide) — no removal needed'
})[material];
// Size — bump up one for high tension, down one for cosmetic priority + low tension face
var size = s.sutureSize;
if (input.tension === 'high' && size === '4-0') size = '3-0';
if (input.tension === 'high' && size === '5-0') size = '4-0';
if (input.tension === 'high' && size === '6-0') size = '5-0';
if (input.cosmetic === 'yes' && input.tension === 'low' && size === '4-0') size = '5-0';
// Technique
var technique = s.techniqueDefault;
if (input.tension === 'high') technique = 'Vertical mattress (or simple interrupted with deep dermal absorbable layer first)';
if (input.cosmetic === 'yes' && input.tension === 'low' && (input.site === 'face' || input.site === 'eyelid_eyebrow')) {
technique = 'Simple interrupted; consider running subcuticular if skill permits — best cosmesis';
}
// Glue alternative (if applicable)
var glue = null;
if (s.glueOK && input.tension !== 'high' && input.contamination === 'clean') {
glue = s.glueNote;
}
// Removal day range (null when an absorbable material was selected)
var removal = s.removalDays;
if (material !== 'monofilament-nonabsorbable') removal = null;
// Tetanus
var tetanus = 'Confirm tetanus status. Tdap if last booster >5 y for tetanus-prone (contaminated, deep, crush, burn, bite); >10 y for clean wound. Add TIG if unimmunized or unknown immunization history with tetanus-prone wound.';
return {
site: s.label,
closure: closure,
material: materialDisplay,
size: size,
technique: technique,
glue: glue,
removalDays: removal,
notes: [s.notes].concat(notes),
warnings: warnings,
tetanus: tetanus
};
}
// ── Render ──────────────────────────────────────────────────────────
function readInput() {
return {
site: getVal('sut-site'),
age: getVal('sut-age'),
tension: getVal('sut-tension'),
cosmetic: getVal('sut-cosmetic'),
contamination: getVal('sut-contam'),
hours: getVal('sut-hours'),
removalAvoid: getVal('sut-removal-avoid')
};
}
function getVal(id) { var el = document.getElementById(id); return el ? el.value : ''; }
function render() {
var el = document.getElementById('sut-result');
if (!el) return;
var inp = readInput();
if (!inp.site) { el.innerHTML = '<p style="color:var(--g500);font-size:13px;">Select a site to see recommendation.</p>'; return; }
var r = recommend(inp);
if (!r) { el.innerHTML = '<p style="color:var(--red);font-size:13px;">Unknown site.</p>'; return; }
var html = '<div style="padding:10px 14px;border-radius:8px;background:#eef2ff;border:1.5px solid #6366f1;margin-bottom:10px;">' +
'<strong style="color:#4338ca;">Suture recommendation — ' + r.site + '</strong></div>';
html += '<div style="padding:12px;background:var(--g50);border-radius:6px;margin-bottom:10px;font-size:13px;line-height:1.6;">';
html += '<div><strong>Closure plan:</strong> ' + r.closure + '</div>';
html += '<div><strong>Material:</strong> ' + r.material + '</div>';
html += '<div><strong>Size:</strong> ' + r.size + '</div>';
html += '<div><strong>Technique:</strong> ' + r.technique + '</div>';
if (r.removalDays) html += '<div><strong>Suture removal:</strong> ' + r.removalDays + ' days</div>';
else html += '<div><strong>Suture removal:</strong> not required (absorbable)</div>';
html += '</div>';
if (r.glue) {
html += '<div style="padding:10px 12px;background:#ecfdf5;border-left:3px solid #10b981;border-radius:6px;margin-bottom:10px;font-size:13px;">' +
'<strong style="color:#047857;">Adhesive alternative:</strong> ' + r.glue + '</div>';
}
if (r.warnings.length) {
html += '<div style="padding:10px 12px;background:#fef2f2;border-left:3px solid #ef4444;border-radius:6px;margin-bottom:10px;font-size:13px;">' +
'<strong style="color:#dc2626;">Warning:</strong><ul style="margin:4px 0 0 18px;padding:0;">' +
r.warnings.map(function(w) { return '<li>' + w + '</li>'; }).join('') + '</ul></div>';
}
if (r.notes.length) {
html += '<div style="padding:10px 12px;background:#fffbeb;border-left:3px solid #f59e0b;border-radius:6px;margin-bottom:10px;font-size:13px;">' +
'<strong style="color:#92400e;">Site notes:</strong><ul style="margin:4px 0 0 18px;padding:0;">' +
r.notes.map(function(n) { return '<li>' + n + '</li>'; }).join('') + '</ul></div>';
}
html += '<div style="padding:10px 12px;background:var(--g50);border-radius:6px;font-size:12px;color:var(--g600);"><strong>Tetanus:</strong> ' + r.tetanus + '</div>';
html += S.ref('Roberts &amp; Hedges Clinical Procedures in Emergency Medicine, 7e (2019), Ch. 36. Fleisher &amp; Ludwig Pediatric Emergency Medicine, 8e (2021). AAP Section on Emergency Medicine — Pediatric laceration repair. UpToDate (Pope JV) — Skin laceration repair with sutures. Suggestions only — verify against your local protocol and the patient in front of you.');
el.innerHTML = html;
}
export function init() {
document.addEventListener('change', function(e) {
if (!e.target || !e.target.id) return;
if (e.target.id.indexOf('sut-') === 0) render();
});
document.addEventListener('click', function(e) {
var pill = e.target.closest && e.target.closest('[data-em="sutures"]');
if (pill) setTimeout(render, 0);
});
document.addEventListener('tabChanged', function() { setTimeout(render, 100); });
}

View file

@ -214,6 +214,7 @@
outputCard.scrollIntoView({ behavior: 'smooth' });
showToast('HPI generated!', 'success');
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('enc-hpi-text', data.hpi, 'hpi', document.getElementById('enc-age').value, document.getElementById('enc-setting').value);
if (typeof suggestDontMiss === 'function') suggestDontMiss('enc-hpi-text', data.hpi, 'hpi', document.getElementById('enc-age').value);
} else showToast(data.error || 'Failed', 'error');
})
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });

View file

@ -339,6 +339,7 @@
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
showToast('Sick visit note generated!', 'success');
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('sick-note-text', data.note, 'sickvisit', document.getElementById('sick-age').value);
if (typeof suggestDontMiss === 'function') suggestDontMiss('sick-note-text', data.note, 'sickvisit', document.getElementById('sick-age').value, document.getElementById('sick-cc').value);
} else {
showToast(data.error || 'Generation failed', 'error');
}

View file

@ -298,6 +298,7 @@ app.use('/api/sessions', require('./src/routes/sessions'));
app.use('/api', require('./src/routes/wellVisit'));
app.use('/api', require('./src/routes/sickVisit'));
app.use('/api', require('./src/routes/edEncounters'));
app.use('/api', require('./src/routes/dontMiss'));
app.use('/api/user', require('./src/routes/userPreferences'));
app.use('/api/admin/learning', require('./src/routes/learningAI'));

79
src/routes/dontMiss.js Normal file
View file

@ -0,0 +1,79 @@
// ============================================================
// DON'T-MISS TOOLTIP ROUTE — post-note "what to clarify or
// consider" suggestions for sick visit + encounter HPI tabs.
// Hard cap of 5 points. Separate AI pass after note generation
// so the existing note-generation routes stay untouched.
// ============================================================
var express = require('express');
var router = express.Router();
var { callAI } = require('../utils/ai');
var PROMPTS = require('../utils/prompts');
var { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
router.use(authMiddleware);
function extractJson(raw) {
var t = String(raw || '').trim();
t = t.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '');
var s = t.indexOf('{');
if (s > 0) t = t.substring(s);
var e = t.lastIndexOf('}');
if (e > -1 && e < t.length - 1) t = t.substring(0, e + 1);
try { return JSON.parse(t); } catch (err) { return null; }
}
// ── POST /dont-miss — return up to 5 high-yield items ─────
// Body:
// noteText — generated note (required)
// noteType — 'hpi' | 'sickvisit' (informational, included in context)
// patientAge — string, optional but strongly preferred
// chiefComplaint — string, optional (sick visit has it; encounter HPI doesn't)
// model — optional override
// Returns: { success, points: [{point, why}], model }
router.post('/dont-miss', async function (req, res) {
var start = Date.now();
try {
var { noteText, noteType, patientAge, chiefComplaint, model } = req.body;
if (!noteText || !noteText.trim()) {
return res.status(400).json({ error: 'noteText is required' });
}
var context = 'NOTE TYPE: ' + (noteType || 'unknown') + '\n';
context += 'PATIENT AGE: ' + (patientAge || 'unknown') + '\n';
if (chiefComplaint && chiefComplaint.trim()) {
context += 'CHIEF COMPLAINT: ' + wrapUserText('chief_complaint', chiefComplaint) + '\n';
}
context += '\nGENERATED NOTE:\n' + wrapUserText('note', noteText);
var result = await callAI([
{ role: 'system', content: PROMPTS.dontMissTooltip + INJECTION_GUARD },
{ role: 'user', content: context }
], { model: model, maxTokens: 1500 });
var parsed = extractJson(result.content);
var points = (parsed && Array.isArray(parsed.points)) ? parsed.points : [];
points = points
.filter(function (p) { return p && p.point; })
.map(function (p) { return { point: String(p.point).trim(), why: String(p.why || '').trim() }; })
.slice(0, 5); // hard cap defense, even if the model ignored the prompt cap
var dur = Date.now() - start;
logger.apiCall(req.user.id, '/api/dont-miss', {
model: result.model,
tokensInput: result.usage && result.usage.input_tokens,
tokensOutput: result.usage && result.usage.output_tokens,
duration: dur,
statusCode: 200
});
res.json({ success: true, points: points, model: result.model });
} catch (e) {
logger.error('[dontMiss] failed', e.message);
res.status(500).json({ error: 'Request failed' });
}
});
module.exports = router;

View file

@ -448,7 +448,35 @@ CRITICAL RULES:
- Use ONLY information present in the note and transcript
- Do NOT invent data points, interventions, or diagnoses
- Be conservative when ambiguous pick the lower level
- The levelRationale must reference specific elements actually documented in the encounter`
- The levelRationale must reference specific elements actually documented in the encounter`,
// ======================== DON'T-MISS TOOLTIP (post-note) ========================
// Used after sick visit / encounter HPI generation. Hard cap of 5 points.
// Returns JSON only — rendered next to the note as a side panel.
dontMissTooltip: `You are an expert pediatrician reviewing a generated clinical note for a colleague. Your job is to surface the highest-yield "don't miss" items the physician should clarify, document, or consider — tailored to the note's chief complaint and the patient's age.
${CORE_RULES}
OUTPUT FORMAT STRICT JSON ONLY, no preamble, no code fences, no commentary:
{
"points": [
{"point": "<short imperative — e.g. 'Document hydration status', 'Ask about sick contacts', 'Consider testicular torsion'>", "why": "<one-line clinical rationale>"},
...
]
}
HARD CAP: at most 5 points. Quality over quantity. If only 2 high-yield items exist, return 2.
WHAT TO SURFACE:
- Important questions the physician should clarify (e.g. "Ask about toxic ingestion" for an obtunded toddler)
- Documentation gaps a chart-reviewer would flag (e.g. "Document immunization status for this fever in infant <3mo")
- High-yield differentials worth ruling out for this age + complaint
- Reassessment / return precautions worth emphasizing
WHAT TO AVOID:
- Items already clearly documented in the note
- Generic boilerplate ("consider differential diagnosis")
- Treatment recommendations (this is a documentation/clinical-reasoning prompt, not a treatment plan)
- Padding to reach 5 fewer is fine`
};
// ── DB override support ────────────────────────────────────────────────────