diff --git "a/src/ui_components_original.py" "b/src/ui_components_original.py" --- "a/src/ui_components_original.py" +++ "b/src/ui_components_original.py" @@ -1,4 +1,4 @@ -# src/ui_components_improved.py +# src/ui_components_original.py import gradio as gr import os @@ -316,501 +316,510 @@ class UIComponents: return ["Default Hospital - Location"] def get_custom_css(self): - # SmartHeal-inspired theme with purple/blue gradient and modern design - return """ -/* =================== SMARTHEAL THEME (Inspired by smartheal.org) =================== */ -:root { - /* Primary colors from SmartHeal */ - --sh-primary: #6366f1; /* indigo-500 */ - --sh-primary-dark: #4f46e5; /* indigo-600 */ - --sh-primary-light: #a5b4fc; /* indigo-300 */ - --sh-secondary: #8b5cf6; /* violet-500 */ - --sh-accent: #06b6d4; /* cyan-500 */ - - /* Neutral colors */ - --sh-gray-50: #f8fafc; - --sh-gray-100: #f1f5f9; - --sh-gray-200: #e2e8f0; - --sh-gray-300: #cbd5e1; - --sh-gray-400: #94a3b8; - --sh-gray-500: #64748b; - --sh-gray-600: #475569; - --sh-gray-700: #334155; - --sh-gray-800: #1e293b; - --sh-gray-900: #0f172a; - - /* Background and surface */ - --sh-bg: #f8fafc; - --sh-surface: #ffffff; - --sh-surface-hover: #f1f5f9; - - /* Shadows and effects */ - --sh-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - --sh-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - --sh-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); - --sh-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); - - /* Border radius */ - --sh-radius-sm: 0.375rem; - --sh-radius: 0.5rem; - --sh-radius-lg: 0.75rem; - --sh-radius-xl: 1rem; - - /* Transitions */ - --sh-transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Global styles */ -html, body, .gradio-container { - background: var(--sh-bg) !important; - color: var(--sh-gray-900) !important; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important; - line-height: 1.6 !important; -} - -/* Header styling inspired by SmartHeal */ -.medical-header { - background: linear-gradient(135deg, var(--sh-primary) 0%, var(--sh-secondary) 100%) !important; - color: white !important; - padding: 2rem 2.5rem !important; - border-radius: var(--sh-radius-xl) !important; - display: flex !important; - align-items: center !important; - justify-content: space-between !important; - margin-bottom: 1.5rem !important; - box-shadow: var(--sh-shadow-xl) !important; - position: relative !important; - overflow: hidden !important; -} - -.medical-header::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: url('data:image/svg+xml,') !important; - opacity: 0.3; -} - -.header-left { - display: flex !important; - align-items: center !important; - gap: 1.5rem !important; - position: relative !important; - z-index: 1 !important; -} - -.logo { - width: 4rem !important; - height: 4rem !important; - border-radius: var(--sh-radius-lg) !important; - background: rgba(255, 255, 255, 0.95) !important; - padding: 0.5rem !important; - box-shadow: var(--sh-shadow-lg) !important; - transition: var(--sh-transition) !important; -} - -.logo:hover { - transform: scale(1.05) !important; - box-shadow: var(--sh-shadow-xl) !important; -} - -.medical-header h1 { - margin: 0 !important; - font-size: 2.25rem !important; - font-weight: 800 !important; - letter-spacing: -0.025em !important; - background: linear-gradient(45deg, #ffffff, #e0e7ff) !important; - -webkit-background-clip: text !important; - -webkit-text-fill-color: transparent !important; - background-clip: text !important; -} - -.medical-header p { - margin: 0.5rem 0 0 0 !important; - opacity: 0.9 !important; - font-weight: 500 !important; - font-size: 1.1rem !important; -} - -.header-nav { - display: flex !important; - gap: 1.5rem !important; - align-items: center !important; - font-weight: 600 !important; - position: relative !important; - z-index: 1 !important; -} - -.header-nav a { - color: rgba(255, 255, 255, 0.9) !important; - text-decoration: none !important; - padding: 0.75rem 1rem !important; - border-radius: var(--sh-radius) !important; - transition: var(--sh-transition) !important; - position: relative !important; - overflow: hidden !important; -} - -.header-nav a::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); - transition: left 0.5s; -} - -.header-nav a:hover { - background: rgba(255, 255, 255, 0.15) !important; - transform: translateY(-2px) !important; - color: white !important; -} - -.header-nav a:hover::before { - left: 100%; -} - -/* Card and form styling */ -.gr-form, .gr-group, .gradio-container .gr-box, .gradio-container .gr-panel { - background: var(--sh-surface) !important; - border-radius: var(--sh-radius-xl) !important; - padding: 2rem !important; - box-shadow: var(--sh-shadow-lg) !important; - border: 1px solid var(--sh-gray-200) !important; - transition: var(--sh-transition) !important; -} - -.gr-form:hover, .gr-group:hover { - box-shadow: var(--sh-shadow-xl) !important; - transform: translateY(-2px) !important; -} - -/* Input styling */ -.gr-textbox, .gr-number, .gr-dropdown, .gr-slider { - border-radius: var(--sh-radius-lg) !important; - border: 2px solid var(--sh-gray-200) !important; - background: var(--sh-surface) !important; - box-shadow: var(--sh-shadow-sm) !important; - transition: var(--sh-transition) !important; - font-size: 1rem !important; - padding: 0.75rem 1rem !important; -} - -.gr-textbox:focus-within, .gr-number:focus-within, .gr-dropdown:focus-within { - border-color: var(--sh-primary) !important; - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1) !important; - transform: translateY(-1px) !important; -} - -/* Button styling */ -button.gr-button, button.gr-button-primary { - background: linear-gradient(135deg, var(--sh-primary) 0%, var(--sh-primary-dark) 100%) !important; - color: white !important; - border: none !important; - border-radius: var(--sh-radius-lg) !important; - font-weight: 700 !important; - padding: 1rem 2rem !important; - letter-spacing: 0.025em !important; - box-shadow: var(--sh-shadow-lg) !important; - transition: var(--sh-transition) !important; - text-transform: none !important; - position: relative !important; - overflow: hidden !important; -} - -button.gr-button::before, button.gr-button-primary::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); - transition: left 0.5s; -} - -button.gr-button:hover, button.gr-button-primary:hover { - transform: translateY(-3px) !important; - box-shadow: var(--sh-shadow-xl) !important; - filter: brightness(1.05) !important; -} - -button.gr-button:hover::before, button.gr-button-primary:hover::before { - left: 100%; -} - -button.gr-button-secondary { - background: var(--sh-gray-100) !important; - color: var(--sh-gray-700) !important; - border: 2px solid var(--sh-gray-300) !important; - box-shadow: var(--sh-shadow) !important; -} - -button.gr-button-secondary:hover { - background: var(--sh-gray-200) !important; - border-color: var(--sh-gray-400) !important; -} - -/* Tab styling */ -.gradio-container .tabs, .gradio-container .tabitem { - border-radius: var(--sh-radius-xl) !important; - background: var(--sh-surface) !important; -} - -.gradio-container .tab-nav button { - border-radius: var(--sh-radius-lg) !important; - background: var(--sh-gray-100) !important; - border: 2px solid var(--sh-gray-200) !important; - color: var(--sh-gray-600) !important; - padding: 0.75rem 1.5rem !important; - font-weight: 600 !important; - transition: var(--sh-transition) !important; - margin-right: 0.5rem !important; -} - -.gradio-container .tab-nav button:hover { - background: var(--sh-gray-200) !important; - color: var(--sh-gray-700) !important; - transform: translateY(-1px) !important; -} - -.gradio-container .tab-nav button.selected { - background: linear-gradient(135deg, var(--sh-primary) 0%, var(--sh-primary-dark) 100%) !important; - border-color: var(--sh-primary) !important; - color: white !important; - box-shadow: var(--sh-shadow-lg) !important; -} - -/* Status messages */ -.status-success { - background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%) !important; - border: 2px solid #22c55e !important; - color: #15803d !important; - padding: 1rem 1.5rem !important; - border-radius: var(--sh-radius-lg) !important; - margin: 1rem 0 !important; - box-shadow: var(--sh-shadow) !important; - font-weight: 600 !important; -} - -.status-error { - background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%) !important; - border: 2px solid #ef4444 !important; - color: #dc2626 !important; - padding: 1rem 1.5rem !important; - border-radius: var(--sh-radius-lg) !important; - margin: 1rem 0 !important; - box-shadow: var(--sh-shadow) !important; - font-weight: 600 !important; -} - -.status-warning { - background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%) !important; - border: 2px solid #f59e0b !important; - color: #d97706 !important; - padding: 1rem 1.5rem !important; - border-radius: var(--sh-radius-lg) !important; - margin: 1rem 0 !important; - box-shadow: var(--sh-shadow) !important; - font-weight: 600 !important; -} - -.status-info { - background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%) !important; - border: 2px solid #3b82f6 !important; - color: #1d4ed8 !important; - padding: 1rem 1.5rem !important; - border-radius: var(--sh-radius-lg) !important; - margin: 1rem 0 !important; - box-shadow: var(--sh-shadow) !important; - font-weight: 600 !important; -} - -/* Image styling */ -.gr-image { - border-radius: var(--sh-radius-xl) !important; - overflow: hidden !important; - box-shadow: var(--sh-shadow-lg) !important; - transition: var(--sh-transition) !important; -} - -.gr-image:hover { - transform: scale(1.02) !important; - box-shadow: var(--sh-shadow-xl) !important; -} - -/* Analysis results styling */ -.analysis-container { - background: var(--sh-surface) !important; - border-radius: var(--sh-radius-xl) !important; - padding: 2rem !important; - margin: 1.5rem 0 !important; - box-shadow: var(--sh-shadow-xl) !important; - border: 1px solid var(--sh-gray-200) !important; -} - -.analysis-header { - background: linear-gradient(135deg, var(--sh-primary) 0%, var(--sh-secondary) 100%) !important; - color: white !important; - padding: 2rem !important; - border-radius: var(--sh-radius-lg) !important; - margin-bottom: 2rem !important; - text-align: center !important; - position: relative !important; - overflow: hidden !important; -} - -.analysis-header::before { - content: ''; - position: absolute; - top: -50%; - left: -50%; - width: 200%; - height: 200%; - background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); - animation: shimmer 3s ease-in-out infinite; -} - -@keyframes shimmer { - 0%, 100% { transform: rotate(0deg); } - 50% { transform: rotate(180deg); } -} - -.metric-grid { - display: grid !important; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) !important; - gap: 1.5rem !important; - margin: 2rem 0 !important; -} - -.metric-card { - background: var(--sh-surface) !important; - padding: 1.5rem !important; - border-radius: var(--sh-radius-lg) !important; - text-align: center !important; - box-shadow: var(--sh-shadow) !important; - border: 1px solid var(--sh-gray-200) !important; - transition: var(--sh-transition) !important; -} - -.metric-card:hover { - transform: translateY(-4px) !important; - box-shadow: var(--sh-shadow-xl) !important; - border-color: var(--sh-primary) !important; -} - -.metric-card h3 { - color: var(--sh-primary) !important; - margin: 0 0 0.5rem 0 !important; - font-size: 1rem !important; - font-weight: 600 !important; - text-transform: uppercase !important; - letter-spacing: 0.05em !important; -} - -.metric-card p { - font-weight: 800 !important; - font-size: 1.5rem !important; - color: var(--sh-gray-900) !important; - margin: 0 !important; -} - -/* Responsive design */ -@media (max-width: 768px) { - .medical-header { - padding: 1.5rem !important; - border-radius: var(--sh-radius-lg) !important; - flex-direction: column !important; - text-align: center !important; - gap: 1rem !important; - } - - .medical-header h1 { - font-size: 1.75rem !important; - } - - .header-nav { - flex-wrap: wrap !important; - justify-content: center !important; - gap: 0.75rem !important; - } - - .logo { - width: 3rem !important; - height: 3rem !important; - } - - .gr-form, .gr-group { - padding: 1.5rem !important; - } - - .metric-grid { - grid-template-columns: 1fr !important; - gap: 1rem !important; - } -} - -/* Loading animations */ -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -.loading { - animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; -} - -/* Scrollbar styling */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: var(--sh-gray-100); - border-radius: var(--sh-radius); -} - -::-webkit-scrollbar-thumb { - background: var(--sh-gray-400); - border-radius: var(--sh-radius); - transition: var(--sh-transition); -} - -::-webkit-scrollbar-thumb:hover { - background: var(--sh-gray-500); -} - -/* Focus styles for accessibility */ -*:focus { - outline: 2px solid var(--sh-primary) !important; - outline-offset: 2px !important; -} - -/* High contrast mode support */ -@media (prefers-contrast: high) { - :root { - --sh-primary: #4338ca; - --sh-primary-dark: #3730a3; - --sh-gray-900: #000000; - --sh-surface: #ffffff; - } -} - -/* Reduced motion support */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} -""" + return """ + /* =================== SMARTHEAL-INSPIRED 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, #f8fafc 0%, #e2e8f0 100%) !important; + color: #1e293b !important; + line-height: 1.6 !important; + } + + /* SmartHeal-inspired Header */ + .medical-header { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important; + color: white !important; + padding: 40px !important; + border-radius: 0 0 24px 24px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + margin-bottom: 0 !important; + box-shadow: 0 20px 60px rgba(99, 102, 241, 0.3) !important; + border: none !important; + position: relative !important; + overflow: hidden !important; + } + + .medical-header::before { + content: '' !important; + position: absolute !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + background: url('data:image/svg+xml,') !important; + opacity: 0.3 !important; + pointer-events: none !important; + } + + .logo { + width: 80px !important; + height: 80px !important; + border-radius: 50% !important; + margin-right: 24px !important; + border: 4px solid rgba(255, 255, 255, 0.2) !important; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2) !important; + background: white !important; + padding: 4px !important; + position: relative !important; + z-index: 2 !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, #f1f5f9) !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; + position: relative !important; + z-index: 2 !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; + position: relative !important; + z-index: 2 !important; + } + + /* Enhanced Form Styling */ + .gr-form { + background: linear-gradient(145deg, #ffffff 0%, #f8fafc 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(99, 102, 241, 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, #6366f1, #8b5cf6, #f97316) !important; + z-index: 1 !important; + } + + /* Professional Input Fields */ + .gr-textbox, .gr-number, .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.04) !important; + font-size: 1rem !important; + color: #1e293b !important; + padding: 16px 20px !important; + } + + .gr-textbox:focus, .gr-number:focus, .gr-dropdown:focus, + .gr-textbox input:focus, .gr-number input:focus, .gr-dropdown select:focus { + border-color: #6366f1 !important; + box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1) !important; + background: #ffffff !important; + outline: none !important; + transform: translateY(-1px) !important; + } + + /* SmartHeal-inspired Button Styling */ + button.gr-button, button.gr-button-primary { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 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(99, 102, 241, 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, #4f46e5 0%, #7c3aed 100%) !important; + box-shadow: 0 8px 32px rgba(99, 102, 241, 0.4) !important; + transform: translateY(-3px) !important; + } + + button.gr-button::before, button.gr-button-primary::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.2), transparent) !important; + transition: left 0.5s !important; + } + + button.gr-button:hover::before, button.gr-button-primary:hover::before { + left: 100% !important; + } + + /* Secondary Button Style */ + button.gr-button-secondary { + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%) !important; + color: #ffffff !important; + border: none !important; + border-radius: 12px !important; + font-weight: 600 !important; + padding: 14px 28px !important; + font-size: 1rem !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 4px 16px rgba(249, 115, 22, 0.3) !important; + } + + button.gr-button-secondary:hover { + background: linear-gradient(135deg, #ea580c 0%, #c2410c 100%) !important; + box-shadow: 0 8px 32px rgba(249, 115, 22, 0.4) !important; + transform: translateY(-2px) !important; + } + + /* Professional Status Messages */ + .status-success { + background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%) !important; + border: 2px solid #22c55e !important; + color: #15803d !important; + padding: 20px 24px !important; + border-radius: 16px !important; + font-weight: 600 !important; + margin: 16px 0 !important; + box-shadow: 0 8px 24px rgba(34, 197, 94, 0.2) !important; + backdrop-filter: blur(10px) !important; + position: relative !important; + } + + .status-success::before { + content: '✅' !important; + position: absolute !important; + left: 20px !important; + top: 50% !important; + transform: translateY(-50%) !important; + font-size: 1.2rem !important; + } + + .status-error { + background: linear-gradient(135deg, #fef2f2 0%, #fecaca 100%) !important; + border: 2px solid #ef4444 !important; + color: #dc2626 !important; + padding: 20px 24px !important; + border-radius: 16px !important; + font-weight: 600 !important; + margin: 16px 0 !important; + box-shadow: 0 8px 24px rgba(239, 68, 68, 0.2) !important; + backdrop-filter: blur(10px) !important; + position: relative !important; + } + + .status-error::before { + content: '❌' !important; + position: absolute !important; + left: 20px !important; + top: 50% !important; + transform: translateY(-50%) !important; + font-size: 1.2rem !important; + } + + .status-warning { + background: linear-gradient(135deg, #fffbeb 0%, #fed7aa 100%) !important; + border: 2px solid #f59e0b !important; + color: #d97706 !important; + padding: 20px 24px !important; + border-radius: 16px !important; + font-weight: 600 !important; + margin: 16px 0 !important; + box-shadow: 0 8px 24px rgba(245, 158, 11, 0.2) !important; + backdrop-filter: blur(10px) !important; + position: relative !important; + } + + .status-warning::before { + content: '⚠️' !important; + position: absolute !important; + left: 20px !important; + top: 50% !important; + transform: translateY(-50%) !important; + font-size: 1.2rem !important; + } + + /* Enhanced Tab Styling */ + .gr-tab-nav { + background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%) !important; + border-radius: 16px 16px 0 0 !important; + padding: 8px !important; + border-bottom: 2px solid #e2e8f0 !important; + } + + .gr-tab-nav button { + background: transparent !important; + border: none !important; + border-radius: 12px !important; + padding: 12px 24px !important; + font-weight: 600 !important; + color: #64748b !important; + transition: all 0.3s ease !important; + margin: 0 4px !important; + } + + .gr-tab-nav button.selected { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important; + color: white !important; + box-shadow: 0 4px 16px rgba(99, 102, 241, 0.3) !important; + } + + .gr-tab-nav button:hover:not(.selected) { + background: rgba(99, 102, 241, 0.1) !important; + color: #6366f1 !important; + } + + /* Enhanced Image Upload Area */ + .gr-file-upload { + border: 3px dashed #6366f1 !important; + border-radius: 16px !important; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important; + padding: 40px !important; + text-align: center !important; + transition: all 0.3s ease !important; + position: relative !important; + overflow: hidden !important; + } + + .gr-file-upload:hover { + border-color: #8b5cf6 !important; + background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%) !important; + transform: translateY(-2px) !important; + box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1) !important; + } + + .gr-file-upload::before { + content: '📸' !important; + font-size: 3rem !important; + display: block !important; + margin-bottom: 16px !important; + } + + /* Enhanced Slider Styling */ + .gr-slider { + background: #f1f5f9 !important; + border-radius: 12px !important; + padding: 20px !important; + border: 2px solid #e2e8f0 !important; + } + + .gr-slider input[type="range"] { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important; + border-radius: 8px !important; + height: 8px !important; + } + + .gr-slider input[type="range"]::-webkit-slider-thumb { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important; + border: 3px solid white !important; + border-radius: 50% !important; + width: 24px !important; + height: 24px !important; + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3) !important; + } + + /* Analysis Results Styling */ + .analysis-container { + background: linear-gradient(145deg, #ffffff 0%, #f8fafc 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(99, 102, 241, 0.1) !important; + position: relative !important; + overflow: hidden !important; + } + + .analysis-container::before { + content: '' !important; + position: absolute !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + height: 4px !important; + background: linear-gradient(90deg, #6366f1, #8b5cf6, #f97316) !important; + z-index: 1 !important; + } + + .analysis-header { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important; + color: white !important; + padding: 24px 32px !important; + border-radius: 16px 16px 0 0 !important; + margin: -32px -32px 24px -32px !important; + text-align: center !important; + position: relative !important; + z-index: 2 !important; + } + + .analysis-header h1 { + margin: 0 !important; + font-size: 2.5rem !important; + font-weight: 800 !important; + text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3) !important; + } + + .analysis-header p { + margin: 8px 0 0 0 !important; + opacity: 0.9 !important; + font-size: 1.1rem !important; + font-weight: 500 !important; + } + + /* Image Gallery Styling */ + .image-gallery { + display: grid !important; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)) !important; + gap: 24px !important; + margin: 24px 0 !important; + } + + .image-item { + background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%) !important; + border-radius: 16px !important; + padding: 20px !important; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06) !important; + border: 1px solid #e2e8f0 !important; + transition: all 0.3s ease !important; + text-align: center !important; + } + + .image-item:hover { + transform: translateY(-4px) !important; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.12) !important; + border-color: #6366f1 !important; + } + + .image-item img { + width: 100% !important; + border-radius: 12px !important; + margin-bottom: 16px !important; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important; + } + + .image-item h4 { + color: #1e293b !important; + margin: 0 0 8px 0 !important; + font-size: 1.2rem !important; + font-weight: 700 !important; + } + + .image-item p { + color: #64748b !important; + margin: 0 !important; + font-size: 0.95rem !important; + line-height: 1.5 !important; + } + + /* Responsive Design */ + @media (max-width: 768px) { + .medical-header { + padding: 24px 16px !important; + text-align: center !important; + flex-direction: column !important; + } + .medical-header h1 { + font-size: 2.5rem !important; + margin-top: 16px !important; + } + .logo { + width: 60px !important; + height: 60px !important; + margin-right: 0 !important; + margin-bottom: 16px !important; + } + .gr-form { + padding: 20px !important; + margin: 16px 8px !important; + } + .image-gallery { + grid-template-columns: 1fr !important; + gap: 16px !important; + } + button.gr-button, button.gr-button-primary { + padding: 14px 24px !important; + font-size: 1rem !important; + } + } + + /* Loading Animation */ + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } + } + + .loading { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important; + } + + /* Gradient Text Effect */ + .gradient-text { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important; + -webkit-background-clip: text !important; + -webkit-text-fill-color: transparent !important; + background-clip: text !important; + font-weight: 700 !important; + } + + /* Card Hover Effects */ + .card-hover { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + } + + .card-hover:hover { + transform: translateY(-8px) scale(1.02) !important; + box-shadow: 0 20px 60px rgba(99, 102, 241, 0.15) !important; + } + + /* Disclaimer Styling */ + .disclaimer { + background: linear-gradient(135deg, #fef2f2 0%, #fecaca 100%) !important; + border: 2px solid #f87171 !important; + border-radius: 16px !important; + padding: 20px 24px !important; + margin: 16px 0 !important; + box-shadow: 0 8px 24px rgba(248, 113, 113, 0.2) !important; + } + + .disclaimer h3 { + color: #dc2626 !important; + margin: 0 0 12px 0 !important; + font-size: 1.3rem !important; + font-weight: 700 !important; + } + + .disclaimer p { + color: #991b1b !important; + margin: 0 !important; + font-weight: 600 !important; + line-height: 1.5 !important; + } + """ def create_interface(self): """ @@ -891,33 +900,23 @@ button.gr-button-secondary:hover { # ----------------------- Blocks UI ----------------------- with gr.Blocks(css=self.get_custom_css(), title="SmartHeal - AI Wound Care Assistant") as app: - # Header with SmartHeal branding + # 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"""
-
- -
-

SmartHeal AI

-

Revolutionising Healthcare with Precision Technology

-
-
-
- Home - About - Product - Technology - Newsroom - Contact + +
+

SmartHeal AI

+

Advanced Wound Care Analysis & Clinical Support System

""") - # Enhanced disclaimer with SmartHeal styling + # Disclaimer gr.HTML(""" -
-

⚠️ IMPORTANT MEDICAL DISCLAIMER

-

This AI-powered system is designed for educational and testing purposes only. It is not intended to replace professional medical judgment or clinical assessment. Always consult qualified healthcare professionals for proper diagnosis and treatment decisions.

+
+

⚠️ IMPORTANT DISCLAIMER

+

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

""") @@ -926,31 +925,29 @@ button.gr-button-secondary:hover { with gr.Column(visible=True) as auth_panel: with gr.Tabs(): with gr.Tab("🔐 Professional Login"): - gr.HTML("

Welcome Back to SmartHeal

") - login_username = gr.Textbox(label="👤 Username", placeholder="Enter your username") - login_password = gr.Textbox(label="🔒 Password", type="password", placeholder="Enter your password") - login_btn = gr.Button("🚀 Sign In", variant="primary", size="lg") - login_status = gr.HTML("
Please sign in to access the SmartHeal platform.
") + 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"): - gr.HTML("

Join SmartHeal Community

") - signup_username = gr.Textbox(label="👤 Username", placeholder="Choose a unique username") - signup_email = gr.Textbox(label="📧 Email", placeholder="your.email@hospital.com") - signup_password = gr.Textbox(label="🔒 Password", type="password", placeholder="Create a secure password") - signup_name = gr.Textbox(label="👨‍⚕️ Full Name", placeholder="Dr. John Smith") + 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", placeholder="City General Hospital") - phone = gr.Textbox(label="Phone", placeholder="+1 (555) 123-4567") - country_code = gr.Textbox(label="Country Code", placeholder="US") - department = gr.Textbox(label="Department", placeholder="Wound Care Center") - location = gr.Textbox(label="Location", placeholder="New York, NY") + 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", size="lg") + signup_btn = gr.Button("✨ Create Account", variant="primary") signup_status = gr.HTML() with gr.Column(visible=False) as practitioner_panel: @@ -960,11 +957,9 @@ button.gr-button-secondary:hover { with gr.Tabs(): # ------------------- WOUND ANALYSIS ------------------- with gr.Tab("🔬 Wound Analysis"): - gr.HTML("

AI-Powered Wound Assessment

") - with gr.Row(): with gr.Column(scale=1): - gr.HTML("

📋 Patient Selection

") + gr.HTML("

📋 Patient Selection

") patient_mode = gr.Radio( ["Existing patient", "New patient"], label="Patient mode", @@ -976,46 +971,49 @@ button.gr-button-secondary:hover { interactive=True ) with gr.Group(visible=False) as new_patient_group: - new_patient_name = gr.Textbox(label="Patient Name", placeholder="Enter patient's full name") + 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, Right forearm") - wound_duration = gr.Textbox(label="Wound Duration", placeholder="e.g., 2 weeks, 1 month") + 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

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

") + gr.HTML("

📸 Wound Image

") wound_image = gr.Image(label="Upload Wound Image", type="filepath") + # Slider to adjust the automatic segmentation mask. Positive values dilate + # (expand) the mask, negative values erode (shrink) it. The value represents + # roughly percentage change where each 5 units corresponds to one iteration. seg_adjust_slider = gr.Slider( minimum=-20, maximum=20, value=0, step=1, label="Segmentation Adjustment", - info="Fine-tune automatic segmentation (negative shrinks, positive expands)" + info="Adjust the automatic segmentation (negative shrinks, positive expands)" ) - gr.HTML("

📝 Medical History

") - previous_treatment = gr.Textbox(label="Previous Treatment", lines=3, placeholder="Describe any previous treatments...") - medical_history = gr.Textbox(label="Medical History", lines=3, placeholder="Relevant medical conditions...") - medications = gr.Textbox(label="Current Medications", lines=2, placeholder="List current medications...") - allergies = gr.Textbox(label="Known Allergies", lines=2, placeholder="List any known allergies...") - additional_notes = gr.Textbox(label="Additional Notes", lines=3, placeholder="Any additional observations...") - - # Analysis workflow buttons - analyze_btn = gr.Button("🔬 Preview Segmentation", variant="primary", size="lg") + 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) + + # Initial analysis button + analyze_btn = gr.Button("🔬 Preview Segmentation", variant="primary", elem_id="analyze-btn") # Segmentation preview section (initially hidden) with gr.Group(visible=False) as segmentation_preview_group: - gr.HTML("

🎯 AI Segmentation Preview

") + gr.HTML("

🎯 Segmentation Preview

") segmentation_preview = gr.Image(label="Automatic Segmentation", interactive=False) with gr.Row(): @@ -1026,11 +1024,11 @@ button.gr-button-secondary:hover { # Manual editing section (initially hidden) with gr.Group(visible=False) as manual_edit_group: gr.HTML(""" -
-

📝 Manual Segmentation Editor

-

+

+

📝 Manual Segmentation Instructions

+

Use the drawing tool below to manually mark the wound area. - Select the pen tool and carefully draw over the wound region to create your custom mask. + Select the pen tool and draw over the wound region to create your mask.

""") @@ -1054,178 +1052,105 @@ button.gr-button-secondary:hover { # ------------------- PATIENT HISTORY ------------------- with gr.Tab("📋 Patient History"): - gr.HTML("

Patient History Management

") - with gr.Row(): with gr.Column(scale=2): - history_btn = gr.Button("📄 Load Complete Patient History", variant="primary", size="lg") + history_btn = gr.Button("📄 Load Patient History", variant="primary") patient_history_output = gr.HTML("") with gr.Column(scale=1): - gr.HTML("

🔍 Search Patients

") - search_patient_name = gr.Textbox(label="Search patient by name", placeholder="Enter patient name...") + 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("
") - + gr.HTML("
") with gr.Row(): - view_details_dd = gr.Dropdown(choices=[], label="Select patient to view detailed timeline") - view_details_btn = gr.Button("📈 View Patient Timeline", variant="primary") + 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

-

Advanced organization management features coming soon to SmartHeal platform.

-
- """) + gr.HTML("
Organization dashboard coming soon.
") logout_btn_org = gr.Button("🚪 Logout", variant="secondary") - # ----------------------- Event handlers ----------------------- + # ----------------------- handlers ----------------------- def toggle_role_fields(role): - if role == "organization": - return gr.update(visible=True), gr.update(visible=False) - else: - return gr.update(visible=False), gr.update(visible=True) + 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, phone, country_code, department, location, organization_dropdown): + def handle_signup(username, email, password, name, role, org_name_v, phone_v, cc_v, dept_v, loc_v, org_dropdown): try: - if not all([username, email, password, name]): - return "
❌ Please fill in all required fields.
" - - if role == "organization": - if not all([org_name, phone, country_code, department, location]): - return "
❌ Please fill in all organization fields.
" - - # Create organization first - org_result = self.auth_manager.create_organization( - name=org_name, - phone=phone, - country_code=country_code, - department=department, - location=location - ) - if not org_result.get("success"): - return f"
❌ Organization creation failed: {org_result.get('error', 'Unknown error')}
" - - org_id = org_result.get("organization_id") - - # Create user account - result = self.auth_manager.create_user( - username=username, - email=email, - password=password, - name=name, - role=role, - organization_id=org_id - ) - else: - # Practitioner - if not organization_dropdown: - return "
❌ Please select an organization.
" - - org_id = _resolve_org_id_from_dropdown(organization_dropdown) - if not org_id: - return "
❌ Invalid organization selection.
" - - result = self.auth_manager.create_user( - username=username, - email=email, - password=password, - name=name, - role=role, - organization_id=org_id - ) - - if result.get("success"): - return "
✅ Account created successfully! Please login with your credentials.
" - else: - return f"
❌ Registration failed: {result.get('error', 'Unknown error')}
" - + 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 "
✅ Account created. Please log in.
" + return "
❌ Could not create account. Username/email may exist.
" except Exception as e: - logging.error(f"Signup error: {e}") - return f"
❌ System error: {html.escape(str(e))}
" + return f"
❌ Error: {html.escape(str(e))}
" def handle_login(username, password): - try: - result = self.auth_manager.authenticate_user(username, password) - if result.get("success"): - self.current_user = result.get("user", {}) - user_role = self.current_user.get("role", "") - user_name = self.current_user.get("name", "User") - user_id = int(self.current_user.get("id", 0)) - - # Refresh patient dropdown for practitioners - if user_role == "practitioner" and user_id: - _refresh_patient_dropdown(user_id) - - user_info_html = f""" -
-

Welcome back, {html.escape(user_name)}! 👋

-

Role: {html.escape(user_role.title())} | ID: {user_id}

-
- """ - - if user_role == "practitioner": - return ( - "
✅ Login successful!
", - gr.update(visible=False), # auth_panel - gr.update(visible=True), # practitioner_panel - gr.update(visible=False), # organization_panel - user_info_html, - gr.update(choices=self._patient_choices), # existing_patient_dd - gr.update(choices=self._patient_choices) # view_details_dd - ) - elif user_role == "organization": - return ( - "
✅ Login successful!
", - gr.update(visible=False), # auth_panel - gr.update(visible=False), # practitioner_panel - gr.update(visible=True), # organization_panel - user_info_html, - gr.update(), # existing_patient_dd - gr.update() # view_details_dd - ) - else: - return ( - f"
❌ Login failed: {result.get('error', 'Invalid credentials')}
", - gr.update(visible=True), # auth_panel - gr.update(visible=False), # practitioner_panel - gr.update(visible=False), # organization_panel - "", - gr.update(), - gr.update() - ) - except Exception as e: - logging.error(f"Login error: {e}") - return ( - f"
❌ System error: {html.escape(str(e))}
", - gr.update(visible=True), - gr.update(visible=False), - gr.update(visible=False), - "", - gr.update(), - gr.update() - ) + 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") + + 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 ( - gr.update(visible=True), # auth_panel - gr.update(visible=False), # practitioner_panel - gr.update(visible=False), # organization_panel - "
Please sign in to access SmartHeal.
", - gr.update(visible=False), # segmentation_preview_group - gr.update(visible=False), # manual_edit_group - "" # analysis_output - ) + return { + auth_panel: gr.update(visible=True), + practitioner_panel: gr.update(visible=False), + organization_panel: gr.update(visible=False), + login_status: "
Please sign in.
", + segmentation_preview_group: gr.update(visible=False), + manual_edit_group: gr.update(visible=False), + analysis_output: "" + } def toggle_patient_mode(mode): - if mode == "New patient": - return gr.update(visible=False), gr.update(visible=True) - else: - return gr.update(visible=True), gr.update(visible=False) + return { + existing_patient_dd: gr.update(visible=(mode == "Existing patient")), + new_patient_group: gr.update(visible=(mode == "New patient")) + } def process_image_for_segmentation( mode, existing_label, np_name, np_age, np_gender, @@ -1233,569 +1158,724 @@ button.gr-button-secondary:hover { prev_tx, med_hist, meds, alls, notes, img_path, seg_adjust ): """Process image and show segmentation preview""" + if not img_path: + return { + segmentation_preview_group: gr.update(visible=False), + analysis_output: "
❌ Please upload a wound image.
" + } + try: - if not img_path: + # Run initial analysis to get segmentation + user_id = int(self.current_user.get("id", 0) or 0) + if not user_id: return { segmentation_preview_group: gr.update(visible=False), - segmentation_preview: None, - manual_edit_group: gr.update(visible=False), - analysis_output: "
❌ Please upload a wound image first.
" + analysis_output: "
❌ Please login first.
" } - # Run visual analysis to get segmentation preview + # Prepare questionnaire data for AI + if mode == "Existing patient": + pid = _label_to_id(existing_label) + if not pid: + return { + segmentation_preview_group: gr.update(visible=False), + analysis_output: "
⚠️ Select an existing patient.
" + } + # Fetch patient data + row = self.database_manager.execute_query_one( + "SELECT id, name, age, gender FROM patients WHERE id=%s LIMIT 1", (pid,) + ) + pcore = row or {} + 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 + + 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 visual analysis only to get segmentation image_pil = Image.open(img_path) visual_results = self.wound_analyzer.perform_visual_analysis(image_pil) if not visual_results: return { segmentation_preview_group: gr.update(visible=False), - segmentation_preview: None, - manual_edit_group: gr.update(visible=False), - analysis_output: "
❌ Failed to process image for segmentation.
" + analysis_output: "
❌ Failed to analyze image.
" } + # Get segmentation image path seg_path = visual_results.get("segmentation_image_path") if not seg_path or not os.path.exists(seg_path): return { segmentation_preview_group: gr.update(visible=False), - segmentation_preview: None, - manual_edit_group: gr.update(visible=False), - analysis_output: "
❌ Segmentation preview not available.
" + analysis_output: "
❌ Segmentation failed.
" } return { segmentation_preview_group: gr.update(visible=True), segmentation_preview: seg_path, manual_edit_group: gr.update(visible=False), - analysis_output: "
✅ Segmentation preview ready! Review the automatic segmentation above and choose your next step.
" + analysis_output: "
✅ Segmentation preview ready. Review and choose to accept or manually edit.
" } except Exception as e: logging.error(f"Segmentation preview error: {e}") return { segmentation_preview_group: gr.update(visible=False), - segmentation_preview: None, - manual_edit_group: gr.update(visible=False), analysis_output: f"
❌ Error: {html.escape(str(e))}
" } - def show_manual_edit(): - """Show manual editing interface""" + def show_manual_edit_interface(img_path): + """Show manual editing interface with the original image""" + if not img_path or not os.path.exists(img_path): + return { + manual_edit_group: gr.update(visible=False), + analysis_output: "
❌ Original image not available for editing.
" + } + return { - manual_edit_group: gr.update(visible=True) + manual_edit_group: gr.update(visible=True), + manual_mask_input: img_path, # Load the original image for manual editing + analysis_output: "
⚠️ Use the drawing tool to manually mark the wound area, then click your desired action.
" } def process_manual_mask(mask_data): - """Process manual mask data and return path to temporary file""" + """Process the manual mask from ImageMask component""" + if not mask_data: + return None + try: - if mask_data is None: - return None - - # mask_data is a dict with 'background' and 'layers' - if isinstance(mask_data, dict) and 'layers' in mask_data: - layers = mask_data['layers'] - if layers and len(layers) > 0: - # Get the first layer (mask) - mask_layer = layers[0] - if isinstance(mask_layer, np.ndarray): - mask = mask_layer - else: - # Convert PIL to numpy if needed - mask = np.array(mask_layer) + # Extract the mask from the ImageMask component + # The mask_data contains both the background image and the drawn mask + if isinstance(mask_data, dict): + # Check if composite exists (newer format) + if "composite" in mask_data: + composite_img = mask_data["composite"] + # Convert to grayscale and extract the drawn areas + gray = cv2.cvtColor(composite_img, cv2.COLOR_RGB2GRAY) + # Create mask where drawn areas are white (255) and background is black (0) + _, mask = cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY) + # Check if layers exist (older format) + elif "layers" in mask_data and len(mask_data["layers"]) > 0: + # Get the alpha channel from the first layer (the drawn mask) + alpha_channel = mask_data["layers"][0][:, :, 3] + # Convert to binary mask - drawn areas have alpha > 0 + mask = np.where(alpha_channel > 0, 255, 0).astype(np.uint8) else: return None + + # Save the mask temporarily + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: + cv2.imwrite(tmp.name, mask) + manual_mask_path = tmp.name + + return manual_mask_path else: return None - - # Ensure mask is binary - if len(mask.shape) == 3: - mask = cv2.cvtColor(mask, cv2.COLOR_RGB2GRAY) - - # Create temporary file - with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: - cv2.imwrite(tmp.name, mask) - manual_mask_path = tmp.name - - return manual_mask_path + except Exception as e: logging.error(f"Manual mask processing error: {e}") return None - else: - return None - def get_segmentation_only(img_path, manual_mask_path=None): - """Get only the segmentation mask without full analysis""" - try: - if not img_path: - return "
❌ No image provided.
" - - # Run visual analysis to get segmentation - image_pil = Image.open(img_path) - - if manual_mask_path: - visual_results = self.wound_analyzer.analyze_wound( - img_path, {}, seg_adjust=0.0, manual_mask_path=manual_mask_path - ) - mask_type = "Manual" - else: - visual_results = self.wound_analyzer.perform_visual_analysis(image_pil) - mask_type = "Automatic" - - if not visual_results: - return "
❌ Failed to generate segmentation.
" - - roi_mask_path = visual_results.get("roi_mask_path") - seg_path = visual_results.get("segmentation_image_path") - - if not roi_mask_path or not os.path.exists(roi_mask_path): - return "
❌ Segmentation mask not found.
" - - mask_b64 = self.image_to_base64(roi_mask_path) - seg_b64 = self.image_to_base64(seg_path) if seg_path and os.path.exists(seg_path) else None - - html_output = f""" -
-
-

🎯 {mask_type} Wound Segmentation

-

Binary mask showing precise wound boundaries

-
+ def get_segmentation_only(img_path, manual_mask_path=None): + """Get only the segmentation mask without full analysis""" + try: + if not img_path: + return "
❌ No image provided.
" -
- ✅ Segmentation Status: {mask_type} segmentation completed successfully with high precision -
+ # Run visual analysis to get segmentation + image_pil = Image.open(img_path) -
-
-

Binary Mask

- {f'Segmentation Mask' if mask_b64 else '

Mask not available

'} -

White = Wound Area, Black = Background

+ if manual_mask_path: + # Use manual mask + visual_results = self.wound_analyzer.analyze_wound( + img_path, {}, seg_adjust=0.0, manual_mask_path=manual_mask_path + ) + mask_type = "Manual" + else: + # Use automatic segmentation + visual_results = self.wound_analyzer.perform_visual_analysis(image_pil) + mask_type = "Automatic" + + if not visual_results: + return "
❌ Failed to generate segmentation.
" + + # Get the segmentation mask path + roi_mask_path = visual_results.get("roi_mask_path") + seg_path = visual_results.get("segmentation_image_path") + + if not roi_mask_path or not os.path.exists(roi_mask_path): + return "
❌ Segmentation mask not found.
" + + # Convert mask to base64 for display + mask_b64 = self.image_to_base64(roi_mask_path) + seg_b64 = self.image_to_base64(seg_path) if seg_path and os.path.exists(seg_path) else None + + html_output = f""" +
+
+

🎯 {mask_type} Wound Segmentation

+

Binary mask showing wound boundaries

-
-

Overlay Visualization

- {f'Segmentation Overlay' if seg_b64 else '

Overlay not available

'} -

Red overlay highlights detected wound region

+
+
+ ✅ Segmentation Status: {mask_type} segmentation completed successfully +
+ +
+
+

Binary Mask

+ {f'Segmentation Mask' if mask_b64 else '

Mask not available

'} +

White = Wound, Black = Background

+
+ +
+

Overlay Visualization

+ {f'Segmentation Overlay' if seg_b64 else '

Overlay not available

'} +

Red overlay shows detected wound area

+
+
+ +
+

📥 Download Instructions

+

Right-click on the binary mask image above and select "Save image as..." to download the segmentation mask for your use.

+
+ """ -
-

📥 Download Instructions

-

Right-click on the binary mask image above and select "Save image as..." to download the segmentation mask for clinical use or further analysis.

-
-
- """ - - if manual_mask_path and os.path.exists(manual_mask_path): - try: - os.unlink(manual_mask_path) - except: - pass - - return html_output - - except Exception as e: - logging.error(f"Segmentation only error: {e}") - return f"
❌ Error: {html.escape(str(e))}
" - - def run_full_analysis_with_manual_mask( - 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, seg_adjust, mask_data - ): - """Run full analysis with manual mask""" - try: - # Process manual mask - manual_mask_path = process_manual_mask(mask_data) - - # Run the full analysis with manual mask - result_html = standalone_run_analysis( - self, self.current_user, self.database_manager, self.wound_analyzer, - 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, - seg_adjust, manual_mask_path - ) - - # Clean up temporary file - if manual_mask_path and os.path.exists(manual_mask_path): - try: - os.unlink(manual_mask_path) - except: - pass - - return { - analysis_output: result_html, - segmentation_preview_group: gr.update(visible=False), - manual_edit_group: gr.update(visible=False) - } - - except Exception as e: - logging.error(f"Manual analysis error: {e}") - return { - analysis_output: f"
❌ Analysis failed: {html.escape(str(e))}
" - } - - def get_manual_segmentation_only(mask_data, img_path): - """Get only the manual segmentation mask""" - try: - manual_mask_path = process_manual_mask(mask_data) - if not manual_mask_path: - return "
❌ Failed to process manual mask.
" - - result = get_segmentation_only(img_path, manual_mask_path) - return { - analysis_output: result, - segmentation_preview_group: gr.update(visible=False), - manual_edit_group: gr.update(visible=False) - } - - except Exception as e: - logging.error(f"Manual segmentation only error: {e}") - return { - analysis_output: f"
❌ Error: {html.escape(str(e))}
" - } + # Clean up temporary file if it exists + if manual_mask_path and os.path.exists(manual_mask_path): + try: + os.unlink(manual_mask_path) + except: + pass + + return html_output + + except Exception as e: + logging.error(f"Segmentation only error: {e}") + return f"
❌ Error: {html.escape(str(e))}
" - def get_auto_segmentation_only(img_path): - """Get only the automatic segmentation mask""" - try: - result = get_segmentation_only(img_path, None) - return { - analysis_output: result, - segmentation_preview_group: gr.update(visible=False), - manual_edit_group: gr.update(visible=False) - } - - except Exception as e: - logging.error(f"Auto segmentation only error: {e}") - return { - analysis_output: f"
❌ Error: {html.escape(str(e))}
" - } + def run_full_analysis_with_manual_mask( + 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, seg_adjust, mask_data + ): + """Run full analysis with manual mask""" + try: + # Process manual mask + manual_mask_path = process_manual_mask(mask_data) + + # Run the full analysis with manual mask + result_html = standalone_run_analysis( + self, self.current_user, self.database_manager, self.wound_analyzer, + 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, + seg_adjust, manual_mask_path + ) + + # Clean up temporary file + if manual_mask_path and os.path.exists(manual_mask_path): + try: + os.unlink(manual_mask_path) + except: + pass + + return { + analysis_output: result_html, + segmentation_preview_group: gr.update(visible=False), + manual_edit_group: gr.update(visible=False) + } + + except Exception as e: + logging.error(f"Manual analysis error: {e}") + return { + analysis_output: f"
❌ Analysis failed: {html.escape(str(e))}
" + } - def run_full_analysis_accept_segmentation( - 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, seg_adjust - ): - """Run full analysis accepting the automatic segmentation""" - try: - result_html = standalone_run_analysis( - self, self.current_user, self.database_manager, self.wound_analyzer, - 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, - seg_adjust, None # No manual mask - ) - - return { - analysis_output: result_html, - segmentation_preview_group: gr.update(visible=False), - manual_edit_group: gr.update(visible=False) - } - - except Exception as e: - logging.error(f"Analysis error: {e}") - return { - analysis_output: f"
❌ Analysis failed: {html.escape(str(e))}
" - } + def get_manual_segmentation_only(mask_data, img_path): + """Get only the manual segmentation mask""" + try: + manual_mask_path = process_manual_mask(mask_data) + if not manual_mask_path: + return "
❌ Failed to process manual mask.
" + + result = get_segmentation_only(img_path, manual_mask_path) + return { + analysis_output: result, + segmentation_preview_group: gr.update(visible=False), + manual_edit_group: gr.update(visible=False) + } + + except Exception as e: + logging.error(f"Manual segmentation only error: {e}") + return { + analysis_output: f"
❌ Error: {html.escape(str(e))}
" + } - def load_patient_history(): - try: - uid = int(self.current_user.get("id", 0)) - if not uid: - return "
❌ Please login first.
" - - history_data = self.patient_history_manager.get_patient_history(uid) - if not history_data: - return "
⚠️ No patient history found for your account.
" - - html_report = self.report_generator.generate_history_report(history_data) - return html_report - except Exception as e: - logging.error(f"History load error: {e}") - return f"
❌ Error: {html.escape(str(e))}
" + def get_auto_segmentation_only(img_path): + """Get only the automatic segmentation mask""" + try: + result = get_segmentation_only(img_path, None) + return { + analysis_output: result, + segmentation_preview_group: gr.update(visible=False), + manual_edit_group: gr.update(visible=False) + } + + except Exception as e: + logging.error(f"Auto segmentation only error: {e}") + return { + analysis_output: f"
❌ Error: {html.escape(str(e))}
" + } - def search_patient_by_name(name): - try: - uid = int(self.current_user.get("id", 0)) - if not uid: - return "
❌ Please login first.
" - - if not name or not name.strip(): - return "
⚠️ Enter a patient name to search.
" - - results = self.patient_history_manager.search_patients_by_name(uid, name.strip()) - if not results: - return f"
⚠️ No patients found matching '{html.escape(name)}'.
" - - html_report = self.report_generator.generate_search_results(results, name) - return html_report - except Exception as e: - logging.error(f"Patient search error: {e}") - return f"
❌ Error: {html.escape(str(e))}
" + def run_full_analysis_accept_segmentation( + 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, seg_adjust + ): + """Run full analysis accepting the automatic segmentation""" + try: + result_html = standalone_run_analysis( + self, self.current_user, self.database_manager, self.wound_analyzer, + 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, + seg_adjust, None # No manual mask + ) + + return { + analysis_output: result_html, + segmentation_preview_group: gr.update(visible=False), + manual_edit_group: gr.update(visible=False) + } + + except Exception as e: + logging.error(f"Analysis error: {e}") + return { + analysis_output: f"
❌ Analysis failed: {html.escape(str(e))}
" + } - def view_patient_details(selected_label): - try: - uid = int(self.current_user.get("id", 0)) - if not uid: - return "
❌ Please login first.
" - - pid = _label_to_id(selected_label) - if not pid: - return "
⚠️ Select a patient to view details.
" - - details = self.patient_history_manager.get_patient_details(uid, pid) - if not details: - return "
⚠️ No details found for selected patient.
" - - html_report = self.report_generator.generate_patient_timeline(details) - return html_report - except Exception as e: - logging.error(f"Patient details error: {e}") - return f"
❌ Error: {html.escape(str(e))}
" - - # ----------------------- Event bindings ----------------------- - signup_role.change(toggle_role_fields, [signup_role], [org_fields, prac_fields]) - - signup_btn.click( - handle_signup, - [signup_username, signup_email, signup_password, signup_name, signup_role, - org_name, phone, country_code, department, location, organization_dropdown], - [signup_status] - ) - - login_btn.click( - handle_login, - [login_username, login_password], - [login_status, auth_panel, practitioner_panel, organization_panel, user_info, - existing_patient_dd, view_details_dd] - ) - - logout_btn_prac.click( - handle_logout, - [], - [auth_panel, practitioner_panel, organization_panel, login_status, - segmentation_preview_group, manual_edit_group, analysis_output] - ) - - logout_btn_org.click( - handle_logout, - [], - [auth_panel, practitioner_panel, organization_panel, login_status, - segmentation_preview_group, manual_edit_group, analysis_output] - ) - - patient_mode.change(toggle_patient_mode, [patient_mode], [existing_patient_dd, new_patient_group]) - - # Segmentation preview workflow - analyze_btn.click( - process_image_for_segmentation, - [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, seg_adjust_slider], - [segmentation_preview_group, segmentation_preview, manual_edit_group, analysis_output] - ) - - # Accept segmentation and generate full report - accept_segmentation_btn.click( - run_full_analysis_accept_segmentation, - [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, seg_adjust_slider], - [analysis_output, segmentation_preview_group, manual_edit_group] - ) - - # Get segmentation only (automatic) - segmentation_only_btn.click( - get_auto_segmentation_only, - [wound_image], - [analysis_output, segmentation_preview_group, manual_edit_group] - ) - - # Show manual edit interface - manual_edit_btn.click( - show_manual_edit, - [], - [manual_edit_group] - ) - - # Process manual mask for full analysis - process_manual_btn.click( - run_full_analysis_with_manual_mask, - [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, seg_adjust_slider, manual_mask_input], - [analysis_output, segmentation_preview_group, manual_edit_group] - ) - - # Get manual segmentation only - manual_segmentation_only_btn.click( - get_manual_segmentation_only, - [manual_mask_input, wound_image], - [analysis_output, segmentation_preview_group, manual_edit_group] - ) - - # Patient history events - history_btn.click(load_patient_history, [], [patient_history_output]) - search_patient_btn.click(search_patient_by_name, [search_patient_name], [specific_patient_output]) - view_details_btn.click(view_patient_details, [view_details_dd], [view_details_output]) + def load_patient_history(): + try: + uid = int(self.current_user.get("id", 0)) + if not uid: + return "
❌ Please login first.
" + + history_data = self.patient_history_manager.get_patient_history(uid) + if not history_data: + return "
⚠️ No patient history found.
" + + html_report = self.report_generator.generate_history_report(history_data) + return html_report + except Exception as e: + logging.error(f"History load error: {e}") + return f"
❌ Error: {html.escape(str(e))}
" - return app + def search_patient_by_name(name): + try: + uid = int(self.current_user.get("id", 0)) + if not uid: + return "
❌ Please login first.
" + + if not name or not name.strip(): + return "
⚠️ Enter a patient name to search.
" + + results = self.patient_history_manager.search_patients_by_name(uid, name.strip()) + if not results: + return f"
⚠️ No patients found matching '{html.escape(name)}'.
" + + html_report = self.report_generator.generate_search_results(results, name) + return html_report + except Exception as e: + logging.error(f"Patient search error: {e}") + return f"
❌ Error: {html.escape(str(e))}
" - def _format_comprehensive_analysis_results(self, analysis_result, image_path, questionnaire_data): - """Format comprehensive analysis results with enhanced SmartHeal styling""" - try: - if not analysis_result or not analysis_result.get("success"): - return "
❌ Analysis failed or incomplete.
" + def view_patient_details(selected_label): + try: + uid = int(self.current_user.get("id", 0)) + if not uid: + return "
❌ Please login first.
" + + pid = _label_to_id(selected_label) + if not pid: + return "
⚠️ Select a patient to view details.
" + + details = self.patient_history_manager.get_patient_details(uid, pid) + if not details: + return "
⚠️ No details found for selected patient.
" + + html_report = self.report_generator.generate_patient_timeline(details) + return html_report + except Exception as e: + logging.error(f"Patient details error: {e}") + return f"
❌ Error: {html.escape(str(e))}
" - # Extract data - visual_analysis = analysis_result.get("visual_analysis", {}) - measurements = visual_analysis.get("measurements", {}) - ai_report = analysis_result.get("ai_report", "") + # ----------------------- Event bindings ----------------------- + signup_role.change(toggle_role_fields, [signup_role], [org_fields, prac_fields]) - # Get measurements - length_cm = measurements.get("length_cm", 0) - breadth_cm = measurements.get("breadth_cm", 0) - area_cm2 = measurements.get("area_cm2", 0) + signup_btn.click( + handle_signup, + [signup_username, signup_email, signup_password, signup_name, signup_role, + org_name, phone, country_code, department, location, organization_dropdown], + [signup_status] + ) - # Get tissue analysis - tissue_analysis = visual_analysis.get("tissue_analysis", {}) - tissue_type = tissue_analysis.get("tissue_type", "Unknown") - skin_tone_analysis = tissue_analysis.get("skin_tone_analysis", {}) - skin_tone_label = skin_tone_analysis.get("skin_tone_label", "Unknown") - ita_deg = skin_tone_analysis.get("ita_degree") - - # Convert image to base64 - image_b64 = self.image_to_base64(image_path) if image_path else None + login_btn.click( + handle_login, + [login_username, login_password], + [login_status, auth_panel, practitioner_panel, organization_panel, user_info, + existing_patient_dd, view_details_dd] + ) - # Get segmentation images - seg_path = visual_analysis.get("segmentation_image_path") - seg_b64 = self.image_to_base64(seg_path) if seg_path and os.path.exists(seg_path) else None + logout_btn_prac.click( + handle_logout, + [], + [auth_panel, practitioner_panel, organization_panel, login_status, + segmentation_preview_group, manual_edit_group, analysis_output] + ) + + logout_btn_org.click( + handle_logout, + [], + [auth_panel, practitioner_panel, organization_panel, login_status, + segmentation_preview_group, manual_edit_group, analysis_output] + ) + + patient_mode.change(toggle_patient_mode, [patient_mode], [existing_patient_dd, new_patient_group]) + + # Segmentation preview workflow + analyze_btn.click( + process_image_for_segmentation, + [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, seg_adjust_slider], + [segmentation_preview_group, segmentation_preview, manual_edit_group, analysis_output] + ) + + # Accept segmentation and generate full report + accept_segmentation_btn.click( + run_full_analysis_accept_segmentation, + [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, seg_adjust_slider], + [analysis_output, segmentation_preview_group, manual_edit_group] + ) - # Process AI report - report_html = self.markdown_to_html(ai_report) if ai_report else "" + # Get segmentation only (automatic) + segmentation_only_btn.click( + get_auto_segmentation_only, + [wound_image], + [analysis_output, segmentation_preview_group, manual_edit_group] + ) + # Show manual edit interface + manual_edit_btn.click( + show_manual_edit_interface, + [wound_image], + [manual_edit_group, manual_mask_input, analysis_output] + ) + + # Process manual mask and generate report + process_manual_btn.click( + run_full_analysis_with_manual_mask, + [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, seg_adjust_slider, manual_mask_input], + [analysis_output, segmentation_preview_group, manual_edit_group] + ) + + # Get manual segmentation only + manual_segmentation_only_btn.click( + get_manual_segmentation_only, + [manual_mask_input, wound_image], + [analysis_output, segmentation_preview_group, manual_edit_group] + ) + + history_btn.click(load_patient_history, [], [patient_history_output]) + search_patient_btn.click(search_patient_by_name, [search_patient_name], [specific_patient_output]) + view_details_btn.click(view_patient_details, [view_details_dd], [view_details_output]) + + return app + + def _format_comprehensive_analysis_results(self, analysis_result, image_path, questionnaire_data): + """Format comprehensive analysis results with enhanced visual presentation + - Shows 'Manual Segmentation' card if a manual mask/overlay was used + - Removes the Measurements card from the Visual Analysis Gallery + """ + import os + try: + visual_analysis = analysis_result.get("visual_analysis", {}) or {} + report = analysis_result.get("report", "") + + # Extract key 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) + skin_tone_label = visual_analysis.get("skin_tone_label", "Unknown") + ita_deg = visual_analysis.get("ita_degrees") + tissue_type = visual_analysis.get("tissue_type", "Unknown") + + # Detect if manual mask was used (look across common keys/flags) + manual_used = bool( + analysis_result.get("manual_mask_used") + or visual_analysis.get("manual_mask_used") + ) + + # Try to discover manual overlay/binary mask paths (handle multiple possible keys) + manual_overlay_path = None + for k in [ + "manual_segmentation_image_path", + "manual_overlay_path", + "manual_segmentation_overlay_path", + ]: + p = visual_analysis.get(k) + if p and os.path.exists(p): + manual_overlay_path = p + manual_used = True + break + + manual_binary_path = None + for k in [ + "manual_roi_mask_path", + "manual_mask_binary_path", + "manual_mask_path", + ]: + p = visual_analysis.get(k) + if p and os.path.exists(p): + manual_binary_path = p + manual_used = True + break + # Generate risk assessment risk_assessment = self._generate_risk_assessment(questionnaire_data) - + risk_level = risk_assessment.get("risk_level", "Unknown") + risk_score = risk_assessment.get("risk_score", 0) + risk_factors = risk_assessment.get("risk_factors", []) + risk_class = risk_level.lower().replace(" ", "_") + + # Format risk factors + if risk_factors: + risk_factors_html = "
    " + for factor in risk_factors: + risk_factors_html += f"
  • {html.escape(str(factor))}
  • " + risk_factors_html += "
" + else: + risk_factors_html = "

No specific risk factors identified.

" + + # ---------------------- Image Gallery ---------------------- + image_gallery_html = "" + + # Convert report markdown to HTML + report_html = self.markdown_to_html(report) if report else "" + + status_line = ( + "Analysis completed successfully with manual segmentation" + if manual_used else + "Analysis completed successfully with comprehensive wound assessment" + ) + html_output = f""" -
-
-

🔬 SmartHeal AI Analysis Report

-

Comprehensive wound assessment powered by advanced AI technology

+
+
+

🔬 SmartHeal AI Comprehensive Analysis

+

Advanced Computer Vision & Medical AI Assessment

+
+

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

+
- - -
-
-

Original Wound Image

- {f'Original Wound' if image_b64 else '

Image not available

'} + +
+
+ ✅ Analysis Status: {status_line}
-
-

AI Segmentation Analysis

- {f'Segmentation Analysis' if seg_b64 else '

Segmentation not available

'} + + +
+

🖼️ Visual Analysis Gallery

+ {image_gallery_html}
-
- - -
-

📊 Clinical Measurements & Assessment

-
-
-

Length

-

{length_cm:.2f} cm

-
-
-

Width

-

{breadth_cm:.2f} cm

-
-
-

Surface Area

-

{area_cm2:.2f} cm²

-
-
-

Location

-

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

-
-
-

Skin Tone

-

{html.escape(str(skin_tone_label))}{f" ({ita_deg:.1f}°)" if ita_deg is not None else ""}

-
-
-

Tissue Type

-

{html.escape(str(tissue_type))}

+ + +
+

🔍 Wound Detection & Classification

+
+
+

Wound Type

+

{html.escape(str(wound_type))}

+
+
+

Location

+

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

+
+
+

Skin Tone

+

{html.escape(str(skin_tone_label))}{f" ({ita_deg:.1f}°)" if ita_deg is not None else ""}

+
+
+

Tissue Type

+

{html.escape(str(tissue_type))}

+
-
- - -
-

⚠️ Risk Assessment

-
-
-
-

Risk Level

-

{risk_assessment['risk_level']}

-

Score: {risk_assessment['risk_score']}/12

+ + +
+

📏 Wound Measurements

+
+
+

Length

+

{length_cm:.2f} cm

+
+
+

Width

+

{breadth_cm:.2f} cm

+
+
+

Surface Area

+

{area_cm2:.2f} cm²

-
-

Identified Risk Factors:

-
    - {(''.join([f'
  • {html.escape(factor)}
  • ' for factor in risk_assessment['risk_factors']]) if risk_assessment['risk_factors'] else '
  • No significant risk factors identified
  • ')} -
+
+ + +
+

👤 Patient Information Summary

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

👤 Patient Information Summary

-
-
Age: {html.escape(str(questionnaire_data.get('age', 'Not specified')))} years
-
Gender: {html.escape(str(questionnaire_data.get('patient_gender', 'Not specified')))}
-
Diabetic Status: {html.escape(str(questionnaire_data.get('diabetic', 'Unknown')))}
-
Pain Level: {html.escape(str(questionnaire_data.get('pain_level', 'Not assessed')))} / 10
-
Wound Duration: {html.escape(str(questionnaire_data.get('wound_duration', 'Not specified')))}
-
Moisture Level: {html.escape(str(questionnaire_data.get('moisture', 'Not assessed')))}
+ + + {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.
  • +
- {f"
Medical History: {html.escape(str(questionnaire_data.get('medical_history', 'None provided')))}
" if questionnaire_data.get('medical_history') else ""} - {f"
Current Medications: {html.escape(str(questionnaire_data.get('medications', 'None listed')))}
" if questionnaire_data.get('medications') else ""} - {f"
Known Allergies: {html.escape(str(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 and educational purposes only and does not constitute medical advice, diagnosis, or treatment recommendations.
  • -
  • Professional Consultation Required: Always consult with qualified healthcare professionals for proper clinical assessment, diagnosis, and treatment decisions.
  • -
  • Measurement Accuracy: All measurements are estimates based on computer vision algorithms and should be verified with clinical measurement tools.
  • -
  • Risk Assessment Limitations: Risk factors are based on provided information and may not reflect the complete clinical picture or patient history.
  • -
  • Technology Limitations: AI analysis may not detect all clinically relevant features and should be used as a supplementary tool only.
  • -
-
- - -
-
-

🏥 SmartHeal AI Platform

+ + +
+

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

-

- Revolutionising Healthcare with Precision Technology
- 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: @@ -1821,13 +1901,13 @@ button.gr-button-secondary:hover { # Diabetes diabetic_status = str(questionnaire_data.get('diabetic', '')).lower() - if 'yes' in diabetic_status or any(t in diabetic_status for t in ['type 1', 'type 2', 'gestational']): + 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 or questionnaire_data.get('infection_signs', 'None') != 'None': + if 'yes' in infection: risk_factors.append("Signs of infection present") risk_score += 3 @@ -1892,4 +1972,3 @@ button.gr-button-secondary:hover { 'risk_level': 'Unknown', 'risk_factors': ['Unable to assess risk due to data processing error'] } -