Spaces:
Runtime error
Runtime error
| import streamlit as st | |
| import requests | |
| import json | |
| from PIL import Image | |
| import pandas as pd | |
| from datetime import datetime | |
| import os | |
| import time | |
| import base64 | |
| import io | |
| import re | |
| import html | |
| # Must be the first Streamlit command | |
| st.set_page_config( | |
| page_title="MediVision - Radiology Report System", | |
| page_icon="🔬", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Configuration | |
| FASTAPI_BASE_URL = "http://localhost:8001" | |
| UPLOAD_ENDPOINT = f"{FASTAPI_BASE_URL}/upload/" | |
| REPORTS_ENDPOINT = f"{FASTAPI_BASE_URL}/reports" | |
| # ============================================================================ | |
| # CUSTOM STYLING (Based on Hospital System) | |
| # ============================================================================ | |
| st.markdown(""" | |
| <style> | |
| /* Hide Streamlit default elements */ | |
| .stDeployButton {display:none;} | |
| .stDecoration {display:none;} | |
| #MainMenu {visibility: hidden;} | |
| footer {visibility: hidden;} | |
| header {visibility: hidden;} | |
| /* Hide sidebar */ | |
| .css-1d391kg {display: none;} | |
| .css-1rs6os {display: none;} | |
| .st-emotion-cache-16idsys {display: none;} | |
| /* Navigation Bar Styling */ | |
| .nav-bar { | |
| background: linear-gradient(135deg, #1e3c72, #2a5298); | |
| padding: 1rem 2rem; | |
| border-radius: 15px; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.15); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .nav-brand { | |
| color: white; | |
| font-size: 1.8rem; | |
| font-weight: 700; | |
| text-shadow: 0 2px 4px rgba(0,0,0,0.3); | |
| margin: 0; | |
| } | |
| .nav-subtitle { | |
| color: rgba(255,255,255,0.9); | |
| font-size: 0.9rem; | |
| margin: 0; | |
| font-weight: 300; | |
| } | |
| .nav-menu { | |
| display: flex; | |
| gap: 1rem; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .nav-item { | |
| background: rgba(255,255,255,0.1); | |
| color: white; | |
| padding: 0.6rem 1.2rem; | |
| border-radius: 25px; | |
| text-decoration: none; | |
| font-weight: 500; | |
| font-size: 0.9rem; | |
| border: 2px solid transparent; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| backdrop-filter: blur(10px); | |
| } | |
| .nav-item:hover { | |
| background: rgba(255,255,255,0.2); | |
| border-color: rgba(255,255,255,0.3); | |
| transform: translateY(-2px); | |
| color: white; | |
| text-decoration: none; | |
| } | |
| .nav-item.active { | |
| background: rgba(255,255,255,0.3); | |
| border-color: rgba(255,255,255,0.5); | |
| font-weight: 600; | |
| } | |
| @media (max-width: 768px) { | |
| .nav-bar { | |
| flex-direction: column; | |
| text-align: center; | |
| gap: 1rem; | |
| } | |
| .nav-menu { | |
| justify-content: center; | |
| width: 100%; | |
| } | |
| .nav-item { | |
| font-size: 0.8rem; | |
| padding: 0.5rem 1rem; | |
| } | |
| } | |
| /* Background image */ | |
| .stApp { | |
| background-image: linear-gradient(rgba(255,255,255,0.85), rgba(255,255,255,0.85)), | |
| url("https://www.servereworldsystem.com/include/blog/1464/14640320083m.jpeg"); | |
| background-size: cover; | |
| background-position: center; | |
| background-attachment: fixed; | |
| background-repeat: no-repeat; | |
| } | |
| /* Main container styling */ | |
| .main .block-container { | |
| padding-top: 1rem; | |
| padding-bottom: 1rem; | |
| max-width: 1200px; | |
| background: rgba(255, 255, 255, 0.95); | |
| border-radius: 20px; | |
| margin-top: 2rem; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.1); | |
| backdrop-filter: blur(10px); | |
| } | |
| /* Header styling */ | |
| .medivision-header { | |
| background: linear-gradient(135deg, #1e3c72, #2a5298); | |
| color: white; | |
| padding: 2rem; | |
| border-radius: 15px; | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.15); | |
| } | |
| .medivision-header h1 { | |
| margin: 0; | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| text-shadow: 0 2px 4px rgba(0,0,0,0.3); | |
| color: white !important; | |
| } | |
| .medivision-header p { | |
| margin: 0.5rem 0 0 0; | |
| opacity: 0.95; | |
| font-size: 1.2rem; | |
| font-weight: 300; | |
| } | |
| /* Section headers */ | |
| .section-header { | |
| background: linear-gradient(135deg, #f8f9fa, #e9ecef); | |
| color: #2c5aa0; | |
| padding: 1rem 1.5rem; | |
| border-radius: 10px; | |
| border-left: 5px solid #4a90e2; | |
| margin: 2rem 0 1rem 0; | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| } | |
| /* Cards and containers */ | |
| .info-card { | |
| background: white; | |
| border-radius: 15px; | |
| padding: 2rem; | |
| margin: 1.5rem 0; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.1); | |
| border: 1px solid #e0e6ed; | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .info-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 30px rgba(0,0,0,0.15); | |
| } | |
| .upload-area { | |
| background: linear-gradient(135deg, #f8f9fa, #ffffff); | |
| border: 2px dashed #2c5aa0; | |
| border-radius: 15px; | |
| padding: 2rem; | |
| text-align: center; | |
| margin: 1rem 0; | |
| transition: all 0.3s ease; | |
| } | |
| .upload-area:hover { | |
| border-color: #4a90e2; | |
| background: linear-gradient(135deg, #ffffff, #f8f9fa); | |
| } | |
| /* Buttons */ | |
| .stButton > button { | |
| background: linear-gradient(135deg, #2c5aa0, #4a90e2); | |
| color: white; | |
| border: none; | |
| border-radius: 25px; | |
| padding: 0.75rem 2rem; | |
| font-weight: 600; | |
| font-size: 1rem; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(44, 90, 160, 0.3); | |
| } | |
| .stButton > button:hover { | |
| background: linear-gradient(135deg, #4a90e2, #6ab7ff); | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(44, 90, 160, 0.4); | |
| } | |
| /* Success/Error messages */ | |
| .stSuccess { | |
| background: linear-gradient(135deg, #4caf50, #66bb6a); | |
| color: white; | |
| border-radius: 10px; | |
| padding: 1rem; | |
| border: none; | |
| } | |
| .stError { | |
| background: linear-gradient(135deg, #f44336, #ef5350); | |
| color: white; | |
| border-radius: 10px; | |
| padding: 1rem; | |
| border: none; | |
| } | |
| .stWarning { | |
| background: linear-gradient(135deg, #ff9800, #ffb74d); | |
| color: white; | |
| border-radius: 10px; | |
| padding: 1rem; | |
| border: none; | |
| } | |
| .stInfo { | |
| background: linear-gradient(135deg, #2196f3, #42a5f5); | |
| color: white; | |
| border-radius: 10px; | |
| padding: 1rem; | |
| border: none; | |
| } | |
| /* Form inputs */ | |
| .stTextInput > div > div > input { | |
| border-radius: 10px; | |
| border: 2px solid #e0e6ed; | |
| padding: 0.75rem; | |
| font-size: 1rem; | |
| transition: border-color 0.3s ease; | |
| } | |
| .stTextInput > div > div > input:focus { | |
| border-color: #2c5aa0; | |
| box-shadow: 0 0 0 3px rgba(44, 90, 160, 0.1); | |
| } | |
| .stSelectbox > div > div > select { | |
| border-radius: 10px; | |
| border: 2px solid #e0e6ed; | |
| padding: 0.75rem; | |
| font-size: 1rem; | |
| } | |
| /* File uploader */ | |
| .stFileUploader > div { | |
| border-radius: 15px; | |
| border: 2px solid #e0e6ed; | |
| background: linear-gradient(135deg, #f8f9fa, #ffffff); | |
| padding: 1rem; | |
| } | |
| .stFileUploader > div:hover { | |
| border-color: #2c5aa0; | |
| } | |
| /* Sidebar */ | |
| .css-1d391kg { | |
| background: linear-gradient(180deg, #2c5aa0, #4a90e2); | |
| } | |
| .css-1d391kg .css-17eq0hr { | |
| color: white; | |
| } | |
| /* Progress bars */ | |
| .stProgress > div > div > div > div { | |
| background: linear-gradient(90deg, #2c5aa0, #4a90e2); | |
| } | |
| /* Metrics */ | |
| .metric-card { | |
| background: white; | |
| border-radius: 15px; | |
| padding: 1.5rem; | |
| text-align: center; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
| border: 1px solid #e0e6ed; | |
| margin: 1rem 0; | |
| } | |
| .metric-value { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| color: #2c5aa0; | |
| margin: 0.5rem 0; | |
| } | |
| .metric-label { | |
| font-size: 1rem; | |
| color: #666; | |
| font-weight: 500; | |
| } | |
| /* Status indicators */ | |
| .status-online { | |
| display: inline-block; | |
| width: 12px; | |
| height: 12px; | |
| background: #4caf50; | |
| border-radius: 50%; | |
| margin-left: 8px; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 0.4; } | |
| 50% { opacity: 0.8; } | |
| 100% { opacity: 0.4; } | |
| } | |
| /* Report cards */ | |
| .report-card { | |
| background: white; | |
| border-radius: 15px; | |
| padding: 1.5rem; | |
| margin: 1rem 0; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
| border-left: 5px solid #4a90e2; | |
| transition: all 0.3s ease; | |
| } | |
| .report-card:hover { | |
| transform: translateX(5px); | |
| box-shadow: 0 6px 25px rgba(0,0,0,0.15); | |
| } | |
| /* Data tables */ | |
| .dataframe { | |
| border-radius: 10px; | |
| overflow: hidden; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
| } | |
| /* Mobile responsive */ | |
| @media (max-width: 768px) { | |
| .medivision-header h1 { | |
| font-size: 2rem; | |
| } | |
| .medivision-header p { | |
| font-size: 1rem; | |
| } | |
| .main .block-container { | |
| padding: 1rem 0.5rem; | |
| } | |
| .info-card { | |
| padding: 1rem; | |
| } | |
| } | |
| /* RTL support for Arabic */ | |
| .rtl { | |
| direction: rtl; | |
| text-align: right; | |
| } | |
| /* Loading animations */ | |
| .loading-spinner { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid rgba(255,255,255,.3); | |
| border-radius: 50%; | |
| border-top-color: #fff; | |
| animation: spin 1s ease-in-out infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* FIXED THINKING INDICATOR - Larger font size and consistent spacing */ | |
| .thinking-indicator { | |
| display: flex; | |
| align-items: center; | |
| margin: 15px 0; | |
| padding: 15px 20px; | |
| border-radius: 10px; | |
| background: #f8f9fa; | |
| color: #666; | |
| font-style: italic; | |
| font-size: 1.1em; /* Increased from 0.9em */ | |
| line-height: 1.5; | |
| font-weight: 500; | |
| border-left: 4px solid #2c5aa0; | |
| } | |
| .thinking-dot { | |
| width: 10px; /* Increased from 8px */ | |
| height: 10px; | |
| background: #2c5aa0; /* Changed from #999 to match theme */ | |
| border-radius: 50%; | |
| margin: 0 3px; /* Increased spacing */ | |
| animation: pulse 1.5s infinite; | |
| } | |
| .thinking-dot:nth-child(2) { | |
| animation-delay: 0.2s; | |
| } | |
| .thinking-dot:nth-child(3) { | |
| animation-delay: 0.4s; | |
| } | |
| /* FIXED AI THINKING - Larger font and better spacing */ | |
| .ai-thinking { | |
| color: #555; /* Darker for better readability */ | |
| font-style: italic; | |
| background: #f8f9fa; | |
| padding: 20px 24px; /* Increased padding */ | |
| border-radius: 10px; | |
| margin: 15px 0; /* Consistent spacing */ | |
| border-left: 4px solid #2c5aa0; | |
| font-size: 1.0em; /* Increased from 0.85em */ | |
| line-height: 1.6; /* Increased line height */ | |
| max-height: 200px; /* Slightly taller */ | |
| overflow-y: auto; | |
| font-weight: 400; | |
| } | |
| .ai-thinking pre { | |
| margin: 8px 0; /* Added margin */ | |
| padding: 0; | |
| white-space: pre-wrap; | |
| font-size: 1.0em; /* Increased from 0.85em */ | |
| font-family: 'Consolas', 'Monaco', monospace; | |
| } | |
| .ai-thinking p { | |
| margin: 10px 0; /* Increased from 4px */ | |
| font-size: 1.0em; /* Increased from 0.85em */ | |
| } | |
| .ai-thinking ul, .ai-thinking ol { | |
| margin: 10px 0; /* Increased from 4px */ | |
| padding-left: 20px; /* Slightly increased */ | |
| } | |
| .ai-thinking li { | |
| margin: 6px 0; /* Increased from 2px */ | |
| font-size: 1.0em; /* Increased from 0.85em */ | |
| } | |
| .stream-text { | |
| white-space: pre-wrap; | |
| font-family: 'Segoe UI', Arial, sans-serif; /* Better font */ | |
| line-height: 1.5; /* Increased */ | |
| font-size: 1.0em; /* Added explicit size */ | |
| } | |
| /* FIXED AI RESPONSE - Better rendering */ | |
| .ai-response { | |
| color: #333; | |
| background: white; | |
| padding: 24px; /* Increased padding */ | |
| border-radius: 12px; | |
| margin: 20px 0; /* Increased margin */ | |
| border-left: 4px solid #2c5aa0; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.12); | |
| font-size: 1.0em; /* Increased from 0.95em */ | |
| line-height: 1.7; /* Increased line height */ | |
| font-family: 'Segoe UI', -apple-system, sans-serif; | |
| } | |
| /* FIXED REPORT HEADER - Better rendering */ | |
| .report-header { | |
| border-bottom: 2px solid #2c5aa0; | |
| margin-bottom: 25px; /* Increased */ | |
| padding-bottom: 20px; | |
| background: linear-gradient(135deg, #f8f9fa, #ffffff); | |
| padding: 25px; /* Increased */ | |
| border-radius: 10px; | |
| margin: -24px -24px 25px -24px; /* Adjusted for new padding */ | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| } | |
| .report-header h2 { | |
| color: #2c5aa0; | |
| margin: 0 0 20px 0; /* Increased margin */ | |
| font-size: 1.5em; /* Increased */ | |
| font-weight: 700; | |
| text-align: center; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 12px; /* Increased gap */ | |
| } | |
| .report-header .hospital-info { | |
| text-align: center; | |
| margin-bottom: 20px; /* Increased */ | |
| padding-bottom: 15px; /* Increased */ | |
| border-bottom: 1px solid #e0e0e0; | |
| } | |
| .report-header .hospital-name { | |
| font-size: 1.2em; /* Increased */ | |
| font-weight: 600; | |
| color: #2c5aa0; | |
| margin-bottom: 8px; /* Increased */ | |
| } | |
| .report-header .patient-details { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; /* Increased */ | |
| margin-top: 20px; /* Increased */ | |
| } | |
| .report-header .patient-detail-item { | |
| background: rgba(44, 90, 160, 0.05); | |
| padding: 12px 16px; /* Increased */ | |
| border-radius: 8px; | |
| font-size: 0.95em; /* Slightly increased */ | |
| border: 1px solid rgba(44, 90, 160, 0.1); | |
| } | |
| .report-header .detail-label { | |
| font-weight: 600; | |
| color: #2c5aa0; | |
| display: inline-block; | |
| min-width: 80px; | |
| } | |
| .report-header .detail-value { | |
| color: #333; | |
| margin-left: 8px; /* Increased */ | |
| } | |
| /* FIXED REPORT CONTENT - Better rendering */ | |
| .report-content { | |
| margin-top: 25px; /* Increased */ | |
| white-space: pre-wrap; | |
| font-family: 'Segoe UI', -apple-system, sans-serif; | |
| line-height: 1.7; /* Increased */ | |
| font-size: 1.0em; /* Added explicit size */ | |
| color: #333; | |
| } | |
| /* Styling for bullet points in report content */ | |
| .report-content ul { | |
| margin: 15px 0; | |
| padding-left: 25px; | |
| list-style-type: disc; | |
| } | |
| .report-content ol { | |
| margin: 15px 0; | |
| padding-left: 25px; | |
| list-style-type: decimal; | |
| } | |
| .report-content li { | |
| margin: 8px 0; | |
| color: #333; | |
| line-height: 1.6; | |
| padding-left: 5px; | |
| } | |
| .report-content ul li::marker { | |
| color: #2c5aa0; | |
| font-size: 1.1em; | |
| } | |
| .report-content ol li::marker { | |
| color: #2c5aa0; | |
| font-weight: 600; | |
| } | |
| /* Section headers within report content */ | |
| .report-content p strong { | |
| color: #333; | |
| font-size: 1.0em; | |
| font-weight: 400; | |
| display: block; | |
| margin: 15px 0 8px 0; | |
| padding-bottom: 0px; | |
| border-bottom: none; | |
| } | |
| .report-content p { | |
| margin: 10px 0; | |
| line-height: 1.6; | |
| } | |
| .report-section { | |
| margin: 25px 0; /* Increased */ | |
| } | |
| .report-section h3 { | |
| color: #2c5aa0; | |
| font-size: 1.2em; /* Increased */ | |
| margin: 20px 0 12px 0; /* Increased */ | |
| font-weight: 600; | |
| border-bottom: 2px solid #e0e0e0; /* Thicker border */ | |
| padding-bottom: 8px; /* Increased */ | |
| } | |
| .report-section h4 { | |
| color: #2c5aa0; | |
| font-size: 1.1em; /* Increased */ | |
| margin: 15px 0 10px 0; /* Increased */ | |
| font-weight: 600; | |
| } | |
| .report-section p { | |
| margin: 12px 0; /* Increased */ | |
| color: #333; | |
| text-align: justify; | |
| line-height: 1.7; | |
| } | |
| .report-section ul, .report-section ol { | |
| margin: 15px 0; /* Increased */ | |
| padding-left: 30px; /* Increased */ | |
| } | |
| .report-section li { | |
| margin: 8px 0; /* Increased */ | |
| color: #333; | |
| line-height: 1.6; | |
| } | |
| /* FIXED REPORT FOOTER */ | |
| .report-footer { | |
| margin-top: 30px; /* Increased */ | |
| padding-top: 20px; /* Increased */ | |
| border-top: 2px solid #eee; /* Thicker border */ | |
| font-size: 0.95em; /* Slightly increased */ | |
| color: #666; | |
| text-align: center; | |
| background: #f9f9f9; | |
| padding: 20px; /* Increased */ | |
| border-radius: 8px; | |
| margin-left: -24px; /* Adjusted */ | |
| margin-right: -24px; | |
| margin-bottom: -24px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ============================================================================ | |
| def main(): | |
| # Modern header with MediVision branding | |
| st.markdown(""" | |
| <div class="medivision-header"> | |
| <h1>🔬 MediVision</h1> | |
| <p>Advanced Radiology Report System - Powered by AI Technology</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Sidebar navigation | |
| st.sidebar.markdown("## 📋 Navigation") | |
| page = st.sidebar.selectbox("Choose Page", [ | |
| "📤 Upload & Generate Report", | |
| "📊 View Reports", | |
| "⚙️ System Status" | |
| ]) | |
| if "Upload" in page: | |
| upload_page() | |
| elif "View Reports" in page: | |
| view_reports_page() | |
| elif "System Status" in page: | |
| system_status_page() | |
| def upload_page(): | |
| st.markdown('<div class="section-header">📤 Upload Medical Images & Generate Reports</div>', unsafe_allow_html=True) | |
| # Create two columns for better layout | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| st.markdown('<div class="info-card">', unsafe_allow_html=True) | |
| st.markdown("### 👤 Patient Information") | |
| # Patient form with modern styling | |
| with st.form("patient_form"): | |
| name = st.text_input("Patient Name *", placeholder="Enter full name") | |
| date_of_birth = st.date_input("Date of Birth *") | |
| gender = st.selectbox("Gender *", ["Male", "Female", "Other"]) | |
| # MRN field (auto-generated, but allow manual override) | |
| col1, col2 = st.columns([3, 1]) | |
| with col1: | |
| medical_record_number = st.text_input( | |
| "Medical Record Number (MRN)", | |
| placeholder="Auto-generated if left empty", | |
| help="Leave empty to auto-generate MRN based on patient name and DOB" | |
| ) | |
| with col2: | |
| st.markdown("<br>", unsafe_allow_html=True) # Add spacing | |
| if st.form_submit_button("🔢 Generate MRN", use_container_width=True): | |
| if name and date_of_birth: | |
| try: | |
| response = requests.get( | |
| f"{FASTAPI_BASE_URL}/generate-mrn/", | |
| params={"patient_name": name, "date_of_birth": str(date_of_birth)}, | |
| timeout=10 | |
| ) | |
| if response.status_code == 200: | |
| data = response.json() | |
| if data.get("success"): | |
| st.session_state.generated_mrn = data.get("mrn") | |
| st.success(f"✅ Generated MRN: {data.get('mrn')}") | |
| st.rerun() | |
| except Exception as e: | |
| st.error(f"❌ Error generating MRN: {str(e)}") | |
| else: | |
| st.error("⚠️ Please enter patient name and date of birth first") | |
| # Show generated MRN if available | |
| if hasattr(st.session_state, 'generated_mrn') and st.session_state.generated_mrn: | |
| medical_record_number = st.text_input( | |
| "Generated MRN", | |
| value=st.session_state.generated_mrn, | |
| disabled=True, | |
| help="Auto-generated MRN - you can edit the field above to override" | |
| ) | |
| referring_physician = st.text_input("Referring Physician *", placeholder="Enter physician name") | |
| date_of_study = st.date_input("Date of Study *", value=datetime.now().date()) | |
| st.markdown("### 🖼️ Medical Image") | |
| st.markdown('<div class="upload-area">', unsafe_allow_html=True) | |
| uploaded_file = st.file_uploader( | |
| "Choose medical image", | |
| type=['png', 'jpg', 'jpeg', 'dcm'], | |
| help="Upload X-ray, CT, MRI, (Max 10MB)", | |
| key="medical_image_upload" | |
| ) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Display uploaded image with modern styling (NO API CALLS) | |
| if uploaded_file is not None: | |
| try: | |
| # Validate file size only (no API calls) | |
| file_size = uploaded_file.size | |
| if file_size > 10 * 1024 * 1024: # 10MB | |
| st.error("⚠️ File size too large. Please upload an image smaller than 10MB.") | |
| else: | |
| # Display success message and file info | |
| st.success(f"✅ File uploaded successfully: {uploaded_file.name}") | |
| st.info(f"📁 File size: {file_size/1024:.1f} KB") | |
| # Display image preview (local only, no backend calls) | |
| if uploaded_file.type.startswith('image/'): | |
| image = Image.open(uploaded_file) | |
| st.image(image, caption="Uploaded Medical Image", use_column_width=True) | |
| else: | |
| st.info("📄 DICOM file uploaded successfully") | |
| # Reset file pointer for later use | |
| uploaded_file.seek(0) | |
| except Exception as e: | |
| st.error(f"❌ Error processing image: {str(e)}") | |
| st.info("💡 Please ensure the file is a valid image format (PNG, JPG, JPEG, DCM)") | |
| else: | |
| st.info("👆 Please upload a medical image to begin analysis") | |
| submit_button = st.form_submit_button("🔍 Generate Report", use_container_width=True) | |
| if submit_button: | |
| # Get the MRN (either generated or manually entered) | |
| final_mrn = medical_record_number | |
| if hasattr(st.session_state, 'generated_mrn') and st.session_state.generated_mrn and not medical_record_number: | |
| final_mrn = st.session_state.generated_mrn | |
| # Validation (MRN is now optional as it can be auto-generated) | |
| if not all([name, date_of_birth, gender, referring_physician, uploaded_file]): | |
| st.error("⚠️ Please fill all required fields and upload an image") | |
| else: | |
| generate_report(name, str(date_of_birth), gender, final_mrn, | |
| referring_physician, str(date_of_study), uploaded_file) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| def generate_report(name, date_of_birth, gender, medical_record_number, | |
| referring_physician, date_of_study, uploaded_file): | |
| """Generate report by sending data to backend and displaying the result""" | |
| try: | |
| if uploaded_file is not None: | |
| with st.spinner("🔄 Analyzing medical image..."): | |
| # Validate file before sending | |
| if uploaded_file.size > 10 * 1024 * 1024: # 10MB limit | |
| st.error("⚠️ File size too large. Please upload an image smaller than 10MB.") | |
| return | |
| # Reset file pointer to beginning | |
| uploaded_file.seek(0) | |
| # Prepare file for upload | |
| files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)} | |
| try: | |
| # Call analyze endpoint | |
| analyze_response = requests.post( | |
| f"{FASTAPI_BASE_URL}/analyze", | |
| files=files, | |
| timeout=60 # 60 second timeout | |
| ) | |
| if analyze_response.status_code == 200: | |
| response_data = analyze_response.json() | |
| # Handle successful response | |
| if response_data.get("success"): | |
| findings = response_data.get("analysis", "Analysis completed") | |
| # Parse thinking and answer sections | |
| thinking_text = "" | |
| report_text = findings | |
| if "thinking:" in findings and "answer:" in findings: | |
| parts = findings.split("answer:", 1) | |
| thinking_text = parts[0].replace("thinking:", "").strip() | |
| report_text = parts[1].strip() | |
| st.success("✅ Analysis completed successfully!") | |
| stream_response(thinking_text, report_text, { | |
| "name": name, | |
| "medical_record_number": medical_record_number, | |
| "referring_physician": referring_physician, | |
| "date_of_study": date_of_study | |
| }) | |
| else: | |
| # Handle failed analysis with fallback | |
| st.warning("⚠️ AI analysis encountered issues, using fallback analysis") | |
| fallback_findings = response_data.get("analysis", "Medical image processed. Professional review recommended.") | |
| stream_response("Fallback analysis used", fallback_findings, { | |
| "name": name, | |
| "medical_record_number": medical_record_number, | |
| "referring_physician": referring_physician, | |
| "date_of_study": date_of_study | |
| }) | |
| findings = fallback_findings | |
| else: | |
| st.error(f"❌ Backend Error (Status {analyze_response.status_code})") | |
| st.error(f"Response: {analyze_response.text}") | |
| return | |
| except requests.exceptions.Timeout: | |
| st.error("⏱️ Request timed out. The analysis is taking too long. Please try again.") | |
| return | |
| except requests.exceptions.ConnectionError: | |
| st.error("🔌 Cannot connect to backend service.") | |
| st.error("Please ensure the backend is running at: http://localhost:8001") | |
| st.info("💡 To start the backend, run: `cd backend && python service.py`") | |
| return | |
| except requests.exceptions.RequestException as e: | |
| st.error(f"❌ Request failed: {str(e)}") | |
| return | |
| # Generate PDF report | |
| with st.spinner("📄 Generating PDF report..."): | |
| try: | |
| pdf_response = requests.post( | |
| f"{FASTAPI_BASE_URL}/generate-report/", | |
| data={ | |
| "patient_name": name, | |
| "medical_record_number": medical_record_number or "AUTO-GENERATED", | |
| "date_of_birth": date_of_birth, | |
| "gender": gender, | |
| "referring_physician": referring_physician, | |
| "study_date": date_of_study, | |
| "findings": findings | |
| }, | |
| timeout=30 | |
| ) | |
| if pdf_response.status_code == 200: | |
| pdf_data = pdf_response.json() | |
| if pdf_data.get("success"): | |
| pdf_url = f"{FASTAPI_BASE_URL}/reports/{name.replace(' ', '_')}/pdf" | |
| st.markdown(f''' | |
| <div style="margin-top: 1rem; padding: 1rem; background: linear-gradient(135deg, #4caf50, #66bb6a); border-radius: 10px;"> | |
| <p style="color: white; margin: 0; font-weight: 600;">✅ PDF Report Generated Successfully!</p> | |
| <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);"> | |
| 📄 Download PDF Report | |
| </a> | |
| </div> | |
| ''', unsafe_allow_html=True) | |
| else: | |
| st.error(f"❌ Failed to generate PDF: {pdf_data.get('message', 'Unknown error')}") | |
| else: | |
| st.error(f"❌ PDF generation failed with status {pdf_response.status_code}") | |
| except requests.exceptions.Timeout: | |
| st.error("⏱️ PDF generation timed out. Please try again.") | |
| except Exception as e: | |
| st.error(f"❌ Error generating PDF: {str(e)}") | |
| except Exception as e: | |
| st.error(f"❌ Unexpected error: {str(e)}") | |
| st.info("💡 Troubleshooting tips:") | |
| st.info(" • Ensure backend service is running on port 8001") | |
| st.info(" • Check that the uploaded file is a valid image") | |
| st.info(" • Try refreshing the page and uploading again") | |
| def view_reports_page(): | |
| st.markdown('<div class="section-header">📊 View Patient Reports - Search by MRN</div>', unsafe_allow_html=True) | |
| # Search section with modern styling | |
| st.markdown('<div class="info-card">', unsafe_allow_html=True) | |
| col1, col2 = st.columns([3, 1]) | |
| with col1: | |
| patient_mrn = st.text_input("🔍 Search by MRN", | |
| placeholder="Enter Medical Record Number (e.g., MV-20250618-A1B2)") | |
| with col2: | |
| st.markdown("<br>", unsafe_allow_html=True) # Add spacing | |
| search_button = st.button("🔍 Search", use_container_width=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Action buttons | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if search_button and patient_mrn: | |
| search_reports_by_mrn(patient_mrn) | |
| with col2: | |
| if st.button("📋 Show All Patients", use_container_width=True): | |
| show_all_patients() | |
| def search_reports_by_mrn(mrn): | |
| """Search for reports by MRN with modern styling""" | |
| with st.spinner(f"🔍 Searching for patient with MRN: {mrn}..."): | |
| try: | |
| response = requests.get(f"{FASTAPI_BASE_URL}/patient/{mrn}", timeout=10) | |
| if response.status_code == 200: | |
| patient_data = response.json() | |
| st.markdown(f""" | |
| <div class="info-card" style="background: linear-gradient(135deg, #4caf50, #66bb6a); color: white;"> | |
| <h4>✅ Patient Found: {patient_data.get('patient_name', 'N/A')}</h4> | |
| <p><b>MRN:</b> {patient_data.get('mrn', 'N/A')}</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Display patient details | |
| display_patient_details(patient_data) | |
| elif response.status_code == 404: | |
| st.markdown(f""" | |
| <div class="info-card" style="background: linear-gradient(135deg, #ff9800, #ffb74d); color: white;"> | |
| <h4>⚠️ No Patient Found</h4> | |
| <p>No patient record found with MRN: <b>{mrn}</b></p> | |
| <p>Please check the MRN and try again.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| st.error(f"❌ Error searching patient: HTTP {response.status_code}") | |
| except requests.exceptions.ConnectionError: | |
| st.error("❌ Cannot connect to the backend server. Please make sure the FastAPI server is running.") | |
| except requests.exceptions.Timeout: | |
| st.error("❌ Search request timed out. Please try again.") | |
| except Exception as e: | |
| st.error(f"❌ An error occurred: {str(e)}") | |
| def show_all_patients(): | |
| """Show all patients in the system""" | |
| with st.spinner("📋 Loading all patient records..."): | |
| try: | |
| response = requests.get(f"{FASTAPI_BASE_URL}/patients", timeout=15) | |
| if response.status_code == 200: | |
| data = response.json() | |
| patients = data.get('patients', []) | |
| if patients: | |
| st.markdown(f""" | |
| <div class="info-card" style="background: linear-gradient(135deg, #2196f3, #42a5f5); color: white;"> | |
| <h4>📊 Found {len(patients)} Patient Records</h4> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Display patients in a table format | |
| for patient in patients: | |
| display_patient_summary(patient) | |
| else: | |
| st.info("📋 No patient records found in the system yet.") | |
| else: | |
| st.error(f"❌ Error retrieving patients: {response.text}") | |
| except requests.exceptions.ConnectionError: | |
| st.error("❌ Cannot connect to the backend server. Please make sure the FastAPI server is running.") | |
| except Exception as e: | |
| st.error(f"❌ An error occurred: {str(e)}") | |
| def display_patient_details(patient_data): | |
| """Display detailed patient information""" | |
| st.markdown('<div class="info-card">', unsafe_allow_html=True) | |
| st.markdown("### 👤 Patient Details") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(f"**Name:** {patient_data.get('patient_name', 'N/A')}") | |
| st.markdown(f"**MRN:** {patient_data.get('mrn', 'N/A')}") | |
| st.markdown(f"**Date of Birth:** {patient_data.get('date_of_birth', 'N/A')}") | |
| st.markdown(f"**Gender:** {patient_data.get('gender', 'N/A')}") | |
| with col2: | |
| st.markdown(f"**Referring Physician:** Dr. {patient_data.get('referring_physician', 'N/A')}") | |
| st.markdown(f"**Study Date:** {patient_data.get('date_of_study', 'N/A')}") | |
| st.markdown(f"**Report Date:** {patient_data.get('report_date', 'N/A')}") | |
| st.markdown(f"**Status:** {patient_data.get('status', 'N/A').title()}") | |
| # Display findings | |
| if patient_data.get('findings'): | |
| st.markdown("### 🔍 Findings") | |
| findings = patient_data.get('findings', '') | |
| # Process findings to show only the answer part | |
| if "answer:" in findings.lower(): | |
| parts = findings.split("answer:", 1) | |
| if len(parts) == 2: | |
| findings_display = parts[1].strip() | |
| else: | |
| findings_display = findings | |
| else: | |
| findings_display = findings | |
| st.markdown(findings_display) | |
| # PDF download link | |
| patient_name = patient_data.get('patient_name', 'Unknown') | |
| pdf_url = f"{FASTAPI_BASE_URL}/reports/{patient_name}/pdf" | |
| st.markdown(f""" | |
| <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;"> | |
| 📄 Download PDF Report | |
| </a> | |
| """, unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| def display_patient_summary(patient_data): | |
| """Display patient summary in a card format""" | |
| st.markdown('<div class="info-card">', unsafe_allow_html=True) | |
| col1, col2, col3 = st.columns([2, 2, 1]) | |
| with col1: | |
| st.markdown(f"**{patient_data.get('patient_name', 'N/A')}**") | |
| st.markdown(f"MRN: {patient_data.get('mrn', 'N/A')}") | |
| with col2: | |
| st.markdown(f"Study: {patient_data.get('date_of_study', 'N/A')}") | |
| st.markdown(f"Physician: Dr. {patient_data.get('referring_physician', 'N/A')}") | |
| with col3: | |
| # Search button for this specific patient | |
| if st.button(f"🔍 View", key=f"view_{patient_data.get('mrn', 'unknown')}"): | |
| search_reports_by_mrn(patient_data.get('mrn', '')) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Remove old search functions - they are replaced by MRN-based search functions above | |
| def display_report_details(report): | |
| """Display detailed report information with modern cards""" | |
| st.markdown('<div class="report-card">', unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("**👤 معلومات المريض - Patient Information:**") | |
| st.markdown(f""" | |
| - **الاسم - Name:** {report.get('name', 'غير متاح - N/A')} | |
| - **تاريخ الميلاد - DOB:** {report.get('date_of_birth', 'غير متاح - N/A')} | |
| - **الجنس - Gender:** {report.get('gender', 'غير متاح - N/A')} | |
| - **رقم السجل - MRN:** {report.get('medical_record_number', 'غير متاح - N/A')} | |
| """) | |
| with col2: | |
| st.markdown("**🏥 معلومات الفحص - Study Information:**") | |
| st.markdown(f""" | |
| - **الطبيب المحيل - Physician:** {report.get('referring_physician', 'غير متاح - N/A')} | |
| - **تاريخ الفحص - Study Date:** {report.get('date_of_study', 'غير متاح - N/A')} | |
| - **النتائج - Findings:** {report.get('findings', 'غير متاح - N/A')} | |
| """) | |
| # PDF download button with modern styling (always use backend endpoint) | |
| if 'name' in report: | |
| pdf_url = f"{FASTAPI_BASE_URL}/reports/{report.get('name')}/pdf" | |
| st.markdown(f""" | |
| <div style="text-align: center; margin-top: 1rem;"> | |
| <a href="{pdf_url}" target="_blank" style=" | |
| display: inline-block; | |
| background: linear-gradient(135deg, #2c5aa0, #4a90e2); | |
| color: white; | |
| padding: 0.5rem 1.5rem; | |
| border-radius: 20px; | |
| text-decoration: none; | |
| font-weight: 600; | |
| box-shadow: 0 4px 15px rgba(44, 90, 160, 0.3); | |
| ">📄 تحميل التقرير - Download PDF Report</a> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| def show_all_reports(): | |
| """Show all reports using the FastAPI backend""" | |
| with st.spinner("📋 Loading all reports..."): | |
| try: | |
| response = requests.get(f"{REPORTS_ENDPOINT}/") | |
| if response.status_code == 200: | |
| data = response.json() | |
| reports = data.get('reports', []) | |
| count = data.get('count', 0) | |
| if reports: | |
| st.success(f"✅ Found {count} total report(s) in the system") | |
| # Create a summary table | |
| if reports: | |
| df_data = [] | |
| for report in reports: | |
| df_data.append({ | |
| 'Patient Name': report.get('name', 'N/A'), | |
| 'Date of Birth': report.get('date_of_birth', 'N/A'), | |
| 'Gender': report.get('gender', 'N/A'), | |
| 'Study Date': report.get('date_of_study', 'N/A'), | |
| 'Physician': report.get('referring_physician', 'N/A') | |
| }) | |
| df = pd.DataFrame(df_data) | |
| st.dataframe(df, use_container_width=True) | |
| # Show detailed reports | |
| st.subheader("Detailed Reports") | |
| for i, report in enumerate(reports): | |
| with st.expander(f"{report.get('name', 'Unknown')} - {report.get('date_of_study', 'Unknown Date')}"): | |
| display_report_details(report) | |
| else: | |
| st.info("📋 No reports found in the system yet.") | |
| else: | |
| st.error(f"❌ Error retrieving reports: {response.text}") | |
| except requests.exceptions.ConnectionError: | |
| st.error("❌ Cannot connect to the backend server. Please make sure the FastAPI server is running.") | |
| except Exception as e: | |
| st.error(f"❌ An error occurred: {str(e)}") | |
| def system_status_page(): | |
| """System status and connectivity check page""" | |
| st.markdown('<div class="section-header">⚙️ System Status & Diagnostics</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="info-card">', unsafe_allow_html=True) | |
| st.markdown("### 🔍 Backend Service Status") | |
| col1, col2 = st.columns([3, 1]) | |
| with col2: | |
| if st.button("🔄 Check Status", use_container_width=True): | |
| check_backend_status() | |
| with col1: | |
| st.info("Click 'Check Status' to test backend connectivity") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Show system information | |
| st.markdown('<div class="info-card">', unsafe_allow_html=True) | |
| st.markdown("### 📊 System Information") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.metric("Frontend", "Streamlit", "Running ✅") | |
| st.metric("Backend URL", FASTAPI_BASE_URL, "Configured") | |
| with col2: | |
| st.metric("Upload Endpoint", "/analyze/", "Ready") | |
| st.metric("Reports Endpoint", "/reports", "Ready") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| def check_backend_status(): | |
| """Check if backend service is running and accessible""" | |
| try: | |
| with st.spinner("Checking backend connectivity..."): | |
| # Test health endpoint | |
| response = requests.get(f"{FASTAPI_BASE_URL}/health", timeout=10) | |
| if response.status_code == 200: | |
| data = response.json() | |
| st.success(f"✅ Backend service is healthy!") | |
| st.json(data) | |
| else: | |
| st.error(f"❌ Backend returned status {response.status_code}") | |
| except requests.exceptions.ConnectionError: | |
| st.error("❌ Cannot connect to backend service") | |
| st.info("💡 Make sure the backend is running:") | |
| st.code("cd backend && python service.py --serve") | |
| except requests.exceptions.Timeout: | |
| st.error("❌ Backend request timed out") | |
| except Exception as e: | |
| st.error(f"❌ Unexpected error: {str(e)}") | |
| def stream_response(thinking_text, response_text, patient_info): | |
| """ | |
| Streams the AI response with proper formatting and bullet points. | |
| """ | |
| # --- Helper function for formatting --- | |
| def format_text_to_html(text): | |
| """Converts plain text with newlines and list-like structures to HTML, keeping section headers as normal text.""" | |
| section_headers = [ | |
| "Initial Assessment:", | |
| "Key Findings:", | |
| "Clinical Significance:", | |
| "Impression:", | |
| "Conclusion:", | |
| "Recommendations:", | |
| "Technical Details:", | |
| "Comparison:", | |
| "History:", | |
| "Exam Type:", | |
| "Findings:", | |
| "Clinical History:", | |
| "Technique:", | |
| "Indication:", | |
| "Result:", | |
| "Results:", | |
| "Summary:", | |
| "Assessment:", | |
| "Plan:", | |
| "Follow-up:", | |
| "Brief Structured Report:", | |
| "Report:", | |
| "Diagnosis:", | |
| "Opinion:" | |
| ] | |
| text = html.escape(text) | |
| lines = text.split('\n') | |
| html_lines = [] | |
| in_list = False | |
| for line in lines: | |
| stripped_line = line.strip() | |
| # Remove leading bullets/numbers for header checking | |
| clean_line = re.sub(r'^[\d+\.]*[\-\*\•]?\s*', '', stripped_line) | |
| # Also try removing just the number prefix for numbered headers | |
| clean_line_no_number = re.sub(r'^\d+\.?\s*', '', stripped_line) | |
| # Check if this is a section header (case-insensitive, flexible matching) | |
| is_section_header = any( | |
| clean_line.lower().startswith(header.lower()) or | |
| clean_line.lower() == header.lower().rstrip(':') or | |
| clean_line_no_number.lower().startswith(header.lower()) or | |
| clean_line_no_number.lower() == header.lower().rstrip(':') or | |
| # Handle cases like "EXAM TYPE:" matching "Exam Type:" | |
| clean_line.lower().replace(' ', '').startswith(header.lower().replace(' ', '').rstrip(':')) or | |
| clean_line_no_number.lower().replace(' ', '').startswith(header.lower().replace(' ', '').rstrip(':')) | |
| for header in section_headers | |
| ) | |
| if is_section_header: | |
| # Close any open list | |
| if in_list: | |
| html_lines.append('</ol>' if 'ol>' in str(html_lines[-3:]) else '</ul>') | |
| in_list = False | |
| # Use the version without numbers/bullets for display | |
| display_text = clean_line_no_number if clean_line_no_number.lower().endswith(':') else clean_line | |
| html_lines.append(f'<p><strong>{display_text}</strong></p>') | |
| elif re.match(r'^[\-\*\•]\s', stripped_line) and not is_section_header: | |
| # This is a bullet point (not a section header) | |
| if not in_list: | |
| html_lines.append('<ul>') | |
| in_list = True | |
| item_text = re.sub(r'^[\-\*\•]\s*', '', stripped_line) | |
| html_lines.append(f'<li>{item_text}</li>') | |
| elif re.match(r'^\d+\.?\s', stripped_line) and not is_section_header: | |
| # This is a numbered list item (not a section header) | |
| if not in_list: | |
| html_lines.append('<ul>') # Convert numbered lists to bullet lists for consistency | |
| in_list = True | |
| item_text = re.sub(r'^\d+\.?\s*', '', stripped_line) | |
| html_lines.append(f'<li>{item_text}</li>') | |
| else: | |
| # Regular text line | |
| if in_list: | |
| html_lines.append('</ul>') | |
| in_list = False | |
| if stripped_line: | |
| html_lines.append(f'<p>{line}</p>') | |
| else: | |
| html_lines.append('<br>') # Preserve empty lines as breaks | |
| # Close any remaining open list | |
| if in_list: | |
| html_lines.append('</ul>') | |
| return ''.join(html_lines) | |
| # --- Thinking Process --- | |
| thinking_container = st.empty() | |
| thinking_container.markdown(""" | |
| <div class="thinking-indicator"> | |
| <span>AI is analyzing</span> | |
| <div class="thinking-dot"></div><div class="thinking-dot"></div><div class="thinking-dot"></div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| 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) | |
| thinking_output = st.empty() | |
| thinking_text_container = "" | |
| for char in thinking_text: | |
| thinking_text_container += char | |
| formatted_thinking = format_text_to_html(thinking_text_container) | |
| thinking_output.markdown(f'<div class="ai-thinking">{formatted_thinking}</div>', unsafe_allow_html=True) | |
| time.sleep(0.005) | |
| thinking_container.empty() | |
| # --- Define Report Components --- | |
| current_date = datetime.now().strftime("%B %d, %Y") | |
| current_time = datetime.now().strftime("%I:%M %p") | |
| report_header = ( | |
| '<div class="report-header">' | |
| '<div class="hospital-info">' | |
| '<div class="hospital-name">🔬 MediVision Radiology Center</div>' | |
| '<div style="font-size: 0.85em; color: #666;">Advanced AI-Powered Medical Imaging Analysis</div>' | |
| '</div>' | |
| '<h2><span>📋 RADIOLOGY REPORT</span></h2>' | |
| '<div class="patient-details">' | |
| f'<div class="patient-detail-item"><span class="detail-label">Patient Name:</span><span class="detail-value">{patient_info["name"]}</span></div>' | |
| f'<div class="patient-detail-item"><span class="detail-label">MRN:</span><span class="detail-value">{patient_info["medical_record_number"]}</span></div>' | |
| 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>' | |
| f'<div class="patient-detail-item"><span class="detail-label">Report Date:</span><span class="detail-value">{current_date} at {current_time}</span></div>' | |
| f'<div class="patient-detail-item"><span class="detail-label">Referring Physician:</span><span class="detail-value">Dr. {patient_info["referring_physician"]}</span></div>' | |
| f'<div class="patient-detail-item"><span class="detail-label">Radiologist:</span><span class="detail-value">MediVision AI Assistant</span></div>' | |
| '</div>' | |
| '</div>' | |
| ) | |
| report_footer = ( | |
| '<div class="report-footer" style="background: #f9f9f9; padding: 15px; border-radius: 5px; margin-top: 20px; text-align: center; font-size: 0.85em; color: #666;">' | |
| f'<strong>Report Generated by:</strong> MediVision AI Assistant<br>' | |
| f'<strong>Supervising Physician:</strong> Dr. {patient_info["referring_physician"]}<br>' | |
| '<strong>Institution:</strong> MediVision Radiology Center<br>' | |
| f'<strong>Generated on:</strong> {current_date} at {current_time}<br>' | |
| '<em>This report has been generated using advanced AI technology and should be reviewed by a qualified radiologist.</em>' | |
| '</div>' | |
| ) | |
| # --- Stream the response --- | |
| report_container = st.empty() | |
| response_text_accumulated = "" | |
| for char in response_text: | |
| response_text_accumulated += char | |
| formatted_content = format_text_to_html(response_text_accumulated) | |
| content_html = ( | |
| '<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);">' | |
| f'{formatted_content}' | |
| '</div>' | |
| ) | |
| full_report_so_far = f'<div class="ai-response">{report_header}{content_html}</div>' | |
| report_container.markdown(full_report_so_far, unsafe_allow_html=True) | |
| time.sleep(0.01) | |
| # Final report with footer | |
| final_formatted_content = format_text_to_html(response_text_accumulated) | |
| final_content_html = ( | |
| '<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);">' | |
| f'{final_formatted_content}' | |
| '</div>' | |
| ) | |
| final_full_report = f'<div class="ai-response">{report_header}{final_content_html}{report_footer}</div>' | |
| report_container.markdown(final_full_report, unsafe_allow_html=True) | |
| if __name__ == "__main__": | |
| main() | |