- Live encounter recording → HPI generation - Physician voice dictation → polished HPI - Developmental milestones checklist (AAP/Nelson) - 3-sentence summary generator - OpenRouter integration with model selector - Docker support - Mobile responsive UI
154 lines
5.2 KiB
JavaScript
154 lines
5.2 KiB
JavaScript
// ============================================================
|
|
// MODULE 1: LIVE ENCOUNTER → HPI (separate from milestones)
|
|
// ============================================================
|
|
(function() {
|
|
var recordBtn = document.getElementById('enc-record-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-transcript');
|
|
var generateBtn = document.getElementById('enc-generate-btn');
|
|
var outputCard = document.getElementById('enc-output');
|
|
var hpiText = document.getElementById('enc-hpi-text');
|
|
var modelUsed = document.getElementById('enc-model-used');
|
|
|
|
var recorder = new AudioRecorder();
|
|
var timer = createTimer(timerEl);
|
|
var isRecording = false;
|
|
|
|
// Web Speech for live preview
|
|
var recognition = null;
|
|
var finalText = '';
|
|
|
|
if (window.SpeechRecognition || window.webkitSpeechRecognition) {
|
|
var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
recognition = new SR();
|
|
recognition.continuous = true;
|
|
recognition.interimResults = true;
|
|
recognition.lang = 'en-US';
|
|
|
|
recognition.onresult = function(e) {
|
|
var interim = '';
|
|
for (var i = e.resultIndex; i < e.results.length; i++) {
|
|
if (e.results[i].isFinal) {
|
|
finalText += e.results[i][0].transcript + ' ';
|
|
} else {
|
|
interim = e.results[i][0].transcript;
|
|
}
|
|
}
|
|
transcript.innerHTML = finalText +
|
|
(interim ? '<span style="color:#9ca3af;">' + interim + '</span>' : '');
|
|
};
|
|
|
|
recognition.onend = function() {
|
|
if (isRecording) { try { recognition.start(); } catch(e) {} }
|
|
};
|
|
|
|
recognition.onerror = function() {};
|
|
}
|
|
|
|
recordBtn.addEventListener('click', function() {
|
|
if (!isRecording) {
|
|
finalText = '';
|
|
recorder.start().then(function() {
|
|
isRecording = true;
|
|
recordBtn.classList.add('recording');
|
|
recordBtn.querySelector('span').textContent = 'Stop Recording';
|
|
indicator.classList.remove('hidden');
|
|
timer.start();
|
|
if (recognition) { try { recognition.start(); } catch(e) {} }
|
|
showToast('Recording started', 'info');
|
|
}).catch(function() {
|
|
showToast('Microphone access denied', 'error');
|
|
});
|
|
} else {
|
|
isRecording = false;
|
|
var duration = timer.stop();
|
|
recordBtn.classList.remove('recording');
|
|
recordBtn.querySelector('span').textContent = 'Start Recording';
|
|
indicator.classList.add('hidden');
|
|
if (recognition) { try { recognition.stop(); } catch(e) {} }
|
|
|
|
showLoading('Transcribing audio with Whisper...');
|
|
|
|
recorder.stop().then(function(blob) {
|
|
if (!blob || blob.size === 0) {
|
|
hideLoading();
|
|
showToast('No audio captured', 'error');
|
|
return;
|
|
}
|
|
|
|
var formData = new FormData();
|
|
formData.append('audio', blob, 'encounter.webm');
|
|
|
|
return fetch('/api/transcribe', { method: 'POST', body: formData })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
hideLoading();
|
|
if (data.success) {
|
|
transcript.textContent = data.text;
|
|
showToast('Transcribed ' + duration + 's of audio', 'success');
|
|
} else {
|
|
showToast(data.error || 'Transcription failed', 'error');
|
|
// Keep the live transcript if whisper failed
|
|
if (finalText.trim()) {
|
|
transcript.textContent = finalText.trim();
|
|
}
|
|
}
|
|
});
|
|
}).catch(function(err) {
|
|
hideLoading();
|
|
showToast('Error: ' + err.message, 'error');
|
|
if (finalText.trim()) {
|
|
transcript.textContent = finalText.trim();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
clearBtn.addEventListener('click', function() {
|
|
transcript.textContent = '';
|
|
finalText = '';
|
|
outputCard.classList.add('hidden');
|
|
});
|
|
|
|
generateBtn.addEventListener('click', function() {
|
|
var text = transcript.innerText.trim();
|
|
if (!text) {
|
|
showToast('No transcript to process', 'error');
|
|
return;
|
|
}
|
|
|
|
showLoading('Generating HPI from encounter...');
|
|
|
|
fetch('/api/generate-hpi-encounter', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
transcript: text,
|
|
patientAge: document.getElementById('enc-patient-age').value,
|
|
patientGender: document.getElementById('enc-patient-gender').value,
|
|
model: getSelectedModel()
|
|
})
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
hideLoading();
|
|
if (data.success) {
|
|
hpiText.textContent = data.hpi;
|
|
modelUsed.textContent = (data.model || '').split('/').pop();
|
|
outputCard.classList.remove('hidden');
|
|
outputCard.scrollIntoView({ behavior: 'smooth' });
|
|
showToast('HPI generated!', 'success');
|
|
} else {
|
|
showToast(data.error || 'Generation failed', 'error');
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
hideLoading();
|
|
showToast('Error: ' + err.message, 'error');
|
|
});
|
|
});
|
|
|
|
console.log('✅ Live Encounter module loaded');
|
|
})();
|