Boghdady9 commited on
Commit
2c3d17b
·
1 Parent(s): 527a351

Initial commit: MediVision Radiology System

Browse files

A comprehensive medical imaging analysis platform with:
- Streamlit frontend for medical image upload and analysis
- FastAPI backend for AI-powered radiology report generation
- Automatic MRN generation and patient search functionality
- PDF report generation and download
- MongoDB integration for patient data storage
- Docker deployment support
- Environment variable configuration for security

All credentials are managed through environment variables:
- HF_TOKEN: Hugging Face API token for AI model access
- MONGODB_URI: MongoDB connection string for database access

Files changed (5) hide show
  1. Dockerfile +9 -2
  2. README.md +1 -1
  3. requirements.txt +8 -1
  4. src/backend/service.py +739 -0
  5. src/streamlit_app.py +1454 -38
Dockerfile CHANGED
@@ -14,8 +14,15 @@ COPY src/ ./src/
14
 
15
  RUN pip3 install -r requirements.txt
16
 
17
- EXPOSE 8501
 
18
 
19
  HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
20
 
21
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
 
 
 
 
 
 
14
 
15
  RUN pip3 install -r requirements.txt
16
 
17
+ # Expose both ports (8001 for backend, 8501 for frontend)
18
+ EXPOSE 8001 8501
19
 
20
  HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
21
 
22
+ # Create startup script to run both services
23
+ RUN echo '#!/bin/bash\n\
24
+ cd /app/src/backend && uvicorn service:app --host 0.0.0.0 --port 8001 &\n\
25
+ cd /app && streamlit run src/streamlit_app.py --server.port=8501 --server.address=0.0.0.0\n\
26
+ ' > /app/start.sh && chmod +x /app/start.sh
27
+
28
+ ENTRYPOINT ["/app/start.sh"]
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Appli
3
  emoji: 🚀
4
  colorFrom: red
5
  colorTo: red
 
1
  ---
2
+ title: Nno
3
  emoji: 🚀
4
  colorFrom: red
5
  colorTo: red
requirements.txt CHANGED
@@ -1,3 +1,10 @@
1
  altair
2
  pandas
3
- streamlit
 
 
 
 
 
 
 
 
1
  altair
2
  pandas
3
+ streamlit
4
+ fastapi==0.104.1
5
+ uvicorn==0.24.0
6
+ python-multipart==0.0.6
7
+ pillow==10.1.0
8
+ requests==2.31.0
9
+ reportlab==4.0.7
10
+ pymongo==4.6.0
src/backend/service.py ADDED
@@ -0,0 +1,739 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import sys
4
+ import json
5
+ import base64
6
+ import certifi
7
+ import requests
8
+ from PIL import Image
9
+ from bson import ObjectId
10
+ from gridfs import GridFS
11
+ from pymongo import MongoClient
12
+ from fastapi import FastAPI, File, UploadFile, HTTPException
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from fastapi.responses import FileResponse
15
+ import uvicorn
16
+ from reportlab.pdfgen import canvas
17
+ from reportlab.lib.pagesizes import letter, A4
18
+ from reportlab.lib.styles import getSampleStyleSheet
19
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
20
+ from reportlab.lib.units import inch
21
+ from datetime import datetime
22
+ import hashlib
23
+ import random
24
+ import string
25
+
26
+ API_URL = "https://n0bcq3t2emgrp7ip.us-east-1.aws.endpoints.huggingface.cloud"
27
+ headers = {
28
+ "Accept": "application/json",
29
+ "Authorization": f"Bearer {os.getenv('HF_TOKEN')}",
30
+ "Content-Type": "application/json"
31
+ }
32
+
33
+ reportsaved= []
34
+
35
+
36
+ # Disable verbose tracebacks
37
+ sys.tracebacklimit = 0
38
+
39
+ # Configure Python path
40
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
41
+ sys.path.append(parent_dir)
42
+
43
+ def get_reports_with_empty_findings():
44
+ reportsaved = []
45
+ try:
46
+ # Use environment variable for MongoDB connection string
47
+ mongo_uri = os.getenv('MONGODB_URI',
48
+ "mongodb://localhost:27017/radiologyDB" # Fallback to local MongoDB
49
+ )
50
+ client = MongoClient(
51
+ mongo_uri,
52
+ tlsCAFile=certifi.where(),
53
+ serverSelectionTimeoutMS=5000
54
+ )
55
+
56
+ db = client.radiologyDB
57
+ reports = [str(doc["_id"]) for doc in db.reports.find({"findings": ""})]
58
+ reportsaved = reports
59
+
60
+ # ✅ First do all processing (analyze, reconstruct, etc.)
61
+ for report_id in reportsaved:
62
+ analyze(report_id) # this includes image reconstruction and update
63
+
64
+ # ✅ Then print JSON as final stdout response
65
+ print(json.dumps({
66
+ "success": True,
67
+ "count": len(reportsaved),
68
+ "reports": reportsaved
69
+ }), flush=True)
70
+
71
+ except Exception as e:
72
+ print(json.dumps({
73
+ "success": False,
74
+ "error": str(e),
75
+ "count": 0,
76
+ "reports": []
77
+ }), flush=True)
78
+ finally:
79
+ if 'client' in locals():
80
+ client.close()
81
+
82
+
83
+ def update_findings(report_id, new_findings_text):
84
+ """Updates findings for a specific report"""
85
+ try:
86
+ mongo_uri = os.getenv('MONGODB_URI',
87
+ "mongodb://localhost:27017/radiologyDB" # Fallback to local MongoDB
88
+ )
89
+ client = MongoClient(
90
+ mongo_uri,
91
+ tlsCAFile=certifi.where()
92
+ )
93
+ db = client.radiologyDB
94
+
95
+ result = db.reports.update_one(
96
+ {"_id": ObjectId(report_id)},
97
+ {"$set": {"findings": new_findings_text.strip()}}
98
+ )
99
+
100
+ return {
101
+ "success": result.modified_count > 0,
102
+ "modified_count": result.modified_count,
103
+ "message": "Updated" if result.modified_count else "No document found"
104
+ }
105
+
106
+ except Exception as e:
107
+ return {
108
+ "success": False,
109
+ "error": str(e),
110
+ "message": f"Error: {str(e)}"
111
+ }
112
+ finally:
113
+ if 'client' in locals():
114
+ client.close()
115
+
116
+
117
+ def reconstruct_image_if_not_exists(report_id):
118
+ try:
119
+ mongo_uri = os.getenv('MONGODB_URI',
120
+ "mongodb://localhost:27017/radiologyDB" # Fallback to local MongoDB
121
+ )
122
+ client = MongoClient(
123
+ mongo_uri,
124
+ tlsCAFile=certifi.where()
125
+ )
126
+
127
+ db = client["radiologyDB"]
128
+ fs = GridFS(db, collection="images") # ✅ Correct bucket
129
+
130
+
131
+ report = db.reports.find_one({"_id": ObjectId(report_id)})
132
+ if not report:
133
+ print(f"❌ Report not found for ID: {report_id}", file=sys.stderr)
134
+ return
135
+
136
+ if "images" not in report or len(report["images"]) == 0:
137
+ print(f"❌ No image ID found in report: {report_id}", file=sys.stderr)
138
+ return
139
+
140
+ image_id = report["images"][0] # Assuming one image per report
141
+
142
+ output_dir = os.path.join(os.path.dirname(__file__), "images")
143
+ os.makedirs(output_dir, exist_ok=True)
144
+
145
+ output_path = os.path.join(output_dir, f"{image_id}.jpg")
146
+
147
+ # Check if image already exists
148
+ if os.path.exists(output_path):
149
+ print(f"✅ Image already exists: {output_path}", file=sys.stderr)
150
+ return
151
+
152
+ # Get the file from GridFS
153
+ grid_out = fs.get(ObjectId(image_id))
154
+ with open(output_path, "wb") as f:
155
+ f.write(grid_out.read())
156
+
157
+ print(f"✅ Reconstructed and saved image: {output_path}", file=sys.stderr)
158
+
159
+
160
+ except Exception as e:
161
+ print(f"❌ Error reconstructing image: {e}", file=sys.stderr)
162
+
163
+ finally:
164
+ if 'client' in locals():
165
+ client.close()
166
+
167
+ def query(payload):
168
+ response = requests.post(API_URL, headers=headers, json=payload)
169
+ print(f"Hugging Face API Response: {response.text}", file=sys.stderr)
170
+ return response.json()
171
+
172
+ def analyze_image(image_path: str):
173
+ """
174
+ Analyze medical image using Hugging Face API.
175
+ Takes an image path and a prompt, then returns AI-generated findings.
176
+ """
177
+ try:
178
+ # Load the image using PIL
179
+ image = Image.open(image_path)
180
+
181
+ # Convert image to base64 string
182
+ buffer = io.BytesIO()
183
+ image.save(buffer, format="PNG")
184
+ img_str = base64.b64encode(buffer.getvalue()).decode("utf-8")
185
+
186
+ # Send to Hugging Face API
187
+ output = query({
188
+ "inputs": img_str,
189
+ })
190
+
191
+ return output
192
+
193
+ except Exception as e:
194
+ # Fallback response in case of API errors
195
+ return f"Error analyzing image: {str(e)}. Using fallback analysis."
196
+
197
+ def analyze(id):
198
+ """
199
+ Analyze medical images using Hugging Face API for findings extraction.
200
+ """
201
+ try:
202
+ # First reconstruct the image
203
+ reconstruct_image_if_not_exists(id)
204
+
205
+ # Get the path to the reconstructed image
206
+ image_path = os.path.join(os.path.dirname(__file__), "images", f"{id}.jpg")
207
+
208
+ # Analyze the image using Hugging Face API
209
+ findings = analyze_image(image_path)
210
+
211
+ # If we got a valid response, update the findings
212
+ if isinstance(findings, str):
213
+ template_response = findings
214
+ else:
215
+ # Format the API response into a readable string (no prefix)
216
+ template_response = json.dumps(findings, indent=2)
217
+
218
+ # Update the findings in the database
219
+ update_findings(id, template_response)
220
+
221
+ return template_response
222
+
223
+ except Exception as e:
224
+ error_msg = f"Error during analysis: {str(e)}"
225
+ print(error_msg, file=sys.stderr)
226
+ return error_msg
227
+
228
+ app = FastAPI()
229
+
230
+ app.add_middleware(
231
+ CORSMiddleware,
232
+ allow_origins=["*"],
233
+ allow_credentials=True,
234
+ allow_methods=["*"],
235
+ allow_headers=["*"],
236
+ )
237
+
238
+ # Mount static files directory
239
+ # Mount static files directory - removed since using remote image
240
+ # app.mount("/static", StaticFiles(directory=os.path.dirname(__file__)), name="static")
241
+
242
+ @app.get("/health")
243
+ async def health_check():
244
+ """Health check endpoint"""
245
+ return {"status": "healthy", "service": "radiology-backend"}
246
+
247
+ @app.get("/")
248
+ async def root():
249
+ """Root endpoint with service info"""
250
+ return {
251
+ "service": "MediVision Radiology Backend",
252
+ "version": "1.0.0",
253
+ "status": "running",
254
+ "endpoints": {
255
+ "analyze": "/analyze/",
256
+ "health": "/health"
257
+ }
258
+ }
259
+
260
+ @app.post("/analyze/")
261
+ async def analyze_endpoint(file: UploadFile = File(...)):
262
+ """
263
+ Accepts an uploaded image file and returns AI-generated findings.
264
+ """
265
+ try:
266
+ # Validate file type
267
+ if not file.content_type.startswith('image/'):
268
+ return {"error": "Invalid file type. Please upload an image file.", "findings": "File type error"}
269
+
270
+ contents = await file.read()
271
+ if len(contents) == 0:
272
+ return {"error": "Empty file uploaded.", "findings": "Empty file error"}
273
+
274
+ # Process image
275
+ image = Image.open(io.BytesIO(contents))
276
+
277
+ # Convert to RGB if necessary
278
+ if image.mode != 'RGB':
279
+ image = image.convert('RGB')
280
+
281
+ # Resize image if too large (optional optimization)
282
+ max_size = (1024, 1024)
283
+ if image.size[0] > max_size[0] or image.size[1] > max_size[1]:
284
+ image.thumbnail(max_size, Image.Resampling.LANCZOS)
285
+
286
+ buffer = io.BytesIO()
287
+ image.save(buffer, format="PNG", optimize=True)
288
+ img_str = base64.b64encode(buffer.getvalue()).decode("utf-8")
289
+
290
+ # Get response from Hugging Face API
291
+ print(f"Sending image to Hugging Face API (size: {len(img_str)} chars)", file=sys.stderr)
292
+ output = query({"inputs": img_str})
293
+
294
+ # Enhanced response processing
295
+ findings = ""
296
+ if isinstance(output, dict):
297
+ if "error" in output:
298
+ findings = f"API Error: {output['error']}"
299
+ elif "generated_text" in output:
300
+ findings = output["generated_text"]
301
+ else:
302
+ findings = str(output)
303
+ elif isinstance(output, list) and len(output) > 0:
304
+ first_result = output[0]
305
+ if isinstance(first_result, dict) and "generated_text" in first_result:
306
+ findings = first_result["generated_text"]
307
+ else:
308
+ findings = str(first_result)
309
+ elif isinstance(output, str):
310
+ findings = output
311
+ else:
312
+ findings = f"Unexpected response format: {type(output)}"
313
+
314
+ print(f"Analysis complete. Findings length: {len(findings)} chars", file=sys.stderr)
315
+
316
+ # Return formatted findings
317
+ return {"findings": findings, "status": "success"}
318
+
319
+ except Exception as e:
320
+ error_msg = f"Error in analyze_endpoint: {str(e)}"
321
+ print(error_msg, file=sys.stderr)
322
+ return {"error": error_msg, "findings": "Error analyzing image", "status": "error"}
323
+
324
+ def generate_pdf_report(patient_name, patient_info, findings):
325
+ """Generate a PDF report for the patient with optimized layout"""
326
+ try:
327
+ # Create reports directory if it doesn't exist
328
+ reports_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "pdf_reports")
329
+ os.makedirs(reports_dir, exist_ok=True)
330
+
331
+ # Create filename
332
+ safe_name = "".join(c for c in patient_name if c.isalnum() or c in (' ', '-', '_')).rstrip()
333
+ pdf_filename = f"{safe_name}_report.pdf"
334
+ pdf_path = os.path.join(reports_dir, pdf_filename)
335
+
336
+ # Create PDF document with optimized margins
337
+ doc = SimpleDocTemplate(pdf_path, pagesize=A4,
338
+ topMargin=0.8*inch, bottomMargin=0.6*inch,
339
+ leftMargin=0.8*inch, rightMargin=0.8*inch)
340
+ styles = getSampleStyleSheet()
341
+ story = []
342
+
343
+ # Custom styles for better space utilization
344
+ compact_style = styles['Normal'].clone('CompactStyle')
345
+ compact_style.fontSize = 10
346
+ compact_style.leading = 12
347
+ compact_style.spaceAfter = 6
348
+
349
+ header_style = styles['Heading1'].clone('CompactHeader')
350
+ header_style.fontSize = 16
351
+ header_style.alignment = 1 # Center
352
+ header_style.spaceAfter = 8
353
+
354
+ section_style = styles['Heading2'].clone('CompactSection')
355
+ section_style.fontSize = 12
356
+ section_style.spaceAfter = 4
357
+ section_style.spaceBefore = 8
358
+
359
+ # Compact header
360
+ story.append(Paragraph("🔬 <b>MediVision Radiology Center</b>", header_style))
361
+ story.append(Paragraph("<i>AI-Powered Medical Imaging Analysis</i>", compact_style))
362
+ story.append(Spacer(1, 0.2*inch))
363
+
364
+ # Patient Information in compact table format
365
+ story.append(Paragraph("<b>RADIOLOGY REPORT</b>", section_style))
366
+
367
+ # Create a compact patient info section
368
+ current_date = datetime.now().strftime("%m/%d/%Y")
369
+ current_time = datetime.now().strftime("%H:%M")
370
+
371
+ patient_info_compact = f"""
372
+ <b>Patient:</b> {patient_info.get('name', 'N/A')} &nbsp;&nbsp;&nbsp;
373
+ <b>MRN:</b> {patient_info.get('medical_record_number', 'N/A')} &nbsp;&nbsp;&nbsp;
374
+ <b>DOB:</b> {patient_info.get('date_of_birth', 'N/A')}<br/>
375
+ <b>Gender:</b> {patient_info.get('gender', 'N/A')} &nbsp;&nbsp;&nbsp;
376
+ <b>Study Date:</b> {patient_info.get('date_of_study', current_date)} &nbsp;&nbsp;&nbsp;
377
+ <b>Physician:</b> Dr. {patient_info.get('referring_physician', 'N/A')}<br/>
378
+ <b>Report Date:</b> {current_date} {current_time} &nbsp;&nbsp;&nbsp;
379
+ <b>Radiologist:</b> MediVision AI
380
+ """
381
+ story.append(Paragraph(patient_info_compact, compact_style))
382
+ story.append(Spacer(1, 0.15*inch))
383
+
384
+ # Process findings to remove duplicates and optimize content
385
+ if "answer:" in findings.lower():
386
+ parts = findings.split("answer:", 1)
387
+ if len(parts) == 2:
388
+ findings_text = parts[1].strip()
389
+ else:
390
+ findings_text = findings
391
+ else:
392
+ findings_text = findings
393
+
394
+ # Clean up findings - remove redundant phrases
395
+ findings_text = findings_text.replace("Brief Structured Report:", "")
396
+ findings_text = findings_text.replace("The chest X-ray is a useful tool for diagnosing conditions like pneumonia, lung cancer, or other lung diseases.", "")
397
+
398
+ # Format findings with better structure
399
+ story.append(Paragraph("<b>FINDINGS & IMPRESSION:</b>", section_style))
400
+
401
+ # Split findings into structured sections if possible
402
+ lines = findings_text.strip().split('\n')
403
+ formatted_findings = ""
404
+
405
+ for line in lines:
406
+ line = line.strip()
407
+ if line:
408
+ # Check if it's a numbered item or bullet point
409
+ if line.startswith(('1.', '2.', '3.', '4.', '5.')):
410
+ formatted_findings += f"<b>{line}</b><br/>"
411
+ elif line.upper().startswith(('EXAM TYPE:', 'FINDINGS:', 'IMPRESSION:')):
412
+ formatted_findings += f"<b>{line}</b><br/>"
413
+ else:
414
+ formatted_findings += f"{line}<br/>"
415
+
416
+ if not formatted_findings.strip():
417
+ formatted_findings = findings_text.replace('\n', '<br/>')
418
+
419
+ story.append(Paragraph(formatted_findings, compact_style))
420
+ story.append(Spacer(1, 0.15*inch))
421
+
422
+ # Compact footer with essential info only
423
+ footer_text = f"""
424
+ <i>Generated by MediVision AI • {current_date} {current_time} •
425
+ This AI-generated report requires radiologist review for clinical use.</i>
426
+ """
427
+ footer_style = compact_style.clone('FooterStyle')
428
+ footer_style.fontSize = 8
429
+ footer_style.alignment = 1 # Center
430
+ footer_style.textColor = 'grey'
431
+
432
+ story.append(Paragraph(footer_text, footer_style))
433
+
434
+ # Build PDF
435
+ doc.build(story)
436
+
437
+ return pdf_path
438
+
439
+ except Exception as e:
440
+ print(f"Error generating PDF: {str(e)}", file=sys.stderr)
441
+ return None
442
+
443
+ @app.get("/generate-mrn/")
444
+ async def generate_mrn_endpoint(patient_name: str, date_of_birth: str):
445
+ """Generate MRN for a patient"""
446
+ try:
447
+ mrn = generate_mrn(patient_name, date_of_birth)
448
+ return {
449
+ "success": True,
450
+ "mrn": mrn,
451
+ "patient_name": patient_name,
452
+ "date_of_birth": date_of_birth
453
+ }
454
+ except Exception as e:
455
+ return {
456
+ "success": False,
457
+ "error": f"Error generating MRN: {str(e)}"
458
+ }
459
+
460
+ @app.get("/patient/{mrn}")
461
+ async def get_patient_by_mrn(mrn: str):
462
+ """Get patient information by MRN"""
463
+ result = search_patient_by_mrn(mrn)
464
+ if result["found"]:
465
+ return result["patient"]
466
+ else:
467
+ raise HTTPException(status_code=404, detail=result.get("message", "Patient not found"))
468
+
469
+ @app.get("/patients")
470
+ async def list_patients():
471
+ """List all patients"""
472
+ result = list_all_patients()
473
+ return result
474
+
475
+ @app.post("/generate-report/")
476
+ async def generate_report_endpoint(
477
+ patient_name: str,
478
+ medical_record_number: str = "",
479
+ date_of_birth: str = "",
480
+ gender: str = "",
481
+ referring_physician: str = "",
482
+ date_of_study: str = "",
483
+ findings: str = ""
484
+ ):
485
+ """Generate and save a PDF report"""
486
+ try:
487
+ patient_info = {
488
+ "name": patient_name,
489
+ "medical_record_number": medical_record_number,
490
+ "date_of_birth": date_of_birth,
491
+ "gender": gender,
492
+ "referring_physician": referring_physician,
493
+ "date_of_study": date_of_study
494
+ }
495
+
496
+ # Generate MRN if not provided
497
+ mrn = medical_record_number
498
+ if not mrn or len(mrn) == 0:
499
+ mrn = generate_mrn(patient_name, date_of_birth)
500
+
501
+ # Save patient record to database
502
+ save_result = save_patient_record(patient_info, findings, mrn)
503
+
504
+ if not save_result.get("success", False):
505
+ return {
506
+ "success": False,
507
+ "error": "Failed to save patient record"
508
+ }
509
+
510
+ pdf_path = generate_pdf_report(patient_name, patient_info, findings)
511
+
512
+ if pdf_path and os.path.exists(pdf_path):
513
+ return {
514
+ "success": True,
515
+ "message": "Report generated successfully",
516
+ "pdf_filename": os.path.basename(pdf_path),
517
+ "mrn": mrn
518
+ }
519
+ else:
520
+ return {
521
+ "success": False,
522
+ "error": "Failed to generate PDF report"
523
+ }
524
+
525
+ except Exception as e:
526
+ return {
527
+ "success": False,
528
+ "error": f"Error generating report: {str(e)}"
529
+ }
530
+
531
+ @app.get("/reports/{patient_name}/pdf")
532
+ async def download_pdf_report(patient_name: str):
533
+ """Download PDF report for a patient"""
534
+ try:
535
+ reports_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "pdf_reports")
536
+ safe_name = "".join(c for c in patient_name if c.isalnum() or c in (' ', '-', '_')).rstrip()
537
+ pdf_filename = f"{safe_name}_report.pdf"
538
+ pdf_path = os.path.join(reports_dir, pdf_filename)
539
+
540
+ if os.path.exists(pdf_path):
541
+ return FileResponse(
542
+ path=pdf_path,
543
+ filename=pdf_filename,
544
+ media_type='application/pdf'
545
+ )
546
+ else:
547
+ raise HTTPException(status_code=404, detail=f"PDF report not found for patient: {patient_name}")
548
+
549
+ except Exception as e:
550
+ raise HTTPException(status_code=500, detail=f"Error retrieving PDF: {str(e)}")
551
+
552
+ @app.get("/reports")
553
+ async def list_reports():
554
+ """List all available PDF reports"""
555
+ try:
556
+ reports_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "pdf_reports")
557
+
558
+ if not os.path.exists(reports_dir):
559
+ return {"reports": []}
560
+
561
+ pdf_files = [f for f in os.listdir(reports_dir) if f.endswith('.pdf')]
562
+ reports = []
563
+
564
+ for pdf_file in pdf_files:
565
+ patient_name = pdf_file.replace('_report.pdf', '').replace('_', ' ')
566
+ pdf_path = os.path.join(reports_dir, pdf_file)
567
+ file_stats = os.stat(pdf_path)
568
+
569
+ reports.append({
570
+ "patient_name": patient_name,
571
+ "filename": pdf_file,
572
+ "created_date": datetime.fromtimestamp(file_stats.st_ctime).strftime('%Y-%m-%d %H:%M:%S'),
573
+ "size": file_stats.st_size
574
+ })
575
+
576
+ return {"reports": reports}
577
+
578
+ except Exception as e:
579
+ return {"error": f"Error listing reports: {str(e)}", "reports": []}
580
+
581
+ def generate_mrn(patient_name, date_of_birth):
582
+ """Generate a unique Medical Record Number (MRN) for a patient"""
583
+ try:
584
+ # Create a hash from patient name and DOB for consistency
585
+ patient_data = f"{patient_name.lower().strip()}{date_of_birth}".encode('utf-8')
586
+ hash_object = hashlib.md5(patient_data)
587
+ hash_hex = hash_object.hexdigest()
588
+
589
+ # Extract first 8 characters and convert to a medical record format
590
+ # Format: MV-YYYYMMDD-XXXX (MV = MediVision, YYYY = year, MM = month, DD = day, XXXX = hash)
591
+ current_date = datetime.now()
592
+ date_part = current_date.strftime("%Y%m%d")
593
+ hash_part = hash_hex[:4].upper()
594
+
595
+ mrn = f"MV-{date_part}-{hash_part}"
596
+ return mrn
597
+
598
+ except Exception as e:
599
+ # Fallback: Generate random MRN
600
+ current_date = datetime.now().strftime("%Y%m%d")
601
+ random_part = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
602
+ return f"MV-{current_date}-{random_part}"
603
+
604
+ def save_patient_record(patient_info, findings, mrn):
605
+ """Save patient record to database with MRN for future searches"""
606
+ try:
607
+ mongo_uri = os.getenv('MONGODB_URI',
608
+ "mongodb://localhost:27017/radiologyDB" # Fallback to local MongoDB
609
+ )
610
+ client = MongoClient(
611
+ mongo_uri,
612
+ tlsCAFile=certifi.where(),
613
+ serverSelectionTimeoutMS=5000
614
+ )
615
+
616
+ db = client.radiologyDB
617
+
618
+ # Create patient record
619
+ patient_record = {
620
+ "mrn": mrn,
621
+ "patient_name": patient_info.get("name", ""),
622
+ "date_of_birth": patient_info.get("date_of_birth", ""),
623
+ "gender": patient_info.get("gender", ""),
624
+ "referring_physician": patient_info.get("referring_physician", ""),
625
+ "date_of_study": patient_info.get("date_of_study", ""),
626
+ "findings": findings,
627
+ "report_date": datetime.now().isoformat(),
628
+ "status": "completed"
629
+ }
630
+
631
+ # Check if patient already exists (by MRN)
632
+ existing_patient = db.patient_reports.find_one({"mrn": mrn})
633
+
634
+ if existing_patient:
635
+ # Update existing record
636
+ result = db.patient_reports.update_one(
637
+ {"mrn": mrn},
638
+ {"$set": patient_record}
639
+ )
640
+ return {"action": "updated", "mrn": mrn, "success": True}
641
+ else:
642
+ # Insert new record
643
+ result = db.patient_reports.insert_one(patient_record)
644
+ return {"action": "created", "mrn": mrn, "success": True}
645
+
646
+ except Exception as e:
647
+ print(f"Error saving patient record: {str(e)}", file=sys.stderr)
648
+ return {"action": "error", "error": str(e), "success": False}
649
+ finally:
650
+ if 'client' in locals():
651
+ client.close()
652
+
653
+ def search_patient_by_mrn(mrn):
654
+ """Search for patient records by MRN"""
655
+ try:
656
+ mongo_uri = os.getenv('MONGODB_URI',
657
+ "mongodb://localhost:27017/radiologyDB" # Fallback to local MongoDB
658
+ )
659
+ client = MongoClient(
660
+ mongo_uri,
661
+ tlsCAFile=certifi.where(),
662
+ serverSelectionTimeoutMS=5000
663
+ )
664
+
665
+ db = client.radiologyDB
666
+
667
+ # Search for patient record
668
+ patient_record = db.patient_reports.find_one({"mrn": mrn})
669
+
670
+ if patient_record:
671
+ # Convert ObjectId to string for JSON serialization
672
+ patient_record["_id"] = str(patient_record["_id"])
673
+ return {"found": True, "patient": patient_record}
674
+ else:
675
+ return {"found": False, "message": f"No patient found with MRN: {mrn}"}
676
+
677
+ except Exception as e:
678
+ print(f"Error searching patient: {str(e)}", file=sys.stderr)
679
+ return {"found": False, "error": str(e)}
680
+ finally:
681
+ if 'client' in locals():
682
+ client.close()
683
+
684
+ def list_all_patients():
685
+ """List all patients in the database"""
686
+ try:
687
+ mongo_uri = os.getenv('MONGODB_URI',
688
+ "mongodb://localhost:27017/radiologyDB" # Fallback to local MongoDB
689
+ )
690
+ client = MongoClient(
691
+ mongo_uri,
692
+ tlsCAFile=certifi.where(),
693
+ serverSelectionTimeoutMS=5000
694
+ )
695
+
696
+ db = client.radiologyDB
697
+
698
+ # Get all patient records, sorted by report date (newest first)
699
+ patients = list(db.patient_reports.find().sort("report_date", -1))
700
+
701
+ # Convert ObjectId to string for JSON serialization
702
+ for patient in patients:
703
+ patient["_id"] = str(patient["_id"])
704
+
705
+ return {"success": True, "patients": patients, "count": len(patients)}
706
+
707
+ except Exception as e:
708
+ print(f"Error listing patients: {str(e)}", file=sys.stderr)
709
+ return {"success": False, "error": str(e), "patients": []}
710
+ finally:
711
+ if 'client' in locals():
712
+ client.close()
713
+
714
+ if __name__ == "__main__":
715
+ import argparse
716
+ parser = argparse.ArgumentParser()
717
+ parser.add_argument('--get-empty', action='store_true', help='Get empty reports')
718
+ parser.add_argument('--update', nargs=2, metavar=('ID', 'TEXT'), help='Update findings')
719
+ parser.add_argument('--serve', action='store_true', help='Run FastAPI server')
720
+
721
+ args = parser.parse_args()
722
+
723
+ if args.serve:
724
+ uvicorn.run("service:app", host="0.0.0.0", port=8001, reload=True)
725
+ sys.exit(0)
726
+
727
+ if args.get_empty:
728
+ get_reports_with_empty_findings() # ✅ DON'T capture or print the return value
729
+ sys.exit(0)
730
+
731
+ elif args.update:
732
+ report_id, findings_text = args.update
733
+ result = update_findings(report_id, findings_text)
734
+ print(json.dumps(result))
735
+ sys.exit(0)
736
+
737
+ else:
738
+ print(json.dumps({"error": "No valid command specified"}))
739
+ sys.exit(1)
src/streamlit_app.py CHANGED
@@ -1,40 +1,1456 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ import requests
3
+ import json
4
+ from PIL import Image
5
+ import pandas as pd
6
+ from datetime import datetime
7
+ import os
8
+ import time
9
+ import base64
10
+ import io
11
+ import re
12
+ import html
13
+
14
+ # Must be the first Streamlit command
15
+ st.set_page_config(
16
+ page_title="MediVision - Radiology Report System",
17
+ page_icon="🔬",
18
+ layout="wide",
19
+ initial_sidebar_state="expanded"
20
+ )
21
+
22
+ # Configuration
23
+ FASTAPI_BASE_URL = "http://localhost:8001"
24
+ UPLOAD_ENDPOINT = f"{FASTAPI_BASE_URL}/upload/"
25
+ REPORTS_ENDPOINT = f"{FASTAPI_BASE_URL}/reports"
26
+
27
+ # ============================================================================
28
+ # CUSTOM STYLING (Based on Hospital System)
29
+ # ============================================================================
30
+
31
+ st.markdown("""
32
+ <style>
33
+ /* Hide Streamlit default elements */
34
+ .stDeployButton {display:none;}
35
+ .stDecoration {display:none;}
36
+ #MainMenu {visibility: hidden;}
37
+ footer {visibility: hidden;}
38
+ header {visibility: hidden;}
39
+
40
+ /* Hide sidebar */
41
+ .css-1d391kg {display: none;}
42
+ .css-1rs6os {display: none;}
43
+ .st-emotion-cache-16idsys {display: none;}
44
+
45
+ /* Navigation Bar Styling */
46
+ .nav-bar {
47
+ background: linear-gradient(135deg, #1e3c72, #2a5298);
48
+ padding: 1rem 2rem;
49
+ border-radius: 15px;
50
+ margin-bottom: 2rem;
51
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
52
+ display: flex;
53
+ justify-content: space-between;
54
+ align-items: center;
55
+ flex-wrap: wrap;
56
+ }
57
+
58
+ .nav-brand {
59
+ color: white;
60
+ font-size: 1.8rem;
61
+ font-weight: 700;
62
+ text-shadow: 0 2px 4px rgba(0,0,0,0.3);
63
+ margin: 0;
64
+ }
65
+
66
+ .nav-subtitle {
67
+ color: rgba(255,255,255,0.9);
68
+ font-size: 0.9rem;
69
+ margin: 0;
70
+ font-weight: 300;
71
+ }
72
+
73
+ .nav-menu {
74
+ display: flex;
75
+ gap: 1rem;
76
+ align-items: center;
77
+ flex-wrap: wrap;
78
+ }
79
+
80
+ .nav-item {
81
+ background: rgba(255,255,255,0.1);
82
+ color: white;
83
+ padding: 0.6rem 1.2rem;
84
+ border-radius: 25px;
85
+ text-decoration: none;
86
+ font-weight: 500;
87
+ font-size: 0.9rem;
88
+ border: 2px solid transparent;
89
+ transition: all 0.3s ease;
90
+ cursor: pointer;
91
+ backdrop-filter: blur(10px);
92
+ }
93
+
94
+ .nav-item:hover {
95
+ background: rgba(255,255,255,0.2);
96
+ border-color: rgba(255,255,255,0.3);
97
+ transform: translateY(-2px);
98
+ color: white;
99
+ text-decoration: none;
100
+ }
101
+
102
+ .nav-item.active {
103
+ background: rgba(255,255,255,0.3);
104
+ border-color: rgba(255,255,255,0.5);
105
+ font-weight: 600;
106
+ }
107
+
108
+ @media (max-width: 768px) {
109
+ .nav-bar {
110
+ flex-direction: column;
111
+ text-align: center;
112
+ gap: 1rem;
113
+ }
114
+
115
+ .nav-menu {
116
+ justify-content: center;
117
+ width: 100%;
118
+ }
119
+
120
+ .nav-item {
121
+ font-size: 0.8rem;
122
+ padding: 0.5rem 1rem;
123
+ }
124
+ }
125
+
126
+ /* Background image */
127
+ .stApp {
128
+ background-image: linear-gradient(rgba(255,255,255,0.85), rgba(255,255,255,0.85)),
129
+ url("https://www.servereworldsystem.com/include/blog/1464/14640320083m.jpeg");
130
+ background-size: cover;
131
+ background-position: center;
132
+ background-attachment: fixed;
133
+ background-repeat: no-repeat;
134
+ }
135
+
136
+ /* Main container styling */
137
+ .main .block-container {
138
+ padding-top: 1rem;
139
+ padding-bottom: 1rem;
140
+ max-width: 1200px;
141
+ background: rgba(255, 255, 255, 0.95);
142
+ border-radius: 20px;
143
+ margin-top: 2rem;
144
+ margin-bottom: 2rem;
145
+ box-shadow: 0 8px 32px rgba(0,0,0,0.1);
146
+ backdrop-filter: blur(10px);
147
+ }
148
+
149
+ /* Header styling */
150
+ .medivision-header {
151
+ background: linear-gradient(135deg, #1e3c72, #2a5298);
152
+ color: white;
153
+ padding: 2rem;
154
+ border-radius: 15px;
155
+ text-align: center;
156
+ margin-bottom: 2rem;
157
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
158
+ }
159
+
160
+ .medivision-header h1 {
161
+ margin: 0;
162
+ font-size: 2.5rem;
163
+ font-weight: 700;
164
+ text-shadow: 0 2px 4px rgba(0,0,0,0.3);
165
+ color: white !important;
166
+ }
167
+
168
+ .medivision-header p {
169
+ margin: 0.5rem 0 0 0;
170
+ opacity: 0.95;
171
+ font-size: 1.2rem;
172
+ font-weight: 300;
173
+ }
174
+
175
+ /* Section headers */
176
+ .section-header {
177
+ background: linear-gradient(135deg, #f8f9fa, #e9ecef);
178
+ color: #2c5aa0;
179
+ padding: 1rem 1.5rem;
180
+ border-radius: 10px;
181
+ border-left: 5px solid #4a90e2;
182
+ margin: 2rem 0 1rem 0;
183
+ font-size: 1.5rem;
184
+ font-weight: 600;
185
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
186
+ }
187
+
188
+ /* Cards and containers */
189
+ .info-card {
190
+ background: white;
191
+ border-radius: 15px;
192
+ padding: 2rem;
193
+ margin: 1.5rem 0;
194
+ box-shadow: 0 4px 20px rgba(0,0,0,0.1);
195
+ border: 1px solid #e0e6ed;
196
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
197
+ }
198
+
199
+ .info-card:hover {
200
+ transform: translateY(-2px);
201
+ box-shadow: 0 8px 30px rgba(0,0,0,0.15);
202
+ }
203
+
204
+ .upload-area {
205
+ background: linear-gradient(135deg, #f8f9fa, #ffffff);
206
+ border: 2px dashed #2c5aa0;
207
+ border-radius: 15px;
208
+ padding: 2rem;
209
+ text-align: center;
210
+ margin: 1rem 0;
211
+ transition: all 0.3s ease;
212
+ }
213
+
214
+ .upload-area:hover {
215
+ border-color: #4a90e2;
216
+ background: linear-gradient(135deg, #ffffff, #f8f9fa);
217
+ }
218
+
219
+ /* Buttons */
220
+ .stButton > button {
221
+ background: linear-gradient(135deg, #2c5aa0, #4a90e2);
222
+ color: white;
223
+ border: none;
224
+ border-radius: 25px;
225
+ padding: 0.75rem 2rem;
226
+ font-weight: 600;
227
+ font-size: 1rem;
228
+ transition: all 0.3s ease;
229
+ box-shadow: 0 4px 15px rgba(44, 90, 160, 0.3);
230
+ }
231
+
232
+ .stButton > button:hover {
233
+ background: linear-gradient(135deg, #4a90e2, #6ab7ff);
234
+ transform: translateY(-2px);
235
+ box-shadow: 0 6px 20px rgba(44, 90, 160, 0.4);
236
+ }
237
+
238
+ /* Success/Error messages */
239
+ .stSuccess {
240
+ background: linear-gradient(135deg, #4caf50, #66bb6a);
241
+ color: white;
242
+ border-radius: 10px;
243
+ padding: 1rem;
244
+ border: none;
245
+ }
246
+
247
+ .stError {
248
+ background: linear-gradient(135deg, #f44336, #ef5350);
249
+ color: white;
250
+ border-radius: 10px;
251
+ padding: 1rem;
252
+ border: none;
253
+ }
254
+
255
+ .stWarning {
256
+ background: linear-gradient(135deg, #ff9800, #ffb74d);
257
+ color: white;
258
+ border-radius: 10px;
259
+ padding: 1rem;
260
+ border: none;
261
+ }
262
+
263
+ .stInfo {
264
+ background: linear-gradient(135deg, #2196f3, #42a5f5);
265
+ color: white;
266
+ border-radius: 10px;
267
+ padding: 1rem;
268
+ border: none;
269
+ }
270
+
271
+ /* Form inputs */
272
+ .stTextInput > div > div > input {
273
+ border-radius: 10px;
274
+ border: 2px solid #e0e6ed;
275
+ padding: 0.75rem;
276
+ font-size: 1rem;
277
+ transition: border-color 0.3s ease;
278
+ }
279
+
280
+ .stTextInput > div > div > input:focus {
281
+ border-color: #2c5aa0;
282
+ box-shadow: 0 0 0 3px rgba(44, 90, 160, 0.1);
283
+ }
284
+
285
+ .stSelectbox > div > div > select {
286
+ border-radius: 10px;
287
+ border: 2px solid #e0e6ed;
288
+ padding: 0.75rem;
289
+ font-size: 1rem;
290
+ }
291
+
292
+ /* File uploader */
293
+ .stFileUploader > div {
294
+ border-radius: 15px;
295
+ border: 2px solid #e0e6ed;
296
+ background: linear-gradient(135deg, #f8f9fa, #ffffff);
297
+ padding: 1rem;
298
+ }
299
+
300
+ .stFileUploader > div:hover {
301
+ border-color: #2c5aa0;
302
+ }
303
+
304
+ /* Sidebar */
305
+ .css-1d391kg {
306
+ background: linear-gradient(180deg, #2c5aa0, #4a90e2);
307
+ }
308
+
309
+ .css-1d391kg .css-17eq0hr {
310
+ color: white;
311
+ }
312
+
313
+ /* Progress bars */
314
+ .stProgress > div > div > div > div {
315
+ background: linear-gradient(90deg, #2c5aa0, #4a90e2);
316
+ }
317
+
318
+ /* Metrics */
319
+ .metric-card {
320
+ background: white;
321
+ border-radius: 15px;
322
+ padding: 1.5rem;
323
+ text-align: center;
324
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
325
+ border: 1px solid #e0e6ed;
326
+ margin: 1rem 0;
327
+ }
328
+
329
+ .metric-value {
330
+ font-size: 2rem;
331
+ font-weight: 700;
332
+ color: #2c5aa0;
333
+ margin: 0.5rem 0;
334
+ }
335
+
336
+ .metric-label {
337
+ font-size: 1rem;
338
+ color: #666;
339
+ font-weight: 500;
340
+ }
341
+
342
+ /* Status indicators */
343
+ .status-online {
344
+ display: inline-block;
345
+ width: 12px;
346
+ height: 12px;
347
+ background: #4caf50;
348
+ border-radius: 50%;
349
+ margin-left: 8px;
350
+ animation: pulse 2s infinite;
351
+ }
352
+
353
+ @keyframes pulse {
354
+ 0% { opacity: 0.4; }
355
+ 50% { opacity: 0.8; }
356
+ 100% { opacity: 0.4; }
357
+ }
358
+
359
+ /* Report cards */
360
+ .report-card {
361
+ background: white;
362
+ border-radius: 15px;
363
+ padding: 1.5rem;
364
+ margin: 1rem 0;
365
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
366
+ border-left: 5px solid #4a90e2;
367
+ transition: all 0.3s ease;
368
+ }
369
+
370
+ .report-card:hover {
371
+ transform: translateX(5px);
372
+ box-shadow: 0 6px 25px rgba(0,0,0,0.15);
373
+ }
374
+
375
+ /* Data tables */
376
+ .dataframe {
377
+ border-radius: 10px;
378
+ overflow: hidden;
379
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
380
+ }
381
+
382
+ /* Mobile responsive */
383
+ @media (max-width: 768px) {
384
+ .medivision-header h1 {
385
+ font-size: 2rem;
386
+ }
387
+
388
+ .medivision-header p {
389
+ font-size: 1rem;
390
+ }
391
+
392
+ .main .block-container {
393
+ padding: 1rem 0.5rem;
394
+ }
395
+
396
+ .info-card {
397
+ padding: 1rem;
398
+ }
399
+ }
400
+
401
+ /* RTL support for Arabic */
402
+ .rtl {
403
+ direction: rtl;
404
+ text-align: right;
405
+ }
406
+
407
+ /* Loading animations */
408
+ .loading-spinner {
409
+ display: inline-block;
410
+ width: 20px;
411
+ height: 20px;
412
+ border: 3px solid rgba(255,255,255,.3);
413
+ border-radius: 50%;
414
+ border-top-color: #fff;
415
+ animation: spin 1s ease-in-out infinite;
416
+ }
417
+
418
+ @keyframes spin {
419
+ to { transform: rotate(360deg); }
420
+ }
421
+
422
+ /* FIXED THINKING INDICATOR - Larger font size and consistent spacing */
423
+ .thinking-indicator {
424
+ display: flex;
425
+ align-items: center;
426
+ margin: 15px 0;
427
+ padding: 15px 20px;
428
+ border-radius: 10px;
429
+ background: #f8f9fa;
430
+ color: #666;
431
+ font-style: italic;
432
+ font-size: 1.1em; /* Increased from 0.9em */
433
+ line-height: 1.5;
434
+ font-weight: 500;
435
+ border-left: 4px solid #2c5aa0;
436
+ }
437
+
438
+ .thinking-dot {
439
+ width: 10px; /* Increased from 8px */
440
+ height: 10px;
441
+ background: #2c5aa0; /* Changed from #999 to match theme */
442
+ border-radius: 50%;
443
+ margin: 0 3px; /* Increased spacing */
444
+ animation: pulse 1.5s infinite;
445
+ }
446
+
447
+ .thinking-dot:nth-child(2) {
448
+ animation-delay: 0.2s;
449
+ }
450
+
451
+ .thinking-dot:nth-child(3) {
452
+ animation-delay: 0.4s;
453
+ }
454
+
455
+ /* FIXED AI THINKING - Larger font and better spacing */
456
+ .ai-thinking {
457
+ color: #555; /* Darker for better readability */
458
+ font-style: italic;
459
+ background: #f8f9fa;
460
+ padding: 20px 24px; /* Increased padding */
461
+ border-radius: 10px;
462
+ margin: 15px 0; /* Consistent spacing */
463
+ border-left: 4px solid #2c5aa0;
464
+ font-size: 1.0em; /* Increased from 0.85em */
465
+ line-height: 1.6; /* Increased line height */
466
+ max-height: 200px; /* Slightly taller */
467
+ overflow-y: auto;
468
+ font-weight: 400;
469
+ }
470
+
471
+ .ai-thinking pre {
472
+ margin: 8px 0; /* Added margin */
473
+ padding: 0;
474
+ white-space: pre-wrap;
475
+ font-size: 1.0em; /* Increased from 0.85em */
476
+ font-family: 'Consolas', 'Monaco', monospace;
477
+ }
478
+
479
+ .ai-thinking p {
480
+ margin: 10px 0; /* Increased from 4px */
481
+ font-size: 1.0em; /* Increased from 0.85em */
482
+ }
483
+
484
+ .ai-thinking ul, .ai-thinking ol {
485
+ margin: 10px 0; /* Increased from 4px */
486
+ padding-left: 20px; /* Slightly increased */
487
+ }
488
+
489
+ .ai-thinking li {
490
+ margin: 6px 0; /* Increased from 2px */
491
+ font-size: 1.0em; /* Increased from 0.85em */
492
+ }
493
+
494
+ .stream-text {
495
+ white-space: pre-wrap;
496
+ font-family: 'Segoe UI', Arial, sans-serif; /* Better font */
497
+ line-height: 1.5; /* Increased */
498
+ font-size: 1.0em; /* Added explicit size */
499
+ }
500
+
501
+ /* FIXED AI RESPONSE - Better rendering */
502
+ .ai-response {
503
+ color: #333;
504
+ background: white;
505
+ padding: 24px; /* Increased padding */
506
+ border-radius: 12px;
507
+ margin: 20px 0; /* Increased margin */
508
+ border-left: 4px solid #2c5aa0;
509
+ box-shadow: 0 4px 20px rgba(0,0,0,0.12);
510
+ font-size: 1.0em; /* Increased from 0.95em */
511
+ line-height: 1.7; /* Increased line height */
512
+ font-family: 'Segoe UI', -apple-system, sans-serif;
513
+ }
514
+
515
+ /* FIXED REPORT HEADER - Better rendering */
516
+ .report-header {
517
+ border-bottom: 2px solid #2c5aa0;
518
+ margin-bottom: 25px; /* Increased */
519
+ padding-bottom: 20px;
520
+ background: linear-gradient(135deg, #f8f9fa, #ffffff);
521
+ padding: 25px; /* Increased */
522
+ border-radius: 10px;
523
+ margin: -24px -24px 25px -24px; /* Adjusted for new padding */
524
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
525
+ }
526
+
527
+ .report-header h2 {
528
+ color: #2c5aa0;
529
+ margin: 0 0 20px 0; /* Increased margin */
530
+ font-size: 1.5em; /* Increased */
531
+ font-weight: 700;
532
+ text-align: center;
533
+ display: flex;
534
+ align-items: center;
535
+ justify-content: center;
536
+ gap: 12px; /* Increased gap */
537
+ }
538
+
539
+ .report-header .hospital-info {
540
+ text-align: center;
541
+ margin-bottom: 20px; /* Increased */
542
+ padding-bottom: 15px; /* Increased */
543
+ border-bottom: 1px solid #e0e0e0;
544
+ }
545
+
546
+ .report-header .hospital-name {
547
+ font-size: 1.2em; /* Increased */
548
+ font-weight: 600;
549
+ color: #2c5aa0;
550
+ margin-bottom: 8px; /* Increased */
551
+ }
552
+
553
+ .report-header .patient-details {
554
+ display: grid;
555
+ grid-template-columns: 1fr 1fr;
556
+ gap: 12px; /* Increased */
557
+ margin-top: 20px; /* Increased */
558
+ }
559
+
560
+ .report-header .patient-detail-item {
561
+ background: rgba(44, 90, 160, 0.05);
562
+ padding: 12px 16px; /* Increased */
563
+ border-radius: 8px;
564
+ font-size: 0.95em; /* Slightly increased */
565
+ border: 1px solid rgba(44, 90, 160, 0.1);
566
+ }
567
+
568
+ .report-header .detail-label {
569
+ font-weight: 600;
570
+ color: #2c5aa0;
571
+ display: inline-block;
572
+ min-width: 80px;
573
+ }
574
+
575
+ .report-header .detail-value {
576
+ color: #333;
577
+ margin-left: 8px; /* Increased */
578
+ }
579
+
580
+ /* FIXED REPORT CONTENT - Better rendering */
581
+ .report-content {
582
+ margin-top: 25px; /* Increased */
583
+ white-space: pre-wrap;
584
+ font-family: 'Segoe UI', -apple-system, sans-serif;
585
+ line-height: 1.7; /* Increased */
586
+ font-size: 1.0em; /* Added explicit size */
587
+ color: #333;
588
+ }
589
+
590
+ /* Styling for bullet points in report content */
591
+ .report-content ul {
592
+ margin: 15px 0;
593
+ padding-left: 25px;
594
+ list-style-type: disc;
595
+ }
596
+
597
+ .report-content ol {
598
+ margin: 15px 0;
599
+ padding-left: 25px;
600
+ list-style-type: decimal;
601
+ }
602
+
603
+ .report-content li {
604
+ margin: 8px 0;
605
+ color: #333;
606
+ line-height: 1.6;
607
+ padding-left: 5px;
608
+ }
609
+
610
+ .report-content ul li::marker {
611
+ color: #2c5aa0;
612
+ font-size: 1.1em;
613
+ }
614
+
615
+ .report-content ol li::marker {
616
+ color: #2c5aa0;
617
+ font-weight: 600;
618
+ }
619
+
620
+ /* Section headers within report content */
621
+ .report-content p strong {
622
+ color: #333;
623
+ font-size: 1.0em;
624
+ font-weight: 400;
625
+ display: block;
626
+ margin: 15px 0 8px 0;
627
+ padding-bottom: 0px;
628
+ border-bottom: none;
629
+ }
630
+
631
+ .report-content p {
632
+ margin: 10px 0;
633
+ line-height: 1.6;
634
+ }
635
+
636
+ .report-section {
637
+ margin: 25px 0; /* Increased */
638
+ }
639
+
640
+ .report-section h3 {
641
+ color: #2c5aa0;
642
+ font-size: 1.2em; /* Increased */
643
+ margin: 20px 0 12px 0; /* Increased */
644
+ font-weight: 600;
645
+ border-bottom: 2px solid #e0e0e0; /* Thicker border */
646
+ padding-bottom: 8px; /* Increased */
647
+ }
648
+
649
+ .report-section h4 {
650
+ color: #2c5aa0;
651
+ font-size: 1.1em; /* Increased */
652
+ margin: 15px 0 10px 0; /* Increased */
653
+ font-weight: 600;
654
+ }
655
+
656
+ .report-section p {
657
+ margin: 12px 0; /* Increased */
658
+ color: #333;
659
+ text-align: justify;
660
+ line-height: 1.7;
661
+ }
662
+
663
+ .report-section ul, .report-section ol {
664
+ margin: 15px 0; /* Increased */
665
+ padding-left: 30px; /* Increased */
666
+ }
667
+
668
+ .report-section li {
669
+ margin: 8px 0; /* Increased */
670
+ color: #333;
671
+ line-height: 1.6;
672
+ }
673
+
674
+ /* FIXED REPORT FOOTER */
675
+ .report-footer {
676
+ margin-top: 30px; /* Increased */
677
+ padding-top: 20px; /* Increased */
678
+ border-top: 2px solid #eee; /* Thicker border */
679
+ font-size: 0.95em; /* Slightly increased */
680
+ color: #666;
681
+ text-align: center;
682
+ background: #f9f9f9;
683
+ padding: 20px; /* Increased */
684
+ border-radius: 8px;
685
+ margin-left: -24px; /* Adjusted */
686
+ margin-right: -24px;
687
+ margin-bottom: -24px;
688
+ }
689
+ </style>
690
+ """, unsafe_allow_html=True)
691
+ # ============================================================================
692
+ def main():
693
+ # Modern header with MediVision branding
694
+ st.markdown("""
695
+ <div class="medivision-header">
696
+ <h1>🔬 MediVision</h1>
697
+ <p>Advanced Radiology Report System - Powered by AI Technology</p>
698
+ </div>
699
+ """, unsafe_allow_html=True)
700
+
701
+ # Sidebar navigation
702
+ st.sidebar.markdown("## 📋 Navigation")
703
+ page = st.sidebar.selectbox("Choose Page", [
704
+ "📤 Upload & Generate Report",
705
+ "📊 View Reports",
706
+ "⚙️ System Status"
707
+ ])
708
+
709
+ if "Upload" in page:
710
+ upload_page()
711
+ elif "View Reports" in page:
712
+ view_reports_page()
713
+ elif "System Status" in page:
714
+ system_status_page()
715
+
716
+ def upload_page():
717
+ st.markdown('<div class="section-header">📤 Upload Medical Images & Generate Reports</div>', unsafe_allow_html=True)
718
+
719
+ # Create two columns for better layout
720
+ col1, col2 = st.columns([2, 1])
721
+
722
+ with col1:
723
+ st.markdown('<div class="info-card">', unsafe_allow_html=True)
724
+ st.markdown("### 👤 Patient Information")
725
+
726
+ # Patient form with modern styling
727
+ with st.form("patient_form"):
728
+ name = st.text_input("Patient Name *", placeholder="Enter full name")
729
+ date_of_birth = st.date_input("Date of Birth *")
730
+ gender = st.selectbox("Gender *", ["Male", "Female", "Other"])
731
+
732
+ # MRN field (auto-generated, but allow manual override)
733
+ col1, col2 = st.columns([3, 1])
734
+ with col1:
735
+ medical_record_number = st.text_input(
736
+ "Medical Record Number (MRN)",
737
+ placeholder="Auto-generated if left empty",
738
+ help="Leave empty to auto-generate MRN based on patient name and DOB"
739
+ )
740
+ with col2:
741
+ st.markdown("<br>", unsafe_allow_html=True) # Add spacing
742
+ if st.form_submit_button("🔢 Generate MRN", use_container_width=True):
743
+ if name and date_of_birth:
744
+ try:
745
+ response = requests.get(
746
+ f"{FASTAPI_BASE_URL}/generate-mrn/",
747
+ params={"patient_name": name, "date_of_birth": str(date_of_birth)},
748
+ timeout=10
749
+ )
750
+ if response.status_code == 200:
751
+ data = response.json()
752
+ if data.get("success"):
753
+ st.session_state.generated_mrn = data.get("mrn")
754
+ st.success(f"✅ Generated MRN: {data.get('mrn')}")
755
+ st.rerun()
756
+ except Exception as e:
757
+ st.error(f"❌ Error generating MRN: {str(e)}")
758
+ else:
759
+ st.error("⚠️ Please enter patient name and date of birth first")
760
+
761
+ # Show generated MRN if available
762
+ if hasattr(st.session_state, 'generated_mrn') and st.session_state.generated_mrn:
763
+ medical_record_number = st.text_input(
764
+ "Generated MRN",
765
+ value=st.session_state.generated_mrn,
766
+ disabled=True,
767
+ help="Auto-generated MRN - you can edit the field above to override"
768
+ )
769
+
770
+ referring_physician = st.text_input("Referring Physician *", placeholder="Enter physician name")
771
+ date_of_study = st.date_input("Date of Study *", value=datetime.now().date())
772
+
773
+ st.markdown("### 🖼️ Medical Image")
774
+ st.markdown('<div class="upload-area">', unsafe_allow_html=True)
775
+ uploaded_file = st.file_uploader(
776
+ "Choose medical image",
777
+ type=['png', 'jpg', 'jpeg', 'dcm'],
778
+ help="Upload X-ray, CT, MRI, or other medical images (Max 10MB)"
779
+ )
780
+ st.markdown('</div>', unsafe_allow_html=True)
781
+
782
+ # Display uploaded image with modern styling
783
+ if uploaded_file is not None:
784
+ try:
785
+ # Validate file size
786
+ file_size = uploaded_file.size
787
+ if file_size > 10 * 1024 * 1024: # 10MB
788
+ st.error("⚠️ File size too large. Please upload an image smaller than 10MB.")
789
+ else:
790
+ # Display file info
791
+ st.info(f"📁 File: {uploaded_file.name} ({file_size/1024:.1f} KB)")
792
+
793
+ # Display image
794
+ image = Image.open(uploaded_file)
795
+ st.image(image, caption="Uploaded Medical Image", use_column_width=True)
796
+
797
+ # Reset file pointer for later use
798
+ uploaded_file.seek(0)
799
+
800
+ except Exception as e:
801
+ st.error(f"❌ Error processing image: {str(e)}")
802
+ st.info("💡 Please ensure the file is a valid image format (PNG, JPG, JPEG, DCM)")
803
+
804
+ submit_button = st.form_submit_button("🔍 Generate Report", use_container_width=True)
805
+
806
+ if submit_button:
807
+ # Get the MRN (either generated or manually entered)
808
+ final_mrn = medical_record_number
809
+ if hasattr(st.session_state, 'generated_mrn') and st.session_state.generated_mrn and not medical_record_number:
810
+ final_mrn = st.session_state.generated_mrn
811
+
812
+ # Validation (MRN is now optional as it can be auto-generated)
813
+ if not all([name, date_of_birth, gender, referring_physician, uploaded_file]):
814
+ st.error("⚠️ Please fill all required fields and upload an image")
815
+ else:
816
+ generate_report(name, str(date_of_birth), gender, final_mrn,
817
+ referring_physician, str(date_of_study), uploaded_file)
818
+
819
+ st.markdown('</div>', unsafe_allow_html=True)
820
+
821
+ def generate_report(name, date_of_birth, gender, medical_record_number,
822
+ referring_physician, date_of_study, uploaded_file):
823
+ """Generate report by sending data to backend and displaying the result"""
824
+ try:
825
+ if uploaded_file is not None:
826
+ with st.spinner("Processing image..."):
827
+ # Validate file before sending
828
+ if uploaded_file.size > 10 * 1024 * 1024: # 10MB limit
829
+ st.error("⚠️ File size too large. Please upload an image smaller than 10MB.")
830
+ return
831
+
832
+ # Reset file pointer to beginning
833
+ uploaded_file.seek(0)
834
+
835
+ # First get AI analysis from backend
836
+ files = {"file": (uploaded_file.name, uploaded_file, uploaded_file.type)}
837
+
838
+ try:
839
+ analyze_response = requests.post(
840
+ f"{FASTAPI_BASE_URL}/analyze/",
841
+ files=files,
842
+ timeout=60 # 60 second timeout
843
+ )
844
+
845
+ if analyze_response.status_code != 200:
846
+ st.error(f"❌ Backend Error (Status {analyze_response.status_code}): {analyze_response.text}")
847
+ return
848
+
849
+ response_data = analyze_response.json()
850
+
851
+ # Check for API errors
852
+ if "error" in response_data:
853
+ st.error(f"❌ Analysis Error: {response_data['error']}")
854
+ return
855
+
856
+ findings = response_data.get("findings", "No findings returned.")
857
+ status = response_data.get("status", "unknown")
858
+
859
+ if status == "error":
860
+ st.error(f"❌ Analysis failed: {findings}")
861
+ return
862
+
863
+ if not findings or findings.strip() == "":
864
+ st.warning("⚠️ No findings were generated. Please try again with a different image.")
865
+ return
866
+
867
+ except requests.exceptions.Timeout:
868
+ st.error("❌ Request timed out. The analysis is taking too long. Please try again.")
869
+ return
870
+ except requests.exceptions.ConnectionError:
871
+ st.error("❌ Cannot connect to backend service. Please ensure the backend is running on http://localhost:8001")
872
+ return
873
+ except requests.exceptions.RequestException as e:
874
+ st.error(f"❌ Request failed: {str(e)}")
875
+ return
876
+
877
+ # Patient info for the report
878
+ patient_info = {
879
+ "name": name,
880
+ "medical_record_number": medical_record_number,
881
+ "referring_physician": referring_physician,
882
+ "date_of_study": date_of_study
883
+ }
884
+
885
+ # Show findings and PDF link
886
+ st.success("✅ Analysis completed successfully!")
887
+
888
+ # Separate thinking process from findings
889
+ thinking_text = ""
890
+ report_text = findings
891
+
892
+ # Debug: Show raw findings
893
+ # st.write("DEBUG - Raw findings:", findings[:200] + "..." if len(findings) > 200 else findings)
894
+
895
+ # Check if findings contains "thinking:" section
896
+ if "thinking:" in findings.lower():
897
+ parts = findings.split("answer:", 1)
898
+ if len(parts) == 2:
899
+ # Extract thinking part (remove "thinking:" prefix)
900
+ thinking_part = parts[0].strip()
901
+ if thinking_part.lower().startswith("thinking:"):
902
+ thinking_text = thinking_part[9:].strip() # Remove "thinking:" prefix
903
+ else:
904
+ thinking_text = thinking_part
905
+
906
+ # Extract answer part
907
+ report_text = parts[1].strip()
908
+
909
+ # Debug: Show extracted parts
910
+ # st.write("DEBUG - Thinking text:", thinking_text[:100] + "..." if len(thinking_text) > 100 else thinking_text)
911
+ # st.write("DEBUG - Report text:", report_text[:100] + "..." if len(report_text) > 100 else report_text)
912
+ else:
913
+ # If no "answer:" found, use the whole thinking section
914
+ thinking_text = findings
915
+ report_text = "Analysis completed."
916
+
917
+ stream_response(thinking_text, report_text, patient_info)
918
+
919
+ # Generate PDF report
920
+ with st.spinner("Generating PDF report..."):
921
+ try:
922
+ pdf_response = requests.post(
923
+ f"{FASTAPI_BASE_URL}/generate-report/",
924
+ params={
925
+ "patient_name": name,
926
+ "medical_record_number": medical_record_number,
927
+ "date_of_birth": date_of_birth,
928
+ "gender": gender,
929
+ "referring_physician": referring_physician,
930
+ "date_of_study": date_of_study,
931
+ "findings": findings
932
+ },
933
+ timeout=30
934
+ )
935
+
936
+ if pdf_response.status_code == 200:
937
+ pdf_data = pdf_response.json()
938
+ if pdf_data.get("success"):
939
+ pdf_url = f"{FASTAPI_BASE_URL}/reports/{name}/pdf"
940
+ st.markdown(f'''
941
+ <div style="margin-top: 1rem; padding: 1rem; background: linear-gradient(135deg, #4caf50, #66bb6a); border-radius: 10px;">
942
+ <p style="color: white; margin: 0; font-weight: 600;">✅ PDF Report Generated Successfully!</p>
943
+ <a href="{pdf_url}" target="_blank" style="display:inline-block;margin-top:0.5em;padding:0.5em 1.5em;background:rgba(255,255,255,0.2);color:white;border-radius:20px;text-decoration:none;font-weight:600;border: 2px solid rgba(255,255,255,0.3);">
944
+ 📄 Download PDF Report
945
+ </a>
946
+ </div>
947
+ ''', unsafe_allow_html=True)
948
+ else:
949
+ st.error(f"❌ Failed to generate PDF: {pdf_data.get('error', 'Unknown error')}")
950
+ else:
951
+ st.error(f"❌ PDF generation failed with status {pdf_response.status_code}")
952
+
953
+ except requests.exceptions.Timeout:
954
+ st.error("❌ PDF generation timed out. Please try again.")
955
+ except Exception as e:
956
+ st.error(f"❌ Error generating PDF: {str(e)}")
957
+
958
+ except Exception as e:
959
+ st.error(f"❌ Unexpected error generating report: {str(e)}")
960
+ st.info("💡 Please check that:")
961
+ st.info(" • The backend service is running (http://localhost:8001)")
962
+ st.info(" • The uploaded file is a valid image")
963
+ st.info(" • Your internet connection is stable")
964
+
965
+ def view_reports_page():
966
+ st.markdown('<div class="section-header">📊 View Patient Reports - Search by MRN</div>', unsafe_allow_html=True)
967
+
968
+ # Search section with modern styling
969
+ st.markdown('<div class="info-card">', unsafe_allow_html=True)
970
+ col1, col2 = st.columns([3, 1])
971
+
972
+ with col1:
973
+ patient_mrn = st.text_input("🔍 Search by MRN",
974
+ placeholder="Enter Medical Record Number (e.g., MV-20250618-A1B2)")
975
+
976
+ with col2:
977
+ st.markdown("<br>", unsafe_allow_html=True) # Add spacing
978
+ search_button = st.button("🔍 Search", use_container_width=True)
979
+
980
+ st.markdown('</div>', unsafe_allow_html=True)
981
+
982
+ # Action buttons
983
+ col1, col2 = st.columns(2)
984
+ with col1:
985
+ if search_button and patient_mrn:
986
+ search_reports_by_mrn(patient_mrn)
987
+
988
+ with col2:
989
+ if st.button("📋 Show All Patients", use_container_width=True):
990
+ show_all_patients()
991
+
992
+ def search_reports_by_mrn(mrn):
993
+ """Search for reports by MRN with modern styling"""
994
+
995
+ with st.spinner(f"🔍 Searching for patient with MRN: {mrn}..."):
996
+ try:
997
+ response = requests.get(f"{FASTAPI_BASE_URL}/patient/{mrn}", timeout=10)
998
+
999
+ if response.status_code == 200:
1000
+ patient_data = response.json()
1001
+
1002
+ st.markdown(f"""
1003
+ <div class="info-card" style="background: linear-gradient(135deg, #4caf50, #66bb6a); color: white;">
1004
+ <h4>✅ Patient Found: {patient_data.get('patient_name', 'N/A')}</h4>
1005
+ <p><b>MRN:</b> {patient_data.get('mrn', 'N/A')}</p>
1006
+ </div>
1007
+ """, unsafe_allow_html=True)
1008
+
1009
+ # Display patient details
1010
+ display_patient_details(patient_data)
1011
+
1012
+ elif response.status_code == 404:
1013
+ st.markdown(f"""
1014
+ <div class="info-card" style="background: linear-gradient(135deg, #ff9800, #ffb74d); color: white;">
1015
+ <h4>⚠️ No Patient Found</h4>
1016
+ <p>No patient record found with MRN: <b>{mrn}</b></p>
1017
+ <p>Please check the MRN and try again.</p>
1018
+ </div>
1019
+ """, unsafe_allow_html=True)
1020
+ else:
1021
+ st.error(f"❌ Error searching patient: HTTP {response.status_code}")
1022
+
1023
+ except requests.exceptions.ConnectionError:
1024
+ st.error("❌ Cannot connect to the backend server. Please make sure the FastAPI server is running.")
1025
+ except requests.exceptions.Timeout:
1026
+ st.error("❌ Search request timed out. Please try again.")
1027
+ except Exception as e:
1028
+ st.error(f"❌ An error occurred: {str(e)}")
1029
+
1030
+ def show_all_patients():
1031
+ """Show all patients in the system"""
1032
+
1033
+ with st.spinner("📋 Loading all patient records..."):
1034
+ try:
1035
+ response = requests.get(f"{FASTAPI_BASE_URL}/patients", timeout=15)
1036
+
1037
+ if response.status_code == 200:
1038
+ data = response.json()
1039
+ patients = data.get('patients', [])
1040
+
1041
+ if patients:
1042
+ st.markdown(f"""
1043
+ <div class="info-card" style="background: linear-gradient(135deg, #2196f3, #42a5f5); color: white;">
1044
+ <h4>📊 Found {len(patients)} Patient Records</h4>
1045
+ </div>
1046
+ """, unsafe_allow_html=True)
1047
+
1048
+ # Display patients in a table format
1049
+ for patient in patients:
1050
+ display_patient_summary(patient)
1051
+
1052
+ else:
1053
+ st.info("📋 No patient records found in the system yet.")
1054
+
1055
+ else:
1056
+ st.error(f"❌ Error retrieving patients: {response.text}")
1057
+
1058
+ except requests.exceptions.ConnectionError:
1059
+ st.error("❌ Cannot connect to the backend server. Please make sure the FastAPI server is running.")
1060
+ except Exception as e:
1061
+ st.error(f"❌ An error occurred: {str(e)}")
1062
+
1063
+ def display_patient_details(patient_data):
1064
+ """Display detailed patient information"""
1065
+ st.markdown('<div class="info-card">', unsafe_allow_html=True)
1066
+ st.markdown("### 👤 Patient Details")
1067
+
1068
+ col1, col2 = st.columns(2)
1069
+
1070
+ with col1:
1071
+ st.markdown(f"**Name:** {patient_data.get('patient_name', 'N/A')}")
1072
+ st.markdown(f"**MRN:** {patient_data.get('mrn', 'N/A')}")
1073
+ st.markdown(f"**Date of Birth:** {patient_data.get('date_of_birth', 'N/A')}")
1074
+ st.markdown(f"**Gender:** {patient_data.get('gender', 'N/A')}")
1075
+
1076
+ with col2:
1077
+ st.markdown(f"**Referring Physician:** Dr. {patient_data.get('referring_physician', 'N/A')}")
1078
+ st.markdown(f"**Study Date:** {patient_data.get('date_of_study', 'N/A')}")
1079
+ st.markdown(f"**Report Date:** {patient_data.get('report_date', 'N/A')}")
1080
+ st.markdown(f"**Status:** {patient_data.get('status', 'N/A').title()}")
1081
+
1082
+ # Display findings
1083
+ if patient_data.get('findings'):
1084
+ st.markdown("### 🔍 Findings")
1085
+ findings = patient_data.get('findings', '')
1086
+
1087
+ # Process findings to show only the answer part
1088
+ if "answer:" in findings.lower():
1089
+ parts = findings.split("answer:", 1)
1090
+ if len(parts) == 2:
1091
+ findings_display = parts[1].strip()
1092
+ else:
1093
+ findings_display = findings
1094
+ else:
1095
+ findings_display = findings
1096
+
1097
+ st.markdown(findings_display)
1098
+
1099
+ # PDF download link
1100
+ patient_name = patient_data.get('patient_name', 'Unknown')
1101
+ pdf_url = f"{FASTAPI_BASE_URL}/reports/{patient_name}/pdf"
1102
+ st.markdown(f"""
1103
+ <a href="{pdf_url}" target="_blank" style="display:inline-block;margin-top:1em;padding:0.5em 1.5em;background:linear-gradient(135deg,#2c5aa0,#4a90e2);color:white;border-radius:20px;text-decoration:none;font-weight:600;">
1104
+ 📄 Download PDF Report
1105
+ </a>
1106
+ """, unsafe_allow_html=True)
1107
+
1108
+ st.markdown('</div>', unsafe_allow_html=True)
1109
+
1110
+ def display_patient_summary(patient_data):
1111
+ """Display patient summary in a card format"""
1112
+ st.markdown('<div class="info-card">', unsafe_allow_html=True)
1113
+
1114
+ col1, col2, col3 = st.columns([2, 2, 1])
1115
+
1116
+ with col1:
1117
+ st.markdown(f"**{patient_data.get('patient_name', 'N/A')}**")
1118
+ st.markdown(f"MRN: {patient_data.get('mrn', 'N/A')}")
1119
+
1120
+ with col2:
1121
+ st.markdown(f"Study: {patient_data.get('date_of_study', 'N/A')}")
1122
+ st.markdown(f"Physician: Dr. {patient_data.get('referring_physician', 'N/A')}")
1123
+
1124
+ with col3:
1125
+ # Search button for this specific patient
1126
+ if st.button(f"🔍 View", key=f"view_{patient_data.get('mrn', 'unknown')}"):
1127
+ search_reports_by_mrn(patient_data.get('mrn', ''))
1128
+
1129
+ st.markdown('</div>', unsafe_allow_html=True)
1130
+
1131
+ # Remove old search functions - they are replaced by MRN-based search functions above
1132
+
1133
+ def display_report_details(report):
1134
+ """Display detailed report information with modern cards"""
1135
+ st.markdown('<div class="report-card">', unsafe_allow_html=True)
1136
+ col1, col2 = st.columns(2)
1137
+ with col1:
1138
+ st.markdown("**👤 معلومات المريض - Patient Information:**")
1139
+ st.markdown(f"""
1140
+ - **الاسم - Name:** {report.get('name', 'غير متاح - N/A')}
1141
+ - **تاريخ الميلاد - DOB:** {report.get('date_of_birth', 'غير متاح - N/A')}
1142
+ - **الجنس - Gender:** {report.get('gender', 'غير متاح - N/A')}
1143
+ - **رقم السجل - MRN:** {report.get('medical_record_number', 'غير متاح - N/A')}
1144
+ """)
1145
+ with col2:
1146
+ st.markdown("**🏥 معلومات الفحص - Study Information:**")
1147
+ st.markdown(f"""
1148
+ - **الطبيب المحيل - Physician:** {report.get('referring_physician', 'غير متاح - N/A')}
1149
+ - **تاريخ الفحص - Study Date:** {report.get('date_of_study', 'غير متاح - N/A')}
1150
+ - **النتائج - Findings:** {report.get('findings', 'غير متاح - N/A')}
1151
+ """)
1152
+ # PDF download button with modern styling (always use backend endpoint)
1153
+ if 'name' in report:
1154
+ pdf_url = f"{FASTAPI_BASE_URL}/reports/{report.get('name')}/pdf"
1155
+ st.markdown(f"""
1156
+ <div style="text-align: center; margin-top: 1rem;">
1157
+ <a href="{pdf_url}" target="_blank" style="
1158
+ display: inline-block;
1159
+ background: linear-gradient(135deg, #2c5aa0, #4a90e2);
1160
+ color: white;
1161
+ padding: 0.5rem 1.5rem;
1162
+ border-radius: 20px;
1163
+ text-decoration: none;
1164
+ font-weight: 600;
1165
+ box-shadow: 0 4px 15px rgba(44, 90, 160, 0.3);
1166
+ ">📄 تحميل التقرير - Download PDF Report</a>
1167
+ </div>
1168
+ """, unsafe_allow_html=True)
1169
+ st.markdown('</div>', unsafe_allow_html=True)
1170
+
1171
+ def show_all_reports():
1172
+ """Show all reports using the FastAPI backend"""
1173
+
1174
+ with st.spinner("📋 Loading all reports..."):
1175
+ try:
1176
+ response = requests.get(f"{REPORTS_ENDPOINT}/")
1177
+
1178
+ if response.status_code == 200:
1179
+ data = response.json()
1180
+ reports = data.get('reports', [])
1181
+ count = data.get('count', 0)
1182
+
1183
+ if reports:
1184
+ st.success(f"✅ Found {count} total report(s) in the system")
1185
+
1186
+ # Create a summary table
1187
+ if reports:
1188
+ df_data = []
1189
+ for report in reports:
1190
+ df_data.append({
1191
+ 'Patient Name': report.get('name', 'N/A'),
1192
+ 'Date of Birth': report.get('date_of_birth', 'N/A'),
1193
+ 'Gender': report.get('gender', 'N/A'),
1194
+ 'Study Date': report.get('date_of_study', 'N/A'),
1195
+ 'Physician': report.get('referring_physician', 'N/A')
1196
+ })
1197
+
1198
+ df = pd.DataFrame(df_data)
1199
+ st.dataframe(df, use_container_width=True)
1200
+
1201
+ # Show detailed reports
1202
+ st.subheader("Detailed Reports")
1203
+ for i, report in enumerate(reports):
1204
+ with st.expander(f"{report.get('name', 'Unknown')} - {report.get('date_of_study', 'Unknown Date')}"):
1205
+ display_report_details(report)
1206
+ else:
1207
+ st.info("📋 No reports found in the system yet.")
1208
+
1209
+ else:
1210
+ st.error(f"❌ Error retrieving reports: {response.text}")
1211
+
1212
+ except requests.exceptions.ConnectionError:
1213
+ st.error("❌ Cannot connect to the backend server. Please make sure the FastAPI server is running.")
1214
+ except Exception as e:
1215
+ st.error(f"❌ An error occurred: {str(e)}")
1216
+
1217
+ def system_status_page():
1218
+ """System status and connectivity check page"""
1219
+ st.markdown('<div class="section-header">⚙️ System Status & Diagnostics</div>', unsafe_allow_html=True)
1220
+
1221
+ st.markdown('<div class="info-card">', unsafe_allow_html=True)
1222
+ st.markdown("### 🔍 Backend Service Status")
1223
+
1224
+ col1, col2 = st.columns([3, 1])
1225
+
1226
+ with col2:
1227
+ if st.button("🔄 Check Status", use_container_width=True):
1228
+ check_backend_status()
1229
+
1230
+ with col1:
1231
+ st.info("Click 'Check Status' to test backend connectivity")
1232
+
1233
+ st.markdown('</div>', unsafe_allow_html=True)
1234
+
1235
+ # Show system information
1236
+ st.markdown('<div class="info-card">', unsafe_allow_html=True)
1237
+ st.markdown("### 📊 System Information")
1238
+
1239
+ col1, col2 = st.columns(2)
1240
+
1241
+ with col1:
1242
+ st.metric("Frontend", "Streamlit", "Running ✅")
1243
+ st.metric("Backend URL", FASTAPI_BASE_URL, "Configured")
1244
+
1245
+ with col2:
1246
+ st.metric("Upload Endpoint", "/analyze/", "Ready")
1247
+ st.metric("Reports Endpoint", "/reports", "Ready")
1248
+
1249
+ st.markdown('</div>', unsafe_allow_html=True)
1250
+
1251
+ def check_backend_status():
1252
+ """Check if backend service is running and accessible"""
1253
+ try:
1254
+ with st.spinner("Checking backend connectivity..."):
1255
+ # Test health endpoint
1256
+ response = requests.get(f"{FASTAPI_BASE_URL}/health", timeout=10)
1257
+
1258
+ if response.status_code == 200:
1259
+ data = response.json()
1260
+ st.success(f"✅ Backend service is healthy!")
1261
+ st.json(data)
1262
+ else:
1263
+ st.error(f"❌ Backend returned status {response.status_code}")
1264
+
1265
+ except requests.exceptions.ConnectionError:
1266
+ st.error("❌ Cannot connect to backend service")
1267
+ st.info("💡 Make sure the backend is running:")
1268
+ st.code("cd backend && python service.py --serve")
1269
+
1270
+ except requests.exceptions.Timeout:
1271
+ st.error("❌ Backend request timed out")
1272
+
1273
+ except Exception as e:
1274
+ st.error(f"❌ Unexpected error: {str(e)}")
1275
+
1276
+ def stream_response(thinking_text, response_text, patient_info):
1277
+ """
1278
+ Streams the AI response with proper formatting and bullet points.
1279
+ """
1280
+ # --- Helper function for formatting ---
1281
+ def format_text_to_html(text):
1282
+ """Converts plain text with newlines and list-like structures to HTML, keeping section headers as normal text."""
1283
+ section_headers = [
1284
+ "Initial Assessment:",
1285
+ "Key Findings:",
1286
+ "Clinical Significance:",
1287
+ "Impression:",
1288
+ "Conclusion:",
1289
+ "Recommendations:",
1290
+ "Technical Details:",
1291
+ "Comparison:",
1292
+ "History:",
1293
+ "Exam Type:",
1294
+ "Findings:",
1295
+ "Clinical History:",
1296
+ "Technique:",
1297
+ "Indication:",
1298
+ "Result:",
1299
+ "Results:",
1300
+ "Summary:",
1301
+ "Assessment:",
1302
+ "Plan:",
1303
+ "Follow-up:",
1304
+ "Brief Structured Report:",
1305
+ "Report:",
1306
+ "Diagnosis:",
1307
+ "Opinion:"
1308
+ ]
1309
+
1310
+ text = html.escape(text)
1311
+ lines = text.split('\n')
1312
+ html_lines = []
1313
+ in_list = False
1314
+
1315
+ for line in lines:
1316
+ stripped_line = line.strip()
1317
+
1318
+ # Remove leading bullets/numbers for header checking
1319
+ clean_line = re.sub(r'^[\d+\.]*[\-\*\•]?\s*', '', stripped_line)
1320
+
1321
+ # Also try removing just the number prefix for numbered headers
1322
+ clean_line_no_number = re.sub(r'^\d+\.?\s*', '', stripped_line)
1323
+
1324
+ # Check if this is a section header (case-insensitive, flexible matching)
1325
+ is_section_header = any(
1326
+ clean_line.lower().startswith(header.lower()) or
1327
+ clean_line.lower() == header.lower().rstrip(':') or
1328
+ clean_line_no_number.lower().startswith(header.lower()) or
1329
+ clean_line_no_number.lower() == header.lower().rstrip(':') or
1330
+ # Handle cases like "EXAM TYPE:" matching "Exam Type:"
1331
+ clean_line.lower().replace(' ', '').startswith(header.lower().replace(' ', '').rstrip(':')) or
1332
+ clean_line_no_number.lower().replace(' ', '').startswith(header.lower().replace(' ', '').rstrip(':'))
1333
+ for header in section_headers
1334
+ )
1335
+
1336
+ if is_section_header:
1337
+ # Close any open list
1338
+ if in_list:
1339
+ html_lines.append('</ol>' if 'ol>' in str(html_lines[-3:]) else '</ul>')
1340
+ in_list = False
1341
+ # Use the version without numbers/bullets for display
1342
+ display_text = clean_line_no_number if clean_line_no_number.lower().endswith(':') else clean_line
1343
+ html_lines.append(f'<p><strong>{display_text}</strong></p>')
1344
+
1345
+ elif re.match(r'^[\-\*\•]\s', stripped_line) and not is_section_header:
1346
+ # This is a bullet point (not a section header)
1347
+ if not in_list:
1348
+ html_lines.append('<ul>')
1349
+ in_list = True
1350
+ item_text = re.sub(r'^[\-\*\•]\s*', '', stripped_line)
1351
+ html_lines.append(f'<li>{item_text}</li>')
1352
+
1353
+ elif re.match(r'^\d+\.?\s', stripped_line) and not is_section_header:
1354
+ # This is a numbered list item (not a section header)
1355
+ if not in_list:
1356
+ html_lines.append('<ul>') # Convert numbered lists to bullet lists for consistency
1357
+ in_list = True
1358
+ item_text = re.sub(r'^\d+\.?\s*', '', stripped_line)
1359
+ html_lines.append(f'<li>{item_text}</li>')
1360
+
1361
+ else:
1362
+ # Regular text line
1363
+ if in_list:
1364
+ html_lines.append('</ul>')
1365
+ in_list = False
1366
+ if stripped_line:
1367
+ html_lines.append(f'<p>{line}</p>')
1368
+ else:
1369
+ html_lines.append('<br>') # Preserve empty lines as breaks
1370
+
1371
+ # Close any remaining open list
1372
+ if in_list:
1373
+ html_lines.append('</ul>')
1374
+
1375
+ return ''.join(html_lines)
1376
+
1377
+ # --- Thinking Process ---
1378
+ thinking_container = st.empty()
1379
+ thinking_container.markdown("""
1380
+ <div class="thinking-indicator">
1381
+ <span>AI is analyzing</span>
1382
+ <div class="thinking-dot"></div><div class="thinking-dot"></div><div class="thinking-dot"></div>
1383
+ </div>
1384
+ """, unsafe_allow_html=True)
1385
+
1386
+ st.markdown('<div style="font-size: 0.8em; color: #666; margin: 10px 0 5px 0; font-weight: 500;">🤔 AI Analysis Process:</div>', unsafe_allow_html=True)
1387
+ thinking_output = st.empty()
1388
+ thinking_text_container = ""
1389
+ for char in thinking_text:
1390
+ thinking_text_container += char
1391
+ formatted_thinking = format_text_to_html(thinking_text_container)
1392
+ thinking_output.markdown(f'<div class="ai-thinking">{formatted_thinking}</div>', unsafe_allow_html=True)
1393
+ time.sleep(0.005)
1394
+
1395
+ thinking_container.empty()
1396
+
1397
+ # --- Define Report Components ---
1398
+ current_date = datetime.now().strftime("%B %d, %Y")
1399
+ current_time = datetime.now().strftime("%I:%M %p")
1400
+
1401
+ report_header = (
1402
+ '<div class="report-header">'
1403
+ '<div class="hospital-info">'
1404
+ '<div class="hospital-name">🔬 MediVision Radiology Center</div>'
1405
+ '<div style="font-size: 0.85em; color: #666;">Advanced AI-Powered Medical Imaging Analysis</div>'
1406
+ '</div>'
1407
+ '<h2><span>📋 RADIOLOGY REPORT</span></h2>'
1408
+ '<div class="patient-details">'
1409
+ f'<div class="patient-detail-item"><span class="detail-label">Patient Name:</span><span class="detail-value">{patient_info["name"]}</span></div>'
1410
+ f'<div class="patient-detail-item"><span class="detail-label">MRN:</span><span class="detail-value">{patient_info["medical_record_number"]}</span></div>'
1411
+ f'<div class="patient-detail-item"><span class="detail-label">Study Date:</span><span class="detail-value">{patient_info.get("date_of_study", current_date)}</span></div>'
1412
+ f'<div class="patient-detail-item"><span class="detail-label">Report Date:</span><span class="detail-value">{current_date} at {current_time}</span></div>'
1413
+ f'<div class="patient-detail-item"><span class="detail-label">Referring Physician:</span><span class="detail-value">Dr. {patient_info["referring_physician"]}</span></div>'
1414
+ f'<div class="patient-detail-item"><span class="detail-label">Radiologist:</span><span class="detail-value">MediVision AI Assistant</span></div>'
1415
+ '</div>'
1416
+ '</div>'
1417
+ )
1418
+
1419
+ report_footer = (
1420
+ '<div class="report-footer" style="background: #f9f9f9; padding: 15px; border-radius: 5px; margin-top: 20px; text-align: center; font-size: 0.85em; color: #666;">'
1421
+ f'<strong>Report Generated by:</strong> MediVision AI Assistant<br>'
1422
+ f'<strong>Supervising Physician:</strong> Dr. {patient_info["referring_physician"]}<br>'
1423
+ '<strong>Institution:</strong> MediVision Radiology Center<br>'
1424
+ f'<strong>Generated on:</strong> {current_date} at {current_time}<br>'
1425
+ '<em>This report has been generated using advanced AI technology and should be reviewed by a qualified radiologist.</em>'
1426
+ '</div>'
1427
+ )
1428
+
1429
+ # --- Stream the response ---
1430
+ report_container = st.empty()
1431
+ response_text_accumulated = ""
1432
+
1433
+ for char in response_text:
1434
+ response_text_accumulated += char
1435
+ formatted_content = format_text_to_html(response_text_accumulated)
1436
+ content_html = (
1437
+ '<div class="report-content" style="background: white; padding: 20px; border-radius: 10px; margin-top: 10px; border-left: 4px solid #2c5aa0; box-shadow: 0 3px 15px rgba(0,0,0,0.1);">'
1438
+ f'{formatted_content}'
1439
+ '</div>'
1440
+ )
1441
+ full_report_so_far = f'<div class="ai-response">{report_header}{content_html}</div>'
1442
+ report_container.markdown(full_report_so_far, unsafe_allow_html=True)
1443
+ time.sleep(0.01)
1444
+
1445
+ # Final report with footer
1446
+ final_formatted_content = format_text_to_html(response_text_accumulated)
1447
+ final_content_html = (
1448
+ '<div class="report-content" style="background: white; padding: 20px; border-radius: 10px; margin-top: 10px; border-left: 4px solid #2c5aa0; box-shadow: 0 3px 15px rgba(0,0,0,0.1);">'
1449
+ f'{final_formatted_content}'
1450
+ '</div>'
1451
+ )
1452
+ final_full_report = f'<div class="ai-response">{report_header}{final_content_html}{report_footer}</div>'
1453
+ report_container.markdown(final_full_report, unsafe_allow_html=True)
1454
 
1455
+ if __name__ == "__main__":
1456
+ main()