|
|
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();
|
|
|
}
|
|
|
}); |