pediatric-ai-scribe-v3/public/js/shadess.js
Daniel Onyejesi 6c0c911dbd Fix module init: defer all tab scripts until component HTML is loaded
All JS modules were running immediately (IIFE/DOMContentLoaded) but
tab HTML is lazy-loaded from /components/*.html, causing null errors
on every addEventListener call. Each module now listens for tabChanged
with its tab name and initializes once after the DOM is ready.

Also: quiz question/explanation boxes changed to textarea for
multi-line support; quiz display uses sanitizeHtml() so formatted
text (bold, lists, etc.) renders correctly.
2026-03-23 22:03:54 -04:00

1004 lines
47 KiB
JavaScript

// ============================================================
// 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 '<div class="shadess-q-row">' +
'<span class="shadess-q-text">' + esc(q.text) + (q.concern_if !== undefined ? ' <span style="font-size:10px;color:var(--g400);">(flag if ' + (q.concern_if ? 'Yes' : 'No') + ')</span>' : '') + '</span>' +
'<div class="shadess-yn-btns">' +
'<select class="shadess-yn" data-domain="' + domain.key + '" data-qid="' + q.id + '" style="font-size:12px;padding:3px 6px;border:1px solid var(--g300);border-radius:6px;">' +
'<option value="">—</option><option value="yes">Yes</option><option value="no">No</option>' +
'</select>' +
'</div>' +
'</div>';
} else {
return '<div class="shadess-q-row">' +
'<span class="shadess-q-text">' + esc(q.text) + '</span>' +
'<input class="shadess-text" data-domain="' + domain.key + '" data-qid="' + q.id + '" type="text" placeholder="' + esc(q.placeholder || '') + '" style="font-size:12px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:160px;">' +
'</div>';
}
}).join('');
return '<div class="shadess-domain" style="border-left:3px solid ' + domain.color + ';">' +
'<div class="shadess-domain-header">' +
'<i class="fas ' + domain.icon + '" style="color:' + domain.color + ';width:16px;"></i>' +
'<strong style="font-size:14px;">' + esc(domain.label) + '</strong>' +
'<span style="font-size:11px;color:var(--g500);flex:1;margin-left:6px;">' + esc(domain.intro) + '</span>' +
'<span id="shadess-concern-' + domain.key + '" class="shadess-concern-badge hidden"><i class="fas fa-triangle-exclamation"></i> Concern</span>' +
'<button class="shadess-flag-btn btn-sm btn-ghost" data-domain="' + domain.key + '" style="font-size:11px;" title="Toggle concern flag"><i class="fas fa-flag"></i></button>' +
'<label style="font-size:11px;color:var(--g500);display:flex;align-items:center;gap:4px;"><input type="checkbox" class="shadess-skip" data-domain="' + domain.key + '"> Skip</label>' +
'</div>' +
'<div id="shadess-body-' + domain.key + '" class="shadess-domain-body">' +
qHtml +
'<div class="shadess-q-row" style="align-items:flex-start;">' +
'<span class="shadess-q-text" style="padding-top:4px;">Additional notes:</span>' +
'<textarea class="shadess-comment" data-domain="' + domain.key + '" rows="2" style="flex:1;font-size:12px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;" placeholder="Free text comments for this domain..."></textarea>' +
'</div>' +
'</div>' +
'</div>';
}
// ── 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 = '<i class="fas fa-stop"></i> 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 = '<i class="fas fa-microphone"></i> Listen In';
recordBtn.classList.remove('recording');
if (pauseBtn) { pauseBtn.classList.add('hidden'); }
indicator.classList.add('hidden');
if (_shhadessRecognition) try { _shhadessRecognition.stop(); } catch(e) {}
showLoading('Transcribing SSHADESS...');
_shadessRecorder.stop().then(function(blob) {
if (!blob || blob.size === 0) { hideLoading(); return; }
return transcribeAudio(blob).then(function(data) {
hideLoading();
if (data.success) {
_shadessTranscript = data.text;
showToast('SSHADESS audio transcribed (' + dur + 's)', 'success');
}
});
}).catch(function() { hideLoading(); });
}
});
if (pauseBtn) {
pauseBtn.addEventListener('click', function() {
if (!_shadessRecording || !_shadessRecorder || !_shadessRecorder.mediaRecorder) return;
if (!_shadessIsPaused) {
_shadessRecorder.mediaRecorder.pause();
_shadessTimer.stop();
_shadessIsPaused = true;
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
if (_shhadessRecognition) try { _shhadessRecognition.stop(); } catch(e) {}
} else {
_shadessRecorder.mediaRecorder.resume();
_shadessTimer.start();
_shadessIsPaused = false;
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> 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;
}
showLoading('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) {
hideLoading();
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) { hideLoading(); 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 '<div class="ros-system-row" data-sys-key="' + esc(sys.key) + '" data-prefix="' + prefix + '">' +
'<span class="ros-system-name" title="' + esc(sys.detail) + '">' + esc(sys.label) + ' <span style="font-size:10px;color:var(--g400);">(' + esc(sys.detail) + ')</span></span>' +
'<button class="ros-status-btn' + wnlAct + '" data-status="wnl" data-sys="' + esc(sys.key) + '" data-prefix="' + prefix + '">' + esc(bl.wnl) + '</button>' +
'<button class="ros-status-btn' + abnAct + '" data-status="abnormal" data-sys="' + esc(sys.key) + '" data-prefix="' + prefix + '">' + esc(bl.abnormal) + '</button>' +
'<button class="ros-status-btn' + notrevAct + '" data-status="notrev" data-sys="' + esc(sys.key) + '" data-prefix="' + prefix + '">' + esc(bl.notrev) + '</button>' +
'<input class="ros-note-input' + noteVis + '" data-sys="' + esc(sys.key) + '" data-prefix="' + prefix + '" type="text" placeholder="Describe..." value="' + esc(note) + '">' +
'</div>';
}).join('');
};
// Wire ROS/PE click events on a container element
window.wireRosContainer = function(containerEl, dataObj) {
containerEl.addEventListener('click', 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('input', 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;
});
};
// 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 =
'<div style="position:relative;margin-bottom:8px;">' +
'<div style="display:flex;gap:6px;align-items:center;">' +
'<div style="flex:1;position:relative;">' +
'<input id="' + searchId + '" type="text" placeholder="Search ICD-10 (e.g. &quot;otitis&quot;, &quot;J06&quot;)..." autocomplete="off" style="width:100%;font-size:13px;padding:7px 10px;border:1px solid var(--g300);border-radius:8px;font-family:inherit;box-sizing:border-box;">' +
'<div id="' + resultsId + '" class="dx-results hidden"></div>' +
'</div>' +
'</div>' +
'<div id="' + tagsId + '" style="display:flex;flex-wrap:wrap;gap:4px;margin-top:8px;min-height:24px;"></div>' +
'<div style="font-size:11px;color:var(--g500);margin:8px 0 4px;">Common pediatric diagnoses:</div>' +
'<div id="' + chipsId + '" style="display:flex;flex-wrap:wrap;gap:4px;"></div>' +
'</div>';
// Render quick-access chips
var chipsEl = containerEl.querySelector('#' + chipsId);
if (chipsEl) {
chipsEl.innerHTML = COMMON_DX.map(function(dx) {
return '<span class="dx-chip" data-code="' + esc(dx.code) + '" data-name="' + esc(dx.name) + '">' + esc(dx.code) + ' ' + esc(dx.name) + '</span>';
}).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 '<div class="dx-result-item" data-code="' + esc(dx.code) + '" data-name="' + esc(dx.name) + '">' +
'<span class="dx-result-code">' + esc(dx.code) + '</span> ' + esc(dx.name) +
'</div>';
}).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 '<span class="dx-tag">' +
(dx.code ? '<strong>' + esc(dx.code) + '</strong> ' : '') + esc(dx.name) +
'<span class="dx-remove" data-idx="' + i + '" title="Remove">&#x2715;</span>' +
'</span>';
}).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 ? '<span style="color:#9ca3af;">' + interim + '</span>' : '');
};
_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 = '<i class="fas fa-stop"></i> 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 = '<i class="fas fa-microphone"></i> Listen In';
recordBtn.classList.remove('recording');
if (pauseBtn) { pauseBtn.classList.add('hidden'); }
indicator.classList.add('hidden');
if (_wvRecognition) try { _wvRecognition.stop(); } catch(e) {}
showLoading('Transcribing...');
_wvRecorder.stop().then(function(blob) {
if (!blob || blob.size === 0) { hideLoading(); if (_wvTranscript.trim() && transcriptEl) transcriptEl.textContent = _wvTranscript.trim(); return; }
return transcribeAudio(blob).then(function(data) {
hideLoading();
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() { hideLoading(); });
}
});
if (pauseBtn) {
pauseBtn.addEventListener('click', function() {
if (!_wvRecording || !_wvRecorder || !_wvRecorder.mediaRecorder) return;
if (!_wvPaused) {
_wvRecorder.mediaRecorder.pause(); _wvTimer.stop(); _wvPaused = true;
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
if (_wvRecognition) try { _wvRecognition.stop(); } catch(e) {}
} else {
_wvRecorder.mediaRecorder.resume(); _wvTimer.start(); _wvPaused = false;
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> 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);
}
if (allWnlBtn) {
allWnlBtn.addEventListener('click', function() {
ROS_SYSTEMS.forEach(function(sys) { _rosData[sys.key + '_status'] = 'wnl'; });
if (rosContainer) {
rosContainer.innerHTML = window.renderRosRows(ROS_SYSTEMS, _rosData, 'ros', { wnl: 'WNL', abnormal: 'Abnormal', notrev: 'Not reviewed' });
window.wireRosContainer(rosContainer, _rosData);
}
showToast('All ROS set to WNL', 'success');
});
}
if (allNormalBtn) {
allNormalBtn.addEventListener('click', function() {
PE_SYSTEMS.forEach(function(sys) { _peData[sys.key + '_status'] = 'wnl'; });
if (peContainer) {
peContainer.innerHTML = window.renderRosRows(PE_SYSTEMS, _peData, 'pe', { wnl: 'Normal', abnormal: 'Abnormal', notrev: 'Not examined' });
window.wireRosContainer(peContainer, _peData);
}
showToast('All PE set to Normal', 'success');
});
}
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 || '') : '';
showLoading('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) {
hideLoading();
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 (tag) tag.textContent = (data.model || '').split('/').pop();
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
showToast('Well visit note generated!', 'success');
} else {
showToast(data.error || 'Generation failed', 'error');
}
})
.catch(function(err) { hideLoading(); 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ── 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');
})();