Spaces:
Runtime error
Runtime error
dylanglenister
commited on
Commit
·
2e2a4b0
1
Parent(s):
e8cda17
Fixing whitespace
Browse files- static/css/styles.css +10 -10
- static/js/app.js +146 -146
static/css/styles.css
CHANGED
|
@@ -707,12 +707,12 @@ body {
|
|
| 707 |
.wave-bar:nth-child(8) { animation-delay: 0.7s; }
|
| 708 |
|
| 709 |
@keyframes wave {
|
| 710 |
-
0%, 100% {
|
| 711 |
-
height: 20px;
|
| 712 |
background-color: var(--primary-color);
|
| 713 |
}
|
| 714 |
-
50% {
|
| 715 |
-
height: 40px;
|
| 716 |
background-color: var(--accent-color);
|
| 717 |
}
|
| 718 |
}
|
|
@@ -1184,12 +1184,12 @@ body {
|
|
| 1184 |
.patient-load-btn:hover {
|
| 1185 |
background: var(--primary-hover);
|
| 1186 |
}
|
| 1187 |
-
.patient-create-link {
|
| 1188 |
-
display:inline-flex; align-items:center;
|
| 1189 |
-
justify-content:center; padding:8px 10px;
|
| 1190 |
-
border: 1px solid var(--border-color);
|
| 1191 |
-
border-radius:6px; color: var(--text-secondary);
|
| 1192 |
-
text-decoration:none;
|
| 1193 |
}
|
| 1194 |
.patient-create-link:hover { background: var(--bg-tertiary); }
|
| 1195 |
|
|
|
|
| 707 |
.wave-bar:nth-child(8) { animation-delay: 0.7s; }
|
| 708 |
|
| 709 |
@keyframes wave {
|
| 710 |
+
0%, 100% {
|
| 711 |
+
height: 20px;
|
| 712 |
background-color: var(--primary-color);
|
| 713 |
}
|
| 714 |
+
50% {
|
| 715 |
+
height: 40px;
|
| 716 |
background-color: var(--accent-color);
|
| 717 |
}
|
| 718 |
}
|
|
|
|
| 1184 |
.patient-load-btn:hover {
|
| 1185 |
background: var(--primary-hover);
|
| 1186 |
}
|
| 1187 |
+
.patient-create-link {
|
| 1188 |
+
display:inline-flex; align-items:center;
|
| 1189 |
+
justify-content:center; padding:8px 10px;
|
| 1190 |
+
border: 1px solid var(--border-color);
|
| 1191 |
+
border-radius:6px; color: var(--text-secondary);
|
| 1192 |
+
text-decoration:none;
|
| 1193 |
}
|
| 1194 |
.patient-create-link:hover { background: var(--bg-tertiary); }
|
| 1195 |
|
static/js/app.js
CHANGED
|
@@ -49,7 +49,7 @@ class MedicalChatbotApp {
|
|
| 49 |
// Ensure a session exists and is displayed immediately if nothing to show
|
| 50 |
this.ensureStartupSession();
|
| 51 |
this.loadChatSessions();
|
| 52 |
-
|
| 53 |
// Bind patient handlers
|
| 54 |
console.log('[DEBUG] Binding patient handlers');
|
| 55 |
this.bindPatientHandlers();
|
|
@@ -58,7 +58,7 @@ class MedicalChatbotApp {
|
|
| 58 |
const prefs = JSON.parse(localStorage.getItem('medicalChatbotPreferences') || '{}');
|
| 59 |
this.setTheme(prefs.theme || 'auto');
|
| 60 |
this.setupTheme();
|
| 61 |
-
|
| 62 |
// Initialize audio recording (guarded if module not present)
|
| 63 |
try {
|
| 64 |
if (typeof AudioRecordingUI !== 'undefined') {
|
|
@@ -79,7 +79,7 @@ class MedicalChatbotApp {
|
|
| 79 |
this.toggleSidebar();
|
| 80 |
});
|
| 81 |
}
|
| 82 |
-
|
| 83 |
// Click outside sidebar to close (mobile/overlay behavior)
|
| 84 |
const overlay = document.getElementById('appOverlay');
|
| 85 |
console.log('[DEBUG] Overlay element found:', !!overlay);
|
|
@@ -97,27 +97,27 @@ class MedicalChatbotApp {
|
|
| 97 |
}
|
| 98 |
}
|
| 99 |
};
|
| 100 |
-
|
| 101 |
// Keep overlay synced when toggling
|
| 102 |
const origToggle = this.toggleSidebar.bind(this);
|
| 103 |
-
this.toggleSidebar = () => {
|
| 104 |
console.log('[DEBUG] Wrapped toggleSidebar called');
|
| 105 |
-
origToggle();
|
| 106 |
-
updateOverlay();
|
| 107 |
};
|
| 108 |
-
|
| 109 |
// Initialize overlay state - ensure it's hidden on startup
|
| 110 |
if (overlay) {
|
| 111 |
overlay.classList.remove('show');
|
| 112 |
}
|
| 113 |
updateOverlay();
|
| 114 |
-
|
| 115 |
// Handle window resize for responsive behavior
|
| 116 |
window.addEventListener('resize', () => {
|
| 117 |
console.log('[DEBUG] Window resized, updating overlay');
|
| 118 |
updateOverlay();
|
| 119 |
});
|
| 120 |
-
|
| 121 |
// Click outside to close sidebar
|
| 122 |
document.addEventListener('click', (e) => {
|
| 123 |
const sidebar = document.getElementById('sidebar');
|
|
@@ -127,9 +127,9 @@ class MedicalChatbotApp {
|
|
| 127 |
const isOpen = sidebar.classList.contains('show');
|
| 128 |
const clickInside = sidebar.contains(e.target) || (toggleBtn && toggleBtn.contains(e.target));
|
| 129 |
const clickOnOverlay = overlay && overlay.contains(e.target);
|
| 130 |
-
|
| 131 |
console.log('[DEBUG] Click event - sidebar open:', isOpen, 'click inside:', clickInside, 'click on overlay:', clickOnOverlay);
|
| 132 |
-
|
| 133 |
if (isOpen && !clickInside) {
|
| 134 |
if (clickOnOverlay) {
|
| 135 |
console.log('[DEBUG] Clicked on overlay, closing sidebar');
|
|
@@ -145,7 +145,7 @@ class MedicalChatbotApp {
|
|
| 145 |
}
|
| 146 |
updateOverlay();
|
| 147 |
}, true);
|
| 148 |
-
|
| 149 |
if (overlay) {
|
| 150 |
overlay.addEventListener('click', () => {
|
| 151 |
console.log('[DEBUG] Overlay clicked directly');
|
|
@@ -239,7 +239,7 @@ class MedicalChatbotApp {
|
|
| 239 |
const userModalClose = document.getElementById('userModalClose');
|
| 240 |
const userModalCancel = document.getElementById('userModalCancel');
|
| 241 |
const userModalSave = document.getElementById('userModalSave');
|
| 242 |
-
|
| 243 |
if (userModalClose) {
|
| 244 |
userModalClose.addEventListener('click', () => {
|
| 245 |
this.hideModal('userModal');
|
|
@@ -262,7 +262,7 @@ class MedicalChatbotApp {
|
|
| 262 |
const settingsModalClose = document.getElementById('settingsModalClose');
|
| 263 |
const settingsModalCancel = document.getElementById('settingsModalCancel');
|
| 264 |
const settingsModalSave = document.getElementById('settingsModalSave');
|
| 265 |
-
|
| 266 |
if (settingsModalClose) {
|
| 267 |
settingsModalClose.addEventListener('click', () => {
|
| 268 |
this.hideModal('settingsModal');
|
|
@@ -297,7 +297,7 @@ class MedicalChatbotApp {
|
|
| 297 |
const editTitleModalClose = document.getElementById('editTitleModalClose');
|
| 298 |
const editTitleModalCancel = document.getElementById('editTitleModalCancel');
|
| 299 |
const editTitleModalSave = document.getElementById('editTitleModalSave');
|
| 300 |
-
|
| 301 |
if (editTitleModalClose) editTitleModalClose.addEventListener('click', closeEdit);
|
| 302 |
if (editTitleModalCancel) editTitleModalCancel.addEventListener('click', closeEdit);
|
| 303 |
if (editTitleModalSave) {
|
|
@@ -485,7 +485,7 @@ How can I assist you today?`;
|
|
| 485 |
// Our submodules aren't lodaed on app.js, so we need to add them here
|
| 486 |
// Perhaps this is FastAPI limitation, remove this when proper deploy this
|
| 487 |
// On UI specific hosting site.
|
| 488 |
-
// ----------------------------------------------------------
|
| 489 |
|
| 490 |
|
| 491 |
// ================================================================================
|
|
@@ -564,16 +564,16 @@ How can I assist you today?`;
|
|
| 564 |
sel.appendChild(createOpt);
|
| 565 |
}
|
| 566 |
if (sel && !sel.value) sel.value = this.currentUser?.name || '__create__';
|
| 567 |
-
|
| 568 |
// Safely set role and specialty with null checks
|
| 569 |
const roleEl = document.getElementById('profileRole');
|
| 570 |
const specialtyEl = document.getElementById('profileSpecialty');
|
| 571 |
if (roleEl) roleEl.value = (this.currentUser && this.currentUser.role) ? this.currentUser.role : 'Medical Professional';
|
| 572 |
if (specialtyEl) specialtyEl.value = (this.currentUser && this.currentUser.specialty) ? this.currentUser.specialty : '';
|
| 573 |
-
|
| 574 |
// Add event listener for doctor selection changes
|
| 575 |
this.setupDoctorSelectionHandler();
|
| 576 |
-
|
| 577 |
this.showModal('userModal');
|
| 578 |
}
|
| 579 |
|
|
@@ -581,24 +581,24 @@ How can I assist you today?`;
|
|
| 581 |
const sel = document.getElementById('profileNameSelect');
|
| 582 |
const roleEl = document.getElementById('profileRole');
|
| 583 |
const specialtyEl = document.getElementById('profileSpecialty');
|
| 584 |
-
|
| 585 |
if (!sel || !roleEl || !specialtyEl) return;
|
| 586 |
-
|
| 587 |
// Remove existing listeners to avoid duplicates
|
| 588 |
sel.removeEventListener('change', this.handleDoctorSelection);
|
| 589 |
-
|
| 590 |
// Add new listener
|
| 591 |
this.handleDoctorSelection = async (event) => {
|
| 592 |
const selectedName = event.target.value;
|
| 593 |
console.log('[DEBUG] Doctor selected:', selectedName);
|
| 594 |
-
|
| 595 |
if (selectedName === '__create__') {
|
| 596 |
// Reset to default values for new doctor
|
| 597 |
roleEl.value = 'Medical Professional';
|
| 598 |
specialtyEl.value = '';
|
| 599 |
return;
|
| 600 |
}
|
| 601 |
-
|
| 602 |
// Find the selected doctor in our doctors list
|
| 603 |
const selectedDoctor = this.doctors.find(d => d.name === selectedName);
|
| 604 |
if (selectedDoctor) {
|
|
@@ -632,7 +632,7 @@ How can I assist you today?`;
|
|
| 632 |
}
|
| 633 |
}
|
| 634 |
};
|
| 635 |
-
|
| 636 |
sel.addEventListener('change', this.handleDoctorSelection);
|
| 637 |
}
|
| 638 |
|
|
@@ -640,7 +640,7 @@ How can I assist you today?`;
|
|
| 640 |
console.log('[DEBUG] showSettingsModal called');
|
| 641 |
this.showModal('settingsModal');
|
| 642 |
}
|
| 643 |
-
|
| 644 |
|
| 645 |
// ================================================================================
|
| 646 |
// SETTINGS.JS FUNCTIONALITY
|
|
@@ -791,14 +791,14 @@ How can I assist you today?`;
|
|
| 791 |
|
| 792 |
async switchToSession(session) {
|
| 793 |
console.log('[DEBUG] Switching to session:', session.id, session.source);
|
| 794 |
-
|
| 795 |
// Clear current session and messages first
|
| 796 |
this.currentSession = null;
|
| 797 |
this.clearChatMessages();
|
| 798 |
-
|
| 799 |
// Set new session
|
| 800 |
this.currentSession = { ...session };
|
| 801 |
-
|
| 802 |
if (session.source === 'backend') {
|
| 803 |
// For backend sessions, always fetch fresh messages
|
| 804 |
console.log('[DEBUG] Fetching messages for backend session:', session.id);
|
|
@@ -823,7 +823,7 @@ How can I assist you today?`;
|
|
| 823 |
console.log('[DEBUG] No messages found for local session:', session.id);
|
| 824 |
}
|
| 825 |
}
|
| 826 |
-
|
| 827 |
this.updateChatTitle();
|
| 828 |
this.loadChatSessions(); // Re-render to update active state
|
| 829 |
}
|
|
@@ -851,28 +851,28 @@ How can I assist you today?`;
|
|
| 851 |
async deleteChatSession(sessionId) {
|
| 852 |
const confirmDelete = confirm('Delete this chat session? This cannot be undone.');
|
| 853 |
if (!confirmDelete) return;
|
| 854 |
-
|
| 855 |
try {
|
| 856 |
// Check if it's a backend session
|
| 857 |
const isBackendSession = this.backendSessions && this.backendSessions.some(s => s.id === sessionId);
|
| 858 |
-
|
| 859 |
if (isBackendSession) {
|
| 860 |
// Delete from backend (MongoDB + memory system)
|
| 861 |
const resp = await fetch(`/sessions/${sessionId}`, {
|
| 862 |
method: 'DELETE',
|
| 863 |
headers: { 'Content-Type': 'application/json' }
|
| 864 |
});
|
| 865 |
-
|
| 866 |
if (!resp.ok) {
|
| 867 |
throw new Error(`HTTP ${resp.status}`);
|
| 868 |
}
|
| 869 |
-
|
| 870 |
const result = await resp.json();
|
| 871 |
console.log('[DEBUG] Backend deletion result:', result);
|
| 872 |
-
|
| 873 |
// Remove from backend sessions
|
| 874 |
this.backendSessions = this.backendSessions.filter(s => s.id !== sessionId);
|
| 875 |
-
|
| 876 |
// Invalidate caches
|
| 877 |
this.invalidateSessionCache(this.currentPatientId);
|
| 878 |
this.invalidateMessageCache(this.currentPatientId, sessionId);
|
|
@@ -881,11 +881,11 @@ How can I assist you today?`;
|
|
| 881 |
const sessions = this.getChatSessions();
|
| 882 |
const index = sessions.findIndex(s => s.id === sessionId);
|
| 883 |
if (index === -1) return;
|
| 884 |
-
|
| 885 |
sessions.splice(index, 1);
|
| 886 |
localStorage.setItem(`chatSessions_${this.currentUser.id}`, JSON.stringify(sessions));
|
| 887 |
}
|
| 888 |
-
|
| 889 |
// Handle current session cleanup
|
| 890 |
if (this.currentSession && this.currentSession.id === sessionId) {
|
| 891 |
if (isBackendSession) {
|
|
@@ -911,9 +911,9 @@ How can I assist you today?`;
|
|
| 911 |
}
|
| 912 |
this.updateChatTitle();
|
| 913 |
}
|
| 914 |
-
|
| 915 |
this.loadChatSessions();
|
| 916 |
-
|
| 917 |
} catch (error) {
|
| 918 |
console.error('Error deleting session:', error);
|
| 919 |
alert('Failed to delete session. Please try again.');
|
|
@@ -1057,10 +1057,10 @@ How can I assist you today?`;
|
|
| 1057 |
const sel = document.getElementById('profileNameSelect');
|
| 1058 |
const newSec = document.getElementById('newDoctorSection');
|
| 1059 |
if (!sel) return;
|
| 1060 |
-
|
| 1061 |
// Load doctors from MongoDB
|
| 1062 |
await this.loadDoctors();
|
| 1063 |
-
|
| 1064 |
sel.innerHTML = '';
|
| 1065 |
const createOpt = document.createElement('option');
|
| 1066 |
createOpt.value = '__create__';
|
|
@@ -1103,23 +1103,23 @@ How can I assist you today?`;
|
|
| 1103 |
// Get current role and specialty from the form
|
| 1104 |
const role = document.getElementById('profileRole').value || 'Medical Professional';
|
| 1105 |
const specialty = document.getElementById('profileSpecialty').value.trim() || '';
|
| 1106 |
-
|
| 1107 |
// Create doctor in MongoDB
|
| 1108 |
-
const result = await this.createDoctor({
|
| 1109 |
-
name,
|
| 1110 |
-
role,
|
| 1111 |
specialty,
|
| 1112 |
medical_roles: [role]
|
| 1113 |
});
|
| 1114 |
if (result) {
|
| 1115 |
-
this.doctors.unshift({
|
| 1116 |
-
name,
|
| 1117 |
-
role,
|
| 1118 |
-
specialty,
|
| 1119 |
-
_id: result.doctor_id
|
| 1120 |
});
|
| 1121 |
this.saveDoctors();
|
| 1122 |
-
|
| 1123 |
// Update current user profile
|
| 1124 |
this.currentUser.name = name;
|
| 1125 |
this.currentUser.role = role;
|
|
@@ -1165,10 +1165,10 @@ How can I assist you today?`;
|
|
| 1165 |
const existingDoctorIndex = this.doctors.findIndex(d => d.name === name);
|
| 1166 |
if (existingDoctorIndex === -1) {
|
| 1167 |
// Add new doctor to local list
|
| 1168 |
-
this.doctors.unshift({
|
| 1169 |
-
name,
|
| 1170 |
-
role,
|
| 1171 |
-
specialty
|
| 1172 |
});
|
| 1173 |
} else {
|
| 1174 |
// Update existing doctor in local list
|
|
@@ -1188,18 +1188,18 @@ How can I assist you today?`;
|
|
| 1188 |
specialty: specialty || null,
|
| 1189 |
medical_roles: [role]
|
| 1190 |
};
|
| 1191 |
-
|
| 1192 |
try {
|
| 1193 |
const resp = await fetch('/doctors', {
|
| 1194 |
method: 'POST',
|
| 1195 |
headers: { 'Content-Type': 'application/json' },
|
| 1196 |
body: JSON.stringify(doctorPayload)
|
| 1197 |
});
|
| 1198 |
-
|
| 1199 |
if (!resp.ok) throw new Error('Failed to create doctor in backend');
|
| 1200 |
const data = await resp.json();
|
| 1201 |
console.log('[Doctor] Created new doctor in backend:', data);
|
| 1202 |
-
|
| 1203 |
// Update local doctor with the ID from backend
|
| 1204 |
const localDoctor = this.doctors.find(d => d.name === name);
|
| 1205 |
if (localDoctor) {
|
|
@@ -1219,7 +1219,7 @@ How can I assist you today?`;
|
|
| 1219 |
// ================================================================================
|
| 1220 |
// PATIENT.JS FUNCTIONALITY
|
| 1221 |
// ================================================================================
|
| 1222 |
-
|
| 1223 |
async getLocalStorageSuggestions(query) {
|
| 1224 |
try {
|
| 1225 |
const storedPatients = JSON.parse(localStorage.getItem('medicalChatbotPatients') || '[]');
|
|
@@ -1243,19 +1243,19 @@ How can I assist you today?`;
|
|
| 1243 |
combinePatientResults(mongoResults, localResults) {
|
| 1244 |
// Create a map to deduplicate by patient_id, with MongoDB results taking priority
|
| 1245 |
const resultMap = new Map();
|
| 1246 |
-
|
| 1247 |
// Add MongoDB results first (they take priority)
|
| 1248 |
mongoResults.forEach(patient => {
|
| 1249 |
resultMap.set(patient.patient_id, patient);
|
| 1250 |
});
|
| 1251 |
-
|
| 1252 |
// Add localStorage results only if not already present
|
| 1253 |
localResults.forEach(patient => {
|
| 1254 |
if (!resultMap.has(patient.patient_id)) {
|
| 1255 |
resultMap.set(patient.patient_id, patient);
|
| 1256 |
}
|
| 1257 |
});
|
| 1258 |
-
|
| 1259 |
return Array.from(resultMap.values());
|
| 1260 |
}
|
| 1261 |
|
|
@@ -1282,7 +1282,7 @@ How can I assist you today?`;
|
|
| 1282 |
const status = document.getElementById('patientStatus');
|
| 1283 |
const actions = document.getElementById('patientActions');
|
| 1284 |
const emrLink = document.getElementById('emrLink');
|
| 1285 |
-
|
| 1286 |
if (status) {
|
| 1287 |
// Try to fetch patient name
|
| 1288 |
try {
|
|
@@ -1298,11 +1298,11 @@ How can I assist you today?`;
|
|
| 1298 |
}
|
| 1299 |
status.style.color = 'var(--text-secondary)';
|
| 1300 |
}
|
| 1301 |
-
|
| 1302 |
// Show EMR link
|
| 1303 |
if (actions) actions.style.display = 'block';
|
| 1304 |
if (emrLink) emrLink.href = `/static/emr.html?patient_id=${pid}`;
|
| 1305 |
-
|
| 1306 |
const input = document.getElementById('patientIdInput');
|
| 1307 |
if (input) input.value = pid;
|
| 1308 |
}
|
|
@@ -1317,7 +1317,7 @@ How can I assist you today?`;
|
|
| 1317 |
const status = document.getElementById('patientStatus');
|
| 1318 |
const actions = document.getElementById('patientActions');
|
| 1319 |
const emrLink = document.getElementById('emrLink');
|
| 1320 |
-
|
| 1321 |
if (status) {
|
| 1322 |
if (patientName) {
|
| 1323 |
status.textContent = `Patient: ${patientName} (${patientId})`;
|
|
@@ -1326,7 +1326,7 @@ How can I assist you today?`;
|
|
| 1326 |
}
|
| 1327 |
status.style.color = 'var(--text-secondary)';
|
| 1328 |
}
|
| 1329 |
-
|
| 1330 |
// Show EMR link
|
| 1331 |
if (actions) actions.style.display = 'block';
|
| 1332 |
if (emrLink) emrLink.href = `/static/emr.html?patient_id=${patientId}`;
|
|
@@ -1338,13 +1338,13 @@ How can I assist you today?`;
|
|
| 1338 |
const status = document.getElementById('patientStatus');
|
| 1339 |
const value = (input?.value || '').trim();
|
| 1340 |
console.log('[DEBUG] Patient input value:', value);
|
| 1341 |
-
|
| 1342 |
if (!value) {
|
| 1343 |
console.log('[DEBUG] No input provided');
|
| 1344 |
if (status) { status.textContent = 'Please enter patient ID or name.'; status.style.color = 'var(--warning-color)'; }
|
| 1345 |
return;
|
| 1346 |
}
|
| 1347 |
-
|
| 1348 |
// If it's a complete 8-digit ID, use it directly
|
| 1349 |
if (/^\d{8}$/.test(value)) {
|
| 1350 |
console.log('[DEBUG] Valid 8-digit ID provided');
|
|
@@ -1365,7 +1365,7 @@ How can I assist you today?`;
|
|
| 1365 |
await this.fetchAndRenderPatientSessions();
|
| 1366 |
return;
|
| 1367 |
}
|
| 1368 |
-
|
| 1369 |
// Otherwise, search for patient by name or partial ID
|
| 1370 |
console.log('[DEBUG] Searching for patient by name/partial ID');
|
| 1371 |
try {
|
|
@@ -1388,7 +1388,7 @@ How can I assist you today?`;
|
|
| 1388 |
} catch (e) {
|
| 1389 |
console.error('[DEBUG] Search error:', e);
|
| 1390 |
}
|
| 1391 |
-
|
| 1392 |
// No patient found
|
| 1393 |
console.log('[DEBUG] No patient found');
|
| 1394 |
if (status) { status.textContent = 'No patient found. Try a different search.'; status.style.color = 'var(--warning-color)'; }
|
|
@@ -1396,12 +1396,12 @@ How can I assist you today?`;
|
|
| 1396 |
|
| 1397 |
fetchAndRenderPatientSessions = async function () {
|
| 1398 |
if (!this.currentPatientId) return;
|
| 1399 |
-
|
| 1400 |
// Check localStorage cache first
|
| 1401 |
const cacheKey = `sessions_${this.currentPatientId}`;
|
| 1402 |
const cached = localStorage.getItem(cacheKey);
|
| 1403 |
let sessions = [];
|
| 1404 |
-
|
| 1405 |
if (cached) {
|
| 1406 |
try {
|
| 1407 |
const cachedData = JSON.parse(cached);
|
|
@@ -1416,7 +1416,7 @@ How can I assist you today?`;
|
|
| 1416 |
console.warn('Failed to parse cached sessions:', e);
|
| 1417 |
}
|
| 1418 |
}
|
| 1419 |
-
|
| 1420 |
// If no cache or cache is stale, fetch from backend
|
| 1421 |
if (sessions.length === 0) {
|
| 1422 |
try {
|
|
@@ -1424,7 +1424,7 @@ How can I assist you today?`;
|
|
| 1424 |
if (resp.ok) {
|
| 1425 |
const data = await resp.json();
|
| 1426 |
sessions = Array.isArray(data.sessions) ? data.sessions : [];
|
| 1427 |
-
|
| 1428 |
// Cache the sessions
|
| 1429 |
localStorage.setItem(cacheKey, JSON.stringify({
|
| 1430 |
sessions: sessions,
|
|
@@ -1438,7 +1438,7 @@ How can I assist you today?`;
|
|
| 1438 |
console.error('Failed to load patient sessions', e);
|
| 1439 |
}
|
| 1440 |
}
|
| 1441 |
-
|
| 1442 |
// Process sessions
|
| 1443 |
this.backendSessions = sessions.map(s => ({
|
| 1444 |
id: s.session_id,
|
|
@@ -1448,12 +1448,12 @@ How can I assist you today?`;
|
|
| 1448 |
lastActivity: s.last_activity || new Date().toISOString(),
|
| 1449 |
source: 'backend'
|
| 1450 |
}));
|
| 1451 |
-
|
| 1452 |
if (this.backendSessions.length > 0) {
|
| 1453 |
this.currentSession = this.backendSessions[0];
|
| 1454 |
await this.hydrateMessagesForSession(this.currentSession.id);
|
| 1455 |
}
|
| 1456 |
-
|
| 1457 |
this.loadChatSessions();
|
| 1458 |
}
|
| 1459 |
|
|
@@ -1463,7 +1463,7 @@ How can I assist you today?`;
|
|
| 1463 |
const cacheKey = `messages_${this.currentPatientId}_${sessionId}`;
|
| 1464 |
const cached = localStorage.getItem(cacheKey);
|
| 1465 |
let messages = [];
|
| 1466 |
-
|
| 1467 |
if (cached) {
|
| 1468 |
try {
|
| 1469 |
const cachedData = JSON.parse(cached);
|
|
@@ -1478,7 +1478,7 @@ How can I assist you today?`;
|
|
| 1478 |
console.warn('Failed to parse cached messages:', e);
|
| 1479 |
}
|
| 1480 |
}
|
| 1481 |
-
|
| 1482 |
// If no cache or cache is stale, fetch from backend
|
| 1483 |
if (messages.length === 0) {
|
| 1484 |
const resp = await fetch(`/sessions/${sessionId}/messages?patient_id=${this.currentPatientId}&limit=1000`);
|
|
@@ -1494,7 +1494,7 @@ How can I assist you today?`;
|
|
| 1494 |
content: m.content,
|
| 1495 |
timestamp: m.timestamp
|
| 1496 |
}));
|
| 1497 |
-
|
| 1498 |
// Cache the messages
|
| 1499 |
localStorage.setItem(cacheKey, JSON.stringify({
|
| 1500 |
messages: messages,
|
|
@@ -1502,14 +1502,14 @@ How can I assist you today?`;
|
|
| 1502 |
}));
|
| 1503 |
console.log('[DEBUG] Cached messages for session:', sessionId, 'count:', messages.length);
|
| 1504 |
}
|
| 1505 |
-
|
| 1506 |
// Sort messages by timestamp (ascending order for display)
|
| 1507 |
const sortedMessages = messages.sort((a, b) => {
|
| 1508 |
const timeA = new Date(a.timestamp || 0).getTime();
|
| 1509 |
const timeB = new Date(b.timestamp || 0).getTime();
|
| 1510 |
return timeA - timeB;
|
| 1511 |
});
|
| 1512 |
-
|
| 1513 |
if (this.currentSession && this.currentSession.id === sessionId) {
|
| 1514 |
this.currentSession.messages = sortedMessages;
|
| 1515 |
this.clearChatMessages();
|
|
@@ -1570,7 +1570,7 @@ How can I assist you today?`;
|
|
| 1570 |
console.log('[DEBUG] Search URL:', url);
|
| 1571 |
const resp = await fetch(url);
|
| 1572 |
console.log('[DEBUG] Search response status:', resp.status);
|
| 1573 |
-
|
| 1574 |
let mongoResults = [];
|
| 1575 |
if (resp.ok) {
|
| 1576 |
const data = await resp.json();
|
|
@@ -1579,16 +1579,16 @@ How can I assist you today?`;
|
|
| 1579 |
} else {
|
| 1580 |
console.warn('MongoDB search request failed', resp.status);
|
| 1581 |
}
|
| 1582 |
-
|
| 1583 |
// Get localStorage suggestions as fallback/additional results
|
| 1584 |
const localResults = await this.getLocalStorageSuggestions(q);
|
| 1585 |
-
|
| 1586 |
// Combine and deduplicate results (MongoDB results take priority)
|
| 1587 |
const combinedResults = this.combinePatientResults(mongoResults, localResults);
|
| 1588 |
console.log('[DEBUG] Combined search results:', combinedResults);
|
| 1589 |
renderSuggestions(combinedResults);
|
| 1590 |
-
|
| 1591 |
-
} catch (e) {
|
| 1592 |
console.error('[DEBUG] Search error:', e);
|
| 1593 |
// Fallback for network errors
|
| 1594 |
console.log('[DEBUG] Trying fallback search after error');
|
|
@@ -1617,7 +1617,7 @@ How can I assist you today?`;
|
|
| 1617 |
const closeBtn = document.getElementById('patientModalClose');
|
| 1618 |
const logoutBtn = document.getElementById('patientLogoutBtn');
|
| 1619 |
const createBtn = document.getElementById('patientCreateBtn');
|
| 1620 |
-
|
| 1621 |
if (profileBtn && modal) {
|
| 1622 |
profileBtn.addEventListener('click', async () => {
|
| 1623 |
const pid = this?.currentPatientId;
|
|
@@ -1641,12 +1641,12 @@ How can I assist you today?`;
|
|
| 1641 |
modal.classList.add('show');
|
| 1642 |
});
|
| 1643 |
}
|
| 1644 |
-
|
| 1645 |
if (closeBtn && modal) {
|
| 1646 |
closeBtn.addEventListener('click', () => modal.classList.remove('show'));
|
| 1647 |
modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('show'); });
|
| 1648 |
}
|
| 1649 |
-
|
| 1650 |
if (logoutBtn) {
|
| 1651 |
logoutBtn.addEventListener('click', () => {
|
| 1652 |
if (confirm('Log out current patient?')) {
|
|
@@ -1660,7 +1660,7 @@ How can I assist you today?`;
|
|
| 1660 |
}
|
| 1661 |
});
|
| 1662 |
}
|
| 1663 |
-
|
| 1664 |
if (createBtn) createBtn.addEventListener('click', () => modal.classList.remove('show'));
|
| 1665 |
}
|
| 1666 |
|
|
@@ -1671,7 +1671,7 @@ How can I assist you today?`;
|
|
| 1671 |
try {
|
| 1672 |
this.audioRecorder = new AudioRecordingUI(this);
|
| 1673 |
const success = await this.audioRecorder.initialize();
|
| 1674 |
-
|
| 1675 |
if (success) {
|
| 1676 |
console.log('[Audio] Audio recording initialized successfully');
|
| 1677 |
// Make globally accessible for voice detection callback
|
|
@@ -1729,7 +1729,7 @@ How can I assist you today?`;
|
|
| 1729 |
const response = await this.callMedicalAPI(message);
|
| 1730 |
this.addMessage('assistant', response);
|
| 1731 |
this.updateCurrentSession();
|
| 1732 |
-
|
| 1733 |
// Invalidate caches after successful message exchange
|
| 1734 |
if (this.currentSession && this.currentSession.id) {
|
| 1735 |
this.invalidateMessageCache(this.currentPatientId, this.currentSession.id);
|
|
@@ -1802,10 +1802,10 @@ How can I assist you today?`;
|
|
| 1802 |
// Check if session needs title generation after messages are loaded
|
| 1803 |
checkAndGenerateSessionTitle() {
|
| 1804 |
if (!this.currentSession || !this.currentSession.messages) return;
|
| 1805 |
-
|
| 1806 |
// Check if this is a new session that needs a title (exactly 2 messages: user + assistant)
|
| 1807 |
-
if (this.currentSession.messages.length === 2 &&
|
| 1808 |
-
this.currentSession.title === 'New Chat' &&
|
| 1809 |
this.currentSession.messages[0].role === 'user') {
|
| 1810 |
const firstMessage = this.currentSession.messages[0].content;
|
| 1811 |
this.summariseAndSetTitle(firstMessage);
|
|
@@ -1889,7 +1889,7 @@ How can I assist you today?`;
|
|
| 1889 |
}
|
| 1890 |
// ----------------------------------------------------------
|
| 1891 |
// Additional UI setup END
|
| 1892 |
-
// ----------------------------------------------------------
|
| 1893 |
|
| 1894 |
|
| 1895 |
// Initialize the app when DOM is loaded
|
|
@@ -1998,13 +1998,13 @@ class AudioRecorder {
|
|
| 1998 |
this.isRecording = true;
|
| 1999 |
this.recordingStartTime = Date.now();
|
| 2000 |
console.log('Audio recording started');
|
| 2001 |
-
|
| 2002 |
// Start timer
|
| 2003 |
this.startTimer();
|
| 2004 |
-
|
| 2005 |
// Start voice detection
|
| 2006 |
this.startVoiceDetection();
|
| 2007 |
-
|
| 2008 |
return true;
|
| 2009 |
} catch (error) {
|
| 2010 |
console.error('Failed to start recording:', error);
|
|
@@ -2021,11 +2021,11 @@ class AudioRecorder {
|
|
| 2021 |
this.mediaRecorder.stop();
|
| 2022 |
this.isRecording = false;
|
| 2023 |
console.log('Audio recording stopped');
|
| 2024 |
-
|
| 2025 |
// Stop timer and voice detection
|
| 2026 |
this.stopTimer();
|
| 2027 |
this.stopVoiceDetection();
|
| 2028 |
-
|
| 2029 |
return true;
|
| 2030 |
} catch (error) {
|
| 2031 |
console.error('Failed to stop recording:', error);
|
|
@@ -2040,7 +2040,7 @@ class AudioRecorder {
|
|
| 2040 |
const minutes = Math.floor(elapsed / 60);
|
| 2041 |
const seconds = elapsed % 60;
|
| 2042 |
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
| 2043 |
-
|
| 2044 |
const timerElement = document.getElementById('recordingTimer');
|
| 2045 |
if (timerElement) {
|
| 2046 |
timerElement.textContent = timeString;
|
|
@@ -2059,24 +2059,24 @@ class AudioRecorder {
|
|
| 2059 |
startVoiceDetection() {
|
| 2060 |
const checkVoice = () => {
|
| 2061 |
if (!this.isRecording || !this.analyser) return;
|
| 2062 |
-
|
| 2063 |
const bufferLength = this.analyser.frequencyBinCount;
|
| 2064 |
const dataArray = new Uint8Array(bufferLength);
|
| 2065 |
this.analyser.getByteFrequencyData(dataArray);
|
| 2066 |
-
|
| 2067 |
// Calculate average volume
|
| 2068 |
const average = dataArray.reduce((sum, value) => sum + value, 0) / bufferLength;
|
| 2069 |
const threshold = 20; // Adjust this value to change sensitivity
|
| 2070 |
-
|
| 2071 |
const container = document.querySelector('.recording-container');
|
| 2072 |
const statusElement = document.getElementById('recordingStatus');
|
| 2073 |
-
|
| 2074 |
if (average > threshold) {
|
| 2075 |
// Voice detected
|
| 2076 |
container.classList.remove('silent');
|
| 2077 |
container.classList.add('listening');
|
| 2078 |
if (statusElement) statusElement.textContent = 'Listening...';
|
| 2079 |
-
|
| 2080 |
// Reset silence timer
|
| 2081 |
this.resetSilenceTimer();
|
| 2082 |
} else {
|
|
@@ -2085,10 +2085,10 @@ class AudioRecorder {
|
|
| 2085 |
container.classList.add('silent');
|
| 2086 |
if (statusElement) statusElement.textContent = 'Silence detected...';
|
| 2087 |
}
|
| 2088 |
-
|
| 2089 |
requestAnimationFrame(checkVoice);
|
| 2090 |
};
|
| 2091 |
-
|
| 2092 |
checkVoice();
|
| 2093 |
}
|
| 2094 |
|
|
@@ -2100,7 +2100,7 @@ class AudioRecorder {
|
|
| 2100 |
if (this.silenceTimer) {
|
| 2101 |
clearTimeout(this.silenceTimer);
|
| 2102 |
}
|
| 2103 |
-
|
| 2104 |
// Auto-stop after 3 seconds of silence
|
| 2105 |
this.silenceTimer = setTimeout(() => {
|
| 2106 |
if (this.isRecording) {
|
|
@@ -2123,10 +2123,10 @@ class AudioRecorder {
|
|
| 2123 |
try {
|
| 2124 |
// Create audio blob
|
| 2125 |
const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
|
| 2126 |
-
|
| 2127 |
// Transcribe audio
|
| 2128 |
const transcribedText = await this.transcribeAudio(audioBlob);
|
| 2129 |
-
|
| 2130 |
return transcribedText;
|
| 2131 |
} catch (error) {
|
| 2132 |
console.error('Failed to process recording:', error);
|
|
@@ -2163,19 +2163,19 @@ class AudioRecorder {
|
|
| 2163 |
this.audioStream.getTracks().forEach(track => track.stop());
|
| 2164 |
this.audioStream = null;
|
| 2165 |
}
|
| 2166 |
-
|
| 2167 |
if (this.audioContext) {
|
| 2168 |
this.audioContext.close();
|
| 2169 |
this.audioContext = null;
|
| 2170 |
}
|
| 2171 |
-
|
| 2172 |
if (this.silenceTimer) {
|
| 2173 |
clearTimeout(this.silenceTimer);
|
| 2174 |
this.silenceTimer = null;
|
| 2175 |
}
|
| 2176 |
-
|
| 2177 |
this.stopTimer();
|
| 2178 |
-
|
| 2179 |
this.mediaRecorder = null;
|
| 2180 |
this.audioChunks = [];
|
| 2181 |
this.isRecording = false;
|
|
@@ -2217,7 +2217,7 @@ class AudioRecordingUI {
|
|
| 2217 |
setupUI() {
|
| 2218 |
this.microphoneBtn = document.getElementById('microphoneBtn');
|
| 2219 |
this.modal = document.getElementById('audioRecordingModal');
|
| 2220 |
-
|
| 2221 |
if (!this.microphoneBtn) {
|
| 2222 |
console.error('Microphone button not found');
|
| 2223 |
return;
|
|
@@ -2230,15 +2230,15 @@ class AudioRecordingUI {
|
|
| 2230 |
|
| 2231 |
// Set up event listeners
|
| 2232 |
this.microphoneBtn.addEventListener('click', (e) => this.startRecording(e));
|
| 2233 |
-
|
| 2234 |
// Modal close handlers
|
| 2235 |
const closeBtn = document.getElementById('audioRecordingModalClose');
|
| 2236 |
const stopBtn = document.getElementById('stopRecordingBtn');
|
| 2237 |
-
|
| 2238 |
if (closeBtn) {
|
| 2239 |
closeBtn.addEventListener('click', () => this.closeModal());
|
| 2240 |
}
|
| 2241 |
-
|
| 2242 |
if (stopBtn) {
|
| 2243 |
stopBtn.addEventListener('click', () => this.stopRecording());
|
| 2244 |
}
|
|
@@ -2260,11 +2260,11 @@ class AudioRecordingUI {
|
|
| 2260 |
}
|
| 2261 |
|
| 2262 |
event.preventDefault();
|
| 2263 |
-
|
| 2264 |
try {
|
| 2265 |
// Show modal
|
| 2266 |
this.showModal();
|
| 2267 |
-
|
| 2268 |
// Start recording
|
| 2269 |
const success = this.recorder.startRecording();
|
| 2270 |
if (success) {
|
|
@@ -2302,14 +2302,14 @@ class AudioRecordingUI {
|
|
| 2302 |
try {
|
| 2303 |
// Process the recording
|
| 2304 |
const transcribedText = await this.recorder.processRecording();
|
| 2305 |
-
|
| 2306 |
if (transcribedText) {
|
| 2307 |
this.insertTranscribedText(transcribedText);
|
| 2308 |
this.showSuccess('Audio transcribed successfully!');
|
| 2309 |
} else {
|
| 2310 |
this.showError('No speech detected. Please try again.');
|
| 2311 |
}
|
| 2312 |
-
|
| 2313 |
this.closeModal();
|
| 2314 |
} catch (error) {
|
| 2315 |
console.error('Failed to process recording:', error);
|
|
@@ -2329,12 +2329,12 @@ class AudioRecordingUI {
|
|
| 2329 |
if (this.modal) {
|
| 2330 |
this.modal.classList.remove('show');
|
| 2331 |
document.body.style.overflow = ''; // Restore scrolling
|
| 2332 |
-
|
| 2333 |
// Stop recording if still active
|
| 2334 |
if (this.recorder.isRecording) {
|
| 2335 |
this.recorder.stopRecording();
|
| 2336 |
}
|
| 2337 |
-
|
| 2338 |
// Reset modal state
|
| 2339 |
this.updateModalState('ready');
|
| 2340 |
}
|
|
@@ -2344,12 +2344,12 @@ class AudioRecordingUI {
|
|
| 2344 |
const container = document.querySelector('.recording-container');
|
| 2345 |
const statusElement = document.getElementById('recordingStatus');
|
| 2346 |
const stopBtn = document.getElementById('stopRecordingBtn');
|
| 2347 |
-
|
| 2348 |
if (!container) return;
|
| 2349 |
|
| 2350 |
// Remove all state classes
|
| 2351 |
container.classList.remove('listening', 'silent', 'processing');
|
| 2352 |
-
|
| 2353 |
switch (state) {
|
| 2354 |
case 'ready':
|
| 2355 |
container.classList.add('listening');
|
|
@@ -2379,23 +2379,23 @@ class AudioRecordingUI {
|
|
| 2379 |
// Append transcribed text to existing content
|
| 2380 |
const currentText = chatInput.value.trim();
|
| 2381 |
const newText = currentText ? `${currentText} ${text}` : text;
|
| 2382 |
-
|
| 2383 |
chatInput.value = newText;
|
| 2384 |
-
|
| 2385 |
// Add visual feedback for transcribed text
|
| 2386 |
chatInput.classList.add('transcribed');
|
| 2387 |
-
|
| 2388 |
// Remove the highlighting after a few seconds
|
| 2389 |
setTimeout(() => {
|
| 2390 |
chatInput.classList.remove('transcribed');
|
| 2391 |
}, 3000);
|
| 2392 |
-
|
| 2393 |
// Trigger input event to update UI
|
| 2394 |
chatInput.dispatchEvent(new Event('input', { bubbles: true }));
|
| 2395 |
-
|
| 2396 |
// Focus the input
|
| 2397 |
chatInput.focus();
|
| 2398 |
-
|
| 2399 |
// Auto-resize if needed
|
| 2400 |
if (this.app && this.app.autoResizeTextarea) {
|
| 2401 |
this.app.autoResizeTextarea(chatInput);
|
|
@@ -2407,7 +2407,7 @@ class AudioRecordingUI {
|
|
| 2407 |
|
| 2408 |
// Remove all state classes
|
| 2409 |
this.microphoneBtn.classList.remove('recording-ready', 'recording-active', 'recording-processing');
|
| 2410 |
-
|
| 2411 |
// Add appropriate state class
|
| 2412 |
switch (state) {
|
| 2413 |
case 'ready':
|
|
@@ -2432,16 +2432,16 @@ class AudioRecordingUI {
|
|
| 2432 |
errorMsg = document.createElement('div');
|
| 2433 |
errorMsg.id = 'audioError';
|
| 2434 |
errorMsg.className = 'audio-error-message';
|
| 2435 |
-
|
| 2436 |
const chatInputContainer = document.querySelector('.chat-input-container');
|
| 2437 |
if (chatInputContainer) {
|
| 2438 |
chatInputContainer.appendChild(errorMsg);
|
| 2439 |
}
|
| 2440 |
}
|
| 2441 |
-
|
| 2442 |
errorMsg.textContent = message;
|
| 2443 |
errorMsg.style.display = 'block';
|
| 2444 |
-
|
| 2445 |
// Hide after 5 seconds
|
| 2446 |
setTimeout(() => {
|
| 2447 |
errorMsg.style.display = 'none';
|
|
@@ -2455,16 +2455,16 @@ class AudioRecordingUI {
|
|
| 2455 |
successMsg = document.createElement('div');
|
| 2456 |
successMsg.id = 'audioSuccess';
|
| 2457 |
successMsg.className = 'audio-success-message';
|
| 2458 |
-
|
| 2459 |
const chatInputContainer = document.querySelector('.chat-input-container');
|
| 2460 |
if (chatInputContainer) {
|
| 2461 |
chatInputContainer.appendChild(successMsg);
|
| 2462 |
}
|
| 2463 |
}
|
| 2464 |
-
|
| 2465 |
successMsg.textContent = message;
|
| 2466 |
successMsg.style.display = 'block';
|
| 2467 |
-
|
| 2468 |
// Hide after 3 seconds
|
| 2469 |
setTimeout(() => {
|
| 2470 |
successMsg.style.display = 'none';
|
|
@@ -2555,4 +2555,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2555 |
if (userModal) {
|
| 2556 |
userModal.addEventListener('click', (e) => { if (e.target === userModal) userModal.classList.remove('show'); });
|
| 2557 |
}
|
| 2558 |
-
});
|
|
|
|
| 49 |
// Ensure a session exists and is displayed immediately if nothing to show
|
| 50 |
this.ensureStartupSession();
|
| 51 |
this.loadChatSessions();
|
| 52 |
+
|
| 53 |
// Bind patient handlers
|
| 54 |
console.log('[DEBUG] Binding patient handlers');
|
| 55 |
this.bindPatientHandlers();
|
|
|
|
| 58 |
const prefs = JSON.parse(localStorage.getItem('medicalChatbotPreferences') || '{}');
|
| 59 |
this.setTheme(prefs.theme || 'auto');
|
| 60 |
this.setupTheme();
|
| 61 |
+
|
| 62 |
// Initialize audio recording (guarded if module not present)
|
| 63 |
try {
|
| 64 |
if (typeof AudioRecordingUI !== 'undefined') {
|
|
|
|
| 79 |
this.toggleSidebar();
|
| 80 |
});
|
| 81 |
}
|
| 82 |
+
|
| 83 |
// Click outside sidebar to close (mobile/overlay behavior)
|
| 84 |
const overlay = document.getElementById('appOverlay');
|
| 85 |
console.log('[DEBUG] Overlay element found:', !!overlay);
|
|
|
|
| 97 |
}
|
| 98 |
}
|
| 99 |
};
|
| 100 |
+
|
| 101 |
// Keep overlay synced when toggling
|
| 102 |
const origToggle = this.toggleSidebar.bind(this);
|
| 103 |
+
this.toggleSidebar = () => {
|
| 104 |
console.log('[DEBUG] Wrapped toggleSidebar called');
|
| 105 |
+
origToggle();
|
| 106 |
+
updateOverlay();
|
| 107 |
};
|
| 108 |
+
|
| 109 |
// Initialize overlay state - ensure it's hidden on startup
|
| 110 |
if (overlay) {
|
| 111 |
overlay.classList.remove('show');
|
| 112 |
}
|
| 113 |
updateOverlay();
|
| 114 |
+
|
| 115 |
// Handle window resize for responsive behavior
|
| 116 |
window.addEventListener('resize', () => {
|
| 117 |
console.log('[DEBUG] Window resized, updating overlay');
|
| 118 |
updateOverlay();
|
| 119 |
});
|
| 120 |
+
|
| 121 |
// Click outside to close sidebar
|
| 122 |
document.addEventListener('click', (e) => {
|
| 123 |
const sidebar = document.getElementById('sidebar');
|
|
|
|
| 127 |
const isOpen = sidebar.classList.contains('show');
|
| 128 |
const clickInside = sidebar.contains(e.target) || (toggleBtn && toggleBtn.contains(e.target));
|
| 129 |
const clickOnOverlay = overlay && overlay.contains(e.target);
|
| 130 |
+
|
| 131 |
console.log('[DEBUG] Click event - sidebar open:', isOpen, 'click inside:', clickInside, 'click on overlay:', clickOnOverlay);
|
| 132 |
+
|
| 133 |
if (isOpen && !clickInside) {
|
| 134 |
if (clickOnOverlay) {
|
| 135 |
console.log('[DEBUG] Clicked on overlay, closing sidebar');
|
|
|
|
| 145 |
}
|
| 146 |
updateOverlay();
|
| 147 |
}, true);
|
| 148 |
+
|
| 149 |
if (overlay) {
|
| 150 |
overlay.addEventListener('click', () => {
|
| 151 |
console.log('[DEBUG] Overlay clicked directly');
|
|
|
|
| 239 |
const userModalClose = document.getElementById('userModalClose');
|
| 240 |
const userModalCancel = document.getElementById('userModalCancel');
|
| 241 |
const userModalSave = document.getElementById('userModalSave');
|
| 242 |
+
|
| 243 |
if (userModalClose) {
|
| 244 |
userModalClose.addEventListener('click', () => {
|
| 245 |
this.hideModal('userModal');
|
|
|
|
| 262 |
const settingsModalClose = document.getElementById('settingsModalClose');
|
| 263 |
const settingsModalCancel = document.getElementById('settingsModalCancel');
|
| 264 |
const settingsModalSave = document.getElementById('settingsModalSave');
|
| 265 |
+
|
| 266 |
if (settingsModalClose) {
|
| 267 |
settingsModalClose.addEventListener('click', () => {
|
| 268 |
this.hideModal('settingsModal');
|
|
|
|
| 297 |
const editTitleModalClose = document.getElementById('editTitleModalClose');
|
| 298 |
const editTitleModalCancel = document.getElementById('editTitleModalCancel');
|
| 299 |
const editTitleModalSave = document.getElementById('editTitleModalSave');
|
| 300 |
+
|
| 301 |
if (editTitleModalClose) editTitleModalClose.addEventListener('click', closeEdit);
|
| 302 |
if (editTitleModalCancel) editTitleModalCancel.addEventListener('click', closeEdit);
|
| 303 |
if (editTitleModalSave) {
|
|
|
|
| 485 |
// Our submodules aren't lodaed on app.js, so we need to add them here
|
| 486 |
// Perhaps this is FastAPI limitation, remove this when proper deploy this
|
| 487 |
// On UI specific hosting site.
|
| 488 |
+
// ----------------------------------------------------------
|
| 489 |
|
| 490 |
|
| 491 |
// ================================================================================
|
|
|
|
| 564 |
sel.appendChild(createOpt);
|
| 565 |
}
|
| 566 |
if (sel && !sel.value) sel.value = this.currentUser?.name || '__create__';
|
| 567 |
+
|
| 568 |
// Safely set role and specialty with null checks
|
| 569 |
const roleEl = document.getElementById('profileRole');
|
| 570 |
const specialtyEl = document.getElementById('profileSpecialty');
|
| 571 |
if (roleEl) roleEl.value = (this.currentUser && this.currentUser.role) ? this.currentUser.role : 'Medical Professional';
|
| 572 |
if (specialtyEl) specialtyEl.value = (this.currentUser && this.currentUser.specialty) ? this.currentUser.specialty : '';
|
| 573 |
+
|
| 574 |
// Add event listener for doctor selection changes
|
| 575 |
this.setupDoctorSelectionHandler();
|
| 576 |
+
|
| 577 |
this.showModal('userModal');
|
| 578 |
}
|
| 579 |
|
|
|
|
| 581 |
const sel = document.getElementById('profileNameSelect');
|
| 582 |
const roleEl = document.getElementById('profileRole');
|
| 583 |
const specialtyEl = document.getElementById('profileSpecialty');
|
| 584 |
+
|
| 585 |
if (!sel || !roleEl || !specialtyEl) return;
|
| 586 |
+
|
| 587 |
// Remove existing listeners to avoid duplicates
|
| 588 |
sel.removeEventListener('change', this.handleDoctorSelection);
|
| 589 |
+
|
| 590 |
// Add new listener
|
| 591 |
this.handleDoctorSelection = async (event) => {
|
| 592 |
const selectedName = event.target.value;
|
| 593 |
console.log('[DEBUG] Doctor selected:', selectedName);
|
| 594 |
+
|
| 595 |
if (selectedName === '__create__') {
|
| 596 |
// Reset to default values for new doctor
|
| 597 |
roleEl.value = 'Medical Professional';
|
| 598 |
specialtyEl.value = '';
|
| 599 |
return;
|
| 600 |
}
|
| 601 |
+
|
| 602 |
// Find the selected doctor in our doctors list
|
| 603 |
const selectedDoctor = this.doctors.find(d => d.name === selectedName);
|
| 604 |
if (selectedDoctor) {
|
|
|
|
| 632 |
}
|
| 633 |
}
|
| 634 |
};
|
| 635 |
+
|
| 636 |
sel.addEventListener('change', this.handleDoctorSelection);
|
| 637 |
}
|
| 638 |
|
|
|
|
| 640 |
console.log('[DEBUG] showSettingsModal called');
|
| 641 |
this.showModal('settingsModal');
|
| 642 |
}
|
| 643 |
+
|
| 644 |
|
| 645 |
// ================================================================================
|
| 646 |
// SETTINGS.JS FUNCTIONALITY
|
|
|
|
| 791 |
|
| 792 |
async switchToSession(session) {
|
| 793 |
console.log('[DEBUG] Switching to session:', session.id, session.source);
|
| 794 |
+
|
| 795 |
// Clear current session and messages first
|
| 796 |
this.currentSession = null;
|
| 797 |
this.clearChatMessages();
|
| 798 |
+
|
| 799 |
// Set new session
|
| 800 |
this.currentSession = { ...session };
|
| 801 |
+
|
| 802 |
if (session.source === 'backend') {
|
| 803 |
// For backend sessions, always fetch fresh messages
|
| 804 |
console.log('[DEBUG] Fetching messages for backend session:', session.id);
|
|
|
|
| 823 |
console.log('[DEBUG] No messages found for local session:', session.id);
|
| 824 |
}
|
| 825 |
}
|
| 826 |
+
|
| 827 |
this.updateChatTitle();
|
| 828 |
this.loadChatSessions(); // Re-render to update active state
|
| 829 |
}
|
|
|
|
| 851 |
async deleteChatSession(sessionId) {
|
| 852 |
const confirmDelete = confirm('Delete this chat session? This cannot be undone.');
|
| 853 |
if (!confirmDelete) return;
|
| 854 |
+
|
| 855 |
try {
|
| 856 |
// Check if it's a backend session
|
| 857 |
const isBackendSession = this.backendSessions && this.backendSessions.some(s => s.id === sessionId);
|
| 858 |
+
|
| 859 |
if (isBackendSession) {
|
| 860 |
// Delete from backend (MongoDB + memory system)
|
| 861 |
const resp = await fetch(`/sessions/${sessionId}`, {
|
| 862 |
method: 'DELETE',
|
| 863 |
headers: { 'Content-Type': 'application/json' }
|
| 864 |
});
|
| 865 |
+
|
| 866 |
if (!resp.ok) {
|
| 867 |
throw new Error(`HTTP ${resp.status}`);
|
| 868 |
}
|
| 869 |
+
|
| 870 |
const result = await resp.json();
|
| 871 |
console.log('[DEBUG] Backend deletion result:', result);
|
| 872 |
+
|
| 873 |
// Remove from backend sessions
|
| 874 |
this.backendSessions = this.backendSessions.filter(s => s.id !== sessionId);
|
| 875 |
+
|
| 876 |
// Invalidate caches
|
| 877 |
this.invalidateSessionCache(this.currentPatientId);
|
| 878 |
this.invalidateMessageCache(this.currentPatientId, sessionId);
|
|
|
|
| 881 |
const sessions = this.getChatSessions();
|
| 882 |
const index = sessions.findIndex(s => s.id === sessionId);
|
| 883 |
if (index === -1) return;
|
| 884 |
+
|
| 885 |
sessions.splice(index, 1);
|
| 886 |
localStorage.setItem(`chatSessions_${this.currentUser.id}`, JSON.stringify(sessions));
|
| 887 |
}
|
| 888 |
+
|
| 889 |
// Handle current session cleanup
|
| 890 |
if (this.currentSession && this.currentSession.id === sessionId) {
|
| 891 |
if (isBackendSession) {
|
|
|
|
| 911 |
}
|
| 912 |
this.updateChatTitle();
|
| 913 |
}
|
| 914 |
+
|
| 915 |
this.loadChatSessions();
|
| 916 |
+
|
| 917 |
} catch (error) {
|
| 918 |
console.error('Error deleting session:', error);
|
| 919 |
alert('Failed to delete session. Please try again.');
|
|
|
|
| 1057 |
const sel = document.getElementById('profileNameSelect');
|
| 1058 |
const newSec = document.getElementById('newDoctorSection');
|
| 1059 |
if (!sel) return;
|
| 1060 |
+
|
| 1061 |
// Load doctors from MongoDB
|
| 1062 |
await this.loadDoctors();
|
| 1063 |
+
|
| 1064 |
sel.innerHTML = '';
|
| 1065 |
const createOpt = document.createElement('option');
|
| 1066 |
createOpt.value = '__create__';
|
|
|
|
| 1103 |
// Get current role and specialty from the form
|
| 1104 |
const role = document.getElementById('profileRole').value || 'Medical Professional';
|
| 1105 |
const specialty = document.getElementById('profileSpecialty').value.trim() || '';
|
| 1106 |
+
|
| 1107 |
// Create doctor in MongoDB
|
| 1108 |
+
const result = await this.createDoctor({
|
| 1109 |
+
name,
|
| 1110 |
+
role,
|
| 1111 |
specialty,
|
| 1112 |
medical_roles: [role]
|
| 1113 |
});
|
| 1114 |
if (result) {
|
| 1115 |
+
this.doctors.unshift({
|
| 1116 |
+
name,
|
| 1117 |
+
role,
|
| 1118 |
+
specialty,
|
| 1119 |
+
_id: result.doctor_id
|
| 1120 |
});
|
| 1121 |
this.saveDoctors();
|
| 1122 |
+
|
| 1123 |
// Update current user profile
|
| 1124 |
this.currentUser.name = name;
|
| 1125 |
this.currentUser.role = role;
|
|
|
|
| 1165 |
const existingDoctorIndex = this.doctors.findIndex(d => d.name === name);
|
| 1166 |
if (existingDoctorIndex === -1) {
|
| 1167 |
// Add new doctor to local list
|
| 1168 |
+
this.doctors.unshift({
|
| 1169 |
+
name,
|
| 1170 |
+
role,
|
| 1171 |
+
specialty
|
| 1172 |
});
|
| 1173 |
} else {
|
| 1174 |
// Update existing doctor in local list
|
|
|
|
| 1188 |
specialty: specialty || null,
|
| 1189 |
medical_roles: [role]
|
| 1190 |
};
|
| 1191 |
+
|
| 1192 |
try {
|
| 1193 |
const resp = await fetch('/doctors', {
|
| 1194 |
method: 'POST',
|
| 1195 |
headers: { 'Content-Type': 'application/json' },
|
| 1196 |
body: JSON.stringify(doctorPayload)
|
| 1197 |
});
|
| 1198 |
+
|
| 1199 |
if (!resp.ok) throw new Error('Failed to create doctor in backend');
|
| 1200 |
const data = await resp.json();
|
| 1201 |
console.log('[Doctor] Created new doctor in backend:', data);
|
| 1202 |
+
|
| 1203 |
// Update local doctor with the ID from backend
|
| 1204 |
const localDoctor = this.doctors.find(d => d.name === name);
|
| 1205 |
if (localDoctor) {
|
|
|
|
| 1219 |
// ================================================================================
|
| 1220 |
// PATIENT.JS FUNCTIONALITY
|
| 1221 |
// ================================================================================
|
| 1222 |
+
|
| 1223 |
async getLocalStorageSuggestions(query) {
|
| 1224 |
try {
|
| 1225 |
const storedPatients = JSON.parse(localStorage.getItem('medicalChatbotPatients') || '[]');
|
|
|
|
| 1243 |
combinePatientResults(mongoResults, localResults) {
|
| 1244 |
// Create a map to deduplicate by patient_id, with MongoDB results taking priority
|
| 1245 |
const resultMap = new Map();
|
| 1246 |
+
|
| 1247 |
// Add MongoDB results first (they take priority)
|
| 1248 |
mongoResults.forEach(patient => {
|
| 1249 |
resultMap.set(patient.patient_id, patient);
|
| 1250 |
});
|
| 1251 |
+
|
| 1252 |
// Add localStorage results only if not already present
|
| 1253 |
localResults.forEach(patient => {
|
| 1254 |
if (!resultMap.has(patient.patient_id)) {
|
| 1255 |
resultMap.set(patient.patient_id, patient);
|
| 1256 |
}
|
| 1257 |
});
|
| 1258 |
+
|
| 1259 |
return Array.from(resultMap.values());
|
| 1260 |
}
|
| 1261 |
|
|
|
|
| 1282 |
const status = document.getElementById('patientStatus');
|
| 1283 |
const actions = document.getElementById('patientActions');
|
| 1284 |
const emrLink = document.getElementById('emrLink');
|
| 1285 |
+
|
| 1286 |
if (status) {
|
| 1287 |
// Try to fetch patient name
|
| 1288 |
try {
|
|
|
|
| 1298 |
}
|
| 1299 |
status.style.color = 'var(--text-secondary)';
|
| 1300 |
}
|
| 1301 |
+
|
| 1302 |
// Show EMR link
|
| 1303 |
if (actions) actions.style.display = 'block';
|
| 1304 |
if (emrLink) emrLink.href = `/static/emr.html?patient_id=${pid}`;
|
| 1305 |
+
|
| 1306 |
const input = document.getElementById('patientIdInput');
|
| 1307 |
if (input) input.value = pid;
|
| 1308 |
}
|
|
|
|
| 1317 |
const status = document.getElementById('patientStatus');
|
| 1318 |
const actions = document.getElementById('patientActions');
|
| 1319 |
const emrLink = document.getElementById('emrLink');
|
| 1320 |
+
|
| 1321 |
if (status) {
|
| 1322 |
if (patientName) {
|
| 1323 |
status.textContent = `Patient: ${patientName} (${patientId})`;
|
|
|
|
| 1326 |
}
|
| 1327 |
status.style.color = 'var(--text-secondary)';
|
| 1328 |
}
|
| 1329 |
+
|
| 1330 |
// Show EMR link
|
| 1331 |
if (actions) actions.style.display = 'block';
|
| 1332 |
if (emrLink) emrLink.href = `/static/emr.html?patient_id=${patientId}`;
|
|
|
|
| 1338 |
const status = document.getElementById('patientStatus');
|
| 1339 |
const value = (input?.value || '').trim();
|
| 1340 |
console.log('[DEBUG] Patient input value:', value);
|
| 1341 |
+
|
| 1342 |
if (!value) {
|
| 1343 |
console.log('[DEBUG] No input provided');
|
| 1344 |
if (status) { status.textContent = 'Please enter patient ID or name.'; status.style.color = 'var(--warning-color)'; }
|
| 1345 |
return;
|
| 1346 |
}
|
| 1347 |
+
|
| 1348 |
// If it's a complete 8-digit ID, use it directly
|
| 1349 |
if (/^\d{8}$/.test(value)) {
|
| 1350 |
console.log('[DEBUG] Valid 8-digit ID provided');
|
|
|
|
| 1365 |
await this.fetchAndRenderPatientSessions();
|
| 1366 |
return;
|
| 1367 |
}
|
| 1368 |
+
|
| 1369 |
// Otherwise, search for patient by name or partial ID
|
| 1370 |
console.log('[DEBUG] Searching for patient by name/partial ID');
|
| 1371 |
try {
|
|
|
|
| 1388 |
} catch (e) {
|
| 1389 |
console.error('[DEBUG] Search error:', e);
|
| 1390 |
}
|
| 1391 |
+
|
| 1392 |
// No patient found
|
| 1393 |
console.log('[DEBUG] No patient found');
|
| 1394 |
if (status) { status.textContent = 'No patient found. Try a different search.'; status.style.color = 'var(--warning-color)'; }
|
|
|
|
| 1396 |
|
| 1397 |
fetchAndRenderPatientSessions = async function () {
|
| 1398 |
if (!this.currentPatientId) return;
|
| 1399 |
+
|
| 1400 |
// Check localStorage cache first
|
| 1401 |
const cacheKey = `sessions_${this.currentPatientId}`;
|
| 1402 |
const cached = localStorage.getItem(cacheKey);
|
| 1403 |
let sessions = [];
|
| 1404 |
+
|
| 1405 |
if (cached) {
|
| 1406 |
try {
|
| 1407 |
const cachedData = JSON.parse(cached);
|
|
|
|
| 1416 |
console.warn('Failed to parse cached sessions:', e);
|
| 1417 |
}
|
| 1418 |
}
|
| 1419 |
+
|
| 1420 |
// If no cache or cache is stale, fetch from backend
|
| 1421 |
if (sessions.length === 0) {
|
| 1422 |
try {
|
|
|
|
| 1424 |
if (resp.ok) {
|
| 1425 |
const data = await resp.json();
|
| 1426 |
sessions = Array.isArray(data.sessions) ? data.sessions : [];
|
| 1427 |
+
|
| 1428 |
// Cache the sessions
|
| 1429 |
localStorage.setItem(cacheKey, JSON.stringify({
|
| 1430 |
sessions: sessions,
|
|
|
|
| 1438 |
console.error('Failed to load patient sessions', e);
|
| 1439 |
}
|
| 1440 |
}
|
| 1441 |
+
|
| 1442 |
// Process sessions
|
| 1443 |
this.backendSessions = sessions.map(s => ({
|
| 1444 |
id: s.session_id,
|
|
|
|
| 1448 |
lastActivity: s.last_activity || new Date().toISOString(),
|
| 1449 |
source: 'backend'
|
| 1450 |
}));
|
| 1451 |
+
|
| 1452 |
if (this.backendSessions.length > 0) {
|
| 1453 |
this.currentSession = this.backendSessions[0];
|
| 1454 |
await this.hydrateMessagesForSession(this.currentSession.id);
|
| 1455 |
}
|
| 1456 |
+
|
| 1457 |
this.loadChatSessions();
|
| 1458 |
}
|
| 1459 |
|
|
|
|
| 1463 |
const cacheKey = `messages_${this.currentPatientId}_${sessionId}`;
|
| 1464 |
const cached = localStorage.getItem(cacheKey);
|
| 1465 |
let messages = [];
|
| 1466 |
+
|
| 1467 |
if (cached) {
|
| 1468 |
try {
|
| 1469 |
const cachedData = JSON.parse(cached);
|
|
|
|
| 1478 |
console.warn('Failed to parse cached messages:', e);
|
| 1479 |
}
|
| 1480 |
}
|
| 1481 |
+
|
| 1482 |
// If no cache or cache is stale, fetch from backend
|
| 1483 |
if (messages.length === 0) {
|
| 1484 |
const resp = await fetch(`/sessions/${sessionId}/messages?patient_id=${this.currentPatientId}&limit=1000`);
|
|
|
|
| 1494 |
content: m.content,
|
| 1495 |
timestamp: m.timestamp
|
| 1496 |
}));
|
| 1497 |
+
|
| 1498 |
// Cache the messages
|
| 1499 |
localStorage.setItem(cacheKey, JSON.stringify({
|
| 1500 |
messages: messages,
|
|
|
|
| 1502 |
}));
|
| 1503 |
console.log('[DEBUG] Cached messages for session:', sessionId, 'count:', messages.length);
|
| 1504 |
}
|
| 1505 |
+
|
| 1506 |
// Sort messages by timestamp (ascending order for display)
|
| 1507 |
const sortedMessages = messages.sort((a, b) => {
|
| 1508 |
const timeA = new Date(a.timestamp || 0).getTime();
|
| 1509 |
const timeB = new Date(b.timestamp || 0).getTime();
|
| 1510 |
return timeA - timeB;
|
| 1511 |
});
|
| 1512 |
+
|
| 1513 |
if (this.currentSession && this.currentSession.id === sessionId) {
|
| 1514 |
this.currentSession.messages = sortedMessages;
|
| 1515 |
this.clearChatMessages();
|
|
|
|
| 1570 |
console.log('[DEBUG] Search URL:', url);
|
| 1571 |
const resp = await fetch(url);
|
| 1572 |
console.log('[DEBUG] Search response status:', resp.status);
|
| 1573 |
+
|
| 1574 |
let mongoResults = [];
|
| 1575 |
if (resp.ok) {
|
| 1576 |
const data = await resp.json();
|
|
|
|
| 1579 |
} else {
|
| 1580 |
console.warn('MongoDB search request failed', resp.status);
|
| 1581 |
}
|
| 1582 |
+
|
| 1583 |
// Get localStorage suggestions as fallback/additional results
|
| 1584 |
const localResults = await this.getLocalStorageSuggestions(q);
|
| 1585 |
+
|
| 1586 |
// Combine and deduplicate results (MongoDB results take priority)
|
| 1587 |
const combinedResults = this.combinePatientResults(mongoResults, localResults);
|
| 1588 |
console.log('[DEBUG] Combined search results:', combinedResults);
|
| 1589 |
renderSuggestions(combinedResults);
|
| 1590 |
+
|
| 1591 |
+
} catch (e) {
|
| 1592 |
console.error('[DEBUG] Search error:', e);
|
| 1593 |
// Fallback for network errors
|
| 1594 |
console.log('[DEBUG] Trying fallback search after error');
|
|
|
|
| 1617 |
const closeBtn = document.getElementById('patientModalClose');
|
| 1618 |
const logoutBtn = document.getElementById('patientLogoutBtn');
|
| 1619 |
const createBtn = document.getElementById('patientCreateBtn');
|
| 1620 |
+
|
| 1621 |
if (profileBtn && modal) {
|
| 1622 |
profileBtn.addEventListener('click', async () => {
|
| 1623 |
const pid = this?.currentPatientId;
|
|
|
|
| 1641 |
modal.classList.add('show');
|
| 1642 |
});
|
| 1643 |
}
|
| 1644 |
+
|
| 1645 |
if (closeBtn && modal) {
|
| 1646 |
closeBtn.addEventListener('click', () => modal.classList.remove('show'));
|
| 1647 |
modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('show'); });
|
| 1648 |
}
|
| 1649 |
+
|
| 1650 |
if (logoutBtn) {
|
| 1651 |
logoutBtn.addEventListener('click', () => {
|
| 1652 |
if (confirm('Log out current patient?')) {
|
|
|
|
| 1660 |
}
|
| 1661 |
});
|
| 1662 |
}
|
| 1663 |
+
|
| 1664 |
if (createBtn) createBtn.addEventListener('click', () => modal.classList.remove('show'));
|
| 1665 |
}
|
| 1666 |
|
|
|
|
| 1671 |
try {
|
| 1672 |
this.audioRecorder = new AudioRecordingUI(this);
|
| 1673 |
const success = await this.audioRecorder.initialize();
|
| 1674 |
+
|
| 1675 |
if (success) {
|
| 1676 |
console.log('[Audio] Audio recording initialized successfully');
|
| 1677 |
// Make globally accessible for voice detection callback
|
|
|
|
| 1729 |
const response = await this.callMedicalAPI(message);
|
| 1730 |
this.addMessage('assistant', response);
|
| 1731 |
this.updateCurrentSession();
|
| 1732 |
+
|
| 1733 |
// Invalidate caches after successful message exchange
|
| 1734 |
if (this.currentSession && this.currentSession.id) {
|
| 1735 |
this.invalidateMessageCache(this.currentPatientId, this.currentSession.id);
|
|
|
|
| 1802 |
// Check if session needs title generation after messages are loaded
|
| 1803 |
checkAndGenerateSessionTitle() {
|
| 1804 |
if (!this.currentSession || !this.currentSession.messages) return;
|
| 1805 |
+
|
| 1806 |
// Check if this is a new session that needs a title (exactly 2 messages: user + assistant)
|
| 1807 |
+
if (this.currentSession.messages.length === 2 &&
|
| 1808 |
+
this.currentSession.title === 'New Chat' &&
|
| 1809 |
this.currentSession.messages[0].role === 'user') {
|
| 1810 |
const firstMessage = this.currentSession.messages[0].content;
|
| 1811 |
this.summariseAndSetTitle(firstMessage);
|
|
|
|
| 1889 |
}
|
| 1890 |
// ----------------------------------------------------------
|
| 1891 |
// Additional UI setup END
|
| 1892 |
+
// ----------------------------------------------------------
|
| 1893 |
|
| 1894 |
|
| 1895 |
// Initialize the app when DOM is loaded
|
|
|
|
| 1998 |
this.isRecording = true;
|
| 1999 |
this.recordingStartTime = Date.now();
|
| 2000 |
console.log('Audio recording started');
|
| 2001 |
+
|
| 2002 |
// Start timer
|
| 2003 |
this.startTimer();
|
| 2004 |
+
|
| 2005 |
// Start voice detection
|
| 2006 |
this.startVoiceDetection();
|
| 2007 |
+
|
| 2008 |
return true;
|
| 2009 |
} catch (error) {
|
| 2010 |
console.error('Failed to start recording:', error);
|
|
|
|
| 2021 |
this.mediaRecorder.stop();
|
| 2022 |
this.isRecording = false;
|
| 2023 |
console.log('Audio recording stopped');
|
| 2024 |
+
|
| 2025 |
// Stop timer and voice detection
|
| 2026 |
this.stopTimer();
|
| 2027 |
this.stopVoiceDetection();
|
| 2028 |
+
|
| 2029 |
return true;
|
| 2030 |
} catch (error) {
|
| 2031 |
console.error('Failed to stop recording:', error);
|
|
|
|
| 2040 |
const minutes = Math.floor(elapsed / 60);
|
| 2041 |
const seconds = elapsed % 60;
|
| 2042 |
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
| 2043 |
+
|
| 2044 |
const timerElement = document.getElementById('recordingTimer');
|
| 2045 |
if (timerElement) {
|
| 2046 |
timerElement.textContent = timeString;
|
|
|
|
| 2059 |
startVoiceDetection() {
|
| 2060 |
const checkVoice = () => {
|
| 2061 |
if (!this.isRecording || !this.analyser) return;
|
| 2062 |
+
|
| 2063 |
const bufferLength = this.analyser.frequencyBinCount;
|
| 2064 |
const dataArray = new Uint8Array(bufferLength);
|
| 2065 |
this.analyser.getByteFrequencyData(dataArray);
|
| 2066 |
+
|
| 2067 |
// Calculate average volume
|
| 2068 |
const average = dataArray.reduce((sum, value) => sum + value, 0) / bufferLength;
|
| 2069 |
const threshold = 20; // Adjust this value to change sensitivity
|
| 2070 |
+
|
| 2071 |
const container = document.querySelector('.recording-container');
|
| 2072 |
const statusElement = document.getElementById('recordingStatus');
|
| 2073 |
+
|
| 2074 |
if (average > threshold) {
|
| 2075 |
// Voice detected
|
| 2076 |
container.classList.remove('silent');
|
| 2077 |
container.classList.add('listening');
|
| 2078 |
if (statusElement) statusElement.textContent = 'Listening...';
|
| 2079 |
+
|
| 2080 |
// Reset silence timer
|
| 2081 |
this.resetSilenceTimer();
|
| 2082 |
} else {
|
|
|
|
| 2085 |
container.classList.add('silent');
|
| 2086 |
if (statusElement) statusElement.textContent = 'Silence detected...';
|
| 2087 |
}
|
| 2088 |
+
|
| 2089 |
requestAnimationFrame(checkVoice);
|
| 2090 |
};
|
| 2091 |
+
|
| 2092 |
checkVoice();
|
| 2093 |
}
|
| 2094 |
|
|
|
|
| 2100 |
if (this.silenceTimer) {
|
| 2101 |
clearTimeout(this.silenceTimer);
|
| 2102 |
}
|
| 2103 |
+
|
| 2104 |
// Auto-stop after 3 seconds of silence
|
| 2105 |
this.silenceTimer = setTimeout(() => {
|
| 2106 |
if (this.isRecording) {
|
|
|
|
| 2123 |
try {
|
| 2124 |
// Create audio blob
|
| 2125 |
const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
|
| 2126 |
+
|
| 2127 |
// Transcribe audio
|
| 2128 |
const transcribedText = await this.transcribeAudio(audioBlob);
|
| 2129 |
+
|
| 2130 |
return transcribedText;
|
| 2131 |
} catch (error) {
|
| 2132 |
console.error('Failed to process recording:', error);
|
|
|
|
| 2163 |
this.audioStream.getTracks().forEach(track => track.stop());
|
| 2164 |
this.audioStream = null;
|
| 2165 |
}
|
| 2166 |
+
|
| 2167 |
if (this.audioContext) {
|
| 2168 |
this.audioContext.close();
|
| 2169 |
this.audioContext = null;
|
| 2170 |
}
|
| 2171 |
+
|
| 2172 |
if (this.silenceTimer) {
|
| 2173 |
clearTimeout(this.silenceTimer);
|
| 2174 |
this.silenceTimer = null;
|
| 2175 |
}
|
| 2176 |
+
|
| 2177 |
this.stopTimer();
|
| 2178 |
+
|
| 2179 |
this.mediaRecorder = null;
|
| 2180 |
this.audioChunks = [];
|
| 2181 |
this.isRecording = false;
|
|
|
|
| 2217 |
setupUI() {
|
| 2218 |
this.microphoneBtn = document.getElementById('microphoneBtn');
|
| 2219 |
this.modal = document.getElementById('audioRecordingModal');
|
| 2220 |
+
|
| 2221 |
if (!this.microphoneBtn) {
|
| 2222 |
console.error('Microphone button not found');
|
| 2223 |
return;
|
|
|
|
| 2230 |
|
| 2231 |
// Set up event listeners
|
| 2232 |
this.microphoneBtn.addEventListener('click', (e) => this.startRecording(e));
|
| 2233 |
+
|
| 2234 |
// Modal close handlers
|
| 2235 |
const closeBtn = document.getElementById('audioRecordingModalClose');
|
| 2236 |
const stopBtn = document.getElementById('stopRecordingBtn');
|
| 2237 |
+
|
| 2238 |
if (closeBtn) {
|
| 2239 |
closeBtn.addEventListener('click', () => this.closeModal());
|
| 2240 |
}
|
| 2241 |
+
|
| 2242 |
if (stopBtn) {
|
| 2243 |
stopBtn.addEventListener('click', () => this.stopRecording());
|
| 2244 |
}
|
|
|
|
| 2260 |
}
|
| 2261 |
|
| 2262 |
event.preventDefault();
|
| 2263 |
+
|
| 2264 |
try {
|
| 2265 |
// Show modal
|
| 2266 |
this.showModal();
|
| 2267 |
+
|
| 2268 |
// Start recording
|
| 2269 |
const success = this.recorder.startRecording();
|
| 2270 |
if (success) {
|
|
|
|
| 2302 |
try {
|
| 2303 |
// Process the recording
|
| 2304 |
const transcribedText = await this.recorder.processRecording();
|
| 2305 |
+
|
| 2306 |
if (transcribedText) {
|
| 2307 |
this.insertTranscribedText(transcribedText);
|
| 2308 |
this.showSuccess('Audio transcribed successfully!');
|
| 2309 |
} else {
|
| 2310 |
this.showError('No speech detected. Please try again.');
|
| 2311 |
}
|
| 2312 |
+
|
| 2313 |
this.closeModal();
|
| 2314 |
} catch (error) {
|
| 2315 |
console.error('Failed to process recording:', error);
|
|
|
|
| 2329 |
if (this.modal) {
|
| 2330 |
this.modal.classList.remove('show');
|
| 2331 |
document.body.style.overflow = ''; // Restore scrolling
|
| 2332 |
+
|
| 2333 |
// Stop recording if still active
|
| 2334 |
if (this.recorder.isRecording) {
|
| 2335 |
this.recorder.stopRecording();
|
| 2336 |
}
|
| 2337 |
+
|
| 2338 |
// Reset modal state
|
| 2339 |
this.updateModalState('ready');
|
| 2340 |
}
|
|
|
|
| 2344 |
const container = document.querySelector('.recording-container');
|
| 2345 |
const statusElement = document.getElementById('recordingStatus');
|
| 2346 |
const stopBtn = document.getElementById('stopRecordingBtn');
|
| 2347 |
+
|
| 2348 |
if (!container) return;
|
| 2349 |
|
| 2350 |
// Remove all state classes
|
| 2351 |
container.classList.remove('listening', 'silent', 'processing');
|
| 2352 |
+
|
| 2353 |
switch (state) {
|
| 2354 |
case 'ready':
|
| 2355 |
container.classList.add('listening');
|
|
|
|
| 2379 |
// Append transcribed text to existing content
|
| 2380 |
const currentText = chatInput.value.trim();
|
| 2381 |
const newText = currentText ? `${currentText} ${text}` : text;
|
| 2382 |
+
|
| 2383 |
chatInput.value = newText;
|
| 2384 |
+
|
| 2385 |
// Add visual feedback for transcribed text
|
| 2386 |
chatInput.classList.add('transcribed');
|
| 2387 |
+
|
| 2388 |
// Remove the highlighting after a few seconds
|
| 2389 |
setTimeout(() => {
|
| 2390 |
chatInput.classList.remove('transcribed');
|
| 2391 |
}, 3000);
|
| 2392 |
+
|
| 2393 |
// Trigger input event to update UI
|
| 2394 |
chatInput.dispatchEvent(new Event('input', { bubbles: true }));
|
| 2395 |
+
|
| 2396 |
// Focus the input
|
| 2397 |
chatInput.focus();
|
| 2398 |
+
|
| 2399 |
// Auto-resize if needed
|
| 2400 |
if (this.app && this.app.autoResizeTextarea) {
|
| 2401 |
this.app.autoResizeTextarea(chatInput);
|
|
|
|
| 2407 |
|
| 2408 |
// Remove all state classes
|
| 2409 |
this.microphoneBtn.classList.remove('recording-ready', 'recording-active', 'recording-processing');
|
| 2410 |
+
|
| 2411 |
// Add appropriate state class
|
| 2412 |
switch (state) {
|
| 2413 |
case 'ready':
|
|
|
|
| 2432 |
errorMsg = document.createElement('div');
|
| 2433 |
errorMsg.id = 'audioError';
|
| 2434 |
errorMsg.className = 'audio-error-message';
|
| 2435 |
+
|
| 2436 |
const chatInputContainer = document.querySelector('.chat-input-container');
|
| 2437 |
if (chatInputContainer) {
|
| 2438 |
chatInputContainer.appendChild(errorMsg);
|
| 2439 |
}
|
| 2440 |
}
|
| 2441 |
+
|
| 2442 |
errorMsg.textContent = message;
|
| 2443 |
errorMsg.style.display = 'block';
|
| 2444 |
+
|
| 2445 |
// Hide after 5 seconds
|
| 2446 |
setTimeout(() => {
|
| 2447 |
errorMsg.style.display = 'none';
|
|
|
|
| 2455 |
successMsg = document.createElement('div');
|
| 2456 |
successMsg.id = 'audioSuccess';
|
| 2457 |
successMsg.className = 'audio-success-message';
|
| 2458 |
+
|
| 2459 |
const chatInputContainer = document.querySelector('.chat-input-container');
|
| 2460 |
if (chatInputContainer) {
|
| 2461 |
chatInputContainer.appendChild(successMsg);
|
| 2462 |
}
|
| 2463 |
}
|
| 2464 |
+
|
| 2465 |
successMsg.textContent = message;
|
| 2466 |
successMsg.style.display = 'block';
|
| 2467 |
+
|
| 2468 |
// Hide after 3 seconds
|
| 2469 |
setTimeout(() => {
|
| 2470 |
successMsg.style.display = 'none';
|
|
|
|
| 2555 |
if (userModal) {
|
| 2556 |
userModal.addEventListener('click', (e) => { if (e.target === userModal) userModal.classList.remove('show'); });
|
| 2557 |
}
|
| 2558 |
+
});
|