Two things in one commit because they're coupled by a gitignore fix:
(1) PE Guide minimum-viable port, (2) a pre-existing bug where
client/src/data/faq.ts was silently gitignored and never committed.
.gitignore — narrow scope
Changed `data/` to `/data/`. The old rule matched every nested
`data/` directory in the tree, including client/src/data/, which
meant faq.ts from the FAQ port (commit c0038da) never landed in
git — the Faq page built locally only because the file existed
on the dev machine. A fresh clone or CI build would fail the
Vite build at '@/data/faq'. The narrowed rule still ignores the
runtime DB directory at repo root while allowing project data
modules to be tracked. This commit also commits the missing
faq.ts so the FAQ page is actually buildable from git again.
client/src/data/pe-guide.ts
Verbatim port of lines 23-311 of public/js/peGuide.js — the stable
reference content:
• SCALES (12 scales: MRC, DTR, plantar, Beighton, ATR, RR, SpO2,
Silverman, Westley, Levine murmur, pulse amp, cap refill)
• SYSTEM_SCALES (per-body-system scale mapping)
• APTM_LEGEND (5 cardiac auscultation points)
• INNOCENT_MURMURS (5 benign childhood murmurs)
• RESP_SOUNDS (7 entries with /audio/respiratory/*.ogg paths)
• CARDIAC_SOUNDS (6 entries with /audio/cardiac/* paths)
Counts preserved exactly. Audio files stay under public/audio/ and
are served unchanged.
What is NOT in this commit — on purpose
PE_DATA (the ~1000-line age-group × system × component × step
hierarchy) stays in the vanilla app. The migration checkpoint memory
explicitly warns about the class of bug where an LLM silently drops
entries from long clinical arrays. PE_DATA porting needs its own
session with per-entry counts + visual diff against the vanilla
source. An amber banner at the top of the React page links to
/#peGuide (the legacy checklist viewer) so users still reach the
full exam-step checklist + Generate-Exam-Report flow.
Also skipped: the big inline APTM_SVG chest diagram. The letter
legend (A/P/E/T/M) carries the clinical content; the pictorial
SVG can land later without content risk.
client/src/pages/PeGuide.tsx — viewer
Grid-of-cards layout: one card per scale, per APTM point, per
innocent-murmur, and per sound entry. Sound cards use native
<audio controls> so the browser does the usual play/pause/seek —
no custom player. data-testid hooks throughout for the spec.
e2e/tests/peguide-react.spec.js — four smoke tests:
all 12 scales render (this is the count that would fail loudly if
someone trimmed SCALES later), APTM has all 5 letters, sound
libraries have the exact respiratory + cardiac keys, and the
legacy-viewer link is present.
Client tsc -b + vite build clean. Bundle 452.91 kB / 129.35 kB gz.
290 lines
15 KiB
TypeScript
290 lines
15 KiB
TypeScript
// ============================================================
|
||
// PE-GUIDE DATA — ported verbatim from public/js/peGuide.js
|
||
// (lines 23-311 of the vanilla file, as of commit before this one).
|
||
//
|
||
// This file ONLY contains the stable reference data:
|
||
// • SCALES — grading scales (MRC, DTR, Levine, Beighton, …)
|
||
// • SYSTEM_SCALES — which scales belong to which body system
|
||
// • APTM_LEGEND — the 5 cardiac auscultation points
|
||
// • INNOCENT_MURMURS — benign childhood murmurs
|
||
// • RESP_SOUNDS — respiratory sounds library (audio paths)
|
||
// • CARDIAC_SOUNDS — cardiac sounds library (audio paths)
|
||
//
|
||
// PE_DATA (the full age-group × system × component × step hierarchy,
|
||
// ~1000 lines) is intentionally NOT ported here. It holds clinically
|
||
// reviewed content and the migration checkpoint explicitly warns
|
||
// "An LLM will sometimes 'simplify' a long array — don't let that
|
||
// happen." PE_DATA port belongs in its own dedicated session with
|
||
// per-entry counts + visual diff verification against the vanilla
|
||
// source. Until that session, the React PE Guide surfaces the
|
||
// reference libraries below and links to the legacy viewer for
|
||
// exam-step checklists and narrative generation.
|
||
//
|
||
// Audio files stay in public/audio/respiratory/ and public/audio/cardiac/
|
||
// and are served unchanged from Express.
|
||
// ============================================================
|
||
|
||
export interface ScaleDef {
|
||
title: string;
|
||
icon: string;
|
||
rows: Array<[string, string]>;
|
||
}
|
||
|
||
export const SCALES: Record<string, ScaleDef> = {
|
||
mrc: {
|
||
title: 'MRC strength grade (0–5)',
|
||
icon: 'fa-hand-fist',
|
||
rows: [
|
||
['5', 'Normal power — holds against full resistance'],
|
||
['4', 'Reduced — moves against gravity + some resistance'],
|
||
['3', 'Moves against gravity only (no added resistance)'],
|
||
['2', 'Full range with gravity eliminated (horizontal plane)'],
|
||
['1', 'Flicker / trace contraction, no joint movement'],
|
||
['0', 'No contraction'],
|
||
],
|
||
},
|
||
dtr: {
|
||
title: 'Deep-tendon reflex grade (0–4+)',
|
||
icon: 'fa-circle-dot',
|
||
rows: [
|
||
['0', 'Absent'],
|
||
['1+', 'Hypoactive — trace, only with reinforcement'],
|
||
['2+', 'Normal'],
|
||
['3+', 'Brisk — may still be normal in anxious patients'],
|
||
['4+', 'Hyperactive with sustained clonus — always abnormal'],
|
||
],
|
||
},
|
||
plantar: {
|
||
title: 'Plantar response (Babinski)',
|
||
icon: 'fa-shoe-prints',
|
||
rows: [
|
||
['Down-going', 'Normal in anyone ≥ 2 years'],
|
||
['Up-going', 'Normal < 2 years; abnormal after — UMN lesion'],
|
||
['Asymmetric', 'Always abnormal at any age'],
|
||
],
|
||
},
|
||
beighton: {
|
||
title: 'Beighton hypermobility score (0–9)',
|
||
icon: 'fa-hands',
|
||
rows: [
|
||
['≤ 3', 'Normal flexibility'],
|
||
['4', 'Borderline — consider in context'],
|
||
['≥ 5', 'Hypermobility spectrum; screen for hEDS if other features present'],
|
||
],
|
||
},
|
||
atr: {
|
||
title: 'Scoliometer — angle of trunk rotation',
|
||
icon: 'fa-ruler',
|
||
rows: [
|
||
['< 5°', 'Normal, no follow-up'],
|
||
['5–6°', 'Borderline — re-check at each visit'],
|
||
['≥ 7°', 'Refer for PA/lateral spine x-ray + orthopedic evaluation'],
|
||
],
|
||
},
|
||
rr: {
|
||
title: 'Respiratory rate — upper limit by age (awake)',
|
||
icon: 'fa-lungs',
|
||
rows: [
|
||
['Newborn', '≤ 60 /min'],
|
||
['< 2 months', '≤ 60 /min (WHO tachypnea cutoff)'],
|
||
['2–12 months', '≤ 50 /min (WHO tachypnea cutoff)'],
|
||
['1–5 years', '≤ 40 /min (WHO tachypnea cutoff)'],
|
||
['6–11 years', '≤ 30 /min'],
|
||
['≥ 12 years', '≤ 20 /min (adult pattern)'],
|
||
],
|
||
},
|
||
spo2: {
|
||
title: 'Pulse oximetry (SpO₂) — at room air',
|
||
icon: 'fa-heart-pulse',
|
||
rows: [
|
||
['≥ 95%', 'Normal'],
|
||
['92–94%', 'Mild hypoxemia — investigate cause'],
|
||
['< 92%', 'Moderate hypoxemia — supplemental O₂'],
|
||
['< 88%', 'Severe — urgent intervention; target ≥ 90% acutely'],
|
||
],
|
||
},
|
||
silverman: {
|
||
title: 'Silverman–Andersen retraction score (neonatal, 0–10)',
|
||
icon: 'fa-baby',
|
||
rows: [
|
||
['0', 'No respiratory distress'],
|
||
['1–3', 'Mild — close observation'],
|
||
['4–6', 'Moderate distress — consider CPAP / support'],
|
||
['7–10', 'Severe — imminent respiratory failure, intubate'],
|
||
],
|
||
},
|
||
westley: {
|
||
title: 'Westley croup severity score',
|
||
icon: 'fa-stethoscope',
|
||
rows: [
|
||
['≤ 2', 'Mild — home management, cool mist, oral dexamethasone'],
|
||
['3–5', 'Moderate — nebulised epinephrine + dexamethasone'],
|
||
['6–11', 'Severe — admit, continuous monitoring'],
|
||
['≥ 12', 'Impending respiratory failure — ICU / airway management'],
|
||
],
|
||
},
|
||
murmurGrade: {
|
||
title: 'Heart-murmur grading (Levine 1–6)',
|
||
icon: 'fa-wave-square',
|
||
rows: [
|
||
['1/6', 'Very faint — heard only with concentration'],
|
||
['2/6', 'Soft but readily heard'],
|
||
['3/6', 'Moderately loud, no thrill'],
|
||
['4/6', 'Loud WITH a palpable thrill'],
|
||
['5/6', 'Very loud; audible with stethoscope just off the chest'],
|
||
['6/6', 'Audible without the stethoscope touching the chest'],
|
||
],
|
||
},
|
||
pulseAmp: {
|
||
title: 'Pulse amplitude grade (0–4)',
|
||
icon: 'fa-heart-pulse',
|
||
rows: [
|
||
['0', 'Absent'],
|
||
['1+', 'Diminished, thready'],
|
||
['2+', 'Normal'],
|
||
['3+', 'Bounding'],
|
||
['4+', 'Bounding with visible pulsation (e.g., aortic regurgitation)'],
|
||
],
|
||
},
|
||
capRefill: {
|
||
title: 'Capillary refill time',
|
||
icon: 'fa-hand',
|
||
rows: [
|
||
['< 2 sec', 'Normal'],
|
||
['2–3 sec', 'Borderline — consider hydration / perfusion'],
|
||
['≥ 3 sec', 'Delayed — dehydration, shock, low cardiac output'],
|
||
],
|
||
},
|
||
};
|
||
|
||
export const SYSTEM_SCALES: Record<string, string[]> = {
|
||
msk: ['atr', 'beighton'],
|
||
neuro: ['mrc', 'dtr', 'plantar'],
|
||
resp: ['rr', 'spo2', 'silverman', 'westley'],
|
||
cv: ['murmurGrade', 'pulseAmp', 'capRefill'],
|
||
};
|
||
|
||
// APTM — the 5 classic cardiac auscultation points
|
||
export interface AptmEntry {
|
||
letter: string;
|
||
color: string;
|
||
title: string;
|
||
location: string;
|
||
listen: string;
|
||
innocent?: string;
|
||
}
|
||
export const APTM_LEGEND: AptmEntry[] = [
|
||
{ letter: 'A', color: '#dc2626', title: 'Aortic area', location: '2nd ICS, right sternal border', listen: 'S2 (aortic component), aortic stenosis, aortic regurgitation' },
|
||
{ letter: 'P', color: '#2563eb', title: 'Pulmonic area', location: '2nd ICS, left sternal border', listen: 'S2 (pulmonic component), pulmonic stenosis, PDA, physiologic split of S2',
|
||
innocent: 'Pulmonary flow murmur (children, adolescents) — upper left sternal border' },
|
||
{ letter: 'E', color: '#059669', title: 'Erb\'s point', location: '3rd ICS, left sternal border', listen: 'Aortic regurgitation (best here), transitional zone murmurs',
|
||
innocent: 'Still\'s murmur classically radiates to Erb\'s / LLSB' },
|
||
{ letter: 'T', color: '#d97706', title: 'Tricuspid area', location: '4th–5th ICS, lower left sternal border', listen: 'Tricuspid regurgitation, VSD, S3/S4, holosystolic murmurs',
|
||
innocent: 'Still\'s murmur — vibratory, musical, age 3–7 y (loudest between LLSB and apex)' },
|
||
{ letter: 'M', color: '#7c3aed', title: 'Mitral area (apex)', location: '5th ICS, mid-clavicular line', listen: 'S1, mitral regurgitation, mitral stenosis (with bell, left-lateral decubitus)' },
|
||
];
|
||
|
||
// Innocent (benign) childhood murmurs
|
||
export interface InnocentMurmur {
|
||
name: string;
|
||
age: string;
|
||
location: string;
|
||
character: string;
|
||
confirm: string;
|
||
}
|
||
export const INNOCENT_MURMURS: InnocentMurmur[] = [
|
||
{ name: 'Still\'s (vibratory) murmur',
|
||
age: '3–7 y (most common in children)',
|
||
location: 'LLSB, radiating to apex',
|
||
character: 'Low-frequency vibratory / musical systolic, grade 2–3/6, mid-systolic, "twanging-string" quality',
|
||
confirm: 'Louder supine, softer or disappears on standing or Valsalva. No radiation to neck/back. Normal S2.' },
|
||
{ name: 'Pulmonary flow murmur',
|
||
age: 'School-age and adolescents, thin chest',
|
||
location: 'Upper left sternal border (2nd–3rd ICS)',
|
||
character: 'Soft blowing early systolic ejection, grade 1–2/6, higher-pitched',
|
||
confirm: 'No ejection click. Physiologic split of S2. Louder supine, softer on standing. No radiation.' },
|
||
{ name: 'Venous hum',
|
||
age: 'Ages 3–8, disappears by adolescence',
|
||
location: 'Supraclavicular or infraclavicular area, usually right',
|
||
character: 'Soft continuous hum, louder in diastole. Only innocent continuous murmur.',
|
||
confirm: 'Disappears when supine OR when jugular vein is gently compressed (key maneuver). Turning head to opposite side also alters it.' },
|
||
{ name: 'Carotid bruit / supraclavicular bruit',
|
||
age: 'Children and adolescents',
|
||
location: 'Supraclavicular fossa, right > left; may radiate to carotid',
|
||
character: 'Brief early systolic, grade 2–3/6, higher-pitched than Still\'s',
|
||
confirm: 'Softer or disappears with hyperextension of the shoulders. Normal cardiac exam otherwise. No radiation below the clavicles.' },
|
||
{ name: 'Peripheral pulmonary stenosis (PPS, neonatal)',
|
||
age: 'Newborns and infants < 6–12 months',
|
||
location: 'Upper LSB, radiates to BOTH axillae and the back',
|
||
character: 'Soft systolic ejection murmur, grade 1–2/6',
|
||
confirm: 'Typical age + radiation to back/axillae. Resolves by age 1 as branch pulmonary arteries grow. Persistence or louder grade warrants echo.' },
|
||
];
|
||
|
||
// Respiratory sounds library — real recordings served from /public/audio/respiratory/
|
||
export interface SoundEntry {
|
||
key: string;
|
||
src: string;
|
||
title: string;
|
||
where: string;
|
||
rate?: string;
|
||
features: string;
|
||
clinical: string;
|
||
}
|
||
export const RESP_SOUNDS: SoundEntry[] = [
|
||
{ key: 'normal', src: '/audio/respiratory/normal-vesicular.ogg', title: 'Normal vesicular breath sounds',
|
||
where: 'Peripheral lung fields',
|
||
features: 'Soft, rustling. Inspiration louder and longer than expiration.',
|
||
clinical: 'Baseline — deviation elsewhere is what you listen for.' },
|
||
{ key: 'wheeze', src: '/audio/respiratory/wheeze.ogg', title: 'Wheeze',
|
||
where: 'Diffuse in asthma; localised in foreign body',
|
||
features: 'Continuous, high-pitched, musical. Usually expiratory; biphasic if severe.',
|
||
clinical: 'Lower-airway narrowing — asthma, bronchiolitis, foreign body, bronchomalacia. Silent chest in severe asthma is an ominous sign.' },
|
||
{ key: 'stridor', src: '/audio/respiratory/stridor.ogg', title: 'Stridor',
|
||
where: 'Louder over neck than chest — upper airway',
|
||
features: 'Continuous, high-pitched, harsh. Classically inspiratory (extrathoracic obstruction); biphasic if fixed.',
|
||
clinical: 'Croup, epiglottitis, foreign body, laryngomalacia (infant). Distinguish from wheeze by auscultating the neck — stridor is loudest there.' },
|
||
{ key: 'finecrackles', src: '/audio/respiratory/crackles-fine.ogg', title: 'Fine (end-inspiratory) crackles',
|
||
where: 'Bibasilar in pulmonary edema/fibrosis; focal in pneumonia',
|
||
features: 'Discontinuous, brief, high-pitched. "Velcro" quality. Late inspiratory, do NOT clear with cough.',
|
||
clinical: 'Alveolar opening — pulmonary fibrosis, pulmonary edema, early pneumonia, atelectasis.' },
|
||
{ key: 'coarsecrackles', src: '/audio/respiratory/crackles-coarse.ogg', title: 'Coarse crackles',
|
||
where: 'Lower lobes; either side',
|
||
features: 'Discontinuous, longer and louder than fine crackles. Lower-pitched. Can be early or late inspiratory; often clear partly with cough.',
|
||
clinical: 'Secretions in larger airways — bronchitis, later pneumonia, bronchiectasis, aspiration.' },
|
||
{ key: 'rhonchi', src: '/audio/respiratory/rhonchi.ogg', title: 'Rhonchi',
|
||
where: 'Central or anywhere with airway secretions',
|
||
features: 'Continuous, low-pitched, snore-like. Typically expiratory. Clear or change with cough.',
|
||
clinical: 'Large-airway secretions — bronchitis, pneumonia with large-airway involvement, cystic fibrosis, bronchiectasis.' },
|
||
{ key: 'pleuralrub', src: '/audio/respiratory/pleural-rub.ogg', title: 'Pleural friction rub',
|
||
where: 'Focal, often lateral or posterior lower chest',
|
||
features: 'Grating, creaky — "leather on leather". Biphasic (heard in inspiration and expiration). Does NOT clear with cough.',
|
||
clinical: 'Pleural inflammation — pleuritis, pulmonary embolism, pneumonia with pleural involvement, viral pleurisy.' },
|
||
];
|
||
|
||
// Cardiac sounds library — real recordings from Wikimedia Commons
|
||
export const CARDIAC_SOUNDS: SoundEntry[] = [
|
||
{ key: 'normal', src: '/audio/cardiac/normal.ogg', title: 'Normal heart sounds (S1, S2)',
|
||
where: 'All four classic auscultation points', rate: '~61 bpm reference',
|
||
features: '"lub-dub": S1 (closure of mitral + tricuspid) louder at apex; S2 (closure of aortic + pulmonic) louder at base. Physiologic S2 split on inspiration.',
|
||
clinical: 'Reference for rhythm, rate, and the normal S1–S2 interval. Listen for what\'s changed — not just what\'s added.' },
|
||
{ key: 'infant-normal', src: '/audio/cardiac/infant-normal.ogg', title: 'Infant normal heart sounds',
|
||
where: 'Infant chest — rate will be higher than adult', rate: 'Pediatric reference (120–160 bpm range)',
|
||
features: 'Same S1–S2 pattern, faster rate. Short diastole makes murmurs easier to miss — careful auscultation needed.',
|
||
clinical: 'Reference for neonatal/infant rhythm. Any murmur in the first 72 h should prompt pre/postductal sat screening.' },
|
||
{ key: 'vsd', src: '/audio/cardiac/vsd.wav', title: 'Ventricular septal defect (VSD)',
|
||
where: 'Lower left sternal border (4th ICS)',
|
||
features: 'Harsh, blowing, holosystolic (pansystolic) murmur — plateau shape through all of systole. Often accompanied by a thrill if large.',
|
||
clinical: 'Most common congenital heart defect. Small VSD: loud murmur, usually asymptomatic, may close spontaneously. Large VSD: softer murmur (less pressure gradient) but signs of heart failure, pulmonary hypertension.' },
|
||
{ key: 'mvp', src: '/audio/cardiac/mitral-prolapse.wav', title: 'Mitral valve prolapse (MVP) — click + late systolic murmur',
|
||
where: 'Apex (5th ICS, mid-clavicular line)',
|
||
features: 'Mid-systolic click followed by a late-systolic crescendo murmur. Timing of click changes with maneuvers: earlier with standing or Valsalva, later with squatting.',
|
||
clinical: 'Often benign, especially in thin young women. Features suggesting need for echo: thickened/redundant leaflets, associated MR, symptoms (palpitations, chest pain), arrhythmias.' },
|
||
{ key: 'stills', src: '/audio/cardiac/stills-murmur.ogg', title: 'Still\'s murmur (innocent)',
|
||
where: 'LLSB, radiating to apex', rate: 'Classic age 3–7 y (this recording is a toddler)',
|
||
features: 'Low-frequency vibratory / musical systolic, grade 2–3/6, mid-systolic, "twanging-string" quality.',
|
||
clinical: 'The most common innocent murmur of childhood. Louder supine, softer or disappears on standing or Valsalva. Normal S2. No radiation to neck or back. No workup needed when classic.' },
|
||
{ key: 'functional', src: '/audio/cardiac/functional-murmur.wav', title: 'Functional (innocent) murmur — adult female',
|
||
where: 'Left sternal border, soft systolic',
|
||
features: 'Soft systolic murmur in a structurally normal heart — often from increased cardiac output, thin chest wall, anemia, hyperthyroidism, or pregnancy.',
|
||
clinical: 'Benign if it meets the 7 S criteria. Investigate if loud (≥3/6), holosystolic, diastolic, radiating, or with thrill / symptoms.' },
|
||
];
|