| | const API_URL = 'http://localhost:8000';
|
| | let sessionId = null;
|
| | let patientData = {};
|
| | let currentPdfUrl = null;
|
| |
|
| |
|
| | let recognition = null;
|
| | let isRecording = false;
|
| | let autoSpeak = false;
|
| | let currentUtterance = null;
|
| | let isSpeaking = false;
|
| | let continuousMode = false;
|
| |
|
| |
|
| | function initSpeechRecognition() {
|
| | if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
|
| | const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| | recognition = new SpeechRecognition();
|
| | recognition.continuous = false;
|
| | recognition.interimResults = false;
|
| | recognition.lang = 'en-US';
|
| |
|
| | recognition.onstart = function() {
|
| | isRecording = true;
|
| | document.getElementById('voiceBtn').classList.add('recording');
|
| | document.getElementById('voiceInputBtn').classList.add('active');
|
| | updateVoiceStatus('π€ Listening... Speak now');
|
| | };
|
| |
|
| | recognition.onresult = function(event) {
|
| | const transcript = event.results[0][0].transcript;
|
| | document.getElementById('messageInput').value = transcript;
|
| | updateVoiceStatus('β Voice input received: "' + transcript + '"');
|
| |
|
| |
|
| | if (continuousMode) {
|
| | setTimeout(() => {
|
| | sendMessage();
|
| | }, 500);
|
| | } else {
|
| | setTimeout(() => hideVoiceStatus(), 2000);
|
| | }
|
| | };
|
| |
|
| | recognition.onerror = function(event) {
|
| | console.error('Speech recognition error:', event.error);
|
| | updateVoiceStatus('β Error: ' + event.error, 'error');
|
| | stopRecording();
|
| |
|
| |
|
| | if (continuousMode && event.error !== 'aborted' && event.error !== 'no-speech') {
|
| | setTimeout(() => {
|
| | if (continuousMode && !isSpeaking) {
|
| | startListening();
|
| | }
|
| | }, 1000);
|
| | }
|
| | };
|
| |
|
| | recognition.onend = function() {
|
| | stopRecording();
|
| |
|
| |
|
| | if (continuousMode && !isSpeaking) {
|
| | setTimeout(() => {
|
| | if (continuousMode) {
|
| | startListening();
|
| | }
|
| | }, 500);
|
| | }
|
| | };
|
| | } else {
|
| | showNotification('Voice recognition not supported in this browser', 'error');
|
| | }
|
| | }
|
| |
|
| | function startListening() {
|
| | if (!recognition) {
|
| | initSpeechRecognition();
|
| | }
|
| |
|
| | if (!isRecording) {
|
| | try {
|
| | recognition.start();
|
| | } catch (e) {
|
| | console.error('Error starting recognition:', e);
|
| | }
|
| | }
|
| | }
|
| |
|
| | function toggleVoiceInput() {
|
| | if (!recognition) {
|
| | initSpeechRecognition();
|
| | }
|
| |
|
| | if (isRecording) {
|
| | recognition.stop();
|
| | } else {
|
| | startListening();
|
| | }
|
| | }
|
| |
|
| | function stopRecording() {
|
| | isRecording = false;
|
| | document.getElementById('voiceBtn').classList.remove('recording');
|
| | document.getElementById('voiceInputBtn').classList.remove('active');
|
| | }
|
| |
|
| | function updateVoiceStatus(message, type = 'info') {
|
| | const status = document.getElementById('voiceStatus');
|
| | status.textContent = message;
|
| | status.className = 'voice-status active';
|
| | if (type === 'error') {
|
| | status.style.background = '#ffebee';
|
| | status.style.color = '#c62828';
|
| | } else {
|
| | status.style.background = '#e3f2fd';
|
| | status.style.color = '#1565c0';
|
| | }
|
| | }
|
| |
|
| | function hideVoiceStatus() {
|
| | document.getElementById('voiceStatus').classList.remove('active');
|
| | }
|
| |
|
| |
|
| | function speakText(text) {
|
| | if (!('speechSynthesis' in window)) {
|
| | console.error('Speech synthesis not supported');
|
| | showNotification('Text-to-speech not supported in this browser', 'error');
|
| | return;
|
| | }
|
| |
|
| | if (isSpeaking) {
|
| | window.speechSynthesis.cancel();
|
| | }
|
| |
|
| | let cleanText = text
|
| | .replace(/π©Ί|π‘|π|π₯|β οΈ|β |π
|π―|π|π‘οΈ|π|π€/g, '')
|
| | .replace(/β+/g, '')
|
| | .replace(/\*\*/g, '')
|
| | .replace(/\n{3,}/g, '\n\n');
|
| |
|
| | currentUtterance = new SpeechSynthesisUtterance(cleanText);
|
| | currentUtterance.rate = 0.85;
|
| | currentUtterance.pitch = 1.1;
|
| | currentUtterance.volume = 1;
|
| | currentUtterance.lang = 'en-US';
|
| |
|
| | const voices = window.speechSynthesis.getVoices();
|
| |
|
| | if (voices.length > 0) {
|
| | const preferredVoice = voices.find(voice =>
|
| | (voice.lang.includes('en-US') || voice.lang.includes('en-GB')) &&
|
| | (voice.name.includes('Female') ||
|
| | voice.name.includes('Samantha') ||
|
| | voice.name.includes('Victoria') ||
|
| | voice.name.includes('Google') ||
|
| | voice.name.includes('Microsoft'))
|
| | ) || voices.find(voice => voice.lang.includes('en')) || voices[0];
|
| |
|
| | currentUtterance.voice = preferredVoice;
|
| | }
|
| |
|
| | currentUtterance.onstart = function() {
|
| | isSpeaking = true;
|
| | };
|
| |
|
| | currentUtterance.onend = function() {
|
| | isSpeaking = false;
|
| |
|
| |
|
| | if (continuousMode) {
|
| | setTimeout(() => {
|
| | if (continuousMode && !isRecording) {
|
| | startListening();
|
| | }
|
| | }, 500);
|
| | }
|
| | };
|
| |
|
| | currentUtterance.onerror = function(event) {
|
| | console.error('Speech synthesis error:', event);
|
| | isSpeaking = false;
|
| | if (event.error !== 'interrupted') {
|
| | showNotification('Speech error: ' + event.error, 'error');
|
| | }
|
| | };
|
| |
|
| | setTimeout(() => {
|
| | window.speechSynthesis.speak(currentUtterance);
|
| | }, 100);
|
| | }
|
| |
|
| | function stopSpeaking() {
|
| | window.speechSynthesis.cancel();
|
| | isSpeaking = false;
|
| | }
|
| |
|
| | function toggleAutoSpeak() {
|
| | autoSpeak = !autoSpeak;
|
| | const indicator = document.getElementById('speakIndicator');
|
| | const text = document.getElementById('autoSpeakText');
|
| |
|
| | if (autoSpeak) {
|
| | indicator.style.background = '#4CAF50';
|
| | text.textContent = 'π Auto-Speak: ON';
|
| | showNotification('Auto-speak enabled - Doctor responses will be spoken', 'success');
|
| | } else {
|
| | indicator.style.background = '#999';
|
| | text.textContent = 'π Auto-Speak: OFF';
|
| | stopSpeaking();
|
| | showNotification('Auto-speak disabled', 'info');
|
| | }
|
| | }
|
| |
|
| | function toggleContinuousMode() {
|
| | continuousMode = !continuousMode;
|
| | const btn = document.getElementById('continuousModeBtn');
|
| |
|
| | if (continuousMode) {
|
| | btn.classList.add('active');
|
| | btn.innerHTML = '<span class="voice-indicator recording"></span><span>π Continuous: ON</span>';
|
| | autoSpeak = true;
|
| | toggleAutoSpeak();
|
| | showNotification('Continuous conversation mode enabled! Speak naturally.', 'success');
|
| | startListening();
|
| | } else {
|
| | btn.classList.remove('active');
|
| | btn.innerHTML = '<span class="voice-indicator"></span><span>π Continuous: OFF</span>';
|
| | showNotification('Continuous mode disabled', 'info');
|
| | if (isRecording) {
|
| | recognition.stop();
|
| | }
|
| | }
|
| | }
|
| |
|
| | window.onload = async () => {
|
| | await startNewSession();
|
| | initSpeechRecognition();
|
| |
|
| | if ('speechSynthesis' in window) {
|
| | window.speechSynthesis.onvoiceschanged = function() {
|
| | window.speechSynthesis.getVoices();
|
| | };
|
| | }
|
| | };
|
| |
|
| | async function startNewSession() {
|
| | try {
|
| | const response = await fetch(`${API_URL}/start-session`, {
|
| | method: 'POST'
|
| | });
|
| | const data = await response.json();
|
| | sessionId = data.session_id;
|
| |
|
| | updateSessionInfo();
|
| | addMessage('doctor', data.message);
|
| |
|
| | if (autoSpeak) {
|
| | speakText(data.message);
|
| | }
|
| |
|
| | showNotification('New consultation started!', 'success');
|
| | } catch (error) {
|
| | console.error('Error starting session:', error);
|
| | showNotification('Failed to connect to server', 'error');
|
| | }
|
| | }
|
| |
|
| | function updateSessionInfo() {
|
| | const info = document.getElementById('sessionInfo');
|
| | const patientInfo = patientData.name ? `Patient: ${patientData.name}` : 'New Patient';
|
| | info.innerHTML = `${patientInfo}`;
|
| |
|
| | const sessionDisplay = document.getElementById('sessionIdDisplay');
|
| | sessionDisplay.textContent = `Session ID: ${sessionId} (Click to copy)`;
|
| | }
|
| |
|
| | function copySessionId() {
|
| | navigator.clipboard.writeText(sessionId).then(() => {
|
| | showNotification('Session ID copied to clipboard!', 'success');
|
| | }).catch(() => {
|
| | showNotification('Failed to copy Session ID', 'error');
|
| | });
|
| | }
|
| |
|
| | async function sendMessage() {
|
| | const input = document.getElementById('messageInput');
|
| | const message = input.value.trim();
|
| |
|
| | if (!message) return;
|
| |
|
| | addMessage('user', message);
|
| | input.value = '';
|
| | showLoading(true);
|
| |
|
| | try {
|
| | const response = await fetch(`${API_URL}/chat`, {
|
| | method: 'POST',
|
| | headers: {
|
| | 'Content-Type': 'application/json'
|
| | },
|
| | body: JSON.stringify({
|
| | session_id: sessionId,
|
| | message: message
|
| | })
|
| | });
|
| |
|
| | const data = await response.json();
|
| | sessionId = data.session_id;
|
| | patientData = data.patient_data;
|
| | updateSessionInfo();
|
| | addMessage('doctor', data.response);
|
| |
|
| | if (autoSpeak || continuousMode) {
|
| | speakText(data.response);
|
| | }
|
| | } catch (error) {
|
| | console.error('Error sending message:', error);
|
| | addMessage('doctor', 'β Sorry, there was an error. Please try again.');
|
| | } finally {
|
| | showLoading(false);
|
| | }
|
| | }
|
| |
|
| | function addMessage(type, text) {
|
| | const container = document.getElementById('chatContainer');
|
| | const messageDiv = document.createElement('div');
|
| | messageDiv.className = `message ${type}`;
|
| |
|
| | const contentDiv = document.createElement('div');
|
| | contentDiv.className = 'message-content';
|
| | contentDiv.textContent = text;
|
| |
|
| | if (type === 'doctor' && 'speechSynthesis' in window) {
|
| | const speakerIcon = document.createElement('span');
|
| | speakerIcon.className = 'speaker-icon';
|
| | speakerIcon.innerHTML = 'π';
|
| | speakerIcon.title = 'Click to hear this message';
|
| | speakerIcon.onclick = function() {
|
| | if (isSpeaking) {
|
| | stopSpeaking();
|
| | speakerIcon.classList.remove('speaking');
|
| | } else {
|
| | speakText(text);
|
| | speakerIcon.classList.add('speaking');
|
| | setTimeout(() => speakerIcon.classList.remove('speaking'), 3000);
|
| | }
|
| | };
|
| | contentDiv.appendChild(speakerIcon);
|
| | }
|
| |
|
| | messageDiv.appendChild(contentDiv);
|
| | container.appendChild(messageDiv);
|
| | container.scrollTop = container.scrollHeight;
|
| | }
|
| |
|
| | function showLoading(show) {
|
| | const loading = document.getElementById('loading');
|
| | loading.className = show ? 'loading active' : 'loading';
|
| | }
|
| |
|
| | async function generateSummary() {
|
| | if (!sessionId) {
|
| | showNotification('No active session', 'error');
|
| | return;
|
| | }
|
| |
|
| | showLoading(true);
|
| | showNotification('Generating comprehensive detailed summary and PDF... This may take 30-60 seconds', 'success');
|
| |
|
| | try {
|
| | const response = await fetch(`${API_URL}/summary`, {
|
| | method: 'POST',
|
| | headers: {
|
| | 'Content-Type': 'application/json'
|
| | },
|
| | body: JSON.stringify({
|
| | session_id: sessionId
|
| | })
|
| | });
|
| |
|
| | const data = await response.json();
|
| |
|
| | currentPdfUrl = `${API_URL}${data.pdf_url}`;
|
| | document.getElementById('summaryText').textContent = data.summary;
|
| | document.getElementById('pdfDownloadInfo').style.display = 'block';
|
| | document.getElementById('downloadPdfBtn').style.display = 'inline-block';
|
| | document.getElementById('viewPdfBtn').style.display = 'inline-block';
|
| | document.getElementById('summaryModal').classList.add('active');
|
| |
|
| | showNotification('Comprehensive summary and professional PDF generated successfully!', 'success');
|
| | } catch (error) {
|
| | console.error('Error generating summary:', error);
|
| | showNotification('Failed to generate summary. Please try again.', 'error');
|
| | } finally {
|
| | showLoading(false);
|
| | }
|
| | }
|
| |
|
| | function downloadPDF() {
|
| | if (!currentPdfUrl) {
|
| | showNotification('No PDF available', 'error');
|
| | return;
|
| | }
|
| |
|
| | const link = document.createElement('a');
|
| | link.href = currentPdfUrl;
|
| | link.download = `Consultation_Summary_${sessionId.substring(0, 8)}.pdf`;
|
| | document.body.appendChild(link);
|
| | link.click();
|
| | document.body.removeChild(link);
|
| |
|
| | showNotification('PDF download started!', 'success');
|
| | }
|
| |
|
| | function togglePDFViewer() {
|
| | const viewerContainer = document.getElementById('pdfViewerContainer');
|
| | const viewer = document.getElementById('pdfViewer');
|
| | const btn = document.getElementById('viewPdfBtn');
|
| |
|
| | if (viewerContainer.style.display === 'none') {
|
| | viewer.src = currentPdfUrl;
|
| | viewerContainer.style.display = 'block';
|
| | btn.textContent = 'π Hide PDF';
|
| | } else {
|
| | viewerContainer.style.display = 'none';
|
| | btn.textContent = 'ποΈ View PDF';
|
| | }
|
| | }
|
| |
|
| | function closeSummary() {
|
| | document.getElementById('summaryModal').classList.remove('active');
|
| | document.getElementById('pdfViewerContainer').style.display = 'none';
|
| | document.getElementById('viewPdfBtn').textContent = 'ποΈ View PDF';
|
| | }
|
| |
|
| | async function showHistoryModal() {
|
| | document.getElementById('historyModal').classList.add('active');
|
| | await loadHistoryList();
|
| | }
|
| |
|
| | function closeHistory() {
|
| | document.getElementById('historyModal').classList.remove('active');
|
| | }
|
| |
|
| | async function loadHistoryList() {
|
| | const historyList = document.getElementById('historyList');
|
| | historyList.innerHTML = '<p style="text-align: center; color: #999;">Loading...</p>';
|
| |
|
| | try {
|
| | const response = await fetch(`${API_URL}/all-sessions`);
|
| | const data = await response.json();
|
| |
|
| | if (data.sessions.length === 0) {
|
| | historyList.innerHTML = '<p style="text-align: center; color: #999;">No previous consultations found</p>';
|
| | return;
|
| | }
|
| |
|
| | historyList.innerHTML = '';
|
| | data.sessions.forEach(session => {
|
| | const item = document.createElement('div');
|
| | item.className = 'history-item';
|
| | item.onclick = () => loadSession(session.session_id);
|
| |
|
| | const date = new Date(session.last_updated).toLocaleString();
|
| | const pdfBadge = session.has_pdf ? '<span class="pdf-badge">π PDF Available</span>' : '';
|
| |
|
| | item.innerHTML = `
|
| | <h4>π€ ${session.patient_name} ${pdfBadge}</h4>
|
| | <p>π
${date}</p>
|
| | <p>π¬ Messages: ${session.message_count}</p>
|
| | <p style="font-family: monospace; font-size: 0.8em;">π ${session.session_id.substring(0, 20)}...</p>
|
| | `;
|
| | historyList.appendChild(item);
|
| | });
|
| | } catch (error) {
|
| | console.error('Error loading history:', error);
|
| | historyList.innerHTML = '<p style="text-align: center; color: #dc3545;">Failed to load history</p>';
|
| | }
|
| | }
|
| |
|
| | async function loadSessionById() {
|
| | const input = document.getElementById('sessionIdInput');
|
| | const id = input.value.trim();
|
| |
|
| | if (!id) {
|
| | showNotification('Please enter a Session ID', 'error');
|
| | return;
|
| | }
|
| |
|
| | await loadSession(id);
|
| | }
|
| |
|
| | async function loadSession(id) {
|
| | showLoading(true);
|
| | closeHistory();
|
| |
|
| | try {
|
| | const response = await fetch(`${API_URL}/load-session/${id}`);
|
| |
|
| | if (!response.ok) {
|
| | throw new Error('Session not found');
|
| | }
|
| |
|
| | const data = await response.json();
|
| |
|
| | stopSpeaking();
|
| |
|
| | document.getElementById('chatContainer').innerHTML = '';
|
| |
|
| | sessionId = data.session_id;
|
| | patientData = data.patient_data;
|
| | updateSessionInfo();
|
| |
|
| | data.history.forEach(msg => {
|
| | addMessage(msg.role === 'user' ? 'user' : 'doctor', msg.content);
|
| | });
|
| |
|
| | if (data.has_pdf && data.pdf_url) {
|
| | currentPdfUrl = `${API_URL}${data.pdf_url}`;
|
| | }
|
| |
|
| | showNotification('Consultation loaded successfully!', 'success');
|
| | } catch (error) {
|
| | console.error('Error loading session:', error);
|
| | showNotification('Failed to load session. Check the ID and try again.', 'error');
|
| | } finally {
|
| | showLoading(false);
|
| | }
|
| | }
|
| |
|
| | async function restartSession() {
|
| | if (!confirm('Are you sure you want to start a new consultation? Current session will be saved.')) {
|
| | return;
|
| | }
|
| |
|
| |
|
| | if (continuousMode) {
|
| | toggleContinuousMode();
|
| | }
|
| |
|
| | stopSpeaking();
|
| | document.getElementById('chatContainer').innerHTML = '';
|
| | patientData = {};
|
| | currentPdfUrl = null;
|
| | await startNewSession();
|
| | }
|
| |
|
| | function showNotification(message, type) {
|
| | const notification = document.getElementById('notification');
|
| | notification.textContent = message;
|
| | notification.className = `notification ${type} active`;
|
| |
|
| | setTimeout(() => {
|
| | notification.classList.remove('active');
|
| | }, 3000);
|
| | }
|
| |
|
| | document.getElementById('messageInput').addEventListener('keypress', (e) => {
|
| | if (e.key === 'Enter') {
|
| | sendMessage();
|
| | }
|
| | });
|
| |
|
| | document.getElementById('sessionIdInput').addEventListener('keypress', (e) => {
|
| | if (e.key === 'Enter') {
|
| | loadSessionById();
|
| | }
|
| | });
|
| |
|
| | window.addEventListener('beforeunload', () => {
|
| | stopSpeaking();
|
| | if (continuousMode && recognition) {
|
| | recognition.stop();
|
| | }
|
| | }); |