-
@@ -572,7 +634,6 @@
-
@@ -717,6 +778,7 @@
+
@@ -777,6 +839,8 @@
No suggestion available.
+
+
@@ -883,6 +947,20 @@
var latestLocalAssist = null;
var complaintSuggestions = [];
var selectedComplaints = [];
+ var complaintsMicMode = 'off';
+ var complaintsSpeechRecognition = null;
+ var complaintsMicActive = false;
+ var complaintsMediaRecorder = null;
+ var complaintsMediaStream = null;
+ var complaintsMediaChunks = [];
+ var complaintsBaseText = '';
+ var medicalSttTarget = '';
+ var medicalSttRecorder = null;
+ var medicalSttStream = null;
+ var medicalSttChunks = [];
+ var medicalSttActive = false;
+ var complaintsSttServerHealthUrl = 'http://139.59.13.39:8000/health';
+ var complaintsSttServerTranscribeUrl = 'http://139.59.13.39:8000/stt/transcribe';
var draftKey = 'opd_rx_draft_' + ($('#opd_id').val() || '0');
var templateLoadState = { target: '', section: '', rows: [] };
var templateSaveState = { section: '', text: '', scope: 'doctor' };
@@ -894,6 +972,7 @@
var invCustomProfileKey = 'opd_inv_custom_profiles';
var legacyInvestigationProfiles = {};
var legacyInvestigationProfileList = [];
+ var investigationBatchActive = false;
var activeMedicineScope = 'active';
var medicineSuggestRows = [];
var investigationProfiles = {
@@ -950,6 +1029,421 @@
}
}
+ function setComplaintsMicMode(mode, note) {
+ complaintsMicMode = mode;
+ var $btn = $('#btn_complaints_mic');
+ if (!$btn.length) {
+ return;
+ }
+
+ $btn.removeClass('btn-outline-success btn-outline-primary btn-outline-secondary btn-danger');
+
+ if (mode === 'server') {
+ $btn.addClass('btn-outline-success').prop('disabled', false).text('🎙 Server Mic');
+ } else if (mode === 'browser') {
+ $btn.addClass('btn-outline-primary').prop('disabled', false).text('🎤 Browser Mic');
+ } else {
+ $btn.addClass('btn-outline-secondary').prop('disabled', true).text('⏸ Mic Off');
+ }
+
+ if (note) {
+ $('.jsError').removeClass('text-danger text-success').addClass('text-muted').text(note);
+ }
+ }
+
+ function setComplaintsMicListening(listening) {
+ var $btn = $('#btn_complaints_mic');
+ if (!$btn.length) {
+ return;
+ }
+
+ complaintsMicActive = listening;
+ if (listening) {
+ $btn.removeClass('btn-outline-success btn-outline-primary btn-outline-secondary').addClass('btn-danger').text('⏹ Stop Mic');
+ } else {
+ setComplaintsMicMode(complaintsMicMode);
+ }
+ }
+
+ function appendTranscriptToComplaints(text) {
+ var transcript = (text || '').toString().trim();
+ if (!transcript) {
+ return;
+ }
+
+ var current = ($('#complaints').val() || '').toString().trim();
+ var nextVal = current ? (current + '\n' + transcript) : transcript;
+ $('#complaints').val(nextVal);
+ refreshCounters();
+ markDirty('Speech text added in complaints');
+ scheduleAutoSave();
+ }
+
+ function appendTranscriptToField(targetId, text) {
+ var transcript = (text || '').toString().trim();
+ if (!transcript || !targetId || !$('#' + targetId).length) {
+ return;
+ }
+
+ var current = ($('#' + targetId).val() || '').toString().trim();
+ var nextVal = current ? (current + '\n' + transcript) : transcript;
+ $('#' + targetId).val(nextVal);
+ refreshCounters();
+ markDirty('Medical speech text added');
+ scheduleAutoSave();
+ }
+
+ function getMedicalContextForField(targetId) {
+ if (targetId === 'finding_examinations') {
+ return 'examination findings';
+ }
+ if (targetId === 'diagnosis') {
+ return 'diagnosis notes';
+ }
+ if (targetId === 'provisional_diagnosis') {
+ return 'provisional diagnosis notes';
+ }
+ return 'clinical notes';
+ }
+
+ function setMedicalMicButtonState(activeTarget) {
+ $('.btn-medical-stt').each(function() {
+ var $btn = $(this);
+ var target = ($btn.data('target') || '').toString();
+ var baseText = ($btn.data('label') || '🎙 Med Mic').toString();
+
+ $btn.removeClass('btn-danger').addClass('btn-medical-stt').prop('disabled', false).text(baseText);
+
+ if (activeTarget && target === activeTarget) {
+ $btn.removeClass('btn-medical-stt').addClass('btn-danger').text('⏹ Stop Mic');
+ }
+ });
+ }
+
+ function stopMedicalSttStream() {
+ if (medicalSttStream && medicalSttStream.getTracks) {
+ medicalSttStream.getTracks().forEach(function(track) {
+ track.stop();
+ });
+ }
+ medicalSttStream = null;
+ medicalSttRecorder = null;
+ medicalSttChunks = [];
+ }
+
+ function sendMedicalAudioToServer(audioBlob, targetId) {
+ var formData = new FormData();
+ formData.append('audio', audioBlob, (targetId || 'clinical') + '.webm');
+ formData.append('lang', 'en-IN');
+ formData.append('medical_context', getMedicalContextForField(targetId));
+
+ return fetch(complaintsSttServerTranscribeUrl, {
+ method: 'POST',
+ mode: 'cors',
+ body: formData
+ }).then(function(response) {
+ if (!response.ok) {
+ return response.json().catch(function() {
+ return {};
+ }).then(function(payload) {
+ var err = new Error(payload.detail || ('Server response: ' + response.status));
+ err.statusCode = response.status;
+ throw err;
+ });
+ }
+ return response.json();
+ }).then(function(payload) {
+ var text = String((payload && (payload.text || payload.transcript || payload.result)) || '').trim();
+ if (!text) {
+ throw new Error('Empty transcript from server');
+ }
+ return text;
+ });
+ }
+
+ function startMedicalServerStt(targetId) {
+ if (!targetId || !$('#' + targetId).length) {
+ return;
+ }
+
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || typeof MediaRecorder === 'undefined') {
+ $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Medical speech is not available in this browser.');
+ return;
+ }
+
+ if (medicalSttActive && medicalSttRecorder) {
+ if (medicalSttTarget === targetId) {
+ medicalSttRecorder.stop();
+ $('.jsError').removeClass('text-danger text-muted').addClass('text-success').text('Processing medical transcript...');
+ return;
+ }
+ $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Medical mic is already recording another field. Stop it first.');
+ return;
+ }
+
+ navigator.mediaDevices.getUserMedia({ audio: true }).then(function(stream) {
+ medicalSttTarget = targetId;
+ medicalSttStream = stream;
+ medicalSttRecorder = new MediaRecorder(stream);
+ medicalSttChunks = [];
+
+ medicalSttRecorder.ondataavailable = function(event) {
+ if (event.data && event.data.size > 0) {
+ medicalSttChunks.push(event.data);
+ }
+ };
+
+ medicalSttRecorder.onstop = function() {
+ var blob = new Blob(medicalSttChunks, {
+ type: (medicalSttRecorder && medicalSttRecorder.mimeType) ? medicalSttRecorder.mimeType : 'audio/webm'
+ });
+ var targetForTranscript = medicalSttTarget;
+
+ stopMedicalSttStream();
+ medicalSttActive = false;
+ medicalSttTarget = '';
+ setMedicalMicButtonState('');
+
+ sendMedicalAudioToServer(blob, targetForTranscript).then(function(transcript) {
+ appendTranscriptToField(targetForTranscript, transcript);
+ $('.jsError').removeClass('text-danger text-muted').addClass('text-success').text('Medical speech text added.');
+ }).catch(function(error) {
+ if (error && error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
+ $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Medical speech input error: ' + (error.message || 'Invalid request'));
+ return;
+ }
+ $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Medical speech server unavailable. Please try again.');
+ });
+ };
+
+ medicalSttActive = true;
+ setMedicalMicButtonState(targetId);
+ $('.jsError').removeClass('text-danger text-muted').addClass('text-success').text('Recording medical speech... click same Mic to stop.');
+ medicalSttRecorder.start();
+ }).catch(function() {
+ medicalSttActive = false;
+ medicalSttTarget = '';
+ setMedicalMicButtonState('');
+ $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Microphone permission denied for medical speech.');
+ });
+ }
+
+ function checkComplaintsSttServer() {
+ if (!window.fetch || !window.AbortController) {
+ return Promise.resolve(false);
+ }
+
+ var controller = new AbortController();
+ var timeoutId = window.setTimeout(function() {
+ controller.abort();
+ }, 3500);
+
+ return fetch(complaintsSttServerHealthUrl, {
+ method: 'GET',
+ mode: 'cors',
+ signal: controller.signal
+ }).then(function(response) {
+ return !!(response && response.ok);
+ }).catch(function() {
+ return false;
+ }).finally(function() {
+ window.clearTimeout(timeoutId);
+ });
+ }
+
+ function sendComplaintsAudioToServer(audioBlob) {
+ var formData = new FormData();
+ formData.append('audio', audioBlob, 'complaints.webm');
+ formData.append('lang', 'en-IN');
+ formData.append('medical_context', (selectedComplaints || []).join(', '));
+
+ return fetch(complaintsSttServerTranscribeUrl, {
+ method: 'POST',
+ mode: 'cors',
+ body: formData
+ }).then(function(response) {
+ if (!response.ok) {
+ return response.json().catch(function() {
+ return {};
+ }).then(function(payload) {
+ var err = new Error(payload.detail || ('Server response: ' + response.status));
+ err.statusCode = response.status;
+ throw err;
+ });
+ }
+ return response.json();
+ }).then(function(payload) {
+ var text = String((payload && (payload.text || payload.transcript || payload.result)) || '').trim();
+ if (!text) {
+ throw new Error('Empty transcript from server');
+ }
+ return text;
+ });
+ }
+
+ function stopComplaintsMediaStream() {
+ if (complaintsMediaStream && complaintsMediaStream.getTracks) {
+ complaintsMediaStream.getTracks().forEach(function(track) {
+ track.stop();
+ });
+ }
+ complaintsMediaStream = null;
+ complaintsMediaRecorder = null;
+ complaintsMediaChunks = [];
+ }
+
+ function startComplaintsBrowserStt() {
+ if (!complaintsSpeechRecognition) {
+ $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Browser speech recognition is not available.');
+ return;
+ }
+
+ if (complaintsMicActive) {
+ complaintsSpeechRecognition.stop();
+ return;
+ }
+
+ setComplaintsMicListening(true);
+ $('.jsError').removeClass('text-danger text-muted').addClass('text-success').text('Listening complaints (browser mode)...');
+
+ try {
+ complaintsSpeechRecognition.start();
+ } catch (e) {
+ setComplaintsMicListening(false);
+ $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Could not start browser speech recognition.');
+ }
+ }
+
+ function startComplaintsServerStt() {
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || typeof MediaRecorder === 'undefined') {
+ if (complaintsSpeechRecognition) {
+ setComplaintsMicMode('browser', 'Server recording is unavailable. Switched to browser mode.');
+ startComplaintsBrowserStt();
+ return;
+ }
+ setComplaintsMicMode('off', 'Speech is not available in this browser.');
+ return;
+ }
+
+ if (complaintsMicActive && complaintsMediaRecorder) {
+ complaintsMediaRecorder.stop();
+ $('.jsError').removeClass('text-danger text-muted').addClass('text-success').text('Processing server transcript...');
+ return;
+ }
+
+ navigator.mediaDevices.getUserMedia({ audio: true }).then(function(stream) {
+ complaintsMediaStream = stream;
+ complaintsMediaRecorder = new MediaRecorder(stream);
+ complaintsMediaChunks = [];
+
+ complaintsMediaRecorder.ondataavailable = function(event) {
+ if (event.data && event.data.size > 0) {
+ complaintsMediaChunks.push(event.data);
+ }
+ };
+
+ complaintsMediaRecorder.onstop = function() {
+ var blob = new Blob(complaintsMediaChunks, {
+ type: (complaintsMediaRecorder && complaintsMediaRecorder.mimeType) ? complaintsMediaRecorder.mimeType : 'audio/webm'
+ });
+
+ stopComplaintsMediaStream();
+ setComplaintsMicListening(false);
+
+ sendComplaintsAudioToServer(blob).then(function(transcript) {
+ appendTranscriptToComplaints(transcript);
+ $('.jsError').removeClass('text-danger text-muted').addClass('text-success').text('Complaints speech text added.');
+ }).catch(function(error) {
+ if (error && error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
+ $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Speech input error: ' + (error.message || 'Invalid request'));
+ return;
+ }
+ if (complaintsSpeechRecognition) {
+ setComplaintsMicMode('browser', 'Server STT unavailable. Switched to browser mode.');
+ } else {
+ setComplaintsMicMode('off', 'Server STT unavailable and browser speech not supported.');
+ }
+ });
+ };
+
+ setComplaintsMicListening(true);
+ $('.jsError').removeClass('text-danger text-muted').addClass('text-success').text('Recording complaints... click Mic again to stop.');
+ complaintsMediaRecorder.start();
+ }).catch(function() {
+ if (complaintsSpeechRecognition) {
+ setComplaintsMicMode('browser', 'Microphone permission failed for server mode. Switched to browser mode.');
+ } else {
+ setComplaintsMicMode('off', 'Microphone permission denied.');
+ }
+ });
+ }
+
+ function initComplaintsSpeech() {
+ var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+ if (SpeechRecognition) {
+ complaintsSpeechRecognition = new SpeechRecognition();
+ complaintsSpeechRecognition.lang = 'en-IN';
+ complaintsSpeechRecognition.interimResults = true;
+ complaintsSpeechRecognition.continuous = false;
+
+ complaintsSpeechRecognition.onresult = function(event) {
+ var interimTranscript = '';
+ var finalTranscript = '';
+ for (var i = event.resultIndex; i < event.results.length; i++) {
+ var t = event.results[i][0].transcript;
+ if (event.results[i].isFinal) {
+ finalTranscript += t;
+ } else {
+ interimTranscript += t;
+ }
+ }
+
+ if (interimTranscript) {
+ // Show interim as a non-destructive preview below the textarea
+ $('#complaints_interim_preview').text('🎤 ' + interimTranscript).show();
+ }
+
+ if (finalTranscript.trim()) {
+ // Final: clear preview, append once to the base text
+ $('#complaints_interim_preview').hide().text('');
+ appendTranscriptToComplaints(finalTranscript.trim());
+ complaintsBaseText = ($('#complaints').val() || '').toString().trim();
+ }
+ };
+
+ complaintsSpeechRecognition.onerror = function(event) {
+ setComplaintsMicListening(false);
+ $('#complaints_interim_preview').hide().text('');
+ $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text('Browser speech error: ' + (event.error || 'unknown'));
+ };
+
+ complaintsSpeechRecognition.onend = function() {
+ setComplaintsMicListening(false);
+ $('#complaints_interim_preview').hide().text('');
+ };
+ }
+
+ if (complaintsSpeechRecognition) {
+ setComplaintsMicMode('browser', 'Complaints Mic is ready (browser mode for local language).');
+ } else {
+ setComplaintsMicMode('off', 'Complaints Mic unavailable in this browser.');
+ }
+
+ $('#btn_complaints_mic').on('click', function() {
+ if (complaintsMicMode === 'browser') {
+ startComplaintsBrowserStt();
+ }
+ });
+ }
+
+ function initMedicalSpeechButtons() {
+ setMedicalMicButtonState('');
+ $(document).on('click', '.btn-medical-stt', function() {
+ var targetId = ($(this).data('target') || '').toString();
+ startMedicalServerStt(targetId);
+ });
+ }
+
function getCustomInvestigationProfiles() {
try {
var parsed = JSON.parse(localStorage.getItem(invCustomProfileKey) || '[]');
@@ -1104,6 +1598,7 @@
function setStatus(type, message) {
var $badge = $('#rx_status_badge');
var $text = $('#rx_status_text');
+ var statusMessage = (message || '').toString();
$badge.removeClass('bg-secondary bg-warning bg-success bg-danger');
if (type === 'saved') {
@@ -1118,7 +1613,7 @@
$badge.addClass('bg-secondary').text('Not Saved');
}
- $text.text(message || '');
+ $text.text(statusMessage).attr('title', statusMessage);
}
function updateScanBanner() {
@@ -1879,6 +2374,40 @@
});
}
+ function renderTemplateSelectOptions(searchQuery) {
+ var list = Array.isArray(templateLoadState.rows) ? templateLoadState.rows : [];
+ var query = (searchQuery || '').toString().trim().toLowerCase();
+ var $sel = $('#rx_template_select');
+ $sel.empty();
+
+ var added = 0;
+ list.forEach(function(row, idx) {
+ var name = (row.template_name || ('Template ' + (idx + 1))).toString();
+ var src = parseInt(row.doc_id || '0', 10) === 0 ? '[Master] ' : '[My] ';
+ var previewText = (row.template_text || '').toString();
+ var haystack = (name + ' ' + previewText + ' ' + src).toLowerCase();
+ if (query && haystack.indexOf(query) === -1) {
+ return;
+ }
+ $sel.append('