refactor(ts): day 3 batch 3 — clinical note generators

sickVisit.ts       — /api/sick-visit/note
  wellVisit.ts       — /api/well-visit/{shadess,note}
  peGuide.ts         — /api/generate-pe-narrative (with typed PeStep)
  milestones.ts      — /api/{milestones-data, generate-milestone-narrative, generate-milestone-summary}
  chartReview.ts     — /api/generate-chart-review (typed VisitEntry etc.)

All five follow the established pattern. Added a handful of inline
interfaces (PeStep, MilestoneItem, VisitEntry) where the existing code
was juggling anonymous object shapes — these will migrate into
shared/types.ts during Day 5 if reused elsewhere.

14 of 29 routes converted. Progress: 14/54 backend files.
tsc --noEmit green.
This commit is contained in:
Daniel 2026-04-23 19:39:51 +02:00
parent 5ebabb633a
commit c48406536b
7 changed files with 366 additions and 320 deletions

View file

@ -1,58 +1,67 @@
const express = require('express');
const router = express.Router();
import express = require('express');
import type { Request, Response } from 'express';
const { callAI } = require('../utils/ai');
const PROMPTS = require('../utils/prompts');
const { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
const logger = require('../utils/logger');
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
const router = express.Router();
interface VisitEntry { date?: string; type?: string; content?: string; labs?: string }
interface SubspecialtyEntry { date?: string; specialistName?: string; specialty?: string; content?: string; labs?: string }
interface LabEntry { date?: string; values?: string }
// Generate chart review
router.post('/generate-chart-review', authMiddleware, async (req, res) => {
router.post('/generate-chart-review', authMiddleware, async (req: Request, res: Response) => {
try {
const {
type, // 'outpatient' | 'subspecialty' | 'ed'
type,
patientAge, patientGender, pmh,
visits, // Array of { date, type, content }
subspecialty, // Array of { date, specialistName, specialty, content }
edVisits, // Array of { date, content, labs }
labs, // Array of { date, values }
visits,
subspecialty,
edVisits,
labs,
model,
additionalInstructions
} = req.body;
additionalInstructions,
} = req.body as {
type?: 'outpatient' | 'subspecialty' | 'ed';
patientAge?: string;
patientGender?: string;
pmh?: string;
visits?: VisitEntry[];
subspecialty?: SubspecialtyEntry[];
edVisits?: VisitEntry[];
labs?: LabEntry[];
model?: string;
additionalInstructions?: string;
};
const today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
let clinicalData = `Today's date: ${today}\nPatient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}`;
if (pmh) clinicalData += `\nPMH: ${wrapUserText('pmh', pmh)}`;
// Use the top-level review type to select the prompt —
// the per-visit types only control data formatting, not the prompt.
let prompt;
if (type === 'ed') {
prompt = PROMPTS.chartReviewED;
} else if (type === 'subspecialty') {
prompt = PROMPTS.chartReviewSubspecialty;
} else {
prompt = PROMPTS.chartReviewOutpatient;
}
let prompt: string;
if (type === 'ed') prompt = PROMPTS.chartReviewED;
else if (type === 'subspecialty') prompt = PROMPTS.chartReviewSubspecialty;
else prompt = PROMPTS.chartReviewOutpatient;
// Include ALL visit data regardless of per-visit type —
// an outpatient chart review should include subspecialty consults too
(visits || []).forEach(v => {
(visits || []).forEach((v) => {
clinicalData += `\n\n=== OUTPATIENT VISIT (${v.date}) ===\n` + wrapUserText('visit', v.content || '');
if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n` + wrapUserText('labs', v.labs);
});
(subspecialty || []).forEach(s => {
(subspecialty || []).forEach((s) => {
clinicalData += `\n\n=== SUBSPECIALTY: ${(s.specialty || 'Subspecialty').toUpperCase()}${s.specialistName || 'Unknown'} (${s.date}) ===\n` + wrapUserText('consult', s.content || '');
if (s.labs && s.labs.trim()) clinicalData += `\n--- Labs from this visit (${s.date}) ---\n` + wrapUserText('labs', s.labs);
});
(edVisits || []).forEach(v => {
(edVisits || []).forEach((v) => {
clinicalData += `\n\n=== ED VISIT (${v.date}) ===\n` + wrapUserText('ed_visit', v.content || '');
if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n` + wrapUserText('labs', v.labs);
});
if (labs && labs.length > 0) {
clinicalData += '\n\n=== ADDITIONAL LABS (not tied to a specific visit) ===';
labs.forEach(l => { clinicalData += `\n${l.date ? l.date + ': ' : ''}` + wrapUserText('labs', l.values || ''); });
labs.forEach((l) => { clinicalData += `\n${l.date ? l.date + ': ' : ''}` + wrapUserText('labs', l.values || ''); });
}
if (additionalInstructions) {
@ -61,14 +70,14 @@ router.post('/generate-chart-review', authMiddleware, async (req, res) => {
const result = await callAI([
{ role: 'system', content: prompt + INJECTION_GUARD },
{ role: 'user', content: clinicalData }
{ role: 'user', content: clinicalData },
], { model, maxTokens: 4000 });
res.json({ success: true, review: result.content, model: result.model });
logger.audit(req.user.id, 'generate_chart_review', 'Generated chart review', req, { category: 'clinical' });
logger.audit(req.user!.id, 'generate_chart_review', 'Generated chart review', req, { category: 'clinical' });
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
module.exports = router;
export = router;

View file

@ -1,97 +0,0 @@
const express = require('express');
const router = express.Router();
const { callAI } = require('../utils/ai');
const PROMPTS = require('../utils/prompts');
const { authMiddleware } = require('../middleware/auth');
const db = require('../db/database');
var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
// Get all milestones for client (authenticated users)
router.get('/milestones-data', authMiddleware, async (req, res) => {
try {
const result = await db.query(
'SELECT * FROM developmental_milestones ORDER BY age_group, domain, sort_order, id'
);
// Group by age_group and domain to match the static data structure
const grouped = {};
result.rows.forEach(row => {
if (!grouped[row.age_group]) {
grouped[row.age_group] = {};
}
if (!grouped[row.age_group][row.domain]) {
grouped[row.age_group][row.domain] = [];
}
grouped[row.age_group][row.domain].push(row.milestone_text);
});
res.json({ success: true, milestones: grouped });
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
// Narrative format
router.post('/generate-milestone-narrative', authMiddleware, async (req, res) => {
try {
const { milestones, ageGroup, patientAge, patientGender, model, format } = req.body;
if (!milestones) return res.status(400).json({ error: 'No milestones' });
const achieved = milestones.filter(m => m.status === 'yes');
const notAchieved = milestones.filter(m => m.status === 'no');
const notAssessed = milestones.filter(m => m.status === null);
if (achieved.length === 0 && notAchieved.length === 0) {
return res.json({ success: true, narrative: 'No developmental milestones were assessed during this visit.', summary: { achieved: 0, notAchieved: 0, notAssessed: notAssessed.length } });
}
let milestoneText = '';
if (achieved.length > 0) {
milestoneText += 'ACHIEVED:\n';
achieved.forEach(m => { milestoneText += ` - [${m.domain}] ${m.milestone}\n`; });
}
if (notAchieved.length > 0) {
milestoneText += '\nNOT YET ACHIEVED:\n';
notAchieved.forEach(m => { milestoneText += ` - [${m.domain}] ${m.milestone}\n`; });
}
// Choose narrative or list format
const prompt = (format === 'list') ? PROMPTS.milestoneList : PROMPTS.milestoneNarrative;
const result = await callAI([
{ role: 'system', content: prompt + INJECTION_GUARD },
{ role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\n` + wrapUserText('milestones', milestoneText) + `\n\nAssessed: ${achieved.length + notAchieved.length} | Achieved: ${achieved.length} | Not achieved: ${notAchieved.length} | Not assessed (OMIT): ${notAssessed.length}` }
], { model });
res.json({
success: true,
narrative: result.content,
model: result.model,
summary: { achieved: achieved.length, notAchieved: notAchieved.length, notAssessed: notAssessed.length }
});
logger.audit(req.user.id, 'generate_milestone_narrative', 'Generated milestone narrative', req, { category: 'clinical' });
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
// 3-sentence summary
router.post('/generate-milestone-summary', authMiddleware, async (req, res) => {
try {
const { narrative, ageGroup, patientAge, patientGender, model } = req.body;
if (!narrative) return res.status(400).json({ error: 'No narrative' });
const result = await callAI([
{ role: 'system', content: PROMPTS.milestoneSummary + INJECTION_GUARD },
{ role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\nNARRATIVE:\n` + wrapUserText('narrative', narrative) }
], { model, maxTokens: 500 });
res.json({ success: true, summary: result.content, model: result.model });
logger.audit(req.user.id, 'generate_milestone_summary', 'Generated milestone summary', req, { category: 'clinical' });
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
module.exports = router;

119
src/routes/milestones.ts Normal file
View file

@ -0,0 +1,119 @@
import express = require('express');
import type { Request, Response } from 'express';
const { callAI } = require('../utils/ai');
const PROMPTS = require('../utils/prompts');
const { authMiddleware } = require('../middleware/auth');
const db = require('../db/database');
const logger = require('../utils/logger');
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
const router = express.Router();
interface MilestoneItem {
domain: string;
milestone: string;
status: 'yes' | 'no' | null;
}
// Get all milestones for client (authenticated users)
router.get('/milestones-data', authMiddleware, async (_req: Request, res: Response) => {
try {
const result = await db.query(
'SELECT * FROM developmental_milestones ORDER BY age_group, domain, sort_order, id'
);
const grouped: Record<string, Record<string, string[]>> = {};
result.rows.forEach((row: any) => {
if (!grouped[row.age_group]) grouped[row.age_group] = {};
if (!grouped[row.age_group][row.domain]) grouped[row.age_group][row.domain] = [];
grouped[row.age_group][row.domain].push(row.milestone_text);
});
res.json({ success: true, milestones: grouped });
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
// Narrative format
router.post('/generate-milestone-narrative', authMiddleware, async (req: Request, res: Response) => {
try {
const { milestones, ageGroup, patientAge, patientGender, model, format } = req.body as {
milestones: MilestoneItem[];
ageGroup?: string;
patientAge?: string;
patientGender?: string;
model?: string;
format?: string;
};
if (!milestones) return res.status(400).json({ error: 'No milestones' });
const achieved = milestones.filter((m) => m.status === 'yes');
const notAchieved = milestones.filter((m) => m.status === 'no');
const notAssessed = milestones.filter((m) => m.status === null);
if (achieved.length === 0 && notAchieved.length === 0) {
return res.json({
success: true,
narrative: 'No developmental milestones were assessed during this visit.',
summary: { achieved: 0, notAchieved: 0, notAssessed: notAssessed.length },
});
}
let milestoneText = '';
if (achieved.length > 0) {
milestoneText += 'ACHIEVED:\n';
achieved.forEach((m) => { milestoneText += ` - [${m.domain}] ${m.milestone}\n`; });
}
if (notAchieved.length > 0) {
milestoneText += '\nNOT YET ACHIEVED:\n';
notAchieved.forEach((m) => { milestoneText += ` - [${m.domain}] ${m.milestone}\n`; });
}
const prompt = format === 'list' ? PROMPTS.milestoneList : PROMPTS.milestoneNarrative;
const result = await callAI([
{ role: 'system', content: prompt + INJECTION_GUARD },
{
role: 'user',
content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\n`
+ wrapUserText('milestones', milestoneText)
+ `\n\nAssessed: ${achieved.length + notAchieved.length} | Achieved: ${achieved.length} | Not achieved: ${notAchieved.length} | Not assessed (OMIT): ${notAssessed.length}`,
},
], { model });
res.json({
success: true,
narrative: result.content,
model: result.model,
summary: { achieved: achieved.length, notAchieved: notAchieved.length, notAssessed: notAssessed.length },
});
logger.audit(req.user!.id, 'generate_milestone_narrative', 'Generated milestone narrative', req, { category: 'clinical' });
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
// 3-sentence summary
router.post('/generate-milestone-summary', authMiddleware, async (req: Request, res: Response) => {
try {
const { narrative, ageGroup, patientAge, patientGender, model } = req.body;
if (!narrative) return res.status(400).json({ error: 'No narrative' });
const result = await callAI([
{ role: 'system', content: PROMPTS.milestoneSummary + INJECTION_GUARD },
{
role: 'user',
content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\nNARRATIVE:\n`
+ wrapUserText('narrative', narrative),
},
], { model, maxTokens: 500 });
res.json({ success: true, summary: result.content, model: result.model });
logger.audit(req.user!.id, 'generate_milestone_summary', 'Generated milestone summary', req, { category: 'clinical' });
} catch (err) {
res.status(500).json({ error: 'Request failed' });
}
});
export = router;

View file

@ -1,104 +0,0 @@
// ============================================================
// PHYSICAL EXAM GUIDE — step-level narrative + list generation
// ============================================================
// Receives a flat array of exam STEPS (each with component name, label,
// method, expected normal, status, and optional abnormal note). Returns
// a professional two-section report (Technique performed + Findings).
// Mirrors the milestone-narrative pattern: same AppRole-injection guard,
// same clinical audit category.
var express = require('express');
var router = express.Router();
var { callAI } = require('../utils/ai');
var PROMPTS = require('../utils/prompts');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
var { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger');
router.post('/generate-pe-narrative', authMiddleware, async function (req, res) {
try {
var { steps, ageGroup, system, patientAge, patientGender, model, format } = req.body;
if (!Array.isArray(steps) || steps.length === 0) {
return res.status(400).json({ error: 'No steps provided' });
}
// Partition by assessment status
var normal = steps.filter(function (s) { return s.status === 'normal'; });
var abnormal = steps.filter(function (s) { return s.status === 'abnormal'; });
if (normal.length === 0 && abnormal.length === 0) {
return res.json({
success: true,
narrative: 'No physical-exam steps were assessed.',
summary: { normal: 0, abnormal: 0, notAssessed: steps.length }
});
}
// Group by component for readability — AI works better with structure.
function groupByComponent(list) {
var groups = {};
list.forEach(function (s) {
var key = s.component || 'Other';
if (!groups[key]) groups[key] = [];
groups[key].push(s);
});
return groups;
}
var normalGroups = groupByComponent(normal);
var abnormalGroups = groupByComponent(abnormal);
// Build structured text for the model
var text = '';
if (Object.keys(normalGroups).length) {
text += 'NORMAL FINDINGS (grouped by exam component):\n';
Object.keys(normalGroups).forEach(function (comp) {
text += '\n[' + comp + ']\n';
normalGroups[comp].forEach(function (s) {
text += ' • ' + s.label + '\n';
text += ' method: ' + (s.method || 'standard method') + '\n';
text += ' expected normal: ' + (s.normal || 'unspecified') + '\n';
});
});
}
if (Object.keys(abnormalGroups).length) {
text += '\nABNORMAL FINDINGS (grouped by exam component):\n';
Object.keys(abnormalGroups).forEach(function (comp) {
text += '\n[' + comp + ']\n';
abnormalGroups[comp].forEach(function (s) {
text += ' • ' + s.label + '\n';
text += ' method: ' + (s.method || 'standard method') + '\n';
text += ' physician note: ' + (s.note || 'abnormal, no detail provided') + '\n';
});
});
}
var prompt = (format === 'list') ? PROMPTS.peGuideList : PROMPTS.peGuideNarrative;
var systemLabel = (system === 'neuro') ? 'Neurologic' : 'Musculoskeletal';
var result = await callAI([
{ role: 'system', content: prompt + INJECTION_GUARD },
{ role: 'user', content:
'Patient: ' + (patientAge || 'Unknown') + ', ' + (patientGender || 'Unknown') + '\n' +
'Age group: ' + (ageGroup || 'Unknown') + '\n' +
'Exam system: ' + systemLabel + '\n\n' +
wrapUserText('pe_steps', text) +
'\n\nAssessed steps: ' + (normal.length + abnormal.length) +
' | Normal: ' + normal.length +
' | Abnormal: ' + abnormal.length
}
], { model: model });
res.json({
success: true,
narrative: result.content,
model: result.model,
summary: { normal: normal.length, abnormal: abnormal.length, notAssessed: steps.length - normal.length - abnormal.length }
});
logger.audit(req.user.id, 'generate_pe_narrative', 'Generated PE narrative (' + systemLabel + ')', req, { category: 'clinical' });
} catch (err) {
logger.error('POST /generate-pe-narrative', err.message);
res.status(500).json({ error: 'Request failed' });
}
});
module.exports = router;

122
src/routes/peGuide.ts Normal file
View file

@ -0,0 +1,122 @@
// ============================================================
// PHYSICAL EXAM GUIDE — step-level narrative + list generation
// ============================================================
// Receives a flat array of exam STEPS (each with component name, label,
// method, expected normal, status, and optional abnormal note). Returns
// a professional two-section report (Technique performed + Findings).
// Mirrors the milestone-narrative pattern: same AppRole-injection guard,
// same clinical audit category.
import express = require('express');
import type { Request, Response } from 'express';
const { callAI } = require('../utils/ai');
const PROMPTS = require('../utils/prompts');
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
const { authMiddleware } = require('../middleware/auth');
const logger = require('../utils/logger');
const router = express.Router();
interface PeStep {
component?: string;
label: string;
method?: string;
normal?: string;
status?: 'normal' | 'abnormal' | null;
note?: string;
}
router.post('/generate-pe-narrative', authMiddleware, async function (req: Request, res: Response) {
try {
const { steps, ageGroup, system, patientAge, patientGender, model, format } = req.body as {
steps: PeStep[];
ageGroup?: string;
system?: string;
patientAge?: string;
patientGender?: string;
model?: string;
format?: string;
};
if (!Array.isArray(steps) || steps.length === 0) {
return res.status(400).json({ error: 'No steps provided' });
}
const normal = steps.filter((s) => s.status === 'normal');
const abnormal = steps.filter((s) => s.status === 'abnormal');
if (normal.length === 0 && abnormal.length === 0) {
return res.json({
success: true,
narrative: 'No physical-exam steps were assessed.',
summary: { normal: 0, abnormal: 0, notAssessed: steps.length },
});
}
function groupByComponent(list: PeStep[]): Record<string, PeStep[]> {
const groups: Record<string, PeStep[]> = {};
list.forEach((s) => {
const key = s.component || 'Other';
if (!groups[key]) groups[key] = [];
groups[key].push(s);
});
return groups;
}
const normalGroups = groupByComponent(normal);
const abnormalGroups = groupByComponent(abnormal);
let text = '';
if (Object.keys(normalGroups).length) {
text += 'NORMAL FINDINGS (grouped by exam component):\n';
Object.keys(normalGroups).forEach((comp) => {
text += '\n[' + comp + ']\n';
normalGroups[comp].forEach((s) => {
text += ' • ' + s.label + '\n';
text += ' method: ' + (s.method || 'standard method') + '\n';
text += ' expected normal: ' + (s.normal || 'unspecified') + '\n';
});
});
}
if (Object.keys(abnormalGroups).length) {
text += '\nABNORMAL FINDINGS (grouped by exam component):\n';
Object.keys(abnormalGroups).forEach((comp) => {
text += '\n[' + comp + ']\n';
abnormalGroups[comp].forEach((s) => {
text += ' • ' + s.label + '\n';
text += ' method: ' + (s.method || 'standard method') + '\n';
text += ' physician note: ' + (s.note || 'abnormal, no detail provided') + '\n';
});
});
}
const prompt = format === 'list' ? PROMPTS.peGuideList : PROMPTS.peGuideNarrative;
const systemLabel = system === 'neuro' ? 'Neurologic' : 'Musculoskeletal';
const result = await callAI([
{ role: 'system', content: prompt + INJECTION_GUARD },
{
role: 'user',
content:
'Patient: ' + (patientAge || 'Unknown') + ', ' + (patientGender || 'Unknown') + '\n' +
'Age group: ' + (ageGroup || 'Unknown') + '\n' +
'Exam system: ' + systemLabel + '\n\n' +
wrapUserText('pe_steps', text) +
'\n\nAssessed steps: ' + (normal.length + abnormal.length) +
' | Normal: ' + normal.length +
' | Abnormal: ' + abnormal.length,
},
], { model });
res.json({
success: true,
narrative: result.content,
model: result.model,
summary: { normal: normal.length, abnormal: abnormal.length, notAssessed: steps.length - normal.length - abnormal.length },
});
logger.audit(req.user!.id, 'generate_pe_narrative', 'Generated PE narrative (' + systemLabel + ')', req, { category: 'clinical' });
} catch (err: any) {
logger.error('POST /generate-pe-narrative', err.message);
res.status(500).json({ error: 'Request failed' });
}
});
export = router;

View file

@ -2,32 +2,32 @@
// SICK VISIT ROUTE — sick visit 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');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
import express = require('express');
import type { Request, Response } from 'express';
const { callAI } = require('../utils/ai');
const PROMPTS = require('../utils/prompts');
const { authMiddleware } = require('../middleware/auth');
const logger = require('../utils/logger');
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
// ── POST generate sick visit note ────────────────────────────────────────
router.post('/sick-visit/note', authMiddleware, async function(req, res) {
var start = Date.now();
const router = express.Router();
router.post('/sick-visit/note', authMiddleware, async function (req: Request, res: Response) {
const start = Date.now();
try {
var {
const {
patientAge, patientGender, chiefComplaint,
transcript, dictation,
ros, physicalExam, diagnoses,
physicianMemories,
model
model,
} = req.body;
if (!chiefComplaint) {
return res.status(400).json({ error: 'Chief complaint is required' });
}
// Assemble context
var context = 'SICK VISIT\n';
let context = 'SICK VISIT\n';
context += 'Patient: ' + (patientAge || 'Unknown age') + ', ' + (patientGender || 'Unknown gender') + '\n';
context += 'Chief Complaint: ' + wrapUserText('chief_complaint', chiefComplaint) + '\n\n';
@ -36,31 +36,31 @@ router.post('/sick-visit/note', authMiddleware, async function(req, res) {
} else if (dictation) {
context += 'PHYSICIAN DICTATION:\n' + wrapUserText('dictation', dictation) + '\n\n';
}
if (ros) context += wrapUserText('ros', ros) + '\n\n';
if (ros) context += wrapUserText('ros', ros) + '\n\n';
if (physicalExam) context += wrapUserText('physical_exam', physicalExam) + '\n\n';
if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n';
if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n';
if (physicianMemories) {
context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n'
+ wrapUserText('style_hints', physicianMemories) + '\n[END STYLE HINTS]\n\n';
}
var result = await callAI([
const result = await callAI([
{ role: 'system', content: PROMPTS.sickVisitNote + INJECTION_GUARD },
{ role: 'user', content: context }
{ role: 'user', content: context },
], { model });
var dur = Date.now() - start;
logger.apiCall(req.user.id, '/api/sick-visit/note', {
const dur = Date.now() - start;
logger.apiCall(req.user!.id, '/api/sick-visit/note', {
model: result.model, tokensInput: result.usage?.input_tokens,
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200,
});
res.json({ success: true, note: result.content, model: result.model });
logger.audit(req.user.id, 'generate_sick_visit', 'Generated sick visit note', req, { category: 'clinical' });
} catch (e) {
logger.audit(req.user!.id, 'generate_sick_visit', 'Generated sick visit note', req, { category: 'clinical' });
} catch (e: any) {
logger.error('[SickVisit] Note generation failed', e.message);
res.status(500).json({ error: 'Request failed' });
}
});
module.exports = router;
export = router;

View file

@ -2,38 +2,38 @@
// 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');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
import express = require('express');
import type { Request, Response } from 'express';
const { callAI } = require('../utils/ai');
const PROMPTS = require('../utils/prompts');
const { authMiddleware } = require('../middleware/auth');
const logger = require('../utils/logger');
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
const router = express.Router();
// Load growth reference and BMI classification data
var scheduleData;
let scheduleData: any;
try {
scheduleData = require('../../public/js/pediatricScheduleData');
} catch(e) {
} catch (e) {
scheduleData = {};
}
var GROWTH_REFERENCE = scheduleData.GROWTH_REFERENCE || {};
var BMI_CLASSIFICATION = scheduleData.BMI_CLASSIFICATION || {};
const GROWTH_REFERENCE: Record<string, any> = scheduleData.GROWTH_REFERENCE || {};
const BMI_CLASSIFICATION: any = scheduleData.BMI_CLASSIFICATION || {};
// Map visit age text to GROWTH_REFERENCE keys
function getGrowthRefForAge(ageStr) {
function getGrowthRefForAge(ageStr?: string): any {
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++) {
const a = ageStr.toLowerCase().trim();
const directKeys = ['newborn', '1mo', '2mo', '4mo', '6mo', '9mo', '12mo', '15mo', '18mo', '24mo', '30mo', '3y', '4y', '5y'];
for (let 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);
const ageMatch = a.match(/(\d+)\s*(month|mo|year|y|yr)/i);
if (ageMatch) {
var num = parseInt(ageMatch[1]);
var unit = ageMatch[2].toLowerCase();
const num = parseInt(ageMatch[1]);
const unit = ageMatch[2].toLowerCase();
if (unit === 'month' || unit === 'mo') {
if (num <= 1) return GROWTH_REFERENCE['1mo'];
if (num <= 3) return GROWTH_REFERENCE['2mo'];
@ -60,17 +60,16 @@ function getGrowthRefForAge(ageStr) {
}
// ── POST generate SSHADESS assessment summary ────────────────────────────
router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
var start = Date.now();
router.post('/well-visit/shadess', authMiddleware, async function (req: Request, res: Response) {
const start = Date.now();
try {
var { domains, patientAge, patientGender, dictationText, model } = req.body;
const { 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';
let inputText = 'PATIENT: ' + (patientAge || 'Adolescent') + ', ' + (patientGender || 'Unknown gender') + '\n\n';
inputText += 'SSHADESS SCREENING DATA:\n\n';
if (dictationText) {
@ -78,8 +77,8 @@ router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
}
if (domains) {
var domainOrder = ['strengths', 'school', 'home', 'activities', 'drugs', 'emotions', 'sexuality', 'safety'];
var domainNames = {
const domainOrder = ['strengths', 'school', 'home', 'activities', 'drugs', 'emotions', 'sexuality', 'safety'];
const domainNames: Record<string, string> = {
strengths: 'STRENGTHS',
school: 'SCHOOL',
home: 'HOME',
@ -87,16 +86,16 @@ router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
drugs: 'DRUGS/SUBSTANCES',
emotions: 'EMOTIONS/EATING',
sexuality: 'SEXUALITY',
safety: 'SAFETY'
safety: 'SAFETY',
};
domainOrder.forEach(function(key) {
var d = domains[key];
domainOrder.forEach((key) => {
const d = domains[key];
if (!d || d.skipped) return;
inputText += domainNames[key] + ':\n';
if (d.questions) {
d.questions.forEach(function(q) {
d.questions.forEach((q: any) => {
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';
}
@ -112,30 +111,30 @@ router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
});
}
var result = await callAI([
const result = await callAI([
{ role: 'system', content: PROMPTS.shadessAssessment + INJECTION_GUARD },
{ role: 'user', content: wrapUserText('sshadess_answers', inputText) }
{ role: 'user', content: wrapUserText('sshadess_answers', inputText) },
], { model });
var dur = Date.now() - start;
logger.apiCall(req.user.id, '/api/well-visit/shadess', {
const 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
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200,
});
res.json({ success: true, assessment: result.content, model: result.model });
logger.audit(req.user.id, 'generate_shadess', 'Generated SSHADESS assessment', req, { category: 'clinical' });
} catch (e) {
logger.audit(req.user!.id, 'generate_shadess', 'Generated SSHADESS assessment', req, { category: 'clinical' });
} catch (e: any) {
logger.error('[WellVisit] SHADESS generation failed', e.message);
res.status(500).json({ error: 'Request failed' });
}
});
// ── POST generate full well visit encounter note ─────────────────────────
router.post('/well-visit/note', authMiddleware, async function(req, res) {
var start = Date.now();
router.post('/well-visit/note', authMiddleware, async function (req: Request, res: Response) {
const start = Date.now();
try {
var {
const {
patientAge, patientGender, visitAge,
vitals, measurements,
transcript, dictation,
@ -144,36 +143,34 @@ router.post('/well-visit/note', authMiddleware, async function(req, res) {
parentConcerns,
ros, physicalExam, diagnoses,
physicianMemories,
noteStyle, // 'full' | 'short'
model
noteStyle,
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';
let context = 'WELL CHILD VISIT\n';
context += 'Patient: ' + (patientAge || visitAge) + ', ' + (patientGender || 'Unknown') + '\n\n';
if (vitals) context += 'VITAL SIGNS:\n' + wrapUserText('vitals', vitals) + '\n\n';
if (vitals) context += 'VITAL SIGNS:\n' + wrapUserText('vitals', vitals) + '\n\n';
if (measurements) context += 'MEASUREMENTS/GROWTH:\n' + wrapUserText('measurements', measurements) + '\n\n';
if (parentConcerns) context += 'PARENT/PATIENT CONCERNS:\n' + wrapUserText('concerns', parentConcerns) + '\n\n';
if (transcript) context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + wrapUserText('transcript', transcript) + '\n\n';
if (transcript) context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + wrapUserText('transcript', transcript) + '\n\n';
else if (dictation) context += 'PHYSICIAN DICTATION:\n' + wrapUserText('dictation', dictation) + '\n\n';
if (screenings) context += 'SCREENINGS COMPLETED:\n' + wrapUserText('screenings', screenings) + '\n\n';
if (vaccines) context += 'IMMUNIZATIONS TODAY:\n' + wrapUserText('vaccines', vaccines) + '\n\n';
if (screenings) context += 'SCREENINGS COMPLETED:\n' + wrapUserText('screenings', screenings) + '\n\n';
if (vaccines) context += 'IMMUNIZATIONS TODAY:\n' + wrapUserText('vaccines', vaccines) + '\n\n';
if (shadessAssessment) context += 'SSHADESS PSYCHOSOCIAL ASSESSMENT:\n' + wrapUserText('sshadess', shadessAssessment) + '\n\n';
if (ros) context += wrapUserText('ros', ros) + '\n\n';
if (ros) context += wrapUserText('ros', ros) + '\n\n';
if (physicalExam) context += wrapUserText('physical_exam', physicalExam) + '\n\n';
if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n';
if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n';
if (physicianMemories) {
context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n'
+ wrapUserText('style_hints', physicianMemories) + '\n[END STYLE HINTS]\n\n';
}
// Add growth reference and feeding guidance for this age
var growthRef = getGrowthRefForAge(patientAge || visitAge);
const 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';
@ -181,36 +178,36 @@ router.post('/well-visit/note', authMiddleware, async function(req, res) {
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'; });
growthRef.feeding.forEach((f: string) => { context += ' - ' + f + '\n'; });
}
if (growthRef.bmiClassification && BMI_CLASSIFICATION.categories) {
context += 'BMI Classification (AAP 2023 / CDC Extended BMI):\n';
BMI_CLASSIFICATION.categories.forEach(function(c) {
BMI_CLASSIFICATION.categories.forEach((c: any) => {
context += ' - ' + c.label + ': ' + c.range + ' → ' + c.action + '\n';
});
}
context += '\n';
}
var prompt = (noteStyle === 'short') ? PROMPTS.wellVisitShort : PROMPTS.wellVisitNote;
const prompt = noteStyle === 'short' ? PROMPTS.wellVisitShort : PROMPTS.wellVisitNote;
var result = await callAI([
const result = await callAI([
{ role: 'system', content: prompt + INJECTION_GUARD },
{ role: 'user', content: context }
{ role: 'user', content: context },
], { model });
var dur = Date.now() - start;
logger.apiCall(req.user.id, '/api/well-visit/note', {
const 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
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200,
});
res.json({ success: true, note: result.content, model: result.model });
logger.audit(req.user.id, 'generate_well_visit', 'Generated well visit note', req, { category: 'clinical' });
} catch (e) {
logger.audit(req.user!.id, 'generate_well_visit', 'Generated well visit note', req, { category: 'clinical' });
} catch (e: any) {
logger.error('[WellVisit] Note generation failed', e.message);
res.status(500).json({ error: 'Request failed' });
}
});
module.exports = router;
export = router;