Retrying a kept recording transcribed it and put the text on the clipboard, leaving you to find the right tab and paste. The app already had the answer: the module was recorded with the audio. Retry now opens that tab and puts the text in its transcript box. Two things had to be true first. The module was not actually being recorded. transcribeAudio never sent one, so the server stored its default for every upload — all 28 rows in audio_backups said "recording", and a retry had nowhere to send anything back to. Each module's call now tags its own upload. And the names disagreed. The recorders tagged 'encounter', 'soap', 'dictation' while the recording-started events said 'enc', 'sick', 'dict'. One table now holds the mapping and resolves the aliases, so the recorder that tags the upload, the backup row that labels it and the retry that delivers it cannot drift apart again. Existing text is appended to, never replaced: a retry usually recovers something on top of a live transcript, and overwriting would lose the words the browser did hear. The box only exists once its tab's markup has been fetched, so delivery polls briefly rather than guessing a delay, and falls back to the clipboard if the tab never opens. An empty result says so rather than claiming success. Backup rows now name their source and the button reads "Retry into SOAP Note" instead of "Retry". Verified in a browser: enc resolves to encounter, delivery switched tabs and produced 'existing live transcript\n\nRECOVERED TEXT'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
218 lines
9.6 KiB
JavaScript
218 lines
9.6 KiB
JavaScript
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,'&').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 ? '<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, 'soap').then(function(data) {
|
|
hideBusy();
|
|
if (data.success && data.text) transcript.textContent = data.text;
|
|
else if (data.success) { if (liveText) transcript.textContent = liveText; showToast('No speech detected in the recording', 'error'); }
|
|
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 {
|
|
recorder.resumeCapture();
|
|
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());
|
|
modelTag.textContent = (data.model || '').split('/').pop();
|
|
outputCard.classList.remove('hidden');
|
|
outputCard.scrollIntoView({ behavior: 'smooth' });
|
|
showToast('SOAP note generated!', 'success');
|
|
if (typeof attachPatientEducation === 'function') attachPatientEducation('soap-text', { patientAge: document.getElementById('soap-age').value });
|
|
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');
|
|
if (typeof attachPatientEducation === 'function') attachPatientEducation('soap-text', { patientAge: document.getElementById('soap-age').value });
|
|
}
|
|
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');
|
|
});
|