From 5bf55499a47380f852c3f9f1ecec5aa2efb766ec Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 11 Apr 2026 05:19:12 +0200 Subject: [PATCH] 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) --- public/components/soap.html | 2 ++ public/js/soap.js | 65 +++++++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/public/components/soap.html b/public/components/soap.html index e336936..e76ea0f 100644 --- a/public/components/soap.html +++ b/public/components/soap.html @@ -42,6 +42,8 @@
+ +
diff --git a/public/js/soap.js b/public/js/soap.js index 355bc71..eb69ec3 100644 --- a/public/js/soap.js +++ b/public/js/soap.js @@ -13,10 +13,14 @@ 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 = ''; @@ -41,7 +45,7 @@ recognition.onend = function() { finalText += sessionFinals; sessionFinals = ''; - if (isRecording) try { recognition.start(); } catch(e) {} + if (isRecording && !isPaused) try { recognition.start(); } catch(e) {} }; } @@ -51,19 +55,33 @@ sessionFinals = ''; recorder.start().then(function() { isRecording = true; + isPaused = false; recordBtn.classList.add('recording'); - recordBtn.querySelector('span').textContent = 'Stop'; + 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 = ' 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) { @@ -91,6 +109,49 @@ } }); + // 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 = ' 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 = ' 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');