// ============================================================ // ED ENCOUNTER TAB — multi-stage emergency note with per-stage // don't-miss tooltips and 2023 E/M MDM finalize. // // State model: // _state = { // stage: 1|2|..., // current stage number (what the recorder is for) // stages: [ // per-stage history — one entry per generated stage // { transcript, note, dontMiss, model, generatedAt } // ], // finalized: false, // finalNote: null, // consolidated final note from /finalize // mdm: null // } // // UX rules: // - Every generated stage stays on screen as its own editable card with // its own don't-miss panel. The physician can edit any stage at any // time; gatherCurrentNotes() reads the live DOM into _state.stages // before any operation that depends on the current text. // - The badge under the patient label reflects exactly where you are: // "Stage N (recording)" — advanced to stage N but no note yet // "Stage N" — stage N's note has been generated // "Finalized" — Save & Done done // - Save & Done sends every stage's current text to the server, which // consolidates them into one polished final note and then generates // MDM from that. Stages become read-only after finalize. // // Persistence: localStorage on every input (debounced) + auto-save // draft row to saved_encounters on stage transitions. // ============================================================ (function() { 'use strict'; var LS_KEY = 'ped_ed_draft_v1'; 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, finalNote: null, mdm: 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; 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 stageTextElId(idx) { return 'ed-stage-text-' + idx; } // Read the current contenteditable text from each rendered stage card // and write it back into _state.stages. Call before any operation // (advance, finalize, persist) that needs the latest user edits. function gatherCurrentNotes() { for (var i = 0; i < _state.stages.length; i++) { var el = document.getElementById(stageTextElId(i)); if (el) { var t = (el.innerText || el.textContent || '').trim(); if (t) _state.stages[i].note = t; } } } // ── Persistence ────────────────────────────────────────────────────── function persistLocal() { if (_saveTimer) clearTimeout(_saveTimer); _saveTimer = setTimeout(function() { try { gatherCurrentNotes(); var snap = { state: _state, label: getVal('ed-label'), age: getVal('ed-age'), gender: getVal('ed-gender'), cc: getVal('ed-cc'), currentTranscript: getText('ed-transcript') }; 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; // Backfill new fields if a snapshot pre-dates this version if (typeof _state.finalNote === 'undefined') _state.finalNote = null; 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 || ''); renderStages(); if (_state.finalNote) renderFinalNote(_state.finalNote); if (_state.finalized && _state.mdm) renderMdm(_state.mdm); updateBadge(); } catch (e) {} } function clearLocal() { try { localStorage.removeItem(LS_KEY); } catch (e) {} } // ── Stage rendering ────────────────────────────────────────────────── function renderStages() { var container = document.getElementById('ed-stages-container'); if (!container) return; container.innerHTML = ''; if (_state.stages.length === 0) { hideEl('ed-tail-controls'); return; } for (var i = 0; i < _state.stages.length; i++) { container.appendChild(buildStageCard(i, _state.stages[i])); } // Tail controls visible once at least one stage exists. // Hidden when finalized — encounter is locked. if (_state.finalized) hideEl('ed-tail-controls'); else showEl('ed-tail-controls'); } function buildStageCard(idx, stage) { var card = document.createElement('div'); card.className = 'card output-card stage-card'; card.dataset.stageIdx = String(idx); card.style.marginBottom = '10px'; var modelShort = (stage.model || '').split('/').pop(); var editable = _state.finalized ? 'false' : 'true'; var dontMissHtml = ''; if (stage.dontMiss && stage.dontMiss.length) { dontMissHtml = '
' + '
' + '' + 'Don\'t Miss — Stage ' + (idx + 1) + '
' + stage.dontMiss.map(function(it) { var why = it.why ? '
' + escHtml(it.why) + '
' : ''; return '
' + '' + escHtml(it.point) + why + '
'; }).join('') + '
'; } card.innerHTML = '
' + '

' + ' Stage ' + (idx + 1) + ' Note' + (modelShort ? ' ' + escHtml(modelShort) + '' : '') + '

' + '
' + '' + '
' + '
' + '
' + dontMissHtml; // Set the note text via textContent so contenteditable shows the correct content // (and so any HTML in the model output is treated as plain text, not parsed). var textEl = card.querySelector('#' + stageTextElId(idx)); if (textEl) textEl.textContent = stage.note || ''; return card; } function renderFinalNote(note) { var container = document.getElementById('ed-final-note-card'); if (!container) { // Create the final-note card lazily and insert before the MDM card container = document.createElement('div'); container.id = 'ed-final-note-card'; container.className = 'card output-card'; container.style.cssText = 'margin-top:10px;border-left:3px solid #2563eb;'; var mdm = document.getElementById('ed-mdm-card'); if (mdm && mdm.parentNode) mdm.parentNode.insertBefore(container, mdm); else document.body.appendChild(container); } container.innerHTML = '
' + '

Final Consolidated Note

' + '
' + '' + '
' + '
' + '
'; var t = container.querySelector('#ed-final-note-text'); if (t) t.textContent = note; container.classList.remove('hidden'); container.scrollIntoView({ behavior: 'smooth' }); } function renderMdm(mdm) { var card = document.getElementById('ed-mdm-card'); var text = document.getElementById('ed-mdm-text'); var tag = document.getElementById('ed-mdm-level-tag'); if (!card || !text || !mdm) return; if (tag) tag.textContent = mdm.suggestedLevel || ''; text.innerHTML = '
Problems Addressed (' + escHtml(mdm.problemsAddressed) + ')
' + escHtml(mdm.problemsNarrative) + '
' + '
Data Reviewed (' + escHtml(mdm.dataReviewed) + ')
' + escHtml(mdm.dataNarrative) + '
' + '
Risk (' + escHtml(mdm.risk) + ')
' + escHtml(mdm.riskNarrative) + '
' + '
Suggested Level: ' + escHtml(mdm.suggestedLevel) + '
' + '' + escHtml(mdm.levelRationale) + '
' + '

Suggestion only. Verify against your institution\'s coding guidelines.

'; card.classList.remove('hidden'); } // Badge accurately reflects state. // - "Stage N (recording)" — advanced past last generation; no note yet for stage N // - "Stage N" — stage N's note has been generated // - "Finalized" — Save & Done complete function updateBadge() { var badge = document.getElementById('ed-stage-badge'); var rec = document.getElementById('ed-rec-stage-num'); var gen = document.getElementById('ed-gen-stage-num'); if (rec) rec.textContent = String(_state.stage); if (gen) gen.textContent = String(_state.stage); if (!badge) return; if (_state.finalized) { badge.textContent = 'Finalized'; badge.style.background = '#d1fae5'; badge.style.color = '#047857'; return; } var generatedHere = _state.stages.length >= _state.stage; if (generatedHere) { badge.textContent = 'Stage ' + _state.stage; badge.style.background = 'var(--g100)'; badge.style.color = 'var(--g600)'; } else { badge.textContent = 'Stage ' + _state.stage + ' (recording)'; badge.style.background = '#fef3c7'; badge.style.color = '#92400e'; } } // ── 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() { if (_state.finalized) { showToast('Encounter is finalized — start a new one', 'error'); return; } 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; } // Capture any in-place edits to earlier stages so the AI sees them as the // baseline for the new stage. gatherCurrentNotes(); 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; } // Place this stage at index (_state.stage - 1). If the user is regenerating // an existing stage (shouldn't happen via UI, but defensively), overwrite. var idx = _state.stage - 1; _state.stages[idx] = { transcript: transcript, note: data.note || '', dontMiss: data.dontMiss || [], model: data.model || '', generatedAt: new Date().toISOString() }; renderStages(); updateBadge(); persistLocal(); autoSaveDraft(); // Scroll to the newly added card var card = document.querySelector('.stage-card[data-stage-idx="' + idx + '"]'); if (card) card.scrollIntoView({ behavior: 'smooth', block: 'start' }); showToast('Stage ' + _state.stage + ' generated. Edit, add more, or Save & Done.', 'success'); }) .catch(function(err) { hideBusy(); showToast(err.message, 'error'); }); } // ── Add another stage — DOES NOT change displayed cards ───────────── function advanceStage() { if (_state.finalized) { showToast('Encounter is finalized', 'error'); return; } if (_state.stages.length < _state.stage) { showToast('Generate the current stage before advancing', 'error'); return; } gatherCurrentNotes(); _state.stage += 1; setText('ed-transcript', ''); _liveTranscript = ''; updateBadge(); persistLocal(); showToast('Ready for Stage ' + _state.stage + ' — record or type additional findings.', 'info'); var rec = document.getElementById('ed-record-btn'); if (rec) rec.scrollIntoView({ behavior: 'smooth', block: 'center' }); } // ── Finalize → consolidate + MDM + persist as final ────────────────── function finalize() { if (_state.finalized) { showToast('Already finalized', 'info'); return; } if (_state.stages.length === 0) { showToast('Generate at least one stage first', 'error'); return; } var label = getVal('ed-label'); if (!label.trim()) { showToast('Enter a patient label before finalizing', 'error'); return; } gatherCurrentNotes(); var modelEl = document.getElementById('ed-model-select'); var selectedModel = modelEl ? modelEl.value : ''; showBusy('Consolidating note + generating MDM...'); fetch('/api/ed-encounters/finalize', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ stages: _state.stages.map(function(s) { return { transcript: s.transcript || '', note: s.note || '' }; }), chiefComplaint: getVal('ed-cc'), patientAge: getVal('ed-age'), patientGender: getVal('ed-gender'), model: selectedModel || undefined }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (!data.success) { hideBusy(); showToast(data.error || 'Finalize failed', 'error'); return; } _state.finalNote = data.finalNote || ''; _state.mdm = data.mdm; _state.finalized = true; renderStages(); // re-render with read-only stage cards renderFinalNote(_state.finalNote); renderMdm(_state.mdm); updateBadge(); var fullTranscript = _state.stages.map(function(s, i) { return '--- Stage ' + (i + 1) + ' ---\n' + (s.transcript || ''); }).join('\n\n'); var partial = { stages: _state.stages, finalNote: _state.finalNote, mdm: _state.mdm, finalized: true }; if (typeof saveEncounter === 'function') { saveEncounter({ id: window._savedEncId_ed || null, label: label, enc_type: 'ed', transcript: fullTranscript, generated_note: composeFinalNoteForSave(_state.finalNote, _state.mdm), partial_data: partial, status: 'final', idempotency_key: 'ed-final-' + Date.now(), onSaved: function(id) { window._savedEncId_ed = id; try { sessionStorage.setItem('_savedEncId_ed', id); } catch(e) {} clearLocal(); hideBusy(); showToast('Encounter finalized and saved.', 'success'); } }); } else { hideBusy(); } }) .catch(function(err) { hideBusy(); showToast(err.message, 'error'); }); } function composeFinalNoteForSave(noteText, mdm) { if (!mdm) return noteText; return [ noteText, '', 'Medical Decision Making (2023 E/M):', 'Problems Addressed (' + (mdm.problemsAddressed || '') + '): ' + (mdm.problemsNarrative || ''), 'Data Reviewed (' + (mdm.dataReviewed || '') + '): ' + (mdm.dataNarrative || ''), 'Risk (' + (mdm.risk || '') + '): ' + (mdm.riskNarrative || ''), 'Suggested Level: ' + (mdm.suggestedLevel || '') + ' — ' + (mdm.levelRationale || '') ].join('\n'); } // ── Save draft (manual / auto on stage transition) ────────────────── function autoSaveDraft() { var label = getVal('ed-label'); if (!label.trim()) return; if (typeof saveEncounter !== 'function') return; gatherCurrentNotes(); var fullTranscript = _state.stages.map(function(s) { return s.transcript || ''; }).join('\n\n'); var partial = { stages: _state.stages, finalized: false }; var lastNote = _state.stages.length ? _state.stages[_state.stages.length - 1].note : ''; saveEncounter({ id: window._savedEncId_ed || null, label: label, enc_type: 'ed', transcript: fullTranscript, generated_note: lastNote, 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', ''); _liveTranscript = ''; var container = document.getElementById('ed-stages-container'); if (container) container.innerHTML = ''; var finalCard = document.getElementById('ed-final-note-card'); if (finalCard) finalCard.remove(); hideEl('ed-tail-controls'); hideEl('ed-mdm-card'); window._savedEncId_ed = null; try { sessionStorage.removeItem('_savedEncId_ed'); } catch(e) {} clearLocal(); updateBadge(); showToast('ED encounter cleared for new patient', 'info'); } // ── Refine the latest stage ────────────────────────────────────────── function refineLatestStage() { if (_state.finalized) { showToast('Encounter is finalized', 'error'); return; } if (_state.stages.length === 0) { showToast('Generate a stage first', 'error'); return; } var idx = _state.stages.length - 1; var elId = stageTextElId(idx); if (typeof refineDocument === 'function') refineDocument(elId, 'ed-refine-input'); } function shortenLatestStage() { if (_state.finalized) { showToast('Encounter is finalized', 'error'); return; } if (_state.stages.length === 0) { showToast('Generate a stage first', 'error'); return; } var idx = _state.stages.length - 1; var elId = stageTextElId(idx); if (typeof shortenDocument === 'function') shortenDocument(elId); } // ── 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')) refineLatestStage(); if (e.target.closest('#ed-shorten-btn')) shortenLatestStage(); }); // Persist on input — capture stage edits via gatherCurrentNotes inside persistLocal document.addEventListener('input', function(e) { if (!e.target) return; var t = e.target; if (t.id === 'ed-label' || t.id === 'ed-age' || t.id === 'ed-gender' || t.id === 'ed-cc' || t.id === 'ed-transcript') { persistLocal(); return; } if (t.id && t.id.indexOf('ed-stage-text-') === 0) { persistLocal(); } }); // ── Load handler (resume from saved encounters list) ──────────────── if (typeof registerEncounterLoadHandler === 'function') { registerEncounterLoadHandler('ed', function(enc) { _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.finalNote = partial.finalNote || null; _state.mdm = partial.mdm || null; } renderStages(); if (_state.finalNote) renderFinalNote(_state.finalNote); if (_state.mdm) renderMdm(_state.mdm); if (enc.transcript && _state.stages.length === 0) setText('ed-transcript', enc.transcript); updateBadge(); }); } // ── Init on tab activation ─────────────────────────────────────────── var _inited = false; document.addEventListener('tabChanged', function(e) { if (e.detail.tab !== 'ed' || _inited) return; _inited = true; initRecording(); loadLocal(); updateBadge(); }); console.log('ED encounters module loaded'); })();