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
204 lines
9.1 KiB
JavaScript
204 lines
9.1 KiB
JavaScript
var _voiceDictationInited = false;
|
|
document.addEventListener('tabChanged', function(e) {
|
|
if (e.detail.tab !== 'dictation' || _voiceDictationInited) return;
|
|
_voiceDictationInited = true;
|
|
var recordBtn = document.getElementById('dict-record-btn');
|
|
var pauseBtn = document.getElementById('dict-pause-btn');
|
|
var indicator = document.getElementById('dict-recording-indicator');
|
|
var timerEl = document.getElementById('dict-timer');
|
|
var transcript = document.getElementById('dict-transcript');
|
|
var clearBtn = document.getElementById('dict-clear');
|
|
var generateBtn = document.getElementById('dict-generate-btn');
|
|
var outputCard = document.getElementById('dict-output');
|
|
var hpiText = document.getElementById('dict-hpi-text');
|
|
var modelTag = document.getElementById('dict-model-tag');
|
|
|
|
var recorder = new AudioRecorder();
|
|
recorder._module = 'dictation';
|
|
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.querySelector('span').textContent = 'Stop';
|
|
if (pauseBtn) pauseBtn.classList.remove('hidden');
|
|
indicator.classList.remove('hidden');
|
|
timer.start();
|
|
if (recognition) try { recognition.start(); } catch(e) {}
|
|
document.dispatchEvent(new CustomEvent('recording-started', { detail: { module: 'dict' } }));
|
|
}).catch(function() { showToast('Microphone denied', 'error'); });
|
|
} else {
|
|
isRecording = false;
|
|
isPaused = false;
|
|
var dur = timer.stop();
|
|
recordBtn.classList.remove('recording');
|
|
recordBtn.querySelector('span').textContent = 'Start Dictation';
|
|
if (pauseBtn) { pauseBtn.classList.add('hidden'); pauseBtn.innerHTML = '<i class="fas fa-pause"></i> Pause'; }
|
|
indicator.classList.add('hidden');
|
|
if (recognition) try { recognition.stop(); } catch(e) {}
|
|
document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'dict' } }));
|
|
|
|
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, 'dictation').then(function(data) {
|
|
hideBusy();
|
|
if (data.success && data.text) { transcript.textContent = data.text; showToast('Transcribed ' + dur + 's', 'success'); }
|
|
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'); });
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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');
|
|
window._savedEncId_dictation = null;
|
|
var labelEl = document.getElementById('dict-label');
|
|
if (labelEl) labelEl.value = '';
|
|
var refineEl = document.getElementById('dict-refine-input');
|
|
if (refineEl) refineEl.value = '';
|
|
});
|
|
|
|
generateBtn.addEventListener('click', function() {
|
|
var text = transcript.innerText.trim();
|
|
if (!text) { showToast('No dictation', 'error'); return; }
|
|
|
|
var outputType = document.getElementById('dict-output-type').value;
|
|
var endpoint, bodyData;
|
|
|
|
showBusy('Generating...');
|
|
|
|
var memoriesPromise = (typeof getUserMemoryContext === 'function') ? getUserMemoryContext() : Promise.resolve('');
|
|
|
|
memoriesPromise.then(function(memCtx) {
|
|
if (outputType === 'soap-full' || outputType === 'soap-subjective') {
|
|
endpoint = '/api/generate-soap';
|
|
bodyData = {
|
|
transcript: text,
|
|
patientAge: document.getElementById('dict-age').value,
|
|
patientGender: document.getElementById('dict-gender').value,
|
|
type: outputType === 'soap-full' ? 'full' : 'subjective',
|
|
physicianMemories: memCtx || null,
|
|
model: getSelectedModel()
|
|
};
|
|
} else {
|
|
endpoint = '/api/generate-hpi-dictation';
|
|
bodyData = {
|
|
transcript: text,
|
|
patientAge: document.getElementById('dict-age').value,
|
|
patientGender: document.getElementById('dict-gender').value,
|
|
setting: document.getElementById('dict-setting').value,
|
|
physicianMemories: memCtx || null,
|
|
model: getSelectedModel()
|
|
};
|
|
}
|
|
return fetch(endpoint, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify(bodyData) });
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
hideBusy();
|
|
if (data.success) {
|
|
setOutputText(hpiText, data.hpi || data.soap);
|
|
storeSourceContext('dict-hpi-text', transcript.innerText.trim());
|
|
modelTag.textContent = (data.model || '').split('/').pop();
|
|
outputCard.classList.remove('hidden');
|
|
outputCard.scrollIntoView({ behavior: 'smooth' });
|
|
showToast('Generated!', 'success');
|
|
if (typeof attachPatientEducation === 'function') attachPatientEducation('dict-hpi-text', { patientAge: document.getElementById('dict-age').value });
|
|
} else showToast(data.error || 'Failed', 'error');
|
|
})
|
|
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
|
});
|
|
|
|
document.getElementById('dict-refine-btn').addEventListener('click', function() { refineDocument('dict-hpi-text', 'dict-refine-input'); });
|
|
document.getElementById('dict-shorten-btn').addEventListener('click', function() { shortenDocument('dict-hpi-text'); });
|
|
|
|
// Register load handler
|
|
if (typeof registerEncounterLoadHandler === 'function') {
|
|
registerEncounterLoadHandler('dictation', function(enc) {
|
|
if (enc.transcript) transcript.textContent = enc.transcript;
|
|
if (enc.generated_note) {
|
|
hpiText.textContent = enc.generated_note;
|
|
outputCard.classList.remove('hidden');
|
|
if (typeof attachPatientEducation === 'function') attachPatientEducation('dict-hpi-text', { patientAge: document.getElementById('dict-age').value });
|
|
}
|
|
});
|
|
}
|
|
|
|
console.log('✅ Dictation module loaded');
|
|
});
|