// ============================================================ // SICK VISIT TAB — Recording, ROS/PE inference, Note generation // ============================================================ (function() { 'use strict'; // ── 15 ROS systems (same keys as shadess.js) ───────────────────────────── var ROS_SYSTEMS = [ { key: 'constitutional', label: 'Constitutional', detail: 'fever, fatigue, weight loss/gain, appetite' }, { key: 'eyes', label: 'Eyes', detail: 'vision changes, discharge, redness' }, { key: 'ent', label: 'ENT', detail: 'ear pain, congestion, sore throat, mouth sores' }, { key: 'cardiovascular', label: 'Cardiovascular', detail: 'chest pain, palpitations, murmur' }, { key: 'respiratory', label: 'Respiratory', detail: 'cough, wheeze, dyspnea' }, { key: 'gastrointestinal', label: 'Gastrointestinal', detail: 'nausea, vomiting, diarrhea, constipation, abdominal pain' }, { key: 'genitourinary', label: 'Genitourinary', detail: 'dysuria, frequency, discharge, menstrual' }, { key: 'musculoskeletal', label: 'Musculoskeletal', detail: 'joint pain, swelling, gait issues, back pain' }, { key: 'skin', label: 'Skin', detail: 'rash, lesions, itch, hair/nail changes' }, { key: 'neurological', label: 'Neurological', detail: 'headache, dizziness, seizures, numbness' }, { key: 'psychiatric', label: 'Psychiatric', detail: 'mood, anxiety, sleep, behavior changes' }, { key: 'endocrine', label: 'Endocrine', detail: 'polyuria/polydipsia, heat/cold intolerance, growth concerns' }, { key: 'hematologic', label: 'Hematologic/Lymphatic', detail: 'bruising, bleeding, lymph nodes' }, { key: 'allergic', label: 'Allergic/Immunologic', detail: 'allergic reactions, frequent infections' }, { key: 'developmental', label: 'Developmental/Behavioral', detail: 'milestones, school, social' }, ]; // ── 17 PE systems ───────────────────────────────────────────────────────── var PE_SYSTEMS = [ { key: 'general', label: 'General Appearance', detail: 'WDWN, alert, NAD, etc.' }, { key: 'peEyes', label: 'Eyes', detail: 'PERRL, conjunctiva, EOM' }, { key: 'ears', label: 'Ears', detail: 'TMs, canals, hearing' }, { key: 'nose', label: 'Nose', detail: 'mucosa, septum, turbinates' }, { key: 'mouthThroat', label: 'Mouth/Throat', detail: 'lips, gums, teeth, tonsils, pharynx' }, { key: 'neck', label: 'Neck', detail: 'lymph nodes, thyroid, masses' }, { key: 'chestLungs', label: 'Chest/Lungs', detail: 'air entry, wheezing, retractions' }, { key: 'peCv', label: 'Cardiovascular', detail: 'S1/S2, murmur, pulses, cap refill' }, { key: 'abdomen', label: 'Abdomen', detail: 'soft, tenderness, organomegaly, BS' }, { key: 'guTanner', label: 'Genitourinary/Tanner', detail: 'Tanner stage, genitalia, hernias' }, { key: 'msk', label: 'Musculoskeletal', detail: 'strength, ROM, gait, spine' }, { key: 'extremities', label: 'Extremities', detail: 'tone, reflexes, clubbing, edema' }, { key: 'peSkin', label: 'Skin', detail: 'rashes, lesions, cafe au lait, bruises' }, { key: 'peNeuro', label: 'Neurological', detail: 'CN intact, DTRs, coordination' }, { key: 'lymphatic', label: 'Lymphatic', detail: 'cervical, axillary, inguinal nodes' }, { key: 'breast', label: 'Breast', detail: 'if indicated by age/Tanner' }, { key: 'pePsych', label: 'Psychiatric', detail: 'affect, mood, behavior during exam' }, ]; // ── Inference rules (CC keyword → ROS and PE system keys) ───────────────── var INFERENCE_RULES = [ { keywords: ['ear', 'hearing', 'otalgia', 'otitis'], ros: ['constitutional', 'ent'], pe: ['general', 'ears', 'nose', 'mouthThroat', 'neck'] }, { keywords: ['throat', 'strep', 'pharyn', 'tonsil'], ros: ['constitutional', 'ent'], pe: ['general', 'mouthThroat', 'neck', 'ears', 'lymphatic'] }, { keywords: ['cough', 'breath', 'asthma', 'wheez', 'respiratory', 'bronch'], ros: ['constitutional', 'respiratory', 'cardiovascular', 'ent'], pe: ['general', 'chestLungs', 'peCv', 'ears', 'nose'] }, { keywords: ['stomach', 'vomit', 'nausea', 'diarr', 'abdomin', 'constip', 'belly'], ros: ['constitutional', 'gastrointestinal', 'genitourinary'], pe: ['general', 'abdomen', 'chestLungs', 'peCv', 'peSkin'] }, { keywords: ['fever', 'temp', 'hot'], ros: ['constitutional', 'ent', 'respiratory', 'gastrointestinal'], pe: ['general', 'ears', 'mouthThroat', 'chestLungs', 'abdomen', 'peSkin', 'lymphatic'] }, { keywords: ['rash', 'skin', 'itch', 'eczema', 'hives', 'dermat'], ros: ['constitutional', 'skin', 'allergic'], pe: ['general', 'peSkin', 'chestLungs', 'peCv', 'lymphatic'] }, { keywords: ['headache', 'head', 'migrain'], ros: ['constitutional', 'neurological', 'eyes', 'ent'], pe: ['general', 'peNeuro', 'peEyes', 'ears', 'neck'] }, { keywords: ['urin', 'uti', 'dysuria', 'bladder', 'kidney'], ros: ['constitutional', 'genitourinary', 'gastrointestinal'], pe: ['general', 'abdomen', 'guTanner', 'chestLungs', 'peCv'] }, { keywords: ['joint', 'knee', 'limb', 'leg', 'arm', 'ankle', 'musculo', 'back', 'pain'], ros: ['constitutional', 'musculoskeletal', 'neurological'], pe: ['general', 'msk', 'extremities', 'peNeuro', 'peSkin'] }, { keywords: ['anxi', 'mood', 'behav', 'depress', 'mental', 'school'], ros: ['constitutional', 'psychiatric', 'neurological', 'developmental'], pe: ['general', 'pePsych', 'peNeuro', 'peSkin'] }, ]; var DEFAULT_ROS = ['constitutional', 'ent', 'respiratory', 'gastrointestinal']; var DEFAULT_PE = ['general', 'ears', 'chestLungs', 'peCv', 'abdomen', 'peSkin', 'peNeuro']; function inferSystems(cc) { var lc = (cc || '').toLowerCase(); var rosSet = {}; var peSet = {}; var matched = false; INFERENCE_RULES.forEach(function(rule) { if (rule.keywords.some(function(kw) { return lc.indexOf(kw) !== -1; })) { matched = true; rule.ros.forEach(function(k) { rosSet[k] = true; }); rule.pe.forEach(function(k) { peSet[k] = true; }); } }); if (!matched) { DEFAULT_ROS.forEach(function(k) { rosSet[k] = true; }); DEFAULT_PE.forEach(function(k) { peSet[k] = true; }); } // Return top 4 ROS, top 7 PE var rosKeys = Object.keys(rosSet).slice(0, 4); var peKeys = Object.keys(peSet).slice(0, 7); // Ensure we always have at least the default counts DEFAULT_ROS.forEach(function(k) { if (rosKeys.length < 4 && rosKeys.indexOf(k) === -1) rosKeys.push(k); }); DEFAULT_PE.forEach(function(k) { if (peKeys.length < 7 && peKeys.indexOf(k) === -1) peKeys.push(k); }); return { ros: rosKeys.map(function(k) { return ROS_SYSTEMS.find(function(s) { return s.key === k; }); }).filter(Boolean), pe: peKeys.map(function(k) { return PE_SYSTEMS.find(function(s) { return s.key === k; }); }).filter(Boolean), }; } // ── Module state ───────────────────────────────────────────────────────── var _sickRosData = {}; var _sickPeData = {}; var _sickSelectedDx = []; var _sickRosSystems = DEFAULT_ROS.map(function(k) { return ROS_SYSTEMS.find(function(s) { return s.key === k; }); }).filter(Boolean); var _sickPeSystems = DEFAULT_PE.map(function(k) { return PE_SYSTEMS.find(function(s) { return s.key === k; }); }).filter(Boolean); // ── Recording ──────────────────────────────────────────────────────────── var _sickRecorder = null; var _sickTimer = null; var _sickRecording = false; var _sickPaused = false; var _sickRecognition = null; var _sickTranscript = ''; function initRecording() { var recordBtn = document.getElementById('sick-record-btn'); var pauseBtn = document.getElementById('sick-pause-btn'); var indicator = document.getElementById('sick-rec-indicator'); var timerEl = document.getElementById('sick-timer'); var transcriptEl = document.getElementById('sick-transcript'); if (!recordBtn) return; _sickTimer = createTimer(timerEl); _sickRecognition = createSpeechRecognition(); if (_sickRecognition) { _sickRecognition.onresult = function(e) { var interim = ''; for (var i = e.resultIndex; i < e.results.length; i++) { if (e.results[i].isFinal) _sickTranscript += e.results[i][0].transcript + ' '; else interim = e.results[i][0].transcript; } if (transcriptEl) transcriptEl.innerHTML = _sickTranscript + (interim ? '' + interim + '' : ''); }; _sickRecognition.onend = function() { if (_sickRecording && !_sickPaused) try { _sickRecognition.start(); } catch(e) {} }; } recordBtn.addEventListener('click', function() { if (!_sickRecording) { _sickTranscript = ''; _sickRecorder = new AudioRecorder(); _sickRecorder.start().then(function() { _sickRecording = true; _sickPaused = false; recordBtn.innerHTML = ' Stop'; recordBtn.classList.add('recording'); if (pauseBtn) pauseBtn.classList.remove('hidden'); indicator.classList.remove('hidden'); _sickTimer.start(); if (_sickRecognition) try { _sickRecognition.start(); } catch(e) {} showToast('Sick visit recording started', 'info'); document.dispatchEvent(new CustomEvent('recording-started', { detail: { module: 'sick' } })); }).catch(function() { showToast('Microphone denied', 'error'); }); } else { _sickRecording = false; _sickPaused = false; var dur = _sickTimer.stop(); recordBtn.innerHTML = ' Listen In'; recordBtn.classList.remove('recording'); if (pauseBtn) pauseBtn.classList.add('hidden'); indicator.classList.add('hidden'); if (_sickRecognition) try { _sickRecognition.stop(); } catch(e) {} document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'sick' } })); showLoading('Transcribing...'); _sickRecorder.stop().then(function(blob) { if (!blob || blob.size === 0) { hideLoading(); if (_sickTranscript.trim() && transcriptEl) transcriptEl.textContent = _sickTranscript.trim(); return; } return transcribeAudio(blob).then(function(data) { hideLoading(); if (data.success) { if (transcriptEl) transcriptEl.textContent = data.text; _sickTranscript = data.text; showToast('Transcribed ' + dur + 's', 'success'); } else { if (_sickTranscript.trim() && transcriptEl) transcriptEl.textContent = _sickTranscript.trim(); } }); }).catch(function() { hideLoading(); }); } }); if (pauseBtn) { pauseBtn.addEventListener('click', function() { if (!_sickRecording || !_sickRecorder || !_sickRecorder.mediaRecorder) return; if (!_sickPaused) { _sickRecorder.mediaRecorder.pause(); _sickTimer.stop(); _sickPaused = true; pauseBtn.innerHTML = ' Resume'; if (_sickRecognition) try { _sickRecognition.stop(); } catch(e) {} } else { _sickRecorder.mediaRecorder.resume(); _sickTimer.start(); _sickPaused = false; pauseBtn.innerHTML = ' Pause'; if (_sickRecognition) try { _sickRecognition.start(); } catch(e) {} } }); } } // ── ROS/PE rendering ────────────────────────────────────────────────────── function populateAddDropdowns() { var rosAddEl = document.getElementById('sick-ros-add'); var peAddEl = document.getElementById('sick-pe-add'); if (rosAddEl) { rosAddEl.innerHTML = '' + ROS_SYSTEMS.map(function(s) { return ''; }).join(''); rosAddEl.addEventListener('change', function() { var key = this.value; if (!key) return; this.value = ''; var sys = ROS_SYSTEMS.find(function(s) { return s.key === key; }); if (!sys) return; if (_sickRosSystems.some(function(s) { return s.key === key; })) return; _sickRosSystems.push(sys); renderRosPe(); }); } if (peAddEl) { peAddEl.innerHTML = '' + PE_SYSTEMS.map(function(s) { return ''; }).join(''); peAddEl.addEventListener('change', function() { var key = this.value; if (!key) return; this.value = ''; var sys = PE_SYSTEMS.find(function(s) { return s.key === key; }); if (!sys) return; if (_sickPeSystems.some(function(s) { return s.key === key; })) return; _sickPeSystems.push(sys); renderRosPe(); }); } } function renderRosPe() { var rosContainer = document.getElementById('sick-ros-container'); var peContainer = document.getElementById('sick-pe-container'); if (rosContainer && typeof window.renderRosRows === 'function') { rosContainer.innerHTML = window.renderRosRows(_sickRosSystems, _sickRosData, 'sros', { wnl: 'WNL', abnormal: 'Abnormal', notrev: 'Not reviewed' }); window.wireRosContainer(rosContainer, _sickRosData); } if (peContainer && typeof window.renderRosRows === 'function') { peContainer.innerHTML = window.renderRosRows(_sickPeSystems, _sickPeData, 'spe', { wnl: 'Normal', abnormal: 'Abnormal', notrev: 'Not examined' }); window.wireRosContainer(peContainer, _sickPeData); } } function inferAndRender() { var cc = (document.getElementById('sick-cc') || {}).value || ''; var inferred = inferSystems(cc); _sickRosSystems = inferred.ros; _sickPeSystems = inferred.pe; _sickRosData = {}; _sickPeData = {}; renderRosPe(); if (cc) showToast('ROS/PE updated for: ' + cc, 'info'); } // ── Diagnoses ───────────────────────────────────────────────────────────── function initDx() { var dxContainer = document.getElementById('sick-dx-container'); if (dxContainer && typeof window.renderDxComponent === 'function') { window.renderDxComponent(dxContainer, _sickSelectedDx, 'sick'); } } // ── Generate note ───────────────────────────────────────────────────────── function generateSickNote() { var age = (document.getElementById('sick-age') || {}).value || ''; var gender = (document.getElementById('sick-gender') || {}).value || ''; var cc = (document.getElementById('sick-cc') || {}).value || ''; if (!cc) { showToast('Enter a chief complaint first', 'error'); return; } var transcriptEl = document.getElementById('sick-transcript'); var transcript = transcriptEl ? (transcriptEl.innerText || transcriptEl.textContent || '') : ''; var rosText = typeof window.formatRosForAI === 'function' ? window.formatRosForAI(_sickRosSystems, _sickRosData, 'Review of Systems') : ''; var peText = typeof window.formatRosForAI === 'function' ? window.formatRosForAI(_sickPeSystems, _sickPeData, 'Physical Examination') : ''; var dxText = typeof window.formatDxForAI === 'function' ? window.formatDxForAI(_sickSelectedDx, 'sick-dx-freetext') : ''; showLoading('Generating sick visit note...'); var modelEl = document.getElementById('sick-model-select'); var memoriesPromise = (typeof getUserMemoryContext === 'function') ? getUserMemoryContext() : Promise.resolve(''); memoriesPromise.then(function(memCtx) { return fetch('/api/sick-visit/note', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ patientAge: age, patientGender: gender, chiefComplaint: cc, transcript: transcript || null, ros: rosText || null, physicalExam: peText || null, diagnoses: dxText || null, physicianMemories: memCtx || null, model: modelEl ? modelEl.value : getSelectedModel() }) }); }) .then(function(r) { return r.json(); }) .then(function(data) { hideLoading(); if (data.success) { var noteEl = document.getElementById('sick-note-text'); var outCard = document.getElementById('sick-note-output'); var tag = document.getElementById('sick-note-model-tag'); if (noteEl) setOutputText(noteEl, data.note); if (tag) tag.textContent = (data.model || '').split('/').pop(); if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); } showToast('Sick visit note generated!', 'success'); } else { showToast(data.error || 'Generation failed', 'error'); } }) .catch(function(err) { hideLoading(); showToast(err.message, 'error'); }); } // ── Reset ───────────────────────────────────────────────────────────────── function resetSickVisit() { var fields = ['sick-age', 'sick-gender', 'sick-cc', 'sick-label']; fields.forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); var transcriptEl = document.getElementById('sick-transcript'); if (transcriptEl) transcriptEl.textContent = ''; _sickTranscript = ''; _sickRosData = {}; _sickPeData = {}; _sickSelectedDx.length = 0; _sickRosSystems = DEFAULT_ROS.map(function(k) { return ROS_SYSTEMS.find(function(s) { return s.key === k; }); }).filter(Boolean); _sickPeSystems = DEFAULT_PE.map(function(k) { return PE_SYSTEMS.find(function(s) { return s.key === k; }); }).filter(Boolean); renderRosPe(); initDx(); var outCard = document.getElementById('sick-note-output'); var noteEl = document.getElementById('sick-note-text'); if (outCard) outCard.classList.add('hidden'); if (noteEl) noteEl.textContent = ''; window._savedEncId_sickvisit = null; showToast('Sick visit cleared for new patient', 'info'); } // ── Click handler ───────────────────────────────────────────────────────── document.addEventListener('click', function(e) { if (e.target.closest('#btn-sick-generate')) generateSickNote(); if (e.target.closest('#btn-sick-infer')) inferAndRender(); if (e.target.closest('#btn-sick-new-patient')) resetSickVisit(); if (e.target.closest('#sick-refine-btn')) refineDocument('sick-note-text', 'sick-refine-input'); if (e.target.closest('#sick-shorten-btn')) shortenDocument('sick-note-text'); if (e.target.closest('#sick-ros-all-wnl')) { _sickRosSystems.forEach(function(sys) { _sickRosData[sys.key + '_status'] = 'wnl'; }); renderRosPe(); showToast('All ROS set to WNL', 'success'); } if (e.target.closest('#sick-pe-all-normal')) { _sickPeSystems.forEach(function(sys) { _sickPeData[sys.key + '_status'] = 'wnl'; }); renderRosPe(); showToast('All PE set to Normal', 'success'); } if (e.target.closest('#btn-sick-save')) { var labelEl = document.getElementById('sick-label'); var transcriptEl = document.getElementById('sick-transcript'); var noteEl = document.getElementById('sick-note-text'); if (typeof saveEncounter === 'function') { saveEncounter({ id: window._savedEncId_sickvisit || null, label: labelEl ? labelEl.value.trim() : '', enc_type: 'sickvisit', transcript: transcriptEl ? (transcriptEl.innerText || '') : '', generated_note: noteEl ? (noteEl.innerText || '') : '', onSaved: function(id) { window._savedEncId_sickvisit = id; } }); } } }); // ── Load handler ────────────────────────────────────────────────────────── if (typeof registerEncounterLoadHandler === 'function') { registerEncounterLoadHandler('sickvisit', function(enc) { var transcriptEl = document.getElementById('sick-transcript'); if (transcriptEl && enc.transcript) transcriptEl.textContent = enc.transcript; if (enc.generated_note) { var noteEl = document.getElementById('sick-note-text'); var outCard = document.getElementById('sick-note-output'); if (noteEl) noteEl.textContent = enc.generated_note; if (outCard) outCard.classList.remove('hidden'); } }); } function esc(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // ── Init ────────────────────────────────────────────────────────────────── var _inited = false; document.addEventListener('tabChanged', function(e) { if (e.detail.tab !== 'sickvisit' || _inited) return; _inited = true; initRecording(); populateAddDropdowns(); renderRosPe(); initDx(); }); console.log('SickVisit module loaded'); })();