// ============================================================
// 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