// ============================================================ // SHADESS.JS — SSHADESS psychosocial screening form + well visit note // ============================================================ (function() { // ── SSHADESS domain definitions ────────────────────────────────────────── // Each domain has key questions (most important shown first) + comment field var SHADESS_DOMAINS = [ { key: 'strengths', label: 'Strengths', icon: 'fa-star', color: '#f59e0b', intro: 'Starting with what you do well helps us get to know you better.', questions: [ { id: 'str1', text: 'Has something they are proud of or enjoy', type: 'yn' }, { id: 'str2', text: 'Describes self positively when asked', type: 'yn' }, { id: 'str3', text: 'Has at least one trusted adult they can talk to', type: 'yn' }, ] }, { key: 'school', label: 'School', icon: 'fa-graduation-cap', color: '#3b82f6', intro: 'Ask about school performance, attendance, and future plans.', questions: [ { id: 'sch1', text: 'Grades are satisfactory / doing their best', type: 'yn' }, { id: 'sch2', text: 'Likes school or finds something enjoyable about it', type: 'yn' }, { id: 'sch3', text: 'Regular attendance (no truancy concerns)', type: 'yn' }, { id: 'sch4', text: 'Has plans or goals for the future', type: 'yn' }, ] }, { key: 'home', label: 'Home', icon: 'fa-house', color: '#10b981', intro: 'Ask about living situation and family relationships.', questions: [ { id: 'hom1', text: 'Stable living situation', type: 'yn' }, { id: 'hom2', text: 'Gets along with people at home', type: 'yn' }, { id: 'hom3', text: 'Would talk to family member if stressed', type: 'yn' }, { id: 'hom4', text: 'Has experienced household violence or instability (concern if YES)', type: 'yn', concern_if: true }, ] }, { key: 'activities', label: 'Activities', icon: 'fa-users', color: '#8b5cf6', intro: 'Ask about friends, hobbies, and peer relationships.', questions: [ { id: 'act1', text: 'Has friends and spends time with them', type: 'yn' }, { id: 'act2', text: 'Involved in sports, clubs, or hobbies', type: 'yn' }, { id: 'act3', text: 'Social media/screen use within healthy limits', type: 'yn' }, { id: 'act4', text: 'Has experienced bullying (concern if YES)', type: 'yn', concern_if: true }, ] }, { key: 'drugs', label: 'Drugs / Substances', icon: 'fa-pills', color: '#ef4444', intro: 'This is confidential. Ask in private.', questions: [ { id: 'drg1', text: 'Has tried cigarettes / vaping / tobacco', type: 'yn', concern_if: true }, { id: 'drg2', text: 'Has tried alcohol', type: 'yn', concern_if: true }, { id: 'drg3', text: 'Has tried marijuana or other drugs', type: 'yn', concern_if: true }, { id: 'drg4', text: 'Friends use substances', type: 'yn' }, { id: 'drg5', text: 'CRAFFT screen result (if done)', type: 'text', placeholder: 'e.g., Score 0 — low risk' }, ] }, { key: 'emotions', label: 'Emotions / Eating', icon: 'fa-heart', color: '#ec4899', intro: 'Screen for depression, anxiety, and disordered eating.', questions: [ { id: 'emo1', text: 'Feeling down, sad, or hopeless recently', type: 'yn', concern_if: true }, { id: 'emo2', text: 'Feeling unusually stressed or anxious', type: 'yn', concern_if: true }, { id: 'emo3', text: 'Trouble sleeping', type: 'yn' }, { id: 'emo4', text: 'PHQ-A / depression screen result (if done)', type: 'text', placeholder: 'e.g., PHQ-A score 3 — minimal' }, { id: 'emo5', text: 'Happy with eating habits and body image', type: 'yn' }, { id: 'emo6', text: 'Restricting food / purging / using diet pills (concern if YES)', type: 'yn', concern_if: true }, ] }, { key: 'sexuality', label: 'Sexuality', icon: 'fa-shield-heart', color: '#f97316', intro: 'Ask in private. Normalize the questions.', questions: [ { id: 'sex1', text: 'Comfortable discussing attraction/identity', type: 'yn' }, { id: 'sex2', text: 'Sexually active', type: 'yn' }, { id: 'sex3', text: 'Uses protection consistently if sexually active', type: 'yn' }, { id: 'sex4', text: 'History of unwanted sexual contact (concern if YES)', type: 'yn', concern_if: true }, { id: 'sex5', text: 'STI screening indicated/done', type: 'yn' }, ] }, { key: 'safety', label: 'Safety', icon: 'fa-shield-halved', color: '#6366f1', intro: 'Ask about violence, weapons, and suicidal ideation.', questions: [ { id: 'saf1', text: 'Feels safe at school and home', type: 'yn' }, { id: 'saf2', text: 'Carries a weapon (concern if YES)', type: 'yn', concern_if: true }, { id: 'saf3', text: 'Has been in physical fights recently', type: 'yn', concern_if: true }, { id: 'saf4', text: 'Wears seatbelt; safe driving practices', type: 'yn' }, { id: 'saf5', text: 'Thoughts of hurting self or suicide (concern if YES — STAT eval)', type: 'yn', concern_if: true }, { id: 'saf6', text: 'Columbia/ASQ suicide screen result (if done)', type: 'text', placeholder: 'e.g., ASQ: negative' }, ] } ]; // ── Render SHADESS form ────────────────────────────────────────────────── var _shadessAnswers = {}; // key -> { questions: {id: answer}, comment: '', concern: false, skipped: false } function initShadess() { var container = document.getElementById('shadess-domains'); if (!container) return; // Initialize answers object SHADESS_DOMAINS.forEach(function(d) { _shadessAnswers[d.key] = { questions: {}, comment: '', concern: false, skipped: false }; }); container.innerHTML = SHADESS_DOMAINS.map(function(domain) { return renderDomain(domain); }).join(''); // Wire up events container.addEventListener('change', function(e) { var el = e.target; // yn question if (el.classList.contains('shadess-yn')) { var domainKey = el.dataset.domain; var qid = el.dataset.qid; if (_shadessAnswers[domainKey]) { _shadessAnswers[domainKey].questions[qid] = el.value; // Auto-flag concern var domain = SHADESS_DOMAINS.find(function(d) { return d.key === domainKey; }); if (domain) { var q = domain.questions.find(function(q) { return q.id === qid; }); if (q && q.concern_if === (el.value === 'yes')) { _shadessAnswers[domainKey].concern = true; var flagEl = document.getElementById('shadess-concern-' + domainKey); if (flagEl) flagEl.classList.remove('hidden'); } } } } // text question if (el.classList.contains('shadess-text')) { if (_shadessAnswers[el.dataset.domain]) { _shadessAnswers[el.dataset.domain].questions[el.dataset.qid] = el.value; } } // comment if (el.classList.contains('shadess-comment')) { if (_shadessAnswers[el.dataset.domain]) { _shadessAnswers[el.dataset.domain].comment = el.value; } } // skip toggle if (el.classList.contains('shadess-skip')) { if (_shadessAnswers[el.dataset.domain]) { _shadessAnswers[el.dataset.domain].skipped = el.checked; var body = document.getElementById('shadess-body-' + el.dataset.domain); if (body) body.style.opacity = el.checked ? '0.3' : '1'; } } }); // Concern flag manual toggle container.addEventListener('click', function(e) { if (e.target.closest('.shadess-flag-btn')) { var btn = e.target.closest('.shadess-flag-btn'); var domainKey = btn.dataset.domain; if (_shadessAnswers[domainKey]) { _shadessAnswers[domainKey].concern = !_shadessAnswers[domainKey].concern; var flagEl = document.getElementById('shadess-concern-' + domainKey); if (flagEl) flagEl.classList.toggle('hidden', !_shadessAnswers[domainKey].concern); } } }); } function renderDomain(domain) { var qHtml = domain.questions.map(function(q) { if (q.type === 'yn') { return '
' + '' + esc(q.text) + (q.concern_if !== undefined ? ' (flag if ' + (q.concern_if ? 'Yes' : 'No') + ')' : '') + '' + '
' + '' + '
' + '
'; } else { return '
' + '' + esc(q.text) + '' + '' + '
'; } }).join(''); return '
' + '
' + '' + '' + esc(domain.label) + '' + '' + esc(domain.intro) + '' + '' + '' + '' + '
' + '
' + qHtml + '
' + 'Additional notes:' + '' + '
' + '
' + '
'; } // ── SHADESS recording (listen in) ──────────────────────────────────────── var _shadessRecorder = null; var _shadessTimer = null; var _shadessRecording = false; var _shadessIsPaused = false; var _shhadessRecognition = null; var _shadessTranscript = ''; function initShadessRecording() { var recordBtn = document.getElementById('shadess-record-btn'); var pauseBtn = document.getElementById('shadess-pause-btn'); var indicator = document.getElementById('shadess-rec-indicator'); var timerEl = document.getElementById('shadess-timer'); if (!recordBtn) return; _shadessTimer = createTimer(timerEl); _shhadessRecognition = createSpeechRecognition(); if (_shhadessRecognition) { _shhadessRecognition.onresult = function(e) { for (var i = e.resultIndex; i < e.results.length; i++) { if (e.results[i].isFinal) _shadessTranscript += e.results[i][0].transcript + ' '; } }; _shhadessRecognition.onend = function() { if (_shadessRecording && !_shadessIsPaused) try { _shhadessRecognition.start(); } catch(e) {} }; } recordBtn.addEventListener('click', function() { if (!_shadessRecording) { _shadessTranscript = ''; _shadessRecorder = new AudioRecorder(); _shadessRecorder.start().then(function() { _shadessRecording = true; _shadessIsPaused = false; recordBtn.innerHTML = ' Stop'; recordBtn.classList.add('recording'); if (pauseBtn) pauseBtn.classList.remove('hidden'); indicator.classList.remove('hidden'); _shadessTimer.start(); if (_shhadessRecognition) try { _shhadessRecognition.start(); } catch(e) {} showToast('SSHADESS recording started', 'info'); }).catch(function() { showToast('Microphone denied', 'error'); }); } else { _shadessRecording = false; _shadessIsPaused = false; var dur = _shadessTimer.stop(); recordBtn.innerHTML = ' Listen In'; recordBtn.classList.remove('recording'); if (pauseBtn) { pauseBtn.classList.add('hidden'); } indicator.classList.add('hidden'); if (_shhadessRecognition) try { _shhadessRecognition.stop(); } catch(e) {} showBusy('Transcribing SSHADESS...'); _shadessRecorder.stop().then(function(blob) { if (!blob || blob.size === 0) { hideBusy(); return; } return transcribeAudio(blob).then(function(data) { hideBusy(); if (data.success) { _shadessTranscript = data.text; showToast('SSHADESS audio transcribed (' + dur + 's)', 'success'); } }); }).catch(function() { hideBusy(); }); } }); if (pauseBtn) { pauseBtn.addEventListener('click', function() { if (!_shadessRecording || !_shadessRecorder || !_shadessRecorder.mediaRecorder) return; if (!_shadessIsPaused) { try { if (_shadessRecorder.mediaRecorder.state === 'recording') _shadessRecorder.mediaRecorder.pause(); } catch(e) {} _shadessTimer.stop(); _shadessIsPaused = true; pauseBtn.innerHTML = ' Resume'; if (_shhadessRecognition) try { _shhadessRecognition.stop(); } catch(e) {} } else { try { if (_shadessRecorder.mediaRecorder.state === 'paused') { _shadessRecorder.mediaRecorder.resume(); } else if (_shadessRecorder.mediaRecorder.state === 'inactive' && _shadessRecorder.stream && _shadessRecorder.stream.active) { var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm'; _shadessRecorder.mediaRecorder = new MediaRecorder(_shadessRecorder.stream, { mimeType: mime, audioBitsPerSecond: 32000 }); _shadessRecorder.mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) _shadessRecorder.chunks.push(e.data); }; _shadessRecorder.mediaRecorder.start(1000); } } catch(e) {} _shadessTimer.resume(); _shadessIsPaused = false; pauseBtn.innerHTML = ' Pause'; if (_shhadessRecognition) try { _shhadessRecognition.start(); } catch(e) {} } }); } } // ── Generate SSHADESS assessment ───────────────────────────────────────── function generateShadess() { var age = document.getElementById('shadess-age').value; var gender = document.getElementById('shadess-gender').value; // Build domains payload from answers var domains = {}; SHADESS_DOMAINS.forEach(function(d) { var ans = _shadessAnswers[d.key]; var questions = d.questions.map(function(q) { return { id: q.id, text: q.text, answer: ans.questions[q.id] || null }; }).filter(function(q) { return q.answer !== null && q.answer !== ''; }); domains[d.key] = { skipped: ans.skipped, questions: questions, comment: ans.comment, concern: ans.concern }; }); // Check something is filled in var hasData = Object.values(domains).some(function(d) { return !d.skipped && (d.questions.length > 0 || d.comment); }); if (!hasData && !_shadessTranscript) { showToast('Fill in at least one domain before generating', 'error'); return; } showBusy('Generating SSHADESS assessment...'); var modelEl = document.querySelector('#wv-panel-shadess .tab-model-select'); fetch('/api/well-visit/shadess', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ patientAge: age, patientGender: gender, domains: domains, dictationText: _shadessTranscript || null, model: modelEl ? modelEl.value : getSelectedModel() }) }) .then(function(r) { return r.json(); }) .then(function(data) { hideBusy(); if (data.success) { var out = document.getElementById('shadess-result-text'); var outCard = document.getElementById('shadess-output'); var tag = document.getElementById('shadess-model-tag'); if (out) setOutputText(out, data.assessment); if (tag) tag.textContent = (data.model || '').split('/').pop(); if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); } showToast('SSHADESS assessment generated', 'success'); // Auto-fill well visit note SSHADESS field var wvShadess = document.getElementById('wv-shadess-text'); if (wvShadess) wvShadess.value = data.assessment; } else { showToast(data.error || 'Generation failed', 'error'); } }) .catch(function(err) { hideBusy(); showToast(err.message, 'error'); }); } // ── Well Visit Note — ROS data ──────────────────────────────────────────── var _rosData = {}; // { constitutionalStatus: 'wnl'|'abnormal'|'', constitutionalNote: '...', ... } var _peData = {}; // 15 ROS systems 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' }, ]; // ── Shared ROS row renderer (used by well visit and sick visit) ────────── // dataObj = reference to _rosData or _peData // systemsList = ROS_SYSTEMS or PE_SYSTEMS // prefix = 'ros' | 'pe' // btnLabels = { wnl:'WNL', abnormal:'Abnormal', notrev:'Not reviewed' } or { wnl:'Normal', abnormal:'Abnormal', notrev:'Not examined' } window.renderRosRows = function(systemsList, dataObj, prefix, btnLabels) { var bl = btnLabels || { wnl: 'WNL', abnormal: 'Abnormal', notrev: 'Not reviewed' }; return systemsList.map(function(sys) { var status = dataObj[sys.key + '_status'] || ''; var note = dataObj[sys.key + '_note'] || ''; var wnlAct = status === 'wnl' ? ' wnl' : ''; var abnAct = status === 'abnormal' ? ' abnormal' : ''; var notrevAct = status === 'notrev' ? ' notrev' : ''; var noteVis = status === 'abnormal' ? ' visible' : ''; return '
' + '' + esc(sys.label) + ' (' + esc(sys.detail) + ')' + '' + '' + '' + '' + '
'; }).join(''); }; // Wire ROS/PE click events on a container element (idempotent — safe to call multiple times) window.wireRosContainer = function(containerEl, dataObj) { // Remove previous handler to prevent duplicate listeners on re-render if (containerEl._rosClickHandler) containerEl.removeEventListener('click', containerEl._rosClickHandler); if (containerEl._rosInputHandler) containerEl.removeEventListener('input', containerEl._rosInputHandler); containerEl._rosClickHandler = function(e) { var btn = e.target.closest('.ros-status-btn'); if (!btn) return; var sysKey = btn.dataset.sys; var status = btn.dataset.status; var prefix = btn.dataset.prefix; if (!sysKey) return; // Toggle var cur = dataObj[sysKey + '_status'] || ''; dataObj[sysKey + '_status'] = (cur === status) ? '' : status; // Update buttons in row var row = btn.closest('.ros-system-row'); if (row) { row.querySelectorAll('.ros-status-btn').forEach(function(b) { b.classList.remove('wnl', 'abnormal', 'notrev'); if (dataObj[sysKey + '_status'] && b.dataset.status === dataObj[sysKey + '_status']) { b.classList.add(dataObj[sysKey + '_status']); } }); var noteInput = row.querySelector('.ros-note-input'); if (noteInput) { if (dataObj[sysKey + '_status'] === 'abnormal') { noteInput.classList.add('visible'); noteInput.focus(); } else { noteInput.classList.remove('visible'); } } } }; containerEl.addEventListener('click', containerEl._rosClickHandler); containerEl._rosInputHandler = function(e) { var inp = e.target.closest('.ros-note-input'); if (!inp) return; var sysKey = inp.dataset.sys; if (sysKey) dataObj[sysKey + '_note'] = inp.value; }; containerEl.addEventListener('input', containerEl._rosInputHandler); }; // Format ROS data as text for AI window.formatRosForAI = function(systemsList, dataObj, heading) { var lines = [heading + ':']; var hasAny = false; systemsList.forEach(function(sys) { var status = dataObj[sys.key + '_status'] || ''; if (!status) return; hasAny = true; var note = dataObj[sys.key + '_note'] || ''; if (status === 'wnl') { // WNL — tell AI to expand with pertinent negatives, include domain details for context lines.push(' ' + sys.label + ': NORMAL [domain: ' + sys.detail + ']'); } else if (status === 'abnormal') { // Abnormal — user's note is the finding; AI should expand/describe it lines.push(' ' + sys.label + ': ABNORMAL' + (note ? ' — ' + note : '') + ' [domain: ' + sys.detail + ']'); } else { lines.push(' ' + sys.label + ': Not reviewed'); } }); return hasAny ? lines.join('\n') : ''; }; // ── ICD-10 Diagnoses ────────────────────────────────────────────────────── var COMMON_DX = [ { code: 'Z00.129', name: 'Routine child health exam, no abnormal findings' }, { code: 'Z00.121', name: 'Routine child health exam, with abnormal findings' }, { code: 'J06.9', name: 'Upper Respiratory Infection, NOS' }, { code: 'J02.9', name: 'Acute pharyngitis, NOS' }, { code: 'J03.90', name: 'Acute tonsillitis, NOS' }, { code: 'H66.90', name: 'Otitis media, NOS' }, { code: 'J20.9', name: 'Acute bronchitis' }, { code: 'J45.20', name: 'Mild intermittent asthma, uncomplicated' }, { code: 'J45.30', name: 'Mild persistent asthma' }, { code: 'A09', name: 'Gastroenteritis/colitis' }, { code: 'L20.9', name: 'Atopic dermatitis, NOS' }, { code: 'L30.9', name: 'Dermatitis NOS' }, { code: 'R50.9', name: 'Fever NOS' }, { code: 'R10.9', name: 'Abdominal pain NOS' }, { code: 'R05.9', name: 'Cough' }, { code: 'B34.9', name: 'Viral infection NOS' }, { code: 'Z23', name: 'Immunization encounter' }, { code: 'Z41.9', name: 'Procedure for non-remedial reason' }, { code: 'K21.0', name: 'GERD with esophagitis' }, { code: 'K21.9', name: 'GERD without esophagitis' }, { code: 'F90.0', name: 'ADHD, predominantly inattentive' }, { code: 'F90.2', name: 'ADHD, combined type' }, { code: 'F41.1', name: 'Generalized anxiety disorder' }, { code: 'F32.9', name: 'Major depressive episode NOS' }, { code: 'E11.9', name: 'Type 2 diabetes' }, { code: 'E03.9', name: 'Hypothyroidism NOS' }, { code: 'M54.5', name: 'Low back pain' }, { code: 'G43.909', name: 'Migraine NOS' }, ]; // Expose so sick visit can reuse window.COMMON_DX = COMMON_DX; // ICD-10 live search via NLM Clinical Tables API (free, no auth, CORS-enabled) var _icdSearchTimer = {}; function searchIcd10(query, onResults) { if (!query || query.length < 2) { onResults([]); return; } fetch('https://clinicaltables.nlm.nih.gov/api/icd10cm/v3/search?sf=code,name&terms=' + encodeURIComponent(query) + '&maxList=12') .then(function(r) { return r.json(); }) .then(function(data) { // data = [count, [codes], null, [[code, name], ...]] var items = (data[3] || []).map(function(row) { return { code: row[0], name: row[1] }; }); onResults(items); }) .catch(function() { onResults([]); }); } // Render the DX component into a container element // selectedDxArr = reference to an array of { code, name } // idPrefix = unique prefix for element IDs (e.g. 'wv' or 'sv') window.renderDxComponent = function(containerEl, selectedDxArr, idPrefix) { var searchId = idPrefix + '-dx-search'; var tagsId = idPrefix + '-dx-tags'; var resultsId = idPrefix + '-dx-results'; var chipsId = idPrefix + '-dx-chips'; containerEl.innerHTML = '
' + '
' + '
' + '' + '' + '
' + '
' + '
' + '
Common pediatric diagnoses:
' + '
' + '
'; // Render quick-access chips var chipsEl = containerEl.querySelector('#' + chipsId); if (chipsEl) { chipsEl.innerHTML = COMMON_DX.map(function(dx) { return '' + esc(dx.code) + ' ' + esc(dx.name) + ''; }).join(''); chipsEl.addEventListener('click', function(e) { var chip = e.target.closest('.dx-chip'); if (!chip) return; addDx(selectedDxArr, { code: chip.dataset.code, name: chip.dataset.name }, containerEl, tagsId); }); } // Live search var searchEl = containerEl.querySelector('#' + searchId); var resultsEl = containerEl.querySelector('#' + resultsId); function showResults(items) { if (!items.length) { resultsEl.classList.add('hidden'); return; } resultsEl.innerHTML = items.map(function(dx) { return '
' + '' + esc(dx.code) + ' ' + esc(dx.name) + '
'; }).join(''); resultsEl.classList.remove('hidden'); resultsEl.querySelectorAll('.dx-result-item').forEach(function(item) { item.addEventListener('mousedown', function(e) { e.preventDefault(); // prevent blur from firing before click addDx(selectedDxArr, { code: item.dataset.code, name: item.dataset.name }, containerEl, tagsId); searchEl.value = ''; resultsEl.classList.add('hidden'); }); }); } if (searchEl) { searchEl.addEventListener('input', function() { var val = searchEl.value.trim(); clearTimeout(_icdSearchTimer[idPrefix]); if (!val) { resultsEl.classList.add('hidden'); return; } _icdSearchTimer[idPrefix] = setTimeout(function() { searchIcd10(val, showResults); }, 280); }); searchEl.addEventListener('keydown', function(e) { if (e.key === 'Escape') { resultsEl.classList.add('hidden'); } if (e.key === 'Enter') { e.preventDefault(); var first = resultsEl.querySelector('.dx-result-item'); if (first) { first.dispatchEvent(new MouseEvent('mousedown')); } } }); searchEl.addEventListener('blur', function() { setTimeout(function() { resultsEl.classList.add('hidden'); }, 150); }); } renderDxTags(selectedDxArr, containerEl, tagsId); }; function addDx(arr, dx, containerEl, tagsId) { // Avoid duplicates var alreadyExists = arr.some(function(d) { return d.code === dx.code && d.name === dx.name; }); if (alreadyExists) return; arr.push(dx); renderDxTags(arr, containerEl, tagsId); } function renderDxTags(arr, containerEl, tagsId) { var tagsEl = containerEl.querySelector('#' + tagsId); if (!tagsEl) return; tagsEl.innerHTML = arr.map(function(dx, i) { return '' + (dx.code ? '' + esc(dx.code) + ' ' : '') + esc(dx.name) + '' + ''; }).join(''); tagsEl.querySelectorAll('.dx-remove').forEach(function(btn) { btn.addEventListener('click', function() { var idx = parseInt(btn.dataset.idx); arr.splice(idx, 1); renderDxTags(arr, containerEl, tagsId); }); }); } window.formatDxForAI = function(selectedDxArr, freetextId) { var lines = []; selectedDxArr.forEach(function(dx) { lines.push((dx.code ? dx.code + ' ' : '') + dx.name); }); var freetextEl = freetextId ? document.getElementById(freetextId) : null; if (freetextEl && freetextEl.value.trim()) { lines.push(freetextEl.value.trim()); } return lines.length ? 'Diagnoses:\n' + lines.map(function(l) { return ' - ' + l; }).join('\n') : ''; }; // ── Well Visit Note — module state ───────────────────────────────────────── var _wvSelectedDx = []; // ── Well Visit Note Generation ─────────────────────────────────────────── var _wvRecorder = null; var _wvTimer = null; var _wvRecording = false; var _wvPaused = false; var _wvRecognition = null; var _wvTranscript = ''; function initWvRecording() { var recordBtn = document.getElementById('wv-record-btn'); var pauseBtn = document.getElementById('wv-pause-btn'); var indicator = document.getElementById('wv-rec-indicator'); var timerEl = document.getElementById('wv-timer'); var transcriptEl = document.getElementById('wv-transcript'); if (!recordBtn) return; _wvTimer = createTimer(timerEl); _wvRecognition = createSpeechRecognition(); if (_wvRecognition) { _wvRecognition.onresult = function(e) { var interim = ''; for (var i = e.resultIndex; i < e.results.length; i++) { if (e.results[i].isFinal) _wvTranscript += e.results[i][0].transcript + ' '; else interim = e.results[i][0].transcript; } if (transcriptEl) transcriptEl.innerHTML = _wvTranscript + (interim ? '' + interim + '' : ''); }; _wvRecognition.onend = function() { if (_wvRecording && !_wvPaused) try { _wvRecognition.start(); } catch(e) {} }; } recordBtn.addEventListener('click', function() { if (!_wvRecording) { _wvTranscript = ''; _wvRecorder = new AudioRecorder(); _wvRecorder.start().then(function() { _wvRecording = true; _wvPaused = false; recordBtn.innerHTML = ' Stop'; recordBtn.classList.add('recording'); if (pauseBtn) pauseBtn.classList.remove('hidden'); indicator.classList.remove('hidden'); _wvTimer.start(); if (_wvRecognition) try { _wvRecognition.start(); } catch(e) {} }).catch(function() { showToast('Microphone denied', 'error'); }); } else { _wvRecording = false; _wvPaused = false; var dur = _wvTimer.stop(); recordBtn.innerHTML = ' Listen In'; recordBtn.classList.remove('recording'); if (pauseBtn) { pauseBtn.classList.add('hidden'); } indicator.classList.add('hidden'); if (_wvRecognition) try { _wvRecognition.stop(); } catch(e) {} showBusy('Transcribing...'); _wvRecorder.stop().then(function(blob) { if (!blob || blob.size === 0) { hideBusy(); if (_wvTranscript.trim() && transcriptEl) transcriptEl.textContent = _wvTranscript.trim(); return; } return transcribeAudio(blob).then(function(data) { hideBusy(); if (data.success) { if (transcriptEl) transcriptEl.textContent = data.text; _wvTranscript = data.text; showToast('Transcribed ' + dur + 's', 'success'); } else { if (_wvTranscript.trim() && transcriptEl) transcriptEl.textContent = _wvTranscript.trim(); } }); }).catch(function() { hideBusy(); }); } }); if (pauseBtn) { pauseBtn.addEventListener('click', function() { if (!_wvRecording || !_wvRecorder || !_wvRecorder.mediaRecorder) return; if (!_wvPaused) { try { if (_wvRecorder.mediaRecorder.state === 'recording') _wvRecorder.mediaRecorder.pause(); } catch(e) {} _wvTimer.stop(); _wvPaused = true; pauseBtn.innerHTML = ' Resume'; if (_wvRecognition) try { _wvRecognition.stop(); } catch(e) {} } else { try { if (_wvRecorder.mediaRecorder.state === 'paused') { _wvRecorder.mediaRecorder.resume(); } else if (_wvRecorder.mediaRecorder.state === 'inactive' && _wvRecorder.stream && _wvRecorder.stream.active) { var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm'; _wvRecorder.mediaRecorder = new MediaRecorder(_wvRecorder.stream, { mimeType: mime, audioBitsPerSecond: 32000 }); _wvRecorder.mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) _wvRecorder.chunks.push(e.data); }; _wvRecorder.mediaRecorder.start(1000); } } catch(e) {} _wvTimer.resume(); _wvPaused = false; pauseBtn.innerHTML = ' Pause'; if (_wvRecognition) try { _wvRecognition.start(); } catch(e) {} } }); } } // ── ROS/PE init for well visit ──────────────────────────────────────────── function initWvRosPe() { var rosContainer = document.getElementById('wv-ros-container'); var peContainer = document.getElementById('wv-pe-container'); var dxContainer = document.getElementById('wv-dx-container'); var allWnlBtn = document.getElementById('wv-ros-all-wnl'); var allNormalBtn = document.getElementById('wv-pe-all-normal'); if (rosContainer) { rosContainer.innerHTML = window.renderRosRows(ROS_SYSTEMS, _rosData, 'ros', { wnl: 'WNL', abnormal: 'Abnormal', notrev: 'Not reviewed' }); window.wireRosContainer(rosContainer, _rosData); } if (peContainer) { peContainer.innerHTML = window.renderRosRows(PE_SYSTEMS, _peData, 'pe', { wnl: 'Normal', abnormal: 'Abnormal', notrev: 'Not examined' }); window.wireRosContainer(peContainer, _peData); } function refreshRos() { if (rosContainer) { rosContainer.innerHTML = window.renderRosRows(ROS_SYSTEMS, _rosData, 'ros', { wnl: 'WNL', abnormal: 'Abnormal', notrev: 'Not reviewed' }); window.wireRosContainer(rosContainer, _rosData); } } function refreshPe() { if (peContainer) { peContainer.innerHTML = window.renderRosRows(PE_SYSTEMS, _peData, 'pe', { wnl: 'Normal', abnormal: 'Abnormal', notrev: 'Not examined' }); window.wireRosContainer(peContainer, _peData); } } if (allWnlBtn) { allWnlBtn.onclick = function() { ROS_SYSTEMS.forEach(function(sys) { _rosData[sys.key + '_status'] = 'wnl'; }); refreshRos(); showToast('All ROS set to WNL', 'success'); }; } var rosClearBtn = document.getElementById('wv-ros-clear'); if (rosClearBtn) { rosClearBtn.onclick = function() { ROS_SYSTEMS.forEach(function(sys) { _rosData[sys.key + '_status'] = ''; _rosData[sys.key + '_note'] = ''; }); refreshRos(); showToast('ROS cleared', 'info'); }; } if (allNormalBtn) { allNormalBtn.onclick = function() { PE_SYSTEMS.forEach(function(sys) { _peData[sys.key + '_status'] = 'wnl'; }); refreshPe(); showToast('All PE set to Normal', 'success'); }; } var peClearBtn = document.getElementById('wv-pe-clear'); if (peClearBtn) { peClearBtn.onclick = function() { PE_SYSTEMS.forEach(function(sys) { _peData[sys.key + '_status'] = ''; _peData[sys.key + '_note'] = ''; }); refreshPe(); showToast('PE cleared', 'info'); }; } if (dxContainer) { window.renderDxComponent(dxContainer, _wvSelectedDx, 'wv'); } } function generateWvNote() { var age = document.getElementById('wv-note-age').value; var gender = document.getElementById('wv-note-gender').value; if (!age) { showToast('Enter patient age/visit age', 'error'); return; } var transcriptEl = document.getElementById('wv-transcript'); var transcript = transcriptEl ? (transcriptEl.innerText || transcriptEl.textContent || '') : ''; showBusy('Generating well visit note...'); var modelEl = document.querySelector('#wv-panel-note .tab-model-select'); // Format ROS, PE, DX var rosText = window.formatRosForAI(ROS_SYSTEMS, _rosData, 'Review of Systems'); var peText = window.formatRosForAI(PE_SYSTEMS, _peData, 'Physical Examination'); var dxText = window.formatDxForAI(_wvSelectedDx, 'wv-dx-freetext'); // Fetch user memories to include physician templates var memoriesPromise = (typeof getUserMemoryContext === 'function') ? getUserMemoryContext() : Promise.resolve(''); memoriesPromise.then(function(memCtx) { return fetch('/api/well-visit/note', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ patientAge: age, visitAge: age, patientGender: gender, vitals: document.getElementById('wv-vitals').value, transcript: transcript || null, screenings: document.getElementById('wv-screenings').value, vaccines: document.getElementById('wv-vaccines').value, shadessAssessment: document.getElementById('wv-shadess-text').value || null, ros: rosText || null, physicalExam: peText || null, diagnoses: dxText || null, physicianMemories: memCtx || null, noteStyle: document.getElementById('wv-note-style').value, model: modelEl ? modelEl.value : getSelectedModel() }) }); }) .then(function(r) { return r.json(); }) .then(function(data) { hideBusy(); if (data.success) { var noteEl = document.getElementById('wv-note-text'); var outCard = document.getElementById('wv-note-output'); var tag = document.getElementById('wv-note-model-tag'); if (noteEl) setOutputText(noteEl, data.note); if (typeof trackAIOutput === 'function') trackAIOutput('wv-note-text', data.note); // Store source for refine — collect all well visit inputs var wvSource = []; var wvTranscript = document.getElementById('wv-note-transcript'); if (wvTranscript && wvTranscript.innerText.trim()) wvSource.push('TRANSCRIPT: ' + wvTranscript.innerText.trim()); var wvAge = document.getElementById('wv-note-age'); if (wvAge && wvAge.value) wvSource.push('Age: ' + wvAge.value); storeSourceContext('wv-note-text', wvSource.join('\n')); if (tag) tag.textContent = (data.model || '').split('/').pop(); if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); } showToast('Well visit note generated!', 'success'); if (typeof suggestBillingCodes === 'function') suggestBillingCodes('wv-note-text', data.note, 'wellvisit', document.getElementById('wv-note-age').value); } else { showToast(data.error || 'Generation failed', 'error'); } }) .catch(function(err) { hideBusy(); showToast(err.message, 'error'); }); } // ── Wire subtab events ─────────────────────────────────────────────────── function initSubtabWiring() { var subtabBar = document.querySelector('.wv-subtab-bar'); if (!subtabBar) return; subtabBar.addEventListener('click', function(e) { var btn = e.target.closest('.wv-subtab-btn'); if (!btn) return; var subtab = btn.dataset.subtab; if (subtab === 'shadess' || subtab === 'note') { // These are handled here; wellVisit.js handles the others showWvSubpanel(subtab); } }); } function showWvSubpanel(name) { document.querySelectorAll('.wv-subpanel').forEach(function(p) { p.classList.add('hidden'); }); document.querySelectorAll('.wv-subtab-btn').forEach(function(b) { b.classList.remove('active'); }); var panel = document.getElementById('wv-panel-' + name); var btn = document.querySelector('.wv-subtab-btn[data-subtab="' + name + '"]'); if (panel) panel.classList.remove('hidden'); if (btn) btn.classList.add('active'); } // ── Save bar for well visit ────────────────────────────────────────────── document.addEventListener('click', function(e) { if (e.target.closest('#btn-shadess-generate')) generateShadess(); if (e.target.closest('#btn-wv-generate')) generateWvNote(); if (e.target.closest('#wv-refine-btn')) refineDocument('wv-note-text', 'wv-refine-input'); if (e.target.closest('#wv-shorten-btn')) shortenDocument('wv-note-text'); if (e.target.closest('#btn-wv-save')) { var labelEl = document.getElementById('wv-label'); var transcriptEl = document.getElementById('wv-transcript'); var noteEl = document.getElementById('wv-note-text'); if (typeof saveEncounter === 'function') { window.saveEncounter({ id: window._savedEncId_wellvisit || null, label: labelEl ? labelEl.value.trim() : '', enc_type: 'wellvisit', transcript: transcriptEl ? (transcriptEl.innerText || '') : '', generated_note: noteEl ? (noteEl.innerText || '') : '', onSaved: function(id) { window._savedEncId_wellvisit = id; } }); } } if (e.target.closest('#btn-shadess-new-patient')) resetShadessForm(); if (e.target.closest('#btn-wv-new-patient')) resetWvNote(); }); // Register well visit load handler if (typeof registerEncounterLoadHandler === 'function') { registerEncounterLoadHandler('wellvisit', function(enc) { var transcriptEl = document.getElementById('wv-transcript'); if (transcriptEl && enc.transcript) transcriptEl.textContent = enc.transcript; if (enc.generated_note) { var noteEl = document.getElementById('wv-note-text'); var outCard = document.getElementById('wv-note-output'); if (noteEl) noteEl.textContent = enc.generated_note; if (outCard) outCard.classList.remove('hidden'); } }); } // ── Reset functions ────────────────────────────────────────────────────── function resetShadessForm() { // Reset answers SHADESS_DOMAINS.forEach(function(d) { _shadessAnswers[d.key] = { questions: {}, comment: '', concern: false, skipped: false }; }); // Clear the DOM form initShadess(); // Clear age/gender inputs var ageEl = document.getElementById('shadess-age'); var genderEl = document.getElementById('shadess-gender'); if (ageEl) ageEl.value = ''; if (genderEl) genderEl.value = ''; // Clear transcript and hide output _shadessTranscript = ''; var outCard = document.getElementById('shadess-output'); var resultEl = document.getElementById('shadess-result-text'); if (outCard) outCard.classList.add('hidden'); if (resultEl) resultEl.textContent = ''; showToast('SSHADESS form cleared for new patient', 'info'); } function resetWvNote() { // Clear all well visit fields ['wv-note-age', 'wv-note-gender', 'wv-vitals', 'wv-screenings', 'wv-vaccines', 'wv-shadess-text', 'wv-label'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); var transcriptEl = document.getElementById('wv-transcript'); if (transcriptEl) transcriptEl.textContent = ''; _wvTranscript = ''; // Reset ROS/PE/DX _rosData = {}; _peData = {}; _wvSelectedDx.length = 0; initWvRosPe(); var outCard = document.getElementById('wv-note-output'); var noteEl = document.getElementById('wv-note-text'); if (outCard) outCard.classList.add('hidden'); if (noteEl) noteEl.textContent = ''; window._savedEncId_wellvisit = null; showToast('Well visit note cleared for new patient', 'info'); } function esc(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // ── Init on DOM ready ──────────────────────────────────────────────────── var _inited = false; document.addEventListener('tabChanged', function(e) { if (e.detail.tab !== 'wellvisit' || _inited) return; _inited = true; initShadess(); initShadessRecording(); initWvRecording(); initSubtabWiring(); initWvRosPe(); }); console.log('SHADESS module loaded'); })();