Spaces:
Sleeping
Sleeping
Commit
·
c6d658a
1
Parent(s):
012d58e
Upd dynamic state change and repolling conversation on page refresh
Browse files- static/js/app.js +105 -16
static/js/app.js
CHANGED
|
@@ -5,17 +5,25 @@ class MedicalChatbotApp {
|
|
| 5 |
this.currentUser = null; // doctor
|
| 6 |
this.currentPatientId = null;
|
| 7 |
this.currentSession = null;
|
|
|
|
| 8 |
this.memory = new Map(); // In-memory storage for demo
|
| 9 |
this.isLoading = false;
|
| 10 |
|
| 11 |
this.init();
|
| 12 |
}
|
| 13 |
|
| 14 |
-
init() {
|
| 15 |
this.setupEventListeners();
|
| 16 |
this.loadUserPreferences();
|
| 17 |
this.initializeUser();
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
this.ensureStartupSession();
|
| 20 |
this.loadChatSessions();
|
| 21 |
this.setupTheme();
|
|
@@ -91,6 +99,28 @@ class MedicalChatbotApp {
|
|
| 91 |
});
|
| 92 |
}
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
setupModalEvents() {
|
| 95 |
// User modal
|
| 96 |
document.getElementById('userModalClose').addEventListener('click', () => {
|
|
@@ -217,7 +247,7 @@ class MedicalChatbotApp {
|
|
| 217 |
|
| 218 |
startNewChat() {
|
| 219 |
if (this.currentSession) {
|
| 220 |
-
// Save current session
|
| 221 |
this.saveCurrentSession();
|
| 222 |
}
|
| 223 |
|
|
@@ -245,6 +275,10 @@ class MedicalChatbotApp {
|
|
| 245 |
}
|
| 246 |
|
| 247 |
ensureStartupSession() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
const sessions = this.getChatSessions();
|
| 249 |
if (sessions.length === 0) {
|
| 250 |
// Create a new session immediately so it shows in sidebar
|
|
@@ -291,8 +325,8 @@ How can I assist you today?`;
|
|
| 291 |
status.style.color = 'var(--warning-color)';
|
| 292 |
return;
|
| 293 |
}
|
| 294 |
-
// For now we accept ID and load sessions from backend
|
| 295 |
this.currentPatientId = id;
|
|
|
|
| 296 |
status.textContent = `Patient: ${id}`;
|
| 297 |
status.style.color = 'var(--text-secondary)';
|
| 298 |
await this.fetchAndRenderPatientSessions();
|
|
@@ -301,18 +335,59 @@ How can I assist you today?`;
|
|
| 301 |
async fetchAndRenderPatientSessions() {
|
| 302 |
if (!this.currentPatientId) return;
|
| 303 |
try {
|
| 304 |
-
const resp = await fetch(`/
|
| 305 |
if (resp.ok) {
|
| 306 |
const data = await resp.json();
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
}
|
| 310 |
} catch (e) {
|
| 311 |
console.error('Failed to load patient sessions', e);
|
|
|
|
| 312 |
}
|
| 313 |
this.loadChatSessions();
|
| 314 |
}
|
| 315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
async sendMessage() {
|
| 317 |
const input = document.getElementById('chatInput');
|
| 318 |
const message = input.value.trim();
|
|
@@ -587,8 +662,10 @@ How can I assist you today?`;
|
|
| 587 |
const sessionsContainer = document.getElementById('chatSessions');
|
| 588 |
sessionsContainer.innerHTML = '';
|
| 589 |
|
| 590 |
-
//
|
| 591 |
-
const sessions = this.
|
|
|
|
|
|
|
| 592 |
|
| 593 |
if (sessions.length === 0) {
|
| 594 |
sessionsContainer.innerHTML = '<div class="no-sessions">No chat sessions yet</div>';
|
|
@@ -598,8 +675,13 @@ How can I assist you today?`;
|
|
| 598 |
sessions.forEach(session => {
|
| 599 |
const sessionElement = document.createElement('div');
|
| 600 |
sessionElement.className = `chat-session ${session.id === this.currentSession?.id ? 'active' : ''}`;
|
| 601 |
-
sessionElement.addEventListener('click', () => {
|
| 602 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
});
|
| 604 |
|
| 605 |
const time = this.formatTime(session.lastActivity);
|
|
@@ -620,12 +702,18 @@ How can I assist you today?`;
|
|
| 620 |
|
| 621 |
sessionsContainer.appendChild(sessionElement);
|
| 622 |
|
| 623 |
-
// Wire 3-dot menu
|
| 624 |
const menuBtn = sessionElement.querySelector('.chat-session-menu');
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
});
|
| 630 |
}
|
| 631 |
|
|
@@ -697,6 +785,7 @@ How can I assist you today?`;
|
|
| 697 |
|
| 698 |
saveCurrentSession() {
|
| 699 |
if (!this.currentSession) return;
|
|
|
|
| 700 |
|
| 701 |
const sessions = this.getChatSessions();
|
| 702 |
const existingIndex = sessions.findIndex(s => s.id === this.currentSession.id);
|
|
|
|
| 5 |
this.currentUser = null; // doctor
|
| 6 |
this.currentPatientId = null;
|
| 7 |
this.currentSession = null;
|
| 8 |
+
this.backendSessions = [];
|
| 9 |
this.memory = new Map(); // In-memory storage for demo
|
| 10 |
this.isLoading = false;
|
| 11 |
|
| 12 |
this.init();
|
| 13 |
}
|
| 14 |
|
| 15 |
+
async init() {
|
| 16 |
this.setupEventListeners();
|
| 17 |
this.loadUserPreferences();
|
| 18 |
this.initializeUser();
|
| 19 |
+
this.loadSavedPatientId();
|
| 20 |
+
|
| 21 |
+
// If a patient is selected, fetch sessions from backend first
|
| 22 |
+
if (this.currentPatientId) {
|
| 23 |
+
await this.fetchAndRenderPatientSessions();
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Ensure a session exists and is displayed immediately if nothing to show
|
| 27 |
this.ensureStartupSession();
|
| 28 |
this.loadChatSessions();
|
| 29 |
this.setupTheme();
|
|
|
|
| 99 |
});
|
| 100 |
}
|
| 101 |
|
| 102 |
+
loadSavedPatientId() {
|
| 103 |
+
const pid = localStorage.getItem('medicalChatbotPatientId');
|
| 104 |
+
if (pid && /^\d{8}$/.test(pid)) {
|
| 105 |
+
this.currentPatientId = pid;
|
| 106 |
+
const status = document.getElementById('patientStatus');
|
| 107 |
+
if (status) {
|
| 108 |
+
status.textContent = `Patient: ${pid}`;
|
| 109 |
+
status.style.color = 'var(--text-secondary)';
|
| 110 |
+
}
|
| 111 |
+
const input = document.getElementById('patientIdInput');
|
| 112 |
+
if (input) input.value = pid;
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
savePatientId() {
|
| 117 |
+
if (this.currentPatientId) {
|
| 118 |
+
localStorage.setItem('medicalChatbotPatientId', this.currentPatientId);
|
| 119 |
+
} else {
|
| 120 |
+
localStorage.removeItem('medicalChatbotPatientId');
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
setupModalEvents() {
|
| 125 |
// User modal
|
| 126 |
document.getElementById('userModalClose').addEventListener('click', () => {
|
|
|
|
| 247 |
|
| 248 |
startNewChat() {
|
| 249 |
if (this.currentSession) {
|
| 250 |
+
// Save current session (local only)
|
| 251 |
this.saveCurrentSession();
|
| 252 |
}
|
| 253 |
|
|
|
|
| 275 |
}
|
| 276 |
|
| 277 |
ensureStartupSession() {
|
| 278 |
+
// If we already have backend sessions for selected patient, do not create a local one
|
| 279 |
+
if (this.backendSessions && this.backendSessions.length > 0) {
|
| 280 |
+
return;
|
| 281 |
+
}
|
| 282 |
const sessions = this.getChatSessions();
|
| 283 |
if (sessions.length === 0) {
|
| 284 |
// Create a new session immediately so it shows in sidebar
|
|
|
|
| 325 |
status.style.color = 'var(--warning-color)';
|
| 326 |
return;
|
| 327 |
}
|
|
|
|
| 328 |
this.currentPatientId = id;
|
| 329 |
+
this.savePatientId();
|
| 330 |
status.textContent = `Patient: ${id}`;
|
| 331 |
status.style.color = 'var(--text-secondary)';
|
| 332 |
await this.fetchAndRenderPatientSessions();
|
|
|
|
| 335 |
async fetchAndRenderPatientSessions() {
|
| 336 |
if (!this.currentPatientId) return;
|
| 337 |
try {
|
| 338 |
+
const resp = await fetch(`/patients/${this.currentPatientId}/sessions`);
|
| 339 |
if (resp.ok) {
|
| 340 |
const data = await resp.json();
|
| 341 |
+
const sessions = Array.isArray(data.sessions) ? data.sessions : [];
|
| 342 |
+
this.backendSessions = sessions.map(s => ({
|
| 343 |
+
id: s.session_id,
|
| 344 |
+
title: s.title || 'New Chat',
|
| 345 |
+
messages: [],
|
| 346 |
+
createdAt: s.created_at || new Date().toISOString(),
|
| 347 |
+
lastActivity: s.last_activity || new Date().toISOString(),
|
| 348 |
+
source: 'backend'
|
| 349 |
+
}));
|
| 350 |
+
// Prefer backend sessions if present
|
| 351 |
+
if (this.backendSessions.length > 0) {
|
| 352 |
+
this.currentSession = this.backendSessions[0];
|
| 353 |
+
await this.hydrateMessagesForSession(this.currentSession.id);
|
| 354 |
+
}
|
| 355 |
+
} else {
|
| 356 |
+
console.warn('Failed to fetch patient sessions', resp.status);
|
| 357 |
+
this.backendSessions = [];
|
| 358 |
}
|
| 359 |
} catch (e) {
|
| 360 |
console.error('Failed to load patient sessions', e);
|
| 361 |
+
this.backendSessions = [];
|
| 362 |
}
|
| 363 |
this.loadChatSessions();
|
| 364 |
}
|
| 365 |
|
| 366 |
+
async hydrateMessagesForSession(sessionId) {
|
| 367 |
+
try {
|
| 368 |
+
const resp = await fetch(`/sessions/${sessionId}/messages?limit=1000`);
|
| 369 |
+
if (!resp.ok) return;
|
| 370 |
+
const data = await resp.json();
|
| 371 |
+
const msgs = Array.isArray(data.messages) ? data.messages : [];
|
| 372 |
+
const normalized = msgs.map(m => ({
|
| 373 |
+
id: m._id || this.generateId(),
|
| 374 |
+
role: m.role,
|
| 375 |
+
content: m.content,
|
| 376 |
+
timestamp: m.timestamp
|
| 377 |
+
}));
|
| 378 |
+
// set into currentSession if matched
|
| 379 |
+
if (this.currentSession && this.currentSession.id === sessionId) {
|
| 380 |
+
this.currentSession.messages = normalized;
|
| 381 |
+
// Render
|
| 382 |
+
this.clearChatMessages();
|
| 383 |
+
this.currentSession.messages.forEach(m => this.displayMessage(m));
|
| 384 |
+
this.updateChatTitle();
|
| 385 |
+
}
|
| 386 |
+
} catch (e) {
|
| 387 |
+
console.error('Failed to hydrate session messages', e);
|
| 388 |
+
}
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
async sendMessage() {
|
| 392 |
const input = document.getElementById('chatInput');
|
| 393 |
const message = input.value.trim();
|
|
|
|
| 662 |
const sessionsContainer = document.getElementById('chatSessions');
|
| 663 |
sessionsContainer.innerHTML = '';
|
| 664 |
|
| 665 |
+
// Prefer backend sessions if a patient is selected and sessions are available
|
| 666 |
+
const sessions = (this.backendSessions && this.backendSessions.length > 0)
|
| 667 |
+
? this.backendSessions
|
| 668 |
+
: this.getChatSessions();
|
| 669 |
|
| 670 |
if (sessions.length === 0) {
|
| 671 |
sessionsContainer.innerHTML = '<div class="no-sessions">No chat sessions yet</div>';
|
|
|
|
| 675 |
sessions.forEach(session => {
|
| 676 |
const sessionElement = document.createElement('div');
|
| 677 |
sessionElement.className = `chat-session ${session.id === this.currentSession?.id ? 'active' : ''}`;
|
| 678 |
+
sessionElement.addEventListener('click', async () => {
|
| 679 |
+
if (session.source === 'backend') {
|
| 680 |
+
this.currentSession = { ...session };
|
| 681 |
+
await this.hydrateMessagesForSession(session.id);
|
| 682 |
+
} else {
|
| 683 |
+
this.loadChatSession(session.id);
|
| 684 |
+
}
|
| 685 |
});
|
| 686 |
|
| 687 |
const time = this.formatTime(session.lastActivity);
|
|
|
|
| 702 |
|
| 703 |
sessionsContainer.appendChild(sessionElement);
|
| 704 |
|
| 705 |
+
// Wire 3-dot menu (local sessions only for now)
|
| 706 |
const menuBtn = sessionElement.querySelector('.chat-session-menu');
|
| 707 |
+
if (session.source !== 'backend') {
|
| 708 |
+
menuBtn.addEventListener('click', (e) => {
|
| 709 |
+
e.stopPropagation();
|
| 710 |
+
this.showSessionMenu(e.currentTarget, session.id);
|
| 711 |
+
});
|
| 712 |
+
} else {
|
| 713 |
+
menuBtn.disabled = true;
|
| 714 |
+
menuBtn.style.opacity = 0.5;
|
| 715 |
+
menuBtn.title = 'Options available for local sessions only';
|
| 716 |
+
}
|
| 717 |
});
|
| 718 |
}
|
| 719 |
|
|
|
|
| 785 |
|
| 786 |
saveCurrentSession() {
|
| 787 |
if (!this.currentSession) return;
|
| 788 |
+
if (this.currentSession.source === 'backend') return; // do not persist backend sessions locally here
|
| 789 |
|
| 790 |
const sessions = this.getChatSessions();
|
| 791 |
const existingIndex = sessions.findIndex(s => s.id === this.currentSession.id);
|