pediatric-ai-scribe-v3/public/js/soap.js
Daniel 5bf55499a4 Add pause/stop buttons to SOAP note recording
- Add Pause and Stop buttons (hidden until recording starts)
- Record button hides during recording (same pattern as encounter)
- Pause: suspends MediaRecorder + speech recognition, shows Resume
- Resume: handles MediaRecorder state recovery if browser killed it
- Stop: triggers the record button's stop flow
- Native mobile: haptic feedback + keep-awake + foreground service
- Recognition respects pause state (doesn't restart during pause)
2026-04-11 05:19:12 +02:00

226 lines
9.9 KiB
JavaScript

(function() {
var _inited = false;
document.addEventListener('tabChanged', function(e) {
if (e.detail.tab !== 'soap' || _inited) return;
_inited = true;
var recordBtn = document.getElementById('soap-record-btn');
var indicator = document.getElementById('soap-recording-indicator');
var timerEl = document.getElementById('soap-timer');
var transcript = document.getElementById('soap-transcript');
var clearBtn = document.getElementById('soap-clear');
var generateBtn = document.getElementById('soap-generate-btn');
var outputCard = document.getElementById('soap-output');
var soapText = document.getElementById('soap-text');
var modelTag = document.getElementById('soap-model-tag');
var pauseBtn = document.getElementById('soap-pause-btn');
var stopBtn = document.getElementById('soap-stop-btn');
var recorder = new AudioRecorder();
recorder._module = 'soap';
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
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 ? '<span style="color:#9ca3af;">' + escHtml(interim) + '</span>' : '');
};
recognition.onend = function() {
finalText += sessionFinals;
sessionFinals = '';
if (isRecording && !isPaused) try { recognition.start(); } catch(e) {}
};
}
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');
}).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 = 'Dictate';
if (pauseBtn) { pauseBtn.classList.add('hidden'); pauseBtn.innerHTML = '<i class="fas fa-pause"></i> 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();
var liveText = (finalText + sessionFinals).trim();
if (window._transcribeAvailable === false) {
recorder.stop().then(function() {});
if (liveText) transcript.textContent = liveText;
showToast('Using browser speech recognition', 'info');
} else {
showBusy('Transcribing...');
recorder.stop().then(function(blob) {
if (!blob) { 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;
else if (data.noProvider) { if (liveText) transcript.textContent = liveText; showToast('Using live transcript', 'info'); }
else { if (liveText) transcript.textContent = liveText; showToast(data.error || 'Transcription failed — using live transcript', 'error'); }
});
}).catch(function(err) { hideBusy(); if (liveText) transcript.textContent = liveText; showToast('Transcription error: ' + (err.message || 'unknown'), 'error'); });
}
}
});
// Dedicated stop button
if (stopBtn) {
stopBtn.addEventListener('click', function() {
if (!isRecording) return;
recordBtn.style.display = '';
recordBtn.click();
});
}
// Pause / Resume
if (pauseBtn) {
pauseBtn.addEventListener('click', function() {
if (!isRecording) return;
if (!isPaused) {
try { if (recorder.mediaRecorder && recorder.mediaRecorder.state === 'recording') recorder.mediaRecorder.pause(); } catch(e) {}
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('Paused', 'info');
} else {
try {
if (recorder.mediaRecorder && recorder.mediaRecorder.state === 'paused') { recorder.mediaRecorder.resume(); }
else if (recorder.mediaRecorder && 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();
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('Resumed', 'info');
}
});
}
clearBtn.addEventListener('click', function() {
transcript.textContent = ''; finalText = ''; outputCard.classList.add('hidden');
var instrEl = document.getElementById('soap-instructions');
if (instrEl) instrEl.value = '';
window._savedEncId_soap = null;
var labelEl = document.getElementById('soap-label');
if (labelEl) labelEl.value = '';
});
generateBtn.addEventListener('click', function() {
var text = transcript.innerText.trim();
if (!text) { showToast('No input', 'error'); return; }
showBusy('Generating SOAP note...');
var memoriesPromise = (typeof getUserMemoryContext === 'function') ? getUserMemoryContext() : Promise.resolve('');
memoriesPromise.then(function(memCtx) {
return fetch('/api/generate-soap', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
transcript: text,
patientAge: document.getElementById('soap-age').value,
patientGender: document.getElementById('soap-gender').value,
type: document.getElementById('soap-type').value,
additionalInstructions: document.getElementById('soap-instructions').value,
physicianMemories: memCtx || null,
model: getSelectedModel()
})
});
})
.then(function(r) { return r.json(); })
.then(function(data) {
hideBusy();
if (data.success) {
setOutputText(soapText, data.soap);
storeSourceContext('soap-text', transcript.innerText.trim());
if (typeof trackAIOutput === 'function') trackAIOutput('soap-text', data.soap);
modelTag.textContent = (data.model || '').split('/').pop();
outputCard.classList.remove('hidden');
outputCard.scrollIntoView({ behavior: 'smooth' });
showToast('SOAP note generated!', 'success');
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('soap-text', data.soap, 'soap', document.getElementById('soap-age').value);
} else showToast(data.error || 'Failed', 'error');
})
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
});
document.getElementById('soap-refine-btn').addEventListener('click', function() { refineDocument('soap-text', 'soap-refine-input'); });
document.getElementById('soap-shorten-btn').addEventListener('click', function() { shortenDocument('soap-text'); });
// Register load handler for resuming saved SOAP notes
if (typeof registerEncounterLoadHandler === 'function') {
registerEncounterLoadHandler('soap', function(enc) {
if (enc.transcript) transcript.textContent = enc.transcript;
if (enc.generated_note) {
setOutputText(soapText, enc.generated_note);
outputCard.classList.remove('hidden');
}
try {
var pd = JSON.parse(enc.partial_data || '{}');
if (pd.age) document.getElementById('soap-age').value = pd.age;
if (pd.gender) document.getElementById('soap-gender').value = pd.gender;
if (pd.type) document.getElementById('soap-type').value = pd.type;
} catch(e) {}
});
}
console.log('✅ SOAP module loaded');
});
})();