pediatric-ai-scribe-v3/public/js/liveEncounter.js
Daniel Onyejesi 5dd08f8da3 Feat: admin CMS, save/resume encounters, user templates, SSHADESS, well visit notes, sick visit, ROS/PE checklists, ICD-10 diagnoses
- Admin CMS: announcements, feature flags, email templates, AI prompts, SMTP, model management, reset-to-defaults
- Save/resume encounter progress with 7-day auto-expiry and patient labels
- Inline searchable load popover (replaces settings-modal redirect)
- User memory/template system (physical exam, ROS, encounter format, etc.)
- SSHADESS psychosocial screening form (8 domains, listen-in recording)
- Well visit note generation with vitals, vaccines, screenings, SSHADESS
- Interactive screenings/vaccines status buttons (Done/Refused/Already Given/N/A)
- ROS 15-system checklist + PE 17-system checklist in well visit note tab
- ICD-10 diagnoses tag picker (28 common pediatric codes + free text)
- Simple sick visit tab: chief complaint, inferred ROS (4) + PE (7), diagnoses, generate note
- Settings converted from modal to full-page tab
- Fix: /api/models moved before authenticated routes (was returning 401 → empty model selector)
- Fix: getUserMemoryContext always fetches from API regardless of cache state
- Pause/resume recording on all recording tabs
- Site-level SMTP config stored in DB; ElevenLabs TTS with browser fallback
2026-03-22 16:43:39 -04:00

155 lines
6.7 KiB
JavaScript

(function() {
var recordBtn = document.getElementById('enc-record-btn');
var pauseBtn = document.getElementById('enc-pause-btn');
var indicator = document.getElementById('enc-recording-indicator');
var timerEl = document.getElementById('enc-timer');
var transcript = document.getElementById('enc-transcript');
var clearBtn = document.getElementById('enc-clear');
var generateBtn = document.getElementById('enc-generate-btn');
var outputCard = document.getElementById('enc-output');
var hpiText = document.getElementById('enc-hpi-text');
var modelTag = document.getElementById('enc-model-tag');
var recorder = new AudioRecorder();
var timer = createTimer(timerEl);
var isRecording = false;
var isPaused = false;
var recognition = createSpeechRecognition();
var finalText = '';
if (recognition) {
recognition.onresult = function(e) {
var interim = '';
for (var i = e.resultIndex; i < e.results.length; i++) {
if (e.results[i].isFinal) finalText += e.results[i][0].transcript + ' ';
else interim = e.results[i][0].transcript;
}
transcript.innerHTML = finalText + (interim ? '<span style="color:#9ca3af;">' + interim + '</span>' : '');
};
recognition.onend = function() { if (isRecording && !isPaused) try { recognition.start(); } catch(e) {} };
recognition.onerror = function() {};
}
recordBtn.addEventListener('click', function() {
if (!isRecording) {
finalText = '';
recorder.start().then(function() {
isRecording = true;
isPaused = false;
recordBtn.classList.add('recording');
recordBtn.querySelector('span').textContent = 'Stop';
if (pauseBtn) pauseBtn.classList.remove('hidden');
indicator.classList.remove('hidden');
timer.start();
if (recognition) try { recognition.start(); } catch(e) {}
showToast('Recording started', 'info');
document.dispatchEvent(new CustomEvent('recording-started', { detail: { module: 'enc' } }));
}).catch(function() { showToast('Microphone denied', 'error'); });
} else {
isRecording = false;
isPaused = false;
var dur = timer.stop();
recordBtn.classList.remove('recording');
recordBtn.querySelector('span').textContent = 'Start Recording';
if (pauseBtn) { pauseBtn.classList.add('hidden'); pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause'; }
indicator.classList.add('hidden');
if (recognition) try { recognition.stop(); } catch(e) {}
document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'enc' } }));
showLoading('Transcribing...');
recorder.stop().then(function(blob) {
if (!blob || blob.size === 0) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); return; }
return transcribeAudio(blob).then(function(data) {
hideLoading();
if (data.success) { transcript.textContent = data.text; showToast('Transcribed ' + dur + 's', 'success'); }
else { if (finalText.trim()) transcript.textContent = finalText.trim(); showToast(data.error || 'Failed', 'error'); }
});
}).catch(function(err) { hideLoading(); if (finalText.trim()) transcript.textContent = finalText.trim(); showToast(err.message, 'error'); });
}
});
// Pause / Resume
if (pauseBtn) {
pauseBtn.addEventListener('click', function() {
if (!isRecording) return;
if (!isPaused) {
if (recorder.mediaRecorder && recorder.mediaRecorder.state === 'recording') {
recorder.mediaRecorder.pause();
timer.stop();
isPaused = true;
pauseBtn.innerHTML = '<i class="fas fa-play"></i> Resume';
pauseBtn.classList.add('btn-primary');
pauseBtn.classList.remove('btn-ghost');
if (recognition) try { recognition.stop(); } catch(e) {}
showToast('Recording paused', 'info');
}
} else {
if (recorder.mediaRecorder && recorder.mediaRecorder.state === 'paused') {
recorder.mediaRecorder.resume();
timer.start();
isPaused = false;
pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause';
pauseBtn.classList.remove('btn-primary');
pauseBtn.classList.add('btn-ghost');
if (recognition) try { recognition.start(); } catch(e) {}
showToast('Recording resumed', 'info');
}
}
});
}
clearBtn.addEventListener('click', function() {
transcript.textContent = ''; finalText = ''; outputCard.classList.add('hidden');
window._savedEncId_encounter = null;
var labelEl = document.getElementById('enc-label');
if (labelEl) labelEl.value = '';
});
generateBtn.addEventListener('click', function() {
var text = transcript.innerText.trim();
if (!text) { showToast('No transcript', 'error'); return; }
showLoading('Generating HPI...');
fetch('/api/generate-hpi-encounter', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
transcript: text,
patientAge: document.getElementById('enc-age').value,
patientGender: document.getElementById('enc-gender').value,
setting: document.getElementById('enc-setting').value,
model: getSelectedModel()
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
hideLoading();
if (data.success) {
hpiText.textContent = data.hpi;
modelTag.textContent = (data.model || '').split('/').pop();
outputCard.classList.remove('hidden');
outputCard.scrollIntoView({ behavior: 'smooth' });
showToast('HPI generated!', 'success');
} else showToast(data.error || 'Failed', 'error');
})
.catch(function(err) { hideLoading(); showToast(err.message, 'error'); });
});
// Refine & Shorten
document.getElementById('enc-refine-btn').addEventListener('click', function() { refineDocument('enc-hpi-text', 'enc-refine-input'); });
document.getElementById('enc-shorten-btn').addEventListener('click', function() { shortenDocument('enc-hpi-text'); });
// Register load handler for resuming saved encounters
if (typeof registerEncounterLoadHandler === 'function') {
registerEncounterLoadHandler('encounter', function(enc) {
if (enc.transcript) transcript.textContent = enc.transcript;
if (enc.generated_note) {
hpiText.textContent = enc.generated_note;
outputCard.classList.remove('hidden');
}
try { var pd = JSON.parse(enc.partial_data || '{}'); if (pd.age) document.getElementById('enc-age').value = pd.age; if (pd.gender) document.getElementById('enc-gender').value = pd.gender; } catch(e) {}
});
}
console.log('✅ Encounter module loaded');
})();