Merge branch 'hf_main' into hf_mongo
Browse files- README.md +137 -275
 - TODO.md +20 -0
 - UI_SETUP.md +147 -0
 - src/api/routes/chat.py +22 -10
 - src/api/routes/session.py +49 -4
 - src/api/routes/user.py +134 -2
 - src/core/memory/history.py +322 -0
 - src/core/memory/memory.py +210 -0
 - src/core/state.py +2 -0
 - src/data/__init__.py +55 -0
 - src/data/connection.py +57 -0
 - src/data/medical/__init__.py +20 -0
 - src/data/medical/operations.py +133 -0
 - src/data/medical_kb.py +2 -1
 - src/data/message/__init__.py +18 -0
 - src/data/message/operations.py +124 -0
 - src/data/mongodb.py.backup +638 -0
 - src/data/patient/__init__.py +18 -0
 - src/data/patient/operations.py +101 -0
 - src/data/session/__init__.py +24 -0
 - src/data/session/operations.py +126 -0
 - src/data/user/__init__.py +24 -0
 - src/data/user/operations.py +146 -0
 - src/data/utils.py +48 -0
 - src/models/chat.py +4 -0
 - src/models/user.py +31 -2
 - start.py +19 -3
 - static/css/emr.css +340 -0
 - static/css/patient.css +22 -0
 - static/css/styles.css +146 -1
 - static/emr.html +127 -0
 - static/index.html +64 -12
 - static/js/app.js +0 -0
 - static/js/chat/messaging.js +162 -0
 - static/js/chat/sessions.js +164 -0
 - static/js/emr.js +288 -0
 - static/js/patient.js +131 -0
 - static/js/ui/doctor.js +124 -0
 - static/js/ui/handlers.js +68 -0
 - static/js/ui/patient.js +246 -0
 - static/js/ui/settings.js +74 -0
 - static/patient.html +59 -0
 
    	
        README.md
    CHANGED
    
    | 
         @@ -13,310 +13,172 @@ app_port: 7860 
     | 
|
| 13 | 
         | 
| 14 | 
         
             
            # Medical AI Assistant
         
     | 
| 15 | 
         | 
| 16 | 
         
            -
             
     | 
| 17 | 
         | 
| 18 | 
         
            -
            ## 🚀 Features
         
     | 
| 19 | 
         | 
| 20 | 
         
            -
             
     | 
| 21 | 
         
            -
            -  
     | 
| 22 | 
         
            -
            -  
     | 
| 23 | 
         
            -
            -  
     | 
| 24 | 
         
            -
            -  
     | 
| 25 | 
         
            -
            -  
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 26 | 
         | 
| 27 | 
         
            -
             
     | 
| 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,  
     | 
| 36 | 
         
            -
             
     | 
| 
         | 
|
| 
         | 
|
| 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 | 
         
            -
             
     | 
| 41 | 
         
            -
             
     | 
| 42 | 
         
            -
             
     | 
| 43 | 
         
            -
             
     | 
| 44 | 
         
            -
             
     | 
| 45 | 
         
            -
             
     | 
| 46 | 
         
            -
             
     | 
| 47 | 
         
            -
             
     | 
| 48 | 
         
            -
             
     | 
| 49 | 
         
            -
             
     | 
| 50 | 
         
            -
             
     | 
| 51 | 
         
            -
             
     | 
| 52 | 
         
            -
             
     | 
| 53 | 
         
            -
             
     | 
| 54 | 
         
            -
             
     | 
| 55 | 
         
            -
             
     | 
| 56 | 
         
            -
             
     | 
| 57 | 
         
            -
             
     | 
| 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. 
     | 
| 75 | 
         
            -
            - pip 
     | 
| 76 | 
         
            -
            - Modern web browser
         
     | 
| 77 | 
         | 
| 78 | 
         
             
            ### Setup
         
     | 
| 79 | 
         
            -
            1.  
     | 
| 80 | 
         
            -
             
     | 
| 81 | 
         
            -
             
     | 
| 82 | 
         
            -
             
     | 
| 83 | 
         
            -
             
     | 
| 84 | 
         
            -
             
     | 
| 85 | 
         
            -
            2.  
     | 
| 86 | 
         
            -
             
     | 
| 87 | 
         
            -
             
     | 
| 88 | 
         
            -
             
     | 
| 89 | 
         
            -
             
     | 
| 90 | 
         
            -
             
     | 
| 91 | 
         
            -
             
     | 
| 92 | 
         
            -
             
     | 
| 93 | 
         
            -
             
     | 
| 94 | 
         
            -
             
     | 
| 95 | 
         
            -
             
     | 
| 96 | 
         
            -
             
     | 
| 97 | 
         
            -
             
     | 
| 98 | 
         
            -
             
     | 
| 99 | 
         
            -
             
     | 
| 100 | 
         
            -
             
     | 
| 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 | 
         
            -
             
     | 
| 119 | 
         
            -
            -  
     | 
| 120 | 
         
            -
            -  
     | 
| 121 | 
         
            -
            -  
     | 
| 
         | 
|
| 122 | 
         | 
| 123 | 
         
             
            ## 📱 Usage
         
     | 
| 124 | 
         
            -
             
     | 
| 125 | 
         
            -
             
     | 
| 126 | 
         
            -
             
     | 
| 127 | 
         
            -
             
     | 
| 128 | 
         
            -
             
     | 
| 129 | 
         
            -
             
     | 
| 130 | 
         
            -
             
     | 
| 131 | 
         
            -
             
     | 
| 132 | 
         
            -
             
     | 
| 133 | 
         
            -
            -  
     | 
| 134 | 
         
            -
            -  
     | 
| 135 | 
         
            -
            -  
     | 
| 136 | 
         
            -
            -  
     | 
| 137 | 
         
            -
             
     | 
| 138 | 
         
            -
             
     | 
| 139 | 
         
            -
             
     | 
| 140 | 
         
            -
            -  
     | 
| 141 | 
         
            -
            -  
     | 
| 142 | 
         
            -
             
     | 
| 143 | 
         
            -
             
     | 
| 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 | 
         
            -
            #  
     | 
| 167 | 
         
            -
             
     | 
| 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 | 
         
            -
             
     | 
| 182 | 
         
            -
            ├── scripts/
         
     | 
| 183 | 
         
            -
            │   └── download_model.py        # Model downloading script
         
     | 
| 184 | 
         
             
            ├── src/
         
     | 
| 185 | 
         
             
            │   ├── api/
         
     | 
| 186 | 
         
            -
            │   │   └── routes/ 
     | 
| 187 | 
         
            -
            │   │       ├── chat.py
         
     | 
| 188 | 
         
            -
            │   │       ├── session.py
         
     | 
| 189 | 
         
            -
            │   │       ├──  
     | 
| 190 | 
         
            -
            │   │       ├── system.py
         
     | 
| 191 | 
         
            -
            │   │       └──  
     | 
| 192 | 
         
             
            │   ├── core/
         
     | 
| 193 | 
         
            -
            │   │   ├── memory/ 
     | 
| 194 | 
         
            -
            │   │   │   ├── memory.py
         
     | 
| 195 | 
         
            -
            │   │   │   └── history.py
         
     | 
| 196 | 
         
            -
            │   │   └── state.py             #  
     | 
| 197 | 
         
            -
            │   ├──  
     | 
| 198 | 
         
            -
            │   │    
     | 
| 199 | 
         
            -
            │   │ 
     | 
| 200 | 
         
            -
            │   ├──  
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 201 | 
         
             
            │   │   ├── chat.py
         
     | 
| 202 | 
         
             
            │   │   └── user.py
         
     | 
| 203 | 
         
            -
            │   ├── services/ 
     | 
| 204 | 
         
            -
            │   │   ├── medical_response.py
         
     | 
| 205 | 
         
            -
            │   │   └── summariser.py
         
     | 
| 206 | 
         
            -
            │   ├── utils/ 
     | 
| 207 | 
         
            -
            │   │   ├── __init__.py
         
     | 
| 208 | 
         
             
            │   │   ├── embeddings.py
         
     | 
| 209 | 
         
             
            │   │   ├── logger.py
         
     | 
| 210 | 
         
            -
            │   │   ├── naming.py
         
     | 
| 211 | 
         
             
            │   │   └── rotator.py
         
     | 
| 212 | 
         
            -
            │    
     | 
| 213 | 
         
            -
            ├── static/ 
     | 
| 214 | 
         
            -
            │   ├──  
     | 
| 215 | 
         
            -
            │   │   ├── app.js
         
     | 
| 216 | 
         
            -
            │   │   └── health.js
         
     | 
| 217 | 
         
             
            │   ├── css/
         
     | 
| 218 | 
         
            -
            │   │    
     | 
| 219 | 
         
            -
            │    
     | 
| 220 | 
         
            -
            │   ├──  
     | 
| 221 | 
         
            -
            │    
     | 
| 222 | 
         
            -
             
     | 
| 223 | 
         
            -
             
     | 
| 224 | 
         
            -
            ├── . 
     | 
| 225 | 
         
            -
            ├──  
     | 
| 226 | 
         
             
            ├── Dockerfile
         
     | 
| 227 | 
         
            -
            ├──  
     | 
| 228 | 
         
            -
            ├──  
     | 
| 229 | 
         
            -
             
     | 
| 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 | 
         
            -
             
     | 
| 262 | 
         
            -
            -  
     | 
| 263 | 
         
            -
            -  
     | 
| 264 | 
         
            -
            -  
     | 
| 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 | 
         
            -
            	""" 
     | 
| 
         | 
|
| 
         | 
|
| 22 | 
         
             
            	start_time = time.time()
         
     | 
| 23 | 
         | 
| 24 | 
         
             
            	try:
         
     | 
| 25 | 
         
            -
            		logger().info(f" 
     | 
| 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 | 
         
            -
            		#  
     | 
| 47 | 
         
            -
            		 
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 48 | 
         
             
            			request.user_id,
         
     | 
| 49 | 
         
            -
            			 
     | 
| 50 | 
         
            -
            			 
     | 
| 
         | 
|
| 
         | 
|
| 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(), 
     | 
| 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  
     | 
| 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 | 
         
            -
             
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 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 | 
         
            -
            		 
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 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  
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 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:  
     | 
| 41 | 
         
            -
            	print("📚 API documentation at:  
     | 
| 42 | 
         
            -
            	print("🔍 Health check at:  
     | 
| 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:  
     | 
| 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> 
     | 
| 126 | 
         
             
                            <button class="modal-close" id="userModalClose">×</button>
         
     | 
| 127 | 
         
             
                        </div>
         
     | 
| 128 | 
         
             
                        <div class="modal-body">
         
     | 
| 129 | 
         
             
                            <div class="form-group">
         
     | 
| 130 | 
         
            -
                                <label for=" 
     | 
| 131 | 
         
            -
                                < 
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 132 | 
         
             
                            </div>
         
     | 
| 133 | 
         
             
                            <div class="form-group">
         
     | 
| 134 | 
         
             
                                <label for="profileRole">Medical Role:</label>
         
     | 
| 135 | 
         
             
                                <select id="profileRole">
         
     | 
| 136 | 
         
            -
                                    <option value=" 
     | 
| 
         | 
|
| 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 | 
         
            -
                 
     | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 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">×</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">×</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">×</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>
         
     |