pediatric-ai-scribe-v3/src/routes/wellVisit.js
Daniel Onyejesi 044c809ff3 v10: Local Whisper transcription, bigger text areas, flexible AI memory
- Add local Whisper (whisper.cpp / faster-whisper) as transcription provider
  Set TRANSCRIBE_PROVIDER=local with configurable model size and binary path
- Upgrade all refine/instruction inputs to resizable textareas across
  encounter, dictation, hospital course, chart review, well visit, sick visit
- Make AI memory injection flexible: physician preferences and corrections
  are now actively applied (not just "formatting reference"), while still
  overridable by current prompt instructions
2026-03-28 22:00:30 +00:00

233 lines
8.8 KiB
JavaScript

// ============================================================
// WELL VISIT ROUTES — SSHADESS assessment + note generation
// ============================================================
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');
// Load growth reference and BMI classification data
var scheduleData;
try {
scheduleData = require('../../public/js/pediatricScheduleData');
} catch(e) {
scheduleData = {};
}
var GROWTH_REFERENCE = scheduleData.GROWTH_REFERENCE || {};
var BMI_CLASSIFICATION = scheduleData.BMI_CLASSIFICATION || {};
// Map visit age text to GROWTH_REFERENCE keys
function getGrowthRefForAge(ageStr) {
if (!ageStr) return null;
var a = ageStr.toLowerCase().trim();
// Direct key matches
var directKeys = ['newborn', '1mo', '2mo', '4mo', '6mo', '9mo', '12mo', '15mo', '18mo', '24mo', '30mo', '3y', '4y', '5y'];
for (var i = 0; i < directKeys.length; i++) {
if (a.indexOf(directKeys[i]) !== -1) return GROWTH_REFERENCE[directKeys[i]] || null;
}
// Age-based range matching
var ageMatch = a.match(/(\d+)\s*(month|mo|year|y|yr)/i);
if (ageMatch) {
var num = parseInt(ageMatch[1]);
var unit = ageMatch[2].toLowerCase();
if (unit === 'month' || unit === 'mo') {
if (num <= 1) return GROWTH_REFERENCE['1mo'];
if (num <= 3) return GROWTH_REFERENCE['2mo'];
if (num <= 5) return GROWTH_REFERENCE['4mo'];
if (num <= 8) return GROWTH_REFERENCE['6mo'];
if (num <= 11) return GROWTH_REFERENCE['9mo'];
if (num <= 14) return GROWTH_REFERENCE['12mo'];
if (num <= 17) return GROWTH_REFERENCE['15mo'];
if (num <= 23) return GROWTH_REFERENCE['18mo'];
if (num <= 29) return GROWTH_REFERENCE['24mo'];
if (num <= 35) return GROWTH_REFERENCE['30mo'];
}
if (unit === 'year' || unit === 'y' || unit === 'yr') {
if (num <= 3) return GROWTH_REFERENCE['3y'];
if (num <= 4) return GROWTH_REFERENCE['4y'];
if (num <= 5) return GROWTH_REFERENCE['5y'];
if (num <= 10) return GROWTH_REFERENCE['6y_to_10y'];
if (num <= 14) return GROWTH_REFERENCE['11y_to_14y'];
return GROWTH_REFERENCE['15y_to_21y'];
}
}
if (a.indexOf('newborn') !== -1 || a.indexOf('birth') !== -1) return GROWTH_REFERENCE['newborn'];
return null;
}
// ── POST generate SSHADESS assessment summary ────────────────────────────
router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
var start = Date.now();
try {
var { domains, patientAge, patientGender, dictationText, model } = req.body;
if (!domains && !dictationText) {
return res.status(400).json({ error: 'Provide domain responses or dictation text' });
}
// Build structured input text
var inputText = 'PATIENT: ' + (patientAge || 'Adolescent') + ', ' + (patientGender || 'Unknown gender') + '\n\n';
inputText += 'SSHADESS SCREENING DATA:\n\n';
if (dictationText) {
inputText += 'PHYSICIAN DICTATION:\n' + dictationText + '\n\n';
}
if (domains) {
var domainOrder = ['strengths', 'school', 'home', 'activities', 'drugs', 'emotions', 'sexuality', 'safety'];
var domainNames = {
strengths: 'STRENGTHS',
school: 'SCHOOL',
home: 'HOME',
activities: 'ACTIVITIES',
drugs: 'DRUGS/SUBSTANCES',
emotions: 'EMOTIONS/EATING',
sexuality: 'SEXUALITY',
safety: 'SAFETY'
};
domainOrder.forEach(function(key) {
var d = domains[key];
if (!d || d.skipped) return;
inputText += domainNames[key] + ':\n';
if (d.questions) {
d.questions.forEach(function(q) {
if (q.answer !== null && q.answer !== undefined && q.answer !== '') {
inputText += ' - ' + q.text + ': ' + (q.answer === true || q.answer === 'yes' ? 'YES' : (q.answer === false || q.answer === 'no' ? 'NO' : q.answer)) + '\n';
}
});
}
if (d.comment && d.comment.trim()) {
inputText += ' Comments: ' + d.comment + '\n';
}
if (d.concern) {
inputText += ' *** CONCERN FLAGGED ***\n';
}
inputText += '\n';
});
}
var result = await callAI([
{ role: 'system', content: PROMPTS.shadessAssessment },
{ role: 'user', content: inputText }
], { model });
var dur = Date.now() - start;
logger.apiCall(req.user.id, '/api/well-visit/shadess', {
model: result.model, tokensInput: result.usage?.input_tokens,
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200
});
res.json({ success: true, assessment: result.content, model: result.model });
} catch (e) {
logger.error('[WellVisit] SHADESS generation failed', e.message);
res.status(500).json({ error: e.message });
}
});
// ── POST generate full well visit encounter note ─────────────────────────
router.post('/well-visit/note', authMiddleware, async function(req, res) {
var start = Date.now();
try {
var {
patientAge, patientGender, visitAge,
vitals, measurements,
transcript, dictation,
screenings, vaccines,
shadessAssessment,
parentConcerns,
ros, physicalExam, diagnoses,
physicianMemories,
noteStyle, // 'full' | 'short'
model
} = req.body;
if (!patientAge && !visitAge) {
return res.status(400).json({ error: 'Patient age or visit age required' });
}
// Assemble context
var context = 'WELL CHILD VISIT\n';
context += 'Patient: ' + (patientAge || visitAge) + ', ' + (patientGender || 'Unknown') + '\n\n';
if (vitals) {
context += 'VITAL SIGNS:\n' + vitals + '\n\n';
}
if (measurements) {
context += 'MEASUREMENTS/GROWTH:\n' + measurements + '\n\n';
}
if (parentConcerns) {
context += 'PARENT/PATIENT CONCERNS:\n' + parentConcerns + '\n\n';
}
if (transcript) {
context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + transcript + '\n\n';
} else if (dictation) {
context += 'PHYSICIAN DICTATION:\n' + dictation + '\n\n';
}
if (screenings) {
context += 'SCREENINGS COMPLETED:\n' + screenings + '\n\n';
}
if (vaccines) {
context += 'IMMUNIZATIONS TODAY:\n' + vaccines + '\n\n';
}
if (shadessAssessment) {
context += 'SSHADESS PSYCHOSOCIAL ASSESSMENT:\n' + shadessAssessment + '\n\n';
}
if (ros) {
context += ros + '\n\n';
}
if (physicalExam) {
context += physicalExam + '\n\n';
}
if (diagnoses) {
context += diagnoses + '\n\n';
}
if (physicianMemories) {
context += '[PHYSICIAN PREFERENCES & LEARNED PATTERNS — Apply these preferences to your output. These reflect how this physician writes notes, their preferred style, terminology, and corrections from past outputs. Adapt your response accordingly, but user instructions in the current prompt take priority if they conflict.]\n' + physicianMemories + '\n[END PREFERENCES]\n\n';
}
// Add growth reference and feeding guidance for this age
var growthRef = getGrowthRefForAge(patientAge || visitAge);
if (growthRef) {
context += 'GROWTH/NUTRITION GUIDANCE (AAP reference for this age):\n';
if (growthRef.weight) context += 'Expected weight gain: ' + growthRef.weight + '\n';
if (growthRef.length) context += 'Expected length/height gain: ' + growthRef.length + '\n';
if (growthRef.headCirc) context += 'Expected head circumference gain: ' + growthRef.headCirc + '\n';
if (growthRef.feeding && growthRef.feeding.length) {
context += 'Feeding guidance:\n';
growthRef.feeding.forEach(function(f) { context += ' - ' + f + '\n'; });
}
if (growthRef.bmiClassification && BMI_CLASSIFICATION.categories) {
context += 'BMI Classification (AAP 2023 / CDC Extended BMI):\n';
BMI_CLASSIFICATION.categories.forEach(function(c) {
context += ' - ' + c.label + ': ' + c.range + ' → ' + c.action + '\n';
});
}
context += '\n';
}
var prompt = (noteStyle === 'short') ? PROMPTS.wellVisitShort : PROMPTS.wellVisitNote;
var result = await callAI([
{ role: 'system', content: prompt },
{ role: 'user', content: context }
], { model });
var dur = Date.now() - start;
logger.apiCall(req.user.id, '/api/well-visit/note', {
model: result.model, tokensInput: result.usage?.input_tokens,
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200
});
res.json({ success: true, note: result.content, model: result.model });
} catch (e) {
logger.error('[WellVisit] Note generation failed', e.message);
res.status(500).json({ error: e.message });
}
});
module.exports = router;