From 87654b60053d020a13bb838b351128823704bd28 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 23 Apr 2026 19:39:51 +0200 Subject: [PATCH] =?UTF-8?q?refactor(ts):=20day=203=20batch=203=20=E2=80=94?= =?UTF-8?q?=20clinical=20note=20generators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/routes/{chartReview.js => chartReview.ts} | 71 +++++----- src/routes/milestones.js | 97 -------------- src/routes/milestones.ts | 119 +++++++++++++++++ src/routes/peGuide.js | 104 --------------- src/routes/peGuide.ts | 122 +++++++++++++++++ src/routes/{sickVisit.js => sickVisit.ts} | 48 +++---- src/routes/{wellVisit.js => wellVisit.ts} | 125 +++++++++--------- 7 files changed, 366 insertions(+), 320 deletions(-) rename src/routes/{chartReview.js => chartReview.ts} (56%) delete mode 100644 src/routes/milestones.js create mode 100644 src/routes/milestones.ts delete mode 100644 src/routes/peGuide.js create mode 100644 src/routes/peGuide.ts rename src/routes/{sickVisit.js => sickVisit.ts} (59%) rename src/routes/{wellVisit.js => wellVisit.ts} (66%) diff --git a/src/routes/chartReview.js b/src/routes/chartReview.ts similarity index 56% rename from src/routes/chartReview.js rename to src/routes/chartReview.ts index 63e69fd..8f92ad3 100644 --- a/src/routes/chartReview.js +++ b/src/routes/chartReview.ts @@ -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; diff --git a/src/routes/milestones.js b/src/routes/milestones.js deleted file mode 100644 index 58b4c62..0000000 --- a/src/routes/milestones.js +++ /dev/null @@ -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; diff --git a/src/routes/milestones.ts b/src/routes/milestones.ts new file mode 100644 index 0000000..a42227d --- /dev/null +++ b/src/routes/milestones.ts @@ -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> = {}; + 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; diff --git a/src/routes/peGuide.js b/src/routes/peGuide.js deleted file mode 100644 index 919e396..0000000 --- a/src/routes/peGuide.js +++ /dev/null @@ -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; diff --git a/src/routes/peGuide.ts b/src/routes/peGuide.ts new file mode 100644 index 0000000..734faf6 --- /dev/null +++ b/src/routes/peGuide.ts @@ -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 { + const groups: Record = {}; + 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; diff --git a/src/routes/sickVisit.js b/src/routes/sickVisit.ts similarity index 59% rename from src/routes/sickVisit.js rename to src/routes/sickVisit.ts index 3cd8a63..3bc85c9 100644 --- a/src/routes/sickVisit.js +++ b/src/routes/sickVisit.ts @@ -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; diff --git a/src/routes/wellVisit.js b/src/routes/wellVisit.ts similarity index 66% rename from src/routes/wellVisit.js rename to src/routes/wellVisit.ts index fd453ac..a4d3fb3 100644 --- a/src/routes/wellVisit.js +++ b/src/routes/wellVisit.ts @@ -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 = 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 = { 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;