MediVision / src /streamlit_app.py
XshinzoX's picture
Update src/streamlit_app.py
46f7fea verified
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()