SmartHeal-Agentic-AI / src /ui_components_original.py
SmartHeal's picture
Update src/ui_components_original.py
ade675f verified
raw
history blame
61 kB
# src/ui_components_original.py
import gradio as gr
import os
import re
import logging
import base64
from datetime import datetime
from PIL import Image
import html
from typing import Optional, Dict, Any
# ---- Safe imports for local vs package execution ----
try:
from .patient_history import PatientHistoryManager, ReportGenerator
except Exception:
from patient_history import PatientHistoryManager, ReportGenerator # local dev
# ---- Optional spaces.GPU fallback (local dev) ----
try:
import spaces
def _SPACES_GPU(*args, **kwargs):
return spaces.GPU(*args, **kwargs)
except Exception:
def _SPACES_GPU(*_args, **_kwargs):
def deco(f):
return f
return deco
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
def pil_to_base64(pil_image: Image.Image) -> Optional[str]:
"""Convert PIL Image to base64 data URL"""
import io
if pil_image is None:
return None
try:
if pil_image.mode != 'RGB':
pil_image = pil_image.convert('RGB')
buffer = io.BytesIO()
pil_image.save(buffer, format='PNG')
img_str = base64.b64encode(buffer.getvalue()).decode()
return f"data:image/png;base64,{img_str}"
except Exception as e:
logging.error(f"Error converting PIL image to base64: {e}")
return None
# =============================================================================
# GPU-DECORATED FUNCTION (STANDALONE)
# =============================================================================
@_SPACES_GPU(enable_queue=True)
def standalone_run_analysis(
# instance/context
ui_instance,
current_user: Dict[str, Any],
database_manager,
wound_analyzer,
# UI inputs
mode, existing_label,
np_name, np_age, np_gender,
w_loc, w_dur, pain, moist, infect, diabetic,
prev_tx, med_hist, meds, alls, notes, img_path,
manual_annotation=None
):
"""Runs in the ZeroGPU worker; returns HTML for the UI."""
def _label_to_id(label: str):
if not label:
return None
try:
return int(str(label).split("•", 1)[0].strip())
except Exception:
return None
def _fetch_patient_core(pid: int):
row = database_manager.execute_query_one(
"SELECT id, name, age, gender FROM patients WHERE id=%s LIMIT 1", (pid,)
)
return row or {}
def _response_to_patient_id(resp_ref):
if isinstance(resp_ref, dict):
pid = resp_ref.get("patient_id")
if pid is not None:
try:
return int(pid)
except Exception:
pass
resp_id = resp_ref.get("response_id") or resp_ref.get("id")
else:
resp_id = resp_ref
if not resp_id:
return None
row = database_manager.execute_query_one(
"SELECT patient_id FROM questionnaire_responses WHERE id=%s LIMIT 1",
(int(resp_id),)
)
try:
return int(row["patient_id"]) if row and "patient_id" in row else None
except Exception:
return None
try:
if not img_path:
return "<div class='status-error'>❌ Please upload a wound image.</div>"
user_id = int(current_user.get("id", 0) or 0)
if not user_id:
return "<div class='status-error'>❌ Please login first.</div>"
# Resolve patient
if mode == "Existing patient":
pid = _label_to_id(existing_label)
if not pid:
return "<div class='status-warning'>⚠️ Select an existing patient.</div>"
pcore = _fetch_patient_core(pid)
patient_name_v = pcore.get("name")
patient_age_v = pcore.get("age")
patient_gender_v = pcore.get("gender")
else:
patient_name_v = np_name
patient_age_v = np_age
patient_gender_v = np_gender
# Save questionnaire
q_payload = {
'user_id': user_id,
'patient_name': patient_name_v,
'patient_age': patient_age_v,
'patient_gender': patient_gender_v,
'wound_location': w_loc,
'wound_duration': w_dur,
'pain_level': pain,
'moisture_level': moist,
'infection_signs': infect,
'diabetic_status': diabetic,
'previous_treatment': prev_tx,
'medical_history': med_hist,
'medications': meds,
'allergies': alls,
'additional_notes': notes
}
response_id = database_manager.save_questionnaire(q_payload)
# normalize
response_id = (response_id.get("response_id") if isinstance(response_id, dict) else response_id)
try:
response_id = int(response_id)
except Exception:
return "<div class='status-error'>❌ Could not resolve response ID.</div>"
patient_id = _response_to_patient_id(response_id)
if not patient_id:
return "<div class='status-error'>❌ Could not resolve patient ID.</div>"
# Save wound image binary
try:
with Image.open(img_path) as pil:
pil = pil.convert("RGB")
img_meta = database_manager.save_wound_image(patient_id, pil)
image_db_id = img_meta["id"] if img_meta else None
except Exception as e:
logging.error(f"save_wound_image error: {e}")
image_db_id = None
# Prepare AI inputs
q_for_ai = {
'age': patient_age_v,
'diabetic': 'Yes' if diabetic != 'Non-diabetic' else 'No',
'allergies': alls,
'date_of_injury': 'Unknown',
'professional_care': 'Yes',
'oozing_bleeding': 'Minor Oozing' if infect != 'None' else 'None',
'infection': 'Yes' if infect != 'None' else 'No',
'moisture': moist,
'patient_name': patient_name_v,
'patient_gender': patient_gender_v,
'wound_location': w_loc,
'wound_duration': w_dur,
'pain_level': pain,
'previous_treatment': prev_tx,
'medical_history': med_hist,
'medications': meds,
'additional_notes': notes
}
# Include manual annotation (mask) in questionnaire for AI
if manual_annotation is not None:
q_for_ai['manual_mask'] = manual_annotation
# Run AI
analysis_result = wound_analyzer.analyze_wound(img_path, q_for_ai)
if not analysis_result or not analysis_result.get("success"):
err = (analysis_result or {}).get("error", "Unknown analysis error")
return f"<div class='status-error'>❌ AI Analysis failed: {html.escape(str(err))}</div>"
# Persist AI analysis
try:
database_manager.save_analysis(response_id, image_db_id, analysis_result)
except Exception as e:
logging.error(f"save_analysis error: {e}")
# Format via instance method to keep UI consistent
return ui_instance._format_comprehensive_analysis_results(
analysis_result, img_path, q_for_ai
)
except Exception as e:
logging.exception("standalone_run_analysis exception")
return f"<div class='status-error'>❌ System error in GPU worker: {html.escape(str(e))}</div>"
# =============================================================================
# UI CLASS DEFINITION
# =============================================================================
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()
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"
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
html_text = html.escape(markdown_text)
# Headers
html_text = re.sub(r"^### (.*?)$", r"<h3>\1</h3>", html_text, flags=re.MULTILINE)
html_text = re.sub(r"^## (.*?)$", r"<h2>\1</h2>", html_text, flags=re.MULTILINE)
html_text = re.sub(r"^# (.*?)$", r"<h1>\1</h1>", html_text, flags=re.MULTILINE)
# Bold, italic
html_text = re.sub(r"\*\*(.*?)\*\*", r"<strong>\1</strong>", html_text)
html_text = re.sub(r"\*(.*?)\*", r"<em>\1</em>", html_text)
# Code blocks
html_text = re.sub(r"```(.*?)```", r"<pre><code>\1</code></pre>", html_text, flags=re.DOTALL)
# Inline code
html_text = re.sub(r"`(.*?)`", r"<code>\1</code>", html_text)
# Blockquotes
html_text = re.sub(r"^> (.*?)$", r"<blockquote>\1</blockquote>", html_text, flags=re.MULTILINE)
# Links
html_text = re.sub(r"\[(.*?)\]\((.*?)\)", r"<a href=\"\2\">\1</a>", html_text)
# Horizontal rules
html_text = re.sub(r"^\s*[-*_]{3,}\s*$", r"<hr>", html_text, flags=re.MULTILINE)
# Bullet points to <ul>
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("<ul>")
in_list = True
result_lines.append(f"<li>{stripped[2:]}</li>")
else:
if in_list:
result_lines.append("</ul>")
in_list = False
if stripped:
result_lines.append(f"<p>{stripped}</p>")
else:
result_lines.append("<br>")
if in_list:
result_lines.append("</ul>")
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 """
/* =================== 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;
}
.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;
}
/* 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;
}
/* 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: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;
}
/* 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;
}
/* Image gallery styling */
.image-gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.image-item { background: #f8f9fa; border-radius: 12px; padding: 15px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); text-align: center; }
.image-item img { max-width: 100%; height: auto; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
.image-item h4 { margin: 15px 0 5px 0; color: #2d3748; font-weight: 600; }
.image-item p { margin: 0; color: #666; font-size: 0.9em; }
/* Analyze button */
#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;
}
/* Responsive */
@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; }
.image-gallery { grid-template-columns: 1fr; }
}
"""
def create_interface(self):
"""
SmartHeal UI – aligned with current DB + history manager:
• Login (practitioner / organization)
• Practitioner: Wound Analysis (existing vs new patient), Patient History, View Details
"""
import gradio as gr
from PIL import Image
import os, html, logging
# ----------------------- helpers (inner) -----------------------
self._patient_choices = [] # list[str] labels in dropdown
self._patient_map = {} # label -> patient_id
def _to_data_url_if_local(path_or_url: str) -> str:
if not path_or_url:
return ""
try:
if os.path.exists(path_or_url):
return self.image_to_base64(path_or_url) or ""
return path_or_url
except Exception:
return ""
def _refresh_patient_dropdown(user_id: int):
"""Query patient's list and prepare dropdown choices."""
self._patient_choices.clear()
self._patient_map.clear()
try:
rows = self.patient_history_manager.get_patient_list(user_id) or []
for r in rows:
pid = int(r.get("id") or 0)
nm = r.get("patient_name") or "Unknown"
age = r.get("patient_age") or ""
gen = r.get("patient_gender") or ""
v = int(r.get("total_visits") or 0)
label = f"{pid}{nm} ({age}y {gen}) — visits: {v}"
self._patient_choices.append(label)
self._patient_map[label] = pid
except Exception as e:
logging.error(f"refresh dropdown error: {e}")
def _label_to_id(label: str):
if not label:
return None
try:
return int(str(label).split("•", 1)[0].strip())
except Exception:
return None
def _resolve_org_id_from_dropdown(label: str) -> Optional[int]:
"""
Dropdown items look like: 'Org Name - Location'.
Try to resolve to organizations.id.
"""
if not label:
return None
try:
if " - " in label:
org_name, location = label.split(" - ", 1)
row = self.database_manager.execute_query_one(
"SELECT id FROM organizations WHERE name=%s AND location=%s ORDER BY id DESC LIMIT 1",
(org_name.strip(), location.strip())
)
if row and "id" in row:
return int(row["id"])
else:
row = self.database_manager.execute_query_one(
"SELECT id FROM organizations WHERE name=%s ORDER BY id DESC LIMIT 1",
(label.strip(),)
)
if row and "id" in row:
return int(row["id"])
except Exception as e:
logging.error(f"resolve org id error: {e}")
return None
# ----------------------- Blocks UI -----------------------
with gr.Blocks(css=self.get_custom_css(), title="SmartHeal - AI Wound Care Assistant") as app:
# Header
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=5F0FMH9ni8QQ7kNvwHM_7v-&_nc_oc=AdnDo4fj3kdh7ShWq75N3ZEXKuGjbAu9-xZpx6bd82Vo4w0y6D-iHL64ETyW4lWod7s&_nc_zt=23&_nc_ht=scontent.fccu31-2.fna&_nc_gid=a4EiY054p4ChBMLqHCkaIA&oh=00_AfVn-aHeCy95qNhA--DhvjkWp6qdzowKpPRyJ8jevATOmQ&oe=68B1CF4B"
gr.HTML(f"""
<div class="medical-header">
<img src="{logo_url}" class="logo" alt="SmartHeal Logo">
<div>
<h1>SmartHeal AI</h1>
<p>Advanced Wound Care Analysis & Clinical Support System</p>
</div>
</div>
""")
# Disclaimer
gr.HTML("""
<div style="border:2px solid #FF6B6B;background:#FFE5E5;padding:15px;border-radius:12px;margin:10px 0;">
<h3 style="color:#D63031;margin:0 0 8px 0;">⚠️ IMPORTANT DISCLAIMER</h3>
<p><strong>This system is for testing/education and not a substitute for clinical judgment.</strong></p>
</div>
""")
# Panels: auth vs practitioner vs organization
with gr.Row():
with gr.Column(visible=True) as auth_panel:
with gr.Tabs():
with gr.Tab("🔐 Professional Login"):
login_username = gr.Textbox(label="👤 Username")
login_password = gr.Textbox(label="🔒 Password", type="password")
login_btn = gr.Button("🚀 Sign In", variant="primary")
login_status = gr.HTML("<div class='status-warning'>Please sign in.</div>")
with gr.Tab("📝 New Registration"):
signup_username = gr.Textbox(label="👤 Username")
signup_email = gr.Textbox(label="📧 Email")
signup_password = gr.Textbox(label="🔒 Password", type="password")
signup_name = gr.Textbox(label="👨‍⚕️ Full Name")
signup_role = gr.Radio(["practitioner", "organization"], label="Account Type", value="practitioner")
with gr.Group(visible=False) as org_fields:
org_name = gr.Textbox(label="Organization Name")
phone = gr.Textbox(label="Phone")
country_code = gr.Textbox(label="Country Code")
department = gr.Textbox(label="Department")
location = gr.Textbox(label="Location")
with gr.Group(visible=True) as prac_fields:
organization_dropdown = gr.Dropdown(choices=self.get_organizations_dropdown(), label="Select Organization")
signup_btn = gr.Button("✨ Create Account", variant="primary")
signup_status = gr.HTML()
with gr.Column(visible=False) as practitioner_panel:
user_info = gr.HTML("")
logout_btn_prac = gr.Button("🚪 Logout", variant="secondary")
with gr.Tabs():
# ------------------- WOUND ANALYSIS -------------------
with gr.Tab("🔬 Wound Analysis"):
with gr.Row():
with gr.Column(scale=1):
gr.HTML("<h3>📋 Patient Selection</h3>")
patient_mode = gr.Radio(
["Existing patient", "New patient"],
label="Patient mode",
value="Existing patient"
)
existing_patient_dd = gr.Dropdown(
choices=[],
label="Select existing patient (ID • Name)",
interactive=True
)
with gr.Group(visible=False) as new_patient_group:
new_patient_name = gr.Textbox(label="Patient Name")
new_patient_age = gr.Number(label="Age", value=30, minimum=0, maximum=120)
new_patient_gender = gr.Dropdown(choices=["Male", "Female", "Other"], value="Male", label="Gender")
gr.HTML("<h3>🩹 Wound Information</h3>")
wound_location = gr.Textbox(label="Wound Location", placeholder="e.g., Left ankle")
wound_duration = gr.Textbox(label="Wound Duration", placeholder="e.g., 2 weeks")
pain_level = gr.Slider(0, 10, value=5, step=1, label="Pain Level (0-10)")
gr.HTML("<h3>⚕️ Clinical Assessment</h3>")
moisture_level = gr.Dropdown(["Dry", "Moist", "Wet", "Saturated"], value="Moist", label="Moisture Level")
infection_signs = gr.Dropdown(["None", "Mild", "Moderate", "Severe"], value="None", label="Signs of Infection")
diabetic_status = gr.Dropdown(["Non-diabetic", "Type 1", "Type 2", "Gestational"], value="Non-diabetic", label="Diabetic Status")
with gr.Column(scale=1):
gr.HTML("<h3>📸 Wound Image</h3>")
wound_image = gr.Image(label="Upload Wound Image", type="filepath")
# Manual annotation field: user can draw wound boundary if model fails
gr.HTML("<h3>✏️ Manual Annotation (optional)</h3>")
manual_annotation = gr.Image(label="Draw wound boundary", tool="sketch", type="pil", interactive=True)
gr.HTML("<h3>📝 Medical History</h3>")
previous_treatment = gr.Textbox(label="Previous Treatment", lines=3)
medical_history = gr.Textbox(label="Medical History", lines=3)
medications = gr.Textbox(label="Current Medications", lines=2)
allergies = gr.Textbox(label="Known Allergies", lines=2)
additional_notes = gr.Textbox(label="Additional Notes", lines=3)
analyze_btn = gr.Button("🔬 Analyze Wound", variant="primary", elem_id="analyze-btn")
analysis_output = gr.HTML("")
# ------------------- PATIENT HISTORY -------------------
with gr.Tab("📋 Patient History"):
with gr.Row():
with gr.Column(scale=2):
history_btn = gr.Button("📄 Load Patient History", variant="primary")
patient_history_output = gr.HTML("")
with gr.Column(scale=1):
search_patient_name = gr.Textbox(label="Search patient by name")
search_patient_btn = gr.Button("🔍 Search", variant="secondary")
specific_patient_output = gr.HTML("")
gr.HTML("<hr style='margin:10px 0 6px 0;border:none;border-top:1px solid #e2e8f0'>")
with gr.Row():
view_details_dd = gr.Dropdown(choices=[], label="Select patient to view details")
view_details_btn = gr.Button("📈 View Details (Timeline)", variant="primary")
view_details_output = gr.HTML("")
with gr.Column(visible=False) as organization_panel:
gr.HTML("<div class='status-warning'>Organization dashboard coming soon.</div>")
logout_btn_org = gr.Button("🚪 Logout", variant="secondary")
# ----------------------- handlers -----------------------
def toggle_role_fields(role):
return {
org_fields: gr.update(visible=(role == "organization")),
prac_fields: gr.update(visible=(role != "organization"))
}
def handle_signup(username, email, password, name, role, org_name_v, phone_v, cc_v, dept_v, loc_v, org_dropdown):
try:
organization_id = None
if role == "practitioner":
organization_id = _resolve_org_id_from_dropdown(org_dropdown)
ok = self.auth_manager.create_user(
username=username,
email=email,
password=password,
name=name,
role=role,
org_name=(org_name_v or name) if role == "organization" else "",
phone=phone_v if role == "organization" else "",
country_code=cc_v if role == "organization" else "",
department=dept_v if role == "organization" else "",
location=loc_v if role == "organization" else "",
organization_id=organization_id
)
if ok:
return "<div class='status-success'>✅ Account created. Please log in.</div>"
return "<div class='status-error'>❌ Could not create account. Username/email may exist.</div>"
except Exception as e:
return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
def handle_login(username, password):
user = self.auth_manager.authenticate_user(username, password)
if not user:
return {
login_status: "<div class='status-error'>❌ Invalid credentials.</div>"
}
self.current_user = user
uid = int(user.get("id"))
role = user.get("role")
if role == "practitioner":
_refresh_patient_dropdown(uid)
info = f"<div class='status-success'>Welcome, <strong>{html.escape(user.get('name','User'))}</strong> — {html.escape(role)}</div>"
updates = {login_status: info}
if role == "practitioner":
updates.update({
auth_panel: gr.update(visible=False),
practitioner_panel: gr.update(visible=True),
user_info: info,
existing_patient_dd: gr.update(choices=self._patient_choices),
view_details_dd: gr.update(choices=self._patient_choices),
})
else:
updates.update({
auth_panel: gr.update(visible=False),
organization_panel: gr.update(visible=True),
})
return updates
def handle_logout():
self.current_user = {}
return {
auth_panel: gr.update(visible=True),
practitioner_panel: gr.update(visible=False),
organization_panel: gr.update(visible=False)
}
def on_patient_mode_change(mode):
return {
new_patient_group: gr.update(visible=(mode == "New patient")),
existing_patient_dd: gr.update(interactive=(mode == "Existing patient"))
}
def load_history():
try:
uid = int(self.current_user.get("id", 0) or 0)
if not uid:
return "<div class='status-error'>❌ Please login first.</div>"
rows = self.patient_history_manager.get_user_patient_history(uid) or []
# inline images
out = []
for r in rows or []:
r = dict(r)
if r.get("image_url"):
r["image_url"] = _to_data_url_if_local(r["image_url"])
out.append(r)
return self.patient_history_manager.format_history_for_display(out)
except Exception as e:
logging.error(f"load_history error: {e}")
return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
def do_search(name):
try:
uid = int(self.current_user.get("id", 0) or 0)
if not uid:
return "<div class='status-error'>❌ Please login first.</div>"
if not (name or "").strip():
return "<div class='status-warning'>⚠️ Enter a name to search.</div>"
rows = self.patient_history_manager.search_patient_by_name(uid, name.strip()) or []
out = []
for r in rows or []:
r = dict(r)
if r.get("image_url"):
r["image_url"] = _to_data_url_if_local(r["image_url"])
out.append(r)
return self.patient_history_manager.format_patient_data_for_display(out)
except Exception as e:
logging.error(f"search error: {e}")
return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
def view_details(existing_label):
try:
uid = int(self.current_user.get("id", 0) or 0)
if not uid:
return "<div class='status-error'>❌ Please login first.</div>"
pid = _label_to_id(existing_label)
if not pid:
return "<div class='status-warning'>⚠️ Select a patient.</div>"
rows = self.patient_history_manager.get_wound_progression_by_id(uid, pid) or []
out = []
for r in rows or []:
r = dict(r)
if r.get("image_url"):
r["image_url"] = _to_data_url_if_local(r["image_url"])
out.append(r)
return self.patient_history_manager.format_patient_progress_for_display(out)
except Exception as e:
logging.error(f"view_details error: {e}")
return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
# ----------------------- wiring -----------------------
signup_role.change(
toggle_role_fields,
inputs=[signup_role],
outputs=[org_fields, prac_fields]
)
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]
)
login_btn.click(
handle_login,
inputs=[login_username, login_password],
outputs=[login_status, auth_panel, practitioner_panel, organization_panel,
user_info, existing_patient_dd, view_details_dd]
)
logout_btn_prac.click(handle_logout, outputs=[auth_panel, practitioner_panel, organization_panel])
logout_btn_org.click(handle_logout, outputs=[auth_panel, practitioner_panel, organization_panel])
patient_mode.change(
on_patient_mode_change,
inputs=[patient_mode],
outputs=[new_patient_group, existing_patient_dd]
)
# Keep manual annotation preview in sync with uploaded image
def _update_annotation_preview(path):
try:
if not path:
return None
from PIL import Image
return Image.open(path).convert("RGB")
except Exception:
return None
wound_image.change(
_update_annotation_preview,
inputs=[wound_image],
outputs=[manual_annotation]
)
# --- IMPORTANT: call standalone GPU function via lambda to pass instance/ctx ---
analyze_btn.click(
fn=lambda mode, ex_lbl, np_n, np_a, np_g, wl, wd, p, m, i, d, pt, mh, med, al, nt, img, annot: \
standalone_run_analysis(
self, self.current_user, self.database_manager, self.wound_analyzer,
mode, ex_lbl, np_n, np_a, np_g, wl, wd, p, m, i, d, pt, mh, med, al, nt, img, annot
),
inputs=[
patient_mode, existing_patient_dd,
new_patient_name, new_patient_age, new_patient_gender,
wound_location, wound_duration, pain_level, moisture_level, infection_signs, diabetic_status,
previous_treatment, medical_history, medications, allergies, additional_notes, wound_image, manual_annotation
],
outputs=[analysis_output]
)
history_btn.click(load_history, outputs=[patient_history_output])
search_patient_btn.click(do_search, inputs=[search_patient_name], outputs=[specific_patient_output])
view_details_btn.click(view_details, inputs=[view_details_dd], outputs=[view_details_output])
return app
# ----------------------- formatting & risk logic -----------------------
def _format_comprehensive_analysis_results(self, analysis_result, image_url=None, questionnaire_data=None):
"""Format comprehensive analysis results with all visualization images from AIProcessor."""
try:
success = analysis_result.get('success', False)
if not success:
error_msg = analysis_result.get('error', 'Unknown error')
return f"<div class='status-error'>❌ Analysis failed: {error_msg}</div>"
visual_analysis = analysis_result.get('visual_analysis', {})
report = analysis_result.get('report', '')
saved_image_path = analysis_result.get('saved_image_path', '')
wound_type = visual_analysis.get('wound_type', 'Unknown')
skin_tone_label = visual_analysis.get('skin_tone_label', 'Unknown')
ita_degrees = visual_analysis.get('ita_degrees', 0)
tissue_type = visual_analysis.get('tissue_type', 'Unknown')
length_cm = visual_analysis.get('length_cm', 0)
breadth_cm = visual_analysis.get('breadth_cm', 0)
area_cm2 = visual_analysis.get('surface_area_cm2', 0)
detection_confidence = visual_analysis.get('detection_confidence', 0)
detection_image_path = visual_analysis.get('detection_image_path', '')
segmentation_image_path = visual_analysis.get('segmentation_image_path', '')
original_image_path = visual_analysis.get('original_image_path', '')
original_image_base64 = None
detection_image_base64 = None
segmentation_image_base64 = None
if image_url and os.path.exists(image_url):
original_image_base64 = self.image_to_base64(image_url)
elif original_image_path and os.path.exists(original_image_path):
original_image_base64 = self.image_to_base64(original_image_path)
elif saved_image_path and os.path.exists(saved_image_path):
original_image_base64 = self.image_to_base64(saved_image_path)
if detection_image_path and os.path.exists(detection_image_path):
detection_image_base64 = self.image_to_base64(detection_image_path)
if segmentation_image_path and os.path.exists(segmentation_image_path):
segmentation_image_base64 = self.image_to_base64(segmentation_image_path)
risk_assessment = self._generate_risk_assessment(questionnaire_data)
risk_level = risk_assessment['risk_level']
risk_score = risk_assessment['risk_score']
risk_factors = risk_assessment['risk_factors']
risk_class = "low"
if risk_level.lower() == "moderate":
risk_class = "moderate"
elif risk_level.lower() in ["high", "very high"]:
risk_class = "high"
risk_factors_html = "<ul>" + "".join(f"<li>{factor}</li>" for factor in risk_factors) + "</ul>" if risk_factors else "<p>No specific risk factors identified.</p>"
image_gallery_html = ""
if original_image_base64 or detection_image_base64 or segmentation_image_base64:
image_gallery_html = '<div class="image-gallery">'
if original_image_base64:
image_gallery_html += f'''
<div class="image-item">
<img src="{original_image_base64}" alt="Original Wound Image">
<h4>📸 Original Wound Image</h4>
<p>Uploaded image for analysis</p>
</div>
'''
if detection_image_base64:
image_gallery_html += f'''
<div class="image-item">
<img src="{detection_image_base64}" alt="Wound Detection">
<h4>🎯 Wound Detection</h4>
<p>AI-detected wound boundaries with {detection_confidence:.1%} confidence</p>
</div>
'''
if segmentation_image_base64:
image_gallery_html += f'''
<div class="image-item">
<img src="{segmentation_image_base64}" alt="Wound Segmentation">
<h4>📏 Wound Segmentation</h4>
<p>Detailed wound area measurement and analysis</p>
</div>
'''
image_gallery_html += '</div>'
report_html = self.markdown_to_html(report) if report else ""
html_output = f"""
<div style="max-width: 1200px; margin: 0 auto; background: white; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); overflow: hidden;">
<div style="background: linear-gradient(135deg, #3182ce 0%, #2c5aa0 100%); color: white; padding: 40px; text-align: center;">
<h1 style="margin: 0; font-size: 32px; font-weight: 700;">🔬 SmartHeal AI Comprehensive Analysis</h1>
<p style="margin: 15px 0 0 0; opacity: 0.9; font-size: 18px;">Advanced Computer Vision & Medical AI Assessment</p>
<div style="background: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px; margin-top: 20px;">
<p style="margin: 0; font-size: 16px;"><strong>Patient:</strong> {html.escape(str(questionnaire_data.get('patient_name', 'Unknown')))} | <strong>Analysis Date:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
</div>
</div>
<div style="padding: 40px;">
<div class="status-success" style="margin-bottom: 30px;">
<strong>✅ Analysis Status:</strong> Analysis completed successfully with comprehensive wound assessment
</div>
<!-- Image Gallery Section -->
<div style="margin-bottom: 40px;">
<h2 style="color: #2d3748; font-size: 24px; margin-bottom: 20px; border-bottom: 2px solid #e53e3e; padding-bottom: 10px;">🖼️ Visual Analysis Gallery</h2>
{image_gallery_html}
</div>
<!-- Wound Detection & Classification -->
<div style="background: #f8f9fa; padding: 30px; border-radius: 12px; margin-bottom: 30px;">
<h2 style="color: #2d3748; margin-top: 0;">🔍 Wound Detection & Classification</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0;">
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<h3 style="color: #3182ce; margin: 0 0 10px 0;">Wound Type</h3>
<p style="font-weight: 600; font-size: 18px; color: #2d3748; margin: 0;">{html.escape(str(wound_type))}</p>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<h3 style="color: #3182ce; margin: 0 0 10px 0;">Detection Confidence</h3>
<p style="font-weight: 600; font-size: 18px; color: #2d3748; margin: 0;">{detection_confidence:.1%}</p>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<h3 style="color: #3182ce; margin: 0 0 10px 0;">Location</h3>
<p style="font-weight: 600; font-size: 18px; color: #2d3748; margin: 0;">{html.escape(str(questionnaire_data.get('wound_location', 'Not specified')))}</p>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<h3 style="color: #3182ce; margin: 0 0 10px 0;">Skin Tone</h3>
<p style="font-weight: 600; font-size: 18px; color: #2d3748; margin: 0;">{html.escape(str(skin_tone_label))} ({ita_degrees:.1f}°)</p>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<h3 style="color: #3182ce; margin: 0 0 10px 0;">Tissue Type</h3>
<p style="font-weight: 600; font-size: 18px; color: #2d3748; margin: 0;">{html.escape(str(tissue_type))}</p>
</div>
</div>
</div>
<!-- Wound Measurements -->
<div style="background: #e7f5ff; padding: 30px; border-radius: 12px; margin-bottom: 30px;">
<h2 style="color: #2d3748; margin-top: 0;">📏 Wound Measurements</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; margin: 20px 0;">
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<h3 style="color: #e53e3e; margin: 0 0 10px 0;">Length</h3>
<p style="font-weight: 700; font-size: 24px; color: #2d3748; margin: 0;">{length_cm:.2f} cm</p>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<h3 style="color: #e53e3e; margin: 0 0 10px 0;">Width</h3>
<p style="font-weight: 700; font-size: 24px; color: #2d3748; margin: 0;">{breadth_cm:.2f} cm</p>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<h3 style="color: #e53e3e; margin: 0 0 10px 0;">Surface Area</h3>
<p style="font-weight: 700; font-size: 24px; color: #2d3748; margin: 0;">{area_cm2:.2f} cm²</p>
</div>
</div>
</div>
<!-- Risk Assessment -->
<div style="background: #fff4e6; padding: 30px; border-radius: 12px; margin-bottom: 30px;">
<h2 style="color: #2d3748; margin-top: 0;">⚠️ Risk Assessment</h2>
<div style="display: flex; align-items: center; margin: 20px 0; flex-wrap: wrap;">
<div style="background: {'#d4edda' if risk_class == 'low' else '#fff3cd' if risk_class == 'moderate' else '#f8d7da'};
color: {'#155724' if risk_class == 'low' else '#856404' if risk_class == 'moderate' else '#721c24'};
padding: 15px 30px;
border-radius: 30px;
font-weight: 700;
font-size: 20px;
text-transform: uppercase;
letter-spacing: 1px;
margin-right: 20px;
margin-bottom: 10px;">
{risk_level} RISK
</div>
<div style="font-size: 18px; color: #2d3748;">
<strong>Risk Score:</strong> {risk_score}/10
</div>
</div>
<div style="background: white; padding: 25px; border-radius: 8px; margin: 15px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
<h3 style="margin-top: 0; color: #2d3748;">Identified Risk Factors:</h3>
{risk_factors_html}
</div>
</div>
<!-- Patient Information Summary -->
<div style="background: #f0f8f0; padding: 30px; border-radius: 12px; margin-bottom: 30px;">
<h2 style="color: #2d3748; margin-top: 0;">👤 Patient Information Summary</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px;">
<div><strong>Age:</strong> {html.escape(str(questionnaire_data.get('age', 'Not specified')))} years</div>
<div><strong>Gender:</strong> {html.escape(str(questionnaire_data.get('patient_gender', 'Not specified')))}</div>
<div><strong>Diabetic Status:</strong> {html.escape(str(questionnaire_data.get('diabetic', 'Unknown')))}</div>
<div><strong>Pain Level:</strong> {html.escape(str(questionnaire_data.get('pain_level', 'Not assessed')))} / 10</div>
<div><strong>Wound Duration:</strong> {html.escape(str(questionnaire_data.get('wound_duration', 'Not specified')))}</div>
<div><strong>Moisture Level:</strong> {html.escape(str(questionnaire_data.get('moisture', 'Not assessed')))}</div>
</div>
{f"<div style='margin-top: 20px;'><strong>Medical History:</strong> {html.escape(str(questionnaire_data.get('medical_history', 'None provided')))}</div>" if questionnaire_data.get('medical_history') else ""}
{f"<div style='margin-top: 10px;'><strong>Current Medications:</strong> {html.escape(str(questionnaire_data.get('medications', 'None listed')))}</div>" if questionnaire_data.get('medications') else ""}
{f"<div style='margin-top: 10px;'><strong>Known Allergies:</strong> {html.escape(str(questionnaire_data.get('allergies', 'None listed')))}</div>" if questionnaire_data.get('allergies') else ""}
</div>
<!-- AI Generated Report -->
{f'<div style="background: #f8f9fa; padding: 30px; border-radius: 12px; margin-bottom: 30px;"><h2 style="color: #2d3748; margin-top: 0;">🤖 AI-Generated Clinical Report</h2><div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">{report_html}</div></div>' if report_html else ''}
<!-- Important Disclaimers -->
<div style="background: #fff5f5; border: 2px solid #feb2b2; padding: 25px; border-radius: 12px; margin: 30px 0;">
<h3 style="color: #c53030; margin-top: 0;">⚠️ Important Medical Disclaimers</h3>
<ul style="color: #742a2a; line-height: 1.6;">
<li><strong>Not a Medical Diagnosis:</strong> This AI analysis is for informational purposes only and does not constitute medical advice, diagnosis, or treatment.</li>
<li><strong>Professional Consultation Required:</strong> Always consult with qualified healthcare professionals for proper clinical assessment and treatment decisions.</li>
<li><strong>Measurement Accuracy:</strong> All measurements are estimates based on computer vision algorithms and should be verified with clinical tools.</li>
<li><strong>Risk Assessment Limitations:</strong> Risk factors are based on provided information and may not reflect the complete clinical picture.</li>
</ul>
</div>
<!-- Footer -->
<div style="text-align: center; padding: 30px 0; border-top: 2px solid #e2e8f0; margin-top: 30px;">
<p style="color: #6c757d; font-style: italic; font-size: 16px; margin: 0;">
🏥 Analysis completed by <strong>SmartHeal AI</strong> - Advanced Wound Care Assistant<br>
<small>Report generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')}</small>
</p>
</div>
</div>
</div>
"""
return html_output
except Exception as e:
logging.error(f"Error formatting comprehensive results: {e}")
return f"<div class='status-error'>❌ Error displaying results: {str(e)}</div>"
def _generate_risk_assessment(self, questionnaire_data):
"""Generate risk assessment based on questionnaire data"""
if not questionnaire_data:
return {'risk_level': 'Unknown', 'risk_score': 0, 'risk_factors': []}
risk_factors = []
risk_score = 0
try:
# Age
age = questionnaire_data.get('age', 0)
if isinstance(age, str):
try:
age = int(age)
except ValueError:
age = 0
if age > 65:
risk_factors.append("Advanced age (>65 years)")
risk_score += 2
elif age > 50:
risk_factors.append("Older adult (50-65 years)")
risk_score += 1
# Diabetes
diabetic_status = str(questionnaire_data.get('diabetic', '')).lower()
if 'yes' in diabetic_status:
risk_factors.append("Diabetes mellitus")
risk_score += 3
# Infection
infection = str(questionnaire_data.get('infection', '')).lower()
if 'yes' in infection:
risk_factors.append("Signs of infection present")
risk_score += 3
# Pain
pain_level = questionnaire_data.get('pain_level', 0)
if isinstance(pain_level, str):
try:
pain_level = float(pain_level)
except ValueError:
pain_level = 0
if pain_level >= 7:
risk_factors.append("High pain level (≥7/10)")
risk_score += 2
elif pain_level >= 5:
risk_factors.append("Moderate pain level (5-6/10)")
risk_score += 1
# Duration
duration = str(questionnaire_data.get('wound_duration', '')).lower()
if any(term in duration for term in ['month', 'months', 'year', 'years']):
risk_factors.append("Chronic wound (>4 weeks)")
risk_score += 3
# Moisture
moisture = str(questionnaire_data.get('moisture', '')).lower()
if any(term in moisture for term in ['wet', 'saturated']):
risk_factors.append("Excessive wound exudate")
risk_score += 1
# Medical history
medical_history = str(questionnaire_data.get('medical_history', '')).lower()
if any(term in medical_history for term in ['vascular', 'circulation', 'heart']):
risk_factors.append("Cardiovascular disease")
risk_score += 2
if any(term in medical_history for term in ['immune', 'cancer', 'steroid']):
risk_factors.append("Immune system compromise")
risk_score += 2
if any(term in medical_history for term in ['smoking', 'tobacco']):
risk_factors.append("Smoking history")
risk_score += 2
# Risk level
if risk_score >= 8:
risk_level = "Very High"
elif risk_score >= 6:
risk_level = "High"
elif risk_score >= 3:
risk_level = "Moderate"
else:
risk_level = "Low"
return {
'risk_score': risk_score,
'risk_level': risk_level,
'risk_factors': risk_factors
}
except Exception as e:
logging.error(f"Risk assessment error: {e}")
return {
'risk_score': 0,
'risk_level': 'Unknown',
'risk_factors': ['Unable to assess risk due to data processing error']
}