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 from .patient_history import PatientHistoryManager, ReportGenerator def pil_to_base64(pil_image): """Convert PIL Image to base64 data URL""" import io import base64 from PIL import Image if pil_image is None: return None try: # Convert image to RGB if it's not already 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 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 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 = 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 """ /* =================== 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 for better visualization */ .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 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; } /* 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; } .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 • Images from disk are shown via data URLs for reliable rendering """ import gradio as gr from PIL import Image # ----------------------- helpers (inner) ----------------------- self._patient_choices = [] # list[str] rendered in dropdown self._patient_map = {} # label -> patient_id (int) 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 # already a URL except Exception: return "" def _refresh_patient_dropdown(user_id: int): """Query patient's list and prepare dropdown choices.""" self._patient_choices = [] self._patient_map = {} try: rows = self.patient_history_manager.get_patient_list(user_id) or [] # label starts with id -> stable parse 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) -> Optional[int]: if not label: return None try: return int(str(label).split("•", 1)[0].strip()) except Exception: return None def _fetch_patient_core(pid: int): """Get name/age/gender for an existing patient id.""" row = self.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_id: int) -> Optional[int]: row = self.database_manager.execute_query_one( "SELECT patient_id FROM questionnaire_responses WHERE id=%s LIMIT 1", (resp_id,) ) try: return int(row["patient_id"]) if row and "patient_id" in row else None except Exception: return None def _rows_with_inline_images(rows: list[dict]) -> list[dict]: """Convert local file paths to data URLs so HTML displays them anywhere.""" 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 out # ----------------------- 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=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"""

SmartHeal AI

Advanced Wound Care Analysis & Clinical Support System

""") # Disclaimer gr.HTML("""

⚠️ IMPORTANT DISCLAIMER

This system is for testing/education and not a substitute for clinical judgment.

""") # 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("
Please sign in.
") 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("

📋 Patient Selection

") 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("

🩹 Wound Information

") 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("

⚕️ Clinical Assessment

") 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("

📸 Wound Image

") wound_image = gr.Image(label="Upload Wound Image", type="filepath") gr.HTML("

📝 Medical History

") 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("
") 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("
Organization dashboard coming soon.
") 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: if role == "organization": org_data = { 'org_name': org_name_v, 'email': email, 'phone': phone_v, 'country_code': cc_v, 'department': dept_v, 'location': loc_v } org_id = self.database_manager.create_organization(org_data) else: # For now pick first org (or default) org_id = 1 user_data = { 'username': username, 'email': email, 'password': password, 'name': name, 'role': role, 'org_id': org_id } ok = self.auth_manager.create_user(user_data) if ok: return "
✅ Account created. Please log in.
" return "
❌ Could not create account. Username/email may exist.
" except Exception as e: return f"
❌ Error: {html.escape(str(e))}
" def handle_login(username, password): user = self.auth_manager.authenticate_user(username, password) if not user: return { login_status: "
❌ Invalid credentials.
" } self.current_user = user uid = int(user.get("id")) role = user.get("role") # Preload patient dropdowns for practitioners if role == "practitioner": _refresh_patient_dropdown(uid) info = f"
Welcome, {html.escape(user.get('name','User'))} — {html.escape(role)}
" 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 run_analysis(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): try: if not img_path: return "
❌ Please upload a wound image.
" user_id = int(self.current_user.get("id", 0) or 0) if not user_id: return "
❌ Please login first.
" # Determine patient core fields (ensures same patient_id for existing) if mode == "Existing patient": pid = _label_to_id(existing_label) if not pid: return "
⚠️ Select an existing patient.
" 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 # Build questionnaire payload 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 } # Save questionnaire -> response_id response_id = self.database_manager.save_questionnaire(q_payload) if not response_id: return "
❌ Could not save questionnaire.
" # Resolve patient_id from response (works for new or existing) patient_id = _response_to_patient_id(response_id) if not patient_id: return "
❌ Could not resolve patient ID.
" # Save wound image to DB try: with Image.open(img_path) as pil: pil = pil.convert("RGB") img_meta = self.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 analyzer questionnaire dict 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 } # Run AI analysis_result = self.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"
❌ AI Analysis failed: {html.escape(str(err))}
" # Persist AI analysis (ties back to template via response->questionnaire_id) try: self.database_manager.save_analysis(response_id, image_db_id, analysis_result) except Exception as e: logging.error(f"save_analysis error: {e}") # If a new patient was created, refresh dropdowns if mode == "New patient": _refresh_patient_dropdown(user_id) # Render fancy results (this method already converts file paths to data URLs) return self._format_comprehensive_analysis_results( analysis_result, img_path, q_for_ai ) except Exception as e: logging.exception("run_analysis exception") return f"
❌ System error: {html.escape(str(e))}
" def load_history(): try: uid = int(self.current_user.get("id", 0) or 0) if not uid: return "
❌ Please login first.
" rows = self.patient_history_manager.get_user_patient_history(uid) or [] rows = _rows_with_inline_images(rows) return self.patient_history_manager.format_history_for_display(rows) except Exception as e: logging.error(f"load_history error: {e}") return f"
❌ Error: {html.escape(str(e))}
" def do_search(name): try: uid = int(self.current_user.get("id", 0) or 0) if not uid: return "
❌ Please login first.
" if not (name or "").strip(): return "
⚠️ Enter a name to search.
" rows = self.patient_history_manager.search_patient_by_name(uid, name.strip()) or [] rows = _rows_with_inline_images(rows) return self.patient_history_manager.format_patient_data_for_display(rows) except Exception as e: logging.error(f"search error: {e}") return f"
❌ Error: {html.escape(str(e))}
" def view_details(existing_label): try: uid = int(self.current_user.get("id", 0) or 0) if not uid: return "
❌ Please login first.
" pid = _label_to_id(existing_label) if not pid: return "
⚠️ Select a patient.
" rows = self.patient_history_manager.get_wound_progression_by_id(uid, pid) or [] rows = _rows_with_inline_images(rows) return self.patient_history_manager.format_patient_progress_for_display(rows) except Exception as e: logging.error(f"view_details error: {e}") return f"
❌ Error: {html.escape(str(e))}
" # ----------------------- 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] ) analyze_btn.click( run_analysis, 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 ], 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 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: # Extract the core analysis results from AIProcessor success = analysis_result.get('success', False) if not success: error_msg = analysis_result.get('error', 'Unknown error') return f"
❌ Analysis failed: {error_msg}
" visual_analysis = analysis_result.get('visual_analysis', {}) report = analysis_result.get('report', '') saved_image_path = analysis_result.get('saved_image_path', '') # Extract wound metrics wound_type = visual_analysis.get('wound_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) # Get image paths for visualizations 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', '') # Convert images to base64 for embedding original_image_base64 = None detection_image_base64 = None segmentation_image_base64 = None # Original uploaded image 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) # Detection visualization if detection_image_path and os.path.exists(detection_image_path): detection_image_base64 = self.image_to_base64(detection_image_path) # Segmentation visualization if segmentation_image_path and os.path.exists(segmentation_image_path): segmentation_image_base64 = self.image_to_base64(segmentation_image_path) # Generate risk assessment from questionnaire data 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'] # Set risk class for styling risk_class = "low" if risk_level.lower() == "moderate": risk_class = "moderate" elif risk_level.lower() in ["high", "very high"]: risk_class = "high" # Format risk factors risk_factors_html = "" if risk_factors else "

No specific risk factors identified.

" # Create image gallery image_gallery_html = "" if original_image_base64 or detection_image_base64 or segmentation_image_base64: image_gallery_html = '' # Convert markdown report to HTML report_html = "" if report: report_html = self.markdown_to_html(report) # Final comprehensive HTML output html_output = f"""

🔬 SmartHeal AI Comprehensive Analysis

Advanced Computer Vision & Medical AI Assessment

Patient: {questionnaire_data.get('patient_name', 'Unknown')} | Analysis Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

✅ Analysis Status: Analysis completed successfully with comprehensive wound assessment

🖼️ Visual Analysis Gallery

{image_gallery_html}

🔍 Wound Detection & Classification

Wound Type

{wound_type}

Detection Confidence

{detection_confidence:.1%}

Location

{questionnaire_data.get('wound_location', 'Not specified')}

📏 Wound Measurements

Length

{length_cm:.2f} cm

Width

{breadth_cm:.2f} cm

Surface Area

{area_cm2:.2f} cm²

⚠️ Risk Assessment

{risk_level} RISK
Risk Score: {risk_score}/10

Identified Risk Factors:

{risk_factors_html}

👤 Patient Information Summary

Age: {questionnaire_data.get('age', 'Not specified')} years
Gender: {questionnaire_data.get('patient_gender', 'Not specified')}
Diabetic Status: {questionnaire_data.get('diabetic', 'Unknown')}
Pain Level: {questionnaire_data.get('pain_level', 'Not assessed')}/10
Wound Duration: {questionnaire_data.get('wound_duration', 'Not specified')}
Moisture Level: {questionnaire_data.get('moisture', 'Not assessed')}
{f"
Medical History: {questionnaire_data.get('medical_history', 'None provided')}
" if questionnaire_data.get('medical_history') else ""} {f"
Current Medications: {questionnaire_data.get('medications', 'None listed')}
" if questionnaire_data.get('medications') else ""} {f"
Known Allergies: {questionnaire_data.get('allergies', 'None listed')}
" if questionnaire_data.get('allergies') else ""}
{f'

🤖 AI-Generated Clinical Report

{report_html}
' if report_html else ''}

⚠️ Important Medical Disclaimers

  • Not a Medical Diagnosis: This AI analysis is for informational purposes only and does not constitute medical advice, diagnosis, or treatment.
  • Professional Consultation Required: Always consult with qualified healthcare professionals for proper clinical assessment and treatment decisions.
  • Measurement Accuracy: All measurements are estimates based on computer vision algorithms and should be verified with clinical tools.
  • Risk Assessment Limitations: Risk factors are based on provided information and may not reflect the complete clinical picture.

🏥 Analysis completed by SmartHeal AI - Advanced Wound Care Assistant
Report generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')}

""" return html_output except Exception as e: logging.error(f"Error formatting comprehensive results: {e}") return f"
❌ Error displaying results: {str(e)}
" 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 assessment 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 # Diabetic status diabetic_status = str(questionnaire_data.get('diabetic', '')).lower() if 'yes' in diabetic_status: risk_factors.append("Diabetes mellitus") risk_score += 3 # Infection signs infection = str(questionnaire_data.get('infection', '')).lower() if 'yes' in infection: risk_factors.append("Signs of infection present") risk_score += 3 # Pain level 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 # Wound 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 level 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 analysis 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 # Determine 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'] }