Spaces:
Running
Running
| 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"<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) | |
| # Convert bold text | |
| html_text = re.sub(r"\*\*(.*?)\*\*", r"<strong>\1</strong>", html_text) | |
| # Convert italic text | |
| html_text = re.sub(r"\*(.*?)\*", r"<em>\1</em>", html_text) | |
| # Convert code blocks (triple backticks) | |
| html_text = re.sub(r"```(.*?)```", r"<pre><code>\1</code></pre>", html_text, flags=re.DOTALL) | |
| # Convert inline code (single backticks) | |
| html_text = re.sub(r"`(.*?)`", r"<code>\1</code>", html_text) | |
| # Convert blockquotes | |
| html_text = re.sub(r"^> (.*?)$", r"<blockquote>\1</blockquote>", html_text, flags=re.MULTILINE) | |
| # Convert links | |
| html_text = re.sub(r"\[(.*?)\]\((.*?)\)", r"<a href=\"\2\">\1</a>", html_text) | |
| # Convert horizontal rules | |
| html_text = re.sub(r"^\s*[-*_]{3,}\s*$", r"<hr>", 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("<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 """ | |
| /* =================== 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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)" /></svg>') !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""" | |
| <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> | |
| """) | |
| # Professional disclaimer (from original) | |
| gr.HTML(""" | |
| <div style="border: 2px solid #FF6B6B; background-color: #FFE5E5; padding: 15px; border-radius: 12px; margin: 10px 0;"> | |
| <h3 style="color: #D63031; margin-top: 0;">⚠️ IMPORTANT DISCLAIMER</h3> | |
| <p><strong>This model is for testing and educational purposes only and is NOT a replacement for professional medical advice.</strong></p> | |
| <p>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.</p> | |
| <p><em>Uploaded images may be stored and used for testing and model improvement purposes.</em></p> | |
| </div> | |
| """) | |
| # 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(""" | |
| <div style="text-align: center; margin: 40px 0;"> | |
| <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; border-radius: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); max-width: 500px; margin: 0 auto;"> | |
| <h2 style="color: white; font-size: 2.5rem; margin-bottom: 10px; font-weight: 700;">🏥 SmartHeal Access</h2> | |
| <p style="color: rgba(255,255,255,0.9); font-size: 1.1rem; margin-bottom: 30px;">Secure Healthcare Professional Portal</p> | |
| </div> | |
| </div> | |
| """) | |
| with gr.Tabs(): | |
| with gr.Tab("🔐 Professional Login") as login_tab: | |
| gr.HTML(""" | |
| <div style="background: white; padding: 40px; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); margin: 20px auto; max-width: 450px;"> | |
| <div style="text-align: center; margin-bottom: 30px;"> | |
| <h3 style="color: #2d3748; font-size: 1.8rem; margin-bottom: 8px;">Welcome Back</h3> | |
| <p style="color: #718096; font-size: 1rem;">Access your professional dashboard</p> | |
| </div> | |
| </div> | |
| """) | |
| 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="<div style='text-align: center; color: #718096; font-size: 0.9rem; margin-top: 15px;'>Enter your credentials to access the system</div>" | |
| ) | |
| with gr.Tab("📝 New Registration") as signup_tab: | |
| gr.HTML(""" | |
| <div style="background: white; padding: 40px; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); margin: 20px auto; max-width: 450px;"> | |
| <div style="text-align: center; margin-bottom: 30px;"> | |
| <h3 style="color: #2d3748; font-size: 1.8rem; margin-bottom: 8px;">Create Account</h3> | |
| <p style="color: #718096; font-size: 1rem;">Join the SmartHeal healthcare network</p> | |
| </div> | |
| </div> | |
| """) | |
| 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("<h4 style='color: #2d3748; margin: 20px 0 10px 0;'>🏢 Organization Details</h4>") | |
| 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("<h4 style='color: #2d3748; margin: 20px 0 10px 0;'>🏥 Affiliation</h4>") | |
| 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="<div style='text-align: center; color: #718096; font-size: 0.9rem; margin-top: 15px;'>Fill in your details to create an account</div>" | |
| ) | |
| # Practitioner Interface (hidden initially) | |
| with gr.Column(visible=False) as practitioner_panel: | |
| gr.HTML('<div class="medical-card-title">👩⚕️ Practitioner Dashboard</div>') | |
| 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("<h3>📋 Patient Information</h3>") | |
| 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("<h3>🩹 Wound Information</h3>") | |
| 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("<h3>⚕️ Clinical Assessment</h3>") | |
| 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("<h3>📸 Wound Image Upload</h3>") | |
| wound_image = gr.Image( | |
| label="Upload Wound Image", | |
| type="filepath", | |
| elem_classes=["image-upload"] | |
| ) | |
| gr.HTML("<h3>📝 Medical History</h3>") | |
| 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("<h3>📊 Patient History Dashboard</h3>") | |
| history_btn = gr.Button("📋 Load Patient History", variant="primary") | |
| patient_history_output = gr.HTML("") | |
| with gr.Column(scale=1): | |
| gr.HTML("<h3>🔍 Search Specific Patient</h3>") | |
| 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: "<div class='status-success'>✅ Login successful! Welcome to SmartHeal</div>" | |
| } | |
| else: | |
| return { | |
| login_status: "<div class='status-error'>❌ Invalid credentials. Please try again.</div>" | |
| } | |
| 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: "<div class='status-success'>✅ Account created successfully! Please login.</div>" | |
| } | |
| else: | |
| return { | |
| signup_status: "<div class='status-error'>❌ Failed to create account. Username or email may already exist.</div>" | |
| } | |
| except Exception as e: | |
| return { | |
| signup_status: f"<div class='status-error'>❌ Error: {str(e)}</div>" | |
| } | |
| 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 "<div class='status-error'>❌ Please upload a wound image for analysis.</div>" | |
| # Show loading state | |
| loading_html = """ | |
| <div style="text-align:center; padding: 30px;"> | |
| <div style="display:inline-block; border:4px solid #3182ce; border-radius:50%; border-top-color:transparent; width:40px; height:40px; animation:spin 1s linear infinite;"></div> | |
| <p style="margin-top:15px; color:#3182ce; font-weight:600;">Processing wound analysis...</p> | |
| <style>@keyframes spin {0% {transform:rotate(0deg)} 100% {transform:rotate(360deg)}}</style> | |
| </div> | |
| """ | |
| # 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""" | |
| <div class='status-error' style='padding: 20px; background: #ffeeee; border-left: 5px solid #ff5555; margin: 20px 0;'> | |
| <h3>❌ Analysis Error</h3> | |
| <p>There was an error analyzing the wound image:</p> | |
| <pre style='background: #f5f5f5; padding: 10px; border-radius: 5px;'>{str(e)}\n{str(e2) if 'e2' in locals() else ''}</pre> | |
| <p>Please try again with a different image or contact support.</p> | |
| </div> | |
| """ | |
| # 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"<div class='status-error'>❌ Analysis failed: {str(e)}</div>" | |
| 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 "<div class='status-error'>❌ Please login first.</div>" | |
| 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"<div class='status-error'>❌ Error loading history: {str(e)}</div>" | |
| def search_specific_patient(patient_name): | |
| try: | |
| user_id = self.current_user.get('id') | |
| if not user_id: | |
| return "<div class='status-error'>❌ Please login first.</div>" | |
| if not patient_name.strip(): | |
| return "<div class='status-warning'>⚠️ Please enter a patient name to search.</div>" | |
| 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"<div class='status-warning'>⚠️ No records found for patient: {patient_name}</div>" | |
| except Exception as e: | |
| logging.error(f"Error searching patient: {e}") | |
| return f"<div class='status-error'>❌ Error searching patient: {str(e)}</div>" | |
| # 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 = "<ul>" + "".join(f"<li>{factor}</li>" for factor in risk_factors) + "</ul>" if risk_factors else "<p>No specific risk factors identified.</p>" | |
| # Format guideline recommendations | |
| guideline_recommendations = analysis_result.get('guideline_recommendations', []) | |
| recommendations_html = "<ul>" + "".join(f"<li>{rec}</li>" for rec in guideline_recommendations if rec and len(rec) > 10) + "</ul>" if guideline_recommendations else "<p>No specific recommendations available.</p>" | |
| # --- 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""" | |
| <div class="image-visualization"> | |
| <h3>Wound Detection Visualization</h3> | |
| <img src="{detection_image_base64}" alt="Wound Detection" style="max-width: 100%; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin: 10px 0;"> | |
| </div> | |
| """ | |
| detection_html = f""" | |
| <div class="section"> | |
| <h2>🔍 Wound Detection & Classification</h2> | |
| {detection_image_html} | |
| <div class="info-cards" style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin: 20px 0;"> | |
| <div class="info-card"> | |
| <h3>Wound Type</h3> | |
| <p style="font-weight: 600; color: #3182ce;">{wound_type}</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Detection Confidence</h3> | |
| <p>{detection_confidence:.1%}</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Total Wounds Detected</h3> | |
| <p>{wound_detection.get('total_wounds', 0)}</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ if wound_detection.get('status') == 'success' else f""" | |
| <div class="status-error"> | |
| <strong>Detection Status:</strong> Failed<br> | |
| <strong>Reason:</strong> {wound_detection.get('message', 'Unknown error')} | |
| </div> | |
| """ | |
| # --- 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""" | |
| <div class="image-visualization"> | |
| <h3>Wound Segmentation Visualization</h3> | |
| <img src="{segmentation_image_base64}" alt="Wound Segmentation" style="max-width: 100%; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin: 10px 0;"> | |
| </div> | |
| """ | |
| segmentation_html = f""" | |
| <div class="section"> | |
| <h2>📏 Wound Measurements</h2> | |
| {segmentation_image_html} | |
| <div class="info-cards" style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin: 20px 0;"> | |
| <div class="info-card"> | |
| <h3>Length</h3> | |
| <p style="font-weight: 600; color: #3182ce;">{length_cm:.2f} cm</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Width</h3> | |
| <p style="font-weight: 600; color: #3182ce;">{breadth_cm:.2f} cm</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Surface Area</h3> | |
| <p style="font-weight: 600; color: #3182ce;">{area_cm2:.2f} cm²</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ if segmentation_result.get('status') == 'success' else f""" | |
| <div class="status-warning"> | |
| <strong>Segmentation Status:</strong> Failed<br> | |
| <strong>Reason:</strong> {segmentation_result.get('message', 'Unknown error')} | |
| </div> | |
| """ | |
| # --- 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""" | |
| <div class="section"> | |
| <h2>🖼️ Wound Image</h2> | |
| <div style="text-align: center; margin: 20px 0;"> | |
| <img src="{image_base64}" alt="Wound Image" style="max-width: 100%; height: auto; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); margin-bottom: 10px;"> | |
| <p style="margin-top: 10px; color: #666; font-size: 0.9em;"> | |
| Analysis completed successfully | |
| </p> | |
| </div> | |
| </div> | |
| """ | |
| # --- 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""" | |
| <div style="max-width: 900px; margin: 0 auto; background: white; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); overflow: hidden;"> | |
| <div style="background: linear-gradient(135deg, #3182ce 0%, #2c5aa0 100%); color: white; padding: 30px; text-align: center;"> | |
| <h2 style="margin: 0; font-size: 28px; font-weight: 600;">🔬 SmartHeal AI Analysis Results</h2> | |
| <p style="margin: 10px 0 0 0; opacity: 0.9; font-size: 16px;">Advanced Computer Vision & Medical AI Assessment</p> | |
| </div> | |
| <div style="padding: 30px;"> | |
| <div class="status-success" style="margin-bottom: 30px;"> | |
| <strong>Analysis Summary:</strong> {summary} | |
| </div> | |
| {image_visualization} | |
| {detection_html} | |
| {segmentation_html} | |
| <div class="section"> | |
| <h2>⚠️ Risk Assessment</h2> | |
| <div style="display: flex; align-items: center; margin: 20px 0;"> | |
| <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: 12px 24px; | |
| border-radius: 30px; | |
| font-weight: 700; | |
| font-size: 18px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| margin-right: 20px;"> | |
| {risk_level} RISK | |
| </div> | |
| <div> | |
| <strong>Risk Score:</strong> {risk_score}/10 | |
| </div> | |
| </div> | |
| <div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 15px 0;"> | |
| <h3 style="margin-top: 0;">Identified Risk Factors:</h3> | |
| {risk_factors_html} | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <h2>💡 Clinical Recommendations</h2> | |
| <div style="background: #e7f5ff; padding: 20px; border-radius: 8px; border-left: 4px solid #3182ce;"> | |
| {recommendations_html} | |
| </div> | |
| <div style="background: #fff4e6; padding: 15px; border-radius: 8px; margin-top: 20px;"> | |
| <p style="margin: 0; color: #e67700;"> | |
| <strong>⚠️ Note:</strong> These recommendations are generated by AI and should be verified by healthcare professionals. | |
| </p> | |
| </div> | |
| </div> | |
| {f'<div class="section"><h2>📋 Comprehensive Report</h2><div style="background: #f8f9fa; padding: 20px; border-radius: 8px;">{comprehensive_report_html}</div></div>' if comprehensive_report_html else ''} | |
| <hr style="border: 0; height: 1px; background: #e9ecef; margin: 30px 0;"> | |
| <div style="text-align: center; padding: 20px 0;"> | |
| <p style="color: #6c757d; font-style: italic;"> | |
| Analysis completed by SmartHeal AI - Advanced Wound Care Assistant | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| return html_output | |
| except Exception as e: | |
| logging.error(f"Error formatting results: {e}") | |
| return f"<div class='status-error'>❌ Error displaying results: {str(e)}</div>" |