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:
parent
f8e883e969
commit
7e93f7c629
10 changed files with 573 additions and 2 deletions
|
|
@ -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>6–12 y</option>
|
||||
<option value="lt1">< 1 y</option>
|
||||
<option value="1to5">1–5 y</option>
|
||||
<option value="gt12">> 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>< 6 h</option>
|
||||
<option value="6to12">6–12 h</option>
|
||||
<option value=">12">> 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>
|
||||
|
|
|
|||
|
|
@ -888,6 +888,61 @@ function suggestBillingCodes(outputElementId, noteText, noteType, patientAge, vi
|
|||
|
||||
function escHtml(s) { return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
|
||||
|
||||
// ── 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);
|
||||
|
|
|
|||
|
|
@ -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(); });
|
||||
|
|
|
|||
|
|
@ -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 + '"]');
|
||||
|
|
|
|||
320
public/js/bedside/sutures.js
Normal file
320
public/js/bedside/sutures.js
Normal 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: '3–5',
|
||||
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: '3–5',
|
||||
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: '3–5',
|
||||
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: '4–5',
|
||||
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: '7–10',
|
||||
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: '5–7',
|
||||
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: '7–10',
|
||||
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: '7–10',
|
||||
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: '10–14',
|
||||
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: '10–14',
|
||||
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: '10–14',
|
||||
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: '10–14',
|
||||
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 5–7 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 25–50% 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 3–5 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 & Hedges Clinical Procedures in Emergency Medicine, 7e (2019), Ch. 36. Fleisher & 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); });
|
||||
}
|
||||
|
|
@ -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'); });
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
79
src/routes/dontMiss.js
Normal 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;
|
||||
|
|
@ -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 ────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in a new issue