LiamKhoaLe commited on
Commit
55b641e
·
1 Parent(s): 16d4f47

Upd EMR parser and updater services (OCR+VLM)

Browse files
src/api/routes/emr.py CHANGED
@@ -3,10 +3,13 @@
3
  from datetime import datetime, timezone
4
  from typing import List, Optional
5
 
6
- from fastapi import APIRouter, Depends, HTTPException
 
7
 
8
- from src.models.emr import EMRResponse, EMRSearchRequest, EMRUpdateRequest
9
  from src.services.service import EMRService
 
 
10
  from src.core.state import AppState, get_state
11
  from src.utils.logger import logger
12
 
@@ -324,3 +327,300 @@ async def bulk_extract_emr(
324
  except Exception as e:
325
  logger().error(f"Error in bulk EMR extraction: {e}")
326
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from datetime import datetime, timezone
4
  from typing import List, Optional
5
 
6
+ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
7
+ from fastapi.responses import JSONResponse
8
 
9
+ from src.models.emr import EMRResponse, EMRSearchRequest, EMRUpdateRequest, ExtractedData
10
  from src.services.service import EMRService
11
+ from src.services.extractor import EMRExtractor
12
+ from src.data.emr_update import EMRUpdateService
13
  from src.core.state import AppState, get_state
14
  from src.utils.logger import logger
15
 
 
327
  except Exception as e:
328
  logger().error(f"Error in bulk EMR extraction: {e}")
329
  raise HTTPException(status_code=500, detail=str(e))
330
+
331
+
332
+ def get_emr_extractor(state: AppState = Depends(get_state)) -> EMRExtractor:
333
+ """Get EMR extractor instance."""
334
+ return EMRExtractor(state.gemini_rotator)
335
+
336
+
337
+ def get_emr_update_service() -> EMRUpdateService:
338
+ """Get EMR update service instance."""
339
+ return EMRUpdateService()
340
+
341
+
342
+ @router.post("/upload-document", response_model=dict)
343
+ async def upload_and_analyze_document(
344
+ patient_id: str = Form(...),
345
+ file: UploadFile = File(...),
346
+ emr_extractor: EMRExtractor = Depends(get_emr_extractor),
347
+ emr_update_service: EMRUpdateService = Depends(get_emr_update_service)
348
+ ):
349
+ """Upload and analyze a medical document to extract EMR data."""
350
+ try:
351
+ # Validate patient ID
352
+ if not patient_id or not patient_id.strip():
353
+ raise HTTPException(status_code=400, detail="Patient ID is required")
354
+
355
+ # Validate file
356
+ if not file or not file.filename:
357
+ raise HTTPException(status_code=400, detail="No file provided")
358
+
359
+ # Check file size (limit to 10MB)
360
+ file_content = await file.read()
361
+ if len(file_content) > 10 * 1024 * 1024: # 10MB
362
+ raise HTTPException(status_code=400, detail="File size exceeds 10MB limit")
363
+
364
+ # Check file type
365
+ allowed_extensions = {'.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png', '.tiff'}
366
+ file_extension = '.' + file.filename.split('.')[-1].lower() if '.' in file.filename else ''
367
+ if file_extension not in allowed_extensions:
368
+ raise HTTPException(
369
+ status_code=400,
370
+ detail=f"Unsupported file type. Allowed types: {', '.join(allowed_extensions)}"
371
+ )
372
+
373
+ logger().info(f"Document upload requested for patient {patient_id}, file: {file.filename}")
374
+
375
+ # Get patient context if available
376
+ patient_context = None
377
+ try:
378
+ from src.data.repositories.patient import get_patient_by_id
379
+ patient = get_patient_by_id(patient_id)
380
+ if patient:
381
+ patient_context = {
382
+ "name": patient.name,
383
+ "age": patient.age,
384
+ "sex": patient.sex,
385
+ "medications": patient.medications or [],
386
+ "past_assessment_summary": patient.past_assessment_summary
387
+ }
388
+ except Exception as e:
389
+ logger().warning(f"Could not fetch patient context: {e}")
390
+
391
+ # Analyze the document
392
+ extracted_data, confidence_score = await emr_extractor.analyze_document(
393
+ file_content=file_content,
394
+ filename=file.filename,
395
+ patient_context=patient_context
396
+ )
397
+
398
+ # Save to database
399
+ emr_id = await emr_update_service.save_document_analysis(
400
+ patient_id=patient_id,
401
+ filename=file.filename,
402
+ file_content=file_content,
403
+ extracted_data=extracted_data,
404
+ confidence_score=confidence_score
405
+ )
406
+
407
+ return {
408
+ "emr_id": emr_id,
409
+ "filename": file.filename,
410
+ "confidence_score": confidence_score,
411
+ "extracted_data": {
412
+ "overview": extracted_data.notes.split("Document Overview: ")[-1] if "Document Overview:" in extracted_data.notes else "",
413
+ "diagnosis": extracted_data.diagnosis or [],
414
+ "symptoms": extracted_data.symptoms or [],
415
+ "medications": [
416
+ {
417
+ "name": med.name,
418
+ "dosage": med.dosage,
419
+ "frequency": med.frequency,
420
+ "duration": med.duration
421
+ }
422
+ for med in extracted_data.medications or []
423
+ ],
424
+ "vital_signs": {
425
+ "blood_pressure": extracted_data.vital_signs.blood_pressure if extracted_data.vital_signs else None,
426
+ "heart_rate": extracted_data.vital_signs.heart_rate if extracted_data.vital_signs else None,
427
+ "temperature": extracted_data.vital_signs.temperature if extracted_data.vital_signs else None,
428
+ "respiratory_rate": extracted_data.vital_signs.respiratory_rate if extracted_data.vital_signs else None,
429
+ "oxygen_saturation": extracted_data.vital_signs.oxygen_saturation if extracted_data.vital_signs else None
430
+ } if extracted_data.vital_signs else None,
431
+ "lab_results": [
432
+ {
433
+ "test_name": lab.test_name,
434
+ "value": lab.value,
435
+ "unit": lab.unit,
436
+ "reference_range": lab.reference_range
437
+ }
438
+ for lab in extracted_data.lab_results or []
439
+ ],
440
+ "procedures": extracted_data.procedures or [],
441
+ "notes": extracted_data.notes or ""
442
+ },
443
+ "message": "Document analyzed and EMR data extracted successfully"
444
+ }
445
+
446
+ except HTTPException:
447
+ raise
448
+ except Exception as e:
449
+ logger().error(f"Error in document upload and analysis: {e}")
450
+ raise HTTPException(status_code=500, detail=str(e))
451
+
452
+
453
+ @router.post("/preview-document", response_model=dict)
454
+ async def preview_document_analysis(
455
+ patient_id: str = Form(...),
456
+ file: UploadFile = File(...),
457
+ emr_extractor: EMRExtractor = Depends(get_emr_extractor)
458
+ ):
459
+ """Upload and analyze a medical document to preview extracted data before saving."""
460
+ try:
461
+ # Validate patient ID
462
+ if not patient_id or not patient_id.strip():
463
+ raise HTTPException(status_code=400, detail="Patient ID is required")
464
+
465
+ # Validate file
466
+ if not file or not file.filename:
467
+ raise HTTPException(status_code=400, detail="No file provided")
468
+
469
+ # Check file size (limit to 10MB)
470
+ file_content = await file.read()
471
+ if len(file_content) > 10 * 1024 * 1024: # 10MB
472
+ raise HTTPException(status_code=400, detail="File size exceeds 10MB limit")
473
+
474
+ # Check file type
475
+ allowed_extensions = {'.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png', '.tiff'}
476
+ file_extension = '.' + file.filename.split('.')[-1].lower() if '.' in file.filename else ''
477
+ if file_extension not in allowed_extensions:
478
+ raise HTTPException(
479
+ status_code=400,
480
+ detail=f"Unsupported file type. Allowed types: {', '.join(allowed_extensions)}"
481
+ )
482
+
483
+ logger().info(f"Document preview requested for patient {patient_id}, file: {file.filename}")
484
+
485
+ # Get patient context if available
486
+ patient_context = None
487
+ try:
488
+ from src.data.repositories.patient import get_patient_by_id
489
+ patient = get_patient_by_id(patient_id)
490
+ if patient:
491
+ patient_context = {
492
+ "name": patient.name,
493
+ "age": patient.age,
494
+ "sex": patient.sex,
495
+ "medications": patient.medications or [],
496
+ "past_assessment_summary": patient.past_assessment_summary
497
+ }
498
+ except Exception as e:
499
+ logger().warning(f"Could not fetch patient context: {e}")
500
+
501
+ # Analyze the document
502
+ extracted_data, confidence_score = await emr_extractor.analyze_document(
503
+ file_content=file_content,
504
+ filename=file.filename,
505
+ patient_context=patient_context
506
+ )
507
+
508
+ return {
509
+ "filename": file.filename,
510
+ "confidence_score": confidence_score,
511
+ "extracted_data": {
512
+ "overview": extracted_data.notes.split("Document Overview: ")[-1] if "Document Overview:" in extracted_data.notes else "",
513
+ "diagnosis": extracted_data.diagnosis or [],
514
+ "symptoms": extracted_data.symptoms or [],
515
+ "medications": [
516
+ {
517
+ "name": med.name,
518
+ "dosage": med.dosage,
519
+ "frequency": med.frequency,
520
+ "duration": med.duration
521
+ }
522
+ for med in extracted_data.medications or []
523
+ ],
524
+ "vital_signs": {
525
+ "blood_pressure": extracted_data.vital_signs.blood_pressure if extracted_data.vital_signs else None,
526
+ "heart_rate": extracted_data.vital_signs.heart_rate if extracted_data.vital_signs else None,
527
+ "temperature": extracted_data.vital_signs.temperature if extracted_data.vital_signs else None,
528
+ "respiratory_rate": extracted_data.vital_signs.respiratory_rate if extracted_data.vital_signs else None,
529
+ "oxygen_saturation": extracted_data.vital_signs.oxygen_saturation if extracted_data.vital_signs else None
530
+ } if extracted_data.vital_signs else None,
531
+ "lab_results": [
532
+ {
533
+ "test_name": lab.test_name,
534
+ "value": lab.value,
535
+ "unit": lab.unit,
536
+ "reference_range": lab.reference_range
537
+ }
538
+ for lab in extracted_data.lab_results or []
539
+ ],
540
+ "procedures": extracted_data.procedures or [],
541
+ "notes": extracted_data.notes or ""
542
+ },
543
+ "message": "Document analyzed successfully. Review the data before saving."
544
+ }
545
+
546
+ except HTTPException:
547
+ raise
548
+ except Exception as e:
549
+ logger().error(f"Error in document preview: {e}")
550
+ raise HTTPException(status_code=500, detail=str(e))
551
+
552
+
553
+ @router.post("/save-document-analysis", response_model=dict)
554
+ async def save_document_analysis(
555
+ patient_id: str = Form(...),
556
+ filename: str = Form(...),
557
+ extracted_data: str = Form(...), # JSON string
558
+ confidence_score: float = Form(...),
559
+ emr_update_service: EMRUpdateService = Depends(get_emr_update_service)
560
+ ):
561
+ """Save document analysis results to EMR database."""
562
+ try:
563
+ import json
564
+
565
+ # Validate inputs
566
+ if not patient_id or not patient_id.strip():
567
+ raise HTTPException(status_code=400, detail="Patient ID is required")
568
+ if not filename or not filename.strip():
569
+ raise HTTPException(status_code=400, detail="Filename is required")
570
+ if not extracted_data or not extracted_data.strip():
571
+ raise HTTPException(status_code=400, detail="Extracted data is required")
572
+
573
+ # Parse extracted data
574
+ try:
575
+ data_dict = json.loads(extracted_data)
576
+ except json.JSONDecodeError as e:
577
+ raise HTTPException(status_code=400, detail=f"Invalid JSON in extracted data: {e}")
578
+
579
+ # Convert to ExtractedData object
580
+ extracted_data_obj = ExtractedData(
581
+ diagnosis=data_dict.get('diagnosis', []),
582
+ symptoms=data_dict.get('symptoms', []),
583
+ medications=[
584
+ {
585
+ "name": med.get('name', ''),
586
+ "dosage": med.get('dosage'),
587
+ "frequency": med.get('frequency'),
588
+ "duration": med.get('duration')
589
+ }
590
+ for med in data_dict.get('medications', [])
591
+ ],
592
+ vital_signs=data_dict.get('vital_signs'),
593
+ lab_results=[
594
+ {
595
+ "test_name": lab.get('test_name', ''),
596
+ "value": lab.get('value', ''),
597
+ "unit": lab.get('unit'),
598
+ "reference_range": lab.get('reference_range')
599
+ }
600
+ for lab in data_dict.get('lab_results', [])
601
+ ],
602
+ procedures=data_dict.get('procedures', []),
603
+ notes=data_dict.get('notes', '') + (f"\n\nDocument Overview: {data_dict.get('overview', '')}" if data_dict.get('overview') else '')
604
+ )
605
+
606
+ logger().info(f"Saving document analysis for patient {patient_id}, file: {filename}")
607
+
608
+ # Save to database (without file content for preview saves)
609
+ emr_id = await emr_update_service.save_document_analysis(
610
+ patient_id=patient_id,
611
+ filename=filename,
612
+ file_content=b"", # Empty for preview saves
613
+ extracted_data=extracted_data_obj,
614
+ confidence_score=confidence_score
615
+ )
616
+
617
+ return {
618
+ "emr_id": emr_id,
619
+ "message": "Document analysis saved to EMR successfully"
620
+ }
621
+
622
+ except HTTPException:
623
+ raise
624
+ except Exception as e:
625
+ logger().error(f"Error saving document analysis: {e}")
626
+ raise HTTPException(status_code=500, detail=str(e))
src/data/emr_update.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/data/emr_update.py
2
+
3
+ import os
4
+ import uuid
5
+ from datetime import datetime
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from src.data.connection import get_database
9
+ from src.models.emr import ExtractedData
10
+ from src.utils.logger import logger
11
+
12
+
13
+ class EMRUpdateService:
14
+ """Service for updating EMR records with document analysis results."""
15
+
16
+ def __init__(self):
17
+ self.db = get_database()
18
+
19
+ async def save_document_analysis(
20
+ self,
21
+ patient_id: str,
22
+ filename: str,
23
+ file_content: bytes,
24
+ extracted_data: ExtractedData,
25
+ confidence_score: float,
26
+ original_message: str = None
27
+ ) -> str:
28
+ """
29
+ Save document analysis results to the EMR database.
30
+
31
+ Args:
32
+ patient_id: The ID of the patient
33
+ filename: The name of the uploaded file
34
+ file_content: The binary content of the file
35
+ extracted_data: The extracted medical data
36
+ confidence_score: The confidence score of the analysis
37
+ original_message: Optional original message (for chat-based entries)
38
+
39
+ Returns:
40
+ The EMR ID of the created record
41
+ """
42
+ try:
43
+ # Generate unique EMR ID
44
+ emr_id = str(uuid.uuid4())
45
+
46
+ # Prepare the EMR record
47
+ emr_record = {
48
+ "emr_id": emr_id,
49
+ "patient_id": patient_id,
50
+ "original_message": original_message or f"Document upload: {filename}",
51
+ "extracted_data": {
52
+ "diagnosis": extracted_data.diagnosis or [],
53
+ "symptoms": extracted_data.symptoms or [],
54
+ "medications": [
55
+ {
56
+ "name": med.name,
57
+ "dosage": med.dosage,
58
+ "frequency": med.frequency,
59
+ "duration": med.duration
60
+ }
61
+ for med in extracted_data.medications or []
62
+ ],
63
+ "vital_signs": {
64
+ "blood_pressure": extracted_data.vital_signs.blood_pressure if extracted_data.vital_signs else None,
65
+ "heart_rate": extracted_data.vital_signs.heart_rate if extracted_data.vital_signs else None,
66
+ "temperature": extracted_data.vital_signs.temperature if extracted_data.vital_signs else None,
67
+ "respiratory_rate": extracted_data.vital_signs.respiratory_rate if extracted_data.vital_signs else None,
68
+ "oxygen_saturation": extracted_data.vital_signs.oxygen_saturation if extracted_data.vital_signs else None
69
+ } if extracted_data.vital_signs else None,
70
+ "lab_results": [
71
+ {
72
+ "test_name": lab.test_name,
73
+ "value": lab.value,
74
+ "unit": lab.unit,
75
+ "reference_range": lab.reference_range
76
+ }
77
+ for lab in extracted_data.lab_results or []
78
+ ],
79
+ "procedures": extracted_data.procedures or [],
80
+ "notes": extracted_data.notes or ""
81
+ },
82
+ "confidence_score": confidence_score,
83
+ "source": "document_upload",
84
+ "filename": filename,
85
+ "created_at": datetime.utcnow(),
86
+ "updated_at": datetime.utcnow()
87
+ }
88
+
89
+ # Save to database
90
+ result = await self.db.emr_records.insert_one(emr_record)
91
+
92
+ if result.inserted_id:
93
+ logger().info(f"Successfully saved document analysis for patient {patient_id}, EMR ID: {emr_id}")
94
+ return emr_id
95
+ else:
96
+ raise Exception("Failed to insert EMR record")
97
+
98
+ except Exception as e:
99
+ logger().error(f"Error saving document analysis: {e}")
100
+ raise
101
+
102
+ async def update_emr_record(
103
+ self,
104
+ emr_id: str,
105
+ extracted_data: ExtractedData,
106
+ confidence_score: float = None
107
+ ) -> bool:
108
+ """
109
+ Update an existing EMR record with new extracted data.
110
+
111
+ Args:
112
+ emr_id: The EMR record ID to update
113
+ extracted_data: The updated extracted medical data
114
+ confidence_score: Optional new confidence score
115
+
116
+ Returns:
117
+ True if update was successful, False otherwise
118
+ """
119
+ try:
120
+ # Prepare update data
121
+ update_data = {
122
+ "extracted_data": {
123
+ "diagnosis": extracted_data.diagnosis or [],
124
+ "symptoms": extracted_data.symptoms or [],
125
+ "medications": [
126
+ {
127
+ "name": med.name,
128
+ "dosage": med.dosage,
129
+ "frequency": med.frequency,
130
+ "duration": med.duration
131
+ }
132
+ for med in extracted_data.medications or []
133
+ ],
134
+ "vital_signs": {
135
+ "blood_pressure": extracted_data.vital_signs.blood_pressure if extracted_data.vital_signs else None,
136
+ "heart_rate": extracted_data.vital_signs.heart_rate if extracted_data.vital_signs else None,
137
+ "temperature": extracted_data.vital_signs.temperature if extracted_data.vital_signs else None,
138
+ "respiratory_rate": extracted_data.vital_signs.respiratory_rate if extracted_data.vital_signs else None,
139
+ "oxygen_saturation": extracted_data.vital_signs.oxygen_saturation if extracted_data.vital_signs else None
140
+ } if extracted_data.vital_signs else None,
141
+ "lab_results": [
142
+ {
143
+ "test_name": lab.test_name,
144
+ "value": lab.value,
145
+ "unit": lab.unit,
146
+ "reference_range": lab.reference_range
147
+ }
148
+ for lab in extracted_data.lab_results or []
149
+ ],
150
+ "procedures": extracted_data.procedures or [],
151
+ "notes": extracted_data.notes or ""
152
+ },
153
+ "updated_at": datetime.utcnow()
154
+ }
155
+
156
+ if confidence_score is not None:
157
+ update_data["confidence_score"] = confidence_score
158
+
159
+ # Update the record
160
+ result = await self.db.emr_records.update_one(
161
+ {"emr_id": emr_id},
162
+ {"$set": update_data}
163
+ )
164
+
165
+ if result.modified_count > 0:
166
+ logger().info(f"Successfully updated EMR record {emr_id}")
167
+ return True
168
+ else:
169
+ logger().warning(f"No EMR record found with ID {emr_id}")
170
+ return False
171
+
172
+ except Exception as e:
173
+ logger().error(f"Error updating EMR record {emr_id}: {e}")
174
+ raise
175
+
176
+ async def get_emr_record(self, emr_id: str) -> Optional[Dict[str, Any]]:
177
+ """
178
+ Retrieve an EMR record by ID.
179
+
180
+ Args:
181
+ emr_id: The EMR record ID
182
+
183
+ Returns:
184
+ The EMR record if found, None otherwise
185
+ """
186
+ try:
187
+ record = await self.db.emr_records.find_one({"emr_id": emr_id})
188
+ if record:
189
+ # Convert ObjectId to string for JSON serialization
190
+ record["_id"] = str(record["_id"])
191
+ return record
192
+
193
+ except Exception as e:
194
+ logger().error(f"Error retrieving EMR record {emr_id}: {e}")
195
+ raise
196
+
197
+ async def delete_emr_record(self, emr_id: str) -> bool:
198
+ """
199
+ Delete an EMR record.
200
+
201
+ Args:
202
+ emr_id: The EMR record ID to delete
203
+
204
+ Returns:
205
+ True if deletion was successful, False otherwise
206
+ """
207
+ try:
208
+ result = await self.db.emr_records.delete_one({"emr_id": emr_id})
209
+
210
+ if result.deleted_count > 0:
211
+ logger().info(f"Successfully deleted EMR record {emr_id}")
212
+ return True
213
+ else:
214
+ logger().warning(f"No EMR record found with ID {emr_id}")
215
+ return False
216
+
217
+ except Exception as e:
218
+ logger().error(f"Error deleting EMR record {emr_id}: {e}")
219
+ raise
220
+
221
+ async def get_patient_emr_records(
222
+ self,
223
+ patient_id: str,
224
+ limit: int = 100,
225
+ skip: int = 0
226
+ ) -> List[Dict[str, Any]]:
227
+ """
228
+ Retrieve EMR records for a specific patient.
229
+
230
+ Args:
231
+ patient_id: The patient ID
232
+ limit: Maximum number of records to return
233
+ skip: Number of records to skip
234
+
235
+ Returns:
236
+ List of EMR records
237
+ """
238
+ try:
239
+ cursor = self.db.emr_records.find(
240
+ {"patient_id": patient_id}
241
+ ).sort("created_at", -1).skip(skip).limit(limit)
242
+
243
+ records = []
244
+ async for record in cursor:
245
+ # Convert ObjectId to string for JSON serialization
246
+ record["_id"] = str(record["_id"])
247
+ records.append(record)
248
+
249
+ return records
250
+
251
+ except Exception as e:
252
+ logger().error(f"Error retrieving EMR records for patient {patient_id}: {e}")
253
+ raise
254
+
255
+ async def get_patient_emr_statistics(self, patient_id: str) -> Dict[str, Any]:
256
+ """
257
+ Get EMR statistics for a patient.
258
+
259
+ Args:
260
+ patient_id: The patient ID
261
+
262
+ Returns:
263
+ Dictionary containing EMR statistics
264
+ """
265
+ try:
266
+ pipeline = [
267
+ {"$match": {"patient_id": patient_id}},
268
+ {
269
+ "$group": {
270
+ "_id": None,
271
+ "total_entries": {"$sum": 1},
272
+ "avg_confidence": {"$avg": "$confidence_score"},
273
+ "diagnosis_count": {
274
+ "$sum": {
275
+ "$cond": [
276
+ {"$gt": [{"$size": {"$ifNull": ["$extracted_data.diagnosis", []]}}, 0]},
277
+ 1,
278
+ 0
279
+ ]
280
+ }
281
+ },
282
+ "medication_count": {
283
+ "$sum": {
284
+ "$cond": [
285
+ {"$gt": [{"$size": {"$ifNull": ["$extracted_data.medications", []]}}, 0]},
286
+ 1,
287
+ 0
288
+ ]
289
+ }
290
+ }
291
+ }
292
+ }
293
+ ]
294
+
295
+ result = await self.db.emr_records.aggregate(pipeline).to_list(1)
296
+
297
+ if result:
298
+ stats = result[0]
299
+ return {
300
+ "total_entries": stats.get("total_entries", 0),
301
+ "avg_confidence": stats.get("avg_confidence", 0.0),
302
+ "diagnosis_count": stats.get("diagnosis_count", 0),
303
+ "medication_count": stats.get("medication_count", 0)
304
+ }
305
+ else:
306
+ return {
307
+ "total_entries": 0,
308
+ "avg_confidence": 0.0,
309
+ "diagnosis_count": 0,
310
+ "medication_count": 0
311
+ }
312
+
313
+ except Exception as e:
314
+ logger().error(f"Error retrieving EMR statistics for patient {patient_id}: {e}")
315
+ raise
src/services/extractor.py CHANGED
@@ -2,6 +2,8 @@
2
 
3
  import json
4
  import re
 
 
5
  from typing import Any, Dict, List, Optional, Tuple
6
 
7
  from src.models.emr import ExtractedData, LabResult, Medication, VitalSigns
@@ -192,7 +194,7 @@ Return the JSON followed by the confidence score on a new line."""
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
@@ -258,3 +260,141 @@ Return the JSON followed by the confidence score on a new line."""
258
  vital_signs['oxygen_saturation'] = o2_match.group(1)
259
 
260
  return vital_signs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  import json
4
  import re
5
+ import base64
6
+ import mimetypes
7
  from typing import Any, Dict, List, Optional, Tuple
8
 
9
  from src.models.emr import ExtractedData, LabResult, Medication, VitalSigns
 
194
  vital_signs=vital_signs,
195
  lab_results=lab_results,
196
  procedures=data.get('procedures', []),
197
+ notes=data.get('notes', '') + (f"\n\nDocument Overview: {data.get('overview', '')}" if data.get('overview') else '')
198
  )
199
 
200
  return extracted_data, confidence
 
260
  vital_signs['oxygen_saturation'] = o2_match.group(1)
261
 
262
  return vital_signs
263
+
264
+ async def analyze_document(self, file_content: bytes, filename: str, patient_context: Optional[Dict[str, Any]] = None) -> Tuple[ExtractedData, float]:
265
+ """
266
+ Analyze a medical document (PDF, image, or text) and extract structured medical data.
267
+
268
+ Args:
269
+ file_content: The binary content of the uploaded file
270
+ filename: The name of the uploaded file
271
+ patient_context: Optional patient context information
272
+
273
+ Returns:
274
+ Tuple of (ExtractedData, confidence_score)
275
+ """
276
+ try:
277
+ # Determine file type and prepare content for Gemini
278
+ mime_type, _ = mimetypes.guess_type(filename)
279
+
280
+ if not mime_type:
281
+ logger().warning(f"Unknown file type for {filename}")
282
+ return ExtractedData(), 0.0
283
+
284
+ # Encode file content to base64
285
+ file_base64 = base64.b64encode(file_content).decode('utf-8')
286
+
287
+ # Build the prompt for document analysis
288
+ prompt = self._build_document_analysis_prompt(file_base64, mime_type, filename, patient_context)
289
+
290
+ # Get response from Gemini
291
+ response = await self._call_gemini_api(prompt)
292
+
293
+ # Parse the response
294
+ extracted_data, confidence = self._parse_gemini_response(response)
295
+
296
+ logger().info(f"Successfully analyzed document {filename} with confidence {confidence:.2f}")
297
+ return extracted_data, confidence
298
+
299
+ except Exception as e:
300
+ logger().error(f"Error analyzing document {filename}: {e}")
301
+ # Return empty data with low confidence
302
+ return ExtractedData(), 0.0
303
+
304
+ def _build_document_analysis_prompt(self, file_base64: str, mime_type: str, filename: str, patient_context: Optional[Dict[str, Any]] = None) -> str:
305
+ """Build the prompt for Gemini AI to analyze medical documents."""
306
+
307
+ context_info = ""
308
+ if patient_context:
309
+ context_info = f"""
310
+ Patient Context:
311
+ - Name: {patient_context.get('name', 'Unknown')}
312
+ - Age: {patient_context.get('age', 'Unknown')}
313
+ - Sex: {patient_context.get('sex', 'Unknown')}
314
+ - Current Medications: {', '.join(patient_context.get('medications', []))}
315
+ - Past Assessment Summary: {patient_context.get('past_assessment_summary', 'None')}
316
+ """
317
+
318
+ # Determine the content type for Gemini
319
+ if mime_type.startswith('image/'):
320
+ content_type = "image"
321
+ elif mime_type == 'application/pdf':
322
+ content_type = "pdf"
323
+ elif mime_type in ['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']:
324
+ content_type = "document"
325
+ else:
326
+ content_type = "text"
327
+
328
+ prompt = f"""You are a medical AI assistant specialized in analyzing medical documents and extracting structured clinical information.
329
+
330
+ {context_info}
331
+
332
+ Please analyze the following medical document and extract all relevant clinical information in the specified JSON format.
333
+
334
+ Document Information:
335
+ - Filename: {filename}
336
+ - Content Type: {content_type}
337
+ - MIME Type: {mime_type}
338
+
339
+ Document Content (Base64 encoded):
340
+ {file_base64}
341
+
342
+ Extract the following information and return ONLY a valid JSON object with this exact structure:
343
+
344
+ {{
345
+ "overview": "Brief summary of the document content and main findings",
346
+ "diagnosis": ["list of diagnoses mentioned or identified"],
347
+ "symptoms": ["list of symptoms described"],
348
+ "medications": [
349
+ {{
350
+ "name": "medication name",
351
+ "dosage": "dosage if mentioned",
352
+ "frequency": "frequency if mentioned",
353
+ "duration": "duration if mentioned"
354
+ }}
355
+ ],
356
+ "vital_signs": {{
357
+ "blood_pressure": "value if mentioned",
358
+ "heart_rate": "value if mentioned",
359
+ "temperature": "value if mentioned",
360
+ "respiratory_rate": "value if mentioned",
361
+ "oxygen_saturation": "value if mentioned"
362
+ }},
363
+ "lab_results": [
364
+ {{
365
+ "test_name": "test name",
366
+ "value": "test value",
367
+ "unit": "unit if mentioned",
368
+ "reference_range": "normal range if mentioned"
369
+ }}
370
+ ],
371
+ "procedures": ["list of procedures mentioned or performed"],
372
+ "notes": "additional clinical notes and observations"
373
+ }}
374
+
375
+ Guidelines for Document Analysis:
376
+ 1. Carefully read and analyze the entire document content
377
+ 2. Extract information that is explicitly mentioned or clearly documented
378
+ 3. Use medical terminology appropriately and maintain accuracy
379
+ 4. If a field has no relevant information, use an empty array [] or null
380
+ 5. For medications, include all prescribed, recommended, or mentioned medications
381
+ 6. Extract vital signs only if specific values are documented
382
+ 7. Include lab results only if specific test values are provided
383
+ 8. Be thorough but conservative - prioritize accuracy over completeness
384
+ 9. For images, focus on visible text, charts, and medical data
385
+ 10. For PDFs and documents, analyze all text content systematically
386
+ 11. Return ONLY the JSON object, no additional text or explanation
387
+
388
+ Confidence Assessment:
389
+ After the JSON, provide a confidence score (0.0-1.0) based on:
390
+ - Document clarity and readability
391
+ - Specificity of medical information
392
+ - Presence of measurable values (vitals, lab results)
393
+ - Overall clinical relevance and completeness
394
+ - Document type and quality
395
+
396
+ Format: CONFIDENCE: 0.85
397
+
398
+ Return the JSON followed by the confidence score on a new line."""
399
+
400
+ return prompt
src/services/guard.py CHANGED
@@ -19,13 +19,14 @@ class SafetyGuard:
19
  - user input safety
20
  - model output safety (in context of the user question)
21
  """
 
22
 
23
  def __init__(self, nvidia_rotator: APIKeyRotator = None):
24
  self.nvidia_rotator = nvidia_rotator or APIKeyRotator("NVIDIA_API_", max_slots=5)
25
  if not self.nvidia_rotator.get_key():
26
  raise ValueError("No NVIDIA API keys found. Set NVIDIA_API_1, NVIDIA_API_2, etc. environment variables")
27
  self.base_url = "https://integrate.api.nvidia.com/v1/chat/completions"
28
- self.model = "meta/llama-guard-4-12b"
29
  self.timeout_s = settings.SAFETY_GUARD_TIMEOUT
30
  self.enabled = settings.SAFETY_GUARD_ENABLED
31
  self.fail_open = settings.SAFETY_GUARD_FAIL_OPEN
 
19
  - user input safety
20
  - model output safety (in context of the user question)
21
  """
22
+ NVIDIA_GUARD = os.getenv("NVIDIA_GUARD", "meta/llama-guard-4-12b")
23
 
24
  def __init__(self, nvidia_rotator: APIKeyRotator = None):
25
  self.nvidia_rotator = nvidia_rotator or APIKeyRotator("NVIDIA_API_", max_slots=5)
26
  if not self.nvidia_rotator.get_key():
27
  raise ValueError("No NVIDIA API keys found. Set NVIDIA_API_1, NVIDIA_API_2, etc. environment variables")
28
  self.base_url = "https://integrate.api.nvidia.com/v1/chat/completions"
29
+ self.model = NVIDIA_GUARD
30
  self.timeout_s = settings.SAFETY_GUARD_TIMEOUT
31
  self.enabled = settings.SAFETY_GUARD_ENABLED
32
  self.fail_open = settings.SAFETY_GUARD_FAIL_OPEN
static/css/emr.css CHANGED
@@ -115,6 +115,365 @@
115
  letter-spacing: 0.05em;
116
  }
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  /* Controls */
119
  .emr-controls {
120
  background-color: var(--bg-primary);
 
115
  letter-spacing: 0.05em;
116
  }
117
 
118
+ /* File Upload Section */
119
+ .emr-upload-section {
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
+ .upload-container {
128
+ max-width: 600px;
129
+ margin: 0 auto;
130
+ }
131
+
132
+ .upload-header {
133
+ text-align: center;
134
+ margin-bottom: var(--spacing-lg);
135
+ }
136
+
137
+ .upload-header h3 {
138
+ margin: 0 0 var(--spacing-sm) 0;
139
+ color: var(--text-primary);
140
+ font-size: 1.25rem;
141
+ font-weight: 600;
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ gap: var(--spacing-sm);
146
+ }
147
+
148
+ .upload-header p {
149
+ margin: 0;
150
+ color: var(--text-secondary);
151
+ font-size: 0.875rem;
152
+ }
153
+
154
+ .upload-area {
155
+ border: 2px dashed var(--border-color);
156
+ border-radius: 12px;
157
+ padding: var(--spacing-2xl);
158
+ text-align: center;
159
+ background-color: var(--bg-secondary);
160
+ transition: all var(--transition-fast);
161
+ cursor: pointer;
162
+ }
163
+
164
+ .upload-area:hover {
165
+ border-color: var(--primary-color);
166
+ background-color: var(--bg-tertiary);
167
+ }
168
+
169
+ .upload-area.dragover {
170
+ border-color: var(--primary-color);
171
+ background-color: var(--primary-color);
172
+ color: white;
173
+ }
174
+
175
+ .upload-content i {
176
+ font-size: 3rem;
177
+ color: var(--primary-color);
178
+ margin-bottom: var(--spacing-md);
179
+ }
180
+
181
+ .upload-area.dragover .upload-content i {
182
+ color: white;
183
+ }
184
+
185
+ .upload-content h4 {
186
+ margin: 0 0 var(--spacing-sm) 0;
187
+ color: var(--text-primary);
188
+ font-size: 1.125rem;
189
+ font-weight: 600;
190
+ }
191
+
192
+ .upload-area.dragover .upload-content h4 {
193
+ color: white;
194
+ }
195
+
196
+ .upload-content p {
197
+ margin: 0 0 var(--spacing-lg) 0;
198
+ color: var(--text-secondary);
199
+ font-size: 0.875rem;
200
+ }
201
+
202
+ .upload-area.dragover .upload-content p {
203
+ color: white;
204
+ }
205
+
206
+ .upload-progress {
207
+ margin-top: var(--spacing-lg);
208
+ }
209
+
210
+ .progress-bar {
211
+ width: 100%;
212
+ height: 8px;
213
+ background-color: var(--bg-tertiary);
214
+ border-radius: 4px;
215
+ overflow: hidden;
216
+ margin-bottom: var(--spacing-sm);
217
+ }
218
+
219
+ .progress-fill {
220
+ height: 100%;
221
+ background-color: var(--primary-color);
222
+ transition: width var(--transition-normal);
223
+ width: 0%;
224
+ }
225
+
226
+ .progress-text {
227
+ font-size: 0.875rem;
228
+ color: var(--text-secondary);
229
+ text-align: center;
230
+ display: block;
231
+ }
232
+
233
+ /* Document Preview Modal */
234
+ .document-preview-modal {
235
+ max-width: 900px;
236
+ max-height: 85vh;
237
+ overflow-y: auto;
238
+ }
239
+
240
+ .document-preview-section {
241
+ margin-bottom: var(--spacing-lg);
242
+ background-color: var(--bg-secondary);
243
+ border-radius: 8px;
244
+ padding: var(--spacing-md);
245
+ border: 1px solid var(--border-color);
246
+ }
247
+
248
+ .document-preview-section h4 {
249
+ margin: 0 0 var(--spacing-md) 0;
250
+ color: var(--text-primary);
251
+ font-size: 1rem;
252
+ font-weight: 600;
253
+ border-bottom: 1px solid var(--border-color);
254
+ padding-bottom: var(--spacing-sm);
255
+ display: flex;
256
+ align-items: center;
257
+ gap: var(--spacing-sm);
258
+ }
259
+
260
+ .document-preview-section h4 i {
261
+ color: var(--primary-color);
262
+ }
263
+
264
+ .editable-field {
265
+ background-color: var(--bg-primary);
266
+ border: 1px solid var(--border-color);
267
+ border-radius: 6px;
268
+ padding: var(--spacing-sm);
269
+ margin-bottom: var(--spacing-sm);
270
+ min-height: 40px;
271
+ transition: border-color var(--transition-fast);
272
+ }
273
+
274
+ .editable-field:focus {
275
+ outline: none;
276
+ border-color: var(--primary-color);
277
+ box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
278
+ }
279
+
280
+ .editable-list {
281
+ list-style: none;
282
+ padding: 0;
283
+ margin: 0;
284
+ }
285
+
286
+ .editable-list li {
287
+ background-color: var(--bg-primary);
288
+ border: 1px solid var(--border-color);
289
+ border-radius: 6px;
290
+ padding: var(--spacing-sm);
291
+ margin-bottom: var(--spacing-sm);
292
+ display: flex;
293
+ align-items: center;
294
+ gap: var(--spacing-sm);
295
+ }
296
+
297
+ .editable-list li input {
298
+ flex: 1;
299
+ background: none;
300
+ border: none;
301
+ outline: none;
302
+ color: var(--text-primary);
303
+ font-size: 0.875rem;
304
+ }
305
+
306
+ .editable-list li button {
307
+ background: none;
308
+ border: none;
309
+ color: var(--accent-color);
310
+ cursor: pointer;
311
+ padding: var(--spacing-xs);
312
+ border-radius: 4px;
313
+ transition: background-color var(--transition-fast);
314
+ }
315
+
316
+ .editable-list li button:hover {
317
+ background-color: var(--bg-tertiary);
318
+ }
319
+
320
+ .add-item-btn {
321
+ background-color: var(--primary-color);
322
+ color: white;
323
+ border: none;
324
+ border-radius: 6px;
325
+ padding: var(--spacing-sm) var(--spacing-md);
326
+ cursor: pointer;
327
+ font-size: 0.875rem;
328
+ transition: background-color var(--transition-fast);
329
+ display: flex;
330
+ align-items: center;
331
+ gap: var(--spacing-xs);
332
+ }
333
+
334
+ .add-item-btn:hover {
335
+ background-color: var(--primary-hover);
336
+ }
337
+
338
+ .medication-preview-item {
339
+ background-color: var(--bg-primary);
340
+ border: 1px solid var(--border-color);
341
+ border-radius: 8px;
342
+ padding: var(--spacing-md);
343
+ margin-bottom: var(--spacing-sm);
344
+ }
345
+
346
+ .medication-preview-item h5 {
347
+ margin: 0 0 var(--spacing-sm) 0;
348
+ color: var(--text-primary);
349
+ font-size: 1rem;
350
+ font-weight: 600;
351
+ }
352
+
353
+ .medication-detail-row {
354
+ display: grid;
355
+ grid-template-columns: 1fr 1fr;
356
+ gap: var(--spacing-md);
357
+ margin-bottom: var(--spacing-sm);
358
+ }
359
+
360
+ .medication-detail-row:last-child {
361
+ margin-bottom: 0;
362
+ }
363
+
364
+ .medication-detail-row label {
365
+ font-size: 0.75rem;
366
+ color: var(--text-secondary);
367
+ text-transform: uppercase;
368
+ letter-spacing: 0.05em;
369
+ margin-bottom: var(--spacing-xs);
370
+ display: block;
371
+ }
372
+
373
+ .medication-detail-row input {
374
+ width: 100%;
375
+ background-color: var(--bg-secondary);
376
+ border: 1px solid var(--border-color);
377
+ border-radius: 4px;
378
+ padding: var(--spacing-sm);
379
+ color: var(--text-primary);
380
+ font-size: 0.875rem;
381
+ }
382
+
383
+ .medication-detail-row input:focus {
384
+ outline: none;
385
+ border-color: var(--primary-color);
386
+ }
387
+
388
+ .vital-signs-preview-grid {
389
+ display: grid;
390
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
391
+ gap: var(--spacing-md);
392
+ }
393
+
394
+ .vital-sign-preview-item {
395
+ background-color: var(--bg-primary);
396
+ border: 1px solid var(--border-color);
397
+ border-radius: 8px;
398
+ padding: var(--spacing-md);
399
+ text-align: center;
400
+ }
401
+
402
+ .vital-sign-preview-item label {
403
+ font-size: 0.75rem;
404
+ color: var(--text-secondary);
405
+ text-transform: uppercase;
406
+ letter-spacing: 0.05em;
407
+ margin-bottom: var(--spacing-xs);
408
+ display: block;
409
+ }
410
+
411
+ .vital-sign-preview-item input {
412
+ width: 100%;
413
+ background-color: var(--bg-secondary);
414
+ border: 1px solid var(--border-color);
415
+ border-radius: 4px;
416
+ padding: var(--spacing-sm);
417
+ color: var(--text-primary);
418
+ font-size: 0.875rem;
419
+ text-align: center;
420
+ }
421
+
422
+ .vital-sign-preview-item input:focus {
423
+ outline: none;
424
+ border-color: var(--primary-color);
425
+ }
426
+
427
+ .lab-result-preview-item {
428
+ background-color: var(--bg-primary);
429
+ border: 1px solid var(--border-color);
430
+ border-radius: 8px;
431
+ padding: var(--spacing-md);
432
+ margin-bottom: var(--spacing-sm);
433
+ }
434
+
435
+ .lab-result-preview-item h5 {
436
+ margin: 0 0 var(--spacing-sm) 0;
437
+ color: var(--text-primary);
438
+ font-size: 1rem;
439
+ font-weight: 600;
440
+ }
441
+
442
+ .lab-result-detail-row {
443
+ display: grid;
444
+ grid-template-columns: 1fr 1fr 1fr;
445
+ gap: var(--spacing-md);
446
+ margin-bottom: var(--spacing-sm);
447
+ }
448
+
449
+ .lab-result-detail-row:last-child {
450
+ margin-bottom: 0;
451
+ }
452
+
453
+ .lab-result-detail-row label {
454
+ font-size: 0.75rem;
455
+ color: var(--text-secondary);
456
+ text-transform: uppercase;
457
+ letter-spacing: 0.05em;
458
+ margin-bottom: var(--spacing-xs);
459
+ display: block;
460
+ }
461
+
462
+ .lab-result-detail-row input {
463
+ width: 100%;
464
+ background-color: var(--bg-secondary);
465
+ border: 1px solid var(--border-color);
466
+ border-radius: 4px;
467
+ padding: var(--spacing-sm);
468
+ color: var(--text-primary);
469
+ font-size: 0.875rem;
470
+ }
471
+
472
+ .lab-result-detail-row input:focus {
473
+ outline: none;
474
+ border-color: var(--primary-color);
475
+ }
476
+
477
  /* Controls */
478
  .emr-controls {
479
  background-color: var(--bg-primary);
static/emr.html CHANGED
@@ -51,6 +51,34 @@
51
  </div>
52
  </div>
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  <!-- Search and Filters -->
55
  <div class="emr-controls">
56
  <div class="search-container">
@@ -239,6 +267,23 @@
239
  </div>
240
  </div>
241
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  </div>
243
 
244
  <script src="/static/js/emr.js"></script>
 
51
  </div>
52
  </div>
53
 
54
+ <!-- File Upload Section -->
55
+ <div class="emr-upload-section">
56
+ <div class="upload-container">
57
+ <div class="upload-header">
58
+ <h3><i class="fas fa-upload"></i> Upload Medical Document</h3>
59
+ <p>Upload PDF, image, or document files to extract medical information</p>
60
+ </div>
61
+ <div class="upload-area" id="uploadArea">
62
+ <div class="upload-content">
63
+ <i class="fas fa-cloud-upload-alt"></i>
64
+ <h4>Drop files here or click to browse</h4>
65
+ <p>Supported formats: PDF, DOC, DOCX, JPG, PNG, TIFF</p>
66
+ <input type="file" id="fileInput" accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.tiff" multiple style="display: none;">
67
+ <button class="btn btn-primary" id="uploadBtn">
68
+ <i class="fas fa-folder-open"></i>
69
+ Choose Files
70
+ </button>
71
+ </div>
72
+ </div>
73
+ <div class="upload-progress" id="uploadProgress" style="display: none;">
74
+ <div class="progress-bar">
75
+ <div class="progress-fill" id="progressFill"></div>
76
+ </div>
77
+ <span class="progress-text" id="progressText">Uploading...</span>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
  <!-- Search and Filters -->
83
  <div class="emr-controls">
84
  <div class="search-container">
 
267
  </div>
268
  </div>
269
  </div>
270
+
271
+ <!-- Document Analysis Preview Modal -->
272
+ <div class="modal" id="documentPreviewModal">
273
+ <div class="modal-content document-preview-modal">
274
+ <div class="modal-header">
275
+ <h3><i class="fas fa-file-medical"></i> Document Analysis Preview</h3>
276
+ <button class="modal-close" id="documentPreviewModalClose">&times;</button>
277
+ </div>
278
+ <div class="modal-body" id="documentPreviewContent">
279
+ <!-- Document analysis results will be populated here -->
280
+ </div>
281
+ <div class="modal-footer">
282
+ <button class="btn btn-secondary" id="documentPreviewCancel">Cancel</button>
283
+ <button class="btn btn-primary" id="saveDocumentAnalysis">Save to EMR</button>
284
+ </div>
285
+ </div>
286
+ </div>
287
  </div>
288
 
289
  <script src="/static/js/emr.js"></script>
static/js/emr.js CHANGED
@@ -58,6 +58,9 @@ class EMRPage {
58
 
59
  // Modal handlers
60
  this.setupModalHandlers();
 
 
 
61
  }
62
 
63
  setupModalHandlers() {
@@ -102,6 +105,81 @@ class EMRPage {
102
  searchModal.classList.remove('show');
103
  });
104
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
 
107
  async loadPatientFromURL() {
 
58
 
59
  // Modal handlers
60
  this.setupModalHandlers();
61
+
62
+ // File upload handlers
63
+ this.setupFileUploadHandlers();
64
  }
65
 
66
  setupModalHandlers() {
 
105
  searchModal.classList.remove('show');
106
  });
107
  }
108
+
109
+ // Document Preview Modal
110
+ const documentPreviewModal = document.getElementById('documentPreviewModal');
111
+ const documentPreviewModalClose = document.getElementById('documentPreviewModalClose');
112
+ const documentPreviewCancel = document.getElementById('documentPreviewCancel');
113
+ const saveDocumentAnalysis = document.getElementById('saveDocumentAnalysis');
114
+
115
+ if (documentPreviewModalClose) {
116
+ documentPreviewModalClose.addEventListener('click', () => {
117
+ documentPreviewModal.classList.remove('show');
118
+ });
119
+ }
120
+
121
+ if (documentPreviewCancel) {
122
+ documentPreviewCancel.addEventListener('click', () => {
123
+ documentPreviewModal.classList.remove('show');
124
+ });
125
+ }
126
+
127
+ if (saveDocumentAnalysis) {
128
+ saveDocumentAnalysis.addEventListener('click', () => {
129
+ this.saveDocumentAnalysis();
130
+ });
131
+ }
132
+ }
133
+
134
+ setupFileUploadHandlers() {
135
+ const uploadArea = document.getElementById('uploadArea');
136
+ const fileInput = document.getElementById('fileInput');
137
+ const uploadBtn = document.getElementById('uploadBtn');
138
+ const uploadProgress = document.getElementById('uploadProgress');
139
+
140
+ // Click to upload
141
+ if (uploadBtn) {
142
+ uploadBtn.addEventListener('click', () => {
143
+ fileInput.click();
144
+ });
145
+ }
146
+
147
+ if (uploadArea) {
148
+ uploadArea.addEventListener('click', () => {
149
+ fileInput.click();
150
+ });
151
+ }
152
+
153
+ // File input change
154
+ if (fileInput) {
155
+ fileInput.addEventListener('change', (e) => {
156
+ if (e.target.files.length > 0) {
157
+ this.handleFileUpload(e.target.files);
158
+ }
159
+ });
160
+ }
161
+
162
+ // Drag and drop
163
+ if (uploadArea) {
164
+ uploadArea.addEventListener('dragover', (e) => {
165
+ e.preventDefault();
166
+ uploadArea.classList.add('dragover');
167
+ });
168
+
169
+ uploadArea.addEventListener('dragleave', (e) => {
170
+ e.preventDefault();
171
+ uploadArea.classList.remove('dragover');
172
+ });
173
+
174
+ uploadArea.addEventListener('drop', (e) => {
175
+ e.preventDefault();
176
+ uploadArea.classList.remove('dragover');
177
+
178
+ if (e.dataTransfer.files.length > 0) {
179
+ this.handleFileUpload(e.dataTransfer.files);
180
+ }
181
+ });
182
+ }
183
  }
184
 
185
  async loadPatientFromURL() {