// ============================================================ // ED ENCOUNTER TAB — multi-stage emergency note with don't-miss // tooltips and 2023 E/M MDM finalize. // // State model: // _state = { // stage: 1|2|..., // current stage number // stages: [ // per-stage history // { transcript, note, dontMiss, model, generatedAt } // ], // finalized: false, // mdm: null // } // // Persistence: localStorage on every mutation (debounced) + auto-save // to saved_encounters DB row (status='draft') on stage transitions. // "Save & Done" generates MDM, marks status='final', clears localStorage. // // Sacred-file note: this file owns ALL ED logic. encounters.js gets // a 2-string touch only (sessionStorage array + tabMap). // ============================================================ (function() { 'use strict'; var LS_KEY = 'ped_ed_draft_v1'; var AUTOSAVE_MS = 1500; var _state = freshState(); var _saveTimer = null; var _recorder = null; var _timer = null; var _recording = false; var _paused = false; var _recognition = null; var _liveTranscript = ''; function freshState() { return { stage: 1, stages: [], finalized: false, mdm: null }; } // ── Persistence ────────────────────────────────────────────────────── function persistLocal() { if (_saveTimer) clearTimeout(_saveTimer); _saveTimer = setTimeout(function() { try { var snap = { state: _state, label: getVal('ed-label'), age: getVal('ed-age'), gender: getVal('ed-gender'), cc: getVal('ed-cc'), currentTranscript: getText('ed-transcript'), editedNote: getText('ed-note-text') }; localStorage.setItem(LS_KEY, JSON.stringify(snap)); } catch (e) {} }, 300); } function loadLocal() { try { var raw = localStorage.getItem(LS_KEY); if (!raw) return; var snap = JSON.parse(raw); if (!snap || !snap.state) return; _state = snap.state; setVal('ed-label', snap.label || ''); setVal('ed-age', snap.age || ''); setVal('ed-gender', snap.gender || ''); setVal('ed-cc', snap.cc || ''); setText('ed-transcript', snap.currentTranscript || ''); // Render last generated stage if present var last = lastStage(); if (last) { setText('ed-note-text', snap.editedNote || last.note || ''); renderDontMiss(last.dontMiss || []); showOutput(); updateStageTags(); } if (_state.finalized && _state.mdm) renderMdm(_state.mdm); } catch (e) {} } function clearLocal() { try { localStorage.removeItem(LS_KEY); } catch (e) {} } function lastStage() { return _state.stages.length ? _state.stages[_state.stages.length - 1] : null; } // ── DOM helpers ────────────────────────────────────────────────────── function getVal(id) { var el = document.getElementById(id); return el ? el.value : ''; } function setVal(id, v) { var el = document.getElementById(id); if (el) el.value = v; } function getText(id) { var el = document.getElementById(id); if (!el) return ''; return (el.innerText || el.textContent || '').trim(); } function setText(id, v) { var el = document.getElementById(id); if (!el) return; if (typeof setOutputText === 'function' && id === 'ed-note-text') setOutputText(el, v); else el.textContent = v; } function showEl(id) { var el = document.getElementById(id); if (el) el.classList.remove('hidden'); } function hideEl(id) { var el = document.getElementById(id); if (el) el.classList.add('hidden'); } function escHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function updateStageTags() { var s = _state.stage; var badge = document.getElementById('ed-stage-badge'); var rec = document.getElementById('ed-rec-stage-num'); var gen = document.getElementById('ed-gen-stage-num'); var tag = document.getElementById('ed-note-stage-tag'); if (badge) badge.textContent = _state.finalized ? 'Finalized' : 'Stage ' + s; if (rec) rec.textContent = String(s); if (gen) gen.textContent = String(s); if (tag) tag.textContent = 'Stage ' + s; } function showOutput() { var card = document.getElementById('ed-note-output'); if (card) { card.classList.remove('hidden'); card.scrollIntoView({ behavior: 'smooth' }); } } // ── Recording ──────────────────────────────────────────────────────── function initRecording() { var recordBtn = document.getElementById('ed-record-btn'); var pauseBtn = document.getElementById('ed-pause-btn'); var indicator = document.getElementById('ed-rec-indicator'); var timerEl = document.getElementById('ed-timer'); var transcriptEl = document.getElementById('ed-transcript'); if (!recordBtn) return; _timer = createTimer(timerEl); _recognition = createSpeechRecognition(); if (_recognition) { _recognition.onresult = function(e) { var interim = ''; for (var i = e.resultIndex; i < e.results.length; i++) { if (e.results[i].isFinal) _liveTranscript += e.results[i][0].transcript + ' '; else interim = e.results[i][0].transcript; } if (transcriptEl) transcriptEl.innerHTML = _liveTranscript + (interim ? '' + interim + '' : ''); }; _recognition.onend = function() { if (_recording && !_paused) try { _recognition.start(); } catch(e) {} }; } recordBtn.addEventListener('click', function() { if (!_recording) { _liveTranscript = ''; _recorder = new AudioRecorder(); _recorder.start().then(function() { _recording = true; _paused = false; recordBtn.innerHTML = ' Stop'; recordBtn.classList.add('recording'); if (pauseBtn) pauseBtn.classList.remove('hidden'); indicator.classList.remove('hidden'); _timer.start(); if (_recognition) try { _recognition.start(); } catch(e) {} showToast('ED recording started — Stage ' + _state.stage, 'info'); document.dispatchEvent(new CustomEvent('recording-started', { detail: { module: 'ed' } })); }).catch(function() { showToast('Microphone denied', 'error'); }); } else { _recording = false; _paused = false; var dur = _timer.stop(); recordBtn.innerHTML = ' Listen In'; recordBtn.classList.remove('recording'); if (pauseBtn) pauseBtn.classList.add('hidden'); indicator.classList.add('hidden'); if (_recognition) try { _recognition.stop(); } catch(e) {} document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'ed' } })); showBusy('Transcribing...'); _recorder.stop().then(function(blob) { if (!blob || blob.size === 0) { hideBusy(); if (_liveTranscript.trim() && transcriptEl) transcriptEl.textContent = _liveTranscript.trim(); persistLocal(); return; } return transcribeAudio(blob).then(function(data) { hideBusy(); if (data && data.success) { if (transcriptEl) transcriptEl.textContent = data.text; showToast('Transcribed ' + dur + 's', 'success'); } else if (_liveTranscript.trim() && transcriptEl) { transcriptEl.textContent = _liveTranscript.trim(); } persistLocal(); }); }).catch(function() { hideBusy(); }); } }); if (pauseBtn) { pauseBtn.addEventListener('click', function() { if (!_recording || !_recorder || !_recorder.mediaRecorder) return; if (!_paused) { try { if (_recorder.mediaRecorder.state === 'recording') _recorder.mediaRecorder.pause(); } catch(e) {} _timer.stop(); _paused = true; pauseBtn.innerHTML = ' Resume'; if (_recognition) try { _recognition.stop(); } catch(e) {} } else { try { if (_recorder.mediaRecorder.state === 'paused') { _recorder.mediaRecorder.resume(); } else if (_recorder.mediaRecorder.state === 'inactive' && _recorder.stream && _recorder.stream.active) { var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm'; _recorder.mediaRecorder = new MediaRecorder(_recorder.stream, { mimeType: mime, audioBitsPerSecond: 32000 }); _recorder.mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) _recorder.chunks.push(e.data); }; _recorder.mediaRecorder.start(1000); } } catch(e) {} _timer.resume(); _paused = false; pauseBtn.innerHTML = ' Pause'; if (_recognition) try { _recognition.start(); } catch(e) {} } }); } } // ── Generate (per-stage) ───────────────────────────────────────────── function generateStage() { var cc = getVal('ed-cc'); if (!cc.trim()) { showToast('Enter a chief complaint first', 'error'); return; } var transcript = getText('ed-transcript'); if (!transcript) { showToast('No transcript — record or type the dictation first', 'error'); return; } var modelEl = document.getElementById('ed-model-select'); var selectedModel = modelEl ? modelEl.value : ''; showBusy('Generating ED note (Stage ' + _state.stage + ')...'); var memoriesPromise = (typeof getUserMemoryContext === 'function') ? getUserMemoryContext() : Promise.resolve(''); memoriesPromise.then(function(memCtx) { var prevNote = (_state.stages.length > 0) ? _state.stages[_state.stages.length - 1].note : ''; return fetch('/api/ed-encounters/generate', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ stage: _state.stage, transcript: transcript, chiefComplaint: cc, patientAge: getVal('ed-age'), patientGender: getVal('ed-gender'), previousNote: prevNote || undefined, physicianMemories: memCtx || undefined, model: selectedModel || undefined }) }); }) .then(function(r) { return r.json(); }) .then(function(data) { hideBusy(); if (!data.success) { showToast(data.error || 'Generation failed', 'error'); return; } _state.stages.push({ transcript: transcript, note: data.note || '', dontMiss: data.dontMiss || [], model: data.model || '', generatedAt: new Date().toISOString() }); setText('ed-note-text', data.note || ''); var modelTag = document.getElementById('ed-note-model-tag'); if (modelTag) modelTag.textContent = (data.model || '').split('/').pop(); renderDontMiss(data.dontMiss || []); updateStageTags(); showOutput(); persistLocal(); autoSaveDraft(); // best-effort DB draft save on stage completion showToast('Stage ' + _state.stage + ' generated. Save & Done, or Add more.', 'success'); }) .catch(function(err) { hideBusy(); showToast(err.message, 'error'); }); } // ── Don't-miss rendering ───────────────────────────────────────────── function renderDontMiss(items) { var card = document.getElementById('ed-dontmiss-card'); var list = document.getElementById('ed-dontmiss-list'); if (!card || !list) return; if (!items || !items.length) { card.classList.add('hidden'); list.innerHTML = ''; return; } list.innerHTML = items.map(function(it) { var why = it.why ? '
Suggestion only. Verify against your institution\'s coding guidelines.
'; text.innerHTML = html; card.classList.remove('hidden'); card.scrollIntoView({ behavior: 'smooth' }); } // ── Save draft (manual / auto on stage advance) ────────────────────── function autoSaveDraft() { var label = getVal('ed-label'); if (!label.trim()) return; // need a label to save; skip silently if (typeof saveEncounter !== 'function') return; var fullTranscript = _state.stages.map(function(s) { return s.transcript || ''; }).join('\n\n'); var partial = { stages: _state.stages, finalized: false }; saveEncounter({ id: window._savedEncId_ed || null, label: label, enc_type: 'ed', transcript: fullTranscript, generated_note: lastStage() ? lastStage().note : '', partial_data: partial, status: 'draft', idempotency_key: 'ed-draft-' + (window._savedEncId_ed || 'new'), onSaved: function(id) { window._savedEncId_ed = id; try { sessionStorage.setItem('_savedEncId_ed', id); } catch(e) {} } }); } function manualSaveDraft() { if (!getVal('ed-label').trim()) { showToast('Enter a patient label first', 'error'); return; } autoSaveDraft(); showToast('Draft saved', 'success'); } // ── Reset / new patient ────────────────────────────────────────────── function resetEncounter() { _state = freshState(); setVal('ed-label', ''); setVal('ed-age', ''); setVal('ed-gender', ''); setVal('ed-cc', ''); setText('ed-transcript', ''); setText('ed-note-text', ''); _liveTranscript = ''; hideEl('ed-note-output'); hideEl('ed-dontmiss-card'); hideEl('ed-mdm-card'); var modelTag = document.getElementById('ed-note-model-tag'); if (modelTag) modelTag.textContent = ''; window._savedEncId_ed = null; try { sessionStorage.removeItem('_savedEncId_ed'); } catch(e) {} clearLocal(); updateStageTags(); showToast('ED encounter cleared for new patient', 'info'); } // ── Wire click handlers ────────────────────────────────────────────── document.addEventListener('click', function(e) { if (e.target.closest('#btn-ed-generate')) generateStage(); if (e.target.closest('#btn-ed-add-more')) advanceStage(); if (e.target.closest('#btn-ed-finalize')) finalize(); if (e.target.closest('#btn-ed-new')) resetEncounter(); if (e.target.closest('#btn-ed-save')) manualSaveDraft(); if (e.target.closest('#ed-refine-btn')) refineDocument('ed-note-text', 'ed-refine-input'); if (e.target.closest('#ed-shorten-btn')) shortenDocument('ed-note-text'); }); // Persist on input changes document.addEventListener('input', function(e) { if (!e.target) return; var id = e.target.id; if (id === 'ed-label' || id === 'ed-age' || id === 'ed-gender' || id === 'ed-cc' || id === 'ed-transcript' || id === 'ed-note-text') { persistLocal(); } }); // ── Load handler (resume from saved encounters list) ──────────────── if (typeof registerEncounterLoadHandler === 'function') { registerEncounterLoadHandler('ed', function(enc) { // Reset and restore from the row _state = freshState(); setVal('ed-label', enc.label || ''); var partial = enc.partial_data; try { if (typeof partial === 'string') partial = JSON.parse(partial); } catch (e) { partial = null; } if (partial && partial.stages) { _state.stages = partial.stages; _state.stage = partial.stages.length || 1; _state.finalized = !!partial.finalized; _state.mdm = partial.mdm || null; var last = lastStage(); if (last) { setText('ed-note-text', last.note || ''); renderDontMiss(last.dontMiss || []); showOutput(); } if (_state.mdm) renderMdm(_state.mdm); } else if (enc.generated_note) { setText('ed-note-text', enc.generated_note); showOutput(); } if (enc.transcript) setText('ed-transcript', enc.transcript); updateStageTags(); }); } // ── Init on tab activation ─────────────────────────────────────────── var _inited = false; document.addEventListener('tabChanged', function(e) { if (e.detail.tab !== 'ed' || _inited) return; _inited = true; initRecording(); loadLocal(); updateStageTags(); }); console.log('ED encounters module loaded'); })();