import gradio as gr
import os
import re
import logging
import tempfile
import shutil
import base64
from datetime import datetime
from PIL import Image
import html # Import the html module for escaping
from .patient_history import PatientHistoryManager, ReportGenerator
class UIComponents:
def __init__(self, auth_manager, database_manager, wound_analyzer):
self.auth_manager = auth_manager
self.database_manager = database_manager
self.wound_analyzer = wound_analyzer
self.current_user = {}
self.patient_history_manager = PatientHistoryManager(database_manager)
self.report_generator = ReportGenerator()
# Ensure uploads directory exists
if not os.path.exists("uploads"):
os.makedirs("uploads", exist_ok=True)
def image_to_base64(self, image_path):
"""Convert image to base64 data URL for embedding in HTML"""
if not image_path or not os.path.exists(image_path):
return None
try:
with open(image_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode()
# Determine image format
image_ext = os.path.splitext(image_path)[1].lower()
if image_ext in [".jpg", ".jpeg"]:
mime_type = "image/jpeg"
elif image_ext == ".png":
mime_type = "image/png"
elif image_ext == ".gif":
mime_type = "image/gif"
else:
mime_type = "image/png" # Default to PNG
return f"data:{mime_type};base64,{encoded_string}"
except Exception as e:
logging.error(f"Error converting image to base64: {e}")
return None
def markdown_to_html(self, markdown_text):
"""Convert markdown text to proper HTML format with enhanced support"""
if not markdown_text:
return ""
# Escape HTML entities first to prevent issues with special characters
html_text = html.escape(markdown_text)
# Convert headers
html_text = re.sub(r"^### (.*?)$", r"
\1 ", html_text, flags=re.MULTILINE)
html_text = re.sub(r"^## (.*?)$", r"\1 ", html_text, flags=re.MULTILINE)
html_text = re.sub(r"^# (.*?)$", r"\1 ", html_text, flags=re.MULTILINE)
# Convert bold text
html_text = re.sub(r"\*\*(.*?)\*\*", r"\1 ", html_text)
# Convert italic text
html_text = re.sub(r"\*(.*?)\*", r"\1 ", html_text)
# Convert code blocks (triple backticks)
html_text = re.sub(r"```(.*?)```", r"\1 ", html_text, flags=re.DOTALL)
# Convert inline code (single backticks)
html_text = re.sub(r"`(.*?)`", r"\1", html_text)
# Convert blockquotes
html_text = re.sub(r"^> (.*?)$", r"\1 ", html_text, flags=re.MULTILINE)
# Convert links
html_text = re.sub(r"\[(.*?)\]\((.*?)\)", r"\1 ", html_text)
# Convert horizontal rules
html_text = re.sub(r"^\s*[-*_]{3,}\s*$", r" ", html_text, flags=re.MULTILINE)
# Convert bullet points and handle nested lists (simplified for example)
lines = html_text.split("\n")
in_list = False
result_lines = []
for line in lines:
stripped = line.strip()
if stripped.startswith("- "):
if not in_list:
result_lines.append("")
in_list = True
result_lines.append(f"{stripped[2:]} ")
else:
if in_list:
result_lines.append(" ")
in_list = False
if stripped:
result_lines.append(f"{stripped}
")
else:
result_lines.append(" ")
if in_list:
result_lines.append("")
return "\n".join(result_lines)
def get_organizations_dropdown(self):
"""Get list of organizations for dropdown"""
try:
organizations = self.database_manager.get_organizations()
return [f"{org['org_name']} - {org['location']}" for org in organizations]
except Exception as e:
logging.error(f"Error getting organizations: {e}")
return ["Default Hospital - Location"]
def get_custom_css(self):
return """
/* =================== ORIGINAL SMARTHEAL CSS =================== */
/* Global Styling */
body, html {
margin: 0 !important;
padding: 0 !important;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif !important;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
color: #1A202C !important;
line-height: 1.6 !important;
}
/* Professional Header with Logo */
.medical-header {
background: linear-gradient(135deg, #3182ce 0%, #2c5aa0 100%) !important;
color: white !important;
padding: 32px 40px !important;
border-radius: 20px 20px 0 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin-bottom: 0 !important;
box-shadow: 0 10px 40px rgba(49, 130, 206, 0.3) !important;
border: none !important;
position: relative !important;
overflow: hidden !important;
}
.medical-header::before {
content: '' !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
background: url('data:image/svg+xml, ') !important;
opacity: 0.1 !important;
z-index: 1 !important;
}
.medical-header > * {
position: relative !important;
z-index: 2 !important;
}
.logo {
width: 80px !important;
height: 80px !important;
border-radius: 50% !important;
margin-right: 24px !important;
border: 4px solid rgba(255, 255, 255, 0.3) !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2) !important;
background: white !important;
padding: 4px !important;
}
.medical-header h1 {
font-size: 3.5rem !important;
font-weight: 800 !important;
margin: 0 !important;
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3) !important;
background: linear-gradient(45deg, #ffffff, #f8f9fa) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
background-clip: text !important;
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.3)) !important;
}
.medical-header p {
font-size: 1.3rem !important;
margin: 8px 0 0 0 !important;
opacity: 0.95 !important;
font-weight: 500 !important;
text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.2) !important;
}
/* Enhanced Form Styling */
.gr-form {
background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%) !important;
border-radius: 20px !important;
padding: 32px !important;
margin: 24px 0 !important;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.1) !important;
border: 1px solid rgba(229, 62, 62, 0.1) !important;
backdrop-filter: blur(10px) !important;
position: relative !important;
overflow: hidden !important;
}
.gr-form::before {
content: '' !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
height: 4px !important;
background: linear-gradient(90deg, #e53e3e 0%, #f56565 50%, #e53e3e 100%) !important;
z-index: 1 !important;
}
/* Professional Input Fields */
.gr-textbox, .gr-number {
border-radius: 12px !important;
border: 2px solid #E2E8F0 !important;
background: #FFFFFF !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important;
font-size: 1rem !important;
color: #1A202C !important;
padding: 16px 20px !important;
}
.gr-textbox:focus,
.gr-number:focus,
.gr-textbox input:focus,
.gr-number input:focus {
border-color: #E53E3E !important;
box-shadow: 0 0 0 4px rgba(229, 62, 62, 0.1) !important;
background: #FFFFFF !important;
outline: none !important;
transform: translateY(-1px) !important;
}
.gr-textbox input,
.gr-number input {
background: transparent !important;
border: none !important;
outline: none !important;
color: #1A202C !important;
font-size: 1rem !important;
width: 100% !important;
padding: 0 !important;
}
.gr-textbox label,
.gr-number label,
.gr-dropdown label,
.gr-radio label,
.gr-checkbox label {
font-weight: 600 !important;
color: #2D3748 !important;
font-size: 1rem !important;
margin-bottom: 8px !important;
display: block !important;
}
/* Enhanced Button Styling */
button.gr-button,
button.gr-button-primary {
background: linear-gradient(135deg, #E53E3E 0%, #C53030 100%) !important;
color: #FFFFFF !important;
border: none !important;
border-radius: 12px !important;
font-weight: 700 !important;
padding: 16px 32px !important;
font-size: 1.1rem !important;
letter-spacing: 0.5px !important;
text-align: center !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 0 4px 16px rgba(229, 62, 62, 0.3) !important;
position: relative !important;
overflow: hidden !important;
text-transform: uppercase !important;
cursor: pointer !important;
}
button.gr-button::before {
content: '' !important;
position: absolute !important;
top: 0 !important;
left: -100% !important;
width: 100% !important;
height: 100% !important;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent) !important;
transition: left 0.5s !important;
}
button.gr-button:hover::before {
left: 100% !important;
}
button.gr-button:hover,
button.gr-button-primary:hover {
background: linear-gradient(135deg, #C53030 0%, #9C2A2A 100%) !important;
box-shadow: 0 8px 32px rgba(229, 62, 62, 0.4) !important;
transform: translateY(-3px) !important;
}
button.gr-button:active,
button.gr-button-primary:active {
transform: translateY(-1px) !important;
box-shadow: 0 4px 16px rgba(229, 62, 62, 0.5) !important;
}
button.gr-button:disabled {
background: #A0AEC0 !important;
color: #718096 !important;
cursor: not-allowed !important;
box-shadow: none !important;
transform: none !important;
}
/* Professional Status Messages */
.status-success {
background: linear-gradient(135deg, #F0FFF4 0%, #E6FFFA 100%) !important;
border: 2px solid #38A169 !important;
color: #22543D !important;
padding: 20px 24px !important;
border-radius: 16px !important;
font-weight: 600 !important;
margin: 16px 0 !important;
box-shadow: 0 8px 24px rgba(56, 161, 105, 0.2) !important;
backdrop-filter: blur(10px) !important;
}
.status-error {
background: linear-gradient(135deg, #FFF5F5 0%, #FED7D7 100%) !important;
border: 2px solid #E53E3E !important;
color: #742A2A !important;
padding: 20px 24px !important;
border-radius: 16px !important;
font-weight: 600 !important;
margin: 16px 0 !important;
box-shadow: 0 8px 24px rgba(229, 62, 62, 0.2) !important;
backdrop-filter: blur(10px) !important;
}
.status-warning {
background: linear-gradient(135deg, #FFFAF0 0%, #FEEBC8 100%) !important;
border: 2px solid #DD6B20 !important;
color: #9C4221 !important;
padding: 20px 24px !important;
border-radius: 16px !important;
font-weight: 600 !important;
margin: 16px 0 !important;
box-shadow: 0 8px 24px rgba(221, 107, 32, 0.2) !important;
backdrop-filter: blur(10px) !important;
}
/* Professional Card Layout */
.medical-card {
background: linear-gradient(145deg, #FFFFFF 0%, #F7FAFC 100%) !important;
border-radius: 20px !important;
padding: 32px !important;
margin: 24px 0 !important;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.08) !important;
border: 1px solid rgba(229, 62, 62, 0.1) !important;
backdrop-filter: blur(10px) !important;
position: relative !important;
overflow: hidden !important;
}
.medical-card::before {
content: '' !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
height: 4px !important;
background: linear-gradient(90deg, #E53E3E 0%, #F56565 50%, #E53E3E 100%) !important;
}
.medical-card-title {
font-size: 1.75rem !important;
font-weight: 700 !important;
color: #1A202C !important;
margin-bottom: 24px !important;
padding-bottom: 16px !important;
border-bottom: 2px solid #E53E3E !important;
text-align: center !important;
position: relative !important;
}
.medical-card-title::after {
content: '' !important;
position: absolute !important;
bottom: -2px !important;
left: 50% !important;
transform: translateX(-50%) !important;
width: 60px !important;
height: 4px !important;
background: linear-gradient(90deg, transparent, #E53E3E, transparent) !important;
border-radius: 2px !important;
}
/* Professional Dropdown Styling */
.gr-dropdown {
border-radius: 12px !important;
border: 2px solid #E2E8F0 !important;
background: #FFFFFF !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important;
}
.gr-dropdown:focus,
.gr-dropdown select:focus {
border-color: #E53E3E !important;
box-shadow: 0 0 0 4px rgba(229, 62, 62, 0.1) !important;
outline: none !important;
}
.gr-dropdown select {
background: transparent !important;
border: none !important;
color: #1A202C !important;
font-size: 1rem !important;
padding: 16px 20px !important;
border-radius: 12px !important;
}
/* Radio button styling */
.gr-radio input[type="radio"] {
margin-right: 8px !important;
transform: scale(1.2) !important;
}
.gr-radio label {
display: flex !important;
align-items: center !important;
padding: 8px 0 !important;
font-size: 1rem !important;
line-height: 1.5 !important;
cursor: pointer !important;
color: #1A202C !important;
}
/* Tab styling */
.gr-tab {
color: #1A202C !important;
font-weight: 500 !important;
font-size: 1rem !important;
padding: 12px 20px !important;
background-color: #F7FAFC !important;
}
.gr-tab.selected {
color: #E53E3E !important;
font-weight: 600 !important;
border-bottom: 2px solid #E53E3E !important;
background-color: #FFFFFF !important;
}
/* Image upload styling */
.gr-image {
border: 3px dashed #CBD5E0 !important;
border-radius: 16px !important;
background-color: #F7FAFC !important;
transition: all 0.2s ease !important;
}
.gr-image:hover {
border-color: #E53E3E !important;
background-color: #FFF5F5 !important;
}
/* Analyze button special styling */
#analyze-btn {
background: linear-gradient(135deg, #1B5CF3 0%, #1E3A8A 100%) !important;
color: #FFFFFF !important;
border: none !important;
border-radius: 8px !important;
font-weight: 700 !important;
padding: 14px 28px !important;
font-size: 1.1rem !important;
letter-spacing: 0.5px !important;
text-align: center !important;
transition: all 0.2s ease-in-out !important;
}
#analyze-btn:hover {
background: linear-gradient(135deg, #174ea6 0%, #123b82 100%) !important;
box-shadow: 0 4px 14px rgba(27, 95, 193, 0.4) !important;
transform: translateY(-2px) !important;
}
#analyze-btn:disabled {
background: #A0AEC0 !important;
color: #1A202C !important;
cursor: not-allowed !important;
box-shadow: none !important;
transform: none !important;
}
/* Responsive design */
@media (max-width: 768px) {
.medical-header {
padding: 16px !important;
text-align: center !important;
}
.medical-header h1 {
font-size: 2rem !important;
}
.logo {
width: 48px !important;
height: 48px !important;
margin-right: 16px !important;
}
.gr-form {
padding: 16px !important;
margin: 8px 0 !important;
}
button.gr-button,
button.gr-button-primary {
padding: 14px 20px !important;
font-size: 14px !important;
}
}
"""
def create_interface(self):
"""Create the main Gradio interface with original styling and base64 image embedding"""
with gr.Blocks(css=self.get_custom_css(), title="SmartHeal - AI Wound Care Assistant") as app:
# Header with SmartHeal logo (from original)
logo_url = "https://scontent.fccu31-2.fna.fbcdn.net/v/t39.30808-6/275933824_102121829111657_3325198727201325354_n.jpg?_nc_cat=104&ccb=1-7&_nc_sid=6ee11a&_nc_ohc=45krrEUpcSUQ7kNvwGVdiMW&_nc_oc=AdkTdxEC_TkYGiyDkEtTJZ_DFZELW17XKFmWpswmFqGB7JSdvTyWtnrQyLS0USngEiY&_nc_zt=23&_nc_ht=scontent.fccu31-2.fna&_nc_gid=ufAA4Hj5gTRwON5POYzz0Q&oh=00_AfW1-jLEN5RGeggqOvGgEaK_gdg0EDgxf_VhKbZwFLUO0Q&oe=6897A98B"
gr.HTML(f"""
""")
# Professional disclaimer (from original)
gr.HTML("""
⚠️ IMPORTANT DISCLAIMER
This model is for testing and educational purposes only and is NOT a replacement for professional medical advice.
Information generated may be inaccurate. Always consult a qualified healthcare provider for medical concerns. This AI system uses chain-of-thought reasoning to show its decision-making process, but should never be used as the sole basis for clinical decisions.
Uploaded images may be stored and used for testing and model improvement purposes.
""")
# Main interface with conditional visibility (ORIGINAL STRUCTURE)
with gr.Row():
# Professional Authentication Panel (visible when not logged in)
with gr.Column(visible=True) as auth_panel:
gr.HTML("""
🏥 SmartHeal Access
Secure Healthcare Professional Portal
""")
with gr.Tabs():
with gr.Tab("🔐 Professional Login") as login_tab:
gr.HTML("""
Welcome Back
Access your professional dashboard
""")
login_username = gr.Textbox(
label="👤 Username",
placeholder="Enter your username"
)
login_password = gr.Textbox(
label="🔒 Password",
type="password",
placeholder="Enter your secure password"
)
login_btn = gr.Button(
"🚀 Sign In to Dashboard",
variant="primary",
size="lg"
)
login_status = gr.HTML(
value="Enter your credentials to access the system
"
)
with gr.Tab("📝 New Registration") as signup_tab:
gr.HTML("""
Create Account
Join the SmartHeal healthcare network
""")
signup_username = gr.Textbox(
label="👤 Username",
placeholder="Choose a unique username"
)
signup_email = gr.Textbox(
label="📧 Email Address",
placeholder="Enter your professional email"
)
signup_password = gr.Textbox(
label="🔒 Password",
type="password",
placeholder="Create a strong password"
)
signup_name = gr.Textbox(
label="👨⚕️ Full Name",
placeholder="Enter your full professional name"
)
signup_role = gr.Radio(
["practitioner", "organization"],
label="🏥 Account Type",
value="practitioner"
)
# Organization-specific fields
with gr.Group(visible=False) as org_fields:
gr.HTML("🏢 Organization Details ")
org_name = gr.Textbox(label="Organization Name", placeholder="Enter organization name")
phone = gr.Textbox(label="Phone Number", placeholder="Enter contact number")
country_code = gr.Textbox(label="Country Code", placeholder="e.g., +1, +44")
department = gr.Textbox(label="Department", placeholder="e.g., Emergency, Surgery")
location = gr.Textbox(label="Location", placeholder="City, State/Province, Country")
# Practitioner-specific fields
with gr.Group(visible=True) as prac_fields:
gr.HTML("🏥 Affiliation ")
organization_dropdown = gr.Dropdown(
choices=self.get_organizations_dropdown(),
label="Select Your Organization"
)
signup_btn = gr.Button(
"✨ Create Professional Account",
variant="primary",
size="lg"
)
signup_status = gr.HTML(
value="Fill in your details to create an account
"
)
# Practitioner Interface (hidden initially)
with gr.Column(visible=False) as practitioner_panel:
gr.HTML('👩⚕️ Practitioner Dashboard
')
user_info = gr.HTML("")
logout_btn_prac = gr.Button("🚪 Logout", variant="secondary", elem_classes=["logout-btn"])
# Main tabs for different functions
with gr.Tabs():
# WOUND ANALYSIS TAB
with gr.Tab("🔬 Wound Analysis"):
with gr.Row():
with gr.Column(scale=1):
gr.HTML("📋 Patient Information ")
patient_name = gr.Textbox(label="Patient Name", placeholder="Enter patient's full name")
patient_age = gr.Number(label="Age", value=30, minimum=0, maximum=120)
patient_gender = gr.Dropdown(
choices=["Male", "Female", "Other"],
label="Gender",
value="Male"
)
gr.HTML("🩹 Wound Information ")
wound_location = gr.Textbox(label="Wound Location", placeholder="e.g., Left ankle, Right arm")
wound_duration = gr.Textbox(label="Wound Duration", placeholder="e.g., 2 weeks, 1 month")
pain_level = gr.Slider(
minimum=0, maximum=10, value=5, step=1,
label="Pain Level (0-10)"
)
gr.HTML("⚕️ Clinical Assessment ")
moisture_level = gr.Dropdown(
choices=["Dry", "Moist", "Wet", "Saturated"],
label="Moisture Level",
value="Moist"
)
infection_signs = gr.Dropdown(
choices=["None", "Mild", "Moderate", "Severe"],
label="Signs of Infection",
value="None"
)
diabetic_status = gr.Dropdown(
choices=["Non-diabetic", "Type 1", "Type 2", "Gestational"],
label="Diabetic Status",
value="Non-diabetic"
)
with gr.Column(scale=1):
gr.HTML("📸 Wound Image Upload ")
wound_image = gr.Image(
label="Upload Wound Image",
type="filepath",
elem_classes=["image-upload"]
)
gr.HTML("📝 Medical History ")
previous_treatment = gr.Textbox(
label="Previous Treatment",
placeholder="Describe any previous treatments...",
lines=3
)
medical_history = gr.Textbox(
label="Medical History",
placeholder="Relevant medical conditions, surgeries, etc...",
lines=3
)
medications = gr.Textbox(
label="Current Medications",
placeholder="List current medications...",
lines=2
)
allergies = gr.Textbox(
label="Known Allergies",
placeholder="List any known allergies...",
lines=2
)
additional_notes = gr.Textbox(
label="Additional Notes",
placeholder="Any additional clinical observations...",
lines=3
)
analyze_btn = gr.Button("🔬 Analyze Wound", variant="primary", size="lg")
analysis_output = gr.HTML("")
# PATIENT HISTORY TAB
with gr.Tab("📋 Patient History"):
with gr.Row():
with gr.Column(scale=2):
gr.HTML("📊 Patient History Dashboard ")
history_btn = gr.Button("📋 Load Patient History", variant="primary")
patient_history_output = gr.HTML("")
with gr.Column(scale=1):
gr.HTML("🔍 Search Specific Patient ")
search_patient_name = gr.Textbox(
label="Patient Name",
placeholder="Enter patient name to search..."
)
search_patient_btn = gr.Button("🔍 Search Patient History", variant="secondary")
specific_patient_output = gr.HTML("")
# Interface already complete above - no additional tabs needed
# Event handlers
def handle_login(username, password):
user_data = self.auth_manager.authenticate_user(username, password)
if user_data:
self.current_user = user_data
return {
auth_panel: gr.update(visible=False),
practitioner_panel: gr.update(visible=True),
login_status: "✅ Login successful! Welcome to SmartHeal
"
}
else:
return {
login_status: "❌ Invalid credentials. Please try again.
"
}
def handle_signup(username, email, password, name, role, org_name, phone, country_code, department, location, organization_dropdown):
try:
if role == "organization":
org_data = {
'org_name': org_name,
'phone': phone,
'country_code': country_code,
'department': department,
'location': location
}
org_id = self.database_manager.create_organization(org_data)
user_data = {
'username': username,
'email': email,
'password': password,
'name': name,
'role': role,
'org_id': org_id
}
else:
# Extract org_id from dropdown selection
org_id = 1 # Default organization for now
user_data = {
'username': username,
'email': email,
'password': password,
'name': name,
'role': role,
'org_id': org_id
}
if self.auth_manager.create_user(user_data):
return {
signup_status: "✅ Account created successfully! Please login.
"
}
else:
return {
signup_status: "❌ Failed to create account. Username or email may already exist.
"
}
except Exception as e:
return {
signup_status: f"❌ Error: {str(e)}
"
}
def handle_analysis(patient_name, patient_age, patient_gender, wound_location, wound_duration,
pain_level, moisture_level, infection_signs, diabetic_status, previous_treatment,
medical_history, medications, allergies, additional_notes, wound_image):
try:
if not wound_image:
return "❌ Please upload a wound image for analysis.
"
# Show loading state
loading_html = """
Processing wound analysis...
"""
# 1. Construct questionnaire dictionary
questionnaire_data = {
'user_id': self.current_user.get('id'),
'patient_name': patient_name,
'patient_age': patient_age,
'patient_gender': patient_gender,
'wound_location': wound_location,
'wound_duration': wound_duration,
'pain_level': pain_level,
'moisture_level': moisture_level,
'infection_signs': infection_signs,
'diabetic_status': diabetic_status,
'previous_treatment': previous_treatment,
'medical_history': medical_history,
'medications': medications,
'allergies': allergies,
'additional_notes': additional_notes
}
# 2. Save questionnaire in DB
questionnaire_id = self.database_manager.save_questionnaire(questionnaire_data)
# 3. Run AI analysis with uploaded image
try:
# Log information about the wound image
if hasattr(wound_image, 'name'):
logging.info(f"Processing image: {wound_image.name}")
# First try direct analysis with the file-like object
analysis_result = self.wound_analyzer.analyze_wound(wound_image, questionnaire_data)
except Exception as e:
logging.error(f"AI analysis error (first attempt): {e}")
try:
# If that fails, try with PIL image
from PIL import Image
import io
# Reset file pointer if possible
if hasattr(wound_image, 'seek'):
wound_image.seek(0)
# Convert to PIL Image
pil_image = Image.open(wound_image)
analysis_result = self.wound_analyzer.analyze_wound(pil_image, questionnaire_data)
except Exception as e2:
logging.error(f"AI analysis error (second attempt): {e2}")
# Return error information for display
return f"""
❌ Analysis Error
There was an error analyzing the wound image:
{str(e)}\n{str(e2) if 'e2' in locals() else ''}
Please try again with a different image or contact support.
"""
# 4. Save AI analysis result
self.database_manager.save_analysis_result(questionnaire_id, analysis_result)
# 5. Save wound image metadata
if isinstance(wound_image, str):
image_url = wound_image
elif hasattr(wound_image, 'name'):
image_url = wound_image.name
else:
image_url = 'unknown'
image_data = {
'image_url': image_url,
'filename': os.path.basename(image_url),
'file_size': None,
'width': None,
'height': None
}
# 6. Format analysis results with visualization
formatted_analysis = self._format_analysis_results(analysis_result, image_url)
# 7. Generate HTML professional report for complete analysis
professional_report = self.report_generator.generate_analysis_report(
questionnaire_data,
analysis_result,
image_data.get('image_url')
)
return formatted_analysis + professional_report
except Exception as e:
logging.error(f"Analysis error: {e}")
return f"❌ Analysis failed: {str(e)}
"
def handle_logout():
self.current_user = {}
return {
auth_panel: gr.update(visible=True),
practitioner_panel: gr.update(visible=False)
}
def toggle_role_fields(role):
if role == "organization":
return {
org_fields: gr.update(visible=True),
prac_fields: gr.update(visible=False)
}
else:
return {
org_fields: gr.update(visible=False),
prac_fields: gr.update(visible=True)
}
def load_patient_history():
try:
user_id = self.current_user.get('id')
if not user_id:
return "❌ Please login first.
"
history_data = self.patient_history_manager.get_user_patient_history(user_id)
formatted_history = self.patient_history_manager.format_history_for_display(history_data)
return formatted_history
except Exception as e:
logging.error(f"Error loading patient history: {e}")
return f"❌ Error loading history: {str(e)}
"
def search_specific_patient(patient_name):
try:
user_id = self.current_user.get('id')
if not user_id:
return "❌ Please login first.
"
if not patient_name.strip():
return "⚠️ Please enter a patient name to search.
"
patient_data = self.patient_history_manager.search_patient_by_name(user_id, patient_name.strip())
if patient_data:
formatted_data = self.patient_history_manager.format_patient_data_for_display(patient_data)
return formatted_data
else:
return f"⚠️ No records found for patient: {patient_name}
"
except Exception as e:
logging.error(f"Error searching patient: {e}")
return f"❌ Error searching patient: {str(e)}
"
# Bind event handlers
login_btn.click(
handle_login,
inputs=[login_username, login_password],
outputs=[auth_panel, practitioner_panel, login_status]
)
signup_btn.click(
handle_signup,
inputs=[signup_username, signup_email, signup_password, signup_name, signup_role,
org_name, phone, country_code, department, location, organization_dropdown],
outputs=[signup_status]
)
signup_role.change(
toggle_role_fields,
inputs=[signup_role],
outputs=[org_fields, prac_fields]
)
analyze_btn.click(
handle_analysis,
inputs=[patient_name, patient_age, patient_gender, wound_location, wound_duration,
pain_level, moisture_level, infection_signs, diabetic_status, previous_treatment,
medical_history, medications, allergies, additional_notes, wound_image],
outputs=[analysis_output]
)
logout_btn_prac.click(
handle_logout,
outputs=[auth_panel, practitioner_panel]
)
history_btn.click(
load_patient_history,
outputs=[patient_history_output]
)
search_patient_btn.click(
search_specific_patient,
inputs=[search_patient_name],
outputs=[specific_patient_output]
)
return app
def _format_analysis_results(self, analysis_result, image_url=None):
"""Format analysis results for HTML display with base64 encoded images, always showing segmentation overlay."""
try:
# Extract key results
summary = analysis_result.get('summary', 'Analysis completed')
wound_detection = analysis_result.get('wound_detection', {})
segmentation_result = analysis_result.get('segmentation_result', {})
risk_assessment = analysis_result.get('risk_assessment', {})
recommendations = analysis_result.get('recommendations', '')
comprehensive_report = analysis_result.get('comprehensive_report', '')
# Detection metrics
detection_confidence = 0.0
wound_type = "Unknown"
length_cm = breadth_cm = area_cm2 = 0
if wound_detection.get('status') == 'success' and wound_detection.get('detections'):
detections = wound_detection.get('detections', [])
if detections:
detection_confidence = detections[0].get('detection_confidence', 0.0)
wound_type = detections[0].get('wound_type', 'Unknown')
length_cm = detections[0].get('length_cm', 0)
breadth_cm = detections[0].get('breadth_cm', 0)
area_cm2 = detections[0].get('surface_area_cm2', 0)
risk_level = risk_assessment.get('risk_level', 'Unknown')
risk_score = risk_assessment.get('risk_score', 0)
risk_factors = risk_assessment.get('risk_factors', [])
# Set risk class for styling
risk_class = "low"
if risk_level.lower() == "moderate":
risk_class = "moderate"
elif risk_level.lower() == "high":
risk_class = "high"
# Format risk factors
risk_factors_html = "" + "".join(f"{factor} " for factor in risk_factors) + " " if risk_factors else "No specific risk factors identified.
"
# Format guideline recommendations
guideline_recommendations = analysis_result.get('guideline_recommendations', [])
recommendations_html = "" + "".join(f"{rec} " for rec in guideline_recommendations if rec and len(rec) > 10) + " " if guideline_recommendations else "No specific recommendations available.
"
# --- Detection image ---
detection_image_base64 = None
if "overlay_path" in wound_detection and os.path.exists(wound_detection["overlay_path"]):
detection_image_base64 = self.image_to_base64(wound_detection["overlay_path"])
elif comprehensive_report:
detection_match = re.search(r"Detection Image: (.+?)(?:\n|$)", comprehensive_report)
if detection_match and detection_match.group(1) != "Not available" and os.path.exists(detection_match.group(1).strip()):
detection_image_base64 = self.image_to_base64(detection_match.group(1).strip())
detection_image_html = ""
if detection_image_base64:
detection_image_html = f"""
Wound Detection Visualization
"""
detection_html = f"""
🔍 Wound Detection & Classification
{detection_image_html}
Detection Confidence
{detection_confidence:.1%}
Total Wounds Detected
{wound_detection.get('total_wounds', 0)}
""" if wound_detection.get('status') == 'success' else f"""
Detection Status: Failed
Reason: {wound_detection.get('message', 'Unknown error')}
"""
# --- Segmentation overlay: prefer direct result! ---
segmentation_image_base64 = None
if "overlay_pil" in segmentation_result and isinstance(segmentation_result["overlay_pil"], Image.Image):
segmentation_image_base64 = pil_to_base64(segmentation_result["overlay_pil"])
elif "overlay_path" in segmentation_result and os.path.exists(segmentation_result["overlay_path"]):
segmentation_image_base64 = self.image_to_base64(segmentation_result["overlay_path"])
elif comprehensive_report:
segmentation_match = re.search(r"Segmentation Image: (.+?)(?:\n|$)", comprehensive_report)
if segmentation_match and segmentation_match.group(1) != "Not available" and os.path.exists(segmentation_match.group(1).strip()):
segmentation_image_base64 = self.image_to_base64(segmentation_match.group(1).strip())
segmentation_image_html = ""
if segmentation_image_base64:
segmentation_image_html = f"""
Wound Segmentation Visualization
"""
segmentation_html = f"""
📏 Wound Measurements
{segmentation_image_html}
Length
{length_cm:.2f} cm
Width
{breadth_cm:.2f} cm
Surface Area
{area_cm2:.2f} cm²
""" if segmentation_result.get('status') == 'success' else f"""
Segmentation Status: Failed
Reason: {segmentation_result.get('message', 'Unknown error')}
"""
# --- Main input image preview ---
image_visualization = ""
if image_url and os.path.exists(image_url):
image_base64 = self.image_to_base64(image_url)
if image_base64:
image_visualization = f"""
🖼️ Wound Image
Analysis completed successfully
"""
# --- Comprehensive report as HTML ---
comprehensive_report_html = ""
if comprehensive_report:
report_without_images = re.sub(r'## Analysis Images.*?(?=##|$)', '', comprehensive_report, flags=re.DOTALL)
comprehensive_report_html = self.markdown_to_html(report_without_images)
# --- Final Output ---
html_output = f"""
🔬 SmartHeal AI Analysis Results
Advanced Computer Vision & Medical AI Assessment
Analysis Summary: {summary}
{image_visualization}
{detection_html}
{segmentation_html}
⚠️ Risk Assessment
{risk_level} RISK
Risk Score: {risk_score}/10
Identified Risk Factors:
{risk_factors_html}
💡 Clinical Recommendations
{recommendations_html}
⚠️ Note: These recommendations are generated by AI and should be verified by healthcare professionals.
{f'
📋 Comprehensive Report {comprehensive_report_html}
' if comprehensive_report_html else ''}
Analysis completed by SmartHeal AI - Advanced Wound Care Assistant
"""
return html_output
except Exception as e:
logging.error(f"Error formatting results: {e}")
return f"❌ Error displaying results: {str(e)}
"