(function() { var _inited = false; document.addEventListener('tabChanged', function(e) { if (e.detail.tab !== 'encounter' || _inited) return; _inited = true; 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 stopBtn = document.getElementById('enc-stop-btn'); var recorder = new AudioRecorder(); recorder._module = 'encounter'; var timer = createTimer(timerEl); var isRecording = false; var isPaused = false; var recognition = createSpeechRecognition(); var finalText = ''; var sessionFinals = ''; function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); } if (recognition) { recognition.onresult = function(e) { var interim = ''; for (var i = e.resultIndex; i < e.results.length; i++) { if (e.results[i].isFinal) { var chunk = e.results[i][0].transcript + ' '; var deduped = deduplicateFinal(chunk, finalText + sessionFinals); sessionFinals += deduped; } else { interim = e.results[i][0].transcript; } } var combined = finalText + sessionFinals; transcript.innerHTML = escHtml(combined) + (interim ? '' + escHtml(interim) + '' : ''); }; recognition.onend = function() { finalText += sessionFinals; sessionFinals = ''; if (isRecording && !isPaused) try { recognition.start(); } catch(e) {} }; recognition.onerror = function(e) { if (e.error === 'no-speech' || e.error === 'aborted') return; console.warn('[SpeechRecognition] error:', e.error); }; } recordBtn.addEventListener('click', function() { if (!isRecording) { finalText = ''; sessionFinals = ''; recorder.start().then(function() { isRecording = true; isPaused = false; recordBtn.classList.add('recording'); recordBtn.style.display = 'none'; if (pauseBtn) pauseBtn.classList.remove('hidden'); if (stopBtn) stopBtn.classList.remove('hidden'); indicator.classList.remove('hidden'); timer.start(); if (recognition) try { recognition.start(); } catch(e) {} if (typeof nativeHaptic === 'function') nativeHaptic('heavy'); if (typeof nativeKeepAwake === 'function') nativeKeepAwake(true); if (typeof nativeStartRecordingService === 'function') nativeStartRecordingService(); 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.style.display = ''; recordBtn.querySelector('span').textContent = 'Start Recording'; if (pauseBtn) { pauseBtn.classList.add('hidden'); pauseBtn.innerHTML = ' Pause'; pauseBtn.classList.remove('btn-primary'); pauseBtn.classList.add('btn-ghost'); } if (stopBtn) stopBtn.classList.add('hidden'); indicator.classList.add('hidden'); if (recognition) try { recognition.stop(); } catch(e) {} if (typeof nativeHaptic === 'function') nativeHaptic('medium'); if (typeof nativeKeepAwake === 'function') nativeKeepAwake(false); if (typeof nativeStopRecordingService === 'function') nativeStopRecordingService(); document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'enc' } })); // If no server transcription API, use live transcript directly (no upload) var liveText = (finalText + sessionFinals).trim(); if (window._transcribeAvailable === false) { recorder.stop().then(function() {}); if (liveText) transcript.textContent = liveText; showToast('Using browser speech recognition (no transcription API configured)', 'info'); } else { showBusy('Transcribing...'); recorder.stop().then(function(blob) { if (!blob || blob.size === 0) { hideBusy(); if (liveText) transcript.textContent = liveText; return; } if (blob.size > 24 * 1024 * 1024) { hideBusy(); transcript.textContent = liveText; showToast('Recording too large for AI transcription — using live transcript', 'info'); return; } return transcribeAudio(blob).then(function(data) { hideBusy(); if (data.success) { transcript.textContent = data.text; showToast('Transcribed ' + dur + 's', 'success'); } else if (data.noProvider) { if (liveText) transcript.textContent = liveText; showToast('Using live transcript', 'info'); } else { if (liveText) transcript.textContent = liveText; showToast(data.error || 'Failed', 'error'); } }); }).catch(function(err) { hideBusy(); if (liveText) transcript.textContent = liveText; showToast(err.message, 'error'); }); } } }); // Dedicated stop button (always visible during recording) if (stopBtn) { stopBtn.addEventListener('click', function() { if (!isRecording) return; // Temporarily show recordBtn so .click() triggers the stop flow recordBtn.style.display = ''; recordBtn.click(); }); } // Pause / Resume if (pauseBtn) { pauseBtn.addEventListener('click', function() { if (!isRecording) return; if (!isPaused) { // Pause: try MediaRecorder.pause(), fall back to stop+accumulate if (recorder.mediaRecorder) { try { if (typeof recorder.mediaRecorder.pause === 'function' && recorder.mediaRecorder.state === 'recording') { recorder.mediaRecorder.pause(); } } catch(e) { console.warn('[Rec] Pause not supported:', e.message); } } timer.stop(); isPaused = true; pauseBtn.innerHTML = ' Resume'; pauseBtn.classList.add('btn-primary'); pauseBtn.classList.remove('btn-ghost'); if (recognition) try { recognition.stop(); } catch(e) {} showToast('Recording paused', 'info'); } else { // Resume: try MediaRecorder.resume(), fall back to just restarting recognition if (recorder.mediaRecorder) { try { if (typeof recorder.mediaRecorder.resume === 'function' && recorder.mediaRecorder.state === 'paused') { recorder.mediaRecorder.resume(); } else if (recorder.mediaRecorder.state === 'inactive') { // MediaRecorder was stopped (browser killed it) — restart it on the same stream if (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) { console.warn('[Rec] Resume error:', e.message); } } timer.resume(); isPaused = false; pauseBtn.innerHTML = ' 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 = ''; var refineEl = document.getElementById('enc-refine-input'); if (refineEl) refineEl.value = ''; }); generateBtn.addEventListener('click', function() { var text = transcript.innerText.trim(); if (!text) { showToast('No transcript', 'error'); return; } showBusy('Generating HPI...'); var memoriesPromise = (typeof getUserMemoryContext === 'function') ? getUserMemoryContext() : Promise.resolve(''); memoriesPromise.then(function(memCtx) { return 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, physicianMemories: memCtx || null, model: getSelectedModel() }) }); }) .then(function(r) { return r.json(); }) .then(function(data) { hideBusy(); if (data.success) { setOutputText(hpiText, data.hpi); storeSourceContext('enc-hpi-text', transcript.innerText.trim()); modelTag.textContent = (data.model || '').split('/').pop(); outputCard.classList.remove('hidden'); outputCard.scrollIntoView({ behavior: 'smooth' }); showToast('HPI generated!', 'success'); if (typeof suggestBillingCodes === 'function') suggestBillingCodes('enc-hpi-text', data.hpi, 'hpi', document.getElementById('enc-age').value, document.getElementById('enc-setting').value); if (typeof suggestDontMiss === 'function') suggestDontMiss('enc-hpi-text', data.hpi, 'hpi', document.getElementById('enc-age').value); } else showToast(data.error || 'Failed', 'error'); }) .catch(function(err) { hideBusy(); 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'); }); })();