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:
Daniel 2026-04-22 16:52:07 +02:00
parent b411dbbbf0
commit fd7f684fe9
6 changed files with 725 additions and 0 deletions

View 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 (028 days)</option>
<option value="infant">Infant (112 months)</option>
<option value="toddler">Toddler (13 years)</option>
<option value="preschool">Preschool (35 years)</option>
<option value="school">School-age (611 years)</option>
<option value="adolescent">Adolescent (1221 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>

View file

@ -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
View 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 (028 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 35 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 (112 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 56mo, tonic neck by 46mo)',
abnormalHints: ['Persistence of any primitive reflex past 6mo warrants evaluation (CP, CNS injury)'] },
{ name: 'Protective / parachute reflex', technique: '69mo and beyond: hold prone, tilt head-down suddenly. Observe protective arm extension.',
normal: 'Symmetric arm extension protectively by 9mo (emerges 69mo)',
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 (13 years)',
msk: {
overview: 'Focus: gait, physiologic alignment changes (bow-legs → knock-knees around age 23), 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 1824mo; genu valgum common by 24y and peaks at 34y',
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 (35 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 45y',
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 67y; 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 (IIXII)', 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 (611 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 (IIXII)', 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 (1221 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 1014)', '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 (IIXII)', 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
})();

View file

@ -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
View 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;

View file

@ -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.