feat(pe-guide): Physical Exam Guide tab — OSCE reference + narrative report
New top-level tab (positioned after Catch-Up Schedule) combining two
functions:
1. Study reference — for each (age group, system) shows OSCE-style
components with technique, expected normal finding, and abnormal-
feature watch-list.
2. Documentation generator — physician marks each component
Normal / Abnormal (with free-text detail) / Skip; AI produces a
two-section report (Technique + Findings), narrative or structured
list format.
Scope v1: MSK + Neuro × 6 age groups (newborn, infant, toddler,
preschool, school-age, adolescent). More systems can be added to the
embedded PE_DATA in peGuide.js without route changes.
Files:
- src/routes/peGuide.js — POST /api/generate-pe-narrative (mirrors
milestone-narrative pattern: AppRole-level
injection guard, clinical audit category,
PHI redaction upstream already in place)
- src/utils/prompts.js — peGuideNarrative + peGuideList prompts,
structured two-section output
- public/components/pe-guide.html — demographics bar + sub-pills + cards
- public/js/peGuide.js — embedded PE_DATA (all clinical content),
render + state + AI call
- public/index.html — tab button, section, script include
- server.js — mount route at /api
No schema change. No PHI stored — findings live in memory only, exported
via existing copy/read-aloud/Nextcloud actions.
This commit is contained in:
parent
b411dbbbf0
commit
fd7f684fe9
6 changed files with 725 additions and 0 deletions
75
public/components/pe-guide.html
Normal file
75
public/components/pe-guide.html
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<div class="module-header">
|
||||
<h2><i class="fas fa-stethoscope"></i> Physical Exam Guide</h2>
|
||||
<p>OSCE-style reference for age-appropriate MSK and Neuro exam + narrative report generator</p>
|
||||
</div>
|
||||
|
||||
<div class="demographics-bar">
|
||||
<div class="demo-field">
|
||||
<label>Age Group</label>
|
||||
<select id="pe-age-group">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="newborn">Newborn (0–28 days)</option>
|
||||
<option value="infant">Infant (1–12 months)</option>
|
||||
<option value="toddler">Toddler (1–3 years)</option>
|
||||
<option value="preschool">Preschool (3–5 years)</option>
|
||||
<option value="school">School-age (6–11 years)</option>
|
||||
<option value="adolescent">Adolescent (12–21 years)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="demo-field">
|
||||
<label>Patient Age</label>
|
||||
<input type="text" id="pe-age" placeholder="e.g., 4 months">
|
||||
</div>
|
||||
<div class="demo-field">
|
||||
<label>Gender</label>
|
||||
<select id="pe-gender">
|
||||
<option value="">Select</option>
|
||||
<option>Male</option>
|
||||
<option>Female</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="demo-field">
|
||||
<label>Report Style</label>
|
||||
<select id="pe-format">
|
||||
<option value="narrative">Narrative (paragraphs)</option>
|
||||
<option value="list">Structured List</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="demo-field demo-field-model">
|
||||
<label><i class="fas fa-robot"></i> Model</label>
|
||||
<select class="tab-model-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wv-subtab-bar">
|
||||
<button class="wv-subtab-btn active" data-pesystem="msk">
|
||||
<i class="fas fa-bone"></i> Musculoskeletal
|
||||
</button>
|
||||
<button class="wv-subtab-btn" data-pesystem="neuro">
|
||||
<i class="fas fa-brain"></i> Neurologic
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="pe-content">
|
||||
<p style="text-align:center;color:#9ca3af;padding:40px;">Select an age group above to see the exam guide.</p>
|
||||
</div>
|
||||
|
||||
<div class="milestone-actions" id="pe-actions" style="display:none">
|
||||
<button id="pe-all-normal" class="btn-sm btn-success"><i class="fas fa-check-double"></i> All Normal</button>
|
||||
<button id="pe-all-clear" class="btn-sm btn-ghost"><i class="fas fa-eraser"></i> Clear All</button>
|
||||
<button id="pe-generate-btn" class="btn-generate btn-generate-teal"><i class="fas fa-wand-magic-sparkles"></i> Generate Exam Report</button>
|
||||
</div>
|
||||
|
||||
<div id="pe-output" class="card output-card hidden">
|
||||
<div class="card-header output-header">
|
||||
<h3><i class="fas fa-file-medical"></i> Physical Exam Report</h3>
|
||||
<div class="output-actions">
|
||||
<span id="pe-model-tag" class="model-tag"></span>
|
||||
<button class="btn-sm btn-primary" data-action="copy" data-target="pe-narrative-text"><i class="fas fa-copy"></i> Copy</button>
|
||||
<button class="btn-sm btn-ghost" data-action="speak" data-target="pe-narrative-text"><i class="fas fa-volume-high"></i> Read</button>
|
||||
<button class="btn-sm btn-ghost" data-action="nc-export" data-target="pe-narrative-text" data-label="physical-exam"><i class="fas fa-cloud-arrow-up"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pe-summary-bar" class="summary-bar hidden"></div>
|
||||
<div id="pe-narrative-text" class="output-text" contenteditable="true"></div>
|
||||
</div>
|
||||
|
|
@ -232,6 +232,10 @@
|
|||
<i class="fas fa-rotate"></i>
|
||||
<span>Catch-Up Schedule</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="peguide">
|
||||
<i class="fas fa-stethoscope"></i>
|
||||
<span>Physical Exam Guide</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="calculators">
|
||||
<i class="fas fa-calculator"></i>
|
||||
<span>Calculators</span>
|
||||
|
|
@ -285,6 +289,7 @@
|
|||
<section id="soap-tab" class="tab-content" data-component="soap"></section>
|
||||
<section id="vaxschedule-tab" class="tab-content" data-component="vaxschedule"></section>
|
||||
<section id="catchup-tab" class="tab-content" data-component="catchup"></section>
|
||||
<section id="peguide-tab" class="tab-content" data-component="pe-guide"></section>
|
||||
<section id="calculators-tab" class="tab-content" data-component="calculators"></section>
|
||||
<section id="learning-tab" class="tab-content" data-component="learning"></section>
|
||||
<section id="cms-tab" class="tab-content" data-component="cms"></section>
|
||||
|
|
@ -437,6 +442,7 @@
|
|||
<script defer src="/js/chartReview.js"></script>
|
||||
<script defer src="/js/soap.js"></script>
|
||||
<script defer src="/js/milestones.js"></script>
|
||||
<script defer src="/js/peGuide.js"></script>
|
||||
<script defer src="/js/nextcloud.js"></script>
|
||||
<script defer src="/js/wellVisit.js"></script>
|
||||
<script defer src="/js/shadess.js"></script>
|
||||
|
|
|
|||
511
public/js/peGuide.js
Normal file
511
public/js/peGuide.js
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
// ============================================================
|
||||
// PHYSICAL EXAM GUIDE — OSCE reference + narrative generator
|
||||
// ============================================================
|
||||
// Data is embedded here rather than fetched: the knowledge is static and
|
||||
// small, and it needs to be available offline-in-browser without an API
|
||||
// round-trip. Scope for v1: MSK + Neuro × 6 age groups. Expand later.
|
||||
//
|
||||
// Each exam component has:
|
||||
// name — what to document
|
||||
// technique — how to perform (OSCE-style)
|
||||
// normal — expected finding text (used verbatim when user picks Normal)
|
||||
// abnormalHints — red flags / things to document if abnormal (reference only)
|
||||
// ============================================================
|
||||
|
||||
(function () {
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// DATA
|
||||
// ────────────────────────────────────────────────────────────
|
||||
var PE_DATA = {
|
||||
newborn: {
|
||||
label: 'Newborn (0–28 days)',
|
||||
msk: {
|
||||
overview: 'Focus: birth injuries, congenital anomalies, and DDH screening (universal Barlow/Ortolani through 6 months). Examine warm, quiet, undressed infant.',
|
||||
components: [
|
||||
{ name: 'Resting posture', technique: 'Observe supine. Note symmetry of limb position.',
|
||||
normal: 'Symmetric flexion at hips, knees, elbows; hands loosely fisted',
|
||||
abnormalHints: ['Frog-leg (hypotonia)', 'Asymmetric arm posture (brachial plexus, clavicle fx)', 'Opisthotonic posturing (CNS)'] },
|
||||
{ name: 'Clavicles', technique: 'Palpate each clavicle from sternoclavicular joint to acromion.',
|
||||
normal: 'Smooth, continuous bilaterally; no crepitus or step-off',
|
||||
abnormalHints: ['Crepitus or step-off (fracture — LGA / shoulder dystocia)', 'Pain with passive arm abduction'] },
|
||||
{ name: 'Hips (Barlow/Ortolani)', technique: 'Flex hips 90°, grasp thighs with thumbs medial. Barlow: adduct + downward pressure. Ortolani: abduct + lift.',
|
||||
normal: 'Hips stable; no clunk on Barlow or Ortolani; symmetric abduction ≥75°',
|
||||
abnormalHints: ['Palpable clunk (positive Ortolani = dislocatable or dislocated)', 'Asymmetric thigh/gluteal folds', 'Limited abduction', 'Leg-length discrepancy (Galeazzi)'] },
|
||||
{ name: 'Spine / back', technique: 'Prone. Palpate midline from C-spine to coccyx. Inspect sacral area.',
|
||||
normal: 'Straight midline; no cutaneous markers; skin intact; no sacral mass',
|
||||
abnormalHints: ['Sacral dimple >5mm or >2.5cm from anus (imaging)', 'Midline hair tuft, hemangioma, lipoma (occult dysraphism)', 'Palpable defect'] },
|
||||
{ name: 'Upper extremities', technique: 'Observe spontaneous movement, count digits, inspect palmar creases.',
|
||||
normal: 'Symmetric antigravity movement; 5 digits each hand; normal palmar creases',
|
||||
abnormalHints: ['Erb/Klumpke palsy (asymmetric movement)', 'Polydactyly, syndactyly', 'Single transverse palmar crease (trisomy 21 — soft sign)'] },
|
||||
{ name: 'Lower extremities / feet', technique: 'Range hips/knees/ankles. Stroke lateral border of foot. Attempt passive correction.',
|
||||
normal: 'Full range; foot correctable to neutral with ease; no hindfoot deformity',
|
||||
abnormalHints: ['Rigid clubfoot (talipes equinovarus — non-correctable)', 'Metatarsus adductus (usually flexible)', 'Rocker-bottom foot (trisomy 18)'] }
|
||||
]
|
||||
},
|
||||
neuro: {
|
||||
overview: 'Focus: primitive reflexes (present and symmetric), tone (passive and active), and absence of focal deficit. A full newborn neuro exam takes 3–5 min on a quiet, satiated baby.',
|
||||
components: [
|
||||
{ name: 'Level of alertness', technique: 'Observe spontaneous alertness and response to voice, touch, light.',
|
||||
normal: 'Alert periods with spontaneous eye opening; responds to voice and light; consolable',
|
||||
abnormalHints: ['Persistent lethargy / hard to arouse', 'Jitteriness', 'Irritability not consoled by feeding'] },
|
||||
{ name: 'Cranial nerves', technique: 'Pupils to light; symmetric facial movement at rest and with crying; intact suck; visual blink to light.',
|
||||
normal: 'Pupils equal and reactive; face symmetric; strong coordinated suck; blinks to bright light',
|
||||
abnormalHints: ['Asymmetric facial movement (CN VII injury)', 'Poor or uncoordinated suck', 'Fixed pupil', 'Absent blink'] },
|
||||
{ name: 'Tone (passive)', technique: 'Pull-to-sit (head lag ≤ minimal at term), ventral suspension (head lifts to horizontal), arm recoil, popliteal angle.',
|
||||
normal: 'Age-appropriate flexor tone; brief head lag on pull-to-sit; head level with spine on ventral suspension',
|
||||
abnormalHints: ['Significant head lag (hypotonia)', 'Slip-through on vertical suspension', 'Hypertonia / scissoring'] },
|
||||
{ name: 'Spontaneous movement', technique: 'Observe limb movement for symmetry, range, antigravity effort.',
|
||||
normal: 'Symmetric, antigravity movements of all four limbs',
|
||||
abnormalHints: ['Focal paucity of movement (stroke, brachial plexus)', 'Jittery movements that stop on passive flexion', 'Clonic or tonic seizure activity'] },
|
||||
{ name: 'Moro reflex', technique: 'Support head, allow ~30° head drop or clap.',
|
||||
normal: 'Symmetric arm extension followed by flexion and often a cry',
|
||||
abnormalHints: ['Absent (severe CNS depression)', 'Asymmetric (brachial plexus injury, clavicle fx, hemiparesis)'] },
|
||||
{ name: 'Rooting / sucking', technique: 'Stroke cheek / touch lips. Insert gloved finger.',
|
||||
normal: 'Turns toward stimulus and opens mouth; strong rhythmic suck',
|
||||
abnormalHints: ['Absent rooting or suck (CNS depression, prematurity)', 'Uncoordinated suck-swallow'] },
|
||||
{ name: 'Palmar / plantar grasp', technique: 'Apply pressure to palm; then to ball of foot.',
|
||||
normal: 'Strong, symmetric finger flexion; toe curl to plantar pressure',
|
||||
abnormalHints: ['Absent or asymmetric palmar grasp', 'Absent plantar grasp'] },
|
||||
{ name: 'Babinski', technique: 'Stroke lateral sole from heel to toes.',
|
||||
normal: 'Up-going great toe with fanning of other toes (expected in neonates)',
|
||||
abnormalHints: ['Asymmetric response is always abnormal'] }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
infant: {
|
||||
label: 'Infant (1–12 months)',
|
||||
msk: {
|
||||
overview: 'Focus: continued DDH screen through 6 months, gross-motor progression, and symmetry of tone and movement.',
|
||||
components: [
|
||||
{ name: 'Hips (Barlow/Ortolani through 6mo; abduction thereafter)', technique: 'Barlow/Ortolani in first 6 months. After 6 months use limited abduction (<70°), Galeazzi sign, asymmetric skin folds.',
|
||||
normal: 'Stable hips with symmetric abduction ≥70°; no Galeazzi; symmetric skin folds',
|
||||
abnormalHints: ['Positive Ortolani / Barlow (before 3mo)', 'Limited abduction after 3mo', 'Asymmetric thigh folds', 'Galeazzi (shortened femur on affected side)'] },
|
||||
{ name: 'Gross-motor observation', technique: 'Observe head control in prone and pull-to-sit; rolling; sitting; standing — appropriate to age.',
|
||||
normal: 'Age-appropriate motor milestones (e.g., sits without support by 6mo, pulls to stand by 9mo)',
|
||||
abnormalHints: ['Asymmetric movement', 'Persistent fisting past 3mo', 'Scissoring', 'Delayed or regressed milestones'] },
|
||||
{ name: 'Spine', technique: 'Palpate midline when sitting. Observe kyphosis/lordosis as development progresses.',
|
||||
normal: 'Midline spine; physiologic gentle kyphosis in young infant; no masses',
|
||||
abnormalHints: ['Fixed kyphoscoliosis', 'Sacral dimple with cutaneous marker', 'Palpable step-off'] },
|
||||
{ name: 'Extremities', technique: 'Passive range of all major joints. Inspect feet.',
|
||||
normal: 'Full range of all joints; feet correctable and aligning; no rigid deformity',
|
||||
abnormalHints: ['Rigid clubfoot', 'Persistent asymmetric tone', 'Joint swelling / warmth (septic arthritis)'] },
|
||||
{ name: 'Clavicles', technique: 'Palpate bilaterally (if not done at newborn).',
|
||||
normal: 'Intact, smooth, symmetric',
|
||||
abnormalHints: ['Healed fracture callus palpable (usually benign finding)'] }
|
||||
]
|
||||
},
|
||||
neuro: {
|
||||
overview: 'Focus: primitive reflexes should be fading on a predictable schedule; protective reflexes should be emerging; milestones and tone tracked together.',
|
||||
components: [
|
||||
{ name: 'Level of alertness', technique: 'Engage with face, voice, and age-appropriate object (rattle, toy).',
|
||||
normal: 'Alert, socially engaged, tracks and reaches appropriately for age',
|
||||
abnormalHints: ['Poor social engagement', 'Absent visual tracking past 3mo'] },
|
||||
{ name: 'Cranial nerves', technique: 'Visual tracking (180° by 3mo), pupil response, facial symmetry with smile/cry, intact suck, symmetric tongue.',
|
||||
normal: 'Tracks past midline; pupils equal reactive; symmetric facial movement',
|
||||
abnormalHints: ['Persistent nystagmus', 'Strabismus past 4mo', 'Asymmetric facial movement'] },
|
||||
{ name: 'Tone', technique: 'Pull-to-sit (no head lag by 4mo), ventral suspension, vertical suspension.',
|
||||
normal: 'Age-appropriate tone; no head lag by 4mo; no slip-through on vertical suspension',
|
||||
abnormalHints: ['Persistent head lag past 4mo (hypotonia)', 'Hypertonia / scissoring (UMN / CP)', 'Mixed tone (dyskinetic)'] },
|
||||
{ name: 'Primitive reflex status', technique: 'Assess Moro, grasp, tonic neck, Galant — all should be integrated (gone) by 6 months.',
|
||||
normal: 'Primitive reflexes fading on schedule (Moro absent by 6mo, palmar grasp by 5–6mo, tonic neck by 4–6mo)',
|
||||
abnormalHints: ['Persistence of any primitive reflex past 6mo warrants evaluation (CP, CNS injury)'] },
|
||||
{ name: 'Protective / parachute reflex', technique: '6–9mo and beyond: hold prone, tilt head-down suddenly. Observe protective arm extension.',
|
||||
normal: 'Symmetric arm extension protectively by 9mo (emerges 6–9mo)',
|
||||
abnormalHints: ['Absent parachute after 12mo (significant concern)', 'Asymmetric response'] },
|
||||
{ name: 'Gait (if walking)', technique: 'Observe barefoot walking on flat surface if baby is ambulating.',
|
||||
normal: 'Wide-based, arms high guard; improving symmetry; appropriate for age',
|
||||
abnormalHints: ['Persistent toe-walking', 'Asymmetric stride / limp', 'Ataxic or scissoring gait'] }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
toddler: {
|
||||
label: 'Toddler (1–3 years)',
|
||||
msk: {
|
||||
overview: 'Focus: gait, physiologic alignment changes (bow-legs → knock-knees around age 2–3), in-toeing, and joint range.',
|
||||
components: [
|
||||
{ name: 'Gait', technique: 'Observe barefoot walking, running if age-appropriate. Note base, symmetry, heel-strike.',
|
||||
normal: 'Symmetric stride; base narrowing; heel-strike present; arms swinging reciprocally',
|
||||
abnormalHints: ['Persistent toe-walking past 2y (idiopathic vs CP)', 'Limp (Legg-Calvé-Perthes, transient synovitis, SCFE)', 'Wide-based ataxic gait'] },
|
||||
{ name: 'Alignment — knees', technique: 'Stand with feet together. Measure intercondylar (varum) or intermalleolar (valgum) distance if abnormal.',
|
||||
normal: 'Physiologic genu varum resolves by 18–24mo; genu valgum common by 2–4y and peaks at 3–4y',
|
||||
abnormalHints: ['Persistent varum past 2y', 'Severe valgum (>8cm intermalleolar)', 'Unilateral alignment abnormality (Blount disease, rickets)'] },
|
||||
{ name: 'Alignment — feet', technique: 'Observe foot position on gait and stance. Check forefoot adduction, heel alignment.',
|
||||
normal: 'Mild in-toeing and flexible flat feet are physiologic',
|
||||
abnormalHints: ['Rigid flat foot (tarsal coalition)', 'Fixed metatarsus adductus', 'Persistent severe in-toeing (femoral anteversion, tibial torsion)'] },
|
||||
{ name: 'Joint range of motion', technique: 'Passive range of hips, knees, ankles, shoulders, elbows, wrists.',
|
||||
normal: 'Full symmetric range at all joints; no pain, crepitus, or swelling',
|
||||
abnormalHints: ['Joint effusion (septic vs reactive)', 'Guarded motion', 'Asymmetric limitation'] },
|
||||
{ name: 'Spine', technique: 'Inspect standing. Palpate midline. Observe symmetry of shoulders and hips.',
|
||||
normal: 'Midline spine, symmetric shoulders and hips; mild lumbar lordosis is normal',
|
||||
abnormalHints: ['Fixed scoliosis', 'Abnormal kyphosis', 'Palpable step-off', 'Pain with palpation'] }
|
||||
]
|
||||
},
|
||||
neuro: {
|
||||
overview: 'Focus: transition to adult-pattern exam. Primitive reflexes should be absent. Cooperation varies — use play-based techniques.',
|
||||
components: [
|
||||
{ name: 'Mental status', technique: 'Observe engagement, language use, ability to follow simple commands.',
|
||||
normal: 'Alert, interactive, age-appropriate language and commands',
|
||||
abnormalHints: ['Language regression', 'Poor engagement', 'Repetitive behaviors (developmental concern)'] },
|
||||
{ name: 'Cranial nerves', technique: 'Follow finger or toy for EOMs; smile/grimace for facial symmetry; open mouth and tongue protrusion.',
|
||||
normal: 'EOMs intact; symmetric face; tongue midline',
|
||||
abnormalHints: ['Strabismus', 'Facial asymmetry', 'Tongue deviation'] },
|
||||
{ name: 'Motor — tone and bulk', technique: 'Passive range all four extremities; observe bulk.',
|
||||
normal: 'Normal tone and bulk throughout; no contractures',
|
||||
abnormalHints: ['Hypotonia', 'Spasticity (especially catch in lower extremities)', 'Muscle wasting'] },
|
||||
{ name: 'Motor — strength (functional)', technique: 'Observe rising from floor (Gowers maneuver), climbing, squatting.',
|
||||
normal: 'Rises from floor without Gowers; climbs onto furniture without difficulty',
|
||||
abnormalHints: ['Gowers sign (proximal weakness — Duchenne muscular dystrophy)', 'Inability to climb at expected age'] },
|
||||
{ name: 'Deep tendon reflexes', technique: 'Patellar, biceps with distraction (toy). Achilles if tolerated.',
|
||||
normal: '2+ symmetric DTRs throughout',
|
||||
abnormalHints: ['Hyperreflexia / clonus (UMN)', 'Absent reflexes (LMN, neuropathy)'] },
|
||||
{ name: 'Coordination / gait', technique: 'Observe walking, running, climbing stairs, kicking ball if age-appropriate.',
|
||||
normal: 'Age-appropriate coordination; able to run, climb stairs with rail',
|
||||
abnormalHints: ['Ataxic gait', 'Tremor with action', 'Clumsiness beyond age expectation'] },
|
||||
{ name: 'Plantar response', technique: 'Stroke lateral sole from heel toward toes.',
|
||||
normal: 'Down-going (plantar flexion) by age 2',
|
||||
abnormalHints: ['Up-going after age 2 is an upper-motor-neuron sign (Babinski positive)'] }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
preschool: {
|
||||
label: 'Preschool (3–5 years)',
|
||||
msk: {
|
||||
overview: 'Focus: functional gait maneuvers, scoliosis screen, and resolving physiologic alignment.',
|
||||
components: [
|
||||
{ name: 'Gait (normal, heel, toe, tandem)', technique: 'Have child walk normally, on heels, on toes, heel-to-toe. Hop on one foot (4y+).',
|
||||
normal: 'Symmetric smooth gait; able to heel-walk, toe-walk, and tandem; hops on one foot by 4–5y',
|
||||
abnormalHints: ['Persistent toe-walking', 'Asymmetric stance', 'Difficulty with tandem'] },
|
||||
{ name: 'Alignment', technique: 'Stand barefoot. Check knees, feet alignment.',
|
||||
normal: 'Mild residual genu valgum resolving; neutral by 6–7y; medial arch developing',
|
||||
abnormalHints: ['Persistent unilateral varum', 'Rigid flat foot'] },
|
||||
{ name: 'Scoliosis screen (Adam forward-bend)', technique: 'Child bends forward at waist, arms hanging. Observe for rib or lumbar hump.',
|
||||
normal: 'Symmetric paraspinal contour; no rib hump or lumbar prominence',
|
||||
abnormalHints: ['Rib hump (thoracic scoliosis)', 'Asymmetric shoulder / scapular height', 'Leg-length discrepancy causing apparent curve'] },
|
||||
{ name: 'Joint range and stability', technique: 'Active and passive range all joints. Check knee stability, elbow carrying angle.',
|
||||
normal: 'Full symmetric range; stable joints; normal carrying angle',
|
||||
abnormalHints: ['Hypermobility (Beighton score)', 'Joint effusion', 'Pain with motion'] },
|
||||
{ name: 'Feet', technique: 'Stand and on tiptoes. Observe arch formation.',
|
||||
normal: 'Flexible flat feet common; arch forms on tiptoe (flexible); symmetric',
|
||||
abnormalHints: ['Rigid flat foot (does not form arch on tiptoe)', 'Pain'] }
|
||||
]
|
||||
},
|
||||
neuro: {
|
||||
overview: 'Focus: more formal exam now possible — most 4- and 5-year-olds cooperate with cranial nerves, strength, coordination, and sensory testing.',
|
||||
components: [
|
||||
{ name: 'Mental status / language', technique: 'Observe speech intelligibility, complexity, and ability to follow 2- and 3-step commands.',
|
||||
normal: 'Speech clear to strangers by 4y; complex sentences; follows multi-step commands',
|
||||
abnormalHints: ['Dysarthria', 'Receptive or expressive language delay', 'Poor engagement'] },
|
||||
{ name: 'Cranial nerves (II–XII)', technique: 'Vision by charts if age-appropriate; EOMs with toy; facial sensation with light touch; symmetric smile; tongue protrusion; shoulder shrug; uvula elevation.',
|
||||
normal: 'Intact throughout',
|
||||
abnormalHints: ['Strabismus', 'Facial asymmetry', 'Uvula deviation', 'Tongue fasciculations'] },
|
||||
{ name: 'Motor — tone, bulk, strength', technique: 'Passive range. Test 5/5 strength in major groups (stand from squat, grip squeeze, arm lift against resistance).',
|
||||
normal: 'Normal tone and bulk; 5/5 strength symmetrically',
|
||||
abnormalHints: ['Spasticity', 'Weakness (Gowers if proximal)', 'Atrophy'] },
|
||||
{ name: 'Deep tendon reflexes', technique: 'Biceps, patellar, Achilles. Compare side to side.',
|
||||
normal: '2+ symmetric DTRs throughout; down-going plantar response bilaterally',
|
||||
abnormalHints: ['Hyperreflexia with clonus', 'Asymmetry', 'Up-going plantar (Babinski)'] },
|
||||
{ name: 'Coordination', technique: 'Finger-to-nose, heel-to-shin, rapid alternating hand movements, tandem walk.',
|
||||
normal: 'Smooth finger-to-nose, intact tandem; rapid alternating movements coordinated',
|
||||
abnormalHints: ['Dysmetria', 'Dysdiadochokinesia', 'Ataxic tandem'] },
|
||||
{ name: 'Sensory (light touch)', technique: 'Light touch distally on all four extremities with eyes closed (if cooperative).',
|
||||
normal: 'Symmetric light touch throughout',
|
||||
abnormalHints: ['Focal sensory loss', 'Stocking-glove pattern'] },
|
||||
{ name: 'Gait (multiple patterns)', technique: 'Normal, heel, toe, tandem, hop on one foot, Romberg (5y+).',
|
||||
normal: 'All maneuvers performed without difficulty',
|
||||
abnormalHints: ['Romberg positive (dorsal column)', 'Ataxic tandem (cerebellar)'] }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
school: {
|
||||
label: 'School-age (6–11 years)',
|
||||
msk: {
|
||||
overview: 'Focus: scoliosis screening (especially peri-puberty), sports-related overuse, and resolution of alignment.',
|
||||
components: [
|
||||
{ name: 'Scoliosis screen (forward-bend)', technique: 'Adam forward-bend test. Consider scoliometer if available (angle of trunk rotation).',
|
||||
normal: 'No rib or lumbar prominence; ATR < 5°; symmetric shoulders and hips',
|
||||
abnormalHints: ['Rib hump', 'ATR ≥ 7° warrants referral', 'Shoulder or scapular asymmetry'] },
|
||||
{ name: 'Alignment', technique: 'Standing exam of knees and feet.',
|
||||
normal: 'Neutral knee alignment; medial arch present; symmetric',
|
||||
abnormalHints: ['Residual valgum', 'Pes cavus (possible CMT)', 'Asymmetric alignment'] },
|
||||
{ name: 'Joint stability and range', technique: 'Active range all joints. Knee Lachman/anterior drawer if sports-active. Shoulder impingement tests.',
|
||||
normal: 'Full stable range at all joints',
|
||||
abnormalHints: ['Joint laxity or hypermobility', 'Sports-related instability', 'Pain with specific maneuvers'] },
|
||||
{ name: 'Gait and functional movement', technique: 'Observe gait, running, squatting, single-leg stance.',
|
||||
normal: 'Symmetric efficient gait; able to squat and single-leg stand',
|
||||
abnormalHints: ['Trendelenburg sign (hip abductor weakness)', 'Antalgic gait', 'Asymmetric squat depth'] },
|
||||
{ name: 'Sports-specific exam (if relevant)', technique: 'Targeted to activity — shoulder, knee, ankle per chief complaint.',
|
||||
normal: 'No pain, instability, or impingement in tested maneuvers',
|
||||
abnormalHints: ['Overuse injury signs (Osgood-Schlatter, Sever, osteochondritis dissecans)', 'Apophysitis'] }
|
||||
]
|
||||
},
|
||||
neuro: {
|
||||
overview: 'Focus: adult-pattern exam across all six components (mental status, cranial nerves, motor, reflexes, sensory, coordination/gait).',
|
||||
components: [
|
||||
{ name: 'Mental status', technique: 'Orientation, attention (count backward), memory (3-item recall), appropriate language.',
|
||||
normal: 'Alert, oriented, attention intact, age-appropriate language',
|
||||
abnormalHints: ['Inattention / ADHD signs', 'Language concerns', 'Memory deficit'] },
|
||||
{ name: 'Cranial nerves (II–XII)', technique: 'Full formal exam including visual fields, EOMs, facial sensation, hearing, palate elevation, tongue.',
|
||||
normal: 'All cranial nerves intact',
|
||||
abnormalHints: ['Any focal cranial nerve deficit warrants workup'] },
|
||||
{ name: 'Motor', technique: 'Inspect bulk; test tone; strength 5/5 in major groups (deltoids, biceps, triceps, grip, hip flexors, knee extensors, dorsiflexion).',
|
||||
normal: 'Normal bulk and tone; 5/5 strength symmetric',
|
||||
abnormalHints: ['Focal weakness', 'Spasticity', 'Atrophy'] },
|
||||
{ name: 'Deep tendon reflexes', technique: 'Biceps, triceps, brachioradialis, patellar, Achilles. Plantar response.',
|
||||
normal: '2+ symmetric DTRs; down-going plantar',
|
||||
abnormalHints: ['Hyperreflexia with clonus (UMN)', 'Hyporeflexia (LMN, neuropathy)', 'Asymmetry', 'Up-going Babinski'] },
|
||||
{ name: 'Sensory', technique: 'Light touch and pain distally; vibration at distal IP joints; proprioception at great toe.',
|
||||
normal: 'Intact light touch, pain, vibration, proprioception throughout',
|
||||
abnormalHints: ['Stocking-glove loss', 'Dermatomal pattern', 'Loss of vibration or proprioception (dorsal column)'] },
|
||||
{ name: 'Coordination', technique: 'Finger-nose-finger, heel-shin, rapid alternating movements.',
|
||||
normal: 'Smooth coordinated movements bilaterally',
|
||||
abnormalHints: ['Dysmetria', 'Intention tremor', 'Dysdiadochokinesia'] },
|
||||
{ name: 'Gait and Romberg', technique: 'Normal, heel, toe, tandem. Romberg with eyes closed.',
|
||||
normal: 'All gait patterns smooth and symmetric; Romberg negative',
|
||||
abnormalHints: ['Wide-based gait (cerebellar)', 'Steppage (peripheral neuropathy)', 'Romberg positive (dorsal column)'] }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
adolescent: {
|
||||
label: 'Adolescent (12–21 years)',
|
||||
msk: {
|
||||
overview: 'Focus: sports-related injuries, late-onset scoliosis, apophyseal overuse injuries, and screening for hypermobility spectrum.',
|
||||
components: [
|
||||
{ name: 'Scoliosis screen', technique: 'Adam forward-bend with scoliometer if available. Consider recurrent screening through adolescent growth spurt.',
|
||||
normal: 'No rib or lumbar prominence; ATR < 5°',
|
||||
abnormalHints: ['Progressive scoliosis (especially girls 10–14)', 'ATR ≥ 7°', 'Shoulder asymmetry'] },
|
||||
{ name: 'Back / spine', technique: 'Palpate midline and paraspinal muscles. Single-leg hyperextension test if back pain present (spondylolysis).',
|
||||
normal: 'Non-tender spine and paraspinals; no pain with hyperextension',
|
||||
abnormalHints: ['Midline tenderness (spondylolysis/spondylolisthesis)', 'Positive stork test', 'Radicular symptoms'] },
|
||||
{ name: 'Joint stability and sports-specific exam', technique: 'Shoulder, knee, ankle exams per activity and complaint. Beighton score if hypermobile presentation.',
|
||||
normal: 'Stable joints; no effusion; Beighton < 4',
|
||||
abnormalHints: ['ACL/MCL laxity', 'Shoulder apprehension', 'Ankle instability', 'Beighton ≥ 5 (hypermobility spectrum)'] },
|
||||
{ name: 'Alignment and gait', technique: 'Observe standing posture and walking.',
|
||||
normal: 'Neutral alignment; symmetric efficient gait',
|
||||
abnormalHints: ['Antalgic gait', 'Leg-length discrepancy', 'Trendelenburg'] },
|
||||
{ name: 'Overuse / apophysitis screen', technique: 'Palpate tibial tubercle, calcaneal apophysis, iliac apophyses if sports-active.',
|
||||
normal: 'Non-tender apophyses',
|
||||
abnormalHints: ['Osgood-Schlatter (tibial tubercle)', 'Sever (calcaneal)', 'Iliac apophysitis in sprinters'] }
|
||||
]
|
||||
},
|
||||
neuro: {
|
||||
overview: 'Focus: adult-pattern full exam. Screen specifically for concussion sequelae if sports-active; frontal release signs should be absent.',
|
||||
components: [
|
||||
{ name: 'Mental status', technique: 'Orientation, attention, memory, language, executive function (MMSE-style questions tailored to age).',
|
||||
normal: 'Alert, oriented, attention/memory intact, age-appropriate executive function',
|
||||
abnormalHints: ['Post-concussion cognitive symptoms', 'Mood changes', 'Subtle executive dysfunction'] },
|
||||
{ name: 'Cranial nerves (II–XII)', technique: 'Full formal adult-pattern exam.',
|
||||
normal: 'All cranial nerves intact',
|
||||
abnormalHints: ['Any focal deficit'] },
|
||||
{ name: 'Motor', technique: 'Bulk, tone, 5/5 strength in all major groups. Fine motor (finger tapping).',
|
||||
normal: 'Normal bulk, tone, strength throughout',
|
||||
abnormalHints: ['Focal weakness', 'Spasticity', 'Subtle tremor'] },
|
||||
{ name: 'Deep tendon reflexes', technique: 'Biceps, triceps, brachioradialis, patellar, Achilles. Plantar response.',
|
||||
normal: '2+ symmetric; down-going plantar',
|
||||
abnormalHints: ['Hyperreflexia, clonus, or up-going Babinski (UMN)', 'Hyporeflexia (peripheral nerve, myopathy, hypothyroid)'] },
|
||||
{ name: 'Sensory', technique: 'Light touch, pain, vibration, proprioception, two-point discrimination.',
|
||||
normal: 'Intact modalities throughout',
|
||||
abnormalHints: ['Dermatomal loss', 'Length-dependent sensory loss', 'Dorsal column signs'] },
|
||||
{ name: 'Coordination', technique: 'Finger-nose-finger, heel-shin, rapid alternating, tandem gait.',
|
||||
normal: 'Smooth coordinated movements; tandem intact',
|
||||
abnormalHints: ['Cerebellar signs', 'Intention tremor'] },
|
||||
{ name: 'Gait and Romberg', technique: 'Normal, heel, toe, tandem, Romberg.',
|
||||
normal: 'All smooth; Romberg negative',
|
||||
abnormalHints: ['Ataxic gait', 'Steppage', 'Romberg positive'] },
|
||||
{ name: 'Frontal release / primitive reflexes', technique: 'Test grasp, snout, glabellar, palmomental. Should be absent.',
|
||||
normal: 'Absent — no frontal release signs',
|
||||
abnormalHints: ['Presence suggests frontal lobe pathology, neurodegenerative disease, or severe TBI — consider in post-concussion setting'] }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// RENDER + STATE
|
||||
// ────────────────────────────────────────────────────────────
|
||||
var _inited = false;
|
||||
var state = {}; // key = 'msk-0' etc → { name, status, note, normal_finding }
|
||||
var currentSystem = 'msk';
|
||||
var currentAgeGroup = '';
|
||||
|
||||
document.addEventListener('tabChanged', function (e) {
|
||||
if (e.detail.tab !== 'peguide' || _inited) return;
|
||||
_inited = true;
|
||||
init();
|
||||
});
|
||||
|
||||
function init() {
|
||||
var ageSelect = document.getElementById('pe-age-group');
|
||||
var content = document.getElementById('pe-content');
|
||||
var actions = document.getElementById('pe-actions');
|
||||
|
||||
ageSelect.addEventListener('change', function () {
|
||||
currentAgeGroup = ageSelect.value;
|
||||
state = {};
|
||||
document.getElementById('pe-output').classList.add('hidden');
|
||||
if (!currentAgeGroup || !PE_DATA[currentAgeGroup]) {
|
||||
content.innerHTML = '<p style="text-align:center;color:#9ca3af;padding:40px;">Select an age group above.</p>';
|
||||
actions.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
renderSystem();
|
||||
actions.style.display = 'flex';
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-pesystem]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
document.querySelectorAll('[data-pesystem]').forEach(function (b) { b.classList.remove('active'); });
|
||||
btn.classList.add('active');
|
||||
currentSystem = btn.dataset.pesystem;
|
||||
state = {};
|
||||
document.getElementById('pe-output').classList.add('hidden');
|
||||
if (currentAgeGroup) renderSystem();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('pe-all-normal').addEventListener('click', setAllNormal);
|
||||
document.getElementById('pe-all-clear').addEventListener('click', clearAll);
|
||||
document.getElementById('pe-generate-btn').addEventListener('click', generate);
|
||||
}
|
||||
|
||||
function renderSystem() {
|
||||
var content = document.getElementById('pe-content');
|
||||
var group = PE_DATA[currentAgeGroup];
|
||||
if (!group || !group[currentSystem]) {
|
||||
content.innerHTML = '<p style="text-align:center;color:#9ca3af;padding:40px;">No data for this combination.</p>';
|
||||
return;
|
||||
}
|
||||
var section = group[currentSystem];
|
||||
var html = '';
|
||||
html += '<div class="card" style="margin-bottom:12px;">';
|
||||
html += ' <div class="card-header"><h3><i class="fas fa-circle-info"></i> OSCE Overview — ' + esc(group.label) + ' (' + (currentSystem === 'msk' ? 'MSK' : 'Neuro') + ')</h3></div>';
|
||||
html += ' <div style="padding:12px 16px;font-size:13px;line-height:1.6;color:var(--g700);">' + esc(section.overview) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
section.components.forEach(function (c, i) {
|
||||
var key = currentSystem + '-' + i;
|
||||
if (!state[key]) state[key] = { name: c.name, status: null, note: '', normal_finding: c.normal, technique: c.technique };
|
||||
html += '<div class="card" style="margin-bottom:10px;" data-pe-key="' + key + '">';
|
||||
html += ' <div class="card-header" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">';
|
||||
html += ' <h3 style="margin:0;flex:1;min-width:0;">' + esc(c.name) + '</h3>';
|
||||
html += ' <div style="display:flex;gap:4px;">';
|
||||
html += ' <button class="visit-status-btn ' + (state[key].status === 'normal' ? 'done' : '') + '" data-pe-status="normal" data-pe-key="' + key + '">Normal</button>';
|
||||
html += ' <button class="visit-status-btn ' + (state[key].status === 'abnormal' ? 'refused' : '') + '" data-pe-status="abnormal" data-pe-key="' + key + '">Abnormal</button>';
|
||||
html += ' <button class="visit-status-btn ' + (state[key].status === null ? 'not-due' : '') + '" data-pe-status="skip" data-pe-key="' + key + '">Skip</button>';
|
||||
html += ' </div>';
|
||||
html += ' </div>';
|
||||
html += ' <div style="padding:10px 16px;font-size:12px;color:var(--g700);line-height:1.6;">';
|
||||
html += ' <div style="margin-bottom:6px;"><strong>How to perform:</strong> ' + esc(c.technique) + '</div>';
|
||||
html += ' <div style="margin-bottom:6px;"><strong>Expected normal:</strong> ' + esc(c.normal) + '</div>';
|
||||
if (c.abnormalHints && c.abnormalHints.length) {
|
||||
html += ' <div style="color:var(--g600);"><strong>Watch for:</strong> ' + c.abnormalHints.map(esc).join(' · ') + '</div>';
|
||||
}
|
||||
html += ' <div class="pe-abnormal-detail" style="margin-top:8px;' + (state[key].status === 'abnormal' ? '' : 'display:none;') + '">';
|
||||
html += ' <input type="text" class="pe-note" data-pe-key="' + key + '" placeholder="Describe the abnormal finding (e.g., positive Ortolani left hip)" value="' + esc(state[key].note || '') + '" style="width:100%;padding:6px 8px;font-size:13px;border:1px solid var(--g300);border-radius:6px;">';
|
||||
html += ' </div>';
|
||||
html += ' </div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
content.innerHTML = html;
|
||||
|
||||
// Wire events
|
||||
content.querySelectorAll('[data-pe-status]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () { handleStatus(btn); });
|
||||
});
|
||||
content.querySelectorAll('.pe-note').forEach(function (inp) {
|
||||
inp.addEventListener('input', function () {
|
||||
var key = inp.dataset.peKey;
|
||||
if (state[key]) state[key].note = inp.value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleStatus(btn) {
|
||||
var key = btn.dataset.peKey;
|
||||
var status = btn.dataset.peStatus;
|
||||
if (!state[key]) return;
|
||||
state[key].status = (status === 'skip') ? null : status;
|
||||
// Redraw this card's buttons + abnormal input visibility
|
||||
var card = btn.closest('[data-pe-key]');
|
||||
if (card) {
|
||||
card.querySelectorAll('[data-pe-status]').forEach(function (b) {
|
||||
b.classList.remove('done', 'refused', 'not-due');
|
||||
if (b.dataset.peStatus === 'normal' && state[key].status === 'normal') b.classList.add('done');
|
||||
if (b.dataset.peStatus === 'abnormal' && state[key].status === 'abnormal') b.classList.add('refused');
|
||||
if (b.dataset.peStatus === 'skip' && state[key].status === null) b.classList.add('not-due');
|
||||
});
|
||||
var detail = card.querySelector('.pe-abnormal-detail');
|
||||
if (detail) detail.style.display = (state[key].status === 'abnormal') ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function setAllNormal() {
|
||||
Object.keys(state).forEach(function (k) { if (k.indexOf(currentSystem + '-') === 0) state[k].status = 'normal'; });
|
||||
renderSystem();
|
||||
}
|
||||
function clearAll() {
|
||||
Object.keys(state).forEach(function (k) { if (k.indexOf(currentSystem + '-') === 0) { state[k].status = null; state[k].note = ''; } });
|
||||
renderSystem();
|
||||
}
|
||||
|
||||
function generate() {
|
||||
if (!currentAgeGroup || !PE_DATA[currentAgeGroup]) return;
|
||||
var components = Object.keys(state)
|
||||
.filter(function (k) { return k.indexOf(currentSystem + '-') === 0; })
|
||||
.map(function (k) { return state[k]; });
|
||||
|
||||
var assessed = components.filter(function (c) { return c.status !== null; });
|
||||
if (assessed.length === 0) { showToast('Mark at least one component Normal or Abnormal', 'error'); return; }
|
||||
|
||||
var btn = document.getElementById('pe-generate-btn');
|
||||
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...';
|
||||
|
||||
var modelSel = document.querySelector('#peguide-tab .tab-model-select');
|
||||
var model = modelSel ? modelSel.value : '';
|
||||
var format = document.getElementById('pe-format').value;
|
||||
|
||||
fetch('/api/generate-pe-narrative', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
components: components,
|
||||
ageGroup: PE_DATA[currentAgeGroup].label,
|
||||
system: currentSystem,
|
||||
patientAge: document.getElementById('pe-age').value,
|
||||
patientGender: document.getElementById('pe-gender').value,
|
||||
model: model,
|
||||
format: format
|
||||
})
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (!data.success) { showToast(data.error || 'Generation failed', 'error'); return; }
|
||||
var out = document.getElementById('pe-output');
|
||||
out.classList.remove('hidden');
|
||||
document.getElementById('pe-narrative-text').textContent = data.narrative;
|
||||
var tag = document.getElementById('pe-model-tag');
|
||||
if (tag) tag.textContent = data.model || '';
|
||||
var bar = document.getElementById('pe-summary-bar');
|
||||
if (bar && data.summary) {
|
||||
bar.classList.remove('hidden');
|
||||
bar.innerHTML = '<span style="color:#10b981;">' + data.summary.normal + ' normal</span> · <span style="color:#ef4444;">' + data.summary.abnormal + ' abnormal</span> · <span style="color:#9ca3af;">' + data.summary.notAssessed + ' skipped</span>';
|
||||
}
|
||||
})
|
||||
.catch(function (err) { console.error('[PEGuide] generate error', err); showToast('Request failed', 'error'); })
|
||||
.finally(function () {
|
||||
btn.disabled = false; btn.innerHTML = '<i class="fas fa-wand-magic-sparkles"></i> Generate Exam Report';
|
||||
});
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
@ -275,6 +275,7 @@ app.use('/api', require('./src/routes/hpi'));
|
|||
app.use('/api', require('./src/routes/hospitalCourse'));
|
||||
app.use('/api', require('./src/routes/chartReview'));
|
||||
app.use('/api', require('./src/routes/milestones'));
|
||||
app.use('/api', require('./src/routes/peGuide'));
|
||||
app.use('/api', require('./src/routes/soap'));
|
||||
app.use('/api', require('./src/routes/tts'));
|
||||
app.use('/api', require('./src/routes/nextcloud'));
|
||||
|
|
|
|||
86
src/routes/peGuide.js
Normal file
86
src/routes/peGuide.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// ============================================================
|
||||
// PHYSICAL EXAM GUIDE — narrative + list generation
|
||||
// ============================================================
|
||||
// Takes an age group, system (msk | neuro), and a set of assessed
|
||||
// components (each flagged Normal or Abnormal, abnormal with optional
|
||||
// note) and returns a professional narrative paragraph or structured
|
||||
// list. Mirrors the milestone-narrative pattern exactly: same guardrails,
|
||||
// same injection wrapping, same audit log category.
|
||||
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var { callAI } = require('../utils/ai');
|
||||
var { PROMPTS, INJECTION_GUARD, wrapUserText } = require('../utils/prompts');
|
||||
var { authMiddleware } = require('../middleware/auth');
|
||||
var logger = require('../utils/logger');
|
||||
|
||||
router.post('/generate-pe-narrative', authMiddleware, async function (req, res) {
|
||||
try {
|
||||
var { components, ageGroup, system, patientAge, patientGender, model, format } = req.body;
|
||||
if (!Array.isArray(components) || components.length === 0) {
|
||||
return res.status(400).json({ error: 'No components provided' });
|
||||
}
|
||||
|
||||
// Partition by assessment status
|
||||
var normal = components.filter(function (c) { return c.status === 'normal'; });
|
||||
var abnormal = components.filter(function (c) { return c.status === 'abnormal'; });
|
||||
|
||||
if (normal.length === 0 && abnormal.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
narrative: 'No physical-exam components were assessed.',
|
||||
summary: { normal: 0, abnormal: 0, notAssessed: components.length }
|
||||
});
|
||||
}
|
||||
|
||||
// Structured text for the model — includes both technique (how the exam
|
||||
// component was performed) and the finding. The prompt instructs the AI
|
||||
// to produce a "Technique" preamble + "Findings" narrative.
|
||||
var text = '';
|
||||
if (normal.length > 0) {
|
||||
text += 'NORMAL findings:\n';
|
||||
normal.forEach(function (c) {
|
||||
text += ' - ' + c.name + '\n';
|
||||
text += ' technique: ' + (c.technique || 'standard method') + '\n';
|
||||
text += ' normal finding: ' + (c.normal_finding || 'unspecified') + '\n';
|
||||
});
|
||||
}
|
||||
if (abnormal.length > 0) {
|
||||
text += '\nABNORMAL findings:\n';
|
||||
abnormal.forEach(function (c) {
|
||||
text += ' - ' + c.name + '\n';
|
||||
text += ' technique: ' + (c.technique || 'standard method') + '\n';
|
||||
text += ' physician note: ' + (c.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_components', text) +
|
||||
'\n\nAssessed: ' + (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: components.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;
|
||||
|
|
@ -213,6 +213,52 @@ Sentence 2: Key strengths — domains on track
|
|||
Sentence 3: Any concerns or delays. If none, state no concerns identified.
|
||||
EXACTLY 3 sentences. Professional language. Only mention assessed milestones.`,
|
||||
|
||||
// ======================== PHYSICAL EXAM GUIDE ========================
|
||||
peGuideNarrative: `You are a pediatric documentation specialist.
|
||||
Generate a professional OSCE-style physical-exam report with TWO sections.
|
||||
${CORE_RULES}
|
||||
|
||||
Structure the output as:
|
||||
|
||||
Technique:
|
||||
A short paragraph describing HOW the examination was performed. Mention the
|
||||
maneuvers, tests, or methods used for each assessed component, using the
|
||||
"technique" strings provided. Weave them into prose — do NOT list them
|
||||
bullet-by-bullet. Omit techniques for components that were not assessed.
|
||||
|
||||
Findings:
|
||||
A narrative paragraph or two describing what was observed.
|
||||
- ONLY components that were assessed (Normal or Abnormal)
|
||||
- Normal findings: use concise language describing the specific normal result
|
||||
(e.g. "Hips stable to Barlow and Ortolani" not "Hips normal")
|
||||
- Abnormal findings: lead with the physician's note verbatim, then add a
|
||||
brief clinical qualifier only if the note is clearly incomplete
|
||||
- Group related findings by region when natural (upper extremities together,
|
||||
spine separately, etc.)
|
||||
- End with a single-phrase overall impression
|
||||
- Do NOT fabricate measurements, grades, or adverbs not provided
|
||||
- Do NOT mention components that were not assessed`,
|
||||
|
||||
peGuideList: `You are a pediatric documentation specialist.
|
||||
Generate an OSCE-style physical-exam report as a STRUCTURED numbered LIST.
|
||||
${CORE_RULES}
|
||||
|
||||
Produce TWO sections, each as a numbered list:
|
||||
|
||||
Technique performed:
|
||||
1. [method for component 1]
|
||||
2. [method for component 2]
|
||||
... using the "technique" strings provided — one per assessed component.
|
||||
|
||||
Findings:
|
||||
1. [finding for component 1]
|
||||
2. [finding for component 2]
|
||||
... one line per assessed component.
|
||||
- Normal findings: describe the specific normal result, not just "normal"
|
||||
- Abnormal findings: start with the physician's note, then qualifier if provided
|
||||
- One-line overall impression at the end
|
||||
- No markdown, no symbols, plain numbered list`,
|
||||
|
||||
// ======================== REFINE ========================
|
||||
refine: `You are a medical documentation editor.
|
||||
The user wants to modify a previously generated medical document.
|
||||
|
|
|
|||
Loading…
Reference in a new issue