#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.
79 lines
3.2 KiB
JavaScript
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;
|