dylanglenister commited on
Commit
b4fcc38
·
2 Parent(s): e548905 ef1c061

Merge branch 'hf_main' into hf_mongo

Browse files
README.md CHANGED
@@ -13,310 +13,172 @@ app_port: 7860
13
 
14
  # Medical AI Assistant
15
 
16
- A sophisticated AI-powered medical chatbot system with ChatGPT-like UI, multi-user support, session management, and medical context awareness.
17
 
18
- ## 🚀 Features
19
 
20
- ### Core Functionality
21
- - **AI-Powered Medical Chat**: Intelligent responses to medical questions using advanced language models
22
- - **Multi-User Support**: Individual user profiles with role-based customization (Physician, Nurse, Medical Student, etc.)
23
- - **Chat Session Management**: Multiple concurrent chat sessions per user with persistent history
24
- - **Medical Context Memory**: LRU-based memory system that maintains conversation context and medical history
25
- - **API Key Rotation**: Dynamic rotation of Gemini API keys for reliability and rate limit management
 
 
 
26
 
27
- ### User Interface
28
- - **ChatGPT-like Design**: Familiar, intuitive interface optimized for medical professionals
29
- - **Responsive Design**: Works seamlessly on desktop, tablet, and mobile devices
30
- - **Dark/Light Theme**: Automatic theme switching with system preference detection
31
- - **Real-time Chat**: Smooth, responsive chat experience with typing indicators
32
- - **Session Management**: Easy navigation between different chat sessions
33
 
34
  ### Medical Features
35
- - **Medical Knowledge Base**: Built-in medical information for common symptoms, conditions, and medications
36
- - **Context Awareness**: Remembers previous conversations and provides relevant medical context
 
 
37
  - **Role-Based Responses**: Tailored responses based on user's medical role and specialty
38
  - **Medical Disclaimers**: Appropriate warnings and disclaimers for medical information
39
- - **Export Functionality**: Export chat sessions for medical records or educational purposes
40
-
41
- ## 🏗️ Architecture
42
-
43
- ### Core Components
44
- ```
45
- MedicalDiagnosisSystem/
46
- ├── app.py # FastAPI main application
47
- ├── memo/
48
- │ ├── memory.py # Enhanced LRU memory system
49
- │ └── history.py # Medical history manager
50
- ├── utils/
51
- │ ├── rotator.py # API key rotation system
52
- │ ├── embeddings.py # Embedding client with fallback
53
- │ └── logger.py # Structured logging
54
- └── static/
55
- ├── index.html # Main UI
56
- ├── styles.css # Styling
57
- └── app.js # Frontend logic
58
- ```
59
-
60
- ### Memory System
61
- - **User Profiles**: Persistent user data with preferences and roles
62
- - **Chat Sessions**: Individual conversation threads with message history
63
- - **Medical Context**: QA summaries stored in LRU cache for quick retrieval
64
- - **Semantic Search**: Embedding-based similarity search for relevant medical information
65
-
66
- ### API Integration
67
- - **Gemini API**: Google's advanced language model for medical responses
68
- - **Key Rotation**: Automatic rotation on rate limits or errors
69
- - **Fallback Support**: Graceful degradation when external APIs are unavailable
70
-
71
- ## 🛠️ Installation
72
 
73
  ### Prerequisites
74
- - Python 3.8+
75
- - pip package manager
76
- - Modern web browser
77
 
78
  ### Setup
79
- 1. **Clone the repository**
80
- ```bash
81
- git clone <repository-url>
82
- cd MedicalDiagnosisSystem
83
- ```
84
-
85
- 2. **Install dependencies**
86
- ```bash
87
- pip install -r requirements.txt
88
- ```
89
-
90
- 3. **Set environment variables**
91
- ```bash
92
- # Create .env file
93
- echo "GEMINI_API_1=your_gemini_api_key_1" > .env
94
- echo "GEMINI_API_2=your_gemini_api_key_2" >> .env
95
- echo "GEMINI_API_3=your_gemini_api_key_3" >> .env
96
- ```
97
-
98
- 4. **Run the application**
99
- ```bash
100
- python app.py
101
- ```
102
-
103
- 5. **Access the UI**
104
- Open your browser and navigate to `http://localhost:8000`
105
-
106
- ## 🔧 Configuration
107
-
108
- ### Environment Variables
109
- - `GEMINI_API_1` through `GEMINI_API_5`: Gemini API keys for rotation
110
- - `LOG_LEVEL`: Logging level (DEBUG, INFO, WARNING, ERROR)
111
- - `PORT`: Server port (default: 8000)
112
-
113
- ### Memory Settings
114
- - **LRU Capacity**: Default 50 QA summaries per user
115
- - **Max Sessions**: Default 20 sessions per user
116
- - **Session Timeout**: Configurable session expiration
117
 
118
- ### Embedding Model
119
- - **Default Model**: `all-MiniLM-L6-v2` (384 dimensions)
120
- - **Fallback Mode**: Hash-based embeddings when model unavailable
121
- - **GPU Support**: Optional CUDA acceleration for embeddings
 
122
 
123
  ## 📱 Usage
124
-
125
- ### Getting Started
126
- 1. **Access the Application**: Navigate to the provided URL
127
- 2. **Create User Profile**: Click on your profile to set name, role, and specialty
128
- 3. **Start New Chat**: Click "New Chat" to begin a conversation
129
- 4. **Ask Medical Questions**: Type your medical queries in natural language
130
- 5. **Manage Sessions**: Use the sidebar to switch between different chat sessions
131
-
132
- ### User Roles
133
- - **Physician**: Full medical context with clinical guidance
134
- - **Nurse**: Nursing-focused responses and care instructions
135
- - **Medical Student**: Educational content with learning objectives
136
- - **Healthcare Professional**: General medical information
137
- - **Patient**: Educational content with appropriate disclaimers
138
-
139
- ### Features
140
- - **Real-time Chat**: Instant responses with typing indicators
141
- - **Session Export**: Download chat history as JSON files
142
- - **Context Memory**: System remembers previous conversations
143
- - **Medical Disclaimers**: Appropriate warnings for medical information
144
- - **Responsive Design**: Works on all device sizes
145
-
146
- ## 🔒 Security & Privacy
147
-
148
- ### Data Protection
149
- - **Local Storage**: User data stored locally in browser (no server persistence)
150
- - **Session Isolation**: Users can only access their own data
151
- - **No PII Storage**: Personal information not logged or stored
152
- - **Medical Disclaimers**: Clear warnings about information limitations
153
-
154
- ### API Security
155
- - **Key Rotation**: Automatic API key rotation for security
156
- - **Rate Limiting**: Built-in protection against API abuse
157
- - **Error Handling**: Graceful degradation on API failures
158
-
159
- ## 🧪 Development
160
-
161
- ### Local Development
162
  ```bash
163
- # Install development dependencies
164
  pip install -r requirements.txt
165
-
166
- # Run with auto-reload
167
- python app.py
168
-
169
- # Run tests
170
- pytest
171
-
172
- # Format code
173
- black .
174
-
175
- # Lint code
176
- flake8
177
  ```
178
 
179
  ### Project Structure
180
  ```
181
- medical_diagnosis_system/
182
- ├── scripts/
183
- │ └── download_model.py # Model downloading script
184
  ├── src/
185
  │ ├── api/
186
- │ │ └── routes/ # API endpoint handlers
187
- │ │ ├── chat.py
188
- │ │ ├── session.py
189
- │ │ ├── static.py
190
- │ │ ├── system.py
191
- │ │ └── user.py
192
  │ ├── core/
193
- │ │ ├── memory/ # Memory management
194
- │ │ │ ├── memory.py
195
- │ │ │ └── history.py
196
- │ │ └── state.py # Application state management
197
- │ ├── domain/
198
- │ │ └── knowledge/ # Domain-specific knowledge
199
- │ │ └── medical_kb.py
200
- │ ├── models/ # Data models
 
 
 
 
 
 
 
 
 
 
 
201
  │ │ ├── chat.py
202
  │ │ └── user.py
203
- │ ├── services/ # Business logic services
204
- │ │ ├── medical_response.py
205
- │ │ └── summariser.py
206
- │ ├── utils/ # Utility functions
207
- │ │ ├── __init__.py
208
  │ │ ├── embeddings.py
209
  │ │ ├── logger.py
210
- │ │ ├── naming.py
211
  │ │ └── rotator.py
212
- main.py # Application entry point
213
- ├── static/ # Frontend assets
214
- │ ├── js/
215
- │ │ ├── app.js
216
- │ │ └── health.js
217
  │ ├── css/
218
- │ │ └── styles.css
219
- ├── health.html
220
- │ ├── icon.svg
221
- └── index.html
222
- ├── .dockerignore
223
- ├── .gitattributes
224
- ├── .gitignore
225
- ├── docker-compose.yml
226
  ├── Dockerfile
227
- ├── LICENSE # Project license
228
- ├── README.md # Project documentation
229
- ├── requirements.txt # Production dependencies
230
- ├── requirements-dev.txt # Development dependencies
231
- ├── SETUP_GUIDE.md # Detailed setup instructions
232
- └── start.py # Application launcher
233
- ```
234
-
235
- ### Adding New Features
236
- 1. **Backend**: Add new endpoints in `app.py`
237
- 2. **Memory**: Extend memory system in `memo/memory.py`
238
- 3. **Frontend**: Update UI components in `static/` files
239
- 4. **Testing**: Add tests for new functionality
240
-
241
- ## 🚀 Deployment
242
-
243
- ### Production Considerations
244
- - **Environment Variables**: Secure API key management
245
- - **HTTPS**: Enable SSL/TLS for production
246
- - **Rate Limiting**: Implement request rate limiting
247
- - **Monitoring**: Add health checks and logging
248
- - **Database**: Consider persistent storage for production
249
-
250
- ### Docker Deployment
251
- ```dockerfile
252
- FROM python:3.9-slim
253
- WORKDIR /app
254
- COPY requirements.txt .
255
- RUN pip install -r requirements.txt
256
- COPY . .
257
- EXPOSE 8000
258
- CMD ["python", "app.py"]
259
  ```
260
 
261
- ### Cloud Deployment
262
- - **AWS**: Deploy on EC2 or Lambda
263
- - **Google Cloud**: Use Cloud Run or App Engine
264
- - **Azure**: Deploy on App Service
265
- - **Heroku**: Simple deployment with Procfile
266
-
267
- ## 📊 Performance
268
-
269
- ### Optimization Features
270
- - **Lazy Loading**: Embedding models loaded on demand
271
- - **LRU Caching**: Efficient memory management
272
- - **API Rotation**: Load balancing across multiple API keys
273
- - **Fallback Modes**: Graceful degradation on failures
274
-
275
- ### Monitoring
276
- - **Health Checks**: `/health` endpoint for system status
277
- - **Resource Usage**: CPU and memory monitoring
278
- - **API Metrics**: Response times and success rates
279
- - **Error Tracking**: Comprehensive error logging
280
-
281
- ## 🤝 Contributing
282
-
283
- ### Development Guidelines
284
- 1. **Code Style**: Follow PEP 8 and use Black formatter
285
- 2. **Testing**: Add tests for new features
286
- 3. **Documentation**: Update README and docstrings
287
- 4. **Security**: Follow security best practices
288
- 5. **Performance**: Consider performance implications
289
-
290
- ### Pull Request Process
291
- 1. Fork the repository
292
- 2. Create a feature branch
293
- 3. Make your changes
294
- 4. Add tests and documentation
295
- 5. Submit a pull request
296
-
297
- ## 📄 License
298
-
299
- This project is licensed under the MIT License - see the LICENSE file for details.
300
-
301
- ## ⚠️ Disclaimer
302
-
303
- **Medical Information Disclaimer**: This application provides educational medical information only. It is not a substitute for professional medical advice, diagnosis, or treatment. Always consult with qualified healthcare professionals for medical decisions.
304
-
305
- **AI Limitations**: While this system uses advanced AI technology, it has limitations and should not be relied upon for critical medical decisions.
306
-
307
- ## 🆘 Support
308
-
309
- ### Getting Help
310
- - **Issues**: Report bugs via GitHub Issues
311
- - **Documentation**: Check this README and code comments
312
- - **Community**: Join discussions in GitHub Discussions
313
-
314
- ### Common Issues
315
- - **API Keys**: Ensure Gemini API keys are properly set
316
- - **Dependencies**: Verify all requirements are installed
317
- - **Port Conflicts**: Check if port 8000 is available
318
- - **Memory Issues**: Monitor system resources
319
-
320
- ---
321
-
322
- **Built with ❤️ for the medical community**
 
13
 
14
  # Medical AI Assistant
15
 
16
+ AI-powered medical chatbot with patient-centric memory, MongoDB persistence, and a fast, modern UI.
17
 
18
+ ## 🚀 Key Features
19
 
20
+ - Patient-centric RAG memory
21
+ - Short-term: last 3 QA summaries in in-memory LRU (fast context)
22
+ - Long-term: last 20 QA summaries per patient persisted in MongoDB (continuity)
23
+ - Chat history persistence per session in MongoDB
24
+ - Patient/Doctor context saved on all messages and summaries
25
+ - Patient search typeahead (name or ID) with instant session hydration
26
+ - Doctor dropdown with built‑in "Create doctor user..." flow
27
+ - Modern UI: sidebar sessions, modals (doctor, settings, patient profile), dark/light mode, mobile-friendly
28
+ - Model integration: Gemini responses, NVIDIA summariser fallback via key rotators
29
 
30
+ ## 🏗️ Architecture (high-level)
 
 
 
 
 
31
 
32
  ### Medical Features
33
+ - **Medical Knowledge Base**: Built-in medical information for common symptoms,
34
+ conditions, and medications
35
+ - **Context Awareness**: Remembers previous conversations and provides relevant medical
36
+ context
37
  - **Role-Based Responses**: Tailored responses based on user's medical role and specialty
38
  - **Medical Disclaimers**: Appropriate warnings and disclaimers for medical information
39
+ - **Export Functionality**: Export chat sessions for medical records or educational
40
+ purposes
41
+
42
+ Backend (FastAPI):
43
+ - `src/core/memory/memory.py`: LRU short‑term memory + sessions
44
+ - `src/core/memory/history.py`: builds context; writes memory/messages to Mongo
45
+ - `src/data/`: Mongo helpers (modularized)
46
+ - `connection.py`: Mongo connection + collection names
47
+ - `session/operations.py`: chat sessions (ensure/list/delete/etc.)
48
+ - `message/operations.py`: chat messages (save/list)
49
+ - `patient/operations.py`: patients (get/create/update/search)
50
+ - `user/operations.py`: accounts/doctors (create/search/list)
51
+ - `medical/operations.py`: medical records + memory summaries
52
+ - `utils.py`: generic helpers (indexing, backups)
53
+ - `src/api/routes/`: `chat`, `session`, `user` (patients), `system`, `static`
54
+
55
+ Frontend (static):
56
+ - `static/index.html`, `static/css/styles.css`
57
+ - `static/js/app.js` (or modularized under `static/js/ui/*` and `static/js/chat/*` — see `UI_SETUP.md`)
58
+
59
+ ## 🛠️ Quick Start
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  ### Prerequisites
62
+ - Python 3.11+
63
+ - pip
 
64
 
65
  ### Setup
66
+ 1. Clone and install
67
+ ```bash
68
+ git clone https://huggingface.co/spaces/MedAI-COS30018/MedicalDiagnosisSystem
69
+ cd MedAI
70
+ pip install -r requirements.txt
71
+ ```
72
+ 2. Configure environment
73
+ ```bash
74
+ # Create .env
75
+ echo "GEMINI_API_1=your_gemini_api_key_1" > .env
76
+ echo "NVIDIA_API_1=your_nvidia_api_key_1" >> .env
77
+ # MongoDB (required)
78
+ echo "MONGO_USER=your_mongodb_connection_string" >> .env
79
+ # Optional DB name (default: medicaldiagnosissystem)
80
+ # Optional DB name (default: medicaldiagnosissystem). Env var key: USER_DB
81
+ echo "USER_DB=medicaldiagnosissystem" >> .env
82
+ ```
83
+ 3. Run
84
+ ```bash
85
+ python -m src.main
86
+ ```
87
+ Helpful: [UI SETUP](https://huggingface.co/spaces/MedAI-COS30018/MedicalDiagnosisSystem/blob/main/UI_SETUP.md) | [SETUP GUIDE](https://huggingface.co/spaces/MedAI-COS30018/MedicalDiagnosisSystem/blob/main/SETUP_GUIDE.md)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
+ ## 🔧 Config
90
+ - `GEMINI_API_1..5`, `NVIDIA_API_1..5`
91
+ - `MONGO_USER`, `MONGO_DB`
92
+ - `LOG_LEVEL`, `PORT`
93
+ - Memory: 3 short‑term, 20 long‑term
94
 
95
  ## 📱 Usage
96
+ 1. Select/create a doctor; set role/specialty.
97
+ 2. Search patient by name/ID; select a result.
98
+ 3. Start a new chat; ask your question.
99
+ 4. Manage sessions in the sidebar (rename/delete from menu).
100
+ 5. View patient profile and create/edit via modals/pages.
101
+
102
+ ## 🔌 Endpoints (selected)
103
+ - `POST /chat` → `{ response, session_id, timestamp, medical_context? }`
104
+ - `POST /sessions` → `{ session_id }`
105
+ - `GET /patients/{patient_id}/sessions`
106
+ - `GET /sessions/{session_id}/messages`
107
+ - `DELETE /sessions/{session_id}` deletes session (cache + Mongo) and its messages
108
+ - `GET /patients/search?q=term&limit=8`
109
+
110
+ ## 🔒 Data & Privacy
111
+ - MongoDB persistence keyed by `patient_id` with `doctor_id` attribution
112
+ - UI localStorage for UX (doctor list, preferences, selected patient)
113
+ - Avoid logging PHI; secure Mongo credentials
114
+
115
+ ## 🧪 Dev
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  ```bash
 
117
  pip install -r requirements.txt
118
+ python -m src.main # run
119
+ pytest # tests
120
+ black . && flake8 # format + lint
 
 
 
 
 
 
 
 
 
121
  ```
122
 
123
  ### Project Structure
124
  ```
125
+ MedAI/
 
 
126
  ├── src/
127
  │ ├── api/
128
+ │ │ └── routes/
129
+ │ │ ├── chat.py # Chat endpoint
130
+ │ │ ├── session.py # Session endpoints
131
+ │ │ ├── user.py # Patient APIs (get/create/update/search)
132
+ │ │ ├── system.py # Health/info
133
+ │ │ └── static.py # Serve index
134
  │ ├── core/
135
+ │ │ ├── memory/
136
+ │ │ │ ├── memory.py # LRU short‑term memory + sessions
137
+ │ │ │ └── history.py # Context builder, persistence hooks
138
+ │ │ └── state.py # App state (rotators, embeddings, memory)
139
+ │ ├── data/
140
+ │ │ ├── __init__.py # Barrel exports for data layer
141
+ │ │ ├── connection.py # Mongo connection + collection names
142
+ ├── utils.py # Indexing, backups
143
+ │ │ ├── session/
144
+ │ │ │ └── operations.py # Sessions: ensure/list/delete
145
+ │ │ ├── message/
146
+ │ │ │ └── operations.py # Messages: save/list
147
+ │ │ ├── patient/
148
+ │ │ │ └── operations.py # Patients: get/create/update/search
149
+ │ │ ├── user/
150
+ │ │ │ └── operations.py # Accounts/Doctors
151
+ │ │ └── medical/
152
+ │ │ └── operations.py # Medical records + memory summaries
153
+ │ ├── models/
154
  │ │ ├── chat.py
155
  │ │ └── user.py
156
+ │ ├── services/
157
+ │ │ ├── medical_response.py # Calls model(s)
158
+ │ │ └── summariser.py # Title/QA summarisation
159
+ │ ├── utils/
 
160
  │ │ ├── embeddings.py
161
  │ │ ├── logger.py
 
162
  │ │ └── rotator.py
163
+ └── main.py # FastAPI entrypoint
164
+ ├── static/
165
+ │ ├── index.html
 
 
166
  │ ├── css/
167
+ │ │ ├── styles.css
168
+ │ └── patient.css
169
+ │ ├── js/
170
+ │ ├── app.js # Submodules under /ui/* and /chat/*
171
+ │ │ └── patient.js
172
+ │ └── patient.html
173
+ ├── requirements.txt
174
+ ├── requirements-dev.txt
175
  ├── Dockerfile
176
+ ├── docker-compose.yml
177
+ ├── LICENSE
178
+ └── README.md
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  ```
180
 
181
+ ## 🧾 License & Disclaimer
182
+ - MIT License (see [LICENSE](https://huggingface.co/spaces/MedAI-COS30018/MedicalDiagnosisSystem/blob/main/LICENSE))
183
+ - Educational information only; not a substitute for professional medical advice
184
+ - Team D1 - COS30018, Swinburne University of Technology
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TODO.md CHANGED
@@ -6,6 +6,7 @@
6
  - [ ] Accounts
7
  - [ ] Saving chats
8
  - [ ] MongoDB hosting
 
9
  ## Suggestions
10
 
11
  1. Dependency Injection: — Investigated
@@ -47,3 +48,22 @@
47
  10. State Management:
48
  - Global state in MedicalState could be improved
49
  - Session management could be more robust
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  - [ ] Accounts
7
  - [ ] Saving chats
8
  - [ ] MongoDB hosting
9
+
10
  ## Suggestions
11
 
12
  1. Dependency Injection: — Investigated
 
48
  10. State Management:
49
  - Global state in MedicalState could be improved
50
  - Session management could be more robust
51
+
52
+ ## PHASE 2:
53
+ - [ ] EMR enhancement
54
+ - [ ] Naive RAG integration
55
+ - [ ] Chat history consistency
56
+ - [ ] Summary history to EMR
57
+
58
+ ## PHASE 3:
59
+ - [ ] Multimodal integration
60
+ - [ ] Reasoning orchestrator
61
+ - [ ] Dynamic/Tree/Graph RAG
62
+ - [ ] Integrate specialist model
63
+
64
+ ## PHASE 4:
65
+ - [ ] Multimodal validation
66
+ - [ ] Orchestrator validation
67
+ - [ ] RAG validation
68
+ - [ ] UI Enhancement
69
+ - [ ] Future suggestions
UI_SETUP.md ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## UI setup and structure
2
+
3
+ This document explains the browser UI code structure, responsibilities of each module, how the app boots, what localStorage keys are used, and how the UI communicates with the Python backend.
4
+
5
+ ### 1) Directory layout (frontend)
6
+
7
+ ```
8
+ static/
9
+ index.html # Main UI page
10
+ css/
11
+ styles.css # Global theme + layout
12
+ patient.css # Patient registration page
13
+ js/
14
+ app.js # Legacy monolith boot (kept for compatibility)
15
+ core.js # (optional) Core app bootstrap if using modules
16
+ boot.js # (optional) Bootstrapping helpers for modules
17
+ ui/ # UI-layer modules (DOM + events)
18
+ sidebar.js # Sidebar open/close, overlay, sessions list
19
+ modals.js # User/doctor modal, settings modal, edit-title modal
20
+ patient.js # Patient section: typeahead, status, select
21
+ theme.js # Theme/font-size handling
22
+ voice.js # Web Speech API setup (optional)
23
+ utils.js # Small DOM utilities (qs, on, etc.)
24
+ chat/ # Chat logic modules
25
+ messages.js # Render and manage messages in chat pane
26
+ sessions.js # Client-side session CRUD (local + fetch from backend)
27
+ api.js # Fetch helpers to talk to backend endpoints
28
+ state.js # Ephemeral UI state (current user/patient/session)
29
+
30
+ patient.html # Patient registration page
31
+ ```
32
+
33
+ Notes:
34
+ - If `core.js` and `boot.js` are present and loaded as type="module", they should import from `js/ui/*` and `js/chat/*`. If you continue using `app.js`, it should delegate to these modules (or keep the current inline logic).
35
+
36
+ ### 2) Boot sequence
37
+
38
+ Minimal boot (non-module):
39
+ - `index.html` loads `app.js` (non-module). `app.js` wires events, restores local state, applies theme, renders sidebar sessions, and binds modals.
40
+
41
+ Module-based boot (recommended):
42
+ - `index.html` includes:
43
+ - `<script type="module" src="/static/js/core.js"></script>`
44
+ - `core.js` imports from `ui/*` and `chat/*`, builds the app, and calls `boot.init()`.
45
+ - `boot.js` provides `init()` that wires all UI modules in a deterministic order.
46
+
47
+ Expected init order:
48
+ 1. Load preferences (theme, font size) and apply to `document.documentElement`.
49
+ 2. Restore user (doctor) profile from localStorage.
50
+ 3. Restore selected patient id from localStorage and, if present, preload sessions from backend.
51
+ 4. Wire global UI events: sidebar toggle, outside-click overlay, send, clear, export.
52
+ 5. Wire modals: user/doctor modal, settings modal, edit-session-title modal.
53
+ 6. Wire patient section: typeahead search + selection + status.
54
+ 7. Render current session messages.
55
+
56
+ ### 3) State model (in-memory)
57
+
58
+ - `state.user`: current doctor `{ id, name, role, specialty, createdAt }`.
59
+ - `state.patientId`: 8-digit patient id string kept in localStorage under `medicalChatbotPatientId`.
60
+ - `state.currentSession`: `{ id, title, messages[], createdAt, lastActivity, source }`.
61
+ - `state.sessions`: local session cache (for non-backend sessions).
62
+
63
+ LocalStorage keys:
64
+ - `medicalChatbotUser`: current doctor object.
65
+ - `medicalChatbotDoctors`: array of `{ name }` (unique, used for the dropdown).
66
+ - `medicalChatbotPatientId`: selected patient id.
67
+ - `medicalChatbotPreferences`: `{ theme, fontSize, autoSave, notifications }`.
68
+
69
+ ### 4) Components and responsibilities
70
+
71
+ UI modules (ui/*):
72
+ - `sidebar.js`: opens/closes sidebar, manages `#sidebarOverlay`, renders session cards, handles outside-click close.
73
+ - `modals.js`: shows/hides user/doctor modal, settings modal, edit-title modal. Populates doctor dropdown with a first item "Create doctor user..." and injects current doctor name if missing.
74
+ - `patient.js`: typeahead over `/patients/search?q=...`, renders suggestions, sets `state.patientId` and persists to localStorage, triggers sessions preload.
75
+ - `theme.js`: reads/writes `medicalChatbotPreferences`, applies `data-theme` on `<html>`, sets root font-size.
76
+ - `voice.js`: optional Web Speech API wiring to fill `#chatInput`.
77
+
78
+ Chat modules (chat/*):
79
+ - `messages.js`: adds user/assistant messages, formats content, timestamps, scrolls to bottom.
80
+ - `sessions.js`: saves/loads local sessions, hydrates backend sessions (`GET /sessions/{id}/messages`), deletes/renames sessions.
81
+ - `api.js`: wrappers around backend endpoints (`/chat`, `/patients/*`, `/sessions/*`). Adds `Accept: application/json` and logs responses.
82
+ - `state.js`: exports a singleton state object used by UI and chat modules.
83
+
84
+ ### 5) Backend endpoints used by the UI
85
+
86
+ - `POST /chat` — main inference call.
87
+ - `GET /patients/search?q=...&limit=...` — typeahead. Returns `{ results: [{ name, patient_id, ...}, ...] }`.
88
+ - `GET /patients/{patient_id}` — patient profile (used by patient modal).
89
+ - `POST /patients` — create patient (used by patient.html). Returns `{ patient_id, name, ... }`.
90
+ - `PATCH /patients/{patient_id}` — update patient fields.
91
+ - `GET /patients/{patient_id}/sessions` — list sessions.
92
+ - `GET /sessions/{session_id}/messages?limit=...` — hydrate messages.
93
+
94
+ ### 6) Theming
95
+
96
+ - CSS variables are declared under `:root` and overridden under `[data-theme="dark"]`.
97
+ - On boot, the app reads `medicalChatbotPreferences.theme` and applies:
98
+ - `auto` => matches `prefers-color-scheme`.
99
+ - `light`/`dark` => sets `document.documentElement.dataset.theme` accordingly.
100
+
101
+ ### 7) Patient typeahead contract
102
+
103
+ - Input: `#patientIdInput`.
104
+ - Suggestions container: `#patientSuggestions` (absolute, below input).
105
+ - Debounce: ~200 ms. Request: `GET /patients/search?q=<term>&limit=8`.
106
+ - On selection: set `state.patientId`, persist to localStorage, update `#patientStatus`, call sessions preload, close suggestions.
107
+ - On Enter:
108
+ - If exact 8 digits, call `loadPatient()`.
109
+ - Otherwise, search and pick the first match if any.
110
+
111
+ ### 8) Sidebar behavior
112
+
113
+ - Open: click `#sidebarToggle`.
114
+ - Close: clicking outside (main area) or on `#sidebarOverlay` hides the sidebar on all viewports.
115
+
116
+ ### 9) Doctor dropdown rules
117
+
118
+ - First option is always "Create doctor user..." (value: `__create__`).
119
+ - If the current doctor name is not in `medicalChatbotDoctors`, it is inserted and saved.
120
+ - Choosing the create option reveals an inline mini-form to add a new doctor; Confirm inserts and selects it.
121
+
122
+ ### 10) Voice input (optional)
123
+
124
+ - If using `voice.js`: checks `window.SpeechRecognition || window.webkitSpeechRecognition`.
125
+ - Streams interim results into `#chatInput`, toggled by `#microphoneBtn`.
126
+
127
+ ### 11) Patient registration flow
128
+
129
+ - `patient.html` posts to `POST /patients` with name/age/sex/etc.
130
+ - On success, shows a modal with the new Patient ID and two actions: Return to main page, Edit patient profile.
131
+ - Stores `medicalChatbotPatientId` and redirects when appropriate.
132
+
133
+ ### 12) Troubleshooting
134
+
135
+ - Sidebar won’t close: ensure `#sidebarOverlay` exists and that `sidebar.js`/`app.js` wires outside-click and overlay click listeners.
136
+ - Doctor dropdown empty: confirm `medicalChatbotDoctors` exists or `populateDoctorSelect()` runs on opening the modal.
137
+ - Typeahead doesn’t show results: open network tab and hit `/patients/search?q=test`; ensure 200 and JSON. Logs are printed by FastAPI (see server console).
138
+ - Theme not changing: ensure `theme.js` sets `data-theme` on `<html>` and `styles.css` uses `[data-theme="dark"]` overrides.
139
+
140
+ ### 13) Migration from app.js to modules
141
+
142
+ If you refactored into `ui/*` and `chat/*`:
143
+ 1. Ensure `index.html` loads `core.js` as a module.
144
+ 2. In `core.js`, import and initialize modules in the order described in Boot sequence.
145
+ 3. Keep `app.js` only if you need compatibility; progressively move code into the relevant module files.
146
+
147
+
src/api/routes/chat.py CHANGED
@@ -11,6 +11,8 @@ from src.services.medical_response import generate_medical_response
11
  from src.services.summariser import summarise_title_with_nvidia
12
  from src.utils.logger import logger
13
 
 
 
14
  router = APIRouter()
15
 
16
  @router.post("/chat", response_model=ChatResponse)
@@ -18,14 +20,16 @@ async def chat_endpoint(
18
  request: ChatRequest,
19
  state: MedicalState = Depends(get_state)
20
  ):
21
- """Handle chat messages and generate medical responses"""
 
 
22
  start_time = time.time()
23
 
24
  try:
25
- logger().info(f"Chat request from user {request.user_id} in session {request.session_id}")
26
  logger().info(f"Message: {request.message[:100]}...") # Log first 100 chars of message
27
 
28
- # Get or create user profile
29
  user_profile = state.memory_system.get_user(request.user_id)
30
  if not user_profile:
31
  state.memory_system.create_user(request.user_id, request.user_role or "Anonymous")
@@ -35,7 +39,7 @@ async def chat_endpoint(
35
  {"specialty": request.user_specialty}
36
  )
37
 
38
- # Get or create session
39
  session = state.memory_system.get_session(request.session_id)
40
  if not session:
41
  session_id = state.memory_system.create_session(request.user_id, request.title or "New Chat")
@@ -43,11 +47,16 @@ async def chat_endpoint(
43
  session = state.memory_system.get_session(session_id)
44
  logger().info(f"Created new session: {session_id}")
45
 
46
- # Get medical context from memory
47
- medical_context = state.history_manager.get_conversation_context(
 
 
 
48
  request.user_id,
49
- #request.session_id,
50
- #request.message
 
 
51
  )
52
 
53
  # Generate response using Gemini AI
@@ -68,7 +77,10 @@ async def chat_endpoint(
68
  request.message,
69
  response,
70
  state.gemini_rotator,
71
- state.nvidia_rotator
 
 
 
72
  )
73
  except Exception as e:
74
  logger().warning(f"Failed to process medical exchange: {e}")
@@ -83,7 +95,7 @@ async def chat_endpoint(
83
  return ChatResponse(
84
  response=response,
85
  session_id=request.session_id,
86
- timestamp=datetime.now(timezone.utc).isoformat(), # Use ISO format for consistency
87
  medical_context=medical_context if medical_context else None
88
  )
89
 
 
11
  from src.services.summariser import summarise_title_with_nvidia
12
  from src.utils.logger import logger
13
 
14
+ from src.data import ensure_session
15
+
16
  router = APIRouter()
17
 
18
  @router.post("/chat", response_model=ChatResponse)
 
20
  request: ChatRequest,
21
  state: MedicalState = Depends(get_state)
22
  ):
23
+ """
24
+ Process a chat message, generate response, and persist short-term cache + long-term Mongo.
25
+ """
26
  start_time = time.time()
27
 
28
  try:
29
+ logger().info(f"POST /chat user={request.user_id} session={request.session_id} patient={request.patient_id} doctor={request.doctor_id}")
30
  logger().info(f"Message: {request.message[:100]}...") # Log first 100 chars of message
31
 
32
+ # Get or create user profile (doctor as current user profile)
33
  user_profile = state.memory_system.get_user(request.user_id)
34
  if not user_profile:
35
  state.memory_system.create_user(request.user_id, request.user_role or "Anonymous")
 
39
  {"specialty": request.user_specialty}
40
  )
41
 
42
+ # Get or create session (cache)
43
  session = state.memory_system.get_session(request.session_id)
44
  if not session:
45
  session_id = state.memory_system.create_session(request.user_id, request.title or "New Chat")
 
47
  session = state.memory_system.get_session(session_id)
48
  logger().info(f"Created new session: {session_id}")
49
 
50
+ # Ensure session exists in Mongo with patient/doctor context
51
+ ensure_session(session_id=request.session_id, patient_id=request.patient_id, doctor_id=request.doctor_id, title=request.title or "New Chat", last_activity=datetime.now(timezone.utc))
52
+
53
+ # Get enhanced medical context with STM + LTM semantic search + NVIDIA reasoning
54
+ medical_context = await state.history_manager.get_enhanced_conversation_context(
55
  request.user_id,
56
+ request.session_id,
57
+ request.message,
58
+ state.nvidia_rotator,
59
+ patient_id=request.patient_id
60
  )
61
 
62
  # Generate response using Gemini AI
 
77
  request.message,
78
  response,
79
  state.gemini_rotator,
80
+ state.nvidia_rotator,
81
+ patient_id=request.patient_id,
82
+ doctor_id=request.doctor_id,
83
+ session_title=request.title or "New Chat"
84
  )
85
  except Exception as e:
86
  logger().warning(f"Failed to process medical exchange: {e}")
 
95
  return ChatResponse(
96
  response=response,
97
  session_id=request.session_id,
98
+ timestamp=datetime.now(timezone.utc).isoformat(),
99
  medical_context=medical_context if medical_context else None
100
  )
101
 
src/api/routes/session.py CHANGED
@@ -3,10 +3,12 @@
3
  from datetime import datetime
4
 
5
  from fastapi import APIRouter, Depends, HTTPException
 
6
 
7
  from src.core.state import MedicalState, get_state
8
  from src.models.chat import SessionRequest
9
  from src.utils.logger import logger
 
10
 
11
  router = APIRouter()
12
 
@@ -15,9 +17,12 @@ async def create_chat_session(
15
  request: SessionRequest,
16
  state: MedicalState = Depends(get_state)
17
  ):
18
- """Create a new chat session"""
19
  try:
 
20
  session_id = state.memory_system.create_session(request.user_id, request.title or "New Chat")
 
 
21
  return {"session_id": session_id, "message": "Session created successfully"}
22
  except Exception as e:
23
  logger().error(f"Error creating session: {e}")
@@ -28,7 +33,7 @@ async def get_chat_session(
28
  session_id: str,
29
  state: MedicalState = Depends(get_state)
30
  ):
31
- """Get chat session details and messages"""
32
  try:
33
  session = state.memory_system.get_session(session_id)
34
  if not session:
@@ -52,15 +57,55 @@ async def get_chat_session(
52
  logger().error(f"Error getting session: {e}")
53
  raise HTTPException(status_code=500, detail=str(e))
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  @router.delete("/sessions/{session_id}")
56
  async def delete_chat_session(
57
  session_id: str,
58
  state: MedicalState = Depends(get_state)
59
  ):
60
- """Delete a chat session"""
61
  try:
 
 
 
62
  state.memory_system.delete_session(session_id)
63
- return {"message": "Session deleted successfully"}
 
 
 
 
 
 
 
 
 
 
 
64
  except Exception as e:
65
  logger().error(f"Error deleting session: {e}")
66
  raise HTTPException(status_code=500, detail=str(e))
 
3
  from datetime import datetime
4
 
5
  from fastapi import APIRouter, Depends, HTTPException
6
+ from datetime import datetime
7
 
8
  from src.core.state import MedicalState, get_state
9
  from src.models.chat import SessionRequest
10
  from src.utils.logger import logger
11
+ from src.data import list_patient_sessions, list_session_messages, ensure_session, delete_session, delete_session_messages
12
 
13
  router = APIRouter()
14
 
 
17
  request: SessionRequest,
18
  state: MedicalState = Depends(get_state)
19
  ):
20
+ """Create a new chat session (cache + Mongo)"""
21
  try:
22
+ logger().info(f"POST /sessions user_id={request.user_id} patient_id={request.patient_id} doctor_id={request.doctor_id}")
23
  session_id = state.memory_system.create_session(request.user_id, request.title or "New Chat")
24
+ # Also ensure in Mongo with patient/doctor
25
+ ensure_session(session_id=session_id, patient_id=request.patient_id, doctor_id=request.doctor_id, title=request.title or "New Chat")
26
  return {"session_id": session_id, "message": "Session created successfully"}
27
  except Exception as e:
28
  logger().error(f"Error creating session: {e}")
 
33
  session_id: str,
34
  state: MedicalState = Depends(get_state)
35
  ):
36
+ """Get session from cache (for quick preview)"""
37
  try:
38
  session = state.memory_system.get_session(session_id)
39
  if not session:
 
57
  logger().error(f"Error getting session: {e}")
58
  raise HTTPException(status_code=500, detail=str(e))
59
 
60
+ @router.get("/patients/{patient_id}/sessions")
61
+ async def list_sessions_for_patient(patient_id: str):
62
+ """List sessions for a patient from Mongo"""
63
+ try:
64
+ logger().info(f"GET /patients/{patient_id}/sessions")
65
+ return {"sessions": list_patient_sessions(patient_id)}
66
+ except Exception as e:
67
+ logger().error(f"Error listing sessions: {e}")
68
+ raise HTTPException(status_code=500, detail=str(e))
69
+
70
+ @router.get("/sessions/{session_id}/messages")
71
+ async def list_messages_for_session(session_id: str, patient_id: str, limit: int | None = None):
72
+ """List messages for a session from Mongo, verified to belong to the patient"""
73
+ try:
74
+ logger().info(f"GET /sessions/{session_id}/messages patient_id={patient_id} limit={limit}")
75
+ msgs = list_session_messages(session_id, patient_id=patient_id, limit=limit)
76
+ # ensure JSON-friendly timestamps
77
+ for m in msgs:
78
+ if isinstance(m.get("timestamp"), datetime):
79
+ m["timestamp"] = m["timestamp"].isoformat()
80
+ m["_id"] = str(m["_id"]) if "_id" in m else None
81
+ return {"messages": msgs}
82
+ except Exception as e:
83
+ logger().error(f"Error listing messages: {e}")
84
+ raise HTTPException(status_code=500, detail=str(e))
85
+
86
  @router.delete("/sessions/{session_id}")
87
  async def delete_chat_session(
88
  session_id: str,
89
  state: MedicalState = Depends(get_state)
90
  ):
91
+ """Delete a chat session from both memory system and MongoDB"""
92
  try:
93
+ logger().info(f"DELETE /sessions/{session_id}")
94
+
95
+ # Delete from memory system
96
  state.memory_system.delete_session(session_id)
97
+
98
+ # Delete from MongoDB
99
+ session_deleted = delete_session(session_id)
100
+ messages_deleted = delete_session_messages(session_id)
101
+
102
+ logger().info(f"Deleted session {session_id}: session={session_deleted}, messages={messages_deleted}")
103
+
104
+ return {
105
+ "message": "Session deleted successfully",
106
+ "session_deleted": session_deleted,
107
+ "messages_deleted": messages_deleted
108
+ }
109
  except Exception as e:
110
  logger().error(f"Error deleting session: {e}")
111
  raise HTTPException(status_code=500, detail=str(e))
src/api/routes/user.py CHANGED
@@ -3,8 +3,9 @@
3
  from fastapi import APIRouter, Depends, HTTPException
4
 
5
  from src.core.state import MedicalState, get_state
6
- from src.models.user import UserProfileRequest
7
  from src.utils.logger import logger
 
 
8
 
9
  router = APIRouter()
10
 
@@ -15,12 +16,25 @@ async def create_user_profile(
15
  ):
16
  """Create or update user profile"""
17
  try:
 
18
  user = state.memory_system.create_user(request.user_id, request.name)
19
  user.set_preference("role", request.role)
20
  if request.specialty:
21
  user.set_preference("specialty", request.specialty)
 
 
22
 
23
- return {"message": "User profile created successfully", "user_id": request.user_id}
 
 
 
 
 
 
 
 
 
 
24
  except Exception as e:
25
  logger().error(f"Error creating user profile: {e}")
26
  raise HTTPException(status_code=500, detail=str(e))
@@ -44,6 +58,7 @@ async def get_user_profile(
44
  "name": user.name,
45
  "role": user.preferences.get("role", "Unknown"),
46
  "specialty": user.preferences.get("specialty", ""),
 
47
  "created_at": user.created_at,
48
  "last_seen": user.last_seen
49
  },
@@ -63,3 +78,120 @@ async def get_user_profile(
63
  except Exception as e:
64
  logger().error(f"Error getting user profile: {e}")
65
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from fastapi import APIRouter, Depends, HTTPException
4
 
5
  from src.core.state import MedicalState, get_state
 
6
  from src.utils.logger import logger
7
+ from src.models.user import UserProfileRequest, PatientCreateRequest, PatientUpdateRequest, DoctorCreateRequest
8
+ from src.data import create_account, create_doctor, get_doctor_by_name, search_doctors, get_all_doctors
9
 
10
  router = APIRouter()
11
 
 
16
  ):
17
  """Create or update user profile"""
18
  try:
19
+ # Persist to in-memory profile (existing behavior)
20
  user = state.memory_system.create_user(request.user_id, request.name)
21
  user.set_preference("role", request.role)
22
  if request.specialty:
23
  user.set_preference("specialty", request.specialty)
24
+ if request.medical_roles:
25
+ user.set_preference("medical_roles", request.medical_roles)
26
 
27
+ # Persist to MongoDB accounts collection
28
+ account_doc = {
29
+ "user_id": request.user_id,
30
+ "name": request.name,
31
+ "role": request.role,
32
+ "medical_roles": request.medical_roles or [request.role] if request.role else [],
33
+ "specialty": request.specialty or None,
34
+ }
35
+ account_id = create_account(account_doc)
36
+
37
+ return {"message": "User profile created successfully", "user_id": request.user_id, "account_id": account_id}
38
  except Exception as e:
39
  logger().error(f"Error creating user profile: {e}")
40
  raise HTTPException(status_code=500, detail=str(e))
 
58
  "name": user.name,
59
  "role": user.preferences.get("role", "Unknown"),
60
  "specialty": user.preferences.get("specialty", ""),
61
+ "medical_roles": user.preferences.get("medical_roles", []),
62
  "created_at": user.created_at,
63
  "last_seen": user.last_seen
64
  },
 
78
  except Exception as e:
79
  logger().error(f"Error getting user profile: {e}")
80
  raise HTTPException(status_code=500, detail=str(e))
81
+
82
+ # -------------------- Patient APIs --------------------
83
+ from src.data import get_patient_by_id, create_patient, update_patient_profile, search_patients
84
+
85
+ @router.get("/patients/search")
86
+ async def search_patients_route(q: str, limit: int = 20):
87
+ try:
88
+ logger().info(f"GET /patients/search q='{q}' limit={limit}")
89
+ results = search_patients(q, limit=limit)
90
+ logger().info(f"Search returned {len(results)} results")
91
+ return {"results": results}
92
+ except Exception as e:
93
+ logger().error(f"Error searching patients: {e}")
94
+ raise HTTPException(status_code=500, detail=str(e))
95
+
96
+ @router.get("/patients/{patient_id}")
97
+ async def get_patient(patient_id: str):
98
+ try:
99
+ logger().info(f"GET /patients/{patient_id}")
100
+ patient = get_patient_by_id(patient_id)
101
+ if not patient:
102
+ raise HTTPException(status_code=404, detail="Patient not found")
103
+ patient["_id"] = str(patient.get("_id")) if patient.get("_id") else None
104
+ return patient
105
+ except HTTPException:
106
+ raise
107
+ except Exception as e:
108
+ logger().error(f"Error getting patient: {e}")
109
+ raise HTTPException(status_code=500, detail=str(e))
110
+
111
+ @router.post("/patients")
112
+ async def create_patient_profile(req: PatientCreateRequest):
113
+ try:
114
+ logger().info(f"POST /patients name={req.name}")
115
+ patient = create_patient(
116
+ name=req.name,
117
+ age=req.age,
118
+ sex=req.sex,
119
+ address=req.address,
120
+ phone=req.phone,
121
+ email=req.email,
122
+ medications=req.medications,
123
+ past_assessment_summary=req.past_assessment_summary,
124
+ assigned_doctor_id=req.assigned_doctor_id
125
+ )
126
+ patient["_id"] = str(patient.get("_id")) if patient.get("_id") else None
127
+ logger().info(f"Created patient {patient.get('name')} id={patient.get('patient_id')}")
128
+ return patient
129
+ except Exception as e:
130
+ logger().error(f"Error creating patient: {e}")
131
+ raise HTTPException(status_code=500, detail=str(e))
132
+
133
+ @router.patch("/patients/{patient_id}")
134
+ async def update_patient(patient_id: str, req: PatientUpdateRequest):
135
+ try:
136
+ payload = {k: v for k, v in req.model_dump().items() if v is not None}
137
+ logger().info(f"PATCH /patients/{patient_id} fields={list(payload.keys())}")
138
+ modified = update_patient_profile(patient_id, payload)
139
+ if modified == 0:
140
+ return {"message": "No changes"}
141
+ return {"message": "Updated"}
142
+ except Exception as e:
143
+ logger().error(f"Error updating patient: {e}")
144
+ raise HTTPException(status_code=500, detail=str(e))
145
+
146
+ # -------------------- Doctor APIs --------------------
147
+ @router.post("/doctors")
148
+ async def create_doctor_profile(req: DoctorCreateRequest):
149
+ try:
150
+ logger().info(f"POST /doctors name={req.name}")
151
+ doctor_id = create_doctor(
152
+ name=req.name,
153
+ role=req.role,
154
+ specialty=req.specialty,
155
+ medical_roles=req.medical_roles
156
+ )
157
+ logger().info(f"Created doctor {req.name} id={doctor_id}")
158
+ return {"doctor_id": doctor_id, "name": req.name}
159
+ except Exception as e:
160
+ logger().error(f"Error creating doctor: {e}")
161
+ raise HTTPException(status_code=500, detail=str(e))
162
+
163
+ @router.get("/doctors/{doctor_name}")
164
+ async def get_doctor(doctor_name: str):
165
+ try:
166
+ logger().info(f"GET /doctors/{doctor_name}")
167
+ doctor = get_doctor_by_name(doctor_name)
168
+ if not doctor:
169
+ raise HTTPException(status_code=404, detail="Doctor not found")
170
+ return doctor
171
+ except HTTPException:
172
+ raise
173
+ except Exception as e:
174
+ logger().error(f"Error getting doctor: {e}")
175
+ raise HTTPException(status_code=500, detail=str(e))
176
+
177
+ @router.get("/doctors/search")
178
+ async def search_doctors_route(q: str, limit: int = 10):
179
+ try:
180
+ logger().info(f"GET /doctors/search q='{q}' limit={limit}")
181
+ results = search_doctors(q, limit=limit)
182
+ logger().info(f"Doctor search returned {len(results)} results")
183
+ return {"results": results}
184
+ except Exception as e:
185
+ logger().error(f"Error searching doctors: {e}")
186
+ raise HTTPException(status_code=500, detail=str(e))
187
+
188
+ @router.get("/doctors")
189
+ async def get_all_doctors_route(limit: int = 50):
190
+ try:
191
+ logger().info(f"GET /doctors limit={limit}")
192
+ results = get_all_doctors(limit=limit)
193
+ logger().info(f"Retrieved {len(results)} doctors")
194
+ return {"results": results}
195
+ except Exception as e:
196
+ logger().error(f"Error getting all doctors: {e}")
197
+ raise HTTPException(status_code=500, detail=str(e))
src/core/memory/history.py ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core/memory/history.py
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ import numpy as np
7
+ from datetime import datetime, timezone
8
+
9
+ from src.services.nvidia import nvidia_chat
10
+ from src.services.summariser import (summarise_qa_with_gemini,
11
+ summarise_qa_with_nvidia)
12
+ from src.utils.embeddings import EmbeddingClient
13
+ from src.utils.logger import get_logger
14
+ from src.data import save_memory_summary, save_chat_message, ensure_session, get_recent_memory_summaries, search_memory_summaries_semantic
15
+
16
+ logger = get_logger("MED_HISTORY")
17
+
18
+ def _safe_json(s: str) -> Any:
19
+ try:
20
+ return json.loads(s)
21
+ except Exception:
22
+ # Try to extract a JSON object from text
23
+ start = s.find("{")
24
+ end = s.rfind("}")
25
+ if start != -1 and end != -1 and end > start:
26
+ try:
27
+ return json.loads(s[start:end+1])
28
+ except Exception:
29
+ return {}
30
+ return {}
31
+
32
+ async def files_relevance(question: str, file_summaries: list[dict[str, str]], rotator) -> dict[str, bool]:
33
+ """
34
+ Ask NVIDIA model to mark each file as relevant (true) or not (false) for the question.
35
+ Returns {filename: bool}
36
+ """
37
+ sys = "You classify file relevance. Return STRICT JSON only with shape {\"relevance\":[{\"filename\":\"...\",\"relevant\":true|false}]}."
38
+ items = [{"filename": f["filename"], "summary": f.get("summary","")} for f in file_summaries]
39
+ user = f"Question: {question}\n\nFiles:\n{json.dumps(items, ensure_ascii=False)}\n\nReturn JSON only."
40
+ out = await nvidia_chat(sys, user, rotator)
41
+ data = _safe_json(out) or {}
42
+ rels = {}
43
+ for row in data.get("relevance", []):
44
+ fn = row.get("filename")
45
+ rv = row.get("relevant")
46
+ if isinstance(fn, str) and isinstance(rv, bool):
47
+ rels[fn] = rv
48
+ # If parsing failed, default to considering all files possibly relevant
49
+ if not rels and file_summaries:
50
+ rels = {f["filename"]: True for f in file_summaries}
51
+ return rels
52
+
53
+ def _cosine(a: np.ndarray, b: np.ndarray) -> float:
54
+ denom = (np.linalg.norm(a) * np.linalg.norm(b)) or 1.0
55
+ return float(np.dot(a, b) / denom)
56
+
57
+ def _as_text(block: str) -> str:
58
+ return block.strip()
59
+
60
+ async def related_recent_and_semantic_context(user_id: str, question: str, memory, embedder: EmbeddingClient, topk_sem: int = 3) -> tuple[str, str]:
61
+ """
62
+ Returns (recent_related_text, semantic_related_text).
63
+ - recent_related_text: NVIDIA checks the last 3 summaries for direct relatedness.
64
+ - semantic_related_text: cosine-sim search over the remaining 17 summaries (top-k).
65
+ """
66
+ recent3 = memory.recent(user_id, 3)
67
+ rest17 = memory.rest(user_id, 3)
68
+
69
+ recent_text = ""
70
+ if recent3:
71
+ sys = "Pick only items that directly relate to the new question. Output the selected items verbatim, no commentary. If none, output nothing."
72
+ numbered = [{"id": i+1, "text": s} for i, s in enumerate(recent3)]
73
+ user = f"Question: {question}\nCandidates:\n{json.dumps(numbered, ensure_ascii=False)}\nSelect any related items and output ONLY their 'text' lines concatenated."
74
+ key = None # We'll let robust_post_json handle rotation via rotator param
75
+ # Semantic over rest17
76
+ sem_text = ""
77
+ if rest17:
78
+ qv = np.array(embedder.embed([question])[0], dtype="float32")
79
+ mats = embedder.embed([_as_text(s) for s in rest17])
80
+ sims = [(_cosine(qv, np.array(v, dtype="float32")), s) for v, s in zip(mats, rest17)]
81
+ sims.sort(key=lambda x: x[0], reverse=True)
82
+ top = [s for (sc, s) in sims[:topk_sem] if sc > 0.15] # small threshold
83
+ if top:
84
+ sem_text = "\n\n".join(top)
85
+ # Return recent empty (to be filled by caller using NVIDIA), and semantic text
86
+ return ("", sem_text)
87
+
88
+ class MedicalHistoryManager:
89
+ """
90
+ Enhanced medical history manager that works with the new memory system
91
+ """
92
+ def __init__(self, memory, embedder: EmbeddingClient | None = None):
93
+ self.memory = memory
94
+ self.embedder = embedder
95
+
96
+ async def process_medical_exchange(self, user_id: str, session_id: str, question: str, answer: str, gemini_rotator, nvidia_rotator=None, *, patient_id: str | None = None, doctor_id: str | None = None, session_title: str | None = None) -> str:
97
+ """
98
+ Process a medical Q&A exchange and store it in memory and MongoDB
99
+ """
100
+ try:
101
+ # Check if we have valid API keys
102
+ if not gemini_rotator or not gemini_rotator.get_key() or gemini_rotator.get_key() == "":
103
+ logger.info("No valid Gemini API keys available, using fallback summary")
104
+ summary = f"q: {question}\na: {answer}"
105
+ else:
106
+ # Try to create summary using Gemini (preferred) or NVIDIA as fallback
107
+ try:
108
+ # First try Gemini
109
+ summary = await summarise_qa_with_gemini(question, answer, gemini_rotator)
110
+ if not summary or summary.strip() == "":
111
+ # Fallback to NVIDIA if Gemini fails
112
+ if nvidia_rotator and nvidia_rotator.get_key():
113
+ summary = await summarise_qa_with_nvidia(question, answer, nvidia_rotator)
114
+ if not summary or summary.strip() == "":
115
+ summary = f"q: {question}\na: {answer}"
116
+ else:
117
+ summary = f"q: {question}\na: {answer}"
118
+ except Exception as e:
119
+ logger.warning(f"Failed to create AI summary: {e}")
120
+ summary = f"q: {question}\na: {answer}"
121
+
122
+ # Short-term cache under patient_id when available
123
+ cache_key = patient_id or user_id
124
+ self.memory.add(cache_key, summary)
125
+
126
+ # Add to session history in cache
127
+ self.memory.add_message_to_session(session_id, "user", question)
128
+ self.memory.add_message_to_session(session_id, "assistant", answer)
129
+
130
+ # Persist to MongoDB with patient/doctor context
131
+ if patient_id and doctor_id:
132
+ ensure_session(session_id=session_id, patient_id=patient_id, doctor_id=doctor_id, title=session_title or "New Chat", last_activity=datetime.now(timezone.utc))
133
+ save_chat_message(session_id=session_id, patient_id=patient_id, doctor_id=doctor_id, role="user", content=question)
134
+ save_chat_message(session_id=session_id, patient_id=patient_id, doctor_id=doctor_id, role="assistant", content=answer)
135
+
136
+ # Generate embedding for semantic search
137
+ embedding = None
138
+ if self.embedder:
139
+ try:
140
+ embedding = self.embedder.embed([summary])[0]
141
+ except Exception as e:
142
+ logger.warning(f"Failed to generate embedding for summary: {e}")
143
+
144
+ save_memory_summary(patient_id=patient_id, doctor_id=doctor_id, summary=summary, embedding=embedding)
145
+
146
+ # Update session title if it's the first message
147
+ session = self.memory.get_session(session_id)
148
+ if session and len(session.messages) == 2: # Just user + assistant
149
+ # Generate a title using NVIDIA API if available
150
+ try:
151
+ from src.services.summariser import summarise_title_with_nvidia
152
+ title = await summarise_title_with_nvidia(question, nvidia_rotator, max_words=5)
153
+ if not title or title.strip() == "":
154
+ title = question[:50] + ("..." if len(question) > 50 else "")
155
+ except Exception as e:
156
+ logger.warning(f"Failed to generate title with NVIDIA: {e}")
157
+ title = question[:50] + ("..." if len(question) > 50 else "")
158
+
159
+ self.memory.update_session_title(session_id, title)
160
+
161
+ # Also update the session in MongoDB
162
+ if patient_id and doctor_id:
163
+ ensure_session(session_id=session_id, patient_id=patient_id, doctor_id=doctor_id, title=title, last_activity=datetime.now(timezone.utc))
164
+
165
+ return summary
166
+
167
+ except Exception as e:
168
+ logger.error(f"Error processing medical exchange: {e}")
169
+ # Fallback: store without summary
170
+ summary = f"q: {question}\na: {answer}"
171
+ cache_key = patient_id or user_id
172
+ self.memory.add(cache_key, summary)
173
+ self.memory.add_message_to_session(session_id, "user", question)
174
+ self.memory.add_message_to_session(session_id, "assistant", answer)
175
+ return summary
176
+
177
+ def get_conversation_context(self, user_id: str, session_id: str, question: str, *, patient_id: str | None = None) -> str:
178
+ """
179
+ Get relevant conversation context combining short-term cache (3) and long-term Mongo (20)
180
+ """
181
+ # Short-term summaries
182
+ cache_key = patient_id or user_id
183
+ recent_qa = self.memory.recent(cache_key, 3)
184
+
185
+ # Long-term summaries from Mongo (exclude ones already likely in cache by time order)
186
+ long_term = []
187
+ if patient_id:
188
+ try:
189
+ long_term = get_recent_memory_summaries(patient_id, limit=20)
190
+ except Exception as e:
191
+ logger.warning(f"Failed to fetch long-term memory: {e}")
192
+
193
+ # Get current session messages for context
194
+ session = self.memory.get_session(session_id)
195
+ session_context = ""
196
+ if session:
197
+ recent_messages = session.get_messages(10)
198
+ session_context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in recent_messages])
199
+
200
+ # Combine context
201
+ context_parts = []
202
+ combined = []
203
+ if long_term:
204
+ combined.extend(long_term[::-1]) # oldest to newest within limit
205
+ if recent_qa:
206
+ combined.extend(recent_qa[::-1])
207
+ if combined:
208
+ context_parts.append("Recent medical context:\n" + "\n".join(combined[-20:]))
209
+ if session_context:
210
+ context_parts.append("Current conversation:\n" + session_context)
211
+
212
+ return "\n\n".join(context_parts) if context_parts else ""
213
+
214
+ async def get_enhanced_conversation_context(self, user_id: str, session_id: str, question: str, nvidia_rotator, *, patient_id: str | None = None) -> str:
215
+ """
216
+ Enhanced context retrieval combining STM (3) + LTM semantic search (2) with NVIDIA reasoning.
217
+ Returns context that NVIDIA model can use to decide between STM and LTM information.
218
+ """
219
+ cache_key = patient_id or user_id
220
+
221
+ # Get STM summaries (recent 3)
222
+ recent_qa = self.memory.recent(cache_key, 3)
223
+
224
+ # Get LTM semantic matches (top 2 most similar)
225
+ ltm_semantic = []
226
+ if patient_id and self.embedder:
227
+ try:
228
+ query_embedding = self.embedder.embed([question])[0]
229
+ ltm_results = search_memory_summaries_semantic(
230
+ patient_id=patient_id,
231
+ query_embedding=query_embedding,
232
+ limit=2,
233
+ similarity_threshold=0.5 # >= 50% semantic similarity
234
+ )
235
+ ltm_semantic = [result["summary"] for result in ltm_results]
236
+ except Exception as e:
237
+ logger.warning(f"Failed to perform LTM semantic search: {e}")
238
+
239
+ # Get current session messages for context
240
+ session = self.memory.get_session(session_id)
241
+ session_context = ""
242
+ if session:
243
+ recent_messages = session.get_messages(10)
244
+ session_context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in recent_messages])
245
+
246
+ # Use NVIDIA to reason about STM relevance
247
+ relevant_stm = []
248
+ if recent_qa and nvidia_rotator:
249
+ try:
250
+ sys = "You are a medical AI assistant. Select only the most relevant recent medical context that directly relates to the new question. Return the selected items verbatim, no commentary. If none are relevant, return nothing."
251
+ numbered = [{"id": i+1, "text": s} for i, s in enumerate(recent_qa)]
252
+ user = f"Question: {question}\n\nRecent medical context (last 3 exchanges):\n{json.dumps(numbered, ensure_ascii=False)}\n\nSelect any relevant items and output ONLY their 'text' lines concatenated."
253
+ relevant_stm_text = await nvidia_chat(sys, user, nvidia_rotator)
254
+ if relevant_stm_text and relevant_stm_text.strip():
255
+ relevant_stm = [relevant_stm_text.strip()]
256
+ except Exception as e:
257
+ logger.warning(f"Failed to get NVIDIA STM reasoning: {e}")
258
+ # Fallback to all recent QA if NVIDIA fails
259
+ relevant_stm = recent_qa
260
+ else:
261
+ relevant_stm = recent_qa
262
+
263
+ # Combine all relevant context
264
+ context_parts = []
265
+
266
+ # Add STM context
267
+ if relevant_stm:
268
+ context_parts.append("Recent relevant medical context (STM):\n" + "\n".join(relevant_stm))
269
+
270
+ # Add LTM semantic context
271
+ if ltm_semantic:
272
+ context_parts.append("Semantically relevant medical history (LTM):\n" + "\n".join(ltm_semantic))
273
+
274
+ # Add current session context
275
+ if session_context:
276
+ context_parts.append("Current conversation:\n" + session_context)
277
+
278
+ return "\n\n".join(context_parts) if context_parts else ""
279
+
280
+ def get_user_medical_history(self, user_id: str, limit: int = 20) -> list[str]:
281
+ """
282
+ Get user's medical history (QA summaries)
283
+ """
284
+ return self.memory.all(user_id)[-limit:]
285
+
286
+ def search_medical_context(self, user_id: str, query: str, top_k: int = 5) -> list[str]:
287
+ """
288
+ Search through user's medical context for relevant information
289
+ """
290
+ if not self.embedder:
291
+ # Fallback to simple text search
292
+ all_context = self.memory.all(user_id)
293
+ query_lower = query.lower()
294
+ relevant = [ctx for ctx in all_context if query_lower in ctx.lower()]
295
+ return relevant[:top_k]
296
+
297
+ try:
298
+ # Semantic search using embeddings
299
+ query_embedding = np.array(self.embedder.embed([query])[0], dtype="float32")
300
+ all_context = self.memory.all(user_id)
301
+
302
+ if not all_context:
303
+ return []
304
+
305
+ context_embeddings = self.embedder.embed(all_context)
306
+ similarities = []
307
+
308
+ for i, ctx_emb in enumerate(context_embeddings):
309
+ sim = _cosine(query_embedding, np.array(ctx_emb, dtype="float32"))
310
+ similarities.append((sim, all_context[i]))
311
+
312
+ # Sort by similarity and return top-k
313
+ similarities.sort(key=lambda x: x[0], reverse=True)
314
+ return [ctx for sim, ctx in similarities[:top_k] if sim > 0.1]
315
+
316
+ except Exception as e:
317
+ logger.error(f"Error in semantic search: {e}")
318
+ # Fallback to simple search
319
+ all_context = self.memory.all(user_id)
320
+ query_lower = query.lower()
321
+ relevant = [ctx for ctx in all_context if query_lower in ctx.lower()]
322
+ return relevant[:top_k]
src/core/memory/memory.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core/memory/memory.py
2
+
3
+ import uuid
4
+ from collections import defaultdict, deque
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+ from src.utils.logger import get_logger
9
+
10
+ logger = get_logger("MEMORY")
11
+
12
+ class ChatSession:
13
+ """Represents a chat session with a user"""
14
+ def __init__(self, session_id: str, user_id: str, title: str = "New Chat"):
15
+ self.session_id = session_id
16
+ self.user_id = user_id
17
+ self.title = title
18
+ self.created_at = datetime.now(timezone.utc)
19
+ self.last_activity = datetime.now(timezone.utc)
20
+ self.messages: list[dict[str, Any]] = []
21
+
22
+ def add_message(self, role: str, content: str, metadata: dict | None = None):
23
+ """Add a message to the session"""
24
+ message = {
25
+ "id": str(uuid.uuid4()),
26
+ "role": role, # "user" or "assistant"
27
+ "content": content,
28
+ "timestamp": datetime.now(timezone.utc),
29
+ "metadata": metadata or {}
30
+ }
31
+ self.messages.append(message)
32
+ self.last_activity = datetime.now(timezone.utc)
33
+
34
+ def get_messages(self, limit: int | None = None) -> list[dict[str, Any]]:
35
+ """Get messages from the session, optionally limited"""
36
+ if limit is None:
37
+ return self.messages
38
+ return self.messages[-limit:]
39
+
40
+ def update_title(self, title: str):
41
+ """Update the session title"""
42
+ self.title = title
43
+ self.last_activity = datetime.now(timezone.utc)
44
+
45
+ class UserProfile:
46
+ """Represents a user profile with multiple chat sessions"""
47
+ def __init__(self, user_id: str, name: str = "Anonymous"):
48
+ self.user_id = user_id
49
+ self.name = name
50
+ self.created_at = datetime.now(timezone.utc)
51
+ self.last_seen = datetime.now(timezone.utc)
52
+ self.preferences: dict[str, Any] = {}
53
+
54
+ def update_activity(self):
55
+ """Update last seen timestamp"""
56
+ self.last_seen = datetime.now(timezone.utc)
57
+
58
+ def set_preference(self, key: str, value: Any):
59
+ """Set a user preference"""
60
+ self.preferences[key] = value
61
+
62
+ @property
63
+ def role(self) -> str:
64
+ """Get user role from preferences"""
65
+ return self.preferences.get("role", "Unknown")
66
+
67
+ class MemoryLRU:
68
+ """
69
+ Enhanced LRU-like memory system supporting:
70
+ - Multiple users with profiles
71
+ - Multiple chat sessions per user
72
+ - Chat history and continuity
73
+ - Medical context summaries
74
+ """
75
+ def __init__(self, capacity: int = 20, max_sessions_per_user: int = 10):
76
+ self.capacity = capacity
77
+ self.max_sessions_per_user = max_sessions_per_user
78
+
79
+ # User profiles and sessions
80
+ self._users: dict[str, UserProfile] = {}
81
+ self._sessions: dict[str, ChatSession] = {}
82
+ self._user_sessions: dict[str, list[str]] = defaultdict(list)
83
+
84
+ # Medical context summaries (QA pairs)
85
+ self._qa_store: dict[str, deque] = defaultdict(lambda: deque(maxlen=self.capacity))
86
+
87
+ def create_user(self, user_id: str, name: str = "Anonymous") -> UserProfile:
88
+ """Create a new user profile"""
89
+ if user_id not in self._users:
90
+ user = UserProfile(user_id, name)
91
+ self._users[user_id] = user
92
+ return self._users[user_id]
93
+
94
+ def get_user(self, user_id: str) -> UserProfile | None:
95
+ """Get user profile by ID"""
96
+ user = self._users.get(user_id)
97
+ if user:
98
+ user.update_activity()
99
+ return user
100
+
101
+ def create_session(self, user_id: str, title: str = "New Chat") -> str:
102
+ """Create a new chat session for a user"""
103
+ # Ensure user exists
104
+ if user_id not in self._users:
105
+ self.create_user(user_id)
106
+
107
+ # Create session
108
+ session_id = str(uuid.uuid4())
109
+ session = ChatSession(session_id, user_id, title)
110
+ self._sessions[session_id] = session
111
+
112
+ # Add to user's session list
113
+ user_sessions = self._user_sessions[user_id]
114
+ user_sessions.append(session_id)
115
+
116
+ # Enforce max sessions per user
117
+ if len(user_sessions) > self.max_sessions_per_user:
118
+ oldest_session = user_sessions.pop(0)
119
+ if oldest_session in self._sessions:
120
+ del self._sessions[oldest_session]
121
+
122
+ return session_id
123
+
124
+ def get_session(self, session_id: str) -> ChatSession | None:
125
+ """Get a chat session by ID"""
126
+ return self._sessions.get(session_id)
127
+
128
+ def get_user_sessions(self, user_id: str) -> list[ChatSession]:
129
+ """Get all sessions for a user"""
130
+ session_ids = self._user_sessions.get(user_id, [])
131
+ sessions = []
132
+ for sid in session_ids:
133
+ if sid in self._sessions:
134
+ sessions.append(self._sessions[sid])
135
+ # Sort by last activity (most recent first)
136
+ sessions.sort(key=lambda x: x.last_activity, reverse=True)
137
+ return sessions
138
+
139
+ def add_message_to_session(self, session_id: str, role: str, content: str, metadata: dict | None = None):
140
+ """Add a message to a specific session"""
141
+ session = self._sessions.get(session_id)
142
+ if session:
143
+ session.add_message(role, content, metadata)
144
+
145
+ def update_session_title(self, session_id: str, title: str):
146
+ """Update the title of a session"""
147
+ session = self._sessions.get(session_id)
148
+ if session:
149
+ session.update_title(title)
150
+
151
+ def delete_session(self, session_id: str):
152
+ """Delete a chat session"""
153
+ if session_id in self._sessions:
154
+ session = self._sessions[session_id]
155
+ user_id = session.user_id
156
+
157
+ # Remove from user's session list
158
+ if user_id in self._user_sessions:
159
+ self._user_sessions[user_id] = [s for s in self._user_sessions[user_id] if s != session_id]
160
+
161
+ # Delete session
162
+ del self._sessions[session_id]
163
+
164
+ def add(self, user_id: str, qa_summary: str):
165
+ """Add a QA summary to the medical context store"""
166
+ self._qa_store[user_id].append(qa_summary)
167
+
168
+ def recent(self, user_id: str, n: int = 3) -> list[str]:
169
+ """Get recent QA summaries for medical context"""
170
+ d = self._qa_store[user_id]
171
+ if not d:
172
+ return []
173
+ return list(d)[-n:][::-1]
174
+
175
+ def rest(self, user_id: str, skip_n: int = 3) -> list[str]:
176
+ """Get older QA summaries for medical context"""
177
+ d = self._qa_store[user_id]
178
+ if not d:
179
+ return []
180
+ return list(d)[:-skip_n] if len(d) > skip_n else []
181
+
182
+ def all(self, user_id: str) -> list[str]:
183
+ """Get all QA summaries for medical context"""
184
+ return list(self._qa_store[user_id])
185
+
186
+ def clear(self, user_id: str) -> None:
187
+ """Clear all cached summaries for the given user"""
188
+ if user_id in self._qa_store:
189
+ self._qa_store[user_id].clear()
190
+
191
+ def get_medical_context(self, user_id: str, session_id: str, question: str) -> str:
192
+ """Get relevant medical context for a question"""
193
+ # Get recent QA summaries
194
+ recent_qa = self.recent(user_id, 5)
195
+
196
+ # Get current session messages for context
197
+ session = self.get_session(session_id)
198
+ session_context = ""
199
+ if session:
200
+ recent_messages = session.get_messages(10)
201
+ session_context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in recent_messages])
202
+
203
+ # Combine context
204
+ context_parts = []
205
+ if recent_qa:
206
+ context_parts.append("Recent medical context:\n" + "\n".join(recent_qa))
207
+ if session_context:
208
+ context_parts.append("Current conversation:\n" + session_context)
209
+
210
+ return "\n\n".join(context_parts) if context_parts else ""
src/core/state.py CHANGED
@@ -30,6 +30,8 @@ class MedicalState:
30
  """Initializes all core application components."""
31
  self.memory_system = MemoryLRU(max_sessions_per_user=20)
32
  self.embedding_client = EmbeddingClient(model_name="all-MiniLM-L6-v2", dimension=384)
 
 
33
  self.history_manager = MedicalHistoryManager(self.memory_system, self.embedding_client)
34
  self.gemini_rotator = APIKeyRotator("GEMINI_API_", max_slots=5)
35
  self.nvidia_rotator = APIKeyRotator("NVIDIA_API_", max_slots=5)
 
30
  """Initializes all core application components."""
31
  self.memory_system = MemoryLRU(max_sessions_per_user=20)
32
  self.embedding_client = EmbeddingClient(model_name="all-MiniLM-L6-v2", dimension=384)
33
+ # Keep only 3 short-term summaries/messages in cache
34
+ #self.memory_system = MemoryLRU(capacity=3, max_sessions_per_user=20)
35
  self.history_manager = MedicalHistoryManager(self.memory_system, self.embedding_client)
36
  self.gemini_rotator = APIKeyRotator("GEMINI_API_", max_slots=5)
37
  self.nvidia_rotator = APIKeyRotator("NVIDIA_API_", max_slots=5)
src/data/__init__.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/__init__.py
2
+ """
3
+ Data layer for MongoDB operations.
4
+ Organized into specialized modules for different data types.
5
+ """
6
+
7
+ from .connection import get_database, get_collection, close_connection
8
+ from .session import *
9
+ from .user import *
10
+ from .message import *
11
+ from .patient import *
12
+ from .medical import *
13
+ from .utils import create_index, backup_collection
14
+
15
+ __all__ = [
16
+ # Connection
17
+ 'get_database',
18
+ 'get_collection',
19
+ 'close_connection',
20
+ # Session functions
21
+ 'create_chat_session',
22
+ 'get_user_sessions',
23
+ 'ensure_session',
24
+ 'list_patient_sessions',
25
+ 'delete_session',
26
+ 'delete_session_messages',
27
+ 'delete_old_sessions',
28
+ # User functions
29
+ 'create_account',
30
+ 'update_account',
31
+ 'get_account_frame',
32
+ 'create_doctor',
33
+ 'get_doctor_by_name',
34
+ 'search_doctors',
35
+ 'get_all_doctors',
36
+ # Message functions
37
+ 'add_message',
38
+ 'get_session_messages',
39
+ 'save_chat_message',
40
+ 'list_session_messages',
41
+ # Patient functions
42
+ 'get_patient_by_id',
43
+ 'create_patient',
44
+ 'update_patient_profile',
45
+ 'search_patients',
46
+ # Medical functions
47
+ 'create_medical_record',
48
+ 'get_user_medical_records',
49
+ 'save_memory_summary',
50
+ 'get_recent_memory_summaries',
51
+ 'search_memory_summaries_semantic',
52
+ # Utility functions
53
+ 'create_index',
54
+ 'backup_collection',
55
+ ]
src/data/connection.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/connection.py
2
+ """
3
+ MongoDB connection management and base database operations.
4
+ """
5
+
6
+ import os
7
+ from typing import Any
8
+
9
+ from pymongo import MongoClient
10
+ from pymongo.collection import Collection
11
+ from pymongo.database import Database
12
+
13
+ from src.utils.logger import get_logger
14
+
15
+ logger = get_logger("MONGO")
16
+
17
+ # Global client instance
18
+ _mongo_client: MongoClient | None = None
19
+
20
+ # Collection Names
21
+ ACCOUNTS_COLLECTION = "accounts"
22
+ CHAT_SESSIONS_COLLECTION = "chat_sessions"
23
+ CHAT_MESSAGES_COLLECTION = "chat_messages"
24
+ MEDICAL_RECORDS_COLLECTION = "medical_records"
25
+ MEDICAL_MEMORY_COLLECTION = "medical_memory"
26
+ PATIENTS_COLLECTION = "patients"
27
+
28
+
29
+ def get_database() -> Database:
30
+ """Get database instance with connection management"""
31
+ global _mongo_client
32
+ if _mongo_client is None:
33
+ CONNECTION_STRING = os.getenv("MONGO_USER", "mongodb://127.0.0.1:27017/") # fall back to local host if no user is provided
34
+ try:
35
+ logger.info("Initializing MongoDB connection")
36
+ _mongo_client = MongoClient(CONNECTION_STRING)
37
+ except Exception as e:
38
+ logger.error(f"Failed to connect to MongoDB: {str(e)}")
39
+ # Pass the error down, code that calls this function should handle it
40
+ raise e
41
+ db_name = os.getenv("USER_DB", "medicaldiagnosissystem")
42
+ return _mongo_client[db_name]
43
+
44
+
45
+ def close_connection():
46
+ """Close MongoDB connection"""
47
+ global _mongo_client
48
+ if _mongo_client is not None:
49
+ # Close the connection and reset the client
50
+ _mongo_client.close()
51
+ _mongo_client = None
52
+
53
+
54
+ def get_collection(name: str, /) -> Collection:
55
+ """Get a MongoDB collection by name"""
56
+ db = get_database()
57
+ return db.get_collection(name)
src/data/medical/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/medical/__init__.py
2
+ """
3
+ Medical records and memory management operations for MongoDB.
4
+ """
5
+
6
+ from .operations import (
7
+ create_medical_record,
8
+ get_user_medical_records,
9
+ save_memory_summary,
10
+ get_recent_memory_summaries,
11
+ search_memory_summaries_semantic,
12
+ )
13
+
14
+ __all__ = [
15
+ 'create_medical_record',
16
+ 'get_user_medical_records',
17
+ 'save_memory_summary',
18
+ 'get_recent_memory_summaries',
19
+ 'search_memory_summaries_semantic',
20
+ ]
src/data/medical/operations.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/medical/operations.py
2
+ """
3
+ Medical records and memory management operations for MongoDB.
4
+ """
5
+
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+
9
+ from pymongo import ASCENDING, DESCENDING
10
+
11
+ from ..connection import get_collection, MEDICAL_RECORDS_COLLECTION, MEDICAL_MEMORY_COLLECTION
12
+ from src.utils.logger import get_logger
13
+
14
+ logger = get_logger("MEDICAL_OPS")
15
+
16
+
17
+ def create_medical_record(
18
+ record_data: dict[str, Any],
19
+ /,
20
+ *,
21
+ collection_name: str = MEDICAL_RECORDS_COLLECTION
22
+ ) -> str:
23
+ """Create a new medical record"""
24
+ collection = get_collection(collection_name)
25
+ now = datetime.now(timezone.utc)
26
+ record_data["created_at"] = now
27
+ record_data["updated_at"] = now
28
+ result = collection.insert_one(record_data)
29
+ return str(result.inserted_id)
30
+
31
+
32
+ def get_user_medical_records(
33
+ user_id: str,
34
+ /,
35
+ *,
36
+ collection_name: str = MEDICAL_RECORDS_COLLECTION
37
+ ) -> list[dict[str, Any]]:
38
+ """Get medical records for a specific user"""
39
+ collection = get_collection(collection_name)
40
+ return list(collection.find({"user_id": user_id}).sort("created_at", ASCENDING))
41
+
42
+
43
+ def save_memory_summary(
44
+ *,
45
+ patient_id: str,
46
+ doctor_id: str,
47
+ summary: str,
48
+ embedding: list[float] | None = None,
49
+ created_at: datetime | None = None,
50
+ collection_name: str = MEDICAL_MEMORY_COLLECTION
51
+ ) -> str:
52
+ collection = get_collection(collection_name)
53
+ ts = created_at or datetime.now(timezone.utc)
54
+ doc = {
55
+ "patient_id": patient_id,
56
+ "doctor_id": doctor_id,
57
+ "summary": summary,
58
+ "created_at": ts
59
+ }
60
+ if embedding is not None:
61
+ doc["embedding"] = embedding
62
+ result = collection.insert_one(doc)
63
+ return str(result.inserted_id)
64
+
65
+
66
+ def get_recent_memory_summaries(
67
+ patient_id: str,
68
+ /,
69
+ *,
70
+ limit: int = 20,
71
+ collection_name: str = MEDICAL_MEMORY_COLLECTION
72
+ ) -> list[str]:
73
+ collection = get_collection(collection_name)
74
+ docs = list(collection.find({"patient_id": patient_id}).sort("created_at", DESCENDING).limit(limit))
75
+ return [d.get("summary", "") for d in docs]
76
+
77
+
78
+ def search_memory_summaries_semantic(
79
+ patient_id: str,
80
+ query_embedding: list[float],
81
+ /,
82
+ *,
83
+ limit: int = 5,
84
+ similarity_threshold: float = 0.5, # >= 50% semantic similarity
85
+ collection_name: str = MEDICAL_MEMORY_COLLECTION
86
+ ) -> list[dict[str, Any]]:
87
+ """
88
+ Search memory summaries using semantic similarity with embeddings.
89
+ Returns list of {summary, similarity_score, created_at} sorted by similarity.
90
+ """
91
+ collection = get_collection(collection_name)
92
+
93
+ # Get all summaries with embeddings for this patient
94
+ docs = list(collection.find({
95
+ "patient_id": patient_id,
96
+ "embedding": {"$exists": True}
97
+ }))
98
+
99
+ if not docs:
100
+ return []
101
+
102
+ # Calculate similarities
103
+ import numpy as np
104
+ query_vec = np.array(query_embedding, dtype="float32")
105
+ results = []
106
+
107
+ for doc in docs:
108
+ embedding = doc.get("embedding")
109
+ if not embedding:
110
+ continue
111
+
112
+ # Calculate cosine similarity
113
+ doc_vec = np.array(embedding, dtype="float32")
114
+ dot_product = np.dot(query_vec, doc_vec)
115
+ norm_query = np.linalg.norm(query_vec)
116
+ norm_doc = np.linalg.norm(doc_vec)
117
+
118
+ if norm_query == 0 or norm_doc == 0:
119
+ similarity = 0.0
120
+ else:
121
+ similarity = float(dot_product / (norm_query * norm_doc))
122
+
123
+ if similarity >= similarity_threshold:
124
+ results.append({
125
+ "summary": doc.get("summary", ""),
126
+ "similarity_score": similarity,
127
+ "created_at": doc.get("created_at"),
128
+ "session_id": doc.get("session_id") # if we add this field later
129
+ })
130
+
131
+ # Sort by similarity (highest first) and return top results
132
+ results.sort(key=lambda x: x["similarity_score"], reverse=True)
133
+ return results[:limit]
src/data/medical_kb.py CHANGED
@@ -1,6 +1,7 @@
1
  # data/medical_kb.py
2
- # Medical Knowledge Base for the Medical AI Assistant
3
 
 
 
4
  MEDICAL_KB = {
5
  "symptoms": {
6
  "fever": "Fever is a temporary increase in body temperature, often due to illness. Normal body temperature is around 98.6°F (37°C).",
 
1
  # data/medical_kb.py
 
2
 
3
+ # TODO: This should be replaced with a more robust knowledge base system that can be updated by the user.
4
+ # Medical Knowledge Base for the Medical AI Assistant
5
  MEDICAL_KB = {
6
  "symptoms": {
7
  "fever": "Fever is a temporary increase in body temperature, often due to illness. Normal body temperature is around 98.6°F (37°C).",
src/data/message/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/message/__init__.py
2
+ """
3
+ Message management operations for MongoDB.
4
+ """
5
+
6
+ from .operations import (
7
+ add_message,
8
+ get_session_messages,
9
+ save_chat_message,
10
+ list_session_messages,
11
+ )
12
+
13
+ __all__ = [
14
+ 'add_message',
15
+ 'get_session_messages',
16
+ 'save_chat_message',
17
+ 'list_session_messages',
18
+ ]
src/data/message/operations.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/message/operations.py
2
+ """
3
+ Message management operations for MongoDB.
4
+ """
5
+
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+
9
+ from bson import ObjectId
10
+ from pymongo import ASCENDING
11
+
12
+ from ..connection import get_collection, CHAT_SESSIONS_COLLECTION, CHAT_MESSAGES_COLLECTION
13
+ from src.utils.logger import get_logger
14
+
15
+ logger = get_logger("MESSAGE_OPS")
16
+
17
+
18
+ def add_message(
19
+ session_id: str,
20
+ message_data: dict[str, Any],
21
+ /,
22
+ *,
23
+ collection_name: str = CHAT_SESSIONS_COLLECTION
24
+ ) -> str | None:
25
+ """Add a message to a chat session"""
26
+ collection = get_collection(collection_name)
27
+
28
+ # Verify session exists first
29
+ session = collection.find_one({
30
+ "$or": [
31
+ {"_id": session_id},
32
+ {"_id": ObjectId(session_id) if ObjectId.is_valid(session_id) else None}
33
+ ]
34
+ })
35
+ if not session:
36
+ logger.error(f"Failed to add message - session not found: {session_id}")
37
+ raise ValueError(f"Chat session not found: {session_id}")
38
+
39
+ now = datetime.now(timezone.utc)
40
+ message_data["timestamp"] = now
41
+ result = collection.update_one(
42
+ {"_id": session["_id"]},
43
+ {
44
+ "$push": {"messages": message_data},
45
+ "$set": {"updated_at": now}
46
+ }
47
+ )
48
+ return str(session_id) if result.modified_count > 0 else None
49
+
50
+
51
+ def get_session_messages(
52
+ session_id: str,
53
+ /,
54
+ limit: int | None = None,
55
+ *,
56
+ collection_name: str = CHAT_SESSIONS_COLLECTION
57
+ ) -> list[dict[str, Any]]:
58
+ """Get messages from a specific chat session"""
59
+ collection = get_collection(collection_name)
60
+ pipeline = [
61
+ {"$match": {"_id": session_id}},
62
+ {"$unwind": "$messages"},
63
+ {"$sort": {"messages.timestamp": -1}}
64
+ ]
65
+ if limit:
66
+ pipeline.append({"$limit": limit})
67
+ return [doc["messages"] for doc in collection.aggregate(pipeline)]
68
+
69
+
70
+ def save_chat_message(
71
+ *,
72
+ session_id: str,
73
+ patient_id: str,
74
+ doctor_id: str,
75
+ role: str,
76
+ content: str,
77
+ timestamp: datetime | None = None,
78
+ collection_name: str = CHAT_MESSAGES_COLLECTION
79
+ ) -> ObjectId:
80
+ collection = get_collection(collection_name)
81
+ ts = timestamp or datetime.now(timezone.utc)
82
+ doc = {
83
+ "session_id": session_id,
84
+ "patient_id": patient_id,
85
+ "doctor_id": doctor_id,
86
+ "role": role,
87
+ "content": content,
88
+ "timestamp": ts,
89
+ "created_at": ts
90
+ }
91
+ result = collection.insert_one(doc)
92
+ return result.inserted_id
93
+
94
+
95
+ def list_session_messages(
96
+ session_id: str,
97
+ /,
98
+ *,
99
+ patient_id: str | None = None,
100
+ limit: int | None = None,
101
+ collection_name: str = CHAT_MESSAGES_COLLECTION
102
+ ) -> list[dict[str, Any]]:
103
+ collection = get_collection(collection_name)
104
+
105
+ # First verify the session belongs to the patient
106
+ if patient_id:
107
+ session_collection = get_collection(CHAT_SESSIONS_COLLECTION)
108
+ session = session_collection.find_one({
109
+ "session_id": session_id,
110
+ "patient_id": patient_id
111
+ })
112
+ if not session:
113
+ logger.warning(f"Session {session_id} not found for patient {patient_id}")
114
+ return []
115
+
116
+ # Query messages with patient_id filter if provided
117
+ query = {"session_id": session_id}
118
+ if patient_id:
119
+ query["patient_id"] = patient_id
120
+
121
+ cursor = collection.find(query).sort("timestamp", ASCENDING)
122
+ if limit is not None:
123
+ cursor = cursor.limit(limit)
124
+ return list(cursor)
src/data/mongodb.py.backup ADDED
@@ -0,0 +1,638 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/mongodb.py
2
+
3
+ """
4
+ Interface for mongodb using pymongo.
5
+ This file has been refactored.
6
+ """
7
+
8
+ from datetime import datetime, timedelta, timezone
9
+ from typing import Any
10
+
11
+ from bson import ObjectId
12
+ from pandas import DataFrame
13
+ from pymongo import ASCENDING, DESCENDING, MongoClient
14
+ from pymongo.collection import Collection
15
+ from pymongo.database import Database
16
+ from pymongo.errors import DuplicateKeyError
17
+
18
+ from src.utils.logger import get_logger
19
+ import os
20
+
21
+ logger = get_logger("MONGO")
22
+
23
+ # Global client instance
24
+ _mongo_client: MongoClient | None = None
25
+
26
+ # Collection Names
27
+ ACCOUNTS_COLLECTION = "accounts"
28
+ CHAT_SESSIONS_COLLECTION = "chat_sessions"
29
+ MEDICAL_RECORDS_COLLECTION = "medical_records"
30
+ # DOCTORS_COLLECTION = "doctors"
31
+
32
+ # Base Database Operations
33
+ def get_database() -> Database:
34
+ """Get database instance with connection management"""
35
+ global _mongo_client
36
+ if _mongo_client is None:
37
+ CONNECTION_STRING = os.getenv("MONGO_USER", "mongodb://127.0.0.1:27017/") # fall back to local host if no user is provided
38
+ try:
39
+ logger.info("Initializing MongoDB connection")
40
+ _mongo_client = MongoClient(CONNECTION_STRING)
41
+ except Exception as e:
42
+ logger.error(f"Failed to connect to MongoDB: {str(e)}")
43
+ # Pass the error down, code that calls this function should handle it
44
+ raise e
45
+ db_name = os.getenv("USER_DB", "medicaldiagnosissystem")
46
+ return _mongo_client[db_name]
47
+
48
+ def close_connection():
49
+ """Close MongoDB connection"""
50
+ global _mongo_client
51
+ if _mongo_client is not None:
52
+ # Close the connection and reset the client
53
+ _mongo_client.close()
54
+ _mongo_client = None
55
+
56
+ def get_collection(name: str, /) -> Collection:
57
+ """Get a MongoDB collection by name"""
58
+ db = get_database()
59
+ return db.get_collection(name)
60
+
61
+
62
+ # Account Management
63
+ def get_account_frame(
64
+ *,
65
+ collection_name: str = ACCOUNTS_COLLECTION
66
+ ) -> DataFrame:
67
+ """Get accounts as a pandas DataFrame"""
68
+ return DataFrame(get_collection(collection_name).find())
69
+
70
+ def create_account(
71
+ user_data: dict[str, Any],
72
+ /, *,
73
+ collection_name: str = ACCOUNTS_COLLECTION
74
+ ) -> str:
75
+ """Create a new user account"""
76
+ collection = get_collection(collection_name)
77
+ now = datetime.now(timezone.utc)
78
+ user_data["created_at"] = now
79
+ user_data["updated_at"] = now
80
+ try:
81
+ result = collection.insert_one(user_data)
82
+ logger.info(f"Created new account: {result.inserted_id}")
83
+ return str(result.inserted_id)
84
+ except DuplicateKeyError as e:
85
+ logger.error(f"Failed to create account - duplicate key: {str(e)}")
86
+ raise DuplicateKeyError(f"Account already exists: {e}") from e
87
+
88
+ def update_account(
89
+ user_id: str,
90
+ updates: dict[str, Any],
91
+ /, *,
92
+ collection_name: str = ACCOUNTS_COLLECTION
93
+ ) -> bool:
94
+ """Update an existing user account"""
95
+ collection = get_collection(collection_name)
96
+ updates["updated_at"] = datetime.now(timezone.utc)
97
+ result = collection.update_one(
98
+ {"_id": user_id},
99
+ {"$set": updates}
100
+ )
101
+ return result.modified_count > 0
102
+
103
+
104
+ # Chat Session Management
105
+ def create_chat_session(
106
+ session_data: dict[str, Any],
107
+ /, *,
108
+ collection_name: str = CHAT_SESSIONS_COLLECTION
109
+ ) -> str:
110
+ """Create a new chat session"""
111
+ collection = get_collection(collection_name)
112
+ now = datetime.now(timezone.utc)
113
+ session_data["created_at"] = now
114
+ session_data["updated_at"] = now
115
+ if "_id" not in session_data:
116
+ session_data["_id"] = str(ObjectId())
117
+ result = collection.insert_one(session_data)
118
+ return str(result.inserted_id)
119
+
120
+ def get_user_sessions(
121
+ user_id: str,
122
+ /,
123
+ limit: int = 20,
124
+ *,
125
+ collection_name: str = CHAT_SESSIONS_COLLECTION
126
+ ) -> list[dict[str, Any]]:
127
+ """Get chat sessions for a specific user"""
128
+ collection = get_collection(collection_name)
129
+ return list(collection.find(
130
+ {"user_id": user_id}
131
+ ).sort("updated_at", DESCENDING).limit(limit))
132
+
133
+
134
+ # Message History
135
+ def add_message(
136
+ session_id: str,
137
+ message_data: dict[str, Any],
138
+ /, *,
139
+ collection_name: str = CHAT_SESSIONS_COLLECTION
140
+ ) -> str | None:
141
+ """Add a message to a chat session"""
142
+ collection = get_collection(collection_name)
143
+
144
+ # Verify session exists first
145
+ session = collection.find_one({
146
+ "$or": [
147
+ {"_id": session_id},
148
+ {"_id": ObjectId(session_id) if ObjectId.is_valid(session_id) else None}
149
+ ]
150
+ })
151
+ if not session:
152
+ logger.error(f"Failed to add message - session not found: {session_id}")
153
+ raise ValueError(f"Chat session not found: {session_id}")
154
+
155
+ now = datetime.now(timezone.utc)
156
+ message_data["timestamp"] = now
157
+ result = collection.update_one(
158
+ {"_id": session["_id"]},
159
+ {
160
+ "$push": {"messages": message_data},
161
+ "$set": {"updated_at": now}
162
+ }
163
+ )
164
+ return str(session_id) if result.modified_count > 0 else None
165
+
166
+ def get_session_messages(
167
+ session_id: str,
168
+ /,
169
+ limit: int | None = None,
170
+ *,
171
+ collection_name: str = CHAT_SESSIONS_COLLECTION
172
+ ) -> list[dict[str, Any]]:
173
+ """Get messages from a specific chat session"""
174
+ collection = get_collection(collection_name)
175
+ pipeline = [
176
+ {"$match": {"_id": session_id}},
177
+ {"$unwind": "$messages"},
178
+ {"$sort": {"messages.timestamp": -1}}
179
+ ]
180
+ if limit:
181
+ pipeline.append({"$limit": limit})
182
+ return [doc["messages"] for doc in collection.aggregate(pipeline)]
183
+
184
+
185
+ # Medical Records
186
+ def create_medical_record(
187
+ record_data: dict[str, Any],
188
+ /, *,
189
+ collection_name: str = MEDICAL_RECORDS_COLLECTION
190
+ ) -> str:
191
+ """Create a new medical record"""
192
+ collection = get_collection(collection_name)
193
+ now = datetime.now(timezone.utc)
194
+ record_data["created_at"] = now
195
+ record_data["updated_at"] = now
196
+ result = collection.insert_one(record_data)
197
+ return str(result.inserted_id)
198
+
199
+ def get_user_medical_records(
200
+ user_id: str,
201
+ /, *,
202
+ collection_name: str = MEDICAL_RECORDS_COLLECTION
203
+ ) -> list[dict[str, Any]]:
204
+ """Get medical records for a specific user"""
205
+ collection = get_collection(collection_name)
206
+ return list(collection.find({"user_id": user_id}).sort("created_at", ASCENDING))
207
+
208
+
209
+ # Utility Functions
210
+ def create_index(
211
+ collection_name: str,
212
+ field_name: str,
213
+ /,
214
+ unique: bool = False
215
+ ) -> None:
216
+ """Create an index on a collection"""
217
+ collection = get_collection(collection_name)
218
+ collection.create_index([(field_name, ASCENDING)], unique=unique)
219
+
220
+ def delete_old_sessions(
221
+ days: int = 30,
222
+ *,
223
+ collection_name: str = CHAT_SESSIONS_COLLECTION
224
+ ) -> int:
225
+ """Delete chat sessions older than specified days"""
226
+ collection = get_collection(collection_name)
227
+ cutoff = datetime.now(timezone.utc) - timedelta(days=days)
228
+ result = collection.delete_many({
229
+ "updated_at": {"$lt": cutoff}
230
+ })
231
+ if result.deleted_count > 0:
232
+ logger.info(f"Deleted {result.deleted_count} old sessions (>{days} days)")
233
+ return result.deleted_count
234
+
235
+ def backup_collection(collection_name: str) -> str:
236
+ """Create a backup of a collection"""
237
+ collection = get_collection(collection_name)
238
+ backup_name = f"{collection_name}_backup_{datetime.now(timezone.utc).strftime('%Y%m%d')}"
239
+ db = get_database()
240
+
241
+ # Drop existing backup if it exists
242
+ if backup_name in db.list_collection_names():
243
+ logger.info(f"Removing existing backup: {backup_name}")
244
+ db.drop_collection(backup_name)
245
+
246
+ db.create_collection(backup_name)
247
+ backup = db[backup_name]
248
+
249
+ doc_count = 0
250
+ for doc in collection.find():
251
+ backup.insert_one(doc)
252
+ doc_count += 1
253
+
254
+ logger.info(f"Created backup {backup_name} with {doc_count} documents")
255
+ return backup_name
256
+
257
+ # New: Chat and Medical Memory Persistence Helpers
258
+
259
+ CHAT_MESSAGES_COLLECTION = "chat_messages"
260
+ MEDICAL_MEMORY_COLLECTION = "medical_memory"
261
+ PATIENTS_COLLECTION = "patients"
262
+
263
+
264
+ def ensure_session(
265
+ *,
266
+ session_id: str,
267
+ patient_id: str,
268
+ doctor_id: str,
269
+ title: str,
270
+ last_activity: datetime | None = None,
271
+ collection_name: str = CHAT_SESSIONS_COLLECTION
272
+ ) -> None:
273
+ collection = get_collection(collection_name)
274
+ now = datetime.now(timezone.utc)
275
+ collection.update_one(
276
+ {"session_id": session_id},
277
+ {"$set": {
278
+ "session_id": session_id,
279
+ "patient_id": patient_id,
280
+ "doctor_id": doctor_id,
281
+ "title": title,
282
+ "last_activity": (last_activity or now),
283
+ "updated_at": now
284
+ }, "$setOnInsert": {"created_at": now}},
285
+ upsert=True
286
+ )
287
+
288
+
289
+ def save_chat_message(
290
+ *,
291
+ session_id: str,
292
+ patient_id: str,
293
+ doctor_id: str,
294
+ role: str,
295
+ content: str,
296
+ timestamp: datetime | None = None,
297
+ collection_name: str = CHAT_MESSAGES_COLLECTION
298
+ ) -> ObjectId:
299
+ collection = get_collection(collection_name)
300
+ ts = timestamp or datetime.now(timezone.utc)
301
+ doc = {
302
+ "session_id": session_id,
303
+ "patient_id": patient_id,
304
+ "doctor_id": doctor_id,
305
+ "role": role,
306
+ "content": content,
307
+ "timestamp": ts,
308
+ "created_at": ts
309
+ }
310
+ result = collection.insert_one(doc)
311
+ return result.inserted_id
312
+
313
+
314
+ def list_session_messages(
315
+ session_id: str,
316
+ /,
317
+ *,
318
+ patient_id: str | None = None,
319
+ limit: int | None = None,
320
+ collection_name: str = CHAT_MESSAGES_COLLECTION
321
+ ) -> list[dict[str, Any]]:
322
+ collection = get_collection(collection_name)
323
+
324
+ # First verify the session belongs to the patient
325
+ if patient_id:
326
+ session_collection = get_collection(CHAT_SESSIONS_COLLECTION)
327
+ session = session_collection.find_one({
328
+ "session_id": session_id,
329
+ "patient_id": patient_id
330
+ })
331
+ if not session:
332
+ logger.warning(f"Session {session_id} not found for patient {patient_id}")
333
+ return []
334
+
335
+ # Query messages with patient_id filter if provided
336
+ query = {"session_id": session_id}
337
+ if patient_id:
338
+ query["patient_id"] = patient_id
339
+
340
+ cursor = collection.find(query).sort("timestamp", ASCENDING)
341
+ if limit is not None:
342
+ cursor = cursor.limit(limit)
343
+ return list(cursor)
344
+
345
+
346
+ def save_memory_summary(
347
+ *,
348
+ patient_id: str,
349
+ doctor_id: str,
350
+ summary: str,
351
+ embedding: list[float] | None = None,
352
+ created_at: datetime | None = None,
353
+ collection_name: str = MEDICAL_MEMORY_COLLECTION
354
+ ) -> ObjectId:
355
+ collection = get_collection(collection_name)
356
+ ts = created_at or datetime.now(timezone.utc)
357
+ doc = {
358
+ "patient_id": patient_id,
359
+ "doctor_id": doctor_id,
360
+ "summary": summary,
361
+ "created_at": ts
362
+ }
363
+ if embedding is not None:
364
+ doc["embedding"] = embedding
365
+ result = collection.insert_one(doc)
366
+ return result.inserted_id
367
+
368
+
369
+ def get_recent_memory_summaries(
370
+ patient_id: str,
371
+ /,
372
+ *,
373
+ limit: int = 20,
374
+ collection_name: str = MEDICAL_MEMORY_COLLECTION
375
+ ) -> list[str]:
376
+ collection = get_collection(collection_name)
377
+ docs = list(collection.find({"patient_id": patient_id}).sort("created_at", DESCENDING).limit(limit))
378
+ return [d.get("summary", "") for d in docs]
379
+
380
+ def search_memory_summaries_semantic(
381
+ patient_id: str,
382
+ query_embedding: list[float],
383
+ /,
384
+ *,
385
+ limit: int = 5,
386
+ similarity_threshold: float = 0.5, # >= 50% semantic similarity
387
+ collection_name: str = MEDICAL_MEMORY_COLLECTION
388
+ ) -> list[dict[str, Any]]:
389
+ """
390
+ Search memory summaries using semantic similarity with embeddings.
391
+ Returns list of {summary, similarity_score, created_at} sorted by similarity.
392
+ """
393
+ collection = get_collection(collection_name)
394
+
395
+ # Get all summaries with embeddings for this patient
396
+ docs = list(collection.find({
397
+ "patient_id": patient_id,
398
+ "embedding": {"$exists": True}
399
+ }))
400
+
401
+ if not docs:
402
+ return []
403
+
404
+ # Calculate similarities
405
+ import numpy as np
406
+ query_vec = np.array(query_embedding, dtype="float32")
407
+ results = []
408
+
409
+ for doc in docs:
410
+ embedding = doc.get("embedding")
411
+ if not embedding:
412
+ continue
413
+
414
+ # Calculate cosine similarity
415
+ doc_vec = np.array(embedding, dtype="float32")
416
+ dot_product = np.dot(query_vec, doc_vec)
417
+ norm_query = np.linalg.norm(query_vec)
418
+ norm_doc = np.linalg.norm(doc_vec)
419
+
420
+ if norm_query == 0 or norm_doc == 0:
421
+ similarity = 0.0
422
+ else:
423
+ similarity = float(dot_product / (norm_query * norm_doc))
424
+
425
+ if similarity >= similarity_threshold:
426
+ results.append({
427
+ "summary": doc.get("summary", ""),
428
+ "similarity_score": similarity,
429
+ "created_at": doc.get("created_at"),
430
+ "session_id": doc.get("session_id") # if we add this field later
431
+ })
432
+
433
+ # Sort by similarity (highest first) and return top results
434
+ results.sort(key=lambda x: x["similarity_score"], reverse=True)
435
+ return results[:limit]
436
+
437
+
438
+ def list_patient_sessions(
439
+ patient_id: str,
440
+ /,
441
+ *,
442
+ collection_name: str = CHAT_SESSIONS_COLLECTION
443
+ ) -> list[dict[str, Any]]:
444
+ collection = get_collection(collection_name)
445
+ sessions = list(collection.find({"patient_id": patient_id}).sort("last_activity", DESCENDING))
446
+ # Convert ObjectId to string for JSON serialization
447
+ for session in sessions:
448
+ if "_id" in session:
449
+ session["_id"] = str(session["_id"])
450
+ return sessions
451
+
452
+ def delete_session(
453
+ session_id: str,
454
+ /,
455
+ *,
456
+ collection_name: str = CHAT_SESSIONS_COLLECTION
457
+ ) -> bool:
458
+ """Delete a chat session from MongoDB"""
459
+ collection = get_collection(collection_name)
460
+ result = collection.delete_one({"session_id": session_id})
461
+ return result.deleted_count > 0
462
+
463
+ def delete_session_messages(
464
+ session_id: str,
465
+ /,
466
+ *,
467
+ collection_name: str = CHAT_MESSAGES_COLLECTION
468
+ ) -> int:
469
+ """Delete all messages for a session from MongoDB"""
470
+ collection = get_collection(collection_name)
471
+ result = collection.delete_many({"session_id": session_id})
472
+ return result.deleted_count
473
+
474
+ # Patients helpers
475
+
476
+ def _generate_patient_id() -> str:
477
+ # Generate zero-padded 8-digit ID
478
+ import random
479
+ return f"{random.randint(0, 99999999):08d}"
480
+
481
+ def get_patient_by_id(patient_id: str) -> dict[str, Any] | None:
482
+ collection = get_collection(PATIENTS_COLLECTION)
483
+ return collection.find_one({"patient_id": patient_id})
484
+
485
+ def create_patient(
486
+ *,
487
+ name: str,
488
+ age: int,
489
+ sex: str,
490
+ address: str | None = None,
491
+ phone: str | None = None,
492
+ email: str | None = None,
493
+ medications: list[str] | None = None,
494
+ past_assessment_summary: str | None = None,
495
+ assigned_doctor_id: str | None = None
496
+ ) -> dict[str, Any]:
497
+ collection = get_collection(PATIENTS_COLLECTION)
498
+ now = datetime.now(timezone.utc)
499
+ # Ensure unique 8-digit id
500
+ for _ in range(10):
501
+ pid = _generate_patient_id()
502
+ if not collection.find_one({"patient_id": pid}):
503
+ break
504
+ else:
505
+ raise RuntimeError("Failed to generate unique patient ID")
506
+ doc = {
507
+ "patient_id": pid,
508
+ "name": name,
509
+ "age": age,
510
+ "sex": sex,
511
+ "address": address,
512
+ "phone": phone,
513
+ "email": email,
514
+ "medications": medications or [],
515
+ "past_assessment_summary": past_assessment_summary or "",
516
+ "assigned_doctor_id": assigned_doctor_id,
517
+ "created_at": now,
518
+ "updated_at": now
519
+ }
520
+ collection.insert_one(doc)
521
+ return doc
522
+
523
+ def update_patient_profile(patient_id: str, updates: dict[str, Any]) -> int:
524
+ collection = get_collection(PATIENTS_COLLECTION)
525
+ updates["updated_at"] = datetime.now(timezone.utc)
526
+ result = collection.update_one({"patient_id": patient_id}, {"$set": updates})
527
+ return result.modified_count
528
+
529
+ def search_patients(query: str, limit: int = 10) -> list[dict[str, Any]]:
530
+ """Search patients by name (case-insensitive starts-with/contains) or partial patient_id."""
531
+ collection = get_collection(PATIENTS_COLLECTION)
532
+ if not query:
533
+ return []
534
+
535
+ logger.info(f"Searching patients with query: '{query}', limit: {limit}")
536
+
537
+ # Build a regex for name search and patient_id partial match
538
+ import re
539
+ pattern = re.compile(re.escape(query), re.IGNORECASE)
540
+
541
+ try:
542
+ cursor = collection.find({
543
+ "$or": [
544
+ {"name": {"$regex": pattern}},
545
+ {"patient_id": {"$regex": pattern}}
546
+ ]
547
+ }).sort("name", ASCENDING).limit(limit)
548
+ results = []
549
+ for p in cursor:
550
+ p["_id"] = str(p.get("_id")) if p.get("_id") else None
551
+ results.append(p)
552
+ logger.info(f"Found {len(results)} patients matching query")
553
+ return results
554
+ except Exception as e:
555
+ logger.error(f"Error in search_patients: {e}")
556
+ return []
557
+
558
+ # Doctor Management
559
+ def create_doctor(
560
+ *,
561
+ name: str,
562
+ role: str | None = None,
563
+ specialty: str | None = None,
564
+ medical_roles: list[str] | None = None
565
+ ) -> str:
566
+ """Create a new doctor profile"""
567
+ collection = get_collection(ACCOUNTS_COLLECTION)
568
+ now = datetime.now(timezone.utc)
569
+ doctor_doc = {
570
+ "name": name,
571
+ "role": role,
572
+ "specialty": specialty,
573
+ "medical_roles": medical_roles or [],
574
+ "created_at": now,
575
+ "updated_at": now
576
+ }
577
+ try:
578
+ result = collection.insert_one(doctor_doc)
579
+ logger.info(f"Created new doctor: {name} with id {result.inserted_id}")
580
+ return str(result.inserted_id)
581
+ except Exception as e:
582
+ logger.error(f"Error creating doctor: {e}")
583
+ raise e
584
+
585
+ def get_doctor_by_name(name: str) -> dict[str, Any] | None:
586
+ """Get doctor by name from accounts collection"""
587
+ collection = get_collection(ACCOUNTS_COLLECTION)
588
+ doctor = collection.find_one({
589
+ "name": name,
590
+ "role": {"$in": ["Doctor", "Healthcare Prof", "General Practitioner", "Cardiologist", "Pediatrician", "Neurologist", "Dermatologist"]}
591
+ })
592
+ if doctor:
593
+ doctor["_id"] = str(doctor.get("_id")) if doctor.get("_id") else None
594
+ return doctor
595
+
596
+ def search_doctors(query: str, limit: int = 10) -> list[dict[str, Any]]:
597
+ """Search doctors by name (case-insensitive contains) from accounts collection"""
598
+ collection = get_collection(ACCOUNTS_COLLECTION)
599
+ if not query:
600
+ return []
601
+
602
+ logger.info(f"Searching doctors with query: '{query}', limit: {limit}")
603
+
604
+ # Build a regex for name search
605
+ import re
606
+ pattern = re.compile(re.escape(query), re.IGNORECASE)
607
+
608
+ try:
609
+ cursor = collection.find({
610
+ "name": {"$regex": pattern},
611
+ "role": {"$in": ["Doctor", "Healthcare Prof", "General Practitioner", "Cardiologist", "Pediatrician", "Neurologist", "Dermatologist"]}
612
+ }).sort("name", ASCENDING).limit(limit)
613
+ results = []
614
+ for d in cursor:
615
+ d["_id"] = str(d.get("_id")) if d.get("_id") else None
616
+ results.append(d)
617
+ logger.info(f"Found {len(results)} doctors matching query")
618
+ return results
619
+ except Exception as e:
620
+ logger.error(f"Error in search_doctors: {e}")
621
+ return []
622
+
623
+ def get_all_doctors(limit: int = 50) -> list[dict[str, Any]]:
624
+ """Get all doctors with optional limit from accounts collection"""
625
+ collection = get_collection(ACCOUNTS_COLLECTION)
626
+ try:
627
+ cursor = collection.find({
628
+ "role": {"$in": ["Doctor", "Healthcare Prof", "General Practitioner", "Cardiologist", "Pediatrician", "Neurologist", "Dermatologist"]}
629
+ }).sort("name", ASCENDING).limit(limit)
630
+ results = []
631
+ for d in cursor:
632
+ d["_id"] = str(d.get("_id")) if d.get("_id") else None
633
+ results.append(d)
634
+ logger.info(f"Retrieved {len(results)} doctors")
635
+ return results
636
+ except Exception as e:
637
+ logger.error(f"Error getting all doctors: {e}")
638
+ return []
src/data/patient/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/patient/__init__.py
2
+ """
3
+ Patient management operations for MongoDB.
4
+ """
5
+
6
+ from .operations import (
7
+ get_patient_by_id,
8
+ create_patient,
9
+ update_patient_profile,
10
+ search_patients,
11
+ )
12
+
13
+ __all__ = [
14
+ 'get_patient_by_id',
15
+ 'create_patient',
16
+ 'update_patient_profile',
17
+ 'search_patients',
18
+ ]
src/data/patient/operations.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/patient/operations.py
2
+ """
3
+ Patient management operations for MongoDB.
4
+ """
5
+
6
+ import re
7
+ from datetime import datetime, timezone
8
+ from typing import Any
9
+
10
+ from pymongo import ASCENDING
11
+
12
+ from ..connection import get_collection, PATIENTS_COLLECTION
13
+ from src.utils.logger import get_logger
14
+
15
+ logger = get_logger("PATIENT_OPS")
16
+
17
+
18
+ def _generate_patient_id() -> str:
19
+ """Generate zero-padded 8-digit ID"""
20
+ import random
21
+ return f"{random.randint(0, 99999999):08d}"
22
+
23
+
24
+ def get_patient_by_id(patient_id: str) -> dict[str, Any] | None:
25
+ collection = get_collection(PATIENTS_COLLECTION)
26
+ return collection.find_one({"patient_id": patient_id})
27
+
28
+
29
+ def create_patient(
30
+ *,
31
+ name: str,
32
+ age: int,
33
+ sex: str,
34
+ address: str | None = None,
35
+ phone: str | None = None,
36
+ email: str | None = None,
37
+ medications: list[str] | None = None,
38
+ past_assessment_summary: str | None = None,
39
+ assigned_doctor_id: str | None = None
40
+ ) -> dict[str, Any]:
41
+ collection = get_collection(PATIENTS_COLLECTION)
42
+ now = datetime.now(timezone.utc)
43
+ # Ensure unique 8-digit id
44
+ for _ in range(10):
45
+ pid = _generate_patient_id()
46
+ if not collection.find_one({"patient_id": pid}):
47
+ break
48
+ else:
49
+ raise RuntimeError("Failed to generate unique patient ID")
50
+ doc = {
51
+ "patient_id": pid,
52
+ "name": name,
53
+ "age": age,
54
+ "sex": sex,
55
+ "address": address,
56
+ "phone": phone,
57
+ "email": email,
58
+ "medications": medications or [],
59
+ "past_assessment_summary": past_assessment_summary or "",
60
+ "assigned_doctor_id": assigned_doctor_id,
61
+ "created_at": now,
62
+ "updated_at": now
63
+ }
64
+ collection.insert_one(doc)
65
+ return doc
66
+
67
+
68
+ def update_patient_profile(patient_id: str, updates: dict[str, Any]) -> int:
69
+ collection = get_collection(PATIENTS_COLLECTION)
70
+ updates["updated_at"] = datetime.now(timezone.utc)
71
+ result = collection.update_one({"patient_id": patient_id}, {"$set": updates})
72
+ return result.modified_count
73
+
74
+
75
+ def search_patients(query: str, limit: int = 10) -> list[dict[str, Any]]:
76
+ """Search patients by name (case-insensitive starts-with/contains) or partial patient_id."""
77
+ collection = get_collection(PATIENTS_COLLECTION)
78
+ if not query:
79
+ return []
80
+
81
+ logger.info(f"Searching patients with query: '{query}', limit: {limit}")
82
+
83
+ # Build a regex for name search and patient_id partial match
84
+ pattern = re.compile(re.escape(query), re.IGNORECASE)
85
+
86
+ try:
87
+ cursor = collection.find({
88
+ "$or": [
89
+ {"name": {"$regex": pattern}},
90
+ {"patient_id": {"$regex": pattern}}
91
+ ]
92
+ }).sort("name", ASCENDING).limit(limit)
93
+ results = []
94
+ for p in cursor:
95
+ p["_id"] = str(p.get("_id")) if p.get("_id") else None
96
+ results.append(p)
97
+ logger.info(f"Found {len(results)} patients matching query")
98
+ return results
99
+ except Exception as e:
100
+ logger.error(f"Error in search_patients: {e}")
101
+ return []
src/data/session/__init__.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/session/__init__.py
2
+ """
3
+ Session management operations for MongoDB.
4
+ """
5
+
6
+ from .operations import (
7
+ create_chat_session,
8
+ get_user_sessions,
9
+ ensure_session,
10
+ list_patient_sessions,
11
+ delete_session,
12
+ delete_session_messages,
13
+ delete_old_sessions,
14
+ )
15
+
16
+ __all__ = [
17
+ 'create_chat_session',
18
+ 'get_user_sessions',
19
+ 'ensure_session',
20
+ 'list_patient_sessions',
21
+ 'delete_session',
22
+ 'delete_session_messages',
23
+ 'delete_old_sessions',
24
+ ]
src/data/session/operations.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/session/operations.py
2
+ """
3
+ Session management operations for MongoDB.
4
+ """
5
+
6
+ from datetime import datetime, timedelta, timezone
7
+ from typing import Any
8
+
9
+ from bson import ObjectId
10
+ from pymongo import ASCENDING, DESCENDING
11
+
12
+ from ..connection import get_collection, CHAT_SESSIONS_COLLECTION, CHAT_MESSAGES_COLLECTION
13
+ from src.utils.logger import get_logger
14
+
15
+ logger = get_logger("SESSION_OPS")
16
+
17
+
18
+ def create_chat_session(
19
+ session_data: dict[str, Any],
20
+ /,
21
+ *,
22
+ collection_name: str = CHAT_SESSIONS_COLLECTION
23
+ ) -> str:
24
+ """Create a new chat session"""
25
+ collection = get_collection(collection_name)
26
+ now = datetime.now(timezone.utc)
27
+ session_data["created_at"] = now
28
+ session_data["updated_at"] = now
29
+ if "_id" not in session_data:
30
+ session_data["_id"] = str(ObjectId())
31
+ result = collection.insert_one(session_data)
32
+ return str(result.inserted_id)
33
+
34
+
35
+ def get_user_sessions(
36
+ user_id: str,
37
+ /,
38
+ limit: int = 20,
39
+ *,
40
+ collection_name: str = CHAT_SESSIONS_COLLECTION
41
+ ) -> list[dict[str, Any]]:
42
+ """Get chat sessions for a specific user"""
43
+ collection = get_collection(collection_name)
44
+ return list(collection.find(
45
+ {"user_id": user_id}
46
+ ).sort("updated_at", DESCENDING).limit(limit))
47
+
48
+
49
+ def ensure_session(
50
+ *,
51
+ session_id: str,
52
+ patient_id: str,
53
+ doctor_id: str,
54
+ title: str,
55
+ last_activity: datetime | None = None,
56
+ collection_name: str = CHAT_SESSIONS_COLLECTION
57
+ ) -> None:
58
+ collection = get_collection(collection_name)
59
+ now = datetime.now(timezone.utc)
60
+ collection.update_one(
61
+ {"session_id": session_id},
62
+ {"$set": {
63
+ "session_id": session_id,
64
+ "patient_id": patient_id,
65
+ "doctor_id": doctor_id,
66
+ "title": title,
67
+ "last_activity": (last_activity or now),
68
+ "updated_at": now
69
+ }, "$setOnInsert": {"created_at": now}},
70
+ upsert=True
71
+ )
72
+
73
+
74
+ def list_patient_sessions(
75
+ patient_id: str,
76
+ /,
77
+ *,
78
+ collection_name: str = CHAT_SESSIONS_COLLECTION
79
+ ) -> list[dict[str, Any]]:
80
+ collection = get_collection(collection_name)
81
+ sessions = list(collection.find({"patient_id": patient_id}).sort("last_activity", DESCENDING))
82
+ # Convert ObjectId to string for JSON serialization
83
+ for session in sessions:
84
+ if "_id" in session:
85
+ session["_id"] = str(session["_id"])
86
+ return sessions
87
+
88
+
89
+ def delete_session(
90
+ session_id: str,
91
+ /,
92
+ *,
93
+ collection_name: str = CHAT_SESSIONS_COLLECTION
94
+ ) -> bool:
95
+ """Delete a chat session from MongoDB"""
96
+ collection = get_collection(collection_name)
97
+ result = collection.delete_one({"session_id": session_id})
98
+ return result.deleted_count > 0
99
+
100
+
101
+ def delete_session_messages(
102
+ session_id: str,
103
+ /,
104
+ *,
105
+ collection_name: str = CHAT_MESSAGES_COLLECTION
106
+ ) -> int:
107
+ """Delete all messages for a session from MongoDB"""
108
+ collection = get_collection(collection_name)
109
+ result = collection.delete_many({"session_id": session_id})
110
+ return result.deleted_count
111
+
112
+
113
+ def delete_old_sessions(
114
+ days: int = 30,
115
+ *,
116
+ collection_name: str = CHAT_SESSIONS_COLLECTION
117
+ ) -> int:
118
+ """Delete chat sessions older than specified days"""
119
+ collection = get_collection(collection_name)
120
+ cutoff = datetime.now(timezone.utc) - timedelta(days=days)
121
+ result = collection.delete_many({
122
+ "updated_at": {"$lt": cutoff}
123
+ })
124
+ if result.deleted_count > 0:
125
+ logger.info(f"Deleted {result.deleted_count} old sessions (>{days} days)")
126
+ return result.deleted_count
src/data/user/__init__.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/user/__init__.py
2
+ """
3
+ User management operations for MongoDB.
4
+ """
5
+
6
+ from .operations import (
7
+ create_account,
8
+ update_account,
9
+ get_account_frame,
10
+ create_doctor,
11
+ get_doctor_by_name,
12
+ search_doctors,
13
+ get_all_doctors,
14
+ )
15
+
16
+ __all__ = [
17
+ 'create_account',
18
+ 'update_account',
19
+ 'get_account_frame',
20
+ 'create_doctor',
21
+ 'get_doctor_by_name',
22
+ 'search_doctors',
23
+ 'get_all_doctors',
24
+ ]
src/data/user/operations.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/user/operations.py
2
+ """
3
+ User management operations for MongoDB.
4
+ """
5
+
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+
9
+ import re
10
+ from pandas import DataFrame
11
+ from pymongo import ASCENDING
12
+ from pymongo.errors import DuplicateKeyError
13
+
14
+ from ..connection import get_collection, ACCOUNTS_COLLECTION
15
+ from src.utils.logger import get_logger
16
+
17
+ logger = get_logger("USER_OPS")
18
+
19
+
20
+ def get_account_frame(
21
+ *,
22
+ collection_name: str = ACCOUNTS_COLLECTION
23
+ ) -> DataFrame:
24
+ """Get accounts as a pandas DataFrame"""
25
+ return DataFrame(get_collection(collection_name).find())
26
+
27
+
28
+ def create_account(
29
+ user_data: dict[str, Any],
30
+ /,
31
+ *,
32
+ collection_name: str = ACCOUNTS_COLLECTION
33
+ ) -> str:
34
+ """Create a new user account"""
35
+ collection = get_collection(collection_name)
36
+ now = datetime.now(timezone.utc)
37
+ user_data["created_at"] = now
38
+ user_data["updated_at"] = now
39
+ try:
40
+ result = collection.insert_one(user_data)
41
+ logger.info(f"Created new account: {result.inserted_id}")
42
+ return str(result.inserted_id)
43
+ except DuplicateKeyError as e:
44
+ logger.error(f"Failed to create account - duplicate key: {str(e)}")
45
+ raise DuplicateKeyError(f"Account already exists: {e}") from e
46
+
47
+
48
+ def update_account(
49
+ user_id: str,
50
+ updates: dict[str, Any],
51
+ /,
52
+ *,
53
+ collection_name: str = ACCOUNTS_COLLECTION
54
+ ) -> bool:
55
+ """Update an existing user account"""
56
+ collection = get_collection(collection_name)
57
+ updates["updated_at"] = datetime.now(timezone.utc)
58
+ result = collection.update_one(
59
+ {"_id": user_id},
60
+ {"$set": updates}
61
+ )
62
+ return result.modified_count > 0
63
+
64
+
65
+ def create_doctor(
66
+ *,
67
+ name: str,
68
+ role: str | None = None,
69
+ specialty: str | None = None,
70
+ medical_roles: list[str] | None = None
71
+ ) -> str:
72
+ """Create a new doctor profile"""
73
+ collection = get_collection(ACCOUNTS_COLLECTION)
74
+ now = datetime.now(timezone.utc)
75
+ doctor_doc = {
76
+ "name": name,
77
+ "role": role,
78
+ "specialty": specialty,
79
+ "medical_roles": medical_roles or [],
80
+ "created_at": now,
81
+ "updated_at": now
82
+ }
83
+ try:
84
+ result = collection.insert_one(doctor_doc)
85
+ logger.info(f"Created new doctor: {name} with id {result.inserted_id}")
86
+ return str(result.inserted_id)
87
+ except Exception as e:
88
+ logger.error(f"Error creating doctor: {e}")
89
+ raise e
90
+
91
+
92
+ def get_doctor_by_name(name: str) -> dict[str, Any] | None:
93
+ """Get doctor by name from accounts collection"""
94
+ collection = get_collection(ACCOUNTS_COLLECTION)
95
+ doctor = collection.find_one({
96
+ "name": name,
97
+ "role": {"$in": ["Doctor", "Healthcare Prof", "General Practitioner", "Cardiologist", "Pediatrician", "Neurologist", "Dermatologist"]}
98
+ })
99
+ if doctor:
100
+ doctor["_id"] = str(doctor.get("_id")) if doctor.get("_id") else None
101
+ return doctor
102
+
103
+
104
+ def search_doctors(query: str, limit: int = 10) -> list[dict[str, Any]]:
105
+ """Search doctors by name (case-insensitive contains) from accounts collection"""
106
+ collection = get_collection(ACCOUNTS_COLLECTION)
107
+ if not query:
108
+ return []
109
+
110
+ logger.info(f"Searching doctors with query: '{query}', limit: {limit}")
111
+
112
+ # Build a regex for name search
113
+ pattern = re.compile(re.escape(query), re.IGNORECASE)
114
+
115
+ try:
116
+ cursor = collection.find({
117
+ "name": {"$regex": pattern},
118
+ "role": {"$in": ["Doctor", "Healthcare Prof", "General Practitioner", "Cardiologist", "Pediatrician", "Neurologist", "Dermatologist"]}
119
+ }).sort("name", ASCENDING).limit(limit)
120
+ results = []
121
+ for d in cursor:
122
+ d["_id"] = str(d.get("_id")) if d.get("_id") else None
123
+ results.append(d)
124
+ logger.info(f"Found {len(results)} doctors matching query")
125
+ return results
126
+ except Exception as e:
127
+ logger.error(f"Error in search_doctors: {e}")
128
+ return []
129
+
130
+
131
+ def get_all_doctors(limit: int = 50) -> list[dict[str, Any]]:
132
+ """Get all doctors with optional limit from accounts collection"""
133
+ collection = get_collection(ACCOUNTS_COLLECTION)
134
+ try:
135
+ cursor = collection.find({
136
+ "role": {"$in": ["Doctor", "Healthcare Prof", "General Practitioner", "Cardiologist", "Pediatrician", "Neurologist", "Dermatologist"]}
137
+ }).sort("name", ASCENDING).limit(limit)
138
+ results = []
139
+ for d in cursor:
140
+ d["_id"] = str(d.get("_id")) if d.get("_id") else None
141
+ results.append(d)
142
+ logger.info(f"Retrieved {len(results)} doctors")
143
+ return results
144
+ except Exception as e:
145
+ logger.error(f"Error getting all doctors: {e}")
146
+ return []
src/data/utils.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data/utils.py
2
+ """
3
+ Utility functions for MongoDB operations.
4
+ """
5
+
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+
9
+ from pymongo import ASCENDING
10
+
11
+ from .connection import get_collection, get_database
12
+ from src.utils.logger import get_logger
13
+
14
+ logger = get_logger("MONGO_UTILS")
15
+
16
+
17
+ def create_index(
18
+ collection_name: str,
19
+ field_name: str,
20
+ /,
21
+ unique: bool = False
22
+ ) -> None:
23
+ """Create an index on a collection"""
24
+ collection = get_collection(collection_name)
25
+ collection.create_index([(field_name, ASCENDING)], unique=unique)
26
+
27
+
28
+ def backup_collection(collection_name: str) -> str:
29
+ """Create a backup of a collection"""
30
+ collection = get_collection(collection_name)
31
+ backup_name = f"{collection_name}_backup_{datetime.now(timezone.utc).strftime('%Y%m%d')}"
32
+ db = get_database()
33
+
34
+ # Drop existing backup if it exists
35
+ if backup_name in db.list_collection_names():
36
+ logger.info(f"Removing existing backup: {backup_name}")
37
+ db.drop_collection(backup_name)
38
+
39
+ db.create_collection(backup_name)
40
+ backup = db[backup_name]
41
+
42
+ doc_count = 0
43
+ for doc in collection.find():
44
+ backup.insert_one(doc)
45
+ doc_count += 1
46
+
47
+ logger.info(f"Created backup {backup_name} with {doc_count} documents")
48
+ return backup_name
src/models/chat.py CHANGED
@@ -4,6 +4,8 @@ from pydantic import BaseModel
4
 
5
  class ChatRequest(BaseModel):
6
  user_id: str
 
 
7
  session_id: str
8
  message: str
9
  user_role: str | None = "Medical Professional"
@@ -18,6 +20,8 @@ class ChatResponse(BaseModel):
18
 
19
  class SessionRequest(BaseModel):
20
  user_id: str
 
 
21
  title: str | None = "New Chat"
22
 
23
  class SummariseRequest(BaseModel):
 
4
 
5
  class ChatRequest(BaseModel):
6
  user_id: str
7
+ patient_id: str
8
+ doctor_id: str
9
  session_id: str
10
  message: str
11
  user_role: str | None = "Medical Professional"
 
20
 
21
  class SessionRequest(BaseModel):
22
  user_id: str
23
+ patient_id: str
24
+ doctor_id: str
25
  title: str | None = "New Chat"
26
 
27
  class SummariseRequest(BaseModel):
src/models/user.py CHANGED
@@ -1,9 +1,38 @@
1
  # model/user.py
2
-
3
  from pydantic import BaseModel
 
4
 
5
  class UserProfileRequest(BaseModel):
6
  user_id: str
7
  name: str
8
  role: str
9
- specialty: str | None = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # model/user.py
 
2
  from pydantic import BaseModel
3
+ from typing import Optional, List
4
 
5
  class UserProfileRequest(BaseModel):
6
  user_id: str
7
  name: str
8
  role: str
9
+ specialty: Optional[str] = None
10
+ medical_roles: Optional[List[str]] = None
11
+
12
+ class PatientCreateRequest(BaseModel):
13
+ name: str
14
+ age: int
15
+ sex: str
16
+ address: Optional[str] = None
17
+ phone: Optional[str] = None
18
+ email: Optional[str] = None
19
+ medications: Optional[List[str]] = None
20
+ past_assessment_summary: Optional[str] = None
21
+ assigned_doctor_id: Optional[str] = None
22
+
23
+ class PatientUpdateRequest(BaseModel):
24
+ name: Optional[str] = None
25
+ age: Optional[int] = None
26
+ sex: Optional[str] = None
27
+ address: Optional[str] = None
28
+ phone: Optional[str] = None
29
+ email: Optional[str] = None
30
+ medications: Optional[List[str]] = None
31
+ past_assessment_summary: Optional[str] = None
32
+ assigned_doctor_id: Optional[str] = None
33
+
34
+ class DoctorCreateRequest(BaseModel):
35
+ name: str
36
+ role: Optional[str] = None
37
+ specialty: Optional[str] = None
38
+ medical_roles: Optional[List[str]] = None
start.py CHANGED
@@ -36,10 +36,26 @@ def main():
36
  else:
37
  print(f"✅ Found {len(gemini_keys)} Gemini API keys")
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  print("\n📱 Starting Medical AI Assistant...")
40
- print("🌐 Web UI will be available at: http://localhost:7860")
41
- print("📚 API documentation at: http://localhost:7860/docs")
42
- print("🔍 Health check at: http://localhost:7860/health")
43
  print("\nPress Ctrl+C to stop the server")
44
  print("=" * 50)
45
 
 
36
  else:
37
  print(f"✅ Found {len(gemini_keys)} Gemini API keys")
38
 
39
+ # Check for MongoDB environment variables
40
+ mongo_user = os.getenv("MONGO_USER")
41
+ user_db = os.getenv("USER_DB")
42
+
43
+ if not mongo_user:
44
+ print("❌ Error: MONGO_USER environment variable not found!")
45
+ print("Set MONGO_USER environment variable for database connectivity.")
46
+ sys.exit(1)
47
+
48
+ if not user_db:
49
+ print("❌ Error: USER_DB environment variable not found!")
50
+ print("Set USER_DB environment variable for database connectivity.")
51
+ sys.exit(1)
52
+
53
+ print("✅ MongoDB environment variables found")
54
+
55
  print("\n📱 Starting Medical AI Assistant...")
56
+ print("🌐 Web UI will be available at: https://medai-cos30018-medicaldiagnosissystem.hf.space")
57
+ print("📚 API documentation at: https://medai-cos30018-medicaldiagnosissystem.hf.space/docs")
58
+ print("🔍 Health check at: https://medai-cos30018-medicaldiagnosissystem.hf.space/health")
59
  print("\nPress Ctrl+C to stop the server")
60
  print("=" * 50)
61
 
static/css/emr.css ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* EMR Page Styles */
2
+ .emr-container {
3
+ min-height: 100vh;
4
+ background-color: var(--bg-primary);
5
+ color: var(--text-primary);
6
+ }
7
+
8
+ .emr-header {
9
+ background-color: var(--bg-secondary);
10
+ border-bottom: 1px solid var(--border-color);
11
+ padding: 1rem 0;
12
+ position: sticky;
13
+ top: 0;
14
+ z-index: 100;
15
+ }
16
+
17
+ .emr-header-content {
18
+ max-width: 1200px;
19
+ margin: 0 auto;
20
+ padding: 0 1rem;
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: space-between;
24
+ flex-wrap: wrap;
25
+ gap: 1rem;
26
+ }
27
+
28
+ .back-link {
29
+ display: inline-flex;
30
+ align-items: center;
31
+ gap: 0.5rem;
32
+ color: var(--primary-color);
33
+ text-decoration: none;
34
+ padding: 0.5rem 1rem;
35
+ border-radius: 6px;
36
+ transition: background-color var(--transition-fast);
37
+ }
38
+
39
+ .back-link:hover {
40
+ background-color: var(--bg-tertiary);
41
+ }
42
+
43
+ .emr-header h1 {
44
+ margin: 0;
45
+ font-size: 1.5rem;
46
+ font-weight: 600;
47
+ color: var(--text-primary);
48
+ }
49
+
50
+ .patient-info-header {
51
+ display: flex;
52
+ flex-direction: column;
53
+ align-items: flex-end;
54
+ text-align: right;
55
+ }
56
+
57
+ .patient-name {
58
+ font-size: 1.1rem;
59
+ font-weight: 600;
60
+ color: var(--text-primary);
61
+ }
62
+
63
+ .patient-id {
64
+ font-size: 0.9rem;
65
+ color: var(--text-secondary);
66
+ font-family: monospace;
67
+ }
68
+
69
+ .emr-main {
70
+ max-width: 1200px;
71
+ margin: 0 auto;
72
+ padding: 2rem 1rem;
73
+ }
74
+
75
+ .emr-section {
76
+ background-color: var(--bg-secondary);
77
+ border: 1px solid var(--border-color);
78
+ border-radius: 8px;
79
+ padding: 1.5rem;
80
+ margin-bottom: 2rem;
81
+ }
82
+
83
+ .emr-section h2 {
84
+ margin: 0 0 1.5rem 0;
85
+ font-size: 1.25rem;
86
+ font-weight: 600;
87
+ color: var(--text-primary);
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 0.5rem;
91
+ }
92
+
93
+ .emr-section h2 i {
94
+ color: var(--primary-color);
95
+ }
96
+
97
+ .emr-grid {
98
+ display: grid;
99
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
100
+ gap: 1.5rem;
101
+ }
102
+
103
+ .emr-field {
104
+ display: flex;
105
+ flex-direction: column;
106
+ gap: 0.5rem;
107
+ }
108
+
109
+ .emr-field.full-width {
110
+ grid-column: 1 / -1;
111
+ }
112
+
113
+ .emr-field label {
114
+ font-weight: 500;
115
+ color: var(--text-primary);
116
+ font-size: 0.9rem;
117
+ }
118
+
119
+ .emr-field input,
120
+ .emr-field select,
121
+ .emr-field textarea {
122
+ padding: 0.75rem;
123
+ border: 1px solid var(--border-color);
124
+ border-radius: 6px;
125
+ background-color: var(--bg-primary);
126
+ color: var(--text-primary);
127
+ font-size: 0.9rem;
128
+ transition: border-color var(--transition-fast);
129
+ }
130
+
131
+ .emr-field input:focus,
132
+ .emr-field select:focus,
133
+ .emr-field textarea:focus {
134
+ outline: none;
135
+ border-color: var(--primary-color);
136
+ box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.1);
137
+ }
138
+
139
+ .emr-field textarea {
140
+ resize: vertical;
141
+ min-height: 80px;
142
+ }
143
+
144
+ /* Medications */
145
+ .medications-container {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 1rem;
149
+ }
150
+
151
+ .medications-list {
152
+ display: flex;
153
+ flex-wrap: wrap;
154
+ gap: 0.5rem;
155
+ min-height: 40px;
156
+ padding: 0.5rem;
157
+ border: 1px solid var(--border-color);
158
+ border-radius: 6px;
159
+ background-color: var(--bg-primary);
160
+ }
161
+
162
+ .medication-tag {
163
+ display: inline-flex;
164
+ align-items: center;
165
+ gap: 0.5rem;
166
+ background-color: var(--primary-color);
167
+ color: white;
168
+ padding: 0.25rem 0.75rem;
169
+ border-radius: 20px;
170
+ font-size: 0.8rem;
171
+ font-weight: 500;
172
+ }
173
+
174
+ .medication-tag .remove-medication {
175
+ background: none;
176
+ border: none;
177
+ color: white;
178
+ cursor: pointer;
179
+ padding: 0;
180
+ margin-left: 0.25rem;
181
+ opacity: 0.7;
182
+ transition: opacity var(--transition-fast);
183
+ }
184
+
185
+ .medication-tag .remove-medication:hover {
186
+ opacity: 1;
187
+ }
188
+
189
+ .add-medication {
190
+ display: flex;
191
+ gap: 0.5rem;
192
+ align-items: center;
193
+ }
194
+
195
+ .add-medication input {
196
+ flex: 1;
197
+ margin: 0;
198
+ }
199
+
200
+ .add-medication button {
201
+ white-space: nowrap;
202
+ }
203
+
204
+ /* Sessions */
205
+ .sessions-container {
206
+ display: flex;
207
+ flex-direction: column;
208
+ gap: 1rem;
209
+ }
210
+
211
+ .session-item {
212
+ background-color: var(--bg-primary);
213
+ border: 1px solid var(--border-color);
214
+ border-radius: 6px;
215
+ padding: 1rem;
216
+ cursor: pointer;
217
+ transition: all var(--transition-fast);
218
+ }
219
+
220
+ .session-item:hover {
221
+ border-color: var(--primary-color);
222
+ background-color: var(--bg-tertiary);
223
+ }
224
+
225
+ .session-title {
226
+ font-weight: 600;
227
+ color: var(--text-primary);
228
+ margin-bottom: 0.5rem;
229
+ }
230
+
231
+ .session-meta {
232
+ display: flex;
233
+ justify-content: space-between;
234
+ align-items: center;
235
+ font-size: 0.8rem;
236
+ color: var(--text-secondary);
237
+ }
238
+
239
+ .session-date {
240
+ font-family: monospace;
241
+ }
242
+
243
+ .session-messages {
244
+ color: var(--text-secondary);
245
+ }
246
+
247
+ /* Actions */
248
+ .emr-actions {
249
+ display: flex;
250
+ gap: 1rem;
251
+ flex-wrap: wrap;
252
+ }
253
+
254
+ .btn-primary,
255
+ .btn-secondary {
256
+ display: inline-flex;
257
+ align-items: center;
258
+ gap: 0.5rem;
259
+ padding: 0.75rem 1.5rem;
260
+ border: none;
261
+ border-radius: 6px;
262
+ font-size: 0.9rem;
263
+ font-weight: 500;
264
+ cursor: pointer;
265
+ transition: all var(--transition-fast);
266
+ text-decoration: none;
267
+ }
268
+
269
+ .btn-primary {
270
+ background-color: var(--primary-color);
271
+ color: white;
272
+ }
273
+
274
+ .btn-primary:hover {
275
+ background-color: var(--primary-color-dark);
276
+ transform: translateY(-1px);
277
+ }
278
+
279
+ .btn-secondary {
280
+ background-color: var(--bg-tertiary);
281
+ color: var(--text-primary);
282
+ border: 1px solid var(--border-color);
283
+ }
284
+
285
+ .btn-secondary:hover {
286
+ background-color: var(--bg-primary);
287
+ border-color: var(--primary-color);
288
+ }
289
+
290
+ /* Loading States */
291
+ .loading {
292
+ opacity: 0.6;
293
+ pointer-events: none;
294
+ }
295
+
296
+ /* Responsive Design */
297
+ @media (max-width: 768px) {
298
+ .emr-header-content {
299
+ flex-direction: column;
300
+ align-items: stretch;
301
+ text-align: center;
302
+ }
303
+
304
+ .patient-info-header {
305
+ align-items: center;
306
+ text-align: center;
307
+ }
308
+
309
+ .emr-main {
310
+ padding: 1rem;
311
+ }
312
+
313
+ .emr-grid {
314
+ grid-template-columns: 1fr;
315
+ }
316
+
317
+ .emr-actions {
318
+ flex-direction: column;
319
+ }
320
+
321
+ .add-medication {
322
+ flex-direction: column;
323
+ align-items: stretch;
324
+ }
325
+ }
326
+
327
+ /* Dark Theme Adjustments */
328
+ [data-theme="dark"] .emr-field input,
329
+ [data-theme="dark"] .emr-field select,
330
+ [data-theme="dark"] .emr-field textarea {
331
+ background-color: var(--bg-secondary);
332
+ }
333
+
334
+ [data-theme="dark"] .medications-list {
335
+ background-color: var(--bg-secondary);
336
+ }
337
+
338
+ [data-theme="dark"] .session-item {
339
+ background-color: var(--bg-secondary);
340
+ }
static/css/patient.css ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #0f172a; color: #e2e8f0; }
2
+ .container { max-width: 900px; margin: 40px auto; padding: 24px; background: #111827; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); }
3
+ .back-link { display:inline-flex; align-items:center; gap:8px; color:#93c5fd; text-decoration:none; margin-bottom:12px; }
4
+ .back-link:hover { text-decoration:underline; }
5
+ h1 { margin-bottom: 16px; font-size: 1.6rem; }
6
+ .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
7
+ label { display: grid; gap: 6px; font-size: 0.9rem; }
8
+ input, select, textarea { padding: 10px; border-radius: 8px; border: 1px solid #334155; background: #0b1220; color: #e2e8f0; }
9
+ textarea { min-height: 100px; }
10
+ .actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px; }
11
+ .primary { background: #2563eb; color: #fff; border: none; padding: 10px 14px; border-radius: 8px; cursor: pointer; }
12
+ .secondary { background: transparent; color: #e2e8f0; border: 1px solid #334155; padding: 10px 14px; border-radius: 8px; cursor: pointer; }
13
+ .result { margin-top: 12px; color: #93c5fd; }
14
+ @media (max-width: 720px) { .grid { grid-template-columns: 1fr; } }
15
+ .modal { position: fixed; inset:0; display:none; align-items:center; justify-content:center; background: rgba(0,0,0,0.45); z-index: 3000; }
16
+ .modal.show { display:flex; }
17
+ .modal-content { background:#111827; color:#e2e8f0; width: 520px; max-width: 92vw; border-radius: 12px; overflow:hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.4); }
18
+ .modal-header, .modal-footer { padding: 16px; border-bottom: 1px solid #334155; display:flex; align-items:center; justify-content: space-between; }
19
+ .modal-footer { border-bottom: none; border-top: 1px solid #334155; justify-content: flex-end; gap: 8px; }
20
+ .modal-body { padding: 16px; }
21
+ .modal-close { background: transparent; border: none; color: #94a3b8; font-size: 20px; cursor: pointer; }
22
+ .big-id { font-size: 28px; letter-spacing: 2px; font-weight: 700; color: #93c5fd; margin: 8px 0 4px; }
static/css/styles.css CHANGED
@@ -80,6 +80,8 @@ body {
80
  height: 100vh;
81
  overflow: hidden;
82
  }
 
 
83
 
84
  /* Sidebar */
85
  .sidebar {
@@ -89,7 +91,7 @@ body {
89
  display: flex;
90
  flex-direction: column;
91
  transition: transform var(--transition-normal);
92
- z-index: 1000;
93
  }
94
 
95
  .sidebar-header {
@@ -358,6 +360,19 @@ body {
358
  background-color: var(--bg-tertiary);
359
  }
360
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  .chat-title {
362
  flex: 1;
363
  font-size: 1.25rem;
@@ -742,11 +757,15 @@ body {
742
  left: 0;
743
  top: 0;
744
  height: 100vh;
 
745
  transform: translateX(-100%);
 
 
746
  }
747
 
748
  .sidebar.show {
749
  transform: translateX(0);
 
750
  }
751
 
752
  .sidebar-toggle {
@@ -757,6 +776,24 @@ body {
757
  font-size: 1rem;
758
  }
759
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760
  .modal-content {
761
  width: 95%;
762
  margin: var(--spacing-md);
@@ -858,3 +895,111 @@ body {
858
  color: var(--primary-color);
859
  margin-right: var(--spacing-sm);
860
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  height: 100vh;
81
  overflow: hidden;
82
  }
83
+ .app-overlay { position: fixed; inset:0; background: rgba(0,0,0,0.2); display:none; z-index: 1000; }
84
+ .app-overlay.show { display:block; }
85
 
86
  /* Sidebar */
87
  .sidebar {
 
91
  display: flex;
92
  flex-direction: column;
93
  transition: transform var(--transition-normal);
94
+ z-index: 1001;
95
  }
96
 
97
  .sidebar-header {
 
360
  background-color: var(--bg-tertiary);
361
  }
362
 
363
+ .sidebar-close {
364
+ background: none;
365
+ border: none;
366
+ color: var(--text-secondary);
367
+ font-size: 1.2rem;
368
+ cursor: pointer;
369
+ padding: var(--spacing-sm);
370
+ border-radius: 6px;
371
+ transition: background-color var(--transition-fast);
372
+ margin-left: auto;
373
+ }
374
+ .sidebar-close:hover { background-color: var(--bg-tertiary); }
375
+
376
  .chat-title {
377
  flex: 1;
378
  font-size: 1.25rem;
 
757
  left: 0;
758
  top: 0;
759
  height: 100vh;
760
+ width: 300px;
761
  transform: translateX(-100%);
762
+ transition: transform 0.3s ease;
763
+ z-index: 1001;
764
  }
765
 
766
  .sidebar.show {
767
  transform: translateX(0);
768
+ z-index: 1002;
769
  }
770
 
771
  .sidebar-toggle {
 
776
  font-size: 1rem;
777
  }
778
 
779
+ .app-overlay {
780
+ position: fixed;
781
+ top: 0;
782
+ left: 0;
783
+ width: 100%;
784
+ height: 100%;
785
+ background: rgba(0, 0, 0, 0.5);
786
+ z-index: 999;
787
+ display: none;
788
+ opacity: 0;
789
+ transition: opacity 0.3s ease;
790
+ }
791
+
792
+ .app-overlay.show {
793
+ display: block;
794
+ opacity: 1;
795
+ }
796
+
797
  .modal-content {
798
  width: 95%;
799
  margin: var(--spacing-md);
 
895
  color: var(--primary-color);
896
  margin-right: var(--spacing-sm);
897
  }
898
+
899
+ .patient-section {
900
+ padding: var(--spacing-lg);
901
+ border-bottom: 1px solid var(--border-color);
902
+ }
903
+
904
+ .patient-header {
905
+ font-weight: 600;
906
+ margin-bottom: var(--spacing-sm);
907
+ color: var(--text-primary);
908
+ }
909
+
910
+ .patient-input-group {
911
+ display: flex;
912
+ gap: var(--spacing-sm);
913
+ align-items: center;
914
+ }
915
+ .patient-typeahead { position: relative; }
916
+ .patient-suggestions { position:absolute; top: 100%; left:0; right:0; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 6px; box-shadow: var(--shadow-lg); z-index: 3500; max-height: 220px; overflow-y: auto; }
917
+ .patient-suggestion { padding: 8px 10px; cursor:pointer; color: var(--text-primary); }
918
+ .patient-suggestion:hover { background: var(--bg-tertiary); }
919
+
920
+ .patient-input {
921
+ flex: 1;
922
+ padding: 8px 10px;
923
+ border: 1px solid var(--border-color);
924
+ border-radius: 6px;
925
+ background: var(--bg-primary);
926
+ color: var(--text-primary);
927
+ }
928
+
929
+ .patient-load-btn {
930
+ padding: 8px 10px;
931
+ background: var(--primary-color);
932
+ color: #fff;
933
+ border: none;
934
+ border-radius: 6px;
935
+ cursor: pointer;
936
+ }
937
+ .patient-load-btn:hover {
938
+ background: var(--primary-hover);
939
+ }
940
+ .patient-create-link {
941
+ display:inline-flex; align-items:center;
942
+ justify-content:center; padding:8px 10px;
943
+ border: 1px solid var(--border-color);
944
+ border-radius:6px; color: var(--text-secondary);
945
+ text-decoration:none;
946
+ }
947
+ .patient-create-link:hover { background: var(--bg-tertiary); }
948
+
949
+ .patient-status {
950
+ margin-top: var(--spacing-sm);
951
+ font-size: 0.8rem;
952
+ color: var(--text-secondary);
953
+ }
954
+
955
+ .patient-actions {
956
+ margin-top: var(--spacing-sm);
957
+ text-align: center;
958
+ }
959
+
960
+ .emr-link {
961
+ display: inline-flex;
962
+ align-items: center;
963
+ gap: 0.5rem;
964
+ padding: 0.5rem 1rem;
965
+ background-color: var(--primary-color);
966
+ color: white;
967
+ text-decoration: none;
968
+ border-radius: 6px;
969
+ font-size: 0.8rem;
970
+ font-weight: 500;
971
+ transition: all var(--transition-fast);
972
+ }
973
+
974
+ .emr-link:hover {
975
+ background-color: var(--primary-color-dark);
976
+ transform: translateY(-1px);
977
+ color: white;
978
+ }
979
+
980
+ /* Sidebar mobile behavior */
981
+ @media (max-width: 1024px) {
982
+ .sidebar {
983
+ position: fixed;
984
+ top: 0;
985
+ bottom: 0;
986
+ left: 0;
987
+ transform: translateX(-100%);
988
+ width: 85vw;
989
+ }
990
+ .sidebar.show {
991
+ transform: translateX(0);
992
+ }
993
+ .main-content {
994
+ position: relative;
995
+ z-index: 1;
996
+ }
997
+ .sidebar-toggle {
998
+ display: inline-flex;
999
+ }
1000
+ }
1001
+
1002
+ /* Loading overlay default hidden */
1003
+ .loading-overlay { display: none; align-items: center; justify-content: center; position: fixed; inset: 0; background: rgba(0,0,0,0.35); z-index: 3000; pointer-events: none; }
1004
+ .loading-overlay.show { display: flex; pointer-events: all; }
1005
+ .loading-spinner { display: grid; gap: 8px; color: #fff; text-align: center; }
static/emr.html ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Patient EMR - Medical AI Assistant</title>
7
+ <link rel="stylesheet" href="/static/css/styles.css">
8
+ <link rel="stylesheet" href="/static/css/emr.css">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
10
+ </head>
11
+ <body>
12
+ <div class="emr-container">
13
+ <!-- Header -->
14
+ <header class="emr-header">
15
+ <div class="emr-header-content">
16
+ <a href="/" class="back-link">
17
+ <i class="fas fa-arrow-left"></i>
18
+ Back to Assistant
19
+ </a>
20
+ <h1>Patient EMR</h1>
21
+ <div class="patient-info-header" id="patientInfoHeader">
22
+ <span class="patient-name" id="patientName">Loading...</span>
23
+ <span class="patient-id" id="patientId">Loading...</span>
24
+ </div>
25
+ </div>
26
+ </header>
27
+
28
+ <!-- Main Content -->
29
+ <main class="emr-main">
30
+ <!-- Patient Overview -->
31
+ <section class="emr-section">
32
+ <h2><i class="fas fa-user"></i> Patient Overview</h2>
33
+ <div class="emr-grid">
34
+ <div class="emr-field">
35
+ <label>Full Name</label>
36
+ <input type="text" id="patientNameInput" placeholder="Enter patient name">
37
+ </div>
38
+ <div class="emr-field">
39
+ <label>Age</label>
40
+ <input type="number" id="patientAgeInput" placeholder="Enter age" min="0" max="150">
41
+ </div>
42
+ <div class="emr-field">
43
+ <label>Sex</label>
44
+ <select id="patientSexInput">
45
+ <option value="">Select sex</option>
46
+ <option value="Male">Male</option>
47
+ <option value="Female">Female</option>
48
+ <option value="Other">Other</option>
49
+ </select>
50
+ </div>
51
+ <div class="emr-field">
52
+ <label>Phone</label>
53
+ <input type="tel" id="patientPhoneInput" placeholder="Enter phone number">
54
+ </div>
55
+ <div class="emr-field">
56
+ <label>Email</label>
57
+ <input type="email" id="patientEmailInput" placeholder="Enter email address">
58
+ </div>
59
+ <div class="emr-field full-width">
60
+ <label>Address</label>
61
+ <textarea id="patientAddressInput" placeholder="Enter full address" rows="3"></textarea>
62
+ </div>
63
+ </div>
64
+ </section>
65
+
66
+ <!-- Medical Information -->
67
+ <section class="emr-section">
68
+ <h2><i class="fas fa-pills"></i> Medical Information</h2>
69
+ <div class="emr-grid">
70
+ <div class="emr-field full-width">
71
+ <label>Current Medications</label>
72
+ <div class="medications-container">
73
+ <div class="medications-list" id="medicationsList">
74
+ <!-- Medications will be added here dynamically -->
75
+ </div>
76
+ <div class="add-medication">
77
+ <input type="text" id="newMedicationInput" placeholder="Add new medication">
78
+ <button id="addMedicationBtn" class="btn-secondary">
79
+ <i class="fas fa-plus"></i> Add
80
+ </button>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ <div class="emr-field full-width">
85
+ <label>Past Assessment Summary</label>
86
+ <textarea id="pastAssessmentInput" placeholder="Enter past assessment summary" rows="4"></textarea>
87
+ </div>
88
+ </div>
89
+ </section>
90
+
91
+ <!-- Recent Sessions -->
92
+ <section class="emr-section">
93
+ <h2><i class="fas fa-comments"></i> Recent Chat Sessions</h2>
94
+ <div class="sessions-container" id="sessionsContainer">
95
+ <!-- Sessions will be loaded here -->
96
+ </div>
97
+ </section>
98
+
99
+ <!-- Actions -->
100
+ <section class="emr-section">
101
+ <h2><i class="fas fa-cog"></i> Actions</h2>
102
+ <div class="emr-actions">
103
+ <button id="savePatientBtn" class="btn-primary">
104
+ <i class="fas fa-save"></i> Save Changes
105
+ </button>
106
+ <button id="refreshPatientBtn" class="btn-secondary">
107
+ <i class="fas fa-refresh"></i> Refresh Data
108
+ </button>
109
+ <button id="exportPatientBtn" class="btn-secondary">
110
+ <i class="fas fa-download"></i> Export EMR
111
+ </button>
112
+ </div>
113
+ </section>
114
+ </main>
115
+ </div>
116
+
117
+ <!-- Loading Overlay -->
118
+ <div class="loading-overlay" id="loadingOverlay">
119
+ <div class="loading-spinner">
120
+ <i class="fas fa-spinner fa-spin"></i>
121
+ <div>Loading...</div>
122
+ </div>
123
+ </div>
124
+
125
+ <script src="/static/js/emr.js"></script>
126
+ </body>
127
+ </html>
static/index.html CHANGED
@@ -10,6 +10,7 @@
10
  </head>
11
  <body>
12
  <div class="app-container">
 
13
  <!-- Sidebar -->
14
  <div class="sidebar" id="sidebar">
15
  <div class="sidebar-header">
@@ -26,7 +27,7 @@
26
  </div>
27
  <div class="user-info">
28
  <div class="user-name" id="userName">Anonymous</div>
29
- <div class="user-status">Medical Professional</div>
30
  </div>
31
  <button class="user-menu-btn" id="userMenuBtn">
32
  <i class="fas fa-ellipsis-v"></i>
@@ -34,6 +35,27 @@
34
  </div>
35
  </div>
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  <div class="chat-sessions" id="chatSessions">
38
  <!-- Chat sessions will be populated here -->
39
  </div>
@@ -122,22 +144,31 @@
122
  <div class="modal" id="userModal">
123
  <div class="modal-content">
124
  <div class="modal-header">
125
- <h3>User Profile</h3>
126
  <button class="modal-close" id="userModalClose">&times;</button>
127
  </div>
128
  <div class="modal-body">
129
  <div class="form-group">
130
- <label for="profileName">Name:</label>
131
- <input type="text" id="profileName" placeholder="Enter your name">
 
 
 
 
 
 
 
 
132
  </div>
133
  <div class="form-group">
134
  <label for="profileRole">Medical Role:</label>
135
  <select id="profileRole">
136
- <option value="Physician">Physician</option>
 
137
  <option value="Nurse">Nurse</option>
 
 
138
  <option value="Medical Student">Medical Student</option>
139
- <option value="Healthcare Professional">Healthcare Professional</option>
140
- <option value="Patient">Patient</option>
141
  <option value="Other">Other</option>
142
  </select>
143
  </div>
@@ -187,10 +218,7 @@
187
  </label>
188
  </div>
189
  </div>
190
- <div class="modal-footer">
191
- <button class="btn btn-secondary" id="settingsModalCancel">Cancel</button>
192
- <button class="btn btn-primary" id="settingsModalSave">Save</button>
193
- </div>
194
  </div>
195
  </div>
196
 
@@ -214,6 +242,27 @@
214
  </div>
215
  </div>
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  <!-- Loading overlay -->
218
  <div class="loading-overlay" id="loadingOverlay">
219
  <div class="loading-spinner">
@@ -222,6 +271,9 @@
222
  </div>
223
  </div>
224
 
225
- <script src="/static/js/app.js"></script>
 
 
 
226
  </body>
227
  </html>
 
10
  </head>
11
  <body>
12
  <div class="app-container">
13
+ <div class="app-overlay" id="appOverlay"></div>
14
  <!-- Sidebar -->
15
  <div class="sidebar" id="sidebar">
16
  <div class="sidebar-header">
 
27
  </div>
28
  <div class="user-info">
29
  <div class="user-name" id="userName">Anonymous</div>
30
+ <div class="user-status" id="userStatus">Medical Professional</div>
31
  </div>
32
  <button class="user-menu-btn" id="userMenuBtn">
33
  <i class="fas fa-ellipsis-v"></i>
 
35
  </div>
36
  </div>
37
 
38
+ <!-- Patient Login Section -->
39
+ <div class="patient-section">
40
+ <div class="patient-header">Patient</div>
41
+ <div class="patient-input-group patient-typeahead">
42
+ <input type="text" id="patientIdInput" class="patient-input" placeholder="Search patient by name or ID">
43
+ <div id="patientSuggestions" class="patient-suggestions" style="display:none;"></div>
44
+ <button class="patient-load-btn" id="loadPatientBtn" title="Load Patient">
45
+ <i class="fas fa-arrow-right"></i>
46
+ </button>
47
+ <a class="patient-create-link" id="createPatientLink" href="/static/patient.html" title="Create new patient">
48
+ <i class="fas fa-user-plus"></i>
49
+ </a>
50
+ </div>
51
+ <div class="patient-status" id="patientStatus">No patient selected</div>
52
+ <div class="patient-actions" id="patientActions" style="display: none;">
53
+ <a href="#" id="emrLink" class="emr-link">
54
+ <i class="fas fa-file-medical"></i> EMR
55
+ </a>
56
+ </div>
57
+ </div>
58
+
59
  <div class="chat-sessions" id="chatSessions">
60
  <!-- Chat sessions will be populated here -->
61
  </div>
 
144
  <div class="modal" id="userModal">
145
  <div class="modal-content">
146
  <div class="modal-header">
147
+ <h3>Doctor Profile</h3>
148
  <button class="modal-close" id="userModalClose">&times;</button>
149
  </div>
150
  <div class="modal-body">
151
  <div class="form-group">
152
+ <label for="profileNameSelect">Name:</label>
153
+ <select id="profileNameSelect"></select>
154
+ </div>
155
+ <div class="form-group" id="newDoctorSection" style="display:none;">
156
+ <label for="newDoctorName">Doctor name</label>
157
+ <input type="text" id="newDoctorName" placeholder="Enter doctor name">
158
+ <div style="display:flex; gap:8px; margin-top:8px;">
159
+ <button type="button" class="btn btn-secondary" id="cancelNewDoctor">Cancel</button>
160
+ <button type="button" class="btn btn-primary" id="confirmNewDoctor">Confirm</button>
161
+ </div>
162
  </div>
163
  <div class="form-group">
164
  <label for="profileRole">Medical Role:</label>
165
  <select id="profileRole">
166
+ <option value="Doctor">Doctor</option>
167
+ <option value="Healthcare Prof">Healthcare Prof</option>
168
  <option value="Nurse">Nurse</option>
169
+ <option value="Caregiver">Caregiver</option>
170
+ <option value="Physician">Physician</option>
171
  <option value="Medical Student">Medical Student</option>
 
 
172
  <option value="Other">Other</option>
173
  </select>
174
  </div>
 
218
  </label>
219
  </div>
220
  </div>
221
+ <div class="modal-footer"></div>
 
 
 
222
  </div>
223
  </div>
224
 
 
242
  </div>
243
  </div>
244
 
245
+ <!-- Patient Modal -->
246
+ <div class="modal" id="patientModal">
247
+ <div class="modal-content">
248
+ <div class="modal-header">
249
+ <h3>Patient Profile</h3>
250
+ <button class="modal-close" id="patientModalClose">&times;</button>
251
+ </div>
252
+ <div class="modal-body">
253
+ <div class="patient-summary" id="patientSummary"></div>
254
+ <div class="patient-details">
255
+ <div><strong>Medications:</strong> <span id="patientMedications">-</span></div>
256
+ <div><strong>Past Assessment:</strong> <span id="patientAssessment">-</span></div>
257
+ </div>
258
+ </div>
259
+ <div class="modal-footer">
260
+ <button id="patientLogoutBtn" class="btn-danger">Log out patient</button>
261
+ <a id="patientCreateBtn" class="btn-primary" href="/static/patient.html">Create new patient</a>
262
+ </div>
263
+ </div>
264
+ </div>
265
+
266
  <!-- Loading overlay -->
267
  <div class="loading-overlay" id="loadingOverlay">
268
  <div class="loading-spinner">
 
271
  </div>
272
  </div>
273
 
274
+ <!-- Sidebar overlay for outside-click close -->
275
+ <div id="sidebarOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:900;"></div>
276
+
277
+ <script type="module" src="/static/js/app.js"></script>
278
  </body>
279
  </html>
static/js/app.js CHANGED
The diff for this file is too large to render. See raw diff
 
static/js/chat/messaging.js ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // chat/messaging.js
2
+ // Send, API call, add/display message, summariseTitle, format content/time
3
+
4
+ export function attachMessagingUI(app) {
5
+ app.sendMessage = async function () {
6
+ const input = document.getElementById('chatInput');
7
+ const message = input.value.trim();
8
+ if (!message || app.isLoading) return;
9
+ if (!app.currentPatientId) {
10
+ const status = document.getElementById('patientStatus');
11
+ if (status) { status.textContent = 'Select a patient before chatting.'; status.style.color = 'var(--warning-color)'; }
12
+ return;
13
+ }
14
+ input.value = '';
15
+ app.autoResizeTextarea(input);
16
+ app.addMessage('user', message);
17
+ app.showLoading(true);
18
+ try {
19
+ const response = await app.callMedicalAPI(message);
20
+ app.addMessage('assistant', response);
21
+ app.updateCurrentSession();
22
+ } catch (error) {
23
+ console.error('Error sending message:', error);
24
+ let errorMessage = 'I apologize, but I encountered an error processing your request.';
25
+ if (error.message.includes('500')) errorMessage = 'The server encountered an internal error. Please try again in a moment.';
26
+ else if (error.message.includes('404')) errorMessage = 'The requested service was not found. Please check your connection.';
27
+ else if (error.message.includes('fetch')) errorMessage = 'Unable to connect to the server. Please check your internet connection.';
28
+ app.addMessage('assistant', errorMessage);
29
+ } finally {
30
+ app.showLoading(false);
31
+ }
32
+ };
33
+
34
+ app.callMedicalAPI = async function (message) {
35
+ try {
36
+ const response = await fetch('/chat', {
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/json' },
39
+ body: JSON.stringify({
40
+ user_id: app.currentUser.id,
41
+ patient_id: app.currentPatientId,
42
+ doctor_id: app.currentUser.id,
43
+ session_id: app.currentSession?.id || 'default',
44
+ message: message,
45
+ user_role: app.currentUser.role,
46
+ user_specialty: app.currentUser.specialty,
47
+ title: app.currentSession?.title || 'New Chat'
48
+ })
49
+ });
50
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
51
+ const data = await response.json();
52
+ return data.response || 'I apologize, but I received an empty response. Please try again.';
53
+ } catch (error) {
54
+ console.error('API call failed:', error);
55
+ console.error('Error details:', {
56
+ message: error.message,
57
+ stack: error.stack,
58
+ user: app.currentUser,
59
+ session: app.currentSession,
60
+ patientId: app.currentPatientId
61
+ });
62
+ if (error.name === 'TypeError' && error.message.includes('fetch')) return app.generateMockResponse(message);
63
+ throw error;
64
+ }
65
+ };
66
+
67
+ app.generateMockResponse = function (message) {
68
+ const responses = [
69
+ "Based on your question about medical topics, I can provide general information. However, please remember that this is for educational purposes only and should not replace professional medical advice.",
70
+ "That's an interesting medical question. While I can offer some general insights, it's important to consult with healthcare professionals for personalized medical advice.",
71
+ "I understand your medical inquiry. For accurate diagnosis and treatment recommendations, please consult with qualified healthcare providers who can assess your specific situation.",
72
+ "Thank you for your medical question. I can provide educational information, but medical decisions should always be made in consultation with healthcare professionals.",
73
+ "I appreciate your interest in medical topics. Remember that medical information found online should be discussed with healthcare providers for proper evaluation."
74
+ ];
75
+ return responses[Math.floor(Math.random() * responses.length)];
76
+ };
77
+
78
+ app.addMessage = function (role, content) {
79
+ if (!app.currentSession) app.startNewChat();
80
+ const message = { id: app.generateId(), role, content, timestamp: new Date().toISOString() };
81
+ app.currentSession.messages.push(message);
82
+ app.displayMessage(message);
83
+ if (role === 'user' && app.currentSession.messages.length === 2) app.summariseAndSetTitle(content);
84
+ };
85
+
86
+ app.summariseAndSetTitle = async function (text) {
87
+ try {
88
+ const resp = await fetch('/summarise', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, max_words: 5 }) });
89
+ if (resp.ok) {
90
+ const data = await resp.json();
91
+ const title = (data.title || 'New Chat').trim();
92
+ app.currentSession.title = title;
93
+ app.updateCurrentSession();
94
+ app.updateChatTitle();
95
+ app.loadChatSessions();
96
+ } else {
97
+ const fallback = text.length > 50 ? text.substring(0, 50) + '...' : text;
98
+ app.currentSession.title = fallback;
99
+ app.updateCurrentSession();
100
+ app.updateChatTitle();
101
+ app.loadChatSessions();
102
+ }
103
+ } catch (e) {
104
+ const fallback = text.length > 50 ? text.substring(0, 50) + '...' : text;
105
+ app.currentSession.title = fallback;
106
+ app.updateCurrentSession();
107
+ app.updateChatTitle();
108
+ app.loadChatSessions();
109
+ }
110
+ };
111
+
112
+ app.displayMessage = function (message) {
113
+ const chatMessages = document.getElementById('chatMessages');
114
+ const messageElement = document.createElement('div');
115
+ messageElement.className = `message ${message.role}-message fade-in`;
116
+ messageElement.id = `message-${message.id}`;
117
+ const avatar = message.role === 'user' ? '<i class="fas fa-user"></i>' : '<i class="fas fa-robot"></i>';
118
+ const time = app.formatTime(message.timestamp);
119
+ messageElement.innerHTML = `
120
+ <div class="message-avatar">${avatar}</div>
121
+ <div class="message-content">
122
+ <div class="message-text">${app.formatMessageContent(message.content)}</div>
123
+ <div class="message-time">${time}</div>
124
+ </div>`;
125
+ chatMessages.appendChild(messageElement);
126
+ chatMessages.scrollTop = chatMessages.scrollHeight;
127
+ if (app.currentSession) app.currentSession.lastActivity = new Date().toISOString();
128
+ };
129
+
130
+ app.formatMessageContent = function (content) {
131
+ return content
132
+ // Handle headers (1-6 # symbols)
133
+ .replace(/^#{1,6}\s+(.+)$/gm, (match, text, offset, string) => {
134
+ const level = match.match(/^#+/)[0].length;
135
+ return `<h${level}>${text}</h${level}>`;
136
+ })
137
+ // Handle bold text
138
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
139
+ // Handle italic text
140
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
141
+ // Handle line breaks
142
+ .replace(/\n/g, '<br>')
143
+ // Handle emojis with colors
144
+ .replace(/🔍/g, '<span style="color: var(--primary-color);">🔍</span>')
145
+ .replace(/📋/g, '<span style="color: var(--secondary-color);">📋</span>')
146
+ .replace(/💊/g, '<span style="color: var(--accent-color);">💊</span>')
147
+ .replace(/📚/g, '<span style="color: var(--success-color);">📚</span>')
148
+ .replace(/⚠️/g, '<span style="color: var(--warning-color);">⚠️</span>');
149
+ };
150
+
151
+ app.formatTime = function (timestamp) {
152
+ const date = new Date(timestamp);
153
+ const now = new Date();
154
+ const diff = now - date;
155
+ if (diff < 60000) return 'Just now';
156
+ if (diff < 3600000) { const minutes = Math.floor(diff / 60000); return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; }
157
+ if (diff < 86400000) { const hours = Math.floor(diff / 3600000); return `${hours} hour${hours > 1 ? 's' : ''} ago`; }
158
+ return date.toLocaleDateString();
159
+ };
160
+ }
161
+
162
+
static/js/chat/sessions.js ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // chat/sessions.js
2
+ // Session sidebar rendering, context menu, rename/delete, local storage helpers
3
+
4
+ export function attachSessionsUI(app) {
5
+ app.getChatSessions = function () {
6
+ const sessions = localStorage.getItem(`chatSessions_${app.currentUser.id}`);
7
+ return sessions ? JSON.parse(sessions) : [];
8
+ };
9
+
10
+ app.saveCurrentSession = function () {
11
+ if (!app.currentSession) return;
12
+ if (app.currentSession.source === 'backend') return; // do not persist backend sessions locally here
13
+ const sessions = app.getChatSessions();
14
+ const existingIndex = sessions.findIndex(s => s.id === app.currentSession.id);
15
+ if (existingIndex >= 0) sessions[existingIndex] = { ...app.currentSession };
16
+ else sessions.unshift(app.currentSession);
17
+ localStorage.setItem(`chatSessions_${app.currentUser.id}`, JSON.stringify(sessions));
18
+ };
19
+
20
+ app.updateCurrentSession = function () {
21
+ if (app.currentSession) {
22
+ app.currentSession.lastActivity = new Date().toISOString();
23
+ app.saveCurrentSession();
24
+ }
25
+ };
26
+
27
+ app.updateChatTitle = function () {
28
+ const titleElement = document.getElementById('chatTitle');
29
+ if (app.currentSession) titleElement.textContent = app.currentSession.title; else titleElement.textContent = 'Medical AI Assistant';
30
+ };
31
+
32
+ app.loadChatSession = function (sessionId) {
33
+ const sessions = app.getChatSessions();
34
+ const session = sessions.find(s => s.id === sessionId);
35
+ if (!session) return;
36
+ app.currentSession = session;
37
+ app.clearChatMessages();
38
+ session.messages.forEach(message => app.displayMessage(message));
39
+ app.updateChatTitle();
40
+ app.loadChatSessions();
41
+ };
42
+
43
+ app.deleteChatSession = function (sessionId) {
44
+ const sessions = app.getChatSessions();
45
+ const index = sessions.findIndex(s => s.id === sessionId);
46
+ if (index === -1) return;
47
+ const confirmDelete = confirm('Delete this chat session? This cannot be undone.');
48
+ if (!confirmDelete) return;
49
+ sessions.splice(index, 1);
50
+ localStorage.setItem(`chatSessions_${app.currentUser.id}`, JSON.stringify(sessions));
51
+ if (app.currentSession && app.currentSession.id === sessionId) {
52
+ if (sessions.length > 0) {
53
+ app.currentSession = sessions[0];
54
+ app.clearChatMessages();
55
+ app.currentSession.messages.forEach(m => app.displayMessage(m));
56
+ app.updateChatTitle();
57
+ } else {
58
+ app.currentSession = null;
59
+ app.clearChatMessages();
60
+ app.updateChatTitle();
61
+ }
62
+ }
63
+ app.loadChatSessions();
64
+ };
65
+
66
+ app.renameChatSession = function (sessionId, newTitle) {
67
+ const sessions = app.getChatSessions();
68
+ const idx = sessions.findIndex(s => s.id === sessionId);
69
+ if (idx === -1) return;
70
+ sessions[idx] = { ...sessions[idx], title: newTitle };
71
+ localStorage.setItem(`chatSessions_${app.currentUser.id}`, JSON.stringify(sessions));
72
+ if (app.currentSession && app.currentSession.id === sessionId) {
73
+ app.currentSession.title = newTitle;
74
+ app.updateChatTitle();
75
+ }
76
+ app.loadChatSessions();
77
+ };
78
+
79
+ app.showSessionMenu = function (anchorEl, sessionId) {
80
+ // Remove existing popover
81
+ document.querySelectorAll('.chat-session-menu-popover').forEach(p => p.remove());
82
+ const rect = anchorEl.getBoundingClientRect();
83
+ const pop = document.createElement('div');
84
+ pop.className = 'chat-session-menu-popover show';
85
+ pop.innerHTML = `
86
+ <div class="chat-session-menu-item" data-action="edit" data-session-id="${sessionId}"><i class="fas fa-pen"></i> Edit Name</div>
87
+ <div class="chat-session-menu-item" data-action="delete" data-session-id="${sessionId}"><i class="fas fa-trash"></i> Delete</div>
88
+ `;
89
+ document.body.appendChild(pop);
90
+ pop.style.top = `${rect.bottom + window.scrollY + 6}px`;
91
+ pop.style.left = `${rect.right + window.scrollX - pop.offsetWidth}px`;
92
+ const onDocClick = (ev) => {
93
+ if (!pop.contains(ev.target) && ev.target !== anchorEl) {
94
+ pop.remove();
95
+ document.removeEventListener('click', onDocClick);
96
+ }
97
+ };
98
+ setTimeout(() => document.addEventListener('click', onDocClick), 0);
99
+ pop.querySelectorAll('.chat-session-menu-item').forEach(item => {
100
+ item.addEventListener('click', (e) => {
101
+ const action = item.getAttribute('data-action');
102
+ const id = item.getAttribute('data-session-id');
103
+ if (action === 'delete') app.deleteChatSession(id);
104
+ else if (action === 'edit') {
105
+ app._pendingEditSessionId = id;
106
+ const sessions = app.getChatSessions();
107
+ const s = sessions.find(x => x.id === id);
108
+ const input = document.getElementById('editSessionTitleInput');
109
+ if (input) input.value = s ? s.title : '';
110
+ app.showModal('editTitleModal');
111
+ }
112
+ pop.remove();
113
+ });
114
+ });
115
+ };
116
+
117
+ app.loadChatSessions = function () {
118
+ const sessionsContainer = document.getElementById('chatSessions');
119
+ sessionsContainer.innerHTML = '';
120
+ const sessions = (app.backendSessions && app.backendSessions.length > 0) ? app.backendSessions : app.getChatSessions();
121
+ if (sessions.length === 0) {
122
+ sessionsContainer.innerHTML = '<div class="no-sessions">No chat sessions yet</div>';
123
+ return;
124
+ }
125
+ sessions.forEach(session => {
126
+ const sessionElement = document.createElement('div');
127
+ sessionElement.className = `chat-session ${session.id === app.currentSession?.id ? 'active' : ''}`;
128
+ sessionElement.addEventListener('click', async () => {
129
+ if (session.source === 'backend') {
130
+ app.currentSession = { ...session };
131
+ await app.hydrateMessagesForSession(session.id);
132
+ } else {
133
+ app.loadChatSession(session.id);
134
+ }
135
+ });
136
+ const time = app.formatTime(session.lastActivity);
137
+ sessionElement.innerHTML = `
138
+ <div class="chat-session-row">
139
+ <div class="chat-session-meta">
140
+ <div class="chat-session-title">${session.title}</div>
141
+ <div class="chat-session-time">${time}</div>
142
+ </div>
143
+ <div class="chat-session-actions">
144
+ <button class="chat-session-menu" title="Options" aria-label="Options" data-session-id="${session.id}">
145
+ <i class="fas fa-ellipsis-vertical"></i>
146
+ </button>
147
+ </div>
148
+ </div>
149
+ `;
150
+ sessionsContainer.appendChild(sessionElement);
151
+ const menuBtn = sessionElement.querySelector('.chat-session-menu');
152
+ if (session.source !== 'backend') {
153
+ menuBtn.addEventListener('click', (e) => {
154
+ e.stopPropagation();
155
+ app.showSessionMenu(e.currentTarget, session.id);
156
+ });
157
+ } else {
158
+ menuBtn.disabled = true;
159
+ menuBtn.style.opacity = 0.5;
160
+ menuBtn.title = 'Options available for local sessions only';
161
+ }
162
+ });
163
+ };
164
+ }
static/js/emr.js ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // EMR Page JavaScript
2
+ class PatientEMR {
3
+ constructor() {
4
+ this.patientId = null;
5
+ this.patientData = null;
6
+ this.medications = [];
7
+ this.sessions = [];
8
+
9
+ this.init();
10
+ }
11
+
12
+ async init() {
13
+ // Get patient ID from URL or localStorage
14
+ this.patientId = this.getPatientIdFromURL() || localStorage.getItem('medicalChatbotPatientId');
15
+
16
+ if (!this.patientId) {
17
+ this.showError('No patient selected. Please go back to the main page and select a patient.');
18
+ return;
19
+ }
20
+
21
+ this.setupEventListeners();
22
+ await this.loadPatientData();
23
+ }
24
+
25
+ getPatientIdFromURL() {
26
+ const urlParams = new URLSearchParams(window.location.search);
27
+ return urlParams.get('patient_id');
28
+ }
29
+
30
+ setupEventListeners() {
31
+ // Save button
32
+ document.getElementById('savePatientBtn').addEventListener('click', () => {
33
+ this.savePatientData();
34
+ });
35
+
36
+ // Refresh button
37
+ document.getElementById('refreshPatientBtn').addEventListener('click', () => {
38
+ this.loadPatientData();
39
+ });
40
+
41
+ // Export button
42
+ document.getElementById('exportPatientBtn').addEventListener('click', () => {
43
+ this.exportPatientData();
44
+ });
45
+
46
+ // Add medication button
47
+ document.getElementById('addMedicationBtn').addEventListener('click', () => {
48
+ this.addMedication();
49
+ });
50
+
51
+ // Add medication on Enter key
52
+ document.getElementById('newMedicationInput').addEventListener('keydown', (e) => {
53
+ if (e.key === 'Enter') {
54
+ this.addMedication();
55
+ }
56
+ });
57
+ }
58
+
59
+ async loadPatientData() {
60
+ this.showLoading(true);
61
+
62
+ try {
63
+ // Load patient data
64
+ const patientResp = await fetch(`/patients/${this.patientId}`);
65
+ if (!patientResp.ok) {
66
+ throw new Error('Failed to load patient data');
67
+ }
68
+
69
+ this.patientData = await patientResp.json();
70
+ this.populatePatientForm();
71
+
72
+ // Load patient sessions
73
+ await this.loadPatientSessions();
74
+
75
+ } catch (error) {
76
+ console.error('Error loading patient data:', error);
77
+ this.showError('Failed to load patient data. Please try again.');
78
+ } finally {
79
+ this.showLoading(false);
80
+ }
81
+ }
82
+
83
+ populatePatientForm() {
84
+ if (!this.patientData) return;
85
+
86
+ // Update header
87
+ document.getElementById('patientName').textContent = this.patientData.name || 'Unknown';
88
+ document.getElementById('patientId').textContent = `ID: ${this.patientData.patient_id}`;
89
+
90
+ // Populate form fields
91
+ document.getElementById('patientNameInput').value = this.patientData.name || '';
92
+ document.getElementById('patientAgeInput').value = this.patientData.age || '';
93
+ document.getElementById('patientSexInput').value = this.patientData.sex || '';
94
+ document.getElementById('patientPhoneInput').value = this.patientData.phone || '';
95
+ document.getElementById('patientEmailInput').value = this.patientData.email || '';
96
+ document.getElementById('patientAddressInput').value = this.patientData.address || '';
97
+ document.getElementById('pastAssessmentInput').value = this.patientData.past_assessment_summary || '';
98
+
99
+ // Populate medications
100
+ this.medications = this.patientData.medications || [];
101
+ this.renderMedications();
102
+ }
103
+
104
+ renderMedications() {
105
+ const container = document.getElementById('medicationsList');
106
+ container.innerHTML = '';
107
+
108
+ if (this.medications.length === 0) {
109
+ container.innerHTML = '<div style="color: var(--text-secondary); font-style: italic;">No medications listed</div>';
110
+ return;
111
+ }
112
+
113
+ this.medications.forEach((medication, index) => {
114
+ const tag = document.createElement('div');
115
+ tag.className = 'medication-tag';
116
+ tag.innerHTML = `
117
+ ${medication}
118
+ <button class="remove-medication" data-index="${index}">
119
+ <i class="fas fa-times"></i>
120
+ </button>
121
+ `;
122
+ container.appendChild(tag);
123
+ });
124
+
125
+ // Add event listeners for remove buttons
126
+ container.querySelectorAll('.remove-medication').forEach(btn => {
127
+ btn.addEventListener('click', (e) => {
128
+ const index = parseInt(e.target.closest('.remove-medication').dataset.index);
129
+ this.removeMedication(index);
130
+ });
131
+ });
132
+ }
133
+
134
+ addMedication() {
135
+ const input = document.getElementById('newMedicationInput');
136
+ const medication = input.value.trim();
137
+
138
+ if (!medication) return;
139
+
140
+ this.medications.push(medication);
141
+ this.renderMedications();
142
+ input.value = '';
143
+ }
144
+
145
+ removeMedication(index) {
146
+ this.medications.splice(index, 1);
147
+ this.renderMedications();
148
+ }
149
+
150
+ async loadPatientSessions() {
151
+ try {
152
+ const resp = await fetch(`/patients/${this.patientId}/sessions`);
153
+ if (resp.ok) {
154
+ const data = await resp.json();
155
+ this.sessions = data.sessions || [];
156
+ this.renderSessions();
157
+ }
158
+ } catch (error) {
159
+ console.error('Error loading sessions:', error);
160
+ }
161
+ }
162
+
163
+ renderSessions() {
164
+ const container = document.getElementById('sessionsContainer');
165
+
166
+ if (this.sessions.length === 0) {
167
+ container.innerHTML = '<div style="color: var(--text-secondary); font-style: italic;">No chat sessions found</div>';
168
+ return;
169
+ }
170
+
171
+ container.innerHTML = '';
172
+
173
+ this.sessions.forEach(session => {
174
+ const sessionEl = document.createElement('div');
175
+ sessionEl.className = 'session-item';
176
+ sessionEl.innerHTML = `
177
+ <div class="session-title">${session.title || 'Untitled Session'}</div>
178
+ <div class="session-meta">
179
+ <span class="session-date">${this.formatDate(session.created_at)}</span>
180
+ <span class="session-messages">${session.message_count || 0} messages</span>
181
+ </div>
182
+ `;
183
+
184
+ sessionEl.addEventListener('click', () => {
185
+ // Could open session details or redirect to main page with session
186
+ window.location.href = `/?session_id=${session.session_id}`;
187
+ });
188
+
189
+ container.appendChild(sessionEl);
190
+ });
191
+ }
192
+
193
+ async savePatientData() {
194
+ this.showLoading(true);
195
+
196
+ try {
197
+ const updateData = {
198
+ name: document.getElementById('patientNameInput').value.trim(),
199
+ age: parseInt(document.getElementById('patientAgeInput').value) || null,
200
+ sex: document.getElementById('patientSexInput').value || null,
201
+ phone: document.getElementById('patientPhoneInput').value.trim() || null,
202
+ email: document.getElementById('patientEmailInput').value.trim() || null,
203
+ address: document.getElementById('patientAddressInput').value.trim() || null,
204
+ medications: this.medications,
205
+ past_assessment_summary: document.getElementById('pastAssessmentInput').value.trim() || null
206
+ };
207
+
208
+ const resp = await fetch(`/patients/${this.patientId}`, {
209
+ method: 'PATCH',
210
+ headers: {
211
+ 'Content-Type': 'application/json'
212
+ },
213
+ body: JSON.stringify(updateData)
214
+ });
215
+
216
+ if (resp.ok) {
217
+ this.showSuccess('Patient data saved successfully!');
218
+ // Update the header with new name
219
+ document.getElementById('patientName').textContent = updateData.name || 'Unknown';
220
+ } else {
221
+ throw new Error('Failed to save patient data');
222
+ }
223
+ } catch (error) {
224
+ console.error('Error saving patient data:', error);
225
+ this.showError('Failed to save patient data. Please try again.');
226
+ } finally {
227
+ this.showLoading(false);
228
+ }
229
+ }
230
+
231
+ exportPatientData() {
232
+ if (!this.patientData) {
233
+ this.showError('No patient data to export');
234
+ return;
235
+ }
236
+
237
+ const exportData = {
238
+ patient_id: this.patientData.patient_id,
239
+ name: this.patientData.name,
240
+ age: this.patientData.age,
241
+ sex: this.patientData.sex,
242
+ phone: this.patientData.phone,
243
+ email: this.patientData.email,
244
+ address: this.patientData.address,
245
+ medications: this.medications,
246
+ past_assessment_summary: this.patientData.past_assessment_summary,
247
+ created_at: this.patientData.created_at,
248
+ updated_at: new Date().toISOString(),
249
+ sessions: this.sessions
250
+ };
251
+
252
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
253
+ const url = URL.createObjectURL(blob);
254
+ const a = document.createElement('a');
255
+ a.href = url;
256
+ a.download = `patient-${this.patientData.patient_id}-emr.json`;
257
+ document.body.appendChild(a);
258
+ a.click();
259
+ document.body.removeChild(a);
260
+ URL.revokeObjectURL(url);
261
+ }
262
+
263
+ formatDate(dateString) {
264
+ if (!dateString) return 'Unknown date';
265
+ const date = new Date(dateString);
266
+ return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
267
+ }
268
+
269
+ showLoading(show) {
270
+ const overlay = document.getElementById('loadingOverlay');
271
+ if (overlay) {
272
+ overlay.style.display = show ? 'flex' : 'none';
273
+ }
274
+ }
275
+
276
+ showError(message) {
277
+ alert('Error: ' + message);
278
+ }
279
+
280
+ showSuccess(message) {
281
+ alert('Success: ' + message);
282
+ }
283
+ }
284
+
285
+ // Initialize EMR when DOM is loaded
286
+ document.addEventListener('DOMContentLoaded', () => {
287
+ new PatientEMR();
288
+ });
static/js/patient.js ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const form = document.getElementById('patientForm');
3
+ const result = document.getElementById('result');
4
+ const cancelBtn = document.getElementById('cancelBtn');
5
+ const successModal = document.getElementById('patientSuccessModal');
6
+ const successClose = document.getElementById('patientSuccessClose');
7
+ const successReturn = document.getElementById('patientSuccessReturn');
8
+ const successEdit = document.getElementById('patientSuccessEdit');
9
+ const createdIdEl = document.getElementById('createdPatientId');
10
+ const submitBtn = form?.querySelector('button[type="submit"]');
11
+ const titleEl = document.querySelector('h1');
12
+
13
+ let isEditMode = false;
14
+ let currentPatientId = null;
15
+
16
+ function getPatientIdFromUrl() {
17
+ const urlParams = new URLSearchParams(window.location.search);
18
+ const pidFromUrl = urlParams.get('patient_id');
19
+ if (pidFromUrl && /^\d{8}$/.test(pidFromUrl)) return pidFromUrl;
20
+ return null;
21
+ }
22
+
23
+ async function loadPatientIntoForm(patientId) {
24
+ try {
25
+ const resp = await fetch(`/patients/${patientId}`);
26
+ if (!resp.ok) return;
27
+ const data = await resp.json();
28
+ document.getElementById('name').value = data.name || '';
29
+ document.getElementById('age').value = data.age ?? '';
30
+ document.getElementById('sex').value = data.sex || 'Other';
31
+ document.getElementById('address').value = data.address || '';
32
+ document.getElementById('phone').value = data.phone || '';
33
+ document.getElementById('email').value = data.email || '';
34
+ document.getElementById('medications').value = Array.isArray(data.medications) ? data.medications.join('\n') : '';
35
+ document.getElementById('summary').value = data.past_assessment_summary || '';
36
+ } catch (e) {
37
+ console.warn('Failed to load patient profile for editing', e);
38
+ }
39
+ }
40
+
41
+ function enableEditMode(patientId) {
42
+ isEditMode = true;
43
+ currentPatientId = patientId;
44
+ if (submitBtn) submitBtn.textContent = 'Update';
45
+ if (titleEl) titleEl.textContent = 'Edit Patient';
46
+ }
47
+
48
+ // Initialize: only enter edit mode if patient_id is explicitly in URL
49
+ const pidFromUrl = getPatientIdFromUrl();
50
+ if (pidFromUrl) {
51
+ enableEditMode(pidFromUrl);
52
+ loadPatientIntoForm(pidFromUrl);
53
+ }
54
+
55
+ cancelBtn.addEventListener('click', () => {
56
+ window.location.href = '/';
57
+ });
58
+
59
+ form.addEventListener('submit', async (e) => {
60
+ e.preventDefault();
61
+ result.textContent = '';
62
+ result.style.color = '';
63
+ const payload = {
64
+ name: document.getElementById('name').value.trim(),
65
+ age: parseInt(document.getElementById('age').value, 10),
66
+ sex: document.getElementById('sex').value,
67
+ address: document.getElementById('address').value.trim() || null,
68
+ phone: document.getElementById('phone').value.trim() || null,
69
+ email: document.getElementById('email').value.trim() || null,
70
+ medications: document.getElementById('medications').value.split('\n').map(s => s.trim()).filter(Boolean),
71
+ past_assessment_summary: document.getElementById('summary').value.trim() || null
72
+ };
73
+ try {
74
+ if (isEditMode && currentPatientId) {
75
+ const resp = await fetch(`/patients/${currentPatientId}`, {
76
+ method: 'PATCH',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify(payload)
79
+ });
80
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
81
+ result.textContent = 'Patient updated successfully.';
82
+ result.style.color = 'green';
83
+ } else {
84
+ const resp = await fetch('/patients', {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify(payload)
88
+ });
89
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
90
+ const data = await resp.json();
91
+ const pid = data.patient_id;
92
+ localStorage.setItem('medicalChatbotPatientId', pid);
93
+
94
+ // Add to localStorage for future suggestions
95
+ const existingPatients = JSON.parse(localStorage.getItem('medicalChatbotPatients') || '[]');
96
+ const newPatient = {
97
+ patient_id: pid,
98
+ name: payload.name,
99
+ age: payload.age,
100
+ sex: payload.sex
101
+ };
102
+ // Check if patient already exists to avoid duplicates
103
+ const exists = existingPatients.some(p => p.patient_id === pid);
104
+ if (!exists) {
105
+ existingPatients.push(newPatient);
106
+ localStorage.setItem('medicalChatbotPatients', JSON.stringify(existingPatients));
107
+ }
108
+
109
+ // Show success modal (stay in create view until user opts to edit)
110
+ if (createdIdEl) createdIdEl.textContent = pid;
111
+ successModal.classList.add('show');
112
+ }
113
+ } catch (err) {
114
+ console.error(err);
115
+ result.textContent = isEditMode ? 'Failed to update patient. Please try again.' : 'Failed to create patient. Please try again.';
116
+ result.style.color = 'crimson';
117
+ }
118
+ });
119
+
120
+ // Success modal wiring
121
+ if (successClose) successClose.addEventListener('click', () => successModal.classList.remove('show'));
122
+ if (successReturn) successReturn.addEventListener('click', () => { window.location.href = '/'; });
123
+ if (successEdit) successEdit.addEventListener('click', () => {
124
+ successModal.classList.remove('show');
125
+ const pid = createdIdEl?.textContent?.trim() || localStorage.getItem('medicalChatbotPatientId');
126
+ if (pid && /^\d{8}$/.test(pid)) {
127
+ enableEditMode(pid);
128
+ loadPatientIntoForm(pid);
129
+ }
130
+ });
131
+ });
static/js/ui/doctor.js ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // Doctor modal open/close wiring
108
+ document.addEventListener('DOMContentLoaded', () => {
109
+ const doctorCard = document.getElementById('userProfile');
110
+ const userModal = document.getElementById('userModal');
111
+ const closeBtn = document.getElementById('userModalClose');
112
+ const cancelBtn = document.getElementById('userModalCancel');
113
+ if (doctorCard && userModal) {
114
+ doctorCard.addEventListener('click', () => userModal.classList.add('show'));
115
+ }
116
+ if (closeBtn) closeBtn.addEventListener('click', () => userModal.classList.remove('show'));
117
+ if (cancelBtn) cancelBtn.addEventListener('click', () => userModal.classList.remove('show'));
118
+ if (userModal) {
119
+ userModal.addEventListener('click', (e) => { if (e.target === userModal) userModal.classList.remove('show'); });
120
+ }
121
+ });
122
+ }
123
+
124
+
static/js/ui/handlers.js ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ console.log('[DEBUG] toggleSidebar called');
9
+ if (sidebar) {
10
+ const wasOpen = sidebar.classList.contains('show');
11
+ sidebar.classList.toggle('show');
12
+ const isNowOpen = sidebar.classList.contains('show');
13
+ console.log('[DEBUG] Sidebar toggled - was open:', wasOpen, 'now open:', isNowOpen);
14
+ } else {
15
+ console.error('[DEBUG] Sidebar element not found');
16
+ }
17
+ };
18
+
19
+ // Textarea autosize
20
+ app.autoResizeTextarea = function (textarea) {
21
+ if (!textarea) return;
22
+ textarea.style.height = 'auto';
23
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
24
+ };
25
+
26
+ // Export current chat as JSON
27
+ app.exportChat = function () {
28
+ if (!app.currentSession || app.currentSession.messages.length === 0) {
29
+ alert('No chat to export.');
30
+ return;
31
+ }
32
+ const chatData = {
33
+ user: app.currentUser?.name || 'Unknown',
34
+ session: app.currentSession.title,
35
+ date: new Date().toISOString(),
36
+ messages: app.currentSession.messages
37
+ };
38
+ const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' });
39
+ const url = URL.createObjectURL(blob);
40
+ const a = document.createElement('a');
41
+ a.href = url;
42
+ a.download = `medical-chat-${app.currentSession.title.replace(/[^a-z0-9]/gi, '-')}.json`;
43
+ document.body.appendChild(a);
44
+ a.click();
45
+ document.body.removeChild(a);
46
+ URL.revokeObjectURL(url);
47
+ };
48
+
49
+ // Clear current chat
50
+ app.clearChat = function () {
51
+ if (confirm('Are you sure you want to clear this chat? This action cannot be undone.')) {
52
+ app.clearChatMessages();
53
+ if (app.currentSession) {
54
+ app.currentSession.messages = [];
55
+ app.currentSession.title = 'New Chat';
56
+ app.updateChatTitle();
57
+ }
58
+ }
59
+ };
60
+
61
+ // Generic modal helpers
62
+ app.showModal = function (modalId) {
63
+ document.getElementById(modalId)?.classList.add('show');
64
+ };
65
+ app.hideModal = function (modalId) {
66
+ document.getElementById(modalId)?.classList.remove('show');
67
+ };
68
+ }
static/js/ui/patient.js ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ console.log('[DEBUG] loadPatient called');
27
+ const input = document.getElementById('patientIdInput');
28
+ const status = document.getElementById('patientStatus');
29
+ const id = (input?.value || '').trim();
30
+ console.log('[DEBUG] Patient ID from input:', id);
31
+ if (!/^\d{8}$/.test(id)) {
32
+ console.log('[DEBUG] Invalid patient ID format');
33
+ if (status) { status.textContent = 'Invalid patient ID. Use 8 digits.'; status.style.color = 'var(--warning-color)'; }
34
+ return;
35
+ }
36
+ console.log('[DEBUG] Setting current patient ID:', id);
37
+ app.currentPatientId = id;
38
+ app.savePatientId();
39
+ if (status) { status.textContent = `Patient: ${id}`; status.style.color = 'var(--text-secondary)'; }
40
+ await app.fetchAndRenderPatientSessions();
41
+ };
42
+
43
+ app.fetchAndRenderPatientSessions = async function () {
44
+ if (!app.currentPatientId) return;
45
+ try {
46
+ const resp = await fetch(`/patients/${app.currentPatientId}/sessions`);
47
+ if (resp.ok) {
48
+ const data = await resp.json();
49
+ const sessions = Array.isArray(data.sessions) ? data.sessions : [];
50
+ app.backendSessions = sessions.map(s => ({
51
+ id: s.session_id,
52
+ title: s.title || 'New Chat',
53
+ messages: [],
54
+ createdAt: s.created_at || new Date().toISOString(),
55
+ lastActivity: s.last_activity || new Date().toISOString(),
56
+ source: 'backend'
57
+ }));
58
+ if (app.backendSessions.length > 0) {
59
+ app.currentSession = app.backendSessions[0];
60
+ await app.hydrateMessagesForSession(app.currentSession.id);
61
+ }
62
+ } else {
63
+ console.warn('Failed to fetch patient sessions', resp.status);
64
+ app.backendSessions = [];
65
+ }
66
+ } catch (e) {
67
+ console.error('Failed to load patient sessions', e);
68
+ app.backendSessions = [];
69
+ }
70
+ app.loadChatSessions();
71
+ };
72
+
73
+ app.hydrateMessagesForSession = async function (sessionId) {
74
+ try {
75
+ const resp = await fetch(`/sessions/${sessionId}/messages?patient_id=${app.currentPatientId}&limit=1000`);
76
+ if (!resp.ok) return;
77
+ const data = await resp.json();
78
+ const msgs = Array.isArray(data.messages) ? data.messages : [];
79
+ const normalized = msgs.map(m => ({
80
+ id: m._id || app.generateId(),
81
+ role: m.role,
82
+ content: m.content,
83
+ timestamp: m.timestamp
84
+ }));
85
+ if (app.currentSession && app.currentSession.id === sessionId) {
86
+ app.currentSession.messages = normalized;
87
+ app.clearChatMessages();
88
+ app.currentSession.messages.forEach(m => app.displayMessage(m));
89
+ app.updateChatTitle();
90
+ }
91
+ } catch (e) {
92
+ console.error('Failed to hydrate session messages', e);
93
+ }
94
+ };
95
+
96
+ // Bind patient input + typeahead + load button
97
+ app.bindPatientHandlers = function () {
98
+ console.log('[DEBUG] bindPatientHandlers called');
99
+ const loadBtn = document.getElementById('loadPatientBtn');
100
+ console.log('[DEBUG] Load button found:', !!loadBtn);
101
+ if (loadBtn) loadBtn.addEventListener('click', () => app.loadPatient());
102
+ const patientInput = document.getElementById('patientIdInput');
103
+ const suggestionsEl = document.getElementById('patientSuggestions');
104
+ console.log('[DEBUG] Patient input found:', !!patientInput);
105
+ console.log('[DEBUG] Suggestions element found:', !!suggestionsEl);
106
+ if (!patientInput) return;
107
+ let debounceTimer;
108
+ const hideSuggestions = () => { if (suggestionsEl) suggestionsEl.style.display = 'none'; };
109
+ const renderSuggestions = (items) => {
110
+ if (!suggestionsEl) return;
111
+ if (!items || items.length === 0) { hideSuggestions(); return; }
112
+ suggestionsEl.innerHTML = '';
113
+ items.forEach(p => {
114
+ const div = document.createElement('div');
115
+ div.className = 'patient-suggestion';
116
+ div.textContent = `${p.name || 'Unknown'} (${p.patient_id})`;
117
+ div.addEventListener('click', async () => {
118
+ app.currentPatientId = p.patient_id;
119
+ app.savePatientId();
120
+ patientInput.value = p.patient_id;
121
+ hideSuggestions();
122
+ const status = document.getElementById('patientStatus');
123
+ if (status) { status.textContent = `Patient: ${p.patient_id}`; status.style.color = 'var(--text-secondary)'; }
124
+ await app.fetchAndRenderPatientSessions();
125
+ });
126
+ suggestionsEl.appendChild(div);
127
+ });
128
+ suggestionsEl.style.display = 'block';
129
+ };
130
+ patientInput.addEventListener('input', () => {
131
+ const q = patientInput.value.trim();
132
+ console.log('[DEBUG] Patient input changed:', q);
133
+ clearTimeout(debounceTimer);
134
+ if (!q) { hideSuggestions(); return; }
135
+ debounceTimer = setTimeout(async () => {
136
+ try {
137
+ console.log('[DEBUG] Searching patients with query:', q);
138
+ const resp = await fetch(`/patients/search?q=${encodeURIComponent(q)}&limit=8`, { headers: { 'Accept': 'application/json' } });
139
+ console.log('[DEBUG] Search response status:', resp.status);
140
+ if (resp.ok) {
141
+ const data = await resp.json();
142
+ console.log('[DEBUG] Search results:', data);
143
+ renderSuggestions(data.results || []);
144
+ } else {
145
+ console.warn('Search request failed', resp.status);
146
+ }
147
+ } catch (e) {
148
+ console.error('[DEBUG] Search error:', e);
149
+ }
150
+ }, 200);
151
+ });
152
+ patientInput.addEventListener('keydown', async (e) => {
153
+ if (e.key === 'Enter') {
154
+ const value = patientInput.value.trim();
155
+ console.log('[DEBUG] Patient input Enter pressed with value:', value);
156
+ if (/^\d{8}$/.test(value)) {
157
+ console.log('[DEBUG] Loading patient with 8-digit ID');
158
+ await app.loadPatient();
159
+ hideSuggestions();
160
+ } else {
161
+ console.log('[DEBUG] Searching for patient by name/partial ID');
162
+ try {
163
+ const resp = await fetch(`/patients/search?q=${encodeURIComponent(value)}&limit=1`);
164
+ console.log('[DEBUG] Search response status:', resp.status);
165
+ if (resp.ok) {
166
+ const data = await resp.json();
167
+ console.log('[DEBUG] Search results for Enter:', data);
168
+ const first = (data.results || [])[0];
169
+ if (first) {
170
+ console.log('[DEBUG] Found patient, setting as current:', first);
171
+ app.currentPatientId = first.patient_id;
172
+ app.savePatientId();
173
+ patientInput.value = first.patient_id;
174
+ hideSuggestions();
175
+ const status = document.getElementById('patientStatus');
176
+ if (status) { status.textContent = `Patient: ${first.patient_id}`; status.style.color = 'var(--text-secondary)'; }
177
+ await app.fetchAndRenderPatientSessions();
178
+ return;
179
+ }
180
+ }
181
+ } catch (e) {
182
+ console.error('[DEBUG] Search error on Enter:', e);
183
+ }
184
+ const status = document.getElementById('patientStatus');
185
+ if (status) { status.textContent = 'No matching patient found'; status.style.color = 'var(--warning-color)'; }
186
+ }
187
+ }
188
+ });
189
+ document.addEventListener('click', (ev) => {
190
+ if (!suggestionsEl) return;
191
+ if (!suggestionsEl.contains(ev.target) && ev.target !== patientInput) hideSuggestions();
192
+ });
193
+ };
194
+
195
+ // Patient modal wiring
196
+ document.addEventListener('DOMContentLoaded', () => {
197
+ const profileBtn = document.getElementById('patientMenuBtn');
198
+ const modal = document.getElementById('patientModal');
199
+ const closeBtn = document.getElementById('patientModalClose');
200
+ const logoutBtn = document.getElementById('patientLogoutBtn');
201
+ const createBtn = document.getElementById('patientCreateBtn');
202
+ if (profileBtn && modal) {
203
+ profileBtn.addEventListener('click', async () => {
204
+ const pid = app?.currentPatientId;
205
+ if (pid) {
206
+ try {
207
+ const resp = await fetch(`/patients/${pid}`);
208
+ if (resp.ok) {
209
+ const p = await resp.json();
210
+ const name = p.name || 'Unknown';
211
+ const age = typeof p.age === 'number' ? p.age : '-';
212
+ const sex = p.sex || '-';
213
+ const meds = Array.isArray(p.medications) && p.medications.length > 0 ? p.medications.join(', ') : '-';
214
+ document.getElementById('patientSummary').textContent = `${name} — ${sex}, ${age}`;
215
+ document.getElementById('patientMedications').textContent = meds;
216
+ document.getElementById('patientAssessment').textContent = p.past_assessment_summary || '-';
217
+ }
218
+ } catch (e) {
219
+ console.error('Failed to load patient profile', e);
220
+ }
221
+ }
222
+ modal.classList.add('show');
223
+ });
224
+ }
225
+ if (closeBtn && modal) {
226
+ closeBtn.addEventListener('click', () => modal.classList.remove('show'));
227
+ modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('show'); });
228
+ }
229
+ if (logoutBtn) {
230
+ logoutBtn.addEventListener('click', () => {
231
+ if (confirm('Log out current patient?')) {
232
+ app.currentPatientId = null;
233
+ localStorage.removeItem('medicalChatbotPatientId');
234
+ const status = document.getElementById('patientStatus');
235
+ if (status) { status.textContent = 'No patient selected'; status.style.color = 'var(--text-secondary)'; }
236
+ const input = document.getElementById('patientIdInput');
237
+ if (input) input.value = '';
238
+ modal.classList.remove('show');
239
+ }
240
+ });
241
+ }
242
+ if (createBtn) createBtn.addEventListener('click', () => modal.classList.remove('show'));
243
+ });
244
+ }
245
+
246
+
static/js/ui/settings.js ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ui/settings.js
2
+ // Settings UI: theme/font preferences, showLoading overlay, settings modal wiring
3
+
4
+ export function attachSettingsUI(app) {
5
+ app.loadUserPreferences = function () {
6
+ const preferences = localStorage.getItem('medicalChatbotPreferences');
7
+ if (preferences) {
8
+ const prefs = JSON.parse(preferences);
9
+ app.setTheme(prefs.theme || 'auto');
10
+ app.setFontSize(prefs.fontSize || 'medium');
11
+ }
12
+ };
13
+
14
+ app.setupTheme = function () {
15
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
16
+ app.setTheme('auto');
17
+ }
18
+ };
19
+
20
+ app.setTheme = function (theme) {
21
+ const root = document.documentElement;
22
+ if (theme === 'auto') {
23
+ const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
24
+ root.setAttribute('data-theme', isDark ? 'dark' : 'light');
25
+ } else {
26
+ root.setAttribute('data-theme', theme);
27
+ }
28
+ const sel = document.getElementById('themeSelect');
29
+ if (sel) sel.value = theme;
30
+ app.savePreferences();
31
+ };
32
+
33
+ app.setFontSize = function (size) {
34
+ const root = document.documentElement;
35
+ root.style.fontSize = size === 'small' ? '14px' : size === 'large' ? '18px' : '16px';
36
+ app.savePreferences();
37
+ };
38
+
39
+ app.savePreferences = function () {
40
+ const preferences = {
41
+ theme: document.getElementById('themeSelect')?.value,
42
+ fontSize: document.getElementById('fontSize')?.value,
43
+ autoSave: document.getElementById('autoSave')?.checked,
44
+ notifications: document.getElementById('notifications')?.checked
45
+ };
46
+ localStorage.setItem('medicalChatbotPreferences', JSON.stringify(preferences));
47
+ };
48
+
49
+ app.showLoading = function (show) {
50
+ app.isLoading = show;
51
+ const overlay = document.getElementById('loadingOverlay');
52
+ const sendBtn = document.getElementById('sendBtn');
53
+ if (!overlay || !sendBtn) return;
54
+ if (show) {
55
+ overlay.classList.add('show');
56
+ sendBtn.disabled = true;
57
+ } else {
58
+ overlay.classList.remove('show');
59
+ sendBtn.disabled = false;
60
+ }
61
+ };
62
+
63
+ // Settings modal open/close wiring
64
+ document.addEventListener('DOMContentLoaded', () => {
65
+ const settingsBtn = document.getElementById('settingsBtn');
66
+ const modal = document.getElementById('settingsModal');
67
+ const closeBtn = document.getElementById('settingsModalClose');
68
+ const cancelBtn = document.getElementById('settingsModalCancel');
69
+ if (settingsBtn && modal) settingsBtn.addEventListener('click', () => modal.classList.add('show'));
70
+ if (closeBtn) closeBtn.addEventListener('click', () => modal.classList.remove('show'));
71
+ if (cancelBtn) cancelBtn.addEventListener('click', () => modal.classList.remove('show'));
72
+ if (modal) modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('show'); });
73
+ });
74
+ }
static/patient.html ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Create Patient</title>
7
+ <link rel="stylesheet" href="/static/css/patient.css">
8
+ <link rel="icon" type="image/svg+xml" href="/static/icon.svg">
9
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
10
+ </head>
11
+ <body>
12
+ <div class="container">
13
+ <a href="/" class="back-link"><i class="fas fa-arrow-left"></i> Back to Assistant</a>
14
+ <h1>Create Patient</h1>
15
+ <form id="patientForm">
16
+ <div class="grid">
17
+ <label>Name<input type="text" id="name" required></label>
18
+ <label>Age<input type="number" id="age" min="0" max="130" required></label>
19
+ <label>Sex
20
+ <select id="sex" required>
21
+ <option value="Male">Male</option>
22
+ <option value="Female">Female</option>
23
+ <option value="Other">Other</option>
24
+ </select>
25
+ </label>
26
+ <label>Phone<input type="tel" id="phone"></label>
27
+ <label>Email<input type="email" id="email"></label>
28
+ <label>Address<input type="text" id="address"></label>
29
+ <label>Active Medications<textarea id="medications" placeholder="One per line"></textarea></label>
30
+ <label>Past Assessment Summary<textarea id="summary"></textarea></label>
31
+ </div>
32
+ <div class="actions">
33
+ <button type="button" id="cancelBtn" class="secondary">Cancel</button>
34
+ <button type="submit" class="primary">Create</button>
35
+ </div>
36
+ </form>
37
+ <div id="result" class="result"></div>
38
+ </div>
39
+ <!-- Success Modal -->
40
+ <div class="modal" id="patientSuccessModal">
41
+ <div class="modal-content">
42
+ <div class="modal-header">
43
+ <h3>Patient Created</h3>
44
+ <button class="modal-close" id="patientSuccessClose">&times;</button>
45
+ </div>
46
+ <div class="modal-body">
47
+ <p>Your new Patient ID is:</p>
48
+ <div id="createdPatientId" class="big-id"></div>
49
+ </div>
50
+ <div class="modal-footer">
51
+ <button class="secondary" id="patientSuccessReturn">Return to main page</button>
52
+ <button class="primary" id="patientSuccessEdit">Edit patient profile</button>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <script src="/static/js/patient.js"></script>
58
+ </body>
59
+ </html>