LiamKhoaLe's picture
Merge remote changes and resolve chat endpoint conflicts
3a138c2
// Medical AI Assistant - Main Application JavaScript
// static/js/app.js
// TEMPORARILY DISABLED SUBMODULES
// import { attachUIHandlers } from './ui/handlers.js';
// import { attachDoctorUI } from './ui/doctor.js';
// import { attachPatientUI } from './ui/patient.js';
// import { attachSettingsUI } from './ui/settings.js';
// import { attachSessionsUI } from './chat/sessions.js';
// import { attachMessagingUI } from './chat/messaging.js';
// import { AudioRecordingUI } from './audio/recorder.js';
// ================================================================================
// MAIN APPLICATION CLASS
// ================================================================================
class MedicalChatbotApp {
constructor() {
this.currentUser = null; // doctor
this.currentPatientId = null;
this.currentSession = null;
this.backendSessions = [];
this.memory = new Map(); // In-memory storage for STM/demo
this.isLoading = false;
this.doctors = this.loadDoctors();
this.audioRecorder = null; // Audio recording UI
this.init();
}
async init() {
// // TEMPORARILY DISABLED SUBMODULES ATTACHMENT
// attachUIHandlers(this);
// // Attach specialized UIs
// attachDoctorUI(this);
// attachPatientUI(this);
// attachSettingsUI(this);
// attachSessionsUI(this);
// attachMessagingUI(this);
this.setupEventListeners();
this.loadUserPreferences();
this.initializeUser();
await this.loadSavedPatientId();
// If a patient is selected, fetch sessions from backend first
if (this.currentPatientId) {
await this.fetchAndRenderPatientSessions();
}
// Ensure a session exists and is displayed immediately if nothing to show
this.ensureStartupSession();
this.loadChatSessions();
// Bind patient handlers
console.log('[DEBUG] Binding patient handlers');
this.bindPatientHandlers();
this.setupPatientModal();
// Apply saved theme immediately
const prefs = JSON.parse(localStorage.getItem('medicalChatbotPreferences') || '{}');
this.setTheme(prefs.theme || 'auto');
this.setupTheme();
// Initialize audio recording (guarded if module not present)
try {
if (typeof AudioRecordingUI !== 'undefined') {
this.initializeAudioRecording();
} else {
console.warn('[Audio] Recorder module not loaded; skipping initialization');
}
} catch (e) {
console.warn('[Audio] Failed to initialize recorder', e);
}
}
setupEventListeners() {
// Sidebar toggle
const sidebarToggle = document.getElementById('sidebarToggle');
if (sidebarToggle) {
sidebarToggle.addEventListener('click', () => {
this.toggleSidebar();
});
}
// Click outside sidebar to close (mobile/overlay behavior)
const overlay = document.getElementById('appOverlay');
console.log('[DEBUG] Overlay element found:', !!overlay);
const updateOverlay = () => {
const sidebar = document.getElementById('sidebar');
const isOpen = sidebar && sidebar.classList.contains('show');
console.log('[DEBUG] Updating overlay - sidebar open:', isOpen);
if (overlay) {
if (isOpen) {
overlay.classList.add('show');
console.log('[DEBUG] Overlay shown');
} else {
overlay.classList.remove('show');
console.log('[DEBUG] Overlay hidden');
}
}
};
// Keep overlay synced when toggling
const origToggle = this.toggleSidebar.bind(this);
this.toggleSidebar = () => {
console.log('[DEBUG] Wrapped toggleSidebar called');
origToggle();
updateOverlay();
};
// Initialize overlay state - ensure it's hidden on startup
if (overlay) {
overlay.classList.remove('show');
}
updateOverlay();
// Handle window resize for responsive behavior
window.addEventListener('resize', () => {
console.log('[DEBUG] Window resized, updating overlay');
updateOverlay();
});
// Click outside to close sidebar
document.addEventListener('click', (e) => {
const sidebar = document.getElementById('sidebar');
const toggleBtn = document.getElementById('sidebarToggle');
const main = document.querySelector('.main-content');
if (!sidebar) return;
const isOpen = sidebar.classList.contains('show');
const clickInside = sidebar.contains(e.target) || (toggleBtn && toggleBtn.contains(e.target));
const clickOnOverlay = overlay && overlay.contains(e.target);
console.log('[DEBUG] Click event - sidebar open:', isOpen, 'click inside:', clickInside, 'click on overlay:', clickOnOverlay);
if (isOpen && !clickInside) {
if (clickOnOverlay) {
console.log('[DEBUG] Clicked on overlay, closing sidebar');
} else {
console.log('[DEBUG] Clicked outside sidebar, closing sidebar');
}
sidebar.classList.remove('show');
}
// Also close if clicking the main-content while open
if (isOpen && main && main.contains(e.target) && !sidebar.contains(e.target)) {
console.log('[DEBUG] Clicked on main content, closing sidebar');
sidebar.classList.remove('show');
}
updateOverlay();
}, true);
if (overlay) {
overlay.addEventListener('click', () => {
console.log('[DEBUG] Overlay clicked directly');
const sidebar = document.getElementById('sidebar');
if (sidebar) sidebar.classList.remove('show');
updateOverlay();
});
}
// New chat button
const newChatBtn = document.getElementById('newChatBtn');
if (newChatBtn) {
newChatBtn.addEventListener('click', () => {
this.startNewChat();
});
}
// Send button and input
const sendBtn = document.getElementById('sendBtn');
if (sendBtn) {
sendBtn.addEventListener('click', () => {
this.sendMessage();
});
}
const chatInput = document.getElementById('chatInput');
if (chatInput) {
chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
// Auto-resize textarea
chatInput.addEventListener('input', (e) => this.autoResizeTextarea(e.target));
}
// User profile
const userProfile = document.getElementById('userProfile');
if (userProfile) {
userProfile.addEventListener('click', () => {
console.log('[DEBUG] User profile clicked');
this.showUserModal();
});
}
// Settings
const settingsBtn = document.getElementById('settingsBtn');
if (settingsBtn) {
settingsBtn.addEventListener('click', () => {
console.log('[DEBUG] Settings clicked');
this.showSettingsModal();
});
}
// Action buttons
const exportBtn = document.getElementById('exportBtn');
const clearBtn = document.getElementById('clearBtn');
if (exportBtn) exportBtn.addEventListener('click', () => this.exportChat());
if (clearBtn) clearBtn.addEventListener('click', () => this.clearChat());
// Modal events
this.setupModalEvents();
// Theme toggle live
const themeSelect = document.getElementById('themeSelect');
if (themeSelect) {
themeSelect.addEventListener('change', (e) => {
console.log('[Theme] change ->', e.target.value);
this.setTheme(e.target.value);
});
}
// Font size live
const fontSize = document.getElementById('fontSize');
if (fontSize) {
fontSize.addEventListener('change', (e) => {
console.log('[Font] change ->', e.target.value);
this.setFontSize(e.target.value);
});
}
// Other preferences live
const autoSaveEl = document.getElementById('autoSave');
const notificationsEl = document.getElementById('notifications');
if (autoSaveEl) autoSaveEl.addEventListener('change', () => this.savePreferences());
if (notificationsEl) notificationsEl.addEventListener('change', () => this.savePreferences());
}
setupModalEvents() {
// User modal
const userModalClose = document.getElementById('userModalClose');
const userModalCancel = document.getElementById('userModalCancel');
const userModalSave = document.getElementById('userModalSave');
if (userModalClose) {
userModalClose.addEventListener('click', () => {
this.hideModal('userModal');
});
}
if (userModalCancel) {
userModalCancel.addEventListener('click', () => {
this.hideModal('userModal');
});
}
if (userModalSave) {
userModalSave.addEventListener('click', () => {
this.saveUserProfile();
});
}
// Settings modal
const settingsModalClose = document.getElementById('settingsModalClose');
const settingsModalCancel = document.getElementById('settingsModalCancel');
const settingsModalSave = document.getElementById('settingsModalSave');
if (settingsModalClose) {
settingsModalClose.addEventListener('click', () => {
this.hideModal('settingsModal');
});
}
if (settingsModalCancel) {
settingsModalCancel.addEventListener('click', () => {
this.hideModal('settingsModal');
});
}
if (settingsModalSave) {
settingsModalSave.addEventListener('click', () => {
this.saveSettings();
});
}
// Close modals when clicking outside
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
this.hideModal(modal.id);
}
});
});
// Edit title modal wiring
const closeEdit = () => this.hideModal('editTitleModal');
const editTitleModal = document.getElementById('editTitleModal');
if (editTitleModal) {
const editTitleModalClose = document.getElementById('editTitleModalClose');
const editTitleModalCancel = document.getElementById('editTitleModalCancel');
const editTitleModalSave = document.getElementById('editTitleModalSave');
if (editTitleModalClose) editTitleModalClose.addEventListener('click', closeEdit);
if (editTitleModalCancel) editTitleModalCancel.addEventListener('click', closeEdit);
if (editTitleModalSave) {
editTitleModalSave.addEventListener('click', () => {
const input = document.getElementById('editSessionTitleInput');
const newTitle = input.value.trim();
if (!newTitle) return;
if (!this._pendingEditSessionId) return;
this.renameChatSession(this._pendingEditSessionId, newTitle);
this._pendingEditSessionId = null;
input.value = '';
this.hideModal('editTitleModal');
});
}
}
// System access button handler
const systemBtn = document.getElementById('systemAccessBtn');
if (systemBtn) {
systemBtn.addEventListener('click', () => {
window.location.href = '/system-status';
});
}
}
initializeUser() {
// Check if user exists in localStorage
const savedUser = localStorage.getItem('medicalChatbotUser');
if (savedUser) {
this.currentUser = JSON.parse(savedUser);
} else {
// Create default user
this.currentUser = {
id: this.generateId(), // This is a temporary local ID
name: 'Anonymous',
role: 'Medical Professional',
specialty: '',
createdAt: new Date().toISOString()
};
this.saveUser();
}
this.updateUserDisplay();
}
startNewChat() {
if (this.currentSession) {
// Save current session (local only)
this.saveCurrentSession();
}
// Create new session
this.currentSession = {
id: 'default',
title: 'New Chat',
messages: [],
createdAt: new Date().toISOString(),
lastActivity: new Date().toISOString()
};
// Clear chat messages
this.clearChatMessages();
// Add welcome message
this.addMessage('assistant', this.getWelcomeMessage());
// Update UI
this.updateChatTitle();
this.loadChatSessions();
// Focus input
document.getElementById('chatInput').focus();
}
ensureStartupSession() {
// If we already have backend sessions for selected patient, do not create a local one
if (this.backendSessions && this.backendSessions.length > 0) {
return;
}
const sessions = this.getChatSessions();
if (sessions.length === 0) {
// Create a new session immediately so it shows in sidebar
this.currentSession = {
id: 'default',
title: 'New Chat',
messages: [],
createdAt: new Date().toISOString(),
lastActivity: new Date().toISOString()
};
this.saveCurrentSession();
this.updateChatTitle();
} else {
// Load the most recent session into view
this.currentSession = sessions[0];
this.clearChatMessages();
this.currentSession.messages.forEach(m => this.displayMessage(m));
this.updateChatTitle();
}
}
getWelcomeMessage() {
return `👋 Welcome to Medical AI Assistant
# Who am I, and what can I do?
I'm here to help you with medical questions, diagnosis assistance, and healthcare information. I can:
🔍 Answer medical questions and provide information
📋 Help with symptom analysis and differential diagnosis
💊 Provide medication and treatment information
📚 Explain medical procedures and conditions
⚠️ Offer general health advice (not medical diagnosis)
# Key Features
**Medical Information:** I can provide evidence-based medical information and explanations.
**Symptom Analysis:** I can help analyze symptoms and suggest possible conditions.
**Treatment Guidance:** I can explain treatments, medications, and procedures.
**Important:** This is for informational purposes only. Always consult with qualified healthcare professionals for medical advice.
How can I assist you today?`;
}
clearChatMessages() {
const chatMessages = document.getElementById('chatMessages');
chatMessages.innerHTML = '';
}
showModal(modalId) {
console.log('[DEBUG] showModal called with ID:', modalId);
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('show');
console.log('[DEBUG] Modal shown:', modalId);
} else {
console.error('[DEBUG] Modal not found:', modalId);
}
}
saveSettings() {
const theme = document.getElementById('themeSelect').value;
const fontSize = document.getElementById('fontSize').value;
const autoSave = document.getElementById('autoSave').checked;
const notifications = document.getElementById('notifications').checked;
console.log('[Settings] save', { theme, fontSize, autoSave, notifications });
this.setTheme(theme);
this.setFontSize(fontSize);
// Save additional preferences
const preferences = {
theme: theme,
fontSize: fontSize,
autoSave: autoSave,
notifications: notifications
};
console.log('[Prefs] write', preferences);
localStorage.setItem('medicalChatbotPreferences', JSON.stringify(preferences));
this.hideModal('settingsModal');
}
updateUserDisplay() {
document.getElementById('userName').textContent = this.currentUser.name;
document.getElementById('userStatus').textContent = this.currentUser.role;
}
saveUser() {
localStorage.setItem('medicalChatbotUser', JSON.stringify(this.currentUser));
}
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// ----------------------------------------------------------
// Additional UI setup START
// -----------------------------
// Our submodules aren't lodaed on app.js, so we need to add them here
// Perhaps this is FastAPI limitation, remove this when proper deploy this
// On UI specific hosting site.
// ----------------------------------------------------------
// ================================================================================
// HANDLERS.JS FUNCTIONALITY
// ================================================================================
toggleSidebar() {
const sidebar = document.getElementById('sidebar');
console.log('[DEBUG] toggleSidebar called');
if (sidebar) {
const wasOpen = sidebar.classList.contains('show');
sidebar.classList.toggle('show');
const isNowOpen = sidebar.classList.contains('show');
console.log('[DEBUG] Sidebar toggled - was open:', wasOpen, 'now open:', isNowOpen);
} else {
console.error('[DEBUG] Sidebar element not found');
}
}
autoResizeTextarea(textarea) {
if (!textarea) return;
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
exportChat() {
if (!this.currentSession || this.currentSession.messages.length === 0) {
alert('No chat to export.');
return;
}
const chatData = {
user: this.currentUser?.name || 'Unknown',
session: this.currentSession.title,
date: new Date().toISOString(),
messages: this.currentSession.messages
};
const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `medical-chat-${this.currentSession.title.replace(/[^a-z0-9]/gi, '-')}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
clearChat() {
if (confirm('Are you sure you want to clear this chat? This action cannot be undone.')) {
this.clearChatMessages();
if (this.currentSession) {
this.currentSession.messages = [];
this.currentSession.title = 'New Chat';
this.updateChatTitle();
}
}
}
hideModal(modalId) {
console.log('[DEBUG] hideModal called with ID:', modalId);
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('show');
console.log('[DEBUG] Modal hidden:', modalId);
} else {
console.error('[DEBUG] Modal not found:', modalId);
}
}
showUserModal() {
this.populateDoctorSelect();
const sel = document.getElementById('profileNameSelect');
if (sel && sel.options.length === 0) {
const createOpt = document.createElement('option');
createOpt.value = '__create__';
createOpt.textContent = 'Create doctor user...';
sel.appendChild(createOpt);
}
if (sel && !sel.value) sel.value = this.currentUser?.name || '__create__';
// Safely set role and specialty with null checks
const roleEl = document.getElementById('profileRole');
const specialtyEl = document.getElementById('profileSpecialty');
if (roleEl) roleEl.value = (this.currentUser && this.currentUser.role) ? this.currentUser.role : 'Medical Professional';
if (specialtyEl) specialtyEl.value = (this.currentUser && this.currentUser.specialty) ? this.currentUser.specialty : '';
// Add event listener for doctor selection changes
this.setupDoctorSelectionHandler();
this.showModal('userModal');
}
setupDoctorSelectionHandler() {
const sel = document.getElementById('profileNameSelect');
const roleEl = document.getElementById('profileRole');
const specialtyEl = document.getElementById('profileSpecialty');
if (!sel || !roleEl || !specialtyEl) return;
// Remove existing listeners to avoid duplicates
sel.removeEventListener('change', this.handleDoctorSelection);
// Add new listener
this.handleDoctorSelection = async (event) => {
const selectedName = event.target.value;
console.log('[DEBUG] Doctor selected:', selectedName);
if (selectedName === '__create__') {
// Reset to default values for new doctor
roleEl.value = 'Medical Professional';
specialtyEl.value = '';
return;
}
// Find the selected doctor in our doctors list
const selectedDoctor = this.doctors.find(d => d.name === selectedName);
if (selectedDoctor) {
// Update role and specialty from the selected doctor
if (selectedDoctor.role) {
roleEl.value = selectedDoctor.role;
}
if (selectedDoctor.specialty) {
specialtyEl.value = selectedDoctor.specialty;
}
console.log('[DEBUG] Updated role and specialty for doctor:', selectedName, selectedDoctor.role, selectedDoctor.specialty);
} else {
// If doctor not found in local list, try to fetch from backend
try {
const resp = await fetch(`/account?q=${encodeURIComponent(selectedName)}&limit=1`);
if (resp.ok) {
const data = await resp.json();
const doctor = data && data[0];
if (doctor) {
if (doctor.role) {
roleEl.value = doctor.role;
}
if (doctor.specialty) {
specialtyEl.value = doctor.specialty;
}
console.log('[DEBUG] Fetched and updated role/specialty from backend:', doctor.role, doctor.specialty);
}
}
} catch (e) {
console.warn('Failed to fetch doctor details from backend:', e);
}
}
};
sel.addEventListener('change', this.handleDoctorSelection);
}
showSettingsModal() {
console.log('[DEBUG] showSettingsModal called');
this.showModal('settingsModal');
}
// ================================================================================
// SETTINGS.JS FUNCTIONALITY
// ================================================================================
loadUserPreferences() {
const prefs = JSON.parse(localStorage.getItem('medicalChatbotPreferences') || '{}');
if (prefs.theme) this.setTheme(prefs.theme);
if (prefs.fontSize) this.setFontSize(prefs.fontSize);
if (prefs.autoSave !== undefined) document.getElementById('autoSave').checked = prefs.autoSave;
if (prefs.notifications !== undefined) document.getElementById('notifications').checked = prefs.notifications;
}
setTheme(theme) {
const root = document.documentElement;
console.log('[Theme] Setting theme to:', theme);
if (theme === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
root.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
console.log('[Theme] Auto theme applied:', prefersDark ? 'dark' : 'light');
} else {
root.setAttribute('data-theme', theme);
console.log('[Theme] Manual theme applied:', theme);
}
// Force a re-render by toggling a class
root.classList.add('theme-updated');
setTimeout(() => root.classList.remove('theme-updated'), 100);
}
setFontSize(size) {
const root = document.documentElement;
const sizes = { small: '14px', medium: '16px', large: '18px' };
const fontSize = sizes[size] || '16px';
console.log('[Font] Setting font size to:', fontSize);
root.style.fontSize = fontSize;
// Force a re-render
root.classList.add('font-updated');
setTimeout(() => root.classList.remove('font-updated'), 100);
}
setupTheme() {
const themeSelect = document.getElementById('themeSelect');
if (themeSelect) {
const prefs = JSON.parse(localStorage.getItem('medicalChatbotPreferences') || '{}');
themeSelect.value = prefs.theme || 'auto';
}
}
savePreferences() {
const preferences = {
theme: document.getElementById('themeSelect').value,
fontSize: document.getElementById('fontSize').value,
autoSave: document.getElementById('autoSave').checked,
notifications: document.getElementById('notifications').checked
};
localStorage.setItem('medicalChatbotPreferences', JSON.stringify(preferences));
}
showLoading(show) {
const overlay = document.getElementById('loadingOverlay');
if (overlay) {
if (show) {
overlay.classList.add('show');
} else {
overlay.classList.remove('show');
}
}
this.isLoading = show;
}
// ================================================================================
// SESSIONS.JS FUNCTIONALITY
// ================================================================================
getChatSessions() {
const sessions = localStorage.getItem(`chatSessions_${this.currentUser.id}`);
return sessions ? JSON.parse(sessions) : [];
}
saveCurrentSession() {
if (!this.currentSession) return;
if (this.currentSession.source === 'backend') return; // do not persist backend sessions locally here
const sessions = this.getChatSessions();
const existingIndex = sessions.findIndex(s => s.id === this.currentSession.id);
if (existingIndex >= 0) {
sessions[existingIndex] = { ...this.currentSession };
} else {
sessions.unshift(this.currentSession);
}
localStorage.setItem(`chatSessions_${this.currentUser.id}`, JSON.stringify(sessions));
}
loadChatSessions() {
const sessionsContainer = document.getElementById('chatSessions');
sessionsContainer.innerHTML = '';
const sessions = (this.backendSessions && this.backendSessions.length > 0) ? this.backendSessions : this.getChatSessions();
if (sessions.length === 0) {
sessionsContainer.innerHTML = '<div class="no-sessions">No chat sessions yet</div>';
return;
}
sessions.forEach(session => {
const sessionElement = document.createElement('div');
sessionElement.className = `chat-session ${session.id === this.currentSession?.id ? 'active' : ''}`;
sessionElement.addEventListener('click', async () => {
// Avoid re-loading if the session is already active
if (session.id === this.currentSession?.id) return;
await this.switchToSession(session);
});
const time = this.formatTime(session.lastActivity);
sessionElement.innerHTML = `
<div class="chat-session-row">
<div class="chat-session-meta">
<div class="chat-session-title">${session.title}</div>
<div class="chat-session-time">${time}</div>
</div>
<div class="chat-session-actions">
<button class="chat-session-menu" title="Options" aria-label="Options" data-session-id="${session.id}">
<i class="fas fa-ellipsis-vertical"></i>
</button>
</div>
</div>
`;
sessionsContainer.appendChild(sessionElement);
const menuBtn = sessionElement.querySelector('.chat-session-menu');
if (session.source !== 'backend') {
menuBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.showSessionMenu(e.currentTarget, session.id);
});
} else {
menuBtn.disabled = true;
menuBtn.style.opacity = 0.5;
menuBtn.title = 'Options available for local sessions only';
}
});
}
updateChatTitle() {
const titleElement = document.getElementById('chatTitle');
if (this.currentSession) {
titleElement.textContent = this.currentSession.title;
} else {
titleElement.textContent = 'Medical AI Assistant';
}
}
async switchToSession(session) {
console.log('[DEBUG] Switching to session:', session.id, session.source);
// Clear current session and messages first
this.currentSession = null;
this.clearChatMessages();
// Set new session
this.currentSession = { ...session };
if (session.source === 'backend') {
// For backend sessions, always fetch fresh messages
console.log('[DEBUG] Fetching messages for backend session:', session.id);
await this.hydrateMessagesForSession(session.id);
} else {
// For local sessions, load from localStorage
console.log('[DEBUG] Loading messages for local session:', session.id);
const localSessions = this.getChatSessions();
const localSession = localSessions.find(s => s.id === session.id);
if (localSession && localSession.messages) {
// Sort messages by timestamp
const sortedMessages = localSession.messages.sort((a, b) => {
const timeA = new Date(a.timestamp || 0).getTime();
const timeB = new Date(b.timestamp || 0).getTime();
return timeA - timeB; // Ascending order for display
});
console.log('[DEBUG] Displaying', sortedMessages.length, 'messages for local session');
sortedMessages.forEach(message => this.displayMessage(message));
// Check if session needs title generation
this.checkAndGenerateSessionTitle();
} else {
console.log('[DEBUG] No messages found for local session:', session.id);
}
}
this.updateChatTitle();
this.loadChatSessions(); // Re-render to update active state
}
loadChatSession(sessionId) {
const sessions = this.getChatSessions();
const session = sessions.find(s => s.id === sessionId);
if (!session) return;
this.switchToSession(session);
}
renameChatSession(sessionId, newTitle) {
const sessions = this.getChatSessions();
const idx = sessions.findIndex(s => s.id === sessionId);
if (idx === -1) return;
sessions[idx] = { ...sessions[idx], title: newTitle };
localStorage.setItem(`chatSessions_${this.currentUser.id}`, JSON.stringify(sessions));
if (this.currentSession && this.currentSession.id === sessionId) {
this.currentSession.title = newTitle;
this.updateChatTitle();
}
this.loadChatSessions();
}
async deleteChatSession(sessionId) {
const confirmDelete = confirm('Delete this chat session? This cannot be undone.');
if (!confirmDelete) return;
try {
// Check if it's a backend session
const isBackendSession = this.backendSessions && this.backendSessions.some(s => s.id === sessionId);
if (isBackendSession) {
// Delete from backend (MongoDB + memory system)
const resp = await fetch(`/session/${sessionId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' }
});
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}`);
}
// Remove from backend sessions
this.backendSessions = this.backendSessions.filter(s => s.id !== sessionId);
// Invalidate caches
this.invalidateSessionCache(this.currentPatientId);
this.invalidateMessageCache(this.currentPatientId, sessionId);
} else {
// Delete from localStorage only
const sessions = this.getChatSessions();
const index = sessions.findIndex(s => s.id === sessionId);
if (index === -1) return;
sessions.splice(index, 1);
localStorage.setItem(`chatSessions_${this.currentUser.id}`, JSON.stringify(sessions));
}
// Handle current session cleanup
if (this.currentSession && this.currentSession.id === sessionId) {
if (isBackendSession) {
// For backend sessions, switch to another session or clear
if (this.backendSessions.length > 0) {
this.currentSession = this.backendSessions[0];
await this.hydrateMessagesForSession(this.currentSession.id);
} else {
this.currentSession = null;
this.clearChatMessages();
}
} else {
// For local sessions, switch to another session or clear
const remainingSessions = this.getChatSessions();
if (remainingSessions.length > 0) {
this.currentSession = remainingSessions[0];
this.clearChatMessages();
this.currentSession.messages.forEach(m => this.displayMessage(m));
} else {
this.currentSession = null;
this.clearChatMessages();
}
}
this.updateChatTitle();
}
this.loadChatSessions();
} catch (error) {
console.error('Error deleting session:', error);
alert('Failed to delete session. Please try again.');
}
}
showSessionMenu(anchorEl, sessionId) {
// Remove existing popover
document.querySelectorAll('.chat-session-menu-popover').forEach(p => p.remove());
const rect = anchorEl.getBoundingClientRect();
const pop = document.createElement('div');
pop.className = 'chat-session-menu-popover show';
pop.innerHTML = `
<div class="chat-session-menu-item" data-action="edit" data-session-id="${sessionId}"><i class="fas fa-pen"></i> Edit Name</div>
<div class="chat-session-menu-item" data-action="delete" data-session-id="${sessionId}"><i class="fas fa-trash"></i> Delete</div>
`;
document.body.appendChild(pop);
pop.style.top = `${rect.bottom + window.scrollY + 6}px`;
pop.style.left = `${rect.right + window.scrollX - pop.offsetWidth}px`;
const onDocClick = (ev) => {
if (!pop.contains(ev.target) && ev.target !== anchorEl) {
pop.remove();
document.removeEventListener('click', onDocClick);
}
};
setTimeout(() => document.addEventListener('click', onDocClick), 0);
pop.querySelectorAll('.chat-session-menu-item').forEach(item => {
item.addEventListener('click', (e) => {
const action = item.getAttribute('data-action');
const id = item.getAttribute('data-session-id');
if (action === 'delete') this.deleteChatSession(id);
else if (action === 'edit') {
this._pendingEditSessionId = id;
const sessions = this.getChatSessions();
const s = sessions.find(x => x.id === id);
const input = document.getElementById('editSessionTitleInput');
if (input) input.value = s ? s.title : '';
this.showModal('editTitleModal');
}
pop.remove();
});
});
}
updateBackendSession(sessionId, updates) {
// This would call the backend API to update session metadata
console.log('Updating backend session:', sessionId, updates);
}
deleteBackendSession(sessionId) {
// This would call the backend API to delete the session
console.log('Deleting backend session:', sessionId);
}
updateCurrentSession() {
if (this.currentSession) {
this.currentSession.lastActivity = new Date().toISOString();
this.saveCurrentSession();
}
}
// ================================================================================
// DOCTOR.JS FUNCTIONALITY
// ================================================================================
async loadDoctors() {
try {
// Fetch doctors from MongoDB
const resp = await fetch('/account');
if (resp.ok) {
const data = await resp.json();
this.doctors = data || [];
// Ensure each doctor has role and specialty information
this.doctors = this.doctors.map(doctor => ({
name: doctor.name,
role: doctor.role || 'Medical Professional',
specialty: doctor.specialty || '',
id: doctor.id
}));
// Also save to localStorage for offline access
localStorage.setItem('medicalChatbotDoctors', JSON.stringify(this.doctors));
console.log('[DEBUG] Loaded doctors with role/specialty:', this.doctors);
return this.doctors;
} else {
// Fallback to localStorage if API fails
const raw = localStorage.getItem('medicalChatbotDoctors');
const arr = raw ? JSON.parse(raw) : [];
const seen = new Set();
this.doctors = arr.filter(x => x && x.name && !seen.has(x.name) && seen.add(x.name));
return this.doctors;
}
} catch (e) {
console.warn('Failed to load doctors from API, using localStorage fallback:', e);
// Fallback to localStorage
const raw = localStorage.getItem('medicalChatbotDoctors');
const arr = raw ? JSON.parse(raw) : [];
const seen = new Set();
this.doctors = arr.filter(x => x && x.name && !seen.has(x.name) && seen.add(x.name));
return this.doctors;
}
}
async searchDoctors(query) {
try {
const resp = await fetch(`/account?q=${encodeURIComponent(query)}&limit=10`);
if (resp.ok) {
const data = await resp.json();
return data || [];
}
} catch (e) {
console.warn('Doctor search failed:', e);
}
return [];
}
async createDoctor(doctorData) {
try {
const resp = await fetch('/account', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(doctorData)
});
if (resp.ok) {
const data = await resp.json();
// Add to local doctors list
this.doctors.push({ name: data.name, id: data.id });
this.saveDoctors();
return data;
}
} catch (e) {
console.error('Failed to create doctor:', e);
}
return null;
}
saveDoctors() {
localStorage.setItem('medicalChatbotDoctors', JSON.stringify(this.doctors));
}
async populateDoctorSelect() {
const sel = document.getElementById('profileNameSelect');
const newSec = document.getElementById('newDoctorSection');
if (!sel) return;
// Load doctors from MongoDB
await this.loadDoctors();
sel.innerHTML = '';
const createOpt = document.createElement('option');
createOpt.value = '__create__';
createOpt.textContent = 'Create doctor user...';
sel.appendChild(createOpt);
// Ensure no duplicates, include current doctor
const names = new Set(this.doctors.map(d => d.name));
if (this.currentUser?.name && !names.has(this.currentUser.name)) {
this.doctors.unshift({ name: this.currentUser.name });
names.add(this.currentUser.name);
this.saveDoctors();
}
this.doctors.forEach(d => {
const opt = document.createElement('option');
opt.value = d.name;
opt.textContent = d.name;
if (this.currentUser?.name === d.name) opt.selected = true;
sel.appendChild(opt);
});
sel.addEventListener('change', () => {
if (sel.value === '__create__') {
newSec.style.display = '';
const input = document.getElementById('newDoctorName');
if (input) input.value = '';
} else {
newSec.style.display = 'none';
// Trigger doctor selection handler to update role/specialty
if (this.handleDoctorSelection) {
this.handleDoctorSelection({ target: sel });
}
}
});
const cancelBtn = document.getElementById('cancelNewDoctor');
const confirmBtn = document.getElementById('confirmNewDoctor');
if (cancelBtn) cancelBtn.onclick = () => { newSec.style.display = 'none'; sel.value = this.currentUser?.name || ''; };
if (confirmBtn) confirmBtn.onclick = async () => {
const name = (document.getElementById('newDoctorName').value || '').trim();
if (!name) return;
if (!this.doctors.find(d => d.name === name)) {
// Get current role and specialty from the form
const role = document.getElementById('profileRole').value || 'Medical Professional';
const specialty = document.getElementById('profileSpecialty').value.trim() || null;
// Create doctor in MongoDB
const result = await this.createDoctor({
name,
role,
specialty
});
if (result && result.id) {
this.doctors.unshift({
name,
role,
specialty,
id: result.id
});
this.saveDoctors();
// Update current user profile
this.currentUser.id = result.id;
this.currentUser.name = name;
this.currentUser.role = role;
this.currentUser.specialty = specialty;
this.saveUser();
this.updateUserDisplay();
}
}
await this.populateDoctorSelect();
sel.value = name;
newSec.style.display = 'none';
};
}
async saveUserProfile() {
const nameSel = document.getElementById('profileNameSelect');
const name = nameSel ? nameSel.value : '';
const role = document.getElementById('profileRole').value;
const specialty = document.getElementById('profileSpecialty').value.trim();
if (!name || name === '__create__') {
alert('Please select or create a doctor name.');
return;
}
// Check if doctor exists in MongoDB first
let existingDoctor = null;
try {
const resp = await fetch(`/account?q=${encodeURIComponent(name)}`);
if(resp.ok) {
const accounts = await resp.json();
existingDoctor = accounts.find(acc => acc.name === name);
}
} catch (e) {
console.warn('Failed to check doctor existence:', e);
}
// Update current user profile info (ID will be updated upon creation)
this.currentUser.name = name;
this.currentUser.role = role;
this.currentUser.specialty = specialty;
// Only create new doctor in MongoDB if it doesn't exist
if (!existingDoctor) {
const doctorPayload = {
name: name,
role: role,
specialty: specialty || null
};
try {
const resp = await fetch('/account', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(doctorPayload)
});
if (!resp.ok) throw new Error('Failed to create doctor in backend');
const data = await resp.json();
console.log('[Doctor] Created new doctor in backend:', data);
if (data.id) {
this.currentUser.id = data.id;
}
// Update local doctor list with the ID from backend
const existingDoctorIndex = this.doctors.findIndex(d => d.name === name);
if (existingDoctorIndex === -1) {
this.doctors.unshift({ name, role, specialty, id: data.id });
} else {
this.doctors[existingDoctorIndex].id = data.id;
}
} catch (err) {
console.warn('[Doctor] failed to create doctor in backend:', err);
}
} else {
// If doctor exists, find their ID and update currentUser
if (existingDoctor && existingDoctor.id) {
this.currentUser.id = existingDoctor.id;
}
console.log('[Doctor] Doctor already exists in backend, no creation needed');
}
// Save the updated user profile (with potentially new ID)
this.saveUser();
this.updateUserDisplay();
this.saveDoctors();
this.hideModal('userModal');
}
// ================================================================================
// PATIENT.JS FUNCTIONALITY
// ================================================================================
async getLocalStorageSuggestions(query) {
try {
const storedPatients = JSON.parse(localStorage.getItem('medicalChatbotPatients') || '[]');
return storedPatients.filter(p => {
const nameMatch = p.name.toLowerCase().includes(query.toLowerCase());
const idMatch = p.id && p.id.includes(query);
return nameMatch || idMatch;
});
} catch (e) {
console.warn('Failed to get localStorage suggestions:', e);
return [];
}
}
combinePatientResults(mongoResults, localResults) {
const resultMap = new Map();
// Add MongoDB results first (they take priority)
mongoResults.forEach(patient => {
if(patient.id) resultMap.set(patient.id, patient);
});
// Add localStorage results only if not already present
localResults.forEach(patient => {
if (patient.id && !resultMap.has(patient.id)) {
resultMap.set(patient.id, patient);
}
});
return Array.from(resultMap.values());
}
async tryFallbackSearch(query, renderSuggestions) {
// Use localStorage for fallback suggestions
try {
const localResults = await this.getLocalStorageSuggestions(query);
if (localResults.length > 0) {
console.log('[DEBUG] Fallback search found matches from localStorage:', localResults);
renderSuggestions(localResults);
} else {
console.log('[DEBUG] No fallback matches found');
renderSuggestions([]);
}
} catch (e) {
console.warn('Fallback search failed:', e);
renderSuggestions([]);
}
}
async loadSavedPatientId() {
const pid = localStorage.getItem('medicalChatbotPatientId');
if (pid) {
this.currentPatientId = pid;
const status = document.getElementById('patientStatus');
const actions = document.getElementById('patientActions');
const emrLink = document.getElementById('emrLink');
if (status) {
// Try to fetch patient name
try {
const resp = await fetch(`/patient/${pid}`);
if (resp.ok) {
const patient = await resp.json();
status.textContent = `Patient: ${patient.name || 'Unknown'} (${patient.id})`;
} else {
status.textContent = `Patient: ${pid}`;
}
} catch (e) {
status.textContent = `Patient: ${pid}`;
}
status.style.color = 'var(--text-secondary)';
}
// Show EMR link
if (actions) actions.style.display = 'block';
if (emrLink) emrLink.href = `/static/emr.html?patient_id=${pid}`;
const input = document.getElementById('patientIdInput');
if (input) input.value = pid;
}
}
savePatientId() {
if (this.currentPatientId) localStorage.setItem('medicalChatbotPatientId', this.currentPatientId);
else localStorage.removeItem('medicalChatbotPatientId');
}
updatePatientDisplay(patientId, patientName = null) {
// Safety check: don't update display if patientId is undefined or null
if (!patientId || patientId === 'undefined' || patientId === 'null') {
console.warn('updatePatientDisplay called with invalid patientId:', patientId);
return;
}
const status = document.getElementById('patientStatus');
const actions = document.getElementById('patientActions');
const emrLink = document.getElementById('emrLink');
if (status) {
if (patientName) {
status.textContent = `Patient: ${patientName} (${patientId})`;
} else {
status.textContent = `Patient: ${patientId}`;
}
status.style.color = 'var(--text-secondary)';
}
// Show EMR link
if (actions) actions.style.display = 'block';
if (emrLink) emrLink.href = `/static/emr.html?patient_id=${patientId}`;
}
loadPatient = async function () {
console.log('[DEBUG] loadPatient called');
const input = document.getElementById('patientIdInput');
const status = document.getElementById('patientStatus');
const value = (input?.value || '').trim();
console.log('[DEBUG] Patient input value:', value);
if (!value) {
console.log('[DEBUG] No input provided');
if (status) { status.textContent = 'Please enter patient ID or name.'; status.style.color = 'var(--warning-color)'; }
return;
}
// Search for patient by name or ID
console.log('[DEBUG] Searching for patient');
try {
const resp = await fetch(`/patient?q=${encodeURIComponent(value)}&limit=1`);
console.log('[DEBUG] Search response status:', resp.status);
if (resp.ok) {
const data = await resp.json();
console.log('[DEBUG] Search results:', data);
const first = (data || [])[0];
if (first && first.id) {
console.log('[DEBUG] Found patient, setting as current:', first);
this.currentPatientId = first.id;
this.savePatientId();
input.value = first.id;
this.updatePatientDisplay(first.id, first.name || 'Unknown');
await this.fetchAndRenderPatientSessions();
return;
}
}
} catch (e) {
console.error('[DEBUG] Search error:', e);
}
// No patient found
console.log('[DEBUG] No patient found');
if (status) { status.textContent = 'No patient found. Try a different search.'; status.style.color = 'var(--warning-color)'; }
}
fetchAndRenderPatientSessions = async function () {
if (!this.currentPatientId) return;
// Check localStorage cache first
const cacheKey = `sessions_${this.currentPatientId}`;
const cached = localStorage.getItem(cacheKey);
let sessions = [];
if (cached) {
try {
const cachedData = JSON.parse(cached);
// Check if cache is recent (less than 2 minutes old)
const cacheTime = new Date(cachedData.timestamp || 0).getTime();
const now = new Date().getTime();
if (now - cacheTime < 2 * 60 * 1000) { // 2 minutes
sessions = cachedData.sessions || [];
console.log('[DEBUG] Using cached sessions for patient:', this.currentPatientId);
}
} catch (e) {
console.warn('Failed to parse cached sessions:', e);
}
}
// If no cache or cache is stale, fetch from backend
if (sessions.length === 0) {
try {
const resp = await fetch(`/patient/${this.currentPatientId}/session`);
if (resp.ok) {
const data = await resp.json();
sessions = Array.isArray(data) ? data : [];
// Cache the sessions
localStorage.setItem(cacheKey, JSON.stringify({
sessions: sessions,
timestamp: new Date().toISOString()
}));
console.log('[DEBUG] Cached sessions for patient:', this.currentPatientId);
} else {
console.warn('Failed to fetch patient sessions', resp.status);
}
} catch (e) {
console.error('Failed to load patient sessions', e);
}
}
// Process sessions
this.backendSessions = sessions.map(s => ({
id: s.id,
title: s.title || 'New Chat',
messages: [],
createdAt: s.created_at || new Date().toISOString(),
lastActivity: s.updated_at || new Date().toISOString(),
source: 'backend'
}));
if (this.backendSessions.length > 0) {
this.currentSession = this.backendSessions[0];
await this.hydrateMessagesForSession(this.currentSession.id);
}
this.loadChatSessions();
}
hydrateMessagesForSession = async function (sessionId) {
if (!sessionId || sessionId === 'undefined') {
console.error('[DEBUG] hydrateMessagesForSession was called with an invalid session ID:', sessionId);
return;
}
try {
// Check localStorage cache first
const cacheKey = `messages_${this.currentPatientId}_${sessionId}`;
const cached = localStorage.getItem(cacheKey);
let messages = [];
if (cached) {
try {
const cachedData = JSON.parse(cached);
// Check if cache is recent (less than 5 minutes old)
const cacheTime = new Date(cachedData.timestamp || 0).getTime();
const now = new Date().getTime();
if (now - cacheTime < 5 * 60 * 1000) { // 5 minutes
messages = cachedData.messages || [];
console.log('[DEBUG] Using cached messages for session:', sessionId);
}
} catch (e) {
console.warn('Failed to parse cached messages:', e);
}
}
// If no cache or cache is stale, fetch from backend
if (messages.length === 0) {
const resp = await fetch(`/session/${sessionId}/messages`);
if (!resp.ok) {
console.warn(`Failed to fetch messages for session ${sessionId}:`, resp.status);
return;
}
const data = await resp.json();
const msgs = Array.isArray(data) ? data : [];
messages = msgs.map(m => ({
id: m.id || this.generateId(),
role: m.sent_by_user ? 'user' : 'assistant',
content: m.content,
timestamp: m.timestamp
}));
// Cache the messages
localStorage.setItem(cacheKey, JSON.stringify({
messages: messages,
timestamp: new Date().toISOString()
}));
console.log('[DEBUG] Cached messages for session:', sessionId, 'count:', messages.length);
}
// Sort messages by timestamp (ascending order for display)
const sortedMessages = messages.sort((a, b) => {
const timeA = new Date(a.timestamp || 0).getTime();
const timeB = new Date(b.timestamp || 0).getTime();
return timeA - timeB;
});
if (this.currentSession && this.currentSession.id === sessionId) {
this.currentSession.messages = sortedMessages;
this.clearChatMessages();
console.log('[DEBUG] Displaying', sortedMessages.length, 'messages for session:', sessionId);
this.currentSession.messages.forEach(m => this.displayMessage(m));
this.updateChatTitle();
// Check if session needs title generation
this.checkAndGenerateSessionTitle();
} else {
console.warn('[DEBUG] Session mismatch - current session:', this.currentSession?.id, 'requested session:', sessionId);
}
} catch (e) {
console.error('Failed to hydrate session messages', e);
}
}
bindPatientHandlers() {
console.log('[DEBUG] bindPatientHandlers called');
const loadBtn = document.getElementById('loadPatientBtn');
console.log('[DEBUG] Load button found:', !!loadBtn);
if (loadBtn) loadBtn.addEventListener('click', () => this.loadPatient());
const patientInput = document.getElementById('patientIdInput');
const suggestionsEl = document.getElementById('patientSuggestions');
console.log('[DEBUG] Patient input found:', !!patientInput);
console.log('[DEBUG] Suggestions element found:', !!suggestionsEl);
if (!patientInput) return;
let debounceTimer;
const hideSuggestions = () => { if (suggestionsEl) suggestionsEl.style.display = 'none'; };
const renderSuggestions = (items) => {
if (!suggestionsEl) return;
if (!items || items.length === 0) { hideSuggestions(); return; }
suggestionsEl.innerHTML = '';
items.forEach(p => {
const div = document.createElement('div');
div.className = 'patient-suggestion';
div.textContent = `${p.name || 'Unknown'} (${p.id})`;
div.addEventListener('click', async () => {
this.currentPatientId = p.id;
this.savePatientId();
patientInput.value = p.id;
hideSuggestions();
this.updatePatientDisplay(p.id, p.name || 'Unknown');
await this.fetchAndRenderPatientSessions();
});
suggestionsEl.appendChild(div);
});
suggestionsEl.style.display = 'block';
};
patientInput.addEventListener('input', () => {
const q = patientInput.value.trim();
console.log('[DEBUG] Patient input changed:', q);
clearTimeout(debounceTimer);
if (!q) { hideSuggestions(); return; }
debounceTimer = setTimeout(async () => {
try {
console.log('[DEBUG] Searching patients with query:', q);
const url = `/patient?q=${encodeURIComponent(q)}&limit=8`;
console.log('[DEBUG] Search URL:', url);
const resp = await fetch(url);
console.log('[DEBUG] Search response status:', resp.status);
let mongoResults = [];
if (resp.ok) {
const data = await resp.json();
mongoResults = data || [];
console.log('[DEBUG] MongoDB search results:', mongoResults);
} else {
console.warn('MongoDB search request failed', resp.status);
}
// Get localStorage suggestions as fallback/additional results
const localResults = await this.getLocalStorageSuggestions(q);
// Combine and deduplicate results (MongoDB results take priority)
const combinedResults = this.combinePatientResults(mongoResults, localResults);
console.log('[DEBUG] Combined search results:', combinedResults);
renderSuggestions(combinedResults);
} catch (e) {
console.error('[DEBUG] Search error:', e);
// Fallback for network errors
console.log('[DEBUG] Trying fallback search after error');
await this.tryFallbackSearch(q, renderSuggestions);
}
}, 200);
});
patientInput.addEventListener('keydown', async (e) => {
if (e.key === 'Enter') {
const value = patientInput.value.trim();
console.log('[DEBUG] Patient input Enter pressed with value:', value);
hideSuggestions();
await this.loadPatient();
}
});
document.addEventListener('click', (ev) => {
if (!suggestionsEl) return;
if (!suggestionsEl.contains(ev.target) && ev.target !== patientInput) hideSuggestions();
});
}
// Patient modal functionality
setupPatientModal() {
const profileBtn = document.getElementById('patientMenuBtn');
const modal = document.getElementById('patientModal');
const closeBtn = document.getElementById('patientModalClose');
const logoutBtn = document.getElementById('patientLogoutBtn');
const createBtn = document.getElementById('patientCreateBtn');
if (profileBtn && modal) {
profileBtn.addEventListener('click', async () => {
const pid = this?.currentPatientId;
if (pid) {
try {
const resp = await fetch(`/patient/${pid}`);
if (resp.ok) {
const p = await resp.json();
const name = p.name || 'Unknown';
const age = typeof p.age === 'number' ? p.age : '-';
const sex = p.sex || '-';
const meds = Array.isArray(p.medications) && p.medications.length > 0 ? p.medications.join(', ') : '-';
document.getElementById('patientSummary').textContent = `${name}${sex}, ${age}`;
document.getElementById('patientMedications').textContent = meds;
document.getElementById('patientAssessment').textContent = p.past_assessment_summary || '-';
}
} catch (e) {
console.error('Failed to load patient profile', e);
}
}
modal.classList.add('show');
});
}
if (closeBtn && modal) {
closeBtn.addEventListener('click', () => modal.classList.remove('show'));
modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('show'); });
}
if (logoutBtn) {
logoutBtn.addEventListener('click', () => {
if (confirm('Log out current patient?')) {
this.currentPatientId = null;
localStorage.removeItem('medicalChatbotPatientId');
const status = document.getElementById('patientStatus');
if (status) { status.textContent = 'No patient selected'; status.style.color = 'var(--text-secondary)'; }
const input = document.getElementById('patientIdInput');
if (input) input.value = '';
modal.classList.remove('show');
}
});
}
if (createBtn) createBtn.addEventListener('click', () => modal.classList.remove('show'));
}
// ================================================================================
// AUDIO RECORDING FUNCTIONALITY
// ================================================================================
async initializeAudioRecording() {
try {
this.audioRecorder = new AudioRecordingUI(this);
const success = await this.audioRecorder.initialize();
if (success) {
console.log('[Audio] Audio recording initialized successfully');
// Make globally accessible for voice detection callback
window.audioRecordingUI = this.audioRecorder;
} else {
console.warn('[Audio] Audio recording initialization failed');
}
} catch (error) {
console.error('[Audio] Failed to initialize audio recording:', error);
}
}
// ================================================================================
// MESSAGING.JS FUNCTIONALITY
// ================================================================================
// Cache invalidation methods
invalidateSessionCache(patientId) {
const cacheKey = `sessions_${patientId}`;
localStorage.removeItem(cacheKey);
console.log('[DEBUG] Invalidated session cache for patient:', patientId);
}
invalidateMessageCache(patientId, sessionId) {
const cacheKey = `messages_${patientId}_${sessionId}`;
localStorage.removeItem(cacheKey);
console.log('[DEBUG] Invalidated message cache for session:', sessionId);
}
invalidateAllCaches(patientId) {
this.invalidateSessionCache(patientId);
// Invalidate all message caches for this patient
const keys = Object.keys(localStorage);
keys.forEach(key => {
if (key.startsWith(`messages_${patientId}_`)) {
localStorage.removeItem(key);
}
});
console.log('[DEBUG] Invalidated all caches for patient:', patientId);
}
sendMessage = async function () {
const input = document.getElementById('chatInput');
const message = input.value.trim();
if (!message || this.isLoading) return;
if (!this.currentPatientId) {
const status = document.getElementById('patientStatus');
if (status) { status.textContent = 'Select a patient before chatting.'; status.style.color = 'var(--warning-color)'; }
return;
}
input.value = '';
this.autoResizeTextarea(input);
this.addMessage('user', message);
this.showLoading(true);
try {
const responseData = await this.callMedicalAPI(message);
this.addMessage('assistant', responseData.response || 'I apologize, but I received an empty response. Please try again.');
this.updateCurrentSession();
// Invalidate caches after successful message exchange
if (this.currentSession && this.currentSession.id) {
this.invalidateMessageCache(this.currentPatientId, this.currentSession.id);
this.invalidateSessionCache(this.currentPatientId);
}
} catch (error) {
console.error('Error sending message:', error);
let errorMessage = 'I apologize, but I encountered an error processing your request.';
if (error.message.includes('500')) errorMessage = 'The server encountered an internal error. Please try again in a moment.';
else if (error.message.includes('404')) errorMessage = 'The requested service was not found. Please check your connection.';
else if (error.message.includes('fetch')) errorMessage = 'Unable to connect to the server. Please check your internet connection.';
this.addMessage('assistant', errorMessage);
} finally {
this.showLoading(false);
}
}
callMedicalAPI = async function (message) {
try {
let sessionId = this.currentSession?.id;
// If no session or default session, create a new one first
if (!sessionId || sessionId === 'default') {
console.log('[DEBUG] Creating new session before sending message');
sessionId = await this.createNewSession();
if (!sessionId) {
throw new Error('Failed to create new session');
}
}
const messagePayload = {
account_id: this.currentUser.id,
patient_id: this.currentPatientId,
message: message
};
const messageResponse = await fetch(`/session/${sessionId}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(messagePayload)
});
if (!messageResponse.ok) throw new Error(`HTTP error! status: ${messageResponse.status}`);
const data = await messageResponse.json();
return data;
} catch (error) {
console.error('API call failed:', error);
console.error('Error details:', {
message: error.message,
stack: error.stack,
user: this.currentUser,
session: this.currentSession,
patientId: this.currentPatientId
});
if (error.name === 'TypeError' && error.message.includes('fetch')) {
return { response: this.generateMockResponse(message) };
}
throw error;
}
}
createNewSession = async function () {
try {
const payload = {
account_id: this.currentUser.id,
patient_id: this.currentPatientId,
title: "New Chat"
};
const response = await fetch('/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const session = await response.json();
console.log('[DEBUG] Created new session:', session);
// Update current session with the new session data
this.currentSession = {
id: session.id,
title: session.title,
messages: [],
createdAt: session.created_at,
lastActivity: new Date().toISOString(),
source: 'backend'
};
return session.id;
} catch (error) {
console.error('Failed to create new session:', error);
throw error;
}
}
generateMockResponse(message) {
const responses = [
"Based on your question about medical topics, I can provide general information. However, please remember that this is for educational purposes only and should not replace professional medical advice.",
"That's an interesting medical question. While I can offer some general insights, it's important to consult with healthcare professionals for personalized medical advice.",
"I understand your medical inquiry. For accurate diagnosis and treatment recommendations, please consult with qualified healthcare providers who can assess your specific situation.",
"Thank you for your medical question. I can provide educational information, but medical decisions should always be made in consultation with healthcare professionals.",
"I appreciate your interest in medical topics. Remember that medical information found online should be discussed with healthcare providers for proper evaluation."
];
return responses[Math.floor(Math.random() * responses.length)];
}
addMessage(role, content) {
if (!this.currentSession) this.startNewChat();
const message = { id: this.generateId(), role, content, timestamp: new Date().toISOString() };
this.currentSession.messages.push(message);
this.displayMessage(message);
if (role === 'user' && this.currentSession.messages.length === 2) this.summariseAndSetTitle(content);
}
// Check if session needs title generation after messages are loaded
checkAndGenerateSessionTitle() {
if (!this.currentSession || !this.currentSession.messages) return;
// Check if this is a new session that needs a title (exactly 2 messages: user + assistant)
if (this.currentSession.messages.length === 2 &&
this.currentSession.title === 'New Chat' &&
this.currentSession.messages[0].role === 'user') {
const firstMessage = this.currentSession.messages[0].content;
this.summariseAndSetTitle(firstMessage);
}
}
summariseAndSetTitle = async function (text) {
try {
const resp = await fetch('/summarise', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, max_words: 5 }) });
if (resp.ok) {
const data = await resp.json();
const title = (data.title || 'New Chat').trim();
this.currentSession.title = title;
this.updateCurrentSession();
this.updateChatTitle();
this.loadChatSessions();
} else {
const fallback = text.length > 50 ? text.substring(0, 50) + '...' : text;
this.currentSession.title = fallback;
this.updateCurrentSession();
this.updateChatTitle();
this.loadChatSessions();
}
} catch (e) {
const fallback = text.length > 50 ? text.substring(0, 50) + '...' : text;
this.currentSession.title = fallback;
this.updateCurrentSession();
this.updateChatTitle();
this.loadChatSessions();
}
}
displayMessage(message) {
const chatMessages = document.getElementById('chatMessages');
const messageElement = document.createElement('div');
messageElement.className = `message ${message.role}-message fade-in`;
messageElement.id = `message-${message.id}`;
const avatar = message.role === 'user' ? '<i class="fas fa-user"></i>' : '<i class="fas fa-robot"></i>';
const time = this.formatTime(message.timestamp);
// Add EMR icon for assistant messages (system-generated)
const emrIcon = message.role === 'assistant' ?
`<div class="message-actions">
<button class="emr-extract-btn" onclick="app.extractEMR('${message.id}')" title="Extract to EMR" data-message-id="${message.id}">
<i class="fas fa-file-medical"></i>
</button>
</div>` : '';
messageElement.innerHTML = `
<div class="message-avatar">${avatar}</div>
<div class="message-content">
<div class="message-text">${this.formatMessageContent(message.content)}</div>
<div class="message-time">${time}</div>
</div>
${emrIcon}`;
chatMessages.appendChild(messageElement);
chatMessages.scrollTop = chatMessages.scrollHeight;
if (this.currentSession) this.currentSession.lastActivity = new Date().toISOString();
}
formatMessageContent(content) {
return content
// Handle headers (1-6 # symbols) - create tagged titles
.replace(/^#{1,6}\s+(.+)$/gm, (match, text, offset, string) => {
const level = match.match(/^#+/)[0].length;
return `<div class="tagged-title">
<div class="tagged-title-bar"></div>
<div class="tagged-title-content">
<h${level}>${text}</h${level}>
</div>
</div>`;
})
// Handle bold text - create blue bubbles (improved regex to handle edge cases)
.replace(/\*\*([^*]+)\*\*/g, '<span class="blue-bubble">$1</span>')
// Handle italic text (only if not already processed as bold)
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>')
// Handle line breaks
.replace(/\n/g, '<br>')
// Handle emojis with colors
.replace(/🔍/g, '<span style="color: var(--primary-color);">🔍</span>')
.replace(/📋/g, '<span style="color: var(--secondary-color);">📋</span>')
.replace(/💊/g, '<span style="color: var(--accent-color);">💊</span>')
.replace(/📚/g, '<span style="color: var(--success-color);">📚</span>')
.replace(/⚠️/g, '<span style="color: var(--warning-color);">⚠️</span>');
}
formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) { const minutes = Math.floor(diff / 60000); return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; }
if (diff < 86400000) { const hours = Math.floor(diff / 3600000); return `${hours} hour${hours > 1 ? 's' : ''} ago`; }
return date.toLocaleDateString();
}
async extractEMR(messageId) {
try {
// Check if patient is selected
if (!this.currentPatientId) {
alert('Please select a patient before extracting EMR data.');
return;
}
// Check if doctor is logged in
if (!this.currentUser) {
alert('Please log in as a doctor before extracting EMR data.');
return;
}
// Find the message
const message = this.currentSession?.messages?.find(m => m.id === messageId);
if (!message) {
console.error('Message not found:', messageId);
return;
}
// Show loading state
const button = document.querySelector(`[onclick="app.extractEMR('${messageId}')"]`);
if (button) {
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
}
// Build URL with query parameters
const params = new URLSearchParams({
patient_id: this.currentPatientId,
doctor_id: this.currentUser.id || 'default-doctor',
message_id: messageId,
session_id: this.currentSession?.id || 'default-session',
message: message.content
});
const url = `/emr/extract?${params.toString()}`;
// Call EMR extraction API
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json', // Header might still be needed by middleware
}
});
if (response.ok) {
const result = await response.json();
console.log('EMR extraction successful:', result);
// Show success message
if (button) {
button.innerHTML = '<i class="fas fa-check"></i>';
button.style.color = 'var(--success-color)';
setTimeout(() => {
button.innerHTML = '<i class="fas fa-file-medical"></i>';
button.style.color = '';
button.disabled = false;
}, 2000);
}
// Show notification
this.showNotification('EMR data extracted successfully!', 'success');
} else {
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
const errorMessage = errorData.detail || `HTTP ${response.status}: ${response.statusText}`;
throw new Error(errorMessage);
}
} catch (error) {
console.error('Error extracting EMR:', error);
// Reset button state
const button = document.querySelector(`[onclick="app.extractEMR('${messageId}')"]`);
if (button) {
button.innerHTML = '<i class="fas fa-file-medical"></i>';
button.disabled = false;
}
// Show error message
this.showNotification('Failed to extract EMR data. Please try again.', 'error');
}
}
showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<div class="notification-content">
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
<span>${message}</span>
</div>
`;
// Add to page
document.body.appendChild(notification);
// Show notification
setTimeout(() => notification.classList.add('show'), 100);
// Remove after 3 seconds
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
}
// ----------------------------------------------------------
// Additional UI setup END
// ----------------------------------------------------------
// Initialize the app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.medicalChatbot = new MedicalChatbotApp();
});
// Handle system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
const themeSelect = document.getElementById('themeSelect');
if (themeSelect && themeSelect.value === 'auto') {
window.medicalChatbot.setTheme('auto');
}
});
// Add patient registration link under patient ID form if not present
(function ensurePatientCreateLink() {
document.addEventListener('DOMContentLoaded', () => {
const patientSection = document.querySelector('.patient-section');
const inputGroup = document.querySelector('.patient-input-group');
if (patientSection && inputGroup) {
let link = document.getElementById('createPatientLink');
if (!link) {
link = document.createElement('a');
link.id = 'createPatientLink';
link.href = '/static/patient.html';
link.className = 'patient-create-link';
link.title = 'Create new patient';
link.innerHTML = '<i class="fas fa-user-plus"></i>';
inputGroup.appendChild(link);
}
}
});
})();
// ================================================================================
// RECORDER.JS FUNCTIONALITY
// ================================================================================
class AudioRecorder {
constructor() {
this.mediaRecorder = null;
this.audioChunks = [];
this.isRecording = false;
this.audioContext = null;
this.audioStream = null;
this.analyser = null;
this.silenceTimer = null;
this.recordingStartTime = null;
this.timerInterval = null;
}
async initialize() {
try {
// Request microphone access
this.audioStream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000, // 16kHz for better speech recognition
channelCount: 1, // Mono
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
// Create audio context for voice detection
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
const source = this.audioContext.createMediaStreamSource(this.audioStream);
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 256;
this.analyser.smoothingTimeConstant = 0.8;
source.connect(this.analyser);
// Create MediaRecorder
this.mediaRecorder = new MediaRecorder(this.audioStream, {
mimeType: 'audio/webm;codecs=opus'
});
// Set up event handlers
this.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.audioChunks.push(event.data);
}
};
this.mediaRecorder.onstop = () => {
this.processRecording();
};
return true;
} catch (error) {
console.error('Failed to initialize audio recorder:', error);
throw new Error('Microphone access denied or not available');
}
}
startRecording() {
if (!this.mediaRecorder || this.isRecording) {
return false;
}
try {
this.audioChunks = [];
this.mediaRecorder.start();
this.isRecording = true;
this.recordingStartTime = Date.now();
console.log('Audio recording started');
// Start timer
this.startTimer();
// Start voice detection
this.startVoiceDetection();
return true;
} catch (error) {
console.error('Failed to start recording:', error);
return false;
}
}
stopRecording() {
if (!this.mediaRecorder || !this.isRecording) {
return false;
}
try {
this.mediaRecorder.stop();
this.isRecording = false;
console.log('Audio recording stopped');
// Stop timer and voice detection
this.stopTimer();
this.stopVoiceDetection();
return true;
} catch (error) {
console.error('Failed to stop recording:', error);
return false;
}
}
startTimer() {
this.timerInterval = setInterval(() => {
if (this.recordingStartTime) {
const elapsed = Math.floor((Date.now() - this.recordingStartTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
const timerElement = document.getElementById('recordingTimer');
if (timerElement) {
timerElement.textContent = timeString;
}
}
}, 1000);
}
stopTimer() {
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
}
}
startVoiceDetection() {
const checkVoice = () => {
if (!this.isRecording || !this.analyser) return;
const bufferLength = this.analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
this.analyser.getByteFrequencyData(dataArray);
// Calculate average volume
const average = dataArray.reduce((sum, value) => sum + value, 0) / bufferLength;
const threshold = 20; // Adjust this value to change sensitivity
const container = document.querySelector('.recording-container');
const statusElement = document.getElementById('recordingStatus');
if (average > threshold) {
// Voice detected
container.classList.remove('silent');
container.classList.add('listening');
if (statusElement) statusElement.textContent = 'Listening...';
// Reset silence timer
this.resetSilenceTimer();
} else {
// Silence detected
container.classList.remove('listening');
container.classList.add('silent');
if (statusElement) statusElement.textContent = 'Silence detected...';
}
requestAnimationFrame(checkVoice);
};
checkVoice();
}
stopVoiceDetection() {
// Voice detection stops when recording stops
}
resetSilenceTimer() {
if (this.silenceTimer) {
clearTimeout(this.silenceTimer);
}
// Auto-stop after 3 seconds of silence
this.silenceTimer = setTimeout(() => {
if (this.isRecording) {
console.log('Auto-stopping recording due to silence');
this.stopRecording();
// Trigger the modal close and processing
if (window.audioRecordingUI) {
window.audioRecordingUI.handleRecordingComplete();
}
}
}, 3000);
}
async processRecording() {
if (this.audioChunks.length === 0) {
console.warn('No audio data recorded');
return null;
}
try {
// Create audio blob
const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
// Transcribe audio
const transcribedText = await this.transcribeAudio(audioBlob);
return transcribedText;
} catch (error) {
console.error('Failed to process recording:', error);
throw error;
}
}
async transcribeAudio(audioBlob) {
try {
const formData = new FormData();
formData.append('file', audioBlob, 'recording.webm');
formData.append('language_code', 'en');
const response = await fetch('/audio/transcribe', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Transcription failed');
}
const result = await response.json();
return result.transcribed_text;
} catch (error) {
console.error('Transcription failed:', error);
throw error;
}
}
cleanup() {
if (this.audioStream) {
this.audioStream.getTracks().forEach(track => track.stop());
this.audioStream = null;
}
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
if (this.silenceTimer) {
clearTimeout(this.silenceTimer);
this.silenceTimer = null;
}
this.stopTimer();
this.mediaRecorder = null;
this.audioChunks = [];
this.isRecording = false;
}
isAvailable() {
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
}
}
// Audio recording modal UI controller
class AudioRecordingUI {
constructor(app) {
this.app = app;
this.recorder = new AudioRecorder();
this.microphoneBtn = null;
this.isInitialized = false;
this.modal = null;
}
async initialize() {
if (!this.recorder.isAvailable()) {
console.warn('Audio recording not supported in this browser');
return false;
}
try {
await this.recorder.initialize();
this.setupUI();
this.isInitialized = true;
return true;
} catch (error) {
console.error('Failed to initialize audio recording UI:', error);
this.showError('Microphone access denied. Please allow microphone access to use voice input.');
return false;
}
}
setupUI() {
this.microphoneBtn = document.getElementById('microphoneBtn');
this.modal = document.getElementById('audioRecordingModal');
if (!this.microphoneBtn) {
console.error('Microphone button not found');
return;
}
if (!this.modal) {
console.error('Audio recording modal not found');
return;
}
// Set up event listeners
this.microphoneBtn.addEventListener('click', (e) => this.startRecording(e));
// Modal close handlers
const closeBtn = document.getElementById('audioRecordingModalClose');
const stopBtn = document.getElementById('stopRecordingBtn');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.closeModal());
}
if (stopBtn) {
stopBtn.addEventListener('click', () => this.stopRecording());
}
// Close modal when clicking outside
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) {
this.closeModal();
}
});
// Update button appearance
this.updateButtonState('ready');
}
async startRecording(event) {
if (!this.isInitialized || this.recorder.isRecording) {
return;
}
event.preventDefault();
try {
// Show modal
this.showModal();
// Start recording
const success = this.recorder.startRecording();
if (success) {
this.updateModalState('listening');
} else {
this.showError('Failed to start recording. Please try again.');
this.closeModal();
}
} catch (error) {
console.error('Failed to start recording:', error);
this.showError('Failed to start recording. Please try again.');
this.closeModal();
}
}
async stopRecording() {
if (!this.recorder.isRecording) {
return;
}
try {
const success = this.recorder.stopRecording();
if (success) {
this.updateModalState('processing');
this.handleRecordingComplete();
}
} catch (error) {
console.error('Failed to stop recording:', error);
this.showError('Failed to stop recording. Please try again.');
this.closeModal();
}
}
async handleRecordingComplete() {
try {
// Process the recording
const transcribedText = await this.recorder.processRecording();
if (transcribedText) {
this.insertTranscribedText(transcribedText);
this.showSuccess('Audio transcribed successfully!');
} else {
this.showError('No speech detected. Please try again.');
}
this.closeModal();
} catch (error) {
console.error('Failed to process recording:', error);
this.showError('Transcription failed. Please try again.');
this.closeModal();
}
}
showModal() {
if (this.modal) {
this.modal.classList.add('show');
document.body.style.overflow = 'hidden'; // Prevent background scrolling
}
}
closeModal() {
if (this.modal) {
this.modal.classList.remove('show');
document.body.style.overflow = ''; // Restore scrolling
// Stop recording if still active
if (this.recorder.isRecording) {
this.recorder.stopRecording();
}
// Reset modal state
this.updateModalState('ready');
}
}
updateModalState(state) {
const container = document.querySelector('.recording-container');
const statusElement = document.getElementById('recordingStatus');
const stopBtn = document.getElementById('stopRecordingBtn');
if (!container) return;
// Remove all state classes
container.classList.remove('listening', 'silent', 'processing');
switch (state) {
case 'ready':
container.classList.add('listening');
if (statusElement) statusElement.textContent = 'Ready to record...';
if (stopBtn) stopBtn.style.display = 'none';
break;
case 'listening':
container.classList.add('listening');
if (statusElement) statusElement.textContent = 'Listening...';
if (stopBtn) stopBtn.style.display = 'block';
break;
case 'processing':
container.classList.add('processing');
if (statusElement) statusElement.textContent = 'Processing audio...';
if (stopBtn) stopBtn.style.display = 'none';
break;
}
}
insertTranscribedText(text) {
const chatInput = document.getElementById('chatInput');
if (!chatInput) {
console.error('Chat input not found');
return;
}
// Append transcribed text to existing content
const currentText = chatInput.value.trim();
const newText = currentText ? `${currentText} ${text}` : text;
chatInput.value = newText;
// Add visual feedback for transcribed text
chatInput.classList.add('transcribed');
// Remove the highlighting after a few seconds
setTimeout(() => {
chatInput.classList.remove('transcribed');
}, 3000);
// Trigger input event to update UI
chatInput.dispatchEvent(new Event('input', { bubbles: true }));
// Focus the input
chatInput.focus();
// Auto-resize if needed
if (this.app && this.app.autoResizeTextarea) {
this.app.autoResizeTextarea(chatInput);
}
}
updateButtonState(state) {
if (!this.microphoneBtn) return;
// Remove all state classes
this.microphoneBtn.classList.remove('recording-ready', 'recording-active', 'recording-processing');
// Add appropriate state class
switch (state) {
case 'ready':
this.microphoneBtn.classList.add('recording-ready');
this.microphoneBtn.title = 'Click to record voice input';
break;
case 'recording':
this.microphoneBtn.classList.add('recording-active');
this.microphoneBtn.title = 'Recording... Click to stop';
break;
case 'processing':
this.microphoneBtn.classList.add('recording-processing');
this.microphoneBtn.title = 'Processing audio...';
break;
}
}
showError(message) {
// Create or update error message
let errorMsg = document.getElementById('audioError');
if (!errorMsg) {
errorMsg = document.createElement('div');
errorMsg.id = 'audioError';
errorMsg.className = 'audio-error-message';
const chatInputContainer = document.querySelector('.chat-input-container');
if (chatInputContainer) {
chatInputContainer.appendChild(errorMsg);
}
}
errorMsg.textContent = message;
errorMsg.style.display = 'block';
// Hide after 5 seconds
setTimeout(() => {
errorMsg.style.display = 'none';
}, 5000);
}
showSuccess(message) {
// Create or update success message
let successMsg = document.getElementById('audioSuccess');
if (!successMsg) {
successMsg = document.createElement('div');
successMsg.id = 'audioSuccess';
successMsg.className = 'audio-success-message';
const chatInputContainer = document.querySelector('.chat-input-container');
if (chatInputContainer) {
chatInputContainer.appendChild(successMsg);
}
}
successMsg.textContent = message;
successMsg.style.display = 'block';
// Hide after 3 seconds
setTimeout(() => {
successMsg.style.display = 'none';
}, 3000);
}
cleanup() {
if (this.recorder) {
this.recorder.cleanup();
}
}
}
// Settings modal open/close wiring (from settings.js)
document.addEventListener('DOMContentLoaded', () => {
const settingsBtn = document.getElementById('settingsBtn');
const modal = document.getElementById('settingsModal');
const closeBtn = document.getElementById('settingsModalClose');
const cancelBtn = document.getElementById('settingsModalCancel');
if (settingsBtn && modal) settingsBtn.addEventListener('click', () => modal.classList.add('show'));
if (closeBtn) closeBtn.addEventListener('click', () => modal.classList.remove('show'));
if (cancelBtn) cancelBtn.addEventListener('click', () => modal.classList.remove('show'));
if (modal) modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('show'); });
});
// Patient modal open/close wiring (from patient.js)
document.addEventListener('DOMContentLoaded', () => {
const profileBtn = document.getElementById('patientMenuBtn');
const modal = document.getElementById('patientModal');
const closeBtn = document.getElementById('patientModalClose');
const logoutBtn = document.getElementById('patientLogoutBtn');
const createBtn = document.getElementById('patientCreateBtn');
if (profileBtn && modal) {
profileBtn.addEventListener('click', async () => {
const pid = window.medicalChatbot?.currentPatientId;
if (pid) {
try {
const resp = await fetch(`/patient/${pid}`);
if (resp.ok) {
const p = await resp.json();
const name = p.name || 'Unknown';
const age = typeof p.age === 'number' ? p.age : '-';
const sex = p.sex || '-';
const meds = Array.isArray(p.medications) && p.medications.length > 0 ? p.medications.join(', ') : '-';
document.getElementById('patientSummary').textContent = `${name}${sex}, ${age}`;
document.getElementById('patientMedications').textContent = meds;
document.getElementById('patientAssessment').textContent = p.past_assessment_summary || '-';
}
} catch (e) {
console.error('Failed to load patient profile', e);
}
}
modal.classList.add('show');
});
}
if (closeBtn && modal) {
closeBtn.addEventListener('click', () => modal.classList.remove('show'));
modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('show'); });
}
if (logoutBtn) {
logoutBtn.addEventListener('click', () => {
if (confirm('Log out current patient?')) {
window.medicalChatbot.currentPatientId = null;
localStorage.removeItem('medicalChatbotPatientId');
const status = document.getElementById('patientStatus');
if (status) { status.textContent = 'No patient selected'; status.style.color = 'var(--text-secondary)'; }
const input = document.getElementById('patientIdInput');
if (input) input.value = '';
modal.classList.remove('show');
}
});
}
if (createBtn) createBtn.addEventListener('click', () => modal.classList.remove('show'));
});
// Doctor modal open/close wiring (from doctor.js)
document.addEventListener('DOMContentLoaded', () => {
const doctorCard = document.getElementById('userProfile');
const userModal = document.getElementById('userModal');
const closeBtn = document.getElementById('userModalClose');
const cancelBtn = document.getElementById('userModalCancel');
if (doctorCard && userModal) {
doctorCard.addEventListener('click', () => userModal.classList.add('show'));
}
if (closeBtn) closeBtn.addEventListener('click', () => userModal.classList.remove('show'));
if (cancelBtn) cancelBtn.addEventListener('click', () => userModal.classList.remove('show'));
if (userModal) {
userModal.addEventListener('click', (e) => { if (e.target === userModal) userModal.classList.remove('show'); });
}
});