dylanglenister commited on
Commit
8f3eae5
·
2 Parent(s): ba4c23e fc4a3e4

Merge remote-tracking branch 'space/emr' into hf_main

Browse files
.dockerignore CHANGED
@@ -4,3 +4,4 @@
4
  .venv
5
  __pycache__
6
  *.pyc
 
 
4
  .venv
5
  __pycache__
6
  *.pyc
7
+ key
.gitignore CHANGED
@@ -9,4 +9,5 @@
9
  **/.env
10
  **.DS_STORE
11
 
12
- review_vi.txt
 
 
9
  **/.env
10
  **.DS_STORE
11
 
12
+ review_vi.txt
13
+ key
schemas/emr_validator.json ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$jsonSchema": {
3
+ "bsonType": "object",
4
+ "title": "EMR validator",
5
+ "required": [
6
+ "patient_id",
7
+ "doctor_id",
8
+ "message_id",
9
+ "session_id",
10
+ "extracted_data",
11
+ "created_at",
12
+ "updated_at"
13
+ ],
14
+ "properties": {
15
+ "patient_id": {
16
+ "bsonType": "string",
17
+ "description": "ID of the patient this EMR entry belongs to"
18
+ },
19
+ "doctor_id": {
20
+ "bsonType": "string",
21
+ "description": "ID of the doctor who triggered the EMR extraction"
22
+ },
23
+ "message_id": {
24
+ "bsonType": "string",
25
+ "description": "ID of the message that was analyzed for EMR extraction"
26
+ },
27
+ "session_id": {
28
+ "bsonType": "string",
29
+ "description": "ID of the chat session this EMR entry belongs to"
30
+ },
31
+ "original_message": {
32
+ "bsonType": "string",
33
+ "description": "The original message content that was analyzed"
34
+ },
35
+ "extracted_data": {
36
+ "bsonType": "object",
37
+ "properties": {
38
+ "diagnosis": {
39
+ "bsonType": "array",
40
+ "items": {"bsonType": "string"},
41
+ "description": "List of diagnoses identified"
42
+ },
43
+ "symptoms": {
44
+ "bsonType": "array",
45
+ "items": {"bsonType": "string"},
46
+ "description": "List of symptoms mentioned"
47
+ },
48
+ "medications": {
49
+ "bsonType": "array",
50
+ "items": {
51
+ "bsonType": "object",
52
+ "properties": {
53
+ "name": {"bsonType": "string"},
54
+ "dosage": {"bsonType": "string"},
55
+ "frequency": {"bsonType": "string"},
56
+ "duration": {"bsonType": "string"}
57
+ }
58
+ },
59
+ "description": "List of medications prescribed or mentioned"
60
+ },
61
+ "vital_signs": {
62
+ "bsonType": "object",
63
+ "properties": {
64
+ "blood_pressure": {"bsonType": "string"},
65
+ "heart_rate": {"bsonType": "string"},
66
+ "temperature": {"bsonType": "string"},
67
+ "respiratory_rate": {"bsonType": "string"},
68
+ "oxygen_saturation": {"bsonType": "string"}
69
+ },
70
+ "description": "Vital signs mentioned in the message"
71
+ },
72
+ "lab_results": {
73
+ "bsonType": "array",
74
+ "items": {
75
+ "bsonType": "object",
76
+ "properties": {
77
+ "test_name": {"bsonType": "string"},
78
+ "value": {"bsonType": "string"},
79
+ "unit": {"bsonType": "string"},
80
+ "reference_range": {"bsonType": "string"}
81
+ }
82
+ },
83
+ "description": "Laboratory results mentioned"
84
+ },
85
+ "procedures": {
86
+ "bsonType": "array",
87
+ "items": {"bsonType": "string"},
88
+ "description": "Medical procedures mentioned or performed"
89
+ },
90
+ "notes": {
91
+ "bsonType": "string",
92
+ "description": "Additional clinical notes and observations"
93
+ }
94
+ },
95
+ "description": "Structured medical data extracted from the message"
96
+ },
97
+ "embeddings": {
98
+ "bsonType": "array",
99
+ "items": {"bsonType": "number"},
100
+ "description": "Vector embeddings for semantic search"
101
+ },
102
+ "confidence_score": {
103
+ "bsonType": "number",
104
+ "minimum": 0,
105
+ "maximum": 1,
106
+ "description": "Confidence score of the extraction (0-1)"
107
+ },
108
+ "created_at": {
109
+ "bsonType": "date",
110
+ "description": "Timestamp when the EMR entry was created"
111
+ },
112
+ "updated_at": {
113
+ "bsonType": "date",
114
+ "description": "Timestamp when the EMR entry was last updated"
115
+ }
116
+ }
117
+ }
118
+ }
src/emr/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # EMR Module
2
+ # Electronic Medical Records functionality for the Medical AI Assistant
src/emr/models/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # EMR Models
src/emr/models/emr.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # emr/models/emr.py
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Dict, List, Optional
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class Medication(BaseModel):
9
+ name: str
10
+ dosage: Optional[str] = None
11
+ frequency: Optional[str] = None
12
+ duration: Optional[str] = None
13
+
14
+
15
+ class VitalSigns(BaseModel):
16
+ blood_pressure: Optional[str] = None
17
+ heart_rate: Optional[str] = None
18
+ temperature: Optional[str] = None
19
+ respiratory_rate: Optional[str] = None
20
+ oxygen_saturation: Optional[str] = None
21
+
22
+
23
+ class LabResult(BaseModel):
24
+ test_name: str
25
+ value: str
26
+ unit: Optional[str] = None
27
+ reference_range: Optional[str] = None
28
+
29
+
30
+ class ExtractedData(BaseModel):
31
+ diagnosis: List[str] = Field(default_factory=list)
32
+ symptoms: List[str] = Field(default_factory=list)
33
+ medications: List[Medication] = Field(default_factory=list)
34
+ vital_signs: Optional[VitalSigns] = None
35
+ lab_results: List[LabResult] = Field(default_factory=list)
36
+ procedures: List[str] = Field(default_factory=list)
37
+ notes: Optional[str] = None
38
+
39
+
40
+ class EMRCreateRequest(BaseModel):
41
+ patient_id: str
42
+ doctor_id: str
43
+ message_id: str
44
+ session_id: str
45
+ original_message: str
46
+ extracted_data: ExtractedData
47
+ confidence_score: float = Field(ge=0, le=1)
48
+
49
+
50
+ class EMRResponse(BaseModel):
51
+ emr_id: str
52
+ patient_id: str
53
+ doctor_id: str
54
+ message_id: str
55
+ session_id: str
56
+ original_message: str
57
+ extracted_data: ExtractedData
58
+ confidence_score: float
59
+ created_at: datetime
60
+ updated_at: datetime
61
+
62
+
63
+ class EMRSearchRequest(BaseModel):
64
+ patient_id: str
65
+ query: Optional[str] = None
66
+ limit: int = Field(default=20, ge=1, le=100)
67
+
68
+
69
+ class EMRUpdateRequest(BaseModel):
70
+ extracted_data: Optional[ExtractedData] = None
71
+ confidence_score: Optional[float] = Field(None, ge=0, le=1)
src/emr/repositories/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # EMR Repositories
src/emr/repositories/emr.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # emr/repositories/emr.py
2
+
3
+ import json
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from bson import ObjectId
8
+ from pymongo import ASCENDING, DESCENDING
9
+ from pymongo.errors import ConnectionFailure, OperationFailure, PyMongoError
10
+
11
+ from src.data.connection import ActionFailed, create_collection, get_collection
12
+ from src.utils.logger import logger
13
+ from src.emr.models.emr import EMRCreateRequest, EMRResponse, ExtractedData
14
+
15
+ EMR_COLLECTION = "emr"
16
+
17
+
18
+ def create():
19
+ """Create the EMR collection with validation schema."""
20
+ try:
21
+ create_collection(EMR_COLLECTION, "schemas/emr_validator.json")
22
+ # Create indexes for better performance
23
+ collection = get_collection(EMR_COLLECTION)
24
+ collection.create_index("patient_id")
25
+ collection.create_index("doctor_id")
26
+ collection.create_index("session_id")
27
+ collection.create_index("message_id")
28
+ collection.create_index("created_at")
29
+ collection.create_index([("patient_id", ASCENDING), ("created_at", DESCENDING)])
30
+ collection.create_index([("patient_id", ASCENDING), ("doctor_id", ASCENDING)])
31
+ collection.create_index([("session_id", ASCENDING), ("created_at", DESCENDING)])
32
+ collection.create_index("confidence_score")
33
+ logger().info("EMR collection created successfully with indexes")
34
+ except Exception as e:
35
+ logger().error(f"Error creating EMR collection: {e}")
36
+ raise
37
+
38
+
39
+ def create_emr_entry(emr_data: EMRCreateRequest, embeddings: List[float]) -> str:
40
+ """Create a new EMR entry in the database."""
41
+ try:
42
+ collection = get_collection(EMR_COLLECTION)
43
+
44
+ # Check if EMR entry already exists for this message
45
+ existing = collection.find_one({"message_id": emr_data.message_id})
46
+ if existing:
47
+ logger().warning(f"EMR entry already exists for message {emr_data.message_id}")
48
+ return str(existing["_id"])
49
+
50
+ now = datetime.now(timezone.utc)
51
+
52
+ doc = {
53
+ "patient_id": emr_data.patient_id,
54
+ "doctor_id": emr_data.doctor_id,
55
+ "message_id": emr_data.message_id,
56
+ "session_id": emr_data.session_id,
57
+ "original_message": emr_data.original_message,
58
+ "extracted_data": emr_data.extracted_data.model_dump(),
59
+ "embeddings": embeddings,
60
+ "confidence_score": emr_data.confidence_score,
61
+ "created_at": now,
62
+ "updated_at": now
63
+ }
64
+
65
+ result = collection.insert_one(doc)
66
+ logger().info(f"Created EMR entry for patient {emr_data.patient_id}, message {emr_data.message_id}")
67
+ return str(result.inserted_id)
68
+
69
+ except Exception as e:
70
+ logger().error(f"Error creating EMR entry: {e}")
71
+ raise
72
+
73
+
74
+ def get_emr_by_id(emr_id: str) -> Optional[Dict[str, Any]]:
75
+ """Get an EMR entry by its ID."""
76
+ try:
77
+ collection = get_collection(EMR_COLLECTION)
78
+ result = collection.find_one({"_id": ObjectId(emr_id)})
79
+ if result:
80
+ result["_id"] = str(result["_id"])
81
+ return result
82
+ except Exception as e:
83
+ logger().error(f"Error getting EMR by ID {emr_id}: {e}")
84
+ return None
85
+
86
+
87
+ def get_patient_emr_entries(
88
+ patient_id: str,
89
+ limit: int = 20,
90
+ offset: int = 0
91
+ ) -> List[Dict[str, Any]]:
92
+ """Get EMR entries for a specific patient, ordered by creation date."""
93
+ try:
94
+ collection = get_collection(EMR_COLLECTION)
95
+ cursor = collection.find(
96
+ {"patient_id": patient_id}
97
+ ).sort("created_at", DESCENDING).skip(offset).limit(limit)
98
+
99
+ results = []
100
+ for doc in cursor:
101
+ doc["_id"] = str(doc["_id"])
102
+ results.append(doc)
103
+
104
+ logger().info(f"Retrieved {len(results)} EMR entries for patient {patient_id}")
105
+ return results
106
+
107
+ except Exception as e:
108
+ logger().error(f"Error getting patient EMR entries: {e}")
109
+ return []
110
+
111
+
112
+ def search_emr_by_semantic_similarity(
113
+ patient_id: str,
114
+ query_embeddings: List[float],
115
+ limit: int = 10,
116
+ threshold: float = 0.7
117
+ ) -> List[Dict[str, Any]]:
118
+ """Search EMR entries using semantic similarity with embeddings."""
119
+ try:
120
+ collection = get_collection(EMR_COLLECTION)
121
+
122
+ # Use MongoDB's vector search capabilities if available
123
+ # For now, we'll implement a simple cosine similarity search
124
+ pipeline = [
125
+ {"$match": {"patient_id": patient_id}},
126
+ {
127
+ "$addFields": {
128
+ "similarity": {
129
+ "$let": {
130
+ "vars": {
131
+ "dotProduct": {
132
+ "$reduce": {
133
+ "input": {"$range": [0, {"$size": "$embeddings"}]},
134
+ "initialValue": 0,
135
+ "in": {
136
+ "$add": [
137
+ "$$value",
138
+ {
139
+ "$multiply": [
140
+ {"$arrayElemAt": ["$embeddings", "$$this"]},
141
+ {"$arrayElemAt": [query_embeddings, "$$this"]}
142
+ ]
143
+ }
144
+ ]
145
+ }
146
+ }
147
+ },
148
+ "magnitudeA": {
149
+ "$sqrt": {
150
+ "$reduce": {
151
+ "input": "$embeddings",
152
+ "initialValue": 0,
153
+ "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}
154
+ }
155
+ }
156
+ },
157
+ "magnitudeB": {
158
+ "$sqrt": {
159
+ "$reduce": {
160
+ "input": query_embeddings,
161
+ "initialValue": 0,
162
+ "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}
163
+ }
164
+ }
165
+ }
166
+ },
167
+ "in": {
168
+ "$divide": [
169
+ "$$dotProduct",
170
+ {"$multiply": ["$$magnitudeA", "$$magnitudeB"]}
171
+ ]
172
+ }
173
+ }
174
+ }
175
+ }
176
+ },
177
+ {"$match": {"similarity": {"$gte": threshold}}},
178
+ {"$sort": {"similarity": DESCENDING}},
179
+ {"$limit": limit}
180
+ ]
181
+
182
+ results = list(collection.aggregate(pipeline))
183
+ for doc in results:
184
+ doc["_id"] = str(doc["_id"])
185
+
186
+ logger().info(f"Found {len(results)} similar EMR entries for patient {patient_id}")
187
+ return results
188
+
189
+ except Exception as e:
190
+ logger().error(f"Error searching EMR by similarity: {e}")
191
+ return []
192
+
193
+
194
+ def update_emr_entry(emr_id: str, updates: Dict[str, Any]) -> bool:
195
+ """Update an EMR entry."""
196
+ try:
197
+ collection = get_collection(EMR_COLLECTION)
198
+ updates["updated_at"] = datetime.now(timezone.utc)
199
+
200
+ result = collection.update_one(
201
+ {"_id": ObjectId(emr_id)},
202
+ {"$set": updates}
203
+ )
204
+
205
+ success = result.modified_count > 0
206
+ if success:
207
+ logger().info(f"Updated EMR entry {emr_id}")
208
+ else:
209
+ logger().warning(f"No EMR entry found with ID {emr_id}")
210
+
211
+ return success
212
+
213
+ except Exception as e:
214
+ logger().error(f"Error updating EMR entry {emr_id}: {e}")
215
+ return False
216
+
217
+
218
+ def delete_emr_entry(emr_id: str) -> bool:
219
+ """Delete an EMR entry."""
220
+ try:
221
+ collection = get_collection(EMR_COLLECTION)
222
+ result = collection.delete_one({"_id": ObjectId(emr_id)})
223
+
224
+ success = result.deleted_count > 0
225
+ if success:
226
+ logger().info(f"Deleted EMR entry {emr_id}")
227
+ else:
228
+ logger().warning(f"No EMR entry found with ID {emr_id}")
229
+
230
+ return success
231
+
232
+ except Exception as e:
233
+ logger().error(f"Error deleting EMR entry {emr_id}: {e}")
234
+ return False
235
+
236
+
237
+ def check_emr_exists(message_id: str) -> bool:
238
+ """Check if an EMR entry already exists for a message."""
239
+ try:
240
+ collection = get_collection(EMR_COLLECTION)
241
+ existing = collection.find_one({"message_id": message_id})
242
+ return existing is not None
243
+ except Exception as e:
244
+ logger().error(f"Error checking EMR existence: {e}")
245
+ return False
246
+
247
+
248
+ def get_emr_statistics(patient_id: str) -> Dict[str, Any]:
249
+ """Get EMR statistics for a patient."""
250
+ try:
251
+ collection = get_collection(EMR_COLLECTION)
252
+
253
+ pipeline = [
254
+ {"$match": {"patient_id": patient_id}},
255
+ {
256
+ "$group": {
257
+ "_id": None,
258
+ "total_entries": {"$sum": 1},
259
+ "avg_confidence": {"$avg": "$confidence_score"},
260
+ "latest_entry": {"$max": "$created_at"},
261
+ "diagnosis_count": {
262
+ "$sum": {"$size": "$extracted_data.diagnosis"}
263
+ },
264
+ "medication_count": {
265
+ "$sum": {"$size": "$extracted_data.medications"}
266
+ }
267
+ }
268
+ }
269
+ ]
270
+
271
+ result = list(collection.aggregate(pipeline))
272
+ if result:
273
+ return result[0]
274
+ else:
275
+ return {
276
+ "total_entries": 0,
277
+ "avg_confidence": 0,
278
+ "latest_entry": None,
279
+ "diagnosis_count": 0,
280
+ "medication_count": 0
281
+ }
282
+
283
+ except Exception as e:
284
+ logger().error(f"Error getting EMR statistics: {e}")
285
+ return {}
src/emr/routes/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # EMR Routes
src/emr/routes/emr.py ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # emr/routes/emr.py
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import List, Optional
5
+
6
+ from fastapi import APIRouter, HTTPException, Depends
7
+
8
+ from src.core.state import MedicalState, get_state
9
+ from src.emr.models.emr import EMRResponse, EMRSearchRequest, EMRUpdateRequest
10
+ from src.emr.services.service import EMRService
11
+ from src.utils.logger import logger
12
+
13
+ router = APIRouter(prefix="/emr", tags=["EMR"])
14
+
15
+
16
+ @router.get("/check/{message_id}", response_model=dict)
17
+ async def check_emr_exists(message_id: str):
18
+ """Check if EMR extraction has already been done for a message."""
19
+ try:
20
+ from src.emr.repositories.emr import check_emr_exists
21
+ exists = check_emr_exists(message_id)
22
+ return {
23
+ "message_id": message_id,
24
+ "emr_exists": exists
25
+ }
26
+ except Exception as e:
27
+ logger().error(f"Error checking EMR existence: {e}")
28
+ raise HTTPException(status_code=500, detail=str(e))
29
+
30
+
31
+ @router.get("/health", response_model=dict)
32
+ async def emr_health_check():
33
+ """Health check endpoint for EMR service."""
34
+ try:
35
+ from src.data.connection import get_collection
36
+ collection = get_collection("emr")
37
+ # Try to count documents to verify collection exists and is accessible
38
+ count = collection.count_documents({})
39
+ return {
40
+ "status": "healthy",
41
+ "collection": "emr",
42
+ "document_count": count,
43
+ "timestamp": datetime.now(timezone.utc).isoformat()
44
+ }
45
+ except Exception as e:
46
+ logger().error(f"EMR health check failed: {e}")
47
+ return {
48
+ "status": "unhealthy",
49
+ "error": str(e),
50
+ "timestamp": datetime.now(timezone.utc).isoformat()
51
+ }
52
+
53
+
54
+ def get_emr_service(state: MedicalState = Depends(get_state)) -> EMRService:
55
+ """Get EMR service instance."""
56
+ return EMRService(state.gemini_rotator, state.embedding_client)
57
+
58
+
59
+ @router.post("/extract", response_model=dict)
60
+ async def extract_emr_from_message(
61
+ patient_id: str,
62
+ doctor_id: str,
63
+ message_id: str,
64
+ session_id: str,
65
+ message: str,
66
+ emr_service: EMRService = Depends(get_emr_service)
67
+ ):
68
+ """Extract and store EMR data from a chat message."""
69
+ try:
70
+ # Input validation
71
+ if not patient_id or not patient_id.strip():
72
+ raise HTTPException(status_code=400, detail="Patient ID is required")
73
+ if not doctor_id or not doctor_id.strip():
74
+ raise HTTPException(status_code=400, detail="Doctor ID is required")
75
+ if not message_id or not message_id.strip():
76
+ raise HTTPException(status_code=400, detail="Message ID is required")
77
+ if not session_id or not session_id.strip():
78
+ raise HTTPException(status_code=400, detail="Session ID is required")
79
+ if not message or not message.strip():
80
+ raise HTTPException(status_code=400, detail="Message content is required")
81
+
82
+ logger().info(f"EMR extraction requested for patient {patient_id}, message {message_id}")
83
+
84
+ # Get patient context if available
85
+ patient_context = None
86
+ try:
87
+ from src.data.repositories.patient import get_patient_by_id
88
+ patient = get_patient_by_id(patient_id)
89
+ if patient:
90
+ patient_context = {
91
+ "name": patient.get("name"),
92
+ "age": patient.get("age"),
93
+ "sex": patient.get("sex"),
94
+ "medications": patient.get("medications", []),
95
+ "past_assessment_summary": patient.get("past_assessment_summary")
96
+ }
97
+ except Exception as e:
98
+ logger().warning(f"Could not fetch patient context: {e}")
99
+
100
+ # Extract and store EMR data
101
+ emr_id = await emr_service.extract_and_store_emr(
102
+ patient_id=patient_id,
103
+ doctor_id=doctor_id,
104
+ message_id=message_id,
105
+ session_id=session_id,
106
+ message=message,
107
+ patient_context=patient_context
108
+ )
109
+
110
+ return {
111
+ "emr_id": emr_id,
112
+ "message": "EMR data extracted and stored successfully"
113
+ }
114
+
115
+ except Exception as e:
116
+ logger().error(f"Error in EMR extraction endpoint: {e}")
117
+ raise HTTPException(status_code=500, detail=str(e))
118
+
119
+
120
+ @router.get("/patient/{patient_id}", response_model=List[EMRResponse])
121
+ async def get_patient_emr(
122
+ patient_id: str,
123
+ limit: int = 20,
124
+ offset: int = 0,
125
+ emr_service: EMRService = Depends(get_emr_service)
126
+ ):
127
+ """Get EMR entries for a specific patient."""
128
+ try:
129
+ logger().info(f"Getting EMR entries for patient {patient_id}")
130
+
131
+ entries = await emr_service.get_patient_emr(patient_id, limit, offset)
132
+ return entries
133
+
134
+ except Exception as e:
135
+ logger().error(f"Error getting patient EMR: {e}")
136
+ raise HTTPException(status_code=500, detail=str(e))
137
+
138
+
139
+ @router.get("/search/{patient_id}", response_model=List[EMRResponse])
140
+ async def search_patient_emr(
141
+ patient_id: str,
142
+ query: str,
143
+ limit: int = 10,
144
+ emr_service: EMRService = Depends(get_emr_service)
145
+ ):
146
+ """Search EMR entries for a patient using semantic similarity."""
147
+ try:
148
+ logger().info(f"Searching EMR for patient {patient_id} with query: {query}")
149
+
150
+ entries = await emr_service.search_emr_semantic(patient_id, query, limit)
151
+ return entries
152
+
153
+ except Exception as e:
154
+ logger().error(f"Error searching patient EMR: {e}")
155
+ raise HTTPException(status_code=500, detail=str(e))
156
+
157
+
158
+ @router.get("/{emr_id}", response_model=EMRResponse)
159
+ async def get_emr_by_id(
160
+ emr_id: str,
161
+ emr_service: EMRService = Depends(get_emr_service)
162
+ ):
163
+ """Get a specific EMR entry by ID."""
164
+ try:
165
+ logger().info(f"Getting EMR entry {emr_id}")
166
+
167
+ entry = await emr_service.get_emr_by_id(emr_id)
168
+ if not entry:
169
+ raise HTTPException(status_code=404, detail="EMR entry not found")
170
+
171
+ return entry
172
+
173
+ except HTTPException:
174
+ raise
175
+ except Exception as e:
176
+ logger().error(f"Error getting EMR by ID: {e}")
177
+ raise HTTPException(status_code=500, detail=str(e))
178
+
179
+
180
+ @router.patch("/{emr_id}", response_model=dict)
181
+ async def update_emr(
182
+ emr_id: str,
183
+ request: EMRUpdateRequest,
184
+ emr_service: EMRService = Depends(get_emr_service)
185
+ ):
186
+ """Update an EMR entry."""
187
+ try:
188
+ logger().info(f"Updating EMR entry {emr_id}")
189
+
190
+ updates = {}
191
+ if request.extracted_data:
192
+ updates["extracted_data"] = request.extracted_data.model_dump()
193
+ if request.confidence_score is not None:
194
+ updates["confidence_score"] = request.confidence_score
195
+
196
+ success = await emr_service.update_emr(emr_id, updates)
197
+ if not success:
198
+ raise HTTPException(status_code=404, detail="EMR entry not found")
199
+
200
+ return {"message": "EMR entry updated successfully"}
201
+
202
+ except HTTPException:
203
+ raise
204
+ except Exception as e:
205
+ logger().error(f"Error updating EMR: {e}")
206
+ raise HTTPException(status_code=500, detail=str(e))
207
+
208
+
209
+ @router.delete("/{emr_id}", response_model=dict)
210
+ async def delete_emr(
211
+ emr_id: str,
212
+ emr_service: EMRService = Depends(get_emr_service)
213
+ ):
214
+ """Delete an EMR entry."""
215
+ try:
216
+ logger().info(f"Deleting EMR entry {emr_id}")
217
+
218
+ success = await emr_service.delete_emr(emr_id)
219
+ if not success:
220
+ raise HTTPException(status_code=404, detail="EMR entry not found")
221
+
222
+ return {"message": "EMR entry deleted successfully"}
223
+
224
+ except HTTPException:
225
+ raise
226
+ except Exception as e:
227
+ logger().error(f"Error deleting EMR: {e}")
228
+ raise HTTPException(status_code=500, detail=str(e))
229
+
230
+
231
+ @router.get("/statistics/{patient_id}", response_model=dict)
232
+ async def get_emr_statistics(
233
+ patient_id: str,
234
+ emr_service: EMRService = Depends(get_emr_service)
235
+ ):
236
+ """Get EMR statistics for a patient."""
237
+ try:
238
+ logger().info(f"Getting EMR statistics for patient {patient_id}")
239
+
240
+ stats = await emr_service.get_emr_statistics(patient_id)
241
+ return stats
242
+
243
+ except Exception as e:
244
+ logger().error(f"Error getting EMR statistics: {e}")
245
+ raise HTTPException(status_code=500, detail=str(e))
246
+
247
+
248
+ @router.post("/bulk-extract", response_model=dict)
249
+ async def bulk_extract_emr(
250
+ extractions: List[dict],
251
+ emr_service: EMRService = Depends(get_emr_service)
252
+ ):
253
+ """Extract EMR data from multiple messages in bulk."""
254
+ try:
255
+ if not extractions or len(extractions) == 0:
256
+ raise HTTPException(status_code=400, detail="No extractions provided")
257
+
258
+ if len(extractions) > 10:
259
+ raise HTTPException(status_code=400, detail="Maximum 10 extractions allowed per request")
260
+
261
+ logger().info(f"Bulk EMR extraction requested for {len(extractions)} messages")
262
+
263
+ results = []
264
+ errors = []
265
+
266
+ for i, extraction in enumerate(extractions):
267
+ try:
268
+ # Validate required fields
269
+ required_fields = ['patient_id', 'doctor_id', 'message_id', 'session_id', 'message']
270
+ for field in required_fields:
271
+ if field not in extraction or not extraction[field]:
272
+ raise ValueError(f"Missing or empty {field}")
273
+
274
+ # Get patient context
275
+ patient_context = None
276
+ try:
277
+ from src.data.repositories.patient import get_patient_by_id
278
+ patient = get_patient_by_id(extraction['patient_id'])
279
+ if patient:
280
+ patient_context = {
281
+ "name": patient.get("name"),
282
+ "age": patient.get("age"),
283
+ "sex": patient.get("sex"),
284
+ "medications": patient.get("medications", []),
285
+ "past_assessment_summary": patient.get("past_assessment_summary")
286
+ }
287
+ except Exception as e:
288
+ logger().warning(f"Could not fetch patient context for extraction {i}: {e}")
289
+
290
+ # Extract and store EMR data
291
+ emr_id = await emr_service.extract_and_store_emr(
292
+ patient_id=extraction['patient_id'],
293
+ doctor_id=extraction['doctor_id'],
294
+ message_id=extraction['message_id'],
295
+ session_id=extraction['session_id'],
296
+ message=extraction['message'],
297
+ patient_context=patient_context
298
+ )
299
+
300
+ results.append({
301
+ "index": i,
302
+ "message_id": extraction['message_id'],
303
+ "emr_id": emr_id,
304
+ "status": "success"
305
+ })
306
+
307
+ except Exception as e:
308
+ logger().error(f"Error in bulk extraction {i}: {e}")
309
+ errors.append({
310
+ "index": i,
311
+ "message_id": extraction.get('message_id', 'unknown'),
312
+ "error": str(e),
313
+ "status": "failed"
314
+ })
315
+
316
+ return {
317
+ "message": f"Bulk extraction completed. {len(results)} successful, {len(errors)} failed.",
318
+ "results": results,
319
+ "errors": errors
320
+ }
321
+
322
+ except HTTPException:
323
+ raise
324
+ except Exception as e:
325
+ logger().error(f"Error in bulk EMR extraction: {e}")
326
+ raise HTTPException(status_code=500, detail=str(e))
src/emr/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # EMR Services
src/emr/services/extractor.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # emr/services/extractor.py
2
+
3
+ import json
4
+ import re
5
+ from typing import Any, Dict, List, Optional, Tuple
6
+
7
+ from src.emr.models.emr import ExtractedData, Medication, VitalSigns, LabResult
8
+ from src.services.gemini import gemini_chat
9
+ from src.utils.rotator import APIKeyRotator
10
+ from src.utils.logger import logger
11
+
12
+
13
+ class EMRExtractor:
14
+ """Service for extracting structured medical data from chat messages using Gemini AI."""
15
+
16
+ def __init__(self, gemini_rotator: APIKeyRotator):
17
+ self.gemini_rotator = gemini_rotator
18
+
19
+ async def extract_medical_data(self, message: str, patient_context: Optional[Dict[str, Any]] = None) -> Tuple[ExtractedData, float]:
20
+ """
21
+ Extract structured medical data from a chat message using Gemini AI.
22
+
23
+ Args:
24
+ message: The chat message to analyze
25
+ patient_context: Optional patient context information
26
+
27
+ Returns:
28
+ Tuple of (ExtractedData, confidence_score)
29
+ """
30
+ try:
31
+ # Prepare the prompt for Gemini
32
+ prompt = self._build_extraction_prompt(message, patient_context)
33
+
34
+ # Get response from Gemini
35
+ response = await self._call_gemini_api(prompt)
36
+
37
+ # Parse the response
38
+ extracted_data, confidence = self._parse_gemini_response(response)
39
+
40
+ logger().info(f"Successfully extracted medical data with confidence {confidence:.2f}")
41
+ return extracted_data, confidence
42
+
43
+ except Exception as e:
44
+ logger().error(f"Error extracting medical data: {e}")
45
+ # Return empty data with low confidence
46
+ return ExtractedData(), 0.0
47
+
48
+ def _build_extraction_prompt(self, message: str, patient_context: Optional[Dict[str, Any]] = None) -> str:
49
+ """Build the prompt for Gemini AI to extract medical data."""
50
+
51
+ context_info = ""
52
+ if patient_context:
53
+ context_info = f"""
54
+ Patient Context:
55
+ - Name: {patient_context.get('name', 'Unknown')}
56
+ - Age: {patient_context.get('age', 'Unknown')}
57
+ - Sex: {patient_context.get('sex', 'Unknown')}
58
+ - Current Medications: {', '.join(patient_context.get('medications', []))}
59
+ - Past Assessment Summary: {patient_context.get('past_assessment_summary', 'None')}
60
+ """
61
+
62
+ prompt = f"""You are a medical AI assistant specialized in extracting structured medical data from clinical conversations.
63
+
64
+ {context_info}
65
+
66
+ Please analyze the following medical message and extract all relevant clinical information in the specified JSON format:
67
+
68
+ Message: "{message}"
69
+
70
+ Extract the following information and return ONLY a valid JSON object with this exact structure:
71
+
72
+ {{
73
+ "diagnosis": ["list of diagnoses mentioned"],
74
+ "symptoms": ["list of symptoms described"],
75
+ "medications": [
76
+ {{
77
+ "name": "medication name",
78
+ "dosage": "dosage if mentioned",
79
+ "frequency": "frequency if mentioned",
80
+ "duration": "duration if mentioned"
81
+ }}
82
+ ],
83
+ "vital_signs": {{
84
+ "blood_pressure": "value if mentioned",
85
+ "heart_rate": "value if mentioned",
86
+ "temperature": "value if mentioned",
87
+ "respiratory_rate": "value if mentioned",
88
+ "oxygen_saturation": "value if mentioned"
89
+ }},
90
+ "lab_results": [
91
+ {{
92
+ "test_name": "test name",
93
+ "value": "test value",
94
+ "unit": "unit if mentioned",
95
+ "reference_range": "normal range if mentioned"
96
+ }}
97
+ ],
98
+ "procedures": ["list of procedures mentioned"],
99
+ "notes": "additional clinical notes and observations"
100
+ }}
101
+
102
+ Guidelines:
103
+ 1. Only extract information that is explicitly mentioned or clearly implied
104
+ 2. Use medical terminology appropriately
105
+ 3. If a field has no relevant information, use an empty array [] or null
106
+ 4. For medications, only include those that are prescribed, recommended, or mentioned as current
107
+ 5. Extract vital signs only if specific values are mentioned
108
+ 6. Include lab results only if specific test values are provided
109
+ 7. Be conservative - it's better to miss something than to hallucinate information
110
+ 8. Return ONLY the JSON object, no additional text or explanation
111
+
112
+ Confidence Assessment:
113
+ After the JSON, provide a confidence score (0.0-1.0) based on:
114
+ - Clarity of medical information in the message
115
+ - Specificity of clinical details
116
+ - Presence of measurable values (vitals, lab results)
117
+ - Overall clinical relevance
118
+
119
+ Format: CONFIDENCE: 0.85
120
+
121
+ Return the JSON followed by the confidence score on a new line."""
122
+
123
+ return prompt
124
+
125
+ async def _call_gemini_api(self, prompt: str) -> str:
126
+ """Call the Gemini API with the extraction prompt."""
127
+ try:
128
+ # Use the gemini_chat function with the rotator
129
+ response = await gemini_chat(prompt, self.gemini_rotator)
130
+ return response
131
+ except Exception as e:
132
+ logger().error(f"Error calling Gemini API: {e}")
133
+ raise
134
+
135
+ def _parse_gemini_response(self, response: str) -> Tuple[ExtractedData, float]:
136
+ """Parse the Gemini response to extract structured data and confidence score."""
137
+ try:
138
+ # Extract confidence score
139
+ confidence = 0.5 # Default confidence
140
+ confidence_match = re.search(r'CONFIDENCE:\s*([0-9.]+)', response)
141
+ if confidence_match:
142
+ confidence = float(confidence_match.group(1))
143
+
144
+ # Extract JSON from response
145
+ json_match = re.search(r'\{.*\}', response, re.DOTALL)
146
+ if not json_match:
147
+ logger().warning("No JSON found in Gemini response")
148
+ return ExtractedData(), confidence
149
+
150
+ json_str = json_match.group(0)
151
+ data = json.loads(json_str)
152
+
153
+ # Parse medications
154
+ medications = []
155
+ for med_data in data.get('medications', []):
156
+ if isinstance(med_data, dict):
157
+ medications.append(Medication(
158
+ name=med_data.get('name', ''),
159
+ dosage=med_data.get('dosage'),
160
+ frequency=med_data.get('frequency'),
161
+ duration=med_data.get('duration')
162
+ ))
163
+
164
+ # Parse vital signs
165
+ vital_signs_data = data.get('vital_signs', {})
166
+ vital_signs = None
167
+ if vital_signs_data and any(vital_signs_data.values()):
168
+ vital_signs = VitalSigns(
169
+ blood_pressure=vital_signs_data.get('blood_pressure'),
170
+ heart_rate=vital_signs_data.get('heart_rate'),
171
+ temperature=vital_signs_data.get('temperature'),
172
+ respiratory_rate=vital_signs_data.get('respiratory_rate'),
173
+ oxygen_saturation=vital_signs_data.get('oxygen_saturation')
174
+ )
175
+
176
+ # Parse lab results
177
+ lab_results = []
178
+ for lab_data in data.get('lab_results', []):
179
+ if isinstance(lab_data, dict):
180
+ lab_results.append(LabResult(
181
+ test_name=lab_data.get('test_name', ''),
182
+ value=lab_data.get('value', ''),
183
+ unit=lab_data.get('unit'),
184
+ reference_range=lab_data.get('reference_range')
185
+ ))
186
+
187
+ # Create ExtractedData object
188
+ extracted_data = ExtractedData(
189
+ diagnosis=data.get('diagnosis', []),
190
+ symptoms=data.get('symptoms', []),
191
+ medications=medications,
192
+ vital_signs=vital_signs,
193
+ lab_results=lab_results,
194
+ procedures=data.get('procedures', []),
195
+ notes=data.get('notes')
196
+ )
197
+
198
+ return extracted_data, confidence
199
+
200
+ except json.JSONDecodeError as e:
201
+ logger().error(f"Error parsing JSON from Gemini response: {e}")
202
+ return ExtractedData(), 0.0
203
+ except Exception as e:
204
+ logger().error(f"Error parsing Gemini response: {e}")
205
+ return ExtractedData(), 0.0
206
+
207
+ def extract_medications_from_text(self, text: str) -> List[str]:
208
+ """Extract medication names from text using pattern matching."""
209
+ # Common medication patterns
210
+ medication_patterns = [
211
+ r'\b(?:acetaminophen|tylenol|ibuprofen|advil|motrin|aspirin|naproxen|aleve)\b',
212
+ r'\b(?:metformin|insulin|glipizide|metoprolol|lisinopril|amlodipine|atorvastatin|simvastatin)\b',
213
+ r'\b(?:omeprazole|pantoprazole|ranitidine|famotidine|sertraline|fluoxetine|paroxetine)\b',
214
+ r'\b(?:prednisone|hydrocortisone|dexamethasone|methylprednisolone)\b',
215
+ r'\b(?:warfarin|heparin|clopidogrel|aspirin)\b',
216
+ r'\b(?:furosemide|hydrochlorothiazide|spironolactone|triamterene)\b'
217
+ ]
218
+
219
+ medications = set()
220
+ for pattern in medication_patterns:
221
+ matches = re.findall(pattern, text, re.IGNORECASE)
222
+ medications.update(matches)
223
+
224
+ return list(medications)
225
+
226
+ def extract_vital_signs_from_text(self, text: str) -> Dict[str, str]:
227
+ """Extract vital signs from text using pattern matching."""
228
+ vital_signs = {}
229
+
230
+ # Blood pressure patterns
231
+ bp_pattern = r'(?:blood pressure|bp|pressure)\s*:?\s*(\d{2,3}/\d{2,3})'
232
+ bp_match = re.search(bp_pattern, text, re.IGNORECASE)
233
+ if bp_match:
234
+ vital_signs['blood_pressure'] = bp_match.group(1)
235
+
236
+ # Heart rate patterns
237
+ hr_pattern = r'(?:heart rate|hr|pulse)\s*:?\s*(\d{2,3})\s*(?:bpm|beats per minute)?'
238
+ hr_match = re.search(hr_pattern, text, re.IGNORECASE)
239
+ if hr_match:
240
+ vital_signs['heart_rate'] = hr_match.group(1)
241
+
242
+ # Temperature patterns
243
+ temp_pattern = r'(?:temperature|temp|fever)\s*:?\s*(\d{2,3}(?:\.\d)?)\s*(?:°?[fc])?'
244
+ temp_match = re.search(temp_pattern, text, re.IGNORECASE)
245
+ if temp_match:
246
+ vital_signs['temperature'] = temp_match.group(1)
247
+
248
+ # Respiratory rate patterns
249
+ rr_pattern = r'(?:respiratory rate|rr|breathing rate)\s*:?\s*(\d{1,2})\s*(?:breaths per minute|bpm)?'
250
+ rr_match = re.search(rr_pattern, text, re.IGNORECASE)
251
+ if rr_match:
252
+ vital_signs['respiratory_rate'] = rr_match.group(1)
253
+
254
+ # Oxygen saturation patterns
255
+ o2_pattern = r'(?:oxygen saturation|o2 sat|spo2)\s*:?\s*(\d{2,3})\s*%?'
256
+ o2_match = re.search(o2_pattern, text, re.IGNORECASE)
257
+ if o2_match:
258
+ vital_signs['oxygen_saturation'] = o2_match.group(1)
259
+
260
+ return vital_signs
src/emr/services/service.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # emr/services/service.py
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from src.emr.models.emr import EMRCreateRequest, EMRResponse, ExtractedData
6
+ from src.emr.repositories.emr import (
7
+ create_emr_entry,
8
+ get_patient_emr_entries,
9
+ get_emr_by_id,
10
+ search_emr_by_semantic_similarity,
11
+ update_emr_entry,
12
+ delete_emr_entry,
13
+ get_emr_statistics
14
+ )
15
+ from src.emr.services.extractor import EMRExtractor
16
+ from src.utils.rotator import APIKeyRotator
17
+ from src.utils.embeddings import EmbeddingClient
18
+ from src.utils.logger import logger
19
+
20
+
21
+ class EMRService:
22
+ """Main service for EMR operations including extraction, storage, and retrieval."""
23
+
24
+ def __init__(self, gemini_rotator: APIKeyRotator, embedding_client: EmbeddingClient):
25
+ self.gemini_rotator = gemini_rotator
26
+ self.embedding_client = embedding_client
27
+ self.extractor = EMRExtractor(gemini_rotator)
28
+
29
+ async def extract_and_store_emr(
30
+ self,
31
+ patient_id: str,
32
+ doctor_id: str,
33
+ message_id: str,
34
+ session_id: str,
35
+ message: str,
36
+ patient_context: Optional[Dict[str, Any]] = None
37
+ ) -> str:
38
+ """
39
+ Extract medical data from a message and store it as an EMR entry.
40
+
41
+ Args:
42
+ patient_id: ID of the patient
43
+ doctor_id: ID of the doctor
44
+ message_id: ID of the message being analyzed
45
+ session_id: ID of the chat session
46
+ message: The message content to analyze
47
+ patient_context: Optional patient context information
48
+
49
+ Returns:
50
+ ID of the created EMR entry
51
+ """
52
+ try:
53
+ logger().info(f"Starting EMR extraction for patient {patient_id}, message {message_id}")
54
+
55
+ # Extract medical data using Gemini AI
56
+ extracted_data, confidence = await self.extractor.extract_medical_data(
57
+ message, patient_context
58
+ )
59
+
60
+ # Generate embeddings for the extracted data
61
+ embeddings = await self._generate_embeddings(extracted_data, message)
62
+
63
+ # Create EMR entry
64
+ emr_request = EMRCreateRequest(
65
+ patient_id=patient_id,
66
+ doctor_id=doctor_id,
67
+ message_id=message_id,
68
+ session_id=session_id,
69
+ original_message=message,
70
+ extracted_data=extracted_data,
71
+ confidence_score=confidence
72
+ )
73
+
74
+ # Store in database
75
+ emr_id = create_emr_entry(emr_request, embeddings)
76
+
77
+ logger().info(f"Successfully created EMR entry {emr_id} with confidence {confidence:.2f}")
78
+ return emr_id
79
+
80
+ except Exception as e:
81
+ logger().error(f"Error in extract_and_store_emr: {e}")
82
+ raise
83
+
84
+ async def _generate_embeddings(self, extracted_data: ExtractedData, original_message: str) -> List[float]:
85
+ """Generate embeddings for the extracted medical data."""
86
+ try:
87
+ # Combine all extracted data into a single text for embedding
88
+ text_parts = []
89
+
90
+ if extracted_data.diagnosis:
91
+ text_parts.append(f"Diagnoses: {', '.join(extracted_data.diagnosis)}")
92
+
93
+ if extracted_data.symptoms:
94
+ text_parts.append(f"Symptoms: {', '.join(extracted_data.symptoms)}")
95
+
96
+ if extracted_data.medications:
97
+ med_text = []
98
+ for med in extracted_data.medications:
99
+ med_str = med.name
100
+ if med.dosage:
101
+ med_str += f" {med.dosage}"
102
+ if med.frequency:
103
+ med_str += f" {med.frequency}"
104
+ med_text.append(med_str)
105
+ text_parts.append(f"Medications: {', '.join(med_text)}")
106
+
107
+ if extracted_data.vital_signs:
108
+ vitals = []
109
+ if extracted_data.vital_signs.blood_pressure:
110
+ vitals.append(f"BP: {extracted_data.vital_signs.blood_pressure}")
111
+ if extracted_data.vital_signs.heart_rate:
112
+ vitals.append(f"HR: {extracted_data.vital_signs.heart_rate}")
113
+ if extracted_data.vital_signs.temperature:
114
+ vitals.append(f"Temp: {extracted_data.vital_signs.temperature}")
115
+ if vitals:
116
+ text_parts.append(f"Vital Signs: {', '.join(vitals)}")
117
+
118
+ if extracted_data.lab_results:
119
+ lab_text = []
120
+ for lab in extracted_data.lab_results:
121
+ lab_str = f"{lab.test_name}: {lab.value}"
122
+ if lab.unit:
123
+ lab_str += f" {lab.unit}"
124
+ lab_text.append(lab_str)
125
+ text_parts.append(f"Lab Results: {', '.join(lab_text)}")
126
+
127
+ if extracted_data.procedures:
128
+ text_parts.append(f"Procedures: {', '.join(extracted_data.procedures)}")
129
+
130
+ if extracted_data.notes:
131
+ text_parts.append(f"Notes: {extracted_data.notes}")
132
+
133
+ # Add original message for context
134
+ text_parts.append(f"Original: {original_message}")
135
+
136
+ # Generate embeddings
137
+ combined_text = " | ".join(text_parts)
138
+ embeddings = await self.embedding_client.generate_embeddings(combined_text)
139
+
140
+ return embeddings
141
+
142
+ except Exception as e:
143
+ logger().error(f"Error generating embeddings: {e}")
144
+ # Return empty embeddings if generation fails
145
+ return []
146
+
147
+ async def get_patient_emr(
148
+ self,
149
+ patient_id: str,
150
+ limit: int = 20,
151
+ offset: int = 0
152
+ ) -> List[EMRResponse]:
153
+ """Get EMR entries for a specific patient."""
154
+ try:
155
+ entries = get_patient_emr_entries(patient_id, limit, offset)
156
+
157
+ emr_responses = []
158
+ for entry in entries:
159
+ emr_response = EMRResponse(
160
+ emr_id=str(entry["_id"]),
161
+ patient_id=entry["patient_id"],
162
+ doctor_id=entry["doctor_id"],
163
+ message_id=entry["message_id"],
164
+ session_id=entry["session_id"],
165
+ original_message=entry["original_message"],
166
+ extracted_data=ExtractedData(**entry["extracted_data"]),
167
+ confidence_score=entry["confidence_score"],
168
+ created_at=entry["created_at"],
169
+ updated_at=entry["updated_at"]
170
+ )
171
+ emr_responses.append(emr_response)
172
+
173
+ return emr_responses
174
+
175
+ except Exception as e:
176
+ logger().error(f"Error getting patient EMR: {e}")
177
+ return []
178
+
179
+ async def search_emr_semantic(
180
+ self,
181
+ patient_id: str,
182
+ query: str,
183
+ limit: int = 10
184
+ ) -> List[EMRResponse]:
185
+ """Search EMR entries using semantic similarity."""
186
+ try:
187
+ # Generate embeddings for the search query
188
+ query_embeddings = await self.embedding_client.generate_embeddings(query)
189
+
190
+ # Search using semantic similarity
191
+ entries = search_emr_by_semantic_similarity(
192
+ patient_id, query_embeddings, limit
193
+ )
194
+
195
+ emr_responses = []
196
+ for entry in entries:
197
+ emr_response = EMRResponse(
198
+ emr_id=str(entry["_id"]),
199
+ patient_id=entry["patient_id"],
200
+ doctor_id=entry["doctor_id"],
201
+ message_id=entry["message_id"],
202
+ session_id=entry["session_id"],
203
+ original_message=entry["original_message"],
204
+ extracted_data=ExtractedData(**entry["extracted_data"]),
205
+ confidence_score=entry["confidence_score"],
206
+ created_at=entry["created_at"],
207
+ updated_at=entry["updated_at"]
208
+ )
209
+ emr_responses.append(emr_response)
210
+
211
+ return emr_responses
212
+
213
+ except Exception as e:
214
+ logger().error(f"Error searching EMR semantically: {e}")
215
+ return []
216
+
217
+ async def get_emr_by_id(self, emr_id: str) -> Optional[EMRResponse]:
218
+ """Get a specific EMR entry by ID."""
219
+ try:
220
+ entry = get_emr_by_id(emr_id)
221
+ if not entry:
222
+ return None
223
+
224
+ return EMRResponse(
225
+ emr_id=str(entry["_id"]),
226
+ patient_id=entry["patient_id"],
227
+ doctor_id=entry["doctor_id"],
228
+ message_id=entry["message_id"],
229
+ session_id=entry["session_id"],
230
+ original_message=entry["original_message"],
231
+ extracted_data=ExtractedData(**entry["extracted_data"]),
232
+ confidence_score=entry["confidence_score"],
233
+ created_at=entry["created_at"],
234
+ updated_at=entry["updated_at"]
235
+ )
236
+
237
+ except Exception as e:
238
+ logger().error(f"Error getting EMR by ID: {e}")
239
+ return None
240
+
241
+ async def update_emr(self, emr_id: str, updates: Dict[str, Any]) -> bool:
242
+ """Update an EMR entry."""
243
+ try:
244
+ return update_emr_entry(emr_id, updates)
245
+ except Exception as e:
246
+ logger().error(f"Error updating EMR: {e}")
247
+ return False
248
+
249
+ async def delete_emr(self, emr_id: str) -> bool:
250
+ """Delete an EMR entry."""
251
+ try:
252
+ return delete_emr_entry(emr_id)
253
+ except Exception as e:
254
+ logger().error(f"Error deleting EMR: {e}")
255
+ return False
256
+
257
+ async def get_emr_statistics(self, patient_id: str) -> Dict[str, Any]:
258
+ """Get EMR statistics for a patient."""
259
+ try:
260
+ return get_emr_statistics(patient_id)
261
+ except Exception as e:
262
+ logger().error(f"Error getting EMR statistics: {e}")
263
+ return {}
src/main.py CHANGED
@@ -31,11 +31,13 @@ from src.api.routes import patient as patients_route
31
  from src.api.routes import session as session_route
32
  from src.api.routes import static as static_route
33
  from src.api.routes import system as system_route
 
34
  from src.core.state import MedicalState, get_state
35
  from src.data.repositories import account as account_repo
36
  from src.data.repositories import medical as medical_repo
37
  from src.data.repositories import patient as patient_repo
38
  from src.data.repositories import session as session_repo
 
39
 
40
 
41
  def startup_event(state: MedicalState):
@@ -82,6 +84,7 @@ def startup_event(state: MedicalState):
82
  patient_repo.init()
83
  session_repo.init()
84
  #medical_repo.init()
 
85
 
86
  def shutdown_event():
87
  """Cleanup on shutdown"""
@@ -127,6 +130,7 @@ app.include_router(account_route.router)
127
  app.include_router(system_route.router)
128
  app.include_router(static_route.router)
129
  app.include_router(audio_route.router)
 
130
 
131
  @app.get("/api/info")
132
  async def get_api_info():
 
31
  from src.api.routes import session as session_route
32
  from src.api.routes import static as static_route
33
  from src.api.routes import system as system_route
34
+ from src.emr.routes import emr as emr_route
35
  from src.core.state import MedicalState, get_state
36
  from src.data.repositories import account as account_repo
37
  from src.data.repositories import medical as medical_repo
38
  from src.data.repositories import patient as patient_repo
39
  from src.data.repositories import session as session_repo
40
+ from src.emr.repositories import emr as emr_repo
41
 
42
 
43
  def startup_event(state: MedicalState):
 
84
  patient_repo.init()
85
  session_repo.init()
86
  #medical_repo.init()
87
+ emr_repo.create()
88
 
89
  def shutdown_event():
90
  """Cleanup on shutdown"""
 
130
  app.include_router(system_route.router)
131
  app.include_router(static_route.router)
132
  app.include_router(audio_route.router)
133
+ app.include_router(emr_route.router)
134
 
135
  @app.get("/api/info")
136
  async def get_api_info():
static/css/emr.css CHANGED
@@ -1,14 +1,15 @@
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;
@@ -17,339 +18,517 @@
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: flex;
99
- flex-direction: column;
100
- gap: 16px;
101
  }
102
 
103
- .emr-grid .row {
104
  display: grid;
105
- gap: 16px;
 
106
  }
107
 
108
- .emr-grid .row:not(.contact-info) {
109
- grid-template-columns: repeat(2, 1fr);
 
 
 
 
110
  }
111
 
112
- .emr-grid .row.contact-info {
113
- grid-template-columns: repeat(3, 1fr);
 
 
 
114
  }
115
 
116
- .emr-field {
117
- display: grid;
118
- gap: 6px;
 
 
119
  }
120
 
121
- .emr-field label {
122
- font-weight: 500;
123
- color: var(--text-primary);
124
- font-size: 0.9rem;
 
 
 
 
 
 
 
 
 
125
  }
126
 
127
- .emr-field input,
128
- .emr-field select,
129
- .emr-field textarea {
130
- padding: 0.75rem;
131
  border: 1px solid var(--border-color);
132
- border-radius: 6px;
133
- background-color: var(--bg-primary);
134
- color: var(--text-primary);
135
- font-size: 0.9rem;
136
  transition: border-color var(--transition-fast);
137
  }
138
 
139
- .emr-field input:focus,
140
- .emr-field select:focus,
141
- .emr-field textarea:focus {
142
  outline: none;
143
  border-color: var(--primary-color);
144
- box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.1);
145
  }
146
 
147
- .emr-field textarea {
148
- resize: vertical;
149
- min-height: 80px;
 
 
 
 
 
150
  }
151
 
152
- /* Medications */
153
- .medications-container {
154
- display: flex;
155
- flex-direction: column;
156
- gap: 1rem;
157
  }
158
 
159
- .medications-list {
160
  display: flex;
161
- flex-wrap: wrap;
162
- gap: 0.5rem;
163
- min-height: 40px;
164
- padding: 0.5rem;
 
165
  border: 1px solid var(--border-color);
166
  border-radius: 6px;
167
  background-color: var(--bg-primary);
 
 
168
  }
169
 
170
- .medication-tag {
171
- display: inline-flex;
172
- align-items: center;
173
- gap: 0.5rem;
174
- background-color: var(--primary-color);
175
- color: white;
176
- padding: 0.25rem 0.75rem;
177
- border-radius: 20px;
178
- font-size: 0.8rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  font-weight: 500;
 
 
180
  }
181
 
182
- .medication-tag .remove-medication {
183
- background: none;
184
- border: none;
185
- color: white;
186
- cursor: pointer;
187
- padding: 0;
188
- margin-left: 0.25rem;
189
- opacity: 0.7;
190
- transition: opacity var(--transition-fast);
191
  }
192
 
193
- .medication-tag .remove-medication:hover {
194
- opacity: 1;
 
195
  }
196
 
197
- .add-medication {
 
 
 
 
 
 
 
 
 
 
 
198
  display: flex;
199
- gap: 0.5rem;
200
  align-items: center;
 
201
  }
202
 
203
- .add-medication input {
204
- flex: 1;
205
- margin: 0;
 
 
 
 
 
 
 
 
206
  }
207
 
208
- .add-medication button {
209
- white-space: nowrap;
210
  }
211
 
212
- /* Sessions */
213
- .sessions-container {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  display: flex;
215
- flex-direction: column;
216
- gap: 1rem;
217
  }
218
 
219
- .session-item {
220
- background-color: var(--bg-primary);
221
- border: 1px solid var(--border-color);
222
- border-radius: 6px;
223
- padding: 1rem;
224
  cursor: pointer;
 
 
225
  transition: all var(--transition-fast);
 
226
  }
227
 
228
- .session-item:hover {
229
- border-color: var(--primary-color);
230
  background-color: var(--bg-tertiary);
 
231
  }
232
 
233
- .session-title {
234
- font-weight: 600;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  color: var(--text-primary);
236
- margin-bottom: 0.5rem;
237
  }
238
 
239
- .session-meta {
240
- display: flex;
241
- justify-content: space-between;
242
- align-items: center;
243
- font-size: 0.8rem;
244
  color: var(--text-secondary);
245
  }
246
 
247
- .session-date {
248
- font-family: monospace;
 
 
 
249
  }
250
 
251
- .session-messages {
252
- color: var(--text-secondary);
253
  }
254
 
255
- /* Actions */
256
- .emr-actions {
257
- display: flex;
258
- gap: 1rem;
259
- flex-wrap: wrap;
 
260
  }
261
 
262
- .btn-primary,
263
- .btn-secondary {
264
- display: inline-flex;
265
- align-items: center;
266
- gap: 0.5rem;
267
- padding: 0.75rem 1.5rem;
268
- border: none;
269
- border-radius: 6px;
270
- font-size: 0.9rem;
271
- font-weight: 500;
272
- cursor: pointer;
273
- transition: all var(--transition-fast);
274
- text-decoration: none;
275
  }
276
 
277
- .btn-primary {
278
- background-color: var(--primary-color);
279
- color: white;
280
  }
281
 
282
- .btn-primary:hover {
283
- background-color: var(--primary-color-dark);
284
- transform: translateY(-1px);
285
  }
286
 
287
- .btn-secondary {
288
- background-color: var(--bg-tertiary);
 
 
 
 
 
 
 
 
289
  color: var(--text-primary);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  border: 1px solid var(--border-color);
291
  }
292
 
293
- .btn-secondary:hover {
294
- background-color: var(--bg-primary);
295
- border-color: var(--primary-color);
 
 
 
296
  }
297
 
298
- /* Loading States */
299
- .loading {
300
- opacity: 0.6;
301
- pointer-events: none;
302
  }
303
 
304
  /* Responsive Design */
305
  @media (max-width: 768px) {
306
  .emr-header-content {
307
  flex-direction: column;
 
308
  align-items: stretch;
309
- text-align: center;
310
  }
311
 
312
- .patient-info-header {
313
- align-items: center;
 
 
 
 
314
  text-align: center;
315
  }
316
 
317
- .emr-main {
318
- padding: 1rem;
319
  }
320
 
321
- .emr-grid {
322
- grid-template-columns: 1fr;
323
  }
324
 
325
- .emr-actions {
326
  flex-direction: column;
327
  }
328
 
329
- .add-medication {
 
 
 
 
 
 
 
 
330
  flex-direction: column;
331
- align-items: stretch;
332
  }
333
  }
334
 
335
- @media (max-width: 720px) {
336
- .emr-grid .row,
337
- .emr-grid .row.contact-info {
338
- grid-template-columns: 1fr;
339
  }
340
- }
341
 
342
- /* Dark Theme Adjustments */
343
- [data-theme="dark"] .emr-field input,
344
- [data-theme="dark"] .emr-field select,
345
- [data-theme="dark"] .emr-field textarea {
346
- background-color: var(--bg-secondary);
347
- }
348
 
349
- [data-theme="dark"] .medications-list {
350
- background-color: var(--bg-secondary);
351
- }
352
 
353
- [data-theme="dark"] .session-item {
354
- background-color: var(--bg-secondary);
355
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* EMR Page Specific Styles */
2
+
3
  .emr-container {
4
  min-height: 100vh;
5
+ background-color: var(--bg-secondary);
 
6
  }
7
 
8
+ /* Header */
9
  .emr-header {
10
+ background-color: var(--bg-primary);
11
  border-bottom: 1px solid var(--border-color);
12
+ padding: var(--spacing-lg);
13
  position: sticky;
14
  top: 0;
15
  z-index: 100;
 
18
  .emr-header-content {
19
  max-width: 1200px;
20
  margin: 0 auto;
 
21
  display: flex;
 
22
  justify-content: space-between;
23
+ align-items: center;
 
24
  }
25
 
26
+ .emr-title {
27
+ display: flex;
28
  align-items: center;
29
+ gap: var(--spacing-md);
 
 
 
 
 
30
  }
31
 
32
+ .emr-title i {
33
+ font-size: 1.5rem;
34
+ color: var(--primary-color);
35
  }
36
 
37
+ .emr-title h1 {
38
  margin: 0;
39
+ color: var(--text-primary);
40
  font-size: 1.5rem;
41
  font-weight: 600;
 
42
  }
43
 
44
+ .emr-actions {
45
  display: flex;
46
+ gap: var(--spacing-md);
47
+ align-items: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
 
50
+ /* Patient Info Bar */
51
+ .patient-info-bar {
52
+ background-color: var(--bg-primary);
53
+ border-bottom: 1px solid var(--border-color);
54
+ padding: var(--spacing-lg);
55
  max-width: 1200px;
56
  margin: 0 auto;
 
57
  }
58
 
59
+ .patient-info {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: var(--spacing-md);
63
+ margin-bottom: var(--spacing-md);
 
64
  }
65
 
66
+ .patient-avatar {
67
+ width: 48px;
68
+ height: 48px;
69
+ border-radius: 50%;
70
+ background-color: var(--primary-color);
71
+ color: white;
72
  display: flex;
73
  align-items: center;
74
+ justify-content: center;
75
+ font-size: 1.25rem;
76
  }
77
 
78
+ .patient-details h3 {
79
+ margin: 0;
80
+ color: var(--text-primary);
81
+ font-size: 1.125rem;
82
  }
83
 
84
+ .patient-details p {
85
+ margin: 0;
86
+ color: var(--text-secondary);
87
+ font-size: 0.875rem;
88
  }
89
 
90
+ .patient-stats {
91
  display: grid;
92
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
93
+ gap: var(--spacing-md);
94
  }
95
 
96
+ .stat-item {
97
+ background-color: var(--bg-secondary);
98
+ padding: var(--spacing-md);
99
+ border-radius: 8px;
100
+ text-align: center;
101
+ border: 1px solid var(--border-color);
102
  }
103
 
104
+ .stat-value {
105
+ font-size: 1.5rem;
106
+ font-weight: 600;
107
+ color: var(--primary-color);
108
+ margin-bottom: var(--spacing-xs);
109
  }
110
 
111
+ .stat-label {
112
+ font-size: 0.75rem;
113
+ color: var(--text-secondary);
114
+ text-transform: uppercase;
115
+ letter-spacing: 0.05em;
116
  }
117
 
118
+ /* Controls */
119
+ .emr-controls {
120
+ background-color: var(--bg-primary);
121
+ border-bottom: 1px solid var(--border-color);
122
+ padding: var(--spacing-lg);
123
+ max-width: 1200px;
124
+ margin: 0 auto;
125
+ }
126
+
127
+ .search-container {
128
+ display: flex;
129
+ gap: var(--spacing-md);
130
+ margin-bottom: var(--spacing-md);
131
  }
132
 
133
+ .search-input {
134
+ flex: 1;
135
+ padding: var(--spacing-md);
 
136
  border: 1px solid var(--border-color);
137
+ border-radius: 8px;
138
+ font-size: 0.875rem;
 
 
139
  transition: border-color var(--transition-fast);
140
  }
141
 
142
+ .search-input:focus {
 
 
143
  outline: none;
144
  border-color: var(--primary-color);
145
+ box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
146
  }
147
 
148
+ .search-btn {
149
+ padding: var(--spacing-md) var(--spacing-lg);
150
+ background-color: var(--primary-color);
151
+ color: white;
152
+ border: none;
153
+ border-radius: 8px;
154
+ cursor: pointer;
155
+ transition: background-color var(--transition-fast);
156
  }
157
 
158
+ .search-btn:hover {
159
+ background-color: var(--primary-hover);
 
 
 
160
  }
161
 
162
+ .filter-container {
163
  display: flex;
164
+ gap: var(--spacing-md);
165
+ }
166
+
167
+ .filter-select {
168
+ padding: var(--spacing-sm) var(--spacing-md);
169
  border: 1px solid var(--border-color);
170
  border-radius: 6px;
171
  background-color: var(--bg-primary);
172
+ font-size: 0.875rem;
173
+ cursor: pointer;
174
  }
175
 
176
+ /* Content */
177
+ .emr-content {
178
+ max-width: 1200px;
179
+ margin: 0 auto;
180
+ padding: var(--spacing-lg);
181
+ }
182
+
183
+ /* Table */
184
+ .emr-table-container {
185
+ background-color: var(--bg-primary);
186
+ border-radius: 12px;
187
+ border: 1px solid var(--border-color);
188
+ overflow: hidden;
189
+ box-shadow: var(--shadow-sm);
190
+ }
191
+
192
+ .emr-table {
193
+ width: 100%;
194
+ border-collapse: collapse;
195
+ }
196
+
197
+ .emr-table th {
198
+ background-color: var(--bg-tertiary);
199
+ padding: var(--spacing-md);
200
+ text-align: left;
201
+ font-weight: 600;
202
+ color: var(--text-primary);
203
+ border-bottom: 1px solid var(--border-color);
204
+ font-size: 0.875rem;
205
+ }
206
+
207
+ .emr-table td {
208
+ padding: var(--spacing-md);
209
+ border-bottom: 1px solid var(--border-color);
210
+ vertical-align: top;
211
+ font-size: 0.875rem;
212
+ }
213
+
214
+ .emr-table tbody tr:hover {
215
+ background-color: var(--bg-secondary);
216
+ }
217
+
218
+ .emr-table tbody tr:last-child td {
219
+ border-bottom: none;
220
+ }
221
+
222
+ /* EMR Entry Types */
223
+ .emr-type {
224
+ display: inline-block;
225
+ padding: var(--spacing-xs) var(--spacing-sm);
226
+ border-radius: 4px;
227
+ font-size: 0.75rem;
228
  font-weight: 500;
229
+ text-transform: uppercase;
230
+ letter-spacing: 0.05em;
231
  }
232
 
233
+ .emr-type-diagnosis {
234
+ background-color: #fef3c7;
235
+ color: #92400e;
236
+ }
237
+
238
+ .emr-type-medication {
239
+ background-color: #dbeafe;
240
+ color: #1e40af;
 
241
  }
242
 
243
+ .emr-type-vitals {
244
+ background-color: #f3e8ff;
245
+ color: #7c3aed;
246
  }
247
 
248
+ .emr-type-lab {
249
+ background-color: #ecfdf5;
250
+ color: #065f46;
251
+ }
252
+
253
+ .emr-type-general {
254
+ background-color: var(--bg-tertiary);
255
+ color: var(--text-secondary);
256
+ }
257
+
258
+ /* Confidence Score */
259
+ .confidence-score {
260
  display: flex;
 
261
  align-items: center;
262
+ gap: var(--spacing-xs);
263
  }
264
 
265
+ .confidence-bar {
266
+ width: 60px;
267
+ height: 4px;
268
+ background-color: var(--bg-tertiary);
269
+ border-radius: 2px;
270
+ overflow: hidden;
271
+ }
272
+
273
+ .confidence-fill {
274
+ height: 100%;
275
+ transition: width var(--transition-normal);
276
  }
277
 
278
+ .confidence-fill.high {
279
+ background-color: var(--success-color);
280
  }
281
 
282
+ .confidence-fill.medium {
283
+ background-color: var(--warning-color);
284
+ }
285
+
286
+ .confidence-fill.low {
287
+ background-color: var(--accent-color);
288
+ }
289
+
290
+ .confidence-text {
291
+ font-size: 0.75rem;
292
+ color: var(--text-secondary);
293
+ font-weight: 500;
294
+ }
295
+
296
+ /* Action Buttons */
297
+ .action-buttons {
298
  display: flex;
299
+ gap: var(--spacing-xs);
 
300
  }
301
 
302
+ .action-btn {
303
+ background: none;
304
+ border: none;
305
+ color: var(--text-muted);
 
306
  cursor: pointer;
307
+ padding: var(--spacing-xs);
308
+ border-radius: 4px;
309
  transition: all var(--transition-fast);
310
+ font-size: 0.875rem;
311
  }
312
 
313
+ .action-btn:hover {
 
314
  background-color: var(--bg-tertiary);
315
+ color: var(--primary-color);
316
  }
317
 
318
+ .action-btn.danger:hover {
319
+ color: var(--accent-color);
320
+ }
321
+
322
+ /* Loading and Empty States */
323
+ .loading-state, .empty-state {
324
+ text-align: center;
325
+ padding: var(--spacing-2xl);
326
+ background-color: var(--bg-primary);
327
+ border-radius: 12px;
328
+ border: 1px solid var(--border-color);
329
+ }
330
+
331
+ .loading-spinner {
332
+ font-size: 2rem;
333
+ color: var(--primary-color);
334
+ margin-bottom: var(--spacing-md);
335
+ }
336
+
337
+ .empty-icon {
338
+ font-size: 3rem;
339
+ color: var(--text-muted);
340
+ margin-bottom: var(--spacing-lg);
341
+ }
342
+
343
+ .empty-state h3 {
344
+ margin: 0 0 var(--spacing-md) 0;
345
  color: var(--text-primary);
 
346
  }
347
 
348
+ .empty-state p {
349
+ margin: 0 0 var(--spacing-lg) 0;
 
 
 
350
  color: var(--text-secondary);
351
  }
352
 
353
+ /* EMR Detail Modal */
354
+ .emr-detail-modal {
355
+ max-width: 800px;
356
+ max-height: 80vh;
357
+ overflow-y: auto;
358
  }
359
 
360
+ .emr-detail-section {
361
+ margin-bottom: var(--spacing-lg);
362
  }
363
 
364
+ .emr-detail-section h4 {
365
+ margin: 0 0 var(--spacing-md) 0;
366
+ color: var(--text-primary);
367
+ font-size: 1rem;
368
+ border-bottom: 1px solid var(--border-color);
369
+ padding-bottom: var(--spacing-sm);
370
  }
371
 
372
+ .emr-detail-list {
373
+ list-style: none;
374
+ padding: 0;
375
+ margin: 0;
 
 
 
 
 
 
 
 
 
376
  }
377
 
378
+ .emr-detail-list li {
379
+ padding: var(--spacing-sm) 0;
380
+ border-bottom: 1px solid var(--bg-tertiary);
381
  }
382
 
383
+ .emr-detail-list li:last-child {
384
+ border-bottom: none;
 
385
  }
386
 
387
+ .medication-item {
388
+ background-color: var(--bg-secondary);
389
+ padding: var(--spacing-md);
390
+ border-radius: 8px;
391
+ margin-bottom: var(--spacing-sm);
392
+ border: 1px solid var(--border-color);
393
+ }
394
+
395
+ .medication-name {
396
+ font-weight: 600;
397
  color: var(--text-primary);
398
+ margin-bottom: var(--spacing-xs);
399
+ }
400
+
401
+ .medication-details {
402
+ font-size: 0.875rem;
403
+ color: var(--text-secondary);
404
+ }
405
+
406
+ .vital-signs-grid {
407
+ display: grid;
408
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
409
+ gap: var(--spacing-md);
410
+ }
411
+
412
+ .vital-sign-item {
413
+ background-color: var(--bg-secondary);
414
+ padding: var(--spacing-md);
415
+ border-radius: 8px;
416
+ text-align: center;
417
  border: 1px solid var(--border-color);
418
  }
419
 
420
+ .vital-sign-label {
421
+ font-size: 0.75rem;
422
+ color: var(--text-secondary);
423
+ text-transform: uppercase;
424
+ letter-spacing: 0.05em;
425
+ margin-bottom: var(--spacing-xs);
426
  }
427
 
428
+ .vital-sign-value {
429
+ font-size: 1.125rem;
430
+ font-weight: 600;
431
+ color: var(--text-primary);
432
  }
433
 
434
  /* Responsive Design */
435
  @media (max-width: 768px) {
436
  .emr-header-content {
437
  flex-direction: column;
438
+ gap: var(--spacing-md);
439
  align-items: stretch;
 
440
  }
441
 
442
+ .emr-actions {
443
+ justify-content: center;
444
+ }
445
+
446
+ .patient-info {
447
+ flex-direction: column;
448
  text-align: center;
449
  }
450
 
451
+ .patient-stats {
452
+ grid-template-columns: repeat(2, 1fr);
453
  }
454
 
455
+ .search-container {
456
+ flex-direction: column;
457
  }
458
 
459
+ .filter-container {
460
  flex-direction: column;
461
  }
462
 
463
+ .emr-table-container {
464
+ overflow-x: auto;
465
+ }
466
+
467
+ .emr-table {
468
+ min-width: 600px;
469
+ }
470
+
471
+ .action-buttons {
472
  flex-direction: column;
 
473
  }
474
  }
475
 
476
+ /* Dark Theme Support */
477
+ @media (prefers-color-scheme: dark) {
478
+ .emr-container {
479
+ background-color: var(--dark-bg-secondary);
480
  }
 
481
 
482
+ .emr-header {
483
+ background-color: var(--dark-bg-primary);
484
+ border-bottom-color: var(--dark-border-color);
485
+ }
 
 
486
 
487
+ .emr-title h1 {
488
+ color: var(--dark-text-primary);
489
+ }
490
 
491
+ .patient-info-bar {
492
+ background-color: var(--dark-bg-primary);
493
+ border-bottom-color: var(--dark-border-color);
494
+ }
495
+
496
+ .patient-details h3 {
497
+ color: var(--dark-text-primary);
498
+ }
499
+
500
+ .patient-details p {
501
+ color: var(--dark-text-secondary);
502
+ }
503
+
504
+ .emr-controls {
505
+ background-color: var(--dark-bg-primary);
506
+ border-bottom-color: var(--dark-border-color);
507
+ }
508
+
509
+ .search-input, .filter-select {
510
+ background-color: var(--dark-bg-secondary);
511
+ border-color: var(--dark-border-color);
512
+ color: var(--dark-text-primary);
513
+ }
514
+
515
+ .emr-table-container {
516
+ background-color: var(--dark-bg-primary);
517
+ border-color: var(--dark-border-color);
518
+ }
519
+
520
+ .emr-table th {
521
+ background-color: var(--dark-bg-tertiary);
522
+ color: var(--dark-text-primary);
523
+ border-bottom-color: var(--dark-border-color);
524
+ }
525
+
526
+ .emr-table td {
527
+ border-bottom-color: var(--dark-border-color);
528
+ color: var(--dark-text-primary);
529
+ }
530
+
531
+ .emr-table tbody tr:hover {
532
+ background-color: var(--dark-bg-secondary);
533
+ }
534
+ }
static/css/styles.css CHANGED
@@ -481,6 +481,96 @@ body {
481
  text-align: right;
482
  }
483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  /* Chat Input */
485
  .chat-input-container {
486
  padding: var(--spacing-lg);
 
481
  text-align: right;
482
  }
483
 
484
+ /* EMR Button Styles */
485
+ .message-actions {
486
+ display: flex;
487
+ align-items: center;
488
+ margin-left: var(--spacing-sm);
489
+ opacity: 0;
490
+ transition: opacity var(--transition-fast);
491
+ }
492
+
493
+ .message:hover .message-actions {
494
+ opacity: 1;
495
+ }
496
+
497
+ .emr-extract-btn {
498
+ background: none;
499
+ border: none;
500
+ color: var(--text-muted);
501
+ cursor: pointer;
502
+ padding: var(--spacing-sm);
503
+ border-radius: 6px;
504
+ transition: all var(--transition-fast);
505
+ font-size: 0.875rem;
506
+ }
507
+
508
+ .emr-extract-btn:hover {
509
+ background-color: var(--bg-tertiary);
510
+ color: var(--primary-color);
511
+ transform: scale(1.1);
512
+ }
513
+
514
+ .emr-extract-btn:disabled {
515
+ opacity: 0.6;
516
+ cursor: not-allowed;
517
+ transform: none;
518
+ }
519
+
520
+ /* Notification Styles */
521
+ .notification {
522
+ position: fixed;
523
+ top: var(--spacing-lg);
524
+ right: var(--spacing-lg);
525
+ background-color: var(--bg-primary);
526
+ border: 1px solid var(--border-color);
527
+ border-radius: 8px;
528
+ box-shadow: var(--shadow-lg);
529
+ padding: var(--spacing-md);
530
+ z-index: 1000;
531
+ transform: translateX(100%);
532
+ transition: transform var(--transition-normal);
533
+ max-width: 300px;
534
+ }
535
+
536
+ .notification.show {
537
+ transform: translateX(0);
538
+ }
539
+
540
+ .notification-content {
541
+ display: flex;
542
+ align-items: center;
543
+ gap: var(--spacing-sm);
544
+ }
545
+
546
+ .notification-success {
547
+ border-left: 4px solid var(--success-color);
548
+ }
549
+
550
+ .notification-error {
551
+ border-left: 4px solid var(--accent-color);
552
+ }
553
+
554
+ .notification-info {
555
+ border-left: 4px solid var(--primary-color);
556
+ }
557
+
558
+ .notification-content i {
559
+ font-size: 1.125rem;
560
+ }
561
+
562
+ .notification-success .notification-content i {
563
+ color: var(--success-color);
564
+ }
565
+
566
+ .notification-error .notification-content i {
567
+ color: var(--accent-color);
568
+ }
569
+
570
+ .notification-info .notification-content i {
571
+ color: var(--primary-color);
572
+ }
573
+
574
  /* Chat Input */
575
  .chat-input-container {
576
  padding: var(--spacing-lg);
static/emr.html CHANGED
@@ -3,139 +3,164 @@
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
- <!-- Row 1: Name and Age -->
35
- <div class="row">
36
- <div class="emr-field">
37
- <label>Full Name</label>
38
- <input type="text" id="patientNameInput" placeholder="Enter patient name">
39
- </div>
40
- <div class="emr-field">
41
- <label>Age</label>
42
- <input type="number" id="patientAgeInput" placeholder="Enter age" min="0" max="150">
43
- </div>
44
- </div>
45
- <!-- Row 2: Sex and Ethnicity -->
46
- <div class="row">
47
- <div class="emr-field">
48
- <label>Sex</label>
49
- <select id="patientSexInput">
50
- <option value="Male">Male</option>
51
- <option value="Female">Female</option>
52
- <option value="Intersex">Intersex</option>
53
- </select>
54
- </div>
55
- <div class="emr-field">
56
- <label>Ethnicity</label>
57
- <input type="ethnicity" id="patientEthnicityInput" placeholder="Enter ethnicity">
58
- </div>
59
- </div>
60
- <!-- Row 3: Contact Information -->
61
- <div class="row contact-info">
62
- <div class="emr-field">
63
- <label>Phone</label>
64
- <input type="tel" id="patientPhoneInput" placeholder="Enter phone number">
65
- </div>
66
- <div class="emr-field">
67
- <label>Email</label>
68
- <input type="email" id="patientEmailInput" placeholder="Enter email address">
69
- </div>
70
- <div class="emr-field">
71
- <label>Address</label>
72
- <input type="text" id="patientAddressInput" placeholder="Enter full address">
73
- </div>
74
- </div>
75
  </div>
76
- </section>
 
 
 
 
77
 
78
- <!-- Medical Information -->
79
- <section class="emr-section">
80
- <h2><i class="fas fa-pills"></i> Medical Information</h2>
81
- <div class="emr-grid">
82
- <div class="row">
83
- <div class="emr-field">
84
- <label>Current Medications</label>
85
- <div class="medications-container">
86
- <div class="medications-list" id="medicationsList">
87
- <!-- Medications will be added here dynamically -->
88
- </div>
89
- <div class="add-medication">
90
- <input type="text" id="newMedicationInput" placeholder="Add new medication">
91
- <button id="addMedicationBtn" class="btn-secondary">
92
- <i class="fas fa-plus"></i> Add
93
- </button>
94
- </div>
95
- </div>
96
- </div>
97
- <div class="emr-field">
98
- <label>Past Assessment Summary</label>
99
- <textarea id="pastAssessmentInput" placeholder="Enter past assessment summary" rows="4"></textarea>
100
- </div>
101
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  </div>
103
- </section>
 
104
 
105
- <!-- Recent Sessions -->
106
- <section class="emr-section">
107
- <h2><i class="fas fa-comments"></i> Recent Chat Sessions</h2>
108
- <div class="sessions-container" id="sessionsContainer">
109
- <!-- Sessions will be loaded here -->
110
  </div>
111
- </section>
 
 
 
 
 
 
 
112
 
113
- <!-- Actions -->
114
- <section class="emr-section">
115
- <h2><i class="fas fa-cog"></i> Actions</h2>
116
- <div class="emr-actions">
117
- <button id="savePatientBtn" class="btn-primary">
118
- <i class="fas fa-save"></i> Save Changes
119
- </button>
120
- <button id="refreshPatientBtn" class="btn-secondary">
121
- <i class="fas fa-refresh"></i> Refresh Data
122
- </button>
123
- <button id="exportPatientBtn" class="btn-secondary">
124
- <i class="fas fa-download"></i> Export EMR
125
- </button>
126
  </div>
127
- </section>
128
- </main>
129
- </div>
 
 
 
 
 
 
130
 
131
- <!-- Loading Overlay -->
132
- <div class="loading-overlay" id="loadingOverlay">
133
- <div class="loading-spinner">
134
- <i class="fas fa-spinner fa-spin"></i>
135
- <div>Loading...</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  </div>
137
  </div>
138
 
139
  <script src="/static/js/emr.js"></script>
140
  </body>
141
- </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>EMR - Medical AI Assistant</title>
7
+ <link rel="icon" type="image/svg+xml" href="/static/icon.svg">
8
  <link rel="stylesheet" href="/static/css/styles.css">
9
  <link rel="stylesheet" href="/static/css/emr.css">
10
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
11
  </head>
12
  <body>
13
  <div class="emr-container">
14
  <!-- Header -->
15
+ <div class="emr-header">
16
  <div class="emr-header-content">
17
+ <div class="emr-title">
18
+ <i class="fas fa-file-medical"></i>
19
+ <h1>Electronic Medical Records</h1>
20
+ </div>
21
+ <div class="emr-actions">
22
+ <button class="btn btn-secondary" id="refreshBtn">
23
+ <i class="fas fa-sync-alt"></i>
24
+ Refresh
25
+ </button>
26
+ <button class="btn btn-primary" id="searchBtn">
27
+ <i class="fas fa-search"></i>
28
+ Search
29
+ </button>
30
+ <a href="/static/index.html" class="btn btn-outline">
31
+ <i class="fas fa-arrow-left"></i>
32
+ Back to Chat
33
+ </a>
34
  </div>
35
  </div>
36
+ </div>
37
 
38
+ <!-- Patient Info Bar -->
39
+ <div class="patient-info-bar" id="patientInfoBar" style="display: none;">
40
+ <div class="patient-info">
41
+ <div class="patient-avatar">
42
+ <i class="fas fa-user"></i>
43
+ </div>
44
+ <div class="patient-details">
45
+ <h3 id="patientName">Patient Name</h3>
46
+ <p id="patientDetails">Age: -- | Sex: -- | ID: --</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  </div>
48
+ </div>
49
+ <div class="patient-stats" id="patientStats">
50
+ <!-- Stats will be populated here -->
51
+ </div>
52
+ </div>
53
 
54
+ <!-- Search and Filters -->
55
+ <div class="emr-controls">
56
+ <div class="search-container">
57
+ <input type="text" id="searchInput" placeholder="Search EMR entries..." class="search-input">
58
+ <button class="search-btn" id="searchButton">
59
+ <i class="fas fa-search"></i>
60
+ </button>
61
+ </div>
62
+ <div class="filter-container">
63
+ <select id="dateFilter" class="filter-select">
64
+ <option value="all">All Time</option>
65
+ <option value="today">Today</option>
66
+ <option value="week">This Week</option>
67
+ <option value="month">This Month</option>
68
+ </select>
69
+ <select id="typeFilter" class="filter-select">
70
+ <option value="all">All Types</option>
71
+ <option value="diagnosis">Diagnosis</option>
72
+ <option value="medication">Medication</option>
73
+ <option value="vitals">Vital Signs</option>
74
+ <option value="lab">Lab Results</option>
75
+ </select>
76
+ </div>
77
+ </div>
78
+
79
+ <!-- EMR Entries Table -->
80
+ <div class="emr-content">
81
+ <div class="emr-table-container">
82
+ <table class="emr-table" id="emrTable">
83
+ <thead>
84
+ <tr>
85
+ <th>Date & Time</th>
86
+ <th>Type</th>
87
+ <th>Diagnosis</th>
88
+ <th>Medications</th>
89
+ <th>Vital Signs</th>
90
+ <th>Confidence</th>
91
+ <th>Actions</th>
92
+ </tr>
93
+ </thead>
94
+ <tbody id="emrTableBody">
95
+ <!-- EMR entries will be populated here -->
96
+ </tbody>
97
+ </table>
98
+ </div>
99
+
100
+ <!-- Loading State -->
101
+ <div class="loading-state" id="loadingState" style="display: none;">
102
+ <div class="loading-spinner">
103
+ <i class="fas fa-spinner fa-spin"></i>
104
  </div>
105
+ <p>Loading EMR data...</p>
106
+ </div>
107
 
108
+ <!-- Empty State -->
109
+ <div class="empty-state" id="emptyState" style="display: none;">
110
+ <div class="empty-icon">
111
+ <i class="fas fa-file-medical"></i>
 
112
  </div>
113
+ <h3>No EMR Data Found</h3>
114
+ <p>No medical records found for this patient. EMR data will appear here when you extract information from chat messages.</p>
115
+ <a href="/static/index.html" class="btn btn-primary">
116
+ <i class="fas fa-comments"></i>
117
+ Go to Chat
118
+ </a>
119
+ </div>
120
+ </div>
121
 
122
+ <!-- EMR Detail Modal -->
123
+ <div class="modal" id="emrDetailModal">
124
+ <div class="modal-content emr-detail-modal">
125
+ <div class="modal-header">
126
+ <h3>EMR Entry Details</h3>
127
+ <button class="modal-close" id="emrDetailModalClose">&times;</button>
 
 
 
 
 
 
 
128
  </div>
129
+ <div class="modal-body" id="emrDetailContent">
130
+ <!-- EMR details will be populated here -->
131
+ </div>
132
+ <div class="modal-footer">
133
+ <button class="btn btn-secondary" id="emrDetailModalCancel">Close</button>
134
+ <button class="btn btn-danger" id="deleteEmrBtn" style="display: none;">Delete Entry</button>
135
+ </div>
136
+ </div>
137
+ </div>
138
 
139
+ <!-- Search Modal -->
140
+ <div class="modal" id="searchModal">
141
+ <div class="modal-content">
142
+ <div class="modal-header">
143
+ <h3>Advanced Search</h3>
144
+ <button class="modal-close" id="searchModalClose">&times;</button>
145
+ </div>
146
+ <div class="modal-body">
147
+ <div class="form-group">
148
+ <label for="semanticSearchInput">Search by meaning (semantic search):</label>
149
+ <input type="text" id="semanticSearchInput" placeholder="e.g., 'chest pain treatment' or 'diabetes medication'">
150
+ </div>
151
+ <div class="form-group">
152
+ <label for="exactSearchInput">Search by exact text:</label>
153
+ <input type="text" id="exactSearchInput" placeholder="e.g., 'hypertension' or 'metformin'">
154
+ </div>
155
+ </div>
156
+ <div class="modal-footer">
157
+ <button class="btn btn-secondary" id="searchModalCancel">Cancel</button>
158
+ <button class="btn btn-primary" id="performSearchBtn">Search</button>
159
+ </div>
160
+ </div>
161
  </div>
162
  </div>
163
 
164
  <script src="/static/js/emr.js"></script>
165
  </body>
166
+ </html>
static/index.html CHANGED
@@ -50,7 +50,7 @@
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>
 
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="/static/emr.html" id="emrLink" class="emr-link">
54
  <i class="fas fa-file-medical"></i> EMR
55
  </a>
56
  </div>
static/js/app.js CHANGED
@@ -1830,15 +1830,30 @@ How can I assist you today?`;
1830
  messageElement.id = `message-${message.id}`;
1831
  const avatar = message.role === 'user' ? '<i class="fas fa-user"></i>' : '<i class="fas fa-robot"></i>';
1832
  const time = this.formatTime(message.timestamp);
 
 
 
 
 
 
 
 
 
1833
  messageElement.innerHTML = `
1834
  <div class="message-avatar">${avatar}</div>
1835
  <div class="message-content">
1836
  <div class="message-text">${this.formatMessageContent(message.content)}</div>
1837
  <div class="message-time">${time}</div>
1838
- </div>`;
 
1839
  chatMessages.appendChild(messageElement);
1840
  chatMessages.scrollTop = chatMessages.scrollHeight;
1841
  if (this.currentSession) this.currentSession.lastActivity = new Date().toISOString();
 
 
 
 
 
1842
  }
1843
 
1844
  formatMessageContent(content) {
@@ -1871,6 +1886,129 @@ How can I assist you today?`;
1871
  if (diff < 86400000) { const hours = Math.floor(diff / 3600000); return `${hours} hour${hours > 1 ? 's' : ''} ago`; }
1872
  return date.toLocaleDateString();
1873
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1874
  }
1875
  // ----------------------------------------------------------
1876
  // Additional UI setup END
 
1830
  messageElement.id = `message-${message.id}`;
1831
  const avatar = message.role === 'user' ? '<i class="fas fa-user"></i>' : '<i class="fas fa-robot"></i>';
1832
  const time = this.formatTime(message.timestamp);
1833
+
1834
+ // Add EMR icon for assistant messages (system-generated)
1835
+ const emrIcon = message.role === 'assistant' ?
1836
+ `<div class="message-actions">
1837
+ <button class="emr-extract-btn" onclick="app.extractEMR('${message.id}')" title="Extract to EMR" data-message-id="${message.id}">
1838
+ <i class="fas fa-file-medical"></i>
1839
+ </button>
1840
+ </div>` : '';
1841
+
1842
  messageElement.innerHTML = `
1843
  <div class="message-avatar">${avatar}</div>
1844
  <div class="message-content">
1845
  <div class="message-text">${this.formatMessageContent(message.content)}</div>
1846
  <div class="message-time">${time}</div>
1847
+ </div>
1848
+ ${emrIcon}`;
1849
  chatMessages.appendChild(messageElement);
1850
  chatMessages.scrollTop = chatMessages.scrollHeight;
1851
  if (this.currentSession) this.currentSession.lastActivity = new Date().toISOString();
1852
+
1853
+ // Check EMR status for assistant messages
1854
+ if (message.role === 'assistant' && this.currentPatientId) {
1855
+ this.checkEMRStatus(message.id);
1856
+ }
1857
  }
1858
 
1859
  formatMessageContent(content) {
 
1886
  if (diff < 86400000) { const hours = Math.floor(diff / 3600000); return `${hours} hour${hours > 1 ? 's' : ''} ago`; }
1887
  return date.toLocaleDateString();
1888
  }
1889
+
1890
+ async extractEMR(messageId) {
1891
+ try {
1892
+ // Check if patient is selected
1893
+ if (!this.currentPatientId) {
1894
+ alert('Please select a patient before extracting EMR data.');
1895
+ return;
1896
+ }
1897
+
1898
+ // Check if doctor is logged in
1899
+ if (!this.currentUser) {
1900
+ alert('Please log in as a doctor before extracting EMR data.');
1901
+ return;
1902
+ }
1903
+
1904
+ // Find the message
1905
+ const message = this.currentSession?.messages?.find(m => m.id === messageId);
1906
+ if (!message) {
1907
+ console.error('Message not found:', messageId);
1908
+ return;
1909
+ }
1910
+
1911
+ // Show loading state
1912
+ const button = document.querySelector(`[onclick="app.extractEMR('${messageId}')"]`);
1913
+ if (button) {
1914
+ button.disabled = true;
1915
+ button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
1916
+ }
1917
+
1918
+ // Call EMR extraction API
1919
+ const response = await fetch('/emr/extract', {
1920
+ method: 'POST',
1921
+ headers: {
1922
+ 'Content-Type': 'application/json',
1923
+ },
1924
+ body: JSON.stringify({
1925
+ patient_id: this.currentPatientId,
1926
+ doctor_id: this.currentUser.id || 'default-doctor',
1927
+ message_id: messageId,
1928
+ session_id: this.currentSession?.id || 'default-session',
1929
+ message: message.content
1930
+ })
1931
+ });
1932
+
1933
+ if (response.ok) {
1934
+ const result = await response.json();
1935
+ console.log('EMR extraction successful:', result);
1936
+
1937
+ // Show success message
1938
+ if (button) {
1939
+ button.innerHTML = '<i class="fas fa-check"></i>';
1940
+ button.style.color = 'var(--success-color)';
1941
+ setTimeout(() => {
1942
+ button.innerHTML = '<i class="fas fa-file-medical"></i>';
1943
+ button.style.color = '';
1944
+ button.disabled = false;
1945
+ }, 2000);
1946
+ }
1947
+
1948
+ // Show notification
1949
+ this.showNotification('EMR data extracted successfully!', 'success');
1950
+ } else {
1951
+ const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
1952
+ const errorMessage = errorData.detail || `HTTP ${response.status}: ${response.statusText}`;
1953
+ throw new Error(errorMessage);
1954
+ }
1955
+
1956
+ } catch (error) {
1957
+ console.error('Error extracting EMR:', error);
1958
+
1959
+ // Reset button state
1960
+ const button = document.querySelector(`[onclick="app.extractEMR('${messageId}')"]`);
1961
+ if (button) {
1962
+ button.innerHTML = '<i class="fas fa-file-medical"></i>';
1963
+ button.disabled = false;
1964
+ }
1965
+
1966
+ // Show error message
1967
+ this.showNotification('Failed to extract EMR data. Please try again.', 'error');
1968
+ }
1969
+ }
1970
+
1971
+ async checkEMRStatus(messageId) {
1972
+ try {
1973
+ const response = await fetch(`/emr/check/${messageId}`);
1974
+ if (response.ok) {
1975
+ const result = await response.json();
1976
+ const button = document.querySelector(`[data-message-id="${messageId}"]`);
1977
+ if (button && result.emr_exists) {
1978
+ button.innerHTML = '<i class="fas fa-check"></i>';
1979
+ button.style.color = 'var(--success-color)';
1980
+ button.title = 'EMR data already extracted';
1981
+ button.disabled = true;
1982
+ }
1983
+ }
1984
+ } catch (error) {
1985
+ console.warn('Could not check EMR status:', error);
1986
+ }
1987
+ }
1988
+
1989
+ showNotification(message, type = 'info') {
1990
+ // Create notification element
1991
+ const notification = document.createElement('div');
1992
+ notification.className = `notification notification-${type}`;
1993
+ notification.innerHTML = `
1994
+ <div class="notification-content">
1995
+ <i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
1996
+ <span>${message}</span>
1997
+ </div>
1998
+ `;
1999
+
2000
+ // Add to page
2001
+ document.body.appendChild(notification);
2002
+
2003
+ // Show notification
2004
+ setTimeout(() => notification.classList.add('show'), 100);
2005
+
2006
+ // Remove after 3 seconds
2007
+ setTimeout(() => {
2008
+ notification.classList.remove('show');
2009
+ setTimeout(() => notification.remove(), 300);
2010
+ }, 3000);
2011
+ }
2012
  }
2013
  // ----------------------------------------------------------
2014
  // Additional UI setup END
static/js/emr.js CHANGED
@@ -1,291 +1,564 @@
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(`/patient/${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._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('patientEthnicityInput').value = this.patientData.ethnicity || '';
95
- document.getElementById('patientPhoneInput').value = this.patientData.phone || '';
96
- document.getElementById('patientEmailInput').value = this.patientData.email || '';
97
- document.getElementById('patientAddressInput').value = this.patientData.address || '';
98
- document.getElementById('pastAssessmentInput').value = this.patientData.past_assessment_summary || '';
99
-
100
- // Populate medications
101
- this.medications = this.patientData.medications || [];
102
- this.renderMedications();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
104
 
105
- renderMedications() {
106
- const container = document.getElementById('medicationsList');
107
- container.innerHTML = '';
108
 
109
- if (this.medications.length === 0) {
110
- container.innerHTML = '<div style="color: var(--text-secondary); font-style: italic;">No medications listed</div>';
111
  return;
112
  }
113
 
114
- this.medications.forEach((medication, index) => {
115
- const tag = document.createElement('div');
116
- tag.className = 'medication-tag';
117
- tag.innerHTML = `
118
- ${medication}
119
- <button class="remove-medication" data-index="${index}">
120
- <i class="fas fa-times"></i>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  </button>
 
 
 
122
  `;
123
- container.appendChild(tag);
124
- });
125
 
126
- // Add event listeners for remove buttons
127
- container.querySelectorAll('.remove-medication').forEach(btn => {
128
- btn.addEventListener('click', (e) => {
129
- const index = parseInt(e.target.closest('.remove-medication').dataset.index);
130
- this.removeMedication(index);
131
- });
132
- });
133
  }
134
 
135
- addMedication() {
136
- const input = document.getElementById('newMedicationInput');
137
- const medication = input.value.trim();
138
 
139
- if (!medication) return;
 
 
 
140
 
141
- this.medications.push(medication);
142
- this.renderMedications();
143
- input.value = '';
144
  }
145
 
146
- removeMedication(index) {
147
- this.medications.splice(index, 1);
148
- this.renderMedications();
 
 
 
 
 
 
 
 
 
149
  }
150
 
151
- async loadPatientSessions() {
152
  try {
153
- const resp = await fetch(`/patient/${this.patientId}/session`);
154
- if (resp.ok) {
155
- const data = await resp.json();
156
- this.sessions = data.sessions || [];
157
- this.renderSessions();
 
158
  }
159
  } catch (error) {
160
- console.error('Error loading sessions:', error);
 
161
  }
162
  }
163
 
164
- renderSessions() {
165
- const container = document.getElementById('sessionsContainer');
 
 
 
166
 
167
- if (this.sessions.length === 0) {
168
- container.innerHTML = '<div style="color: var(--text-secondary); font-style: italic;">No chat sessions found</div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  return;
170
  }
171
 
172
- container.innerHTML = '';
 
 
 
173
 
174
- this.sessions.forEach(session => {
175
- const sessionEl = document.createElement('div');
176
- sessionEl.className = 'session-item';
177
- sessionEl.innerHTML = `
178
- <div class="session-title">${session.title || 'Untitled Session'}</div>
179
- <div class="session-meta">
180
- <span class="session-date">${this.formatDate(session.created_at)}</span>
181
- <span class="session-messages">${session.message_count || 0} messages</span>
182
- </div>
183
- `;
 
184
 
185
- sessionEl.addEventListener('click', () => {
186
- // Could open session details or redirect to main page with session
187
- window.location.href = `/?session_id=${session.session_id}`;
 
 
 
 
 
 
 
 
 
 
188
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
- container.appendChild(sessionEl);
191
  });
 
 
 
 
 
 
192
  }
193
 
194
- async savePatientData() {
 
 
 
 
 
 
 
 
195
  this.showLoading(true);
196
 
197
  try {
198
- const updateData = {
199
- name: document.getElementById('patientNameInput').value.trim(),
200
- age: parseInt(document.getElementById('patientAgeInput').value) || null,
201
- sex: document.getElementById('patientSexInput').value || null,
202
- ethnicity: document.getElementById('patientEthnicityInput').value || null,
203
- phone: document.getElementById('patientPhoneInput').value.trim() || null,
204
- email: document.getElementById('patientEmailInput').value.trim() || null,
205
- address: document.getElementById('patientAddressInput').value.trim() || null,
206
- medications: this.medications,
207
- past_assessment_summary: document.getElementById('pastAssessmentInput').value.trim() || null
208
- };
209
-
210
- const resp = await fetch(`/patient/${this.patientId}`, {
211
- method: 'PATCH',
212
- headers: {
213
- 'Content-Type': 'application/json'
214
- },
215
- body: JSON.stringify(updateData)
216
- });
217
 
218
- if (resp.ok) {
219
- this.showSuccess('Patient data saved successfully!');
220
- // Update the header with new name
221
- document.getElementById('patientName').textContent = updateData.name || 'Unknown';
 
 
 
 
 
 
 
 
 
 
 
 
222
  } else {
223
- throw new Error('Failed to save patient data');
 
224
  }
 
 
 
 
225
  } catch (error) {
226
- console.error('Error saving patient data:', error);
227
- this.showError('Failed to save patient data. Please try again.');
228
  } finally {
229
  this.showLoading(false);
230
  }
231
  }
232
 
233
- exportPatientData() {
234
- if (!this.patientData) {
235
- this.showError('No patient data to export');
236
- return;
 
 
 
 
 
 
237
  }
238
-
239
- const exportData = {
240
- _id: this.patientData._id,
241
- name: this.patientData.name,
242
- age: this.patientData.age,
243
- sex: this.patientData.sex,
244
- ethnicity: this.patientData.ethnicity,
245
- phone: this.patientData.phone,
246
- email: this.patientData.email,
247
- address: this.patientData.address,
248
- medications: this.medications,
249
- past_assessment_summary: this.patientData.past_assessment_summary,
250
- created_at: this.patientData.created_at,
251
- updated_at: this.patientData.updated_at,
252
- sessions: this.sessions
253
- };
254
-
255
- const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
256
- const url = URL.createObjectURL(blob);
257
- const a = document.createElement('a');
258
- a.href = url;
259
- a.download = `patient-${this.patientData._id}-emr.json`;
260
- document.body.appendChild(a);
261
- a.click();
262
- document.body.removeChild(a);
263
- URL.revokeObjectURL(url);
264
  }
265
 
266
- formatDate(dateString) {
267
- if (!dateString) return 'Unknown date';
268
- const date = new Date(dateString);
269
- return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
270
- }
271
 
272
- showLoading(show) {
273
- const overlay = document.getElementById('loadingOverlay');
274
- if (overlay) {
275
- overlay.style.display = show ? 'flex' : 'none';
276
- }
277
  }
278
 
279
- showError(message) {
280
- alert('Error: ' + message);
281
- }
 
 
 
 
 
282
 
283
- showSuccess(message) {
284
- alert('Success: ' + message);
285
  }
286
  }
287
 
288
- // Initialize EMR when DOM is loaded
289
  document.addEventListener('DOMContentLoaded', () => {
290
- new PatientEMR();
291
  });
 
1
  // EMR Page JavaScript
2
+ // static/js/emr.js
3
+
4
+ class EMRPage {
5
  constructor() {
6
+ this.currentPatientId = null;
7
+ this.currentPatient = null;
8
+ this.emrEntries = [];
9
+ this.filteredEntries = [];
10
+ this.currentPage = 1;
11
+ this.entriesPerPage = 20;
12
 
13
  this.init();
14
  }
15
 
16
  async init() {
 
 
 
 
 
 
 
 
17
  this.setupEventListeners();
18
+ await this.loadPatientFromURL();
19
+ if (this.currentPatientId) {
20
+ await this.loadEMRData();
21
+ await this.loadPatientStats();
22
+ } else {
23
+ this.showEmptyState();
24
+ }
25
  }
26
 
27
  setupEventListeners() {
28
+ // Refresh button
29
+ document.getElementById('refreshBtn').addEventListener('click', () => {
30
+ this.loadEMRData();
31
  });
32
 
33
+ // Search button
34
+ document.getElementById('searchBtn').addEventListener('click', () => {
35
+ this.openSearchModal();
36
  });
37
 
38
+ // Search input
39
+ document.getElementById('searchInput').addEventListener('input', (e) => {
40
+ this.filterEntries(e.target.value);
41
  });
42
 
43
+ // Filter selects
44
+ document.getElementById('dateFilter').addEventListener('change', () => {
45
+ this.applyFilters();
46
  });
47
 
48
+ document.getElementById('typeFilter').addEventListener('change', () => {
49
+ this.applyFilters();
 
 
 
50
  });
51
+
52
+ // Modal handlers
53
+ this.setupModalHandlers();
54
  }
55
 
56
+ setupModalHandlers() {
57
+ // EMR Detail Modal
58
+ const emrDetailModal = document.getElementById('emrDetailModal');
59
+ const emrDetailModalClose = document.getElementById('emrDetailModalClose');
60
+ const emrDetailModalCancel = document.getElementById('emrDetailModalCancel');
61
 
62
+ if (emrDetailModalClose) {
63
+ emrDetailModalClose.addEventListener('click', () => {
64
+ emrDetailModal.classList.remove('show');
65
+ });
66
+ }
67
+
68
+ if (emrDetailModalCancel) {
69
+ emrDetailModalCancel.addEventListener('click', () => {
70
+ emrDetailModal.classList.remove('show');
71
+ });
72
+ }
73
+
74
+ // Search Modal
75
+ const searchModal = document.getElementById('searchModal');
76
+ const searchModalClose = document.getElementById('searchModalClose');
77
+ const searchModalCancel = document.getElementById('searchModalCancel');
78
+ const performSearchBtn = document.getElementById('performSearchBtn');
79
+
80
+ if (searchModalClose) {
81
+ searchModalClose.addEventListener('click', () => {
82
+ searchModal.classList.remove('show');
83
+ });
84
+ }
85
+
86
+ if (searchModalCancel) {
87
+ searchModalCancel.addEventListener('click', () => {
88
+ searchModal.classList.remove('show');
89
+ });
90
+ }
91
+
92
+ if (performSearchBtn) {
93
+ performSearchBtn.addEventListener('click', () => {
94
+ this.performAdvancedSearch();
95
+ searchModal.classList.remove('show');
96
+ });
97
+ }
98
+ }
99
+
100
+ async loadPatientFromURL() {
101
+ const urlParams = new URLSearchParams(window.location.search);
102
+ const patientId = urlParams.get('patient_id');
103
+
104
+ if (patientId) {
105
+ this.currentPatientId = patientId;
106
+ await this.loadPatientInfo();
107
+ } else {
108
+ // Try to get from localStorage
109
+ const savedPatientId = localStorage.getItem('medicalChatbotPatientId');
110
+ if (savedPatientId) {
111
+ this.currentPatientId = savedPatientId;
112
+ await this.loadPatientInfo();
113
+ }
114
+ }
115
+ }
116
+
117
+ async loadPatientInfo() {
118
  try {
119
+ const response = await fetch(`/patient/${this.currentPatientId}`);
120
+ if (response.ok) {
121
+ this.currentPatient = await response.json();
122
+ this.updatePatientInfoBar();
123
+ } else {
124
+ console.error('Failed to load patient info');
125
+ this.showEmptyState();
126
  }
127
+ } catch (error) {
128
+ console.error('Error loading patient info:', error);
129
+ this.showEmptyState();
130
+ }
131
+ }
132
 
133
+ updatePatientInfoBar() {
134
+ if (!this.currentPatient) return;
135
 
136
+ const patientInfoBar = document.getElementById('patientInfoBar');
137
+ const patientName = document.getElementById('patientName');
138
+ const patientDetails = document.getElementById('patientDetails');
139
+
140
+ patientName.textContent = this.currentPatient.name;
141
+ patientDetails.textContent = `Age: ${this.currentPatient.age} | Sex: ${this.currentPatient.sex} | ID: ${this.currentPatient._id}`;
142
+
143
+ patientInfoBar.style.display = 'block';
144
+ }
145
 
146
+ async loadPatientStats() {
147
+ try {
148
+ const response = await fetch(`/emr/statistics/${this.currentPatientId}`);
149
+ if (response.ok) {
150
+ const stats = await response.json();
151
+ this.updatePatientStats(stats);
152
+ }
153
  } catch (error) {
154
+ console.error('Error loading patient stats:', error);
 
 
 
155
  }
156
  }
157
 
158
+ updatePatientStats(stats) {
159
+ const patientStats = document.getElementById('patientStats');
160
+
161
+ patientStats.innerHTML = `
162
+ <div class="stat-item">
163
+ <div class="stat-value">${stats.total_entries || 0}</div>
164
+ <div class="stat-label">Total Entries</div>
165
+ </div>
166
+ <div class="stat-item">
167
+ <div class="stat-value">${Math.round((stats.avg_confidence || 0) * 100)}%</div>
168
+ <div class="stat-label">Avg Confidence</div>
169
+ </div>
170
+ <div class="stat-item">
171
+ <div class="stat-value">${stats.diagnosis_count || 0}</div>
172
+ <div class="stat-label">Diagnoses</div>
173
+ </div>
174
+ <div class="stat-item">
175
+ <div class="stat-value">${stats.medication_count || 0}</div>
176
+ <div class="stat-label">Medications</div>
177
+ </div>
178
+ `;
179
+ }
180
+
181
+ async loadEMRData() {
182
+ if (!this.currentPatientId) return;
183
+
184
+ this.showLoading(true);
185
+
186
+ try {
187
+ const response = await fetch(`/emr/patient/${this.currentPatientId}?limit=100`);
188
+ if (response.ok) {
189
+ this.emrEntries = await response.json();
190
+ this.filteredEntries = [...this.emrEntries];
191
+ this.renderEMRTable();
192
+ } else {
193
+ console.error('Failed to load EMR data');
194
+ this.showEmptyState();
195
+ }
196
+ } catch (error) {
197
+ console.error('Error loading EMR data:', error);
198
+ this.showErrorState('Failed to load EMR data. Please try again.');
199
+ } finally {
200
+ this.showLoading(false);
201
+ }
202
  }
203
 
204
+ renderEMRTable() {
205
+ const tableBody = document.getElementById('emrTableBody');
 
206
 
207
+ if (this.filteredEntries.length === 0) {
208
+ this.showEmptyState();
209
  return;
210
  }
211
 
212
+ tableBody.innerHTML = this.filteredEntries.map(entry => {
213
+ const date = new Date(entry.created_at).toLocaleString();
214
+ const type = this.getEMRType(entry.extracted_data);
215
+ const diagnosis = entry.extracted_data.diagnosis?.slice(0, 2).join(', ') || '-';
216
+ const medications = entry.extracted_data.medications?.slice(0, 2).map(m => m.name).join(', ') || '-';
217
+ const vitals = this.formatVitalSigns(entry.extracted_data.vital_signs);
218
+ const confidence = this.formatConfidence(entry.confidence_score);
219
+
220
+ return `
221
+ <tr>
222
+ <td>${date}</td>
223
+ <td><span class="emr-type emr-type-${type}">${type}</span></td>
224
+ <td>${diagnosis}</td>
225
+ <td>${medications}</td>
226
+ <td>${vitals}</td>
227
+ <td>${confidence}</td>
228
+ <td>
229
+ <div class="action-buttons">
230
+ <button class="action-btn" onclick="emrPage.viewEMRDetail('${entry.emr_id}')" title="View Details">
231
+ <i class="fas fa-eye"></i>
232
+ </button>
233
+ <button class="action-btn danger" onclick="emrPage.deleteEMREntry('${entry.emr_id}')" title="Delete">
234
+ <i class="fas fa-trash"></i>
235
  </button>
236
+ </div>
237
+ </td>
238
+ </tr>
239
  `;
240
+ }).join('');
241
+ }
242
 
243
+ getEMRType(extractedData) {
244
+ if (extractedData.diagnosis?.length > 0) return 'diagnosis';
245
+ if (extractedData.medications?.length > 0) return 'medication';
246
+ if (extractedData.vital_signs && Object.values(extractedData.vital_signs).some(v => v)) return 'vitals';
247
+ if (extractedData.lab_results?.length > 0) return 'lab';
248
+ return 'general';
 
249
  }
250
 
251
+ formatVitalSigns(vitalSigns) {
252
+ if (!vitalSigns) return '-';
 
253
 
254
+ const vitals = [];
255
+ if (vitalSigns.blood_pressure) vitals.push(`BP: ${vitalSigns.blood_pressure}`);
256
+ if (vitalSigns.heart_rate) vitals.push(`HR: ${vitalSigns.heart_rate}`);
257
+ if (vitalSigns.temperature) vitals.push(`Temp: ${vitalSigns.temperature}`);
258
 
259
+ return vitals.length > 0 ? vitals.join(', ') : '-';
 
 
260
  }
261
 
262
+ formatConfidence(score) {
263
+ const percentage = Math.round(score * 100);
264
+ const level = score >= 0.8 ? 'high' : score >= 0.6 ? 'medium' : 'low';
265
+
266
+ return `
267
+ <div class="confidence-score">
268
+ <div class="confidence-bar">
269
+ <div class="confidence-fill ${level}" style="width: ${percentage}%"></div>
270
+ </div>
271
+ <span class="confidence-text">${percentage}%</span>
272
+ </div>
273
+ `;
274
  }
275
 
276
+ async viewEMRDetail(emrId) {
277
  try {
278
+ const response = await fetch(`/emr/${emrId}`);
279
+ if (response.ok) {
280
+ const entry = await response.json();
281
+ this.showEMRDetailModal(entry);
282
+ } else {
283
+ alert('Failed to load EMR details');
284
  }
285
  } catch (error) {
286
+ console.error('Error loading EMR detail:', error);
287
+ alert('Error loading EMR details');
288
  }
289
  }
290
 
291
+ showEMRDetailModal(entry) {
292
+ const modal = document.getElementById('emrDetailModal');
293
+ const content = document.getElementById('emrDetailContent');
294
+
295
+ const date = new Date(entry.created_at).toLocaleString();
296
 
297
+ content.innerHTML = `
298
+ <div class="emr-detail-section">
299
+ <h4>Basic Information</h4>
300
+ <p><strong>Date:</strong> ${date}</p>
301
+ <p><strong>Confidence:</strong> ${Math.round(entry.confidence_score * 100)}%</p>
302
+ <p><strong>Original Message:</strong></p>
303
+ <div style="background-color: var(--bg-secondary); padding: var(--spacing-md); border-radius: 8px; margin-top: var(--spacing-sm);">
304
+ ${entry.original_message}
305
+ </div>
306
+ </div>
307
+
308
+ ${entry.extracted_data.diagnosis?.length > 0 ? `
309
+ <div class="emr-detail-section">
310
+ <h4>Diagnoses</h4>
311
+ <ul class="emr-detail-list">
312
+ ${entry.extracted_data.diagnosis.map(d => `<li>${d}</li>`).join('')}
313
+ </ul>
314
+ </div>
315
+ ` : ''}
316
+
317
+ ${entry.extracted_data.symptoms?.length > 0 ? `
318
+ <div class="emr-detail-section">
319
+ <h4>Symptoms</h4>
320
+ <ul class="emr-detail-list">
321
+ ${entry.extracted_data.symptoms.map(s => `<li>${s}</li>`).join('')}
322
+ </ul>
323
+ </div>
324
+ ` : ''}
325
+
326
+ ${entry.extracted_data.medications?.length > 0 ? `
327
+ <div class="emr-detail-section">
328
+ <h4>Medications</h4>
329
+ ${entry.extracted_data.medications.map(med => `
330
+ <div class="medication-item">
331
+ <div class="medication-name">${med.name}</div>
332
+ <div class="medication-details">
333
+ ${med.dosage ? `Dosage: ${med.dosage}` : ''}
334
+ ${med.frequency ? ` | Frequency: ${med.frequency}` : ''}
335
+ ${med.duration ? ` | Duration: ${med.duration}` : ''}
336
+ </div>
337
+ </div>
338
+ `).join('')}
339
+ </div>
340
+ ` : ''}
341
+
342
+ ${entry.extracted_data.vital_signs && Object.values(entry.extracted_data.vital_signs).some(v => v) ? `
343
+ <div class="emr-detail-section">
344
+ <h4>Vital Signs</h4>
345
+ <div class="vital-signs-grid">
346
+ ${Object.entries(entry.extracted_data.vital_signs).map(([key, value]) =>
347
+ value ? `
348
+ <div class="vital-sign-item">
349
+ <div class="vital-sign-label">${key.replace('_', ' ').toUpperCase()}</div>
350
+ <div class="vital-sign-value">${value}</div>
351
+ </div>
352
+ ` : ''
353
+ ).join('')}
354
+ </div>
355
+ </div>
356
+ ` : ''}
357
+
358
+ ${entry.extracted_data.lab_results?.length > 0 ? `
359
+ <div class="emr-detail-section">
360
+ <h4>Lab Results</h4>
361
+ <ul class="emr-detail-list">
362
+ ${entry.extracted_data.lab_results.map(lab => `
363
+ <li>
364
+ <strong>${lab.test_name}:</strong> ${lab.value} ${lab.unit || ''}
365
+ ${lab.reference_range ? ` (Normal: ${lab.reference_range})` : ''}
366
+ </li>
367
+ `).join('')}
368
+ </ul>
369
+ </div>
370
+ ` : ''}
371
+
372
+ ${entry.extracted_data.procedures?.length > 0 ? `
373
+ <div class="emr-detail-section">
374
+ <h4>Procedures</h4>
375
+ <ul class="emr-detail-list">
376
+ ${entry.extracted_data.procedures.map(p => `<li>${p}</li>`).join('')}
377
+ </ul>
378
+ </div>
379
+ ` : ''}
380
+
381
+ ${entry.extracted_data.notes ? `
382
+ <div class="emr-detail-section">
383
+ <h4>Notes</h4>
384
+ <p>${entry.extracted_data.notes}</p>
385
+ </div>
386
+ ` : ''}
387
+ `;
388
+
389
+ modal.classList.add('show');
390
+ }
391
+
392
+ async deleteEMREntry(emrId) {
393
+ if (!confirm('Are you sure you want to delete this EMR entry?')) {
394
  return;
395
  }
396
 
397
+ try {
398
+ const response = await fetch(`/emr/${emrId}`, {
399
+ method: 'DELETE'
400
+ });
401
 
402
+ if (response.ok) {
403
+ this.loadEMRData(); // Refresh the data
404
+ this.loadPatientStats(); // Refresh stats
405
+ } else {
406
+ alert('Failed to delete EMR entry');
407
+ }
408
+ } catch (error) {
409
+ console.error('Error deleting EMR entry:', error);
410
+ alert('Error deleting EMR entry');
411
+ }
412
+ }
413
 
414
+ filterEntries(query) {
415
+ if (!query.trim()) {
416
+ this.filteredEntries = [...this.emrEntries];
417
+ } else {
418
+ this.filteredEntries = this.emrEntries.filter(entry => {
419
+ const searchText = query.toLowerCase();
420
+ return (
421
+ entry.original_message.toLowerCase().includes(searchText) ||
422
+ entry.extracted_data.diagnosis?.some(d => d.toLowerCase().includes(searchText)) ||
423
+ entry.extracted_data.symptoms?.some(s => s.toLowerCase().includes(searchText)) ||
424
+ entry.extracted_data.medications?.some(m => m.name.toLowerCase().includes(searchText)) ||
425
+ entry.extracted_data.notes?.toLowerCase().includes(searchText)
426
+ );
427
  });
428
+ }
429
+ this.renderEMRTable();
430
+ }
431
+
432
+ applyFilters() {
433
+ const dateFilter = document.getElementById('dateFilter').value;
434
+ const typeFilter = document.getElementById('typeFilter').value;
435
+
436
+ this.filteredEntries = this.emrEntries.filter(entry => {
437
+ // Date filter
438
+ if (dateFilter !== 'all') {
439
+ const entryDate = new Date(entry.created_at);
440
+ const now = new Date();
441
+
442
+ switch (dateFilter) {
443
+ case 'today':
444
+ if (entryDate.toDateString() !== now.toDateString()) return false;
445
+ break;
446
+ case 'week':
447
+ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
448
+ if (entryDate < weekAgo) return false;
449
+ break;
450
+ case 'month':
451
+ const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
452
+ if (entryDate < monthAgo) return false;
453
+ break;
454
+ }
455
+ }
456
+
457
+ // Type filter
458
+ if (typeFilter !== 'all') {
459
+ const entryType = this.getEMRType(entry.extracted_data);
460
+ if (entryType !== typeFilter) return false;
461
+ }
462
 
463
+ return true;
464
  });
465
+
466
+ this.renderEMRTable();
467
+ }
468
+
469
+ openSearchModal() {
470
+ document.getElementById('searchModal').classList.add('show');
471
  }
472
 
473
+ async performAdvancedSearch() {
474
+ const semanticQuery = document.getElementById('semanticSearchInput').value.trim();
475
+ const exactQuery = document.getElementById('exactSearchInput').value.trim();
476
+
477
+ if (!semanticQuery && !exactQuery) {
478
+ alert('Please enter a search query');
479
+ return;
480
+ }
481
+
482
  this.showLoading(true);
483
 
484
  try {
485
+ let searchResults = [];
486
+
487
+ if (semanticQuery) {
488
+ const response = await fetch(`/emr/search/${this.currentPatientId}?query=${encodeURIComponent(semanticQuery)}&limit=50`);
489
+ if (response.ok) {
490
+ searchResults = await response.json();
491
+ }
492
+ }
 
 
 
 
 
 
 
 
 
 
 
493
 
494
+ if (exactQuery) {
495
+ const exactResults = this.emrEntries.filter(entry => {
496
+ const searchText = exactQuery.toLowerCase();
497
+ return (
498
+ entry.original_message.toLowerCase().includes(searchText) ||
499
+ entry.extracted_data.diagnosis?.some(d => d.toLowerCase().includes(searchText)) ||
500
+ entry.extracted_data.symptoms?.some(s => s.toLowerCase().includes(searchText)) ||
501
+ entry.extracted_data.medications?.some(m => m.name.toLowerCase().includes(searchText)) ||
502
+ entry.extracted_data.notes?.toLowerCase().includes(searchText)
503
+ );
504
+ });
505
+
506
+ // Merge results if both searches were performed
507
+ if (semanticQuery) {
508
+ const exactIds = new Set(exactResults.map(r => r.emr_id));
509
+ searchResults = searchResults.concat(exactResults.filter(r => !exactIds.has(r.emr_id)));
510
  } else {
511
+ searchResults = exactResults;
512
+ }
513
  }
514
+
515
+ this.filteredEntries = searchResults;
516
+ this.renderEMRTable();
517
+
518
  } catch (error) {
519
+ console.error('Error performing search:', error);
520
+ alert('Error performing search');
521
  } finally {
522
  this.showLoading(false);
523
  }
524
  }
525
 
526
+ showLoading(show) {
527
+ const loadingState = document.getElementById('loadingState');
528
+ const tableContainer = document.querySelector('.emr-table-container');
529
+
530
+ if (show) {
531
+ loadingState.style.display = 'block';
532
+ tableContainer.style.display = 'none';
533
+ } else {
534
+ loadingState.style.display = 'none';
535
+ tableContainer.style.display = 'block';
536
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  }
538
 
539
+ showEmptyState() {
540
+ const emptyState = document.getElementById('emptyState');
541
+ const tableContainer = document.querySelector('.emr-table-container');
 
 
542
 
543
+ emptyState.style.display = 'block';
544
+ tableContainer.style.display = 'none';
 
 
 
545
  }
546
 
547
+ showErrorState(message) {
548
+ const emptyState = document.getElementById('emptyState');
549
+ const tableContainer = document.querySelector('.emr-table-container');
550
+
551
+ // Update the empty state to show error message
552
+ emptyState.querySelector('h3').textContent = 'Error Loading EMR Data';
553
+ emptyState.querySelector('p').textContent = message;
554
+ emptyState.querySelector('.btn').style.display = 'none';
555
 
556
+ emptyState.style.display = 'block';
557
+ tableContainer.style.display = 'none';
558
  }
559
  }
560
 
561
+ // Initialize the EMR page when DOM is loaded
562
  document.addEventListener('DOMContentLoaded', () => {
563
+ window.emrPage = new EMRPage();
564
  });