diff --git "a/static/js/app.js" "b/static/js/app.js"
--- "a/static/js/app.js"
+++ "b/static/js/app.js"
@@ -1,42 +1,163 @@
// 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';
class MedicalChatbotApp {
constructor() {
- this.currentUser = null;
+ this.currentUser = null; // doctor
+ this.currentPatientId = null;
this.currentSession = null;
- this.memory = new Map(); // In-memory storage for demo
+ this.backendSessions = [];
+ this.memory = new Map(); // In-memory storage for STM/demo
this.isLoading = false;
+ this.doctors = this.loadDoctors();
this.init();
}
- 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();
- // Ensure a session exists and is displayed immediately
+ 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();
}
setupEventListeners() {
// Sidebar toggle
- document.getElementById('sidebarToggle').addEventListener('click', () => {
+ 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
- document.getElementById('newChatBtn').addEventListener('click', () => {
+ const newChatBtn = document.getElementById('newChatBtn');
+ if (newChatBtn) {
+ newChatBtn.addEventListener('click', () => {
this.startNewChat();
});
+ }
// Send button and input
- document.getElementById('sendBtn').addEventListener('click', () => {
+ const sendBtn = document.getElementById('sendBtn');
+ if (sendBtn) {
+ sendBtn.addEventListener('click', () => {
this.sendMessage();
});
+ }
- document.getElementById('chatInput').addEventListener('keydown', (e) => {
+ const chatInput = document.getElementById('chatInput');
+ if (chatInput) {
+ chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
@@ -44,64 +165,105 @@ class MedicalChatbotApp {
});
// Auto-resize textarea
- document.getElementById('chatInput').addEventListener('input', (e) => {
- this.autoResizeTextarea(e.target);
- });
+ chatInput.addEventListener('input', (e) => this.autoResizeTextarea(e.target));
+ }
// User profile
- document.getElementById('userProfile').addEventListener('click', () => {
+ const userProfile = document.getElementById('userProfile');
+ if (userProfile) {
+ userProfile.addEventListener('click', () => {
+ console.log('[DEBUG] User profile clicked');
this.showUserModal();
});
+ }
// Settings
- document.getElementById('settingsBtn').addEventListener('click', () => {
+ const settingsBtn = document.getElementById('settingsBtn');
+ if (settingsBtn) {
+ settingsBtn.addEventListener('click', () => {
+ console.log('[DEBUG] Settings clicked');
this.showSettingsModal();
});
+ }
// Action buttons
- document.getElementById('exportBtn').addEventListener('click', () => {
- this.exportChat();
- });
-
- document.getElementById('clearBtn').addEventListener('click', () => {
- this.clearChat();
- });
+ 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
- document.getElementById('themeSelect').addEventListener('change', (e) => {
+ // 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
- document.getElementById('userModalClose').addEventListener('click', () => {
+ const userModalClose = document.getElementById('userModalClose');
+ const userModalCancel = document.getElementById('userModalCancel');
+ const userModalSave = document.getElementById('userModalSave');
+
+ if (userModalClose) {
+ userModalClose.addEventListener('click', () => {
this.hideModal('userModal');
});
+ }
- document.getElementById('userModalCancel').addEventListener('click', () => {
+ if (userModalCancel) {
+ userModalCancel.addEventListener('click', () => {
this.hideModal('userModal');
});
+ }
- document.getElementById('userModalSave').addEventListener('click', () => {
+ if (userModalSave) {
+ userModalSave.addEventListener('click', () => {
this.saveUserProfile();
});
+ }
// Settings modal
- document.getElementById('settingsModalClose').addEventListener('click', () => {
+ const settingsModalClose = document.getElementById('settingsModalClose');
+ const settingsModalCancel = document.getElementById('settingsModalCancel');
+ const settingsModalSave = document.getElementById('settingsModalSave');
+
+ if (settingsModalClose) {
+ settingsModalClose.addEventListener('click', () => {
this.hideModal('settingsModal');
});
+ }
- document.getElementById('settingsModalCancel').addEventListener('click', () => {
+ if (settingsModalCancel) {
+ settingsModalCancel.addEventListener('click', () => {
this.hideModal('settingsModal');
});
+ }
- document.getElementById('settingsModalSave').addEventListener('click', () => {
+ if (settingsModalSave) {
+ settingsModalSave.addEventListener('click', () => {
this.saveSettings();
});
+ }
// Close modals when clicking outside
document.querySelectorAll('.modal').forEach(modal => {
@@ -116,9 +278,14 @@ class MedicalChatbotApp {
const closeEdit = () => this.hideModal('editTitleModal');
const editTitleModal = document.getElementById('editTitleModal');
if (editTitleModal) {
- document.getElementById('editTitleModalClose').addEventListener('click', closeEdit);
- document.getElementById('editTitleModalCancel').addEventListener('click', closeEdit);
- document.getElementById('editTitleModalSave').addEventListener('click', () => {
+ 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;
@@ -128,6 +295,7 @@ class MedicalChatbotApp {
input.value = '';
this.hideModal('editTitleModal');
});
+ }
}
}
@@ -151,58 +319,9 @@ class MedicalChatbotApp {
this.updateUserDisplay();
}
- loadUserPreferences() {
- const preferences = localStorage.getItem('medicalChatbotPreferences');
- if (preferences) {
- const prefs = JSON.parse(preferences);
- this.setTheme(prefs.theme || 'auto');
- this.setFontSize(prefs.fontSize || 'medium');
- }
- }
-
- setupTheme() {
- // Check system preference
- if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
- this.setTheme('auto');
- }
- }
-
- setTheme(theme) {
- const root = document.documentElement;
-
- if (theme === 'auto') {
- const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
- root.setAttribute('data-theme', isDark ? 'dark' : 'light');
- } else {
- root.setAttribute('data-theme', theme);
- }
-
- // Update select element
- document.getElementById('themeSelect').value = theme;
-
- // Save preference
- this.savePreferences();
- }
-
- setFontSize(size) {
- const root = document.documentElement;
- root.style.fontSize = size === 'small' ? '14px' : size === 'large' ? '18px' : '16px';
-
- // Save preference
- this.savePreferences();
- }
-
- savePreferences() {
- const preferences = {
- theme: document.getElementById('themeSelect').value,
- fontSize: document.getElementById('fontSize').value
- };
- localStorage.setItem('medicalChatbotPreferences', JSON.stringify(preferences));
- }
-
startNewChat() {
if (this.currentSession) {
- // Save current session
+ // Save current session (local only)
this.saveCurrentSession();
}
@@ -230,6 +349,10 @@ class MedicalChatbotApp {
}
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
@@ -253,302 +376,343 @@ class MedicalChatbotApp {
getWelcomeMessage() {
return `👋 Welcome to Medical AI Assistant
-
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)
-
**Important:** This is for informational purposes only. Always consult with qualified healthcare professionals for medical advice.
-
How can I assist you today?`;
}
- async sendMessage() {
- const input = document.getElementById('chatInput');
- const message = input.value.trim();
-
- if (!message || this.isLoading) return;
-
- // Clear input
- input.value = '';
- this.autoResizeTextarea(input);
-
- // Add user message
- this.addMessage('user', message);
+ clearChatMessages() {
+ const chatMessages = document.getElementById('chatMessages');
+ chatMessages.innerHTML = '';
+ }
- // Show loading
- this.showLoading(true);
+ 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);
+ }
+ }
- try {
- // Send to API
- const response = await this.callMedicalAPI(message);
+ 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;
- // Add assistant response
- this.addMessage('assistant', response);
+ console.log('[Settings] save', { theme, fontSize, autoSave, notifications });
+ this.setTheme(theme);
+ this.setFontSize(fontSize);
- // Update session
- this.updateCurrentSession();
+ // Save additional preferences
+ const preferences = {
+ theme: theme,
+ fontSize: fontSize,
+ autoSave: autoSave,
+ notifications: notifications
+ };
+ console.log('[Prefs] write', preferences);
+ localStorage.setItem('medicalChatbotPreferences', JSON.stringify(preferences));
- } catch (error) {
- console.error('Error sending message:', error);
+ this.hideModal('settingsModal');
+ }
- // Show more specific error messages
- let errorMessage = 'I apologize, but I encountered an error processing your request.';
+ updateUserDisplay() {
+ document.getElementById('userName').textContent = this.currentUser.name;
+ document.getElementById('userStatus').textContent = this.currentUser.role;
+ }
- 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.';
- }
+ saveUser() {
+ localStorage.setItem('medicalChatbotUser', JSON.stringify(this.currentUser));
+ }
- this.addMessage('assistant', errorMessage);
- } finally {
- this.showLoading(false);
- }
+ generateId() {
+ return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
- async callMedicalAPI(message) {
- try {
- const response = await fetch('/chat', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- user_id: this.currentUser.id,
- session_id: this.currentSession?.id || 'default',
- message: message,
- user_role: this.currentUser.role,
- user_specialty: this.currentUser.specialty,
- title: this.currentSession?.title || 'New Chat'
- })
- });
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const data = await response.json();
- return data.response || 'I apologize, but I received an empty response. Please try again.';
+// ----------------------------------------------------------
+// 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.
+// ----------------------------------------------------------
- } catch (error) {
- console.error('API call failed:', error);
- // Log detailed error information
- console.error('Error details:', {
- message: error.message,
- stack: error.stack,
- user: this.currentUser,
- session: this.currentSession
- });
- // Only return mock response if it's a network error, not a server error
- if (error.name === 'TypeError' && error.message.includes('fetch')) {
- return this.generateMockResponse(message);
+ // ================================================================================
+ // 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 {
- throw error; // Re-throw server errors to show proper error message
- }
+ console.error('[DEBUG] Sidebar element not found');
}
}
- 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)];
+ autoResizeTextarea(textarea) {
+ if (!textarea) return;
+ textarea.style.height = 'auto';
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
- addMessage(role, content) {
- if (!this.currentSession) {
- this.startNewChat();
+ exportChat() {
+ if (!this.currentSession || this.currentSession.messages.length === 0) {
+ alert('No chat to export.');
+ return;
}
-
- const message = {
- id: this.generateId(),
- role: role,
- content: content,
- timestamp: new Date().toISOString()
+ 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);
+ }
- this.currentSession.messages.push(message);
+ 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();
+ }
+ }
+ }
- // Update UI
- this.displayMessage(message);
+ 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);
+ }
+ }
- // Update session title if it's the first user message -> call summariser
- if (role === 'user' && this.currentSession.messages.length === 2) {
- this.summariseAndSetTitle(content);
+ 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');
}
- async summariseAndSetTitle(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();
+ 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 {
- // Fallback: simple truncation
- const fallback = text.length > 50 ? text.substring(0, 50) + '...' : text;
- this.currentSession.title = fallback;
- this.updateCurrentSession();
- this.updateChatTitle();
- this.loadChatSessions();
+ // If doctor not found in local list, try to fetch from backend
+ try {
+ const resp = await fetch(`/doctors/search?q=${encodeURIComponent(selectedName)}&limit=1`);
+ if (resp.ok) {
+ const data = await resp.json();
+ const doctor = data.results && data.results[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);
+ }
}
- } catch (e) {
- const fallback = text.length > 50 ? text.substring(0, 50) + '...' : text;
- this.currentSession.title = fallback;
- this.updateCurrentSession();
- this.updateChatTitle();
- this.loadChatSessions();
- }
+ };
+
+ sel.addEventListener('change', this.handleDoctorSelection);
}
- 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}`;
+ showSettingsModal() {
+ console.log('[DEBUG] showSettingsModal called');
+ this.showModal('settingsModal');
+ }
+
- const avatar = message.role === 'user' ?
- '' :
- '';
+ // ================================================================================
+ // 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;
+ }
- const time = this.formatTime(message.timestamp);
+ 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);
+ }
- messageElement.innerHTML = `
-
- ${avatar}
-
-
-
- ${this.formatMessageContent(message.content)}
-
-
${time}
-
- `;
-
- chatMessages.appendChild(messageElement);
-
- // Scroll to bottom
- chatMessages.scrollTop = chatMessages.scrollHeight;
-
- // Add to session if it exists
- if (this.currentSession) {
- this.currentSession.lastActivity = new Date().toISOString();
- }
- }
-
- formatMessageContent(content) {
- // Convert markdown-like syntax to HTML
- return content
- .replace(/\*\*(.*?)\*\*/g, '$1')
- .replace(/\*(.*?)\*/g, '$1')
- .replace(/\n/g, '
')
- .replace(/🔍/g, '🔍')
- .replace(/📋/g, '📋')
- .replace(/💊/g, '💊')
- .replace(/📚/g, '📚')
- .replace(/⚠️/g, '⚠️');
+ 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);
}
- formatTime(timestamp) {
- const date = new Date(timestamp);
- const now = new Date();
- const diff = now - date;
-
- if (diff < 60000) { // Less than 1 minute
- return 'Just now';
- } else if (diff < 3600000) { // Less than 1 hour
- const minutes = Math.floor(diff / 60000);
- return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
- } else if (diff < 86400000) { // Less than 1 day
- const hours = Math.floor(diff / 3600000);
- return `${hours} hour${hours > 1 ? 's' : ''} ago`;
- } else {
- return date.toLocaleDateString();
+ setupTheme() {
+ const themeSelect = document.getElementById('themeSelect');
+ if (themeSelect) {
+ const prefs = JSON.parse(localStorage.getItem('medicalChatbotPreferences') || '{}');
+ themeSelect.value = prefs.theme || 'auto';
}
}
- clearChatMessages() {
- const chatMessages = document.getElementById('chatMessages');
- chatMessages.innerHTML = '';
+ 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));
}
- 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();
+ showLoading(show) {
+ const overlay = document.getElementById('loadingOverlay');
+ if (overlay) {
+ if (show) {
+ overlay.classList.add('show');
+ } else {
+ overlay.classList.remove('show');
}
}
+ this.isLoading = show;
}
- exportChat() {
- if (!this.currentSession || this.currentSession.messages.length === 0) {
- alert('No chat to export.');
- return;
- }
- const chatData = {
- user: this.currentUser.name,
- session: this.currentSession.title,
- date: new Date().toISOString(),
- messages: this.currentSession.messages
- };
+ // ================================================================================
+ // SESSIONS.JS FUNCTIONALITY
+ // ================================================================================
+ getChatSessions() {
+ const sessions = localStorage.getItem(`chatSessions_${this.currentUser.id}`);
+ return sessions ? JSON.parse(sessions) : [];
+ }
- 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);
+ 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 = '';
-
- // Get sessions from localStorage
- const sessions = this.getChatSessions();
-
+ const sessions = (this.backendSessions && this.backendSessions.length > 0) ? this.backendSessions : this.getChatSessions();
if (sessions.length === 0) {
sessionsContainer.innerHTML = 'No chat sessions yet
';
return;
}
-
sessions.forEach(session => {
const sessionElement = document.createElement('div');
sessionElement.className = `chat-session ${session.id === this.currentSession?.id ? 'active' : ''}`;
- sessionElement.addEventListener('click', () => {
+ sessionElement.addEventListener('click', async () => {
+ if (session.source === 'backend') {
+ this.currentSession = { ...session };
+ await this.hydrateMessagesForSession(session.id);
+ } else {
this.loadChatSession(session.id);
+ }
});
-
const time = this.formatTime(session.lastActivity);
-
sessionElement.innerHTML = `
@@ -562,18 +726,161 @@ How can I assist you today?`;
`;
-
sessionsContainer.appendChild(sessionElement);
-
- // Wire 3-dot menu
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(`/sessions/${sessionId}`, {
+ method: 'DELETE',
+ headers: { 'Content-Type': 'application/json' }
+ });
+
+ if (!resp.ok) {
+ throw new Error(`HTTP ${resp.status}`);
+ }
+
+ const result = await resp.json();
+ console.log('[DEBUG] Backend deletion result:', result);
+
+ // 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());
@@ -585,10 +892,8 @@ How can I assist you today?`;
`;
document.body.appendChild(pop);
- // Position near button
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();
@@ -596,19 +901,17 @@ How can I assist you today?`;
}
};
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') {
+ 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');
- input.value = s ? s.title : '';
+ if (input) input.value = s ? s.title : '';
this.showModal('editTitleModal');
}
pop.remove();
@@ -616,43 +919,14 @@ How can I assist you today?`;
});
}
- loadChatSession(sessionId) {
- const sessions = this.getChatSessions();
- const session = sessions.find(s => s.id === sessionId);
-
- if (!session) return;
-
- this.currentSession = session;
-
- // Clear and reload messages
- this.clearChatMessages();
- session.messages.forEach(message => {
- this.displayMessage(message);
- });
-
- // Update UI
- this.updateChatTitle();
- this.loadChatSessions();
- }
-
- getChatSessions() {
- const sessions = localStorage.getItem(`chatSessions_${this.currentUser.id}`);
- return sessions ? JSON.parse(sessions) : [];
+ updateBackendSession(sessionId, updates) {
+ // This would call the backend API to update session metadata
+ console.log('Updating backend session:', sessionId, updates);
}
- saveCurrentSession() {
- if (!this.currentSession) return;
-
- 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));
+ deleteBackendSession(sessionId) {
+ // This would call the backend API to delete the session
+ console.log('Deleting backend session:', sessionId);
}
updateCurrentSession() {
@@ -662,154 +936,902 @@ How can I assist you today?`;
}
}
- updateChatTitle() {
- const titleElement = document.getElementById('chatTitle');
- if (this.currentSession) {
- titleElement.textContent = this.currentSession.title;
- } else {
- titleElement.textContent = 'Medical AI Assistant';
- }
- }
-
- deleteChatSession(sessionId) {
- const sessions = this.getChatSessions();
- const index = sessions.findIndex(s => s.id === sessionId);
- if (index === -1) return;
-
- const confirmDelete = confirm('Delete this chat session? This cannot be undone.');
- if (!confirmDelete) return;
-
- sessions.splice(index, 1);
- localStorage.setItem(`chatSessions_${this.currentUser.id}`, JSON.stringify(sessions));
- // If deleting the current session, switch to another or clear view
- if (this.currentSession && this.currentSession.id === sessionId) {
- if (sessions.length > 0) {
- this.currentSession = sessions[0];
- this.clearChatMessages();
- this.currentSession.messages.forEach(m => this.displayMessage(m));
- this.updateChatTitle();
+ // ================================================================================
+ // DOCTOR.JS FUNCTIONALITY
+ // ================================================================================
+ async loadDoctors() {
+ try {
+ // Fetch doctors from MongoDB
+ const resp = await fetch('/doctors');
+ if (resp.ok) {
+ const data = await resp.json();
+ this.doctors = data.results || [];
+ // 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 || doctor.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 {
- this.currentSession = null;
- this.clearChatMessages();
- this.updateChatTitle();
+ // 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;
}
+ }
- this.loadChatSessions();
+ async searchDoctors(query) {
+ try {
+ const resp = await fetch(`/doctors/search?q=${encodeURIComponent(query)}&limit=10`);
+ if (resp.ok) {
+ const data = await resp.json();
+ return data.results || [];
+ }
+ } catch (e) {
+ console.warn('Doctor search failed:', e);
+ }
+ return [];
}
- 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();
+ async createDoctor(doctorData) {
+ try {
+ const resp = await fetch('/doctors', {
+ 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.doctor_id });
+ this.saveDoctors();
+ return data;
+ }
+ } catch (e) {
+ console.error('Failed to create doctor:', e);
}
- this.loadChatSessions();
+ return null;
}
- showUserModal() {
- // Populate form with current user data
- document.getElementById('profileName').value = this.currentUser.name;
- document.getElementById('profileRole').value = this.currentUser.role;
- document.getElementById('profileSpecialty').value = this.currentUser.specialty || '';
+ saveDoctors() {
+ localStorage.setItem('medicalChatbotDoctors', JSON.stringify(this.doctors));
+ }
- this.showModal('userModal');
+ 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() || '';
+
+ // Create doctor in MongoDB
+ const result = await this.createDoctor({
+ name,
+ role,
+ specialty,
+ medical_roles: [role]
+ });
+ if (result) {
+ this.doctors.unshift({
+ name,
+ role,
+ specialty,
+ _id: result.doctor_id
+ });
+ this.saveDoctors();
+
+ // Update current user profile
+ 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';
+ };
}
- saveUserProfile() {
- const name = document.getElementById('profileName').value.trim();
+ 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) {
- alert('Please enter a name.');
+ if (!name || name === '__create__') {
+ alert('Please select or create a doctor name.');
return;
}
+ // Check if doctor exists in MongoDB first
+ let doctorExists = false;
+ try {
+ const resp = await fetch(`/doctors/${encodeURIComponent(name)}`);
+ doctorExists = resp.ok;
+ } catch (e) {
+ console.warn('Failed to check doctor existence:', e);
+ }
+
+ // Update current user profile
this.currentUser.name = name;
this.currentUser.role = role;
this.currentUser.specialty = specialty;
-
this.saveUser();
this.updateUserDisplay();
- this.hideModal('userModal');
- }
-
- showSettingsModal() {
- this.showModal('settingsModal');
- }
- 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;
+ // Update local doctors list
+ const existingDoctorIndex = this.doctors.findIndex(d => d.name === name);
+ if (existingDoctorIndex === -1) {
+ // Add new doctor to local list
+ this.doctors.unshift({
+ name,
+ role,
+ specialty
+ });
+ } else {
+ // Update existing doctor in local list
+ this.doctors[existingDoctorIndex] = {
+ ...this.doctors[existingDoctorIndex],
+ role: role,
+ specialty: specialty
+ };
+ }
+ this.saveDoctors();
+
+ // Only create new doctor in MongoDB if it doesn't exist
+ if (!doctorExists) {
+ const doctorPayload = {
+ name: name,
+ role: role,
+ specialty: specialty || null,
+ medical_roles: [role]
+ };
+
+ try {
+ const resp = await fetch('/doctors', {
+ 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);
+
+ // Update local doctor with the ID from backend
+ const localDoctor = this.doctors.find(d => d.name === name);
+ if (localDoctor) {
+ localDoctor._id = data.doctor_id;
+ this.saveDoctors();
+ }
+ } catch (err) {
+ console.warn('[Doctor] failed to create doctor in backend:', err);
+ }
+ } else {
+ console.log('[Doctor] Doctor already exists in backend, no creation needed');
+ }
- this.setTheme(theme);
- this.setFontSize(fontSize);
+ this.hideModal('userModal');
+ }
- // Save additional preferences
- const preferences = {
- theme: theme,
- fontSize: fontSize,
- autoSave: autoSave,
- notifications: notifications
+ // ================================================================================
+ // PATIENT.JS FUNCTIONALITY
+ // ================================================================================
+
+ async getLocalStorageSuggestions(query) {
+ try {
+ const storedPatients = JSON.parse(localStorage.getItem('medicalChatbotPatients') || '[]');
+ return storedPatients.filter(p => {
+ // Check name match (case-insensitive contains)
+ const nameMatch = p.name.toLowerCase().includes(query.toLowerCase());
+ // Check patient_id match
+ let idMatch = p.patient_id.includes(query);
+ // Special handling for numeric queries - check if patient_id starts with the query
+ if (/^\d+$/.test(query)) {
+ idMatch = p.patient_id.startsWith(query) || p.patient_id.includes(query);
+ }
+ return nameMatch || idMatch;
+ });
+ } catch (e) {
+ console.warn('Failed to get localStorage suggestions:', e);
+ return [];
+ }
+ }
+
+ combinePatientResults(mongoResults, localResults) {
+ // Create a map to deduplicate by patient_id, with MongoDB results taking priority
+ const resultMap = new Map();
+
+ // Add MongoDB results first (they take priority)
+ mongoResults.forEach(patient => {
+ resultMap.set(patient.patient_id, patient);
+ });
+
+ // Add localStorage results only if not already present
+ localResults.forEach(patient => {
+ if (!resultMap.has(patient.patient_id)) {
+ resultMap.set(patient.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 && /^\d{8}$/.test(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(`/patients/${pid}`);
+ if (resp.ok) {
+ const patient = await resp.json();
+ status.textContent = `Patient: ${patient.name || 'Unknown'} (${pid})`;
+ } 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) {
+ 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;
+ }
+
+ // If it's a complete 8-digit ID, use it directly
+ if (/^\d{8}$/.test(value)) {
+ console.log('[DEBUG] Valid 8-digit ID provided');
+ this.currentPatientId = value;
+ this.savePatientId();
+ // Try to get patient name for display
+ try {
+ const resp = await fetch(`/patients/${value}`);
+ if (resp.ok) {
+ const patient = await resp.json();
+ this.updatePatientDisplay(value, patient.name || 'Unknown');
+ } else {
+ this.updatePatientDisplay(value);
+ }
+ } catch (e) {
+ this.updatePatientDisplay(value);
+ }
+ await this.fetchAndRenderPatientSessions();
+ return;
+ }
+
+ // Otherwise, search for patient by name or partial ID
+ console.log('[DEBUG] Searching for patient by name/partial ID');
+ try {
+ const resp = await fetch(`/patients/search?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.results || [])[0];
+ if (first) {
+ console.log('[DEBUG] Found patient, setting as current:', first);
+ this.currentPatientId = first.patient_id;
+ this.savePatientId();
+ input.value = first.patient_id;
+ this.updatePatientDisplay(first.patient_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(`/patients/${this.currentPatientId}/sessions`);
+ if (resp.ok) {
+ const data = await resp.json();
+ sessions = Array.isArray(data.sessions) ? data.sessions : [];
+
+ // 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.session_id,
+ title: s.title || 'New Chat',
+ messages: [],
+ createdAt: s.created_at || new Date().toISOString(),
+ lastActivity: s.last_activity || 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) {
+ 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(`/sessions/${sessionId}/messages?patient_id=${this.currentPatientId}&limit=1000`);
+ 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.messages) ? data.messages : [];
+ messages = msgs.map(m => ({
+ id: m._id || this.generateId(),
+ role: m.role,
+ 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.patient_id})`;
+ div.addEventListener('click', async () => {
+ this.currentPatientId = p.patient_id;
+ this.savePatientId();
+ patientInput.value = p.patient_id;
+ hideSuggestions();
+ this.updatePatientDisplay(p.patient_id, p.name || 'Unknown');
+ await this.fetchAndRenderPatientSessions();
+ });
+ suggestionsEl.appendChild(div);
+ });
+ suggestionsEl.style.display = 'block';
};
- localStorage.setItem('medicalChatbotPreferences', JSON.stringify(preferences));
+ 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 = `/patients/search?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.results || [];
+ 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();
+ });
+ }
- this.hideModal('settingsModal');
+ // 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(`/patients/${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'));
}
- showModal(modalId) {
- document.getElementById(modalId).classList.add('show');
+ // ================================================================================
+ // MESSAGING.JS FUNCTIONALITY
+ // ================================================================================
+ // Cache invalidation methods
+ invalidateSessionCache(patientId) {
+ const cacheKey = `sessions_${patientId}`;
+ localStorage.removeItem(cacheKey);
+ console.log('[DEBUG] Invalidated session cache for patient:', patientId);
}
- hideModal(modalId) {
- document.getElementById(modalId).classList.remove('show');
+ invalidateMessageCache(patientId, sessionId) {
+ const cacheKey = `messages_${patientId}_${sessionId}`;
+ localStorage.removeItem(cacheKey);
+ console.log('[DEBUG] Invalidated message cache for session:', sessionId);
}
- showLoading(show) {
- this.isLoading = show;
- const overlay = document.getElementById('loadingOverlay');
- const sendBtn = document.getElementById('sendBtn');
+ 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);
+ }
- if (show) {
- overlay.classList.add('show');
- sendBtn.disabled = true;
- } else {
- overlay.classList.remove('show');
- sendBtn.disabled = false;
+ 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 response = await this.callMedicalAPI(message);
+ this.addMessage('assistant', response);
+ 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);
}
}
- toggleSidebar() {
- const sidebar = document.getElementById('sidebar');
- sidebar.classList.toggle('show');
+ callMedicalAPI = async function (message) {
+ try {
+ const response = await fetch('/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ user_id: this.currentUser.id,
+ patient_id: this.currentPatientId,
+ doctor_id: this.currentUser.id,
+ session_id: this.currentSession?.id || 'default',
+ message: message,
+ user_role: this.currentUser.role,
+ user_specialty: this.currentUser.specialty,
+ title: this.currentSession?.title || 'New Chat'
+ })
+ });
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
+ const data = await response.json();
+ return data.response || 'I apologize, but I received an empty response. Please try again.';
+ } 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 this.generateMockResponse(message);
+ throw error;
+ }
}
- updateUserDisplay() {
- document.getElementById('userName').textContent = this.currentUser.name;
- document.getElementById('userStatus').textContent = this.currentUser.role;
+ 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)];
}
- saveUser() {
- localStorage.setItem('medicalChatbotUser', JSON.stringify(this.currentUser));
+ 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);
}
- autoResizeTextarea(textarea) {
- textarea.style.height = 'auto';
- textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
+ // 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);
+ }
}
- generateId() {
- return Date.now().toString(36) + Math.random().toString(36).substr(2);
+ 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' ? '' : '';
+ const time = this.formatTime(message.timestamp);
+ messageElement.innerHTML = `
+ ${avatar}
+
+
${this.formatMessageContent(message.content)}
+
${time}
+
`;
+ 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)
+ .replace(/^#{1,6}\s+(.+)$/gm, (match, text, offset, string) => {
+ const level = match.match(/^#+/)[0].length;
+ return `${text}`;
+ })
+ // Handle bold text
+ .replace(/\*\*(.*?)\*\*/g, '$1')
+ // Handle italic text
+ .replace(/\*(.*?)\*/g, '$1')
+ // Handle line breaks
+ .replace(/\n/g, '
')
+ // Handle emojis with colors
+ .replace(/🔍/g, '🔍')
+ .replace(/📋/g, '📋')
+ .replace(/💊/g, '💊')
+ .replace(/📚/g, '📚')
+ .replace(/⚠️/g, '⚠️');
+ }
+
+ 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();
}
}
+// ----------------------------------------------------------
+// Additional UI setup END
+// ----------------------------------------------------------
+
// Initialize the app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
@@ -823,3 +1845,101 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e)
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 = '';
+ inputGroup.appendChild(link);
+ }
+ }
+ });
+})();
+
+// 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(`/patients/${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'); });
+ }
+});
\ No newline at end of file