// ============================================================ // PHYSICAL EXAM GUIDE — step-by-step OSCE reference + report generator // ============================================================ // Sources for the exam content: // - Bates' Guide to Physical Examination, 13th edition (technique) // - Nelson Textbook of Pediatrics, 22nd edition (age-specific content) // - Hutchison's Clinical Methods, 25th edition (systematic approach) // - Developmental reflex timing: Fenichel Clinical Pediatric Neurology, 8th ed // // Data model: // PE_DATA[ageGroup][system] = { overview, components: [{ name, steps: [{label, method, normal}], abnormalHints }] } // Each step has its own Normal / Abnormal / Skip toggle so the physician // ticks the exam off step-by-step and flags specific abnormal findings. // Report generation (POST /api/generate-pe-narrative) receives the flat // step array and produces a Technique + Findings narrative. // ============================================================ var SCALES = {}; var SYSTEM_SCALES = {}; var APTM_LEGEND = []; var INNOCENT_MURMURS = []; var RESP_SOUNDS = []; var CARDIAC_SOUNDS = []; var PE_DATA = {}; var PE_GUIDE_DATA_URL = '/data/pe-guide.json'; var peGuideDataPromise = null; function applyPeGuideData(data) { data = data || {}; SCALES = data.scales || {}; SYSTEM_SCALES = data.systemScales || {}; APTM_LEGEND = data.aptmLegend || []; INNOCENT_MURMURS = data.innocentMurmurs || []; RESP_SOUNDS = data.respiratorySounds || []; CARDIAC_SOUNDS = data.cardiacSounds || []; PE_DATA = data.peData || {}; } function loadPeGuideData() { if (!peGuideDataPromise) { peGuideDataPromise = fetch(PE_GUIDE_DATA_URL) .then(function (r) { if (!r.ok) throw new Error('Failed to load PE Guide data'); return r.json(); }) .then(function (data) { applyPeGuideData(data); return data; }); } return peGuideDataPromise; } // ──────────────────────────────────────────────────────────── // STATE + RENDER // ──────────────────────────────────────────────────────────── var _inited = false; // state keyed as 'system-componentIdx-stepIdx' → { label, method, normal, status, note } var state = {}; var currentSystem = 'msk'; var currentAgeGroup = ''; document.addEventListener('tabChanged', function (e) { if (e.detail.tab !== 'peguide' || _inited) return; _inited = true; init().catch(function (err) { console.error('[PEGuide] init error', err); var content = document.getElementById('pe-content'); if (content) content.innerHTML = '

Unable to load PE Guide data.

'; }); }); async function init() { await loadPeGuideData(); var ageSelect = document.getElementById('pe-age-group'); var content = document.getElementById('pe-content'); var actions = document.getElementById('pe-actions'); function applyAgeGroup(ag) { currentAgeGroup = ag; state = {}; document.getElementById('pe-output').classList.add('hidden'); if (!currentAgeGroup || !PE_DATA[currentAgeGroup]) { content.innerHTML = '

Select an age group above.

'; actions.style.display = 'none'; return; } renderSystem(); actions.style.display = 'flex'; } ageSelect.addEventListener('change', function () { applyAgeGroup(ageSelect.value); if (window.UIState) window.UIState.set('pe.ageGroup', ageSelect.value || ''); }); function applySystem(sys) { document.querySelectorAll('[data-pesystem]').forEach(function (b) { b.classList.toggle('active', b.dataset.pesystem === sys); }); currentSystem = sys; state = {}; document.getElementById('pe-output').classList.add('hidden'); if (currentAgeGroup) renderSystem(); } document.querySelectorAll('[data-pesystem]').forEach(function (btn) { btn.addEventListener('click', function () { applySystem(btn.dataset.pesystem); if (window.UIState) window.UIState.set('pe.system', btn.dataset.pesystem); }); }); // Restore persisted selections. Age group must be restored first because // the system render depends on it. if (window.UIState) { var savedAge = window.UIState.get('pe.ageGroup'); if (savedAge && PE_DATA[savedAge]) { ageSelect.value = savedAge; applyAgeGroup(savedAge); } var savedSys = window.UIState.get('pe.system'); if (savedSys && /^(msk|neuro|resp|cv)$/.test(savedSys)) { applySystem(savedSys); } } document.getElementById('pe-all-normal').addEventListener('click', setAllNormal); document.getElementById('pe-all-clear').addEventListener('click', clearAll); document.getElementById('pe-generate-btn').addEventListener('click', generate); } // Responsive two-column grid helper: side-by-side on wide screens, stacked on narrow var TWO_COL_GRID = 'display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;align-items:start;'; // Render a single sound card with native