pediatric-ai-scribe-v3/src/routes/dontMiss.js
Daniel 560ec43f8b 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.
2026-04-28 03:09:38 +02:00

79 lines
3.2 KiB
JavaScript

// ============================================================
// 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;