LiamKhoaLe commited on
Commit
655b3a1
·
1 Parent(s): 43339a7

Refactor app.js into subfiles doctor, patient and handlers JS scripts

Browse files
static/js/app.js CHANGED
@@ -1,4 +1,7 @@
1
  // Medical AI Assistant - Main Application JavaScript
 
 
 
2
 
3
  class MedicalChatbotApp {
4
  constructor() {
@@ -6,7 +9,7 @@ class MedicalChatbotApp {
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
  this.doctors = this.loadDoctors();
12
 
@@ -14,6 +17,11 @@ class MedicalChatbotApp {
14
  }
15
 
16
  async init() {
 
 
 
 
 
17
  this.setupEventListeners();
18
  this.loadUserPreferences();
19
  this.initializeUser();
@@ -76,64 +84,7 @@ class MedicalChatbotApp {
76
  this.startNewChat();
77
  });
78
 
79
- // Patient load button
80
- const loadBtn = document.getElementById('loadPatientBtn');
81
- if (loadBtn) {
82
- loadBtn.addEventListener('click', () => this.loadPatient());
83
- }
84
- const patientInput = document.getElementById('patientIdInput');
85
- const suggestionsEl = document.getElementById('patientSuggestions');
86
- if (patientInput) {
87
- let debounceTimer;
88
- const hideSuggestions = () => { if (suggestionsEl) suggestionsEl.style.display = 'none'; };
89
- const renderSuggestions = (items) => {
90
- if (!suggestionsEl) return;
91
- if (!items || items.length === 0) { hideSuggestions(); return; }
92
- suggestionsEl.innerHTML = '';
93
- items.forEach(p => {
94
- const div = document.createElement('div');
95
- div.className = 'patient-suggestion';
96
- div.textContent = `${p.name || 'Unknown'} (${p.patient_id})`;
97
- div.addEventListener('click', () => {
98
- this.currentPatientId = p.patient_id;
99
- this.savePatientId();
100
- patientInput.value = p.patient_id;
101
- hideSuggestions();
102
- this.fetchAndRenderPatientSessions();
103
- const status = document.getElementById('patientStatus');
104
- if (status) { status.textContent = `Patient: ${p.patient_id}`; status.style.color = 'var(--text-secondary)'; }
105
- });
106
- suggestionsEl.appendChild(div);
107
- });
108
- suggestionsEl.style.display = 'block';
109
- };
110
- patientInput.addEventListener('input', () => {
111
- const q = patientInput.value.trim();
112
- clearTimeout(debounceTimer);
113
- if (!q) { hideSuggestions(); return; }
114
- debounceTimer = setTimeout(async () => {
115
- try {
116
- const resp = await fetch(`/patients/search?q=${encodeURIComponent(q)}&limit=8`, { headers: { 'Accept': 'application/json' } });
117
- if (resp.ok) {
118
- const data = await resp.json();
119
- renderSuggestions(data.results || []);
120
- } else {
121
- console.warn('Search request failed', resp.status);
122
- }
123
- } catch (_) { /* ignore */ }
124
- }, 200);
125
- });
126
- patientInput.addEventListener('keydown', (e) => {
127
- if (e.key === 'Enter') {
128
- this.loadPatient();
129
- hideSuggestions();
130
- }
131
- });
132
- document.addEventListener('click', (ev) => {
133
- if (!suggestionsEl) return;
134
- if (!suggestionsEl.contains(ev.target) && ev.target !== patientInput) hideSuggestions();
135
- });
136
- }
137
 
138
  // Send button and input
139
  document.getElementById('sendBtn').addEventListener('click', () => {
@@ -148,9 +99,7 @@ class MedicalChatbotApp {
148
  });
149
 
150
  // Auto-resize textarea
151
- document.getElementById('chatInput').addEventListener('input', (e) => {
152
- this.autoResizeTextarea(e.target);
153
- });
154
 
155
  // User profile
156
  document.getElementById('userProfile').addEventListener('click', () => {
@@ -163,13 +112,8 @@ class MedicalChatbotApp {
163
  });
164
 
165
  // Action buttons
166
- document.getElementById('exportBtn').addEventListener('click', () => {
167
- this.exportChat();
168
- });
169
-
170
- document.getElementById('clearBtn').addEventListener('click', () => {
171
- this.clearChat();
172
- });
173
 
174
  // Modal events
175
  this.setupModalEvents();
@@ -189,88 +133,9 @@ class MedicalChatbotApp {
189
  if (notificationsEl) notificationsEl.addEventListener('change', () => this.savePreferences());
190
  }
191
 
192
- loadDoctors() {
193
- try {
194
- const raw = localStorage.getItem('medicalChatbotDoctors');
195
- const arr = raw ? JSON.parse(raw) : [];
196
- const seen = new Set();
197
- return arr.filter(x => x && x.name && !seen.has(x.name) && seen.add(x.name));
198
- } catch { return []; }
199
- }
200
-
201
- saveDoctors() {
202
- localStorage.setItem('medicalChatbotDoctors', JSON.stringify(this.doctors));
203
- }
204
-
205
- populateDoctorSelect() {
206
- const sel = document.getElementById('profileNameSelect');
207
- const newSec = document.getElementById('newDoctorSection');
208
- if (!sel) return;
209
- sel.innerHTML = '';
210
- const createOpt = document.createElement('option');
211
- createOpt.value = '__create__';
212
- createOpt.textContent = 'Create doctor user...';
213
- sel.appendChild(createOpt);
214
- // Ensure no duplicates, and include current user name if not in list
215
- const names = new Set(this.doctors.map(d => d.name));
216
- if (this.currentUser?.name && !names.has(this.currentUser.name)) {
217
- this.doctors.unshift({ name: this.currentUser.name });
218
- names.add(this.currentUser.name);
219
- this.saveDoctors();
220
- }
221
- this.doctors.forEach(d => {
222
- const opt = document.createElement('option');
223
- opt.value = d.name;
224
- opt.textContent = d.name;
225
- if (this.currentUser?.name === d.name) opt.selected = true;
226
- sel.appendChild(opt);
227
- });
228
- sel.addEventListener('change', () => {
229
- if (sel.value === '__create__') {
230
- newSec.style.display = '';
231
- const input = document.getElementById('newDoctorName');
232
- if (input) input.value = '';
233
- } else {
234
- newSec.style.display = 'none';
235
- }
236
- });
237
- const cancelBtn = document.getElementById('cancelNewDoctor');
238
- const confirmBtn = document.getElementById('confirmNewDoctor');
239
- if (cancelBtn) cancelBtn.onclick = () => { newSec.style.display = 'none'; sel.value = this.currentUser?.name || ''; };
240
- if (confirmBtn) confirmBtn.onclick = () => {
241
- const name = (document.getElementById('newDoctorName').value || '').trim();
242
- if (!name) return;
243
- if (!this.doctors.find(d => d.name === name)) {
244
- this.doctors.unshift({ name });
245
- this.saveDoctors();
246
- }
247
- this.populateDoctorSelect();
248
- sel.value = name;
249
- newSec.style.display = 'none';
250
- };
251
- }
252
-
253
- loadSavedPatientId() {
254
- const pid = localStorage.getItem('medicalChatbotPatientId');
255
- if (pid && /^\d{8}$/.test(pid)) {
256
- this.currentPatientId = pid;
257
- const status = document.getElementById('patientStatus');
258
- if (status) {
259
- status.textContent = `Patient: ${pid}`;
260
- status.style.color = 'var(--text-secondary)';
261
- }
262
- const input = document.getElementById('patientIdInput');
263
- if (input) input.value = pid;
264
- }
265
- }
266
 
267
- savePatientId() {
268
- if (this.currentPatientId) {
269
- localStorage.setItem('medicalChatbotPatientId', this.currentPatientId);
270
- } else {
271
- localStorage.removeItem('medicalChatbotPatientId');
272
- }
273
- }
274
 
275
  setupModalEvents() {
276
  // User modal
@@ -485,61 +350,9 @@ How can I assist you today?`;
485
  await this.fetchAndRenderPatientSessions();
486
  }
487
 
488
- async fetchAndRenderPatientSessions() {
489
- if (!this.currentPatientId) return;
490
- try {
491
- const resp = await fetch(`/patients/${this.currentPatientId}/sessions`);
492
- if (resp.ok) {
493
- const data = await resp.json();
494
- const sessions = Array.isArray(data.sessions) ? data.sessions : [];
495
- this.backendSessions = sessions.map(s => ({
496
- id: s.session_id,
497
- title: s.title || 'New Chat',
498
- messages: [],
499
- createdAt: s.created_at || new Date().toISOString(),
500
- lastActivity: s.last_activity || new Date().toISOString(),
501
- source: 'backend'
502
- }));
503
- // Prefer backend sessions if present
504
- if (this.backendSessions.length > 0) {
505
- this.currentSession = this.backendSessions[0];
506
- await this.hydrateMessagesForSession(this.currentSession.id);
507
- }
508
- } else {
509
- console.warn('Failed to fetch patient sessions', resp.status);
510
- this.backendSessions = [];
511
- }
512
- } catch (e) {
513
- console.error('Failed to load patient sessions', e);
514
- this.backendSessions = [];
515
- }
516
- this.loadChatSessions();
517
- }
518
 
519
- async hydrateMessagesForSession(sessionId) {
520
- try {
521
- const resp = await fetch(`/sessions/${sessionId}/messages?limit=1000`);
522
- if (!resp.ok) return;
523
- const data = await resp.json();
524
- const msgs = Array.isArray(data.messages) ? data.messages : [];
525
- const normalized = msgs.map(m => ({
526
- id: m._id || this.generateId(),
527
- role: m.role,
528
- content: m.content,
529
- timestamp: m.timestamp
530
- }));
531
- // set into currentSession if matched
532
- if (this.currentSession && this.currentSession.id === sessionId) {
533
- this.currentSession.messages = normalized;
534
- // Render
535
- this.clearChatMessages();
536
- this.currentSession.messages.forEach(m => this.displayMessage(m));
537
- this.updateChatTitle();
538
- }
539
- } catch (e) {
540
- console.error('Failed to hydrate session messages', e);
541
- }
542
- }
543
 
544
  async sendMessage() {
545
  const input = document.getElementById('chatInput');
@@ -776,40 +589,9 @@ How can I assist you today?`;
776
  chatMessages.innerHTML = '';
777
  }
778
 
779
- clearChat() {
780
- if (confirm('Are you sure you want to clear this chat? This action cannot be undone.')) {
781
- this.clearChatMessages();
782
- if (this.currentSession) {
783
- this.currentSession.messages = [];
784
- this.currentSession.title = 'New Chat';
785
- this.updateChatTitle();
786
- }
787
- }
788
- }
789
-
790
- exportChat() {
791
- if (!this.currentSession || this.currentSession.messages.length === 0) {
792
- alert('No chat to export.');
793
- return;
794
- }
795
 
796
- const chatData = {
797
- user: this.currentUser.name,
798
- session: this.currentSession.title,
799
- date: new Date().toISOString(),
800
- messages: this.currentSession.messages
801
- };
802
-
803
- const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' });
804
- const url = URL.createObjectURL(blob);
805
- const a = document.createElement('a');
806
- a.href = url;
807
- a.download = `medical-chat-${this.currentSession.title.replace(/[^a-z0-9]/gi, '-')}.json`;
808
- document.body.appendChild(a);
809
- a.click();
810
- document.body.removeChild(a);
811
- URL.revokeObjectURL(url);
812
- }
813
 
814
  loadChatSessions() {
815
  const sessionsContainer = document.getElementById('chatSessions');
@@ -1009,48 +791,7 @@ How can I assist you today?`;
1009
  this.loadChatSessions();
1010
  }
1011
 
1012
- showUserModal() {
1013
- this.populateDoctorSelect();
1014
- // Ensure the dropdown is visible immediately with options
1015
- const sel = document.getElementById('profileNameSelect');
1016
- if (sel && sel.options.length === 0) {
1017
- // Fallback in unlikely case populate didn't run
1018
- const createOpt = document.createElement('option');
1019
- createOpt.value = '__create__';
1020
- createOpt.textContent = 'Create doctor user...';
1021
- sel.appendChild(createOpt);
1022
- }
1023
- if (sel && !sel.value) sel.value = this.currentUser?.name || '__create__';
1024
- document.getElementById('profileRole').value = this.currentUser.role;
1025
- document.getElementById('profileSpecialty').value = this.currentUser.specialty || '';
1026
-
1027
- this.showModal('userModal');
1028
- }
1029
-
1030
- saveUserProfile() {
1031
- const nameSel = document.getElementById('profileNameSelect');
1032
- const name = nameSel ? nameSel.value : '';
1033
- const role = document.getElementById('profileRole').value;
1034
- const specialty = document.getElementById('profileSpecialty').value.trim();
1035
-
1036
- if (!name || name === '__create__') {
1037
- alert('Please select or create a doctor name.');
1038
- return;
1039
- }
1040
-
1041
- if (!this.doctors.find(d => d.name === name)) {
1042
- this.doctors.unshift({ name });
1043
- this.saveDoctors();
1044
- }
1045
-
1046
- this.currentUser.name = name;
1047
- this.currentUser.role = role;
1048
- this.currentUser.specialty = specialty;
1049
-
1050
- this.saveUser();
1051
- this.updateUserDisplay();
1052
- this.hideModal('userModal');
1053
- }
1054
 
1055
  showSettingsModal() {
1056
  this.showModal('settingsModal');
@@ -1077,13 +818,8 @@ How can I assist you today?`;
1077
  this.hideModal('settingsModal');
1078
  }
1079
 
1080
- showModal(modalId) {
1081
- document.getElementById(modalId).classList.add('show');
1082
- }
1083
-
1084
- hideModal(modalId) {
1085
- document.getElementById(modalId).classList.remove('show');
1086
- }
1087
 
1088
  showLoading(show) {
1089
  this.isLoading = show;
@@ -1099,10 +835,8 @@ How can I assist you today?`;
1099
  }
1100
  }
1101
 
1102
- toggleSidebar() {
1103
- const sidebar = document.getElementById('sidebar');
1104
- sidebar.classList.toggle('show');
1105
- }
1106
 
1107
  updateUserDisplay() {
1108
  document.getElementById('userName').textContent = this.currentUser.name;
@@ -1113,10 +847,8 @@ How can I assist you today?`;
1113
  localStorage.setItem('medicalChatbotUser', JSON.stringify(this.currentUser));
1114
  }
1115
 
1116
- autoResizeTextarea(textarea) {
1117
- textarea.style.height = 'auto';
1118
- textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
1119
- }
1120
 
1121
  generateId() {
1122
  return Date.now().toString(36) + Math.random().toString(36).substr(2);
 
1
  // Medical AI Assistant - Main Application JavaScript
2
+ import { attachUIHandlers } from './ui/handlers.js';
3
+ import { attachDoctorUI } from './ui/doctor.js';
4
+ import { attachPatientUI } from './ui/patient.js';
5
 
6
  class MedicalChatbotApp {
7
  constructor() {
 
9
  this.currentPatientId = null;
10
  this.currentSession = null;
11
  this.backendSessions = [];
12
+ this.memory = new Map(); // In-memory storage for STM/demo
13
  this.isLoading = false;
14
  this.doctors = this.loadDoctors();
15
 
 
17
  }
18
 
19
  async init() {
20
+ // Attach shared UI helpers once
21
+ attachUIHandlers(this);
22
+ // Attach specialized UIs
23
+ attachDoctorUI(this);
24
+ attachPatientUI(this);
25
  this.setupEventListeners();
26
  this.loadUserPreferences();
27
  this.initializeUser();
 
84
  this.startNewChat();
85
  });
86
 
87
+ // Patient handlers moved to ui/patient.js
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  // Send button and input
90
  document.getElementById('sendBtn').addEventListener('click', () => {
 
99
  });
100
 
101
  // Auto-resize textarea
102
+ document.getElementById('chatInput').addEventListener('input', (e) => this.autoResizeTextarea(e.target));
 
 
103
 
104
  // User profile
105
  document.getElementById('userProfile').addEventListener('click', () => {
 
112
  });
113
 
114
  // Action buttons
115
+ document.getElementById('exportBtn').addEventListener('click', () => this.exportChat());
116
+ document.getElementById('clearBtn').addEventListener('click', () => this.clearChat());
 
 
 
 
 
117
 
118
  // Modal events
119
  this.setupModalEvents();
 
133
  if (notificationsEl) notificationsEl.addEventListener('change', () => this.savePreferences());
134
  }
135
 
136
+ // doctor UI moved to ui/doctor.js
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ // patient state moved to ui/patient.js
 
 
 
 
 
 
139
 
140
  setupModalEvents() {
141
  // User modal
 
350
  await this.fetchAndRenderPatientSessions();
351
  }
352
 
353
+ // patient session fetch moved to ui/patient.js
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
 
355
+ // patient message hydration moved to ui/patient.js
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
 
357
  async sendMessage() {
358
  const input = document.getElementById('chatInput');
 
589
  chatMessages.innerHTML = '';
590
  }
591
 
592
+ // clearChat() { /* moved to ui/handlers.js */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
 
594
+ // exportChat() { /* moved to ui/handlers.js */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
 
596
  loadChatSessions() {
597
  const sessionsContainer = document.getElementById('chatSessions');
 
791
  this.loadChatSessions();
792
  }
793
 
794
+ // doctor modal logic moved to ui/doctor.js
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
795
 
796
  showSettingsModal() {
797
  this.showModal('settingsModal');
 
818
  this.hideModal('settingsModal');
819
  }
820
 
821
+ // showModal(modalId) { /* moved to ui/handlers.js */ }
822
+ // hideModal(modalId) { /* moved to ui/handlers.js */ }
 
 
 
 
 
823
 
824
  showLoading(show) {
825
  this.isLoading = show;
 
835
  }
836
  }
837
 
838
+ // toggleSidebar logic moved to ui/handlers.js
839
+
 
 
840
 
841
  updateUserDisplay() {
842
  document.getElementById('userName').textContent = this.currentUser.name;
 
847
  localStorage.setItem('medicalChatbotUser', JSON.stringify(this.currentUser));
848
  }
849
 
850
+ // autoResizeTextarea logic moved to ui/handlers.js
851
+
 
 
852
 
853
  generateId() {
854
  return Date.now().toString(36) + Math.random().toString(36).substr(2);
static/js/ui/doctor.js ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ui/doctor.js
2
+ // Doctor list load/save, dropdown populate, create-flow, show/save profile
3
+
4
+ export function attachDoctorUI(app) {
5
+ // Model: list of doctors persisted in localStorage
6
+ app.loadDoctors = function () {
7
+ try {
8
+ const raw = localStorage.getItem('medicalChatbotDoctors');
9
+ const arr = raw ? JSON.parse(raw) : [];
10
+ const seen = new Set();
11
+ return arr.filter(x => x && x.name && !seen.has(x.name) && seen.add(x.name));
12
+ } catch { return []; }
13
+ };
14
+
15
+ app.saveDoctors = function () {
16
+ localStorage.setItem('medicalChatbotDoctors', JSON.stringify(app.doctors));
17
+ };
18
+
19
+ app.populateDoctorSelect = function () {
20
+ const sel = document.getElementById('profileNameSelect');
21
+ const newSec = document.getElementById('newDoctorSection');
22
+ if (!sel) return;
23
+ sel.innerHTML = '';
24
+ const createOpt = document.createElement('option');
25
+ createOpt.value = '__create__';
26
+ createOpt.textContent = 'Create doctor user...';
27
+ sel.appendChild(createOpt);
28
+ // Ensure no duplicates, include current doctor
29
+ const names = new Set(app.doctors.map(d => d.name));
30
+ if (app.currentUser?.name && !names.has(app.currentUser.name)) {
31
+ app.doctors.unshift({ name: app.currentUser.name });
32
+ names.add(app.currentUser.name);
33
+ app.saveDoctors();
34
+ }
35
+ app.doctors.forEach(d => {
36
+ const opt = document.createElement('option');
37
+ opt.value = d.name;
38
+ opt.textContent = d.name;
39
+ if (app.currentUser?.name === d.name) opt.selected = true;
40
+ sel.appendChild(opt);
41
+ });
42
+ sel.addEventListener('change', () => {
43
+ if (sel.value === '__create__') {
44
+ newSec.style.display = '';
45
+ const input = document.getElementById('newDoctorName');
46
+ if (input) input.value = '';
47
+ } else {
48
+ newSec.style.display = 'none';
49
+ }
50
+ });
51
+ const cancelBtn = document.getElementById('cancelNewDoctor');
52
+ const confirmBtn = document.getElementById('confirmNewDoctor');
53
+ if (cancelBtn) cancelBtn.onclick = () => { newSec.style.display = 'none'; sel.value = app.currentUser?.name || ''; };
54
+ if (confirmBtn) confirmBtn.onclick = () => {
55
+ const name = (document.getElementById('newDoctorName').value || '').trim();
56
+ if (!name) return;
57
+ if (!app.doctors.find(d => d.name === name)) {
58
+ app.doctors.unshift({ name });
59
+ app.saveDoctors();
60
+ }
61
+ app.populateDoctorSelect();
62
+ sel.value = name;
63
+ newSec.style.display = 'none';
64
+ };
65
+ };
66
+
67
+ app.showUserModal = function () {
68
+ app.populateDoctorSelect();
69
+ const sel = document.getElementById('profileNameSelect');
70
+ if (sel && sel.options.length === 0) {
71
+ const createOpt = document.createElement('option');
72
+ createOpt.value = '__create__';
73
+ createOpt.textContent = 'Create doctor user...';
74
+ sel.appendChild(createOpt);
75
+ }
76
+ if (sel && !sel.value) sel.value = app.currentUser?.name || '__create__';
77
+ document.getElementById('profileRole').value = app.currentUser.role;
78
+ document.getElementById('profileSpecialty').value = app.currentUser.specialty || '';
79
+ app.showModal('userModal');
80
+ };
81
+
82
+ app.saveUserProfile = function () {
83
+ const nameSel = document.getElementById('profileNameSelect');
84
+ const name = nameSel ? nameSel.value : '';
85
+ const role = document.getElementById('profileRole').value;
86
+ const specialty = document.getElementById('profileSpecialty').value.trim();
87
+
88
+ if (!name || name === '__create__') {
89
+ alert('Please select or create a doctor name.');
90
+ return;
91
+ }
92
+
93
+ if (!app.doctors.find(d => d.name === name)) {
94
+ app.doctors.unshift({ name });
95
+ app.saveDoctors();
96
+ }
97
+
98
+ app.currentUser.name = name;
99
+ app.currentUser.role = role;
100
+ app.currentUser.specialty = specialty;
101
+
102
+ app.saveUser();
103
+ app.updateUserDisplay();
104
+ app.hideModal('userModal');
105
+ };
106
+ }
107
+
108
+
static/js/ui/handlers.js ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ui/handlers.js
2
+ // DOM wiring helpers: sidebar open/close, modal wiring, textarea autosize, export/clear
3
+
4
+ export function attachUIHandlers(app) {
5
+ // Sidebar toggle implementation
6
+ app.toggleSidebar = function () {
7
+ const sidebar = document.getElementById('sidebar');
8
+ if (sidebar) sidebar.classList.toggle('show');
9
+ };
10
+
11
+ // Textarea autosize
12
+ app.autoResizeTextarea = function (textarea) {
13
+ if (!textarea) return;
14
+ textarea.style.height = 'auto';
15
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
16
+ };
17
+
18
+ // Export current chat as JSON
19
+ app.exportChat = function () {
20
+ if (!app.currentSession || app.currentSession.messages.length === 0) {
21
+ alert('No chat to export.');
22
+ return;
23
+ }
24
+ const chatData = {
25
+ user: app.currentUser?.name || 'Unknown',
26
+ session: app.currentSession.title,
27
+ date: new Date().toISOString(),
28
+ messages: app.currentSession.messages
29
+ };
30
+ const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' });
31
+ const url = URL.createObjectURL(blob);
32
+ const a = document.createElement('a');
33
+ a.href = url;
34
+ a.download = `medical-chat-${app.currentSession.title.replace(/[^a-z0-9]/gi, '-')}.json`;
35
+ document.body.appendChild(a);
36
+ a.click();
37
+ document.body.removeChild(a);
38
+ URL.revokeObjectURL(url);
39
+ };
40
+
41
+ // Clear current chat
42
+ app.clearChat = function () {
43
+ if (confirm('Are you sure you want to clear this chat? This action cannot be undone.')) {
44
+ app.clearChatMessages();
45
+ if (app.currentSession) {
46
+ app.currentSession.messages = [];
47
+ app.currentSession.title = 'New Chat';
48
+ app.updateChatTitle();
49
+ }
50
+ }
51
+ };
52
+
53
+ // Generic modal helpers
54
+ app.showModal = function (modalId) {
55
+ document.getElementById(modalId)?.classList.add('show');
56
+ };
57
+ app.hideModal = function (modalId) {
58
+ document.getElementById(modalId)?.classList.remove('show');
59
+ };
60
+ }
61
+
62
+
static/js/ui/patient.js ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ui/patient.js
2
+ // Patient selection, typeahead search, load/hydrate, patient modal wiring
3
+
4
+ export function attachPatientUI(app) {
5
+ // State helpers
6
+ app.loadSavedPatientId = function () {
7
+ const pid = localStorage.getItem('medicalChatbotPatientId');
8
+ if (pid && /^\d{8}$/.test(pid)) {
9
+ app.currentPatientId = pid;
10
+ const status = document.getElementById('patientStatus');
11
+ if (status) {
12
+ status.textContent = `Patient: ${pid}`;
13
+ status.style.color = 'var(--text-secondary)';
14
+ }
15
+ const input = document.getElementById('patientIdInput');
16
+ if (input) input.value = pid;
17
+ }
18
+ };
19
+
20
+ app.savePatientId = function () {
21
+ if (app.currentPatientId) localStorage.setItem('medicalChatbotPatientId', app.currentPatientId);
22
+ else localStorage.removeItem('medicalChatbotPatientId');
23
+ };
24
+
25
+ app.loadPatient = async function () {
26
+ const input = document.getElementById('patientIdInput');
27
+ const status = document.getElementById('patientStatus');
28
+ const id = (input?.value || '').trim();
29
+ if (!/^\d{8}$/.test(id)) {
30
+ if (status) { status.textContent = 'Invalid patient ID. Use 8 digits.'; status.style.color = 'var(--warning-color)'; }
31
+ return;
32
+ }
33
+ app.currentPatientId = id;
34
+ app.savePatientId();
35
+ if (status) { status.textContent = `Patient: ${id}`; status.style.color = 'var(--text-secondary)'; }
36
+ await app.fetchAndRenderPatientSessions();
37
+ };
38
+
39
+ app.fetchAndRenderPatientSessions = async function () {
40
+ if (!app.currentPatientId) return;
41
+ try {
42
+ const resp = await fetch(`/patients/${app.currentPatientId}/sessions`);
43
+ if (resp.ok) {
44
+ const data = await resp.json();
45
+ const sessions = Array.isArray(data.sessions) ? data.sessions : [];
46
+ app.backendSessions = sessions.map(s => ({
47
+ id: s.session_id,
48
+ title: s.title || 'New Chat',
49
+ messages: [],
50
+ createdAt: s.created_at || new Date().toISOString(),
51
+ lastActivity: s.last_activity || new Date().toISOString(),
52
+ source: 'backend'
53
+ }));
54
+ if (app.backendSessions.length > 0) {
55
+ app.currentSession = app.backendSessions[0];
56
+ await app.hydrateMessagesForSession(app.currentSession.id);
57
+ }
58
+ } else {
59
+ console.warn('Failed to fetch patient sessions', resp.status);
60
+ app.backendSessions = [];
61
+ }
62
+ } catch (e) {
63
+ console.error('Failed to load patient sessions', e);
64
+ app.backendSessions = [];
65
+ }
66
+ app.loadChatSessions();
67
+ };
68
+
69
+ app.hydrateMessagesForSession = async function (sessionId) {
70
+ try {
71
+ const resp = await fetch(`/sessions/${sessionId}/messages?limit=1000`);
72
+ if (!resp.ok) return;
73
+ const data = await resp.json();
74
+ const msgs = Array.isArray(data.messages) ? data.messages : [];
75
+ const normalized = msgs.map(m => ({
76
+ id: m._id || app.generateId(),
77
+ role: m.role,
78
+ content: m.content,
79
+ timestamp: m.timestamp
80
+ }));
81
+ if (app.currentSession && app.currentSession.id === sessionId) {
82
+ app.currentSession.messages = normalized;
83
+ app.clearChatMessages();
84
+ app.currentSession.messages.forEach(m => app.displayMessage(m));
85
+ app.updateChatTitle();
86
+ }
87
+ } catch (e) {
88
+ console.error('Failed to hydrate session messages', e);
89
+ }
90
+ };
91
+
92
+ // Bind patient input + typeahead + load button
93
+ app.bindPatientHandlers = function () {
94
+ const loadBtn = document.getElementById('loadPatientBtn');
95
+ if (loadBtn) loadBtn.addEventListener('click', () => app.loadPatient());
96
+ const patientInput = document.getElementById('patientIdInput');
97
+ const suggestionsEl = document.getElementById('patientSuggestions');
98
+ if (!patientInput) return;
99
+ let debounceTimer;
100
+ const hideSuggestions = () => { if (suggestionsEl) suggestionsEl.style.display = 'none'; };
101
+ const renderSuggestions = (items) => {
102
+ if (!suggestionsEl) return;
103
+ if (!items || items.length === 0) { hideSuggestions(); return; }
104
+ suggestionsEl.innerHTML = '';
105
+ items.forEach(p => {
106
+ const div = document.createElement('div');
107
+ div.className = 'patient-suggestion';
108
+ div.textContent = `${p.name || 'Unknown'} (${p.patient_id})`;
109
+ div.addEventListener('click', async () => {
110
+ app.currentPatientId = p.patient_id;
111
+ app.savePatientId();
112
+ patientInput.value = p.patient_id;
113
+ hideSuggestions();
114
+ const status = document.getElementById('patientStatus');
115
+ if (status) { status.textContent = `Patient: ${p.patient_id}`; status.style.color = 'var(--text-secondary)'; }
116
+ await app.fetchAndRenderPatientSessions();
117
+ });
118
+ suggestionsEl.appendChild(div);
119
+ });
120
+ suggestionsEl.style.display = 'block';
121
+ };
122
+ patientInput.addEventListener('input', () => {
123
+ const q = patientInput.value.trim();
124
+ clearTimeout(debounceTimer);
125
+ if (!q) { hideSuggestions(); return; }
126
+ debounceTimer = setTimeout(async () => {
127
+ try {
128
+ const resp = await fetch(`/patients/search?q=${encodeURIComponent(q)}&limit=8`, { headers: { 'Accept': 'application/json' } });
129
+ if (resp.ok) {
130
+ const data = await resp.json();
131
+ renderSuggestions(data.results || []);
132
+ } else {
133
+ console.warn('Search request failed', resp.status);
134
+ }
135
+ } catch (_) { /* ignore */ }
136
+ }, 200);
137
+ });
138
+ patientInput.addEventListener('keydown', async (e) => {
139
+ if (e.key === 'Enter') {
140
+ const value = patientInput.value.trim();
141
+ if (/^\d{8}$/.test(value)) {
142
+ await app.loadPatient();
143
+ hideSuggestions();
144
+ } else {
145
+ try {
146
+ const resp = await fetch(`/patients/search?q=${encodeURIComponent(value)}&limit=1`);
147
+ if (resp.ok) {
148
+ const data = await resp.json();
149
+ const first = (data.results || [])[0];
150
+ if (first) {
151
+ app.currentPatientId = first.patient_id;
152
+ app.savePatientId();
153
+ patientInput.value = first.patient_id;
154
+ hideSuggestions();
155
+ const status = document.getElementById('patientStatus');
156
+ if (status) { status.textContent = `Patient: ${first.patient_id}`; status.style.color = 'var(--text-secondary)'; }
157
+ await app.fetchAndRenderPatientSessions();
158
+ return;
159
+ }
160
+ }
161
+ } catch (_) {}
162
+ const status = document.getElementById('patientStatus');
163
+ if (status) { status.textContent = 'No matching patient found'; status.style.color = 'var(--warning-color)'; }
164
+ }
165
+ }
166
+ });
167
+ document.addEventListener('click', (ev) => {
168
+ if (!suggestionsEl) return;
169
+ if (!suggestionsEl.contains(ev.target) && ev.target !== patientInput) hideSuggestions();
170
+ });
171
+ };
172
+
173
+ // Patient modal wiring
174
+ document.addEventListener('DOMContentLoaded', () => {
175
+ const profileBtn = document.getElementById('patientMenuBtn');
176
+ const modal = document.getElementById('patientModal');
177
+ const closeBtn = document.getElementById('patientModalClose');
178
+ const logoutBtn = document.getElementById('patientLogoutBtn');
179
+ const createBtn = document.getElementById('patientCreateBtn');
180
+ if (profileBtn && modal) {
181
+ profileBtn.addEventListener('click', async () => {
182
+ const pid = app?.currentPatientId;
183
+ if (pid) {
184
+ try {
185
+ const resp = await fetch(`/patients/${pid}`);
186
+ if (resp.ok) {
187
+ const p = await resp.json();
188
+ const name = p.name || 'Unknown';
189
+ const age = typeof p.age === 'number' ? p.age : '-';
190
+ const sex = p.sex || '-';
191
+ const meds = Array.isArray(p.medications) && p.medications.length > 0 ? p.medications.join(', ') : '-';
192
+ document.getElementById('patientSummary').textContent = `${name} — ${sex}, ${age}`;
193
+ document.getElementById('patientMedications').textContent = meds;
194
+ document.getElementById('patientAssessment').textContent = p.past_assessment_summary || '-';
195
+ }
196
+ } catch (e) {
197
+ console.error('Failed to load patient profile', e);
198
+ }
199
+ }
200
+ modal.classList.add('show');
201
+ });
202
+ }
203
+ if (closeBtn && modal) {
204
+ closeBtn.addEventListener('click', () => modal.classList.remove('show'));
205
+ modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('show'); });
206
+ }
207
+ if (logoutBtn) {
208
+ logoutBtn.addEventListener('click', () => {
209
+ if (confirm('Log out current patient?')) {
210
+ app.currentPatientId = null;
211
+ localStorage.removeItem('medicalChatbotPatientId');
212
+ const status = document.getElementById('patientStatus');
213
+ if (status) { status.textContent = 'No patient selected'; status.style.color = 'var(--text-secondary)'; }
214
+ const input = document.getElementById('patientIdInput');
215
+ if (input) input.value = '';
216
+ modal.classList.remove('show');
217
+ }
218
+ });
219
+ }
220
+ if (createBtn) createBtn.addEventListener('click', () => modal.classList.remove('show'));
221
+ });
222
+ }
223
+
224
+