pediatric-ai-scribe-v3/public/js/shadess.js
Daniel b3d66caaca feat: a retried transcript goes back to the tab the audio came from
Retrying a kept recording transcribed it and put the text on the clipboard,
leaving you to find the right tab and paste. The app already had the answer: the
module was recorded with the audio. Retry now opens that tab and puts the text
in its transcript box.

Two things had to be true first.

The module was not actually being recorded. transcribeAudio never sent one, so
the server stored its default for every upload — all 28 rows in audio_backups
said "recording", and a retry had nowhere to send anything back to. Each
module's call now tags its own upload.

And the names disagreed. The recorders tagged 'encounter', 'soap', 'dictation'
while the recording-started events said 'enc', 'sick', 'dict'. One table now
holds the mapping and resolves the aliases, so the recorder that tags the
upload, the backup row that labels it and the retry that delivers it cannot
drift apart again.

Existing text is appended to, never replaced: a retry usually recovers
something on top of a live transcript, and overwriting would lose the words the
browser did hear. The box only exists once its tab's markup has been fetched, so
delivery polls briefly rather than guessing a delay, and falls back to the
clipboard if the tab never opens. An empty result says so rather than claiming
success. Backup rows now name their source and the button reads "Retry into
SOAP Note" instead of "Retry".

Verified in a browser: enc resolves to encounter, delivery switched tabs and
produced 'existing live transcript\n\nRECOVERED TEXT'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-11 12:39:50 +02:00

1048 lines
50 KiB
JavaScript

// ============================================================
// SHADESS.JS — SSHADESS psychosocial screening form + well visit note
// ============================================================
function setupShadessModule() {
// ── 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) {}
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 = '<i class="fas fa-play"></i> Resume';
if (_shhadessRecognition) try { _shhadessRecognition.stop(); } catch(e) {}
} else {
_shadessRecorder.resumeCapture();
_shadessTimer.resume();
_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;
}
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 '<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 (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 =
'<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 = esc(_wvTranscript) + (interim ? '<span style="color:#9ca3af;">' + esc(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) {}
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, 'wellvisit').then(function(data) {
hideBusy();
if (data.success && data.text) { if (transcriptEl) transcriptEl.textContent = data.text; _wvTranscript = data.text; showToast('Transcribed ' + dur + 's', 'success'); }
else if (data.success) { if (_wvTranscript.trim() && transcriptEl) transcriptEl.textContent = _wvTranscript.trim(); showToast('No speech detected in the recording', 'error'); }
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 = '<i class="fas fa-play"></i> Resume';
if (_wvRecognition) try { _wvRecognition.stop(); } catch(e) {}
} else {
_wvRecorder.resumeCapture();
_wvTimer.resume(); _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);
}
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);
// Store source for refine — collect all well visit inputs
var wvSource = [];
var wvTranscript = document.getElementById('wv-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 attachPatientEducation === 'function') attachPatientEducation('wv-note-text', { patientAge: document.getElementById('wv-note-age').value });
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, '&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');
}
setupShadessModule();