pediatric-ai-scribe-v3/shared/clinical/ros-pe-dx.ts
Daniel 10dbabf83b feat(notes): structured ROS + PE + ICD-10 diagnosis pickers (WellVisit, SickVisit)
Ports the vanilla ROS / Physical Exam / ICD-10 diagnosis cards from
public/js/shadess.js (@be14578) — renderRosRows, wireRosContainer,
renderDxComponent — which the earlier React port had collapsed into
plain-text textareas.

Clinical data → shared/clinical/ros-pe-dx.ts:
  • ROS_SYSTEMS (15 systems, each with label + detail hint)
  • PE_SYSTEMS (17 systems)
  • COMMON_DX (28 pediatric quick-pick ICD-10 codes)
  • formatRosForAI / formatDxForAI — byte-identical to vanilla so
    the server-side prompt context is unchanged.
  Tests: ros-pe-dx.test.ts asserts the table counts, ordering, and
  format strings — catches the class of bug where an LLM silently
  drops an entry or re-orders the clinical reference during a port.

New React components:
  client/src/components/RosPeTable.tsx — tri-state row (WNL/Abnormal/
    Not reviewed) with auto-revealed note field on Abnormal.
    rosAllWnl / rosClear helpers mirror the "All WNL" / "Clear"
    buttons from vanilla. Accepts a btnLabels prop so the same
    component renders ROS ("WNL"/"Not reviewed") and PE ("Normal"/
    "Not examined").

  client/src/components/DxPicker.tsx — ICD-10 live search via NLM
    Clinical Tables API (free, no auth, CORS-OK) with 280ms debounce
    + AbortController; chip-style selected tags with × remove;
    28-entry common-diagnosis quick-pick grid. Enter key adds the
    top result.

Wired into WellVisit / VisitNote.tsx and SickVisit.tsx:
  • Submit now sends ros / physicalExam / diagnoses as formatted
    strings to /api/well-visit/note and /api/sick-visit/note.
  • State persists through the Save/Load encounter flow via
    partialData → the rosData/peData/diagnoses round-trip.
2026-04-24 05:24:09 +02:00

130 lines
7.4 KiB
TypeScript

// ============================================================
// ROS / PE systems + common ICD-10 diagnoses + AI formatters.
// Ported verbatim from public/js/shadess.js (@be14578):
// ROS_SYSTEMS — 15 systems
// PE_SYSTEMS — 17 systems
// COMMON_DX — 28 pediatric diagnoses quick-pick
// formatRosForAI / formatDxForAI — string format the AI receives.
// These tables are clinical reference data — do NOT re-order or
// silently "simplify" them during maintenance. Copy from the vanilla
// source when it changes.
// ============================================================
export interface SystemEntry {
key: string;
label: string;
detail: string;
}
export const ROS_SYSTEMS: ReadonlyArray<SystemEntry> = [
{ key: 'constitutional', label: 'Constitutional', detail: 'fever, fatigue, weight loss/gain, appetite' },
{ key: 'eyes', label: 'Eyes', detail: 'vision changes, discharge, redness' },
{ key: 'ent', label: 'ENT', detail: 'ear pain, congestion, sore throat, mouth sores' },
{ key: 'cardiovascular', label: 'Cardiovascular', detail: 'chest pain, palpitations, murmur' },
{ key: 'respiratory', label: 'Respiratory', detail: 'cough, wheeze, dyspnea' },
{ key: 'gastrointestinal', label: 'Gastrointestinal', detail: 'nausea, vomiting, diarrhea, constipation, abdominal pain' },
{ key: 'genitourinary', label: 'Genitourinary', detail: 'dysuria, frequency, discharge, menstrual' },
{ key: 'musculoskeletal', label: 'Musculoskeletal', detail: 'joint pain, swelling, gait issues, back pain' },
{ key: 'skin', label: 'Skin', detail: 'rash, lesions, itch, hair/nail changes' },
{ key: 'neurological', label: 'Neurological', detail: 'headache, dizziness, seizures, numbness' },
{ key: 'psychiatric', label: 'Psychiatric', detail: 'mood, anxiety, sleep, behavior changes' },
{ key: 'endocrine', label: 'Endocrine', detail: 'polyuria/polydipsia, heat/cold intolerance, growth concerns' },
{ key: 'hematologic', label: 'Hematologic/Lymphatic', detail: 'bruising, bleeding, lymph nodes' },
{ key: 'allergic', label: 'Allergic/Immunologic', detail: 'allergic reactions, frequent infections' },
{ key: 'developmental', label: 'Developmental/Behavioral', detail: 'milestones, school, social' },
];
export const PE_SYSTEMS: ReadonlyArray<SystemEntry> = [
{ key: 'general', label: 'General Appearance', detail: 'WDWN, alert, NAD, etc.' },
{ key: 'peEyes', label: 'Eyes', detail: 'PERRL, conjunctiva, EOM' },
{ key: 'ears', label: 'Ears', detail: 'TMs, canals, hearing' },
{ key: 'nose', label: 'Nose', detail: 'mucosa, septum, turbinates' },
{ key: 'mouthThroat', label: 'Mouth/Throat', detail: 'lips, gums, teeth, tonsils, pharynx' },
{ key: 'neck', label: 'Neck', detail: 'lymph nodes, thyroid, masses' },
{ key: 'chestLungs', label: 'Chest/Lungs', detail: 'air entry, wheezing, retractions' },
{ key: 'peCv', label: 'Cardiovascular', detail: 'S1/S2, murmur, pulses, cap refill' },
{ key: 'abdomen', label: 'Abdomen', detail: 'soft, tenderness, organomegaly, BS' },
{ key: 'guTanner', label: 'Genitourinary/Tanner', detail: 'Tanner stage, genitalia, hernias' },
{ key: 'msk', label: 'Musculoskeletal', detail: 'strength, ROM, gait, spine' },
{ key: 'extremities', label: 'Extremities', detail: 'tone, reflexes, clubbing, edema' },
{ key: 'peSkin', label: 'Skin', detail: 'rashes, lesions, cafe au lait, bruises' },
{ key: 'peNeuro', label: 'Neurological', detail: 'CN intact, DTRs, coordination' },
{ key: 'lymphatic', label: 'Lymphatic', detail: 'cervical, axillary, inguinal nodes' },
{ key: 'breast', label: 'Breast', detail: 'if indicated by age/Tanner' },
{ key: 'pePsych', label: 'Psychiatric', detail: 'affect, mood, behavior during exam' },
];
export interface DxEntry { code: string; name: string }
export const COMMON_DX: ReadonlyArray<DxEntry> = [
{ code: 'Z00.129', name: 'Routine child health exam, no abnormal findings' },
{ code: 'Z00.121', name: 'Routine child health exam, with abnormal findings' },
{ code: 'J06.9', name: 'Upper Respiratory Infection, NOS' },
{ code: 'J02.9', name: 'Acute pharyngitis, NOS' },
{ code: 'J03.90', name: 'Acute tonsillitis, NOS' },
{ code: 'H66.90', name: 'Otitis media, NOS' },
{ code: 'J20.9', name: 'Acute bronchitis' },
{ code: 'J45.20', name: 'Mild intermittent asthma, uncomplicated' },
{ code: 'J45.30', name: 'Mild persistent asthma' },
{ code: 'A09', name: 'Gastroenteritis/colitis' },
{ code: 'L20.9', name: 'Atopic dermatitis, NOS' },
{ code: 'L30.9', name: 'Dermatitis NOS' },
{ code: 'R50.9', name: 'Fever NOS' },
{ code: 'R10.9', name: 'Abdominal pain NOS' },
{ code: 'R05.9', name: 'Cough' },
{ code: 'B34.9', name: 'Viral infection NOS' },
{ code: 'Z23', name: 'Immunization encounter' },
{ code: 'Z41.9', name: 'Procedure for non-remedial reason' },
{ code: 'K21.0', name: 'GERD with esophagitis' },
{ code: 'K21.9', name: 'GERD without esophagitis' },
{ code: 'F90.0', name: 'ADHD, predominantly inattentive' },
{ code: 'F90.2', name: 'ADHD, combined type' },
{ code: 'F41.1', name: 'Generalized anxiety disorder' },
{ code: 'F32.9', name: 'Major depressive episode NOS' },
{ code: 'E11.9', name: 'Type 2 diabetes' },
{ code: 'E03.9', name: 'Hypothyroidism NOS' },
{ code: 'M54.5', name: 'Low back pain' },
{ code: 'G43.909', name: 'Migraine NOS' },
];
// Per-system status — undefined/'' = Not Reviewed → omit when formatted.
export type RosStatus = '' | 'wnl' | 'abnormal' | 'notrev';
export interface RosCell { status?: RosStatus; note?: string }
export type RosData = Record<string, RosCell>;
// Format ROS/PE for the AI. Mirrors window.formatRosForAI exactly —
// tells the AI which systems to expand with pertinent negatives vs
// which to treat as abnormal findings to describe.
export function formatRosForAI(
systems: ReadonlyArray<SystemEntry>,
data: RosData,
heading: string,
): string {
const lines: string[] = [heading + ':'];
let hasAny = false;
for (const sys of systems) {
const cell = data[sys.key] || {};
if (!cell.status) continue;
hasAny = true;
if (cell.status === 'wnl') {
lines.push(' ' + sys.label + ': NORMAL [domain: ' + sys.detail + ']');
} else if (cell.status === 'abnormal') {
lines.push(' ' + sys.label + ': ABNORMAL' + (cell.note ? ' — ' + cell.note : '') + ' [domain: ' + sys.detail + ']');
} else {
lines.push(' ' + sys.label + ': Not reviewed');
}
}
return hasAny ? lines.join('\n') : '';
}
// Format diagnoses list for the AI. Mirrors window.formatDxForAI.
export function formatDxForAI(dx: ReadonlyArray<DxEntry>, freetext?: string): string {
const lines: string[] = [];
for (const d of dx) {
lines.push((d.code ? d.code + ' ' : '') + d.name);
}
const t = (freetext || '').trim();
if (t) lines.push(t);
return lines.length ? 'Diagnoses:\n' + lines.map((l) => ' - ' + l).join('\n') : '';
}