Upload 16 files
Browse files- .gitattributes +3 -0
- enhanced_app.py +180 -0
- favicon.ico +0 -0
- requirements.txt +57 -0
- src/IWGDF Guideline.pdf +3 -0
- src/auth.py +164 -0
- src/auth_manager.py +143 -0
- src/best.pt +3 -0
- src/config.py +76 -0
- src/dashboard_api.py +408 -0
- src/dashboard_database_manager.py +571 -0
- src/eHealth in Wound Care.pdf +3 -0
- src/enhanced_ai_processor.py +613 -0
- src/enhanced_ui_components.py +890 -0
- src/evaluation.pdf +3 -0
- src/segmentation_model.h5 +3 -0
- static/style.css +391 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
src/eHealth[[:space:]]in[[:space:]]Wound[[:space:]]Care.pdf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
src/evaluation.pdf filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
src/IWGDF[[:space:]]Guideline.pdf filter=lfs diff=lfs merge=lfs -text
|
enhanced_app.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
import logging
|
| 6 |
+
import traceback
|
| 7 |
+
import gradio as gr
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
# Add src directory to path
|
| 11 |
+
sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
|
| 12 |
+
|
| 13 |
+
# Import original modules (copy from original bot)
|
| 14 |
+
from src.config import Config
|
| 15 |
+
from src.auth import AuthManager
|
| 16 |
+
|
| 17 |
+
# Import enhanced modules
|
| 18 |
+
from src.dashboard_database_manager import DashboardDatabaseManager
|
| 19 |
+
from src.enhanced_ai_processor import EnhancedAIProcessor
|
| 20 |
+
from src.enhanced_ui_components import EnhancedUIComponents
|
| 21 |
+
|
| 22 |
+
# Logging setup
|
| 23 |
+
logging.basicConfig(
|
| 24 |
+
level=logging.INFO,
|
| 25 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
| 26 |
+
handlers=[
|
| 27 |
+
logging.FileHandler('smartheal_enhanced.log'),
|
| 28 |
+
logging.StreamHandler()
|
| 29 |
+
]
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
class EnhancedSmartHealApp:
|
| 33 |
+
"""Enhanced SmartHeal Application with Dashboard Integration"""
|
| 34 |
+
|
| 35 |
+
def __init__(self):
|
| 36 |
+
self.ui_components = None
|
| 37 |
+
try:
|
| 38 |
+
logging.info("🚀 Initializing Enhanced SmartHeal App...")
|
| 39 |
+
|
| 40 |
+
# Initialize configuration
|
| 41 |
+
self.config = Config()
|
| 42 |
+
logging.info("✅ Configuration loaded")
|
| 43 |
+
|
| 44 |
+
# Initialize enhanced database manager
|
| 45 |
+
self.database_manager = DashboardDatabaseManager(self.config.get_mysql_config())
|
| 46 |
+
logging.info("✅ Enhanced database manager initialized")
|
| 47 |
+
|
| 48 |
+
# Initialize authentication manager
|
| 49 |
+
self.auth_manager = AuthManager(self.database_manager)
|
| 50 |
+
logging.info("✅ Authentication manager initialized")
|
| 51 |
+
|
| 52 |
+
# Initialize enhanced AI processor
|
| 53 |
+
self.ai_processor = EnhancedAIProcessor()
|
| 54 |
+
logging.info("✅ Enhanced AI processor initialized")
|
| 55 |
+
|
| 56 |
+
# Initialize enhanced UI components
|
| 57 |
+
self.ui_components = EnhancedUIComponents(
|
| 58 |
+
self.auth_manager,
|
| 59 |
+
self.database_manager,
|
| 60 |
+
self.ai_processor
|
| 61 |
+
)
|
| 62 |
+
logging.info("✅ Enhanced UI components initialized")
|
| 63 |
+
|
| 64 |
+
# Create database tables if they don't exist
|
| 65 |
+
self._ensure_database_tables()
|
| 66 |
+
|
| 67 |
+
logging.info("🎉 Enhanced SmartHeal App initialized successfully!")
|
| 68 |
+
|
| 69 |
+
except Exception as e:
|
| 70 |
+
logging.error(f"❌ Initialization error: {e}")
|
| 71 |
+
traceback.print_exc()
|
| 72 |
+
raise
|
| 73 |
+
|
| 74 |
+
def _ensure_database_tables(self):
|
| 75 |
+
"""Ensure all required database tables exist"""
|
| 76 |
+
try:
|
| 77 |
+
# Check if dashboard tables exist, if not create them
|
| 78 |
+
tables_to_check = [
|
| 79 |
+
'ai_analyses',
|
| 80 |
+
'analysis_sessions',
|
| 81 |
+
'bot_interactions',
|
| 82 |
+
'questionnaire_responses',
|
| 83 |
+
'wound_images'
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
for table in tables_to_check:
|
| 87 |
+
result = self.database_manager.execute_query_one(f"SHOW TABLES LIKE '{table}'")
|
| 88 |
+
if not result:
|
| 89 |
+
logging.warning(f"Table '{table}' not found in database")
|
| 90 |
+
else:
|
| 91 |
+
logging.info(f"✅ Table '{table}' exists")
|
| 92 |
+
|
| 93 |
+
logging.info("✅ Database table check completed")
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
logging.error(f"❌ Error checking database tables: {e}")
|
| 97 |
+
|
| 98 |
+
def launch(self, port=7860, share=True, server_name="0.0.0.0"):
|
| 99 |
+
"""Launch the enhanced application"""
|
| 100 |
+
try:
|
| 101 |
+
logging.info(f"🌐 Launching Enhanced SmartHeal App on {server_name}:{port}")
|
| 102 |
+
|
| 103 |
+
# Create the interface
|
| 104 |
+
interface = self.ui_components.create_interface()
|
| 105 |
+
|
| 106 |
+
# Launch with enhanced configuration
|
| 107 |
+
interface.launch(
|
| 108 |
+
server_name=server_name,
|
| 109 |
+
server_port=port,
|
| 110 |
+
share=share,
|
| 111 |
+
show_error=True,
|
| 112 |
+
quiet=False,
|
| 113 |
+
favicon_path="favicon.ico" if os.path.exists("favicon.ico") else None
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
except Exception as e:
|
| 117 |
+
logging.error(f"❌ Error launching application: {e}")
|
| 118 |
+
raise
|
| 119 |
+
|
| 120 |
+
def get_status(self):
|
| 121 |
+
"""Get application status"""
|
| 122 |
+
try:
|
| 123 |
+
status = {
|
| 124 |
+
'app_initialized': self.ui_components is not None,
|
| 125 |
+
'database_connected': self.database_manager.get_connection() is not None,
|
| 126 |
+
'ai_models_loaded': len(self.ai_processor.models_cache) > 0,
|
| 127 |
+
'dashboard_integration': self.ui_components.dashboard_integration.get_integration_status() if self.ui_components else {},
|
| 128 |
+
'timestamp': datetime.now().isoformat()
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
return status
|
| 132 |
+
|
| 133 |
+
except Exception as e:
|
| 134 |
+
logging.error(f"Error getting status: {e}")
|
| 135 |
+
return {'error': str(e), 'timestamp': datetime.now().isoformat()}
|
| 136 |
+
|
| 137 |
+
def main():
|
| 138 |
+
"""Main application entry point"""
|
| 139 |
+
try:
|
| 140 |
+
print("=" * 60)
|
| 141 |
+
print("🏥 SmartHeal AI - Enhanced Edition")
|
| 142 |
+
print("Advanced Wound Care Analysis with Dashboard Integration")
|
| 143 |
+
print("=" * 60)
|
| 144 |
+
|
| 145 |
+
# Initialize and launch the app
|
| 146 |
+
app = EnhancedSmartHealApp()
|
| 147 |
+
|
| 148 |
+
# Print status
|
| 149 |
+
status = app.get_status()
|
| 150 |
+
print("\n📊 Application Status:")
|
| 151 |
+
print(f" • App Initialized: {status.get('app_initialized', False)}")
|
| 152 |
+
print(f" • Database Connected: {status.get('database_connected', False)}")
|
| 153 |
+
print(f" • AI Models Loaded: {status.get('ai_models_loaded', False)}")
|
| 154 |
+
|
| 155 |
+
dashboard_status = status.get('dashboard_integration', {})
|
| 156 |
+
print(f" • Dashboard API: {dashboard_status.get('api_running', False)}")
|
| 157 |
+
print(f" • Dashboard DB: {dashboard_status.get('database_connected', False)}")
|
| 158 |
+
|
| 159 |
+
print("\n🚀 Starting application...")
|
| 160 |
+
print("📱 Access the application at: http://localhost:7860")
|
| 161 |
+
print("📊 Dashboard API available at: http://localhost:5001")
|
| 162 |
+
print("📈 Dashboard Integration: Real-time analytics enabled")
|
| 163 |
+
print("\n⚠️ Press Ctrl+C to stop the application")
|
| 164 |
+
print("=" * 60)
|
| 165 |
+
|
| 166 |
+
# Launch the application
|
| 167 |
+
app.launch()
|
| 168 |
+
|
| 169 |
+
except KeyboardInterrupt:
|
| 170 |
+
print("\n\n👋 Application interrupted by user.")
|
| 171 |
+
logging.info("Application interrupted by user")
|
| 172 |
+
except Exception as e:
|
| 173 |
+
print(f"\n❌ Application failed to start: {e}")
|
| 174 |
+
logging.error(f"Application failed to start: {e}")
|
| 175 |
+
traceback.print_exc()
|
| 176 |
+
raise
|
| 177 |
+
|
| 178 |
+
if __name__ == "__main__":
|
| 179 |
+
main()
|
| 180 |
+
|
favicon.ico
ADDED
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Enhanced SmartHeal Bot Requirements
|
| 2 |
+
# --- Core Python Packages ---
|
| 3 |
+
numpy
|
| 4 |
+
pillow
|
| 5 |
+
opencv-python
|
| 6 |
+
matplotlib
|
| 7 |
+
requests
|
| 8 |
+
tqdm
|
| 9 |
+
pypdf
|
| 10 |
+
|
| 11 |
+
# --- Deep Learning ---
|
| 12 |
+
torch>=2.0.0
|
| 13 |
+
tensorflow
|
| 14 |
+
transformers>=4.40.0
|
| 15 |
+
accelerate
|
| 16 |
+
bitsandbytes
|
| 17 |
+
tf-keras
|
| 18 |
+
|
| 19 |
+
# --- Hugging Face ---
|
| 20 |
+
huggingface-hub
|
| 21 |
+
sentence-transformers
|
| 22 |
+
|
| 23 |
+
# --- LangChain & RAG ---
|
| 24 |
+
langchain>=0.1.14
|
| 25 |
+
faiss-cpu
|
| 26 |
+
pymupdf
|
| 27 |
+
langchain-community
|
| 28 |
+
|
| 29 |
+
# --- YOLO Detection ---
|
| 30 |
+
ultralytics
|
| 31 |
+
|
| 32 |
+
# --- Gradio UI ---
|
| 33 |
+
gradio>=4.28.0
|
| 34 |
+
spaces
|
| 35 |
+
|
| 36 |
+
# --- Logging & Utility ---
|
| 37 |
+
python-dotenv
|
| 38 |
+
|
| 39 |
+
# --- Database ---
|
| 40 |
+
mysql-connector-python>=8.0.0
|
| 41 |
+
|
| 42 |
+
# --- Enhanced Dashboard Integration ---
|
| 43 |
+
flask>=2.3.0
|
| 44 |
+
flask-cors>=4.0.0
|
| 45 |
+
flask-login>=0.6.0
|
| 46 |
+
sqlalchemy>=1.4.0
|
| 47 |
+
werkzeug>=2.3.0
|
| 48 |
+
bcrypt>=4.0.0
|
| 49 |
+
|
| 50 |
+
# --- Additional Analytics ---
|
| 51 |
+
pandas>=1.5.0
|
| 52 |
+
scikit-learn>=1.1.0
|
| 53 |
+
seaborn>=0.11.0
|
| 54 |
+
|
| 55 |
+
# --- Development Tools ---
|
| 56 |
+
pytest>=7.0.0
|
| 57 |
+
black>=22.0.0
|
src/IWGDF Guideline.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:da4378da81d438142a096c37f21bd47270b44fd13e9177732ee5a1b02c0a8703
|
| 3 |
+
size 1032231
|
src/auth.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
import hashlib
|
| 4 |
+
|
| 5 |
+
class AuthManager:
|
| 6 |
+
"""Authentication and user management"""
|
| 7 |
+
|
| 8 |
+
def __init__(self, db_manager):
|
| 9 |
+
"""Initialize auth manager with database manager"""
|
| 10 |
+
self.db = db_manager
|
| 11 |
+
|
| 12 |
+
def hash_password(self, password):
|
| 13 |
+
"""Simple password hashing (in production, use bcrypt or similar)"""
|
| 14 |
+
return hashlib.sha256(password.encode()).hexdigest()
|
| 15 |
+
|
| 16 |
+
def authenticate_user(self, username, password):
|
| 17 |
+
"""Authenticate user and return user data"""
|
| 18 |
+
try:
|
| 19 |
+
user = self.db.execute_query_one(
|
| 20 |
+
"SELECT id, username, email, name, role, org FROM users WHERE username = %s",
|
| 21 |
+
(username,)
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
if not user:
|
| 25 |
+
logging.warning(f"User not found: {username}")
|
| 26 |
+
return None
|
| 27 |
+
|
| 28 |
+
user_password = self.db.execute_query_one(
|
| 29 |
+
"SELECT password FROM users WHERE username = %s",
|
| 30 |
+
(username,)
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
if not user_password:
|
| 34 |
+
logging.warning(f"Password not found for user: {username}")
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
stored_password = user_password['password']
|
| 38 |
+
|
| 39 |
+
# Check if stored password is hashed (64 chars) or plain text
|
| 40 |
+
is_hashed = len(stored_password) == 64 and all(c in '0123456789abcdef' for c in stored_password)
|
| 41 |
+
|
| 42 |
+
if is_hashed:
|
| 43 |
+
# Compare with hashed input
|
| 44 |
+
password_match = stored_password == self.hash_password(password)
|
| 45 |
+
else:
|
| 46 |
+
# Compare with plain text (for legacy compatibility)
|
| 47 |
+
password_match = stored_password == password
|
| 48 |
+
|
| 49 |
+
logging.info(f"Debug - Username: {username}, Password type: {'hashed' if is_hashed else 'plain'}")
|
| 50 |
+
|
| 51 |
+
if not password_match:
|
| 52 |
+
logging.warning(f"Invalid password for user: {username}")
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
# Update last login (use updated_at since last_login column may not exist)
|
| 56 |
+
try:
|
| 57 |
+
self.db.execute_query(
|
| 58 |
+
"UPDATE users SET updated_at = %s WHERE id = %s",
|
| 59 |
+
(datetime.now(), user['id'])
|
| 60 |
+
)
|
| 61 |
+
except Exception as e:
|
| 62 |
+
logging.warning(f"Could not update last login: {e}")
|
| 63 |
+
|
| 64 |
+
logging.info(f"User authenticated successfully: {username}")
|
| 65 |
+
return user
|
| 66 |
+
|
| 67 |
+
except Exception as e:
|
| 68 |
+
logging.error(f"Authentication error: {e}")
|
| 69 |
+
return None
|
| 70 |
+
|
| 71 |
+
def get_user_data(self, username):
|
| 72 |
+
"""Get user data by username - for compatibility with UI"""
|
| 73 |
+
try:
|
| 74 |
+
user = self.db.execute_query_one(
|
| 75 |
+
"SELECT id, username, email, name, role, org FROM users WHERE username = %s",
|
| 76 |
+
(username,)
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
if user:
|
| 80 |
+
logging.info(f"Retrieved user data for: {username}")
|
| 81 |
+
return user
|
| 82 |
+
else:
|
| 83 |
+
logging.warning(f"User not found: {username}")
|
| 84 |
+
return None
|
| 85 |
+
|
| 86 |
+
except Exception as e:
|
| 87 |
+
logging.error(f"Error getting user data: {e}")
|
| 88 |
+
return None
|
| 89 |
+
|
| 90 |
+
def create_user(self, username, email, password, name, role, org_name="",
|
| 91 |
+
phone="", country_code="", department="", location="", organization_id=None):
|
| 92 |
+
"""Create a new user account"""
|
| 93 |
+
try:
|
| 94 |
+
# Check if user already exists
|
| 95 |
+
existing_user = self.db.execute_query_one(
|
| 96 |
+
"SELECT id FROM users WHERE username = %s OR email = %s",
|
| 97 |
+
(username, email)
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
if existing_user:
|
| 101 |
+
logging.warning(f"User already exists: {username} or {email}")
|
| 102 |
+
return False
|
| 103 |
+
|
| 104 |
+
# Create organization if role is organization
|
| 105 |
+
if role == 'organization':
|
| 106 |
+
org_result = self.db.execute_query(
|
| 107 |
+
"""INSERT INTO organizations (name, email, phone, country_code, department, location, created_at)
|
| 108 |
+
VALUES (%s, %s, %s, %s, %s, %s, %s)""",
|
| 109 |
+
(org_name or name, email, phone, country_code, department, location, datetime.now())
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
if not org_result:
|
| 113 |
+
logging.error("Failed to create organization")
|
| 114 |
+
return False
|
| 115 |
+
|
| 116 |
+
# Get the organization ID
|
| 117 |
+
org_id = self.db.execute_query_one(
|
| 118 |
+
"SELECT id FROM organizations WHERE email = %s ORDER BY created_at DESC LIMIT 1",
|
| 119 |
+
(email,)
|
| 120 |
+
)
|
| 121 |
+
organization_id = org_id['id'] if org_id else None
|
| 122 |
+
|
| 123 |
+
# Create user with hashed password
|
| 124 |
+
hashed_password = self.hash_password(password)
|
| 125 |
+
user_result = self.db.execute_query(
|
| 126 |
+
"""INSERT INTO users (username, email, password, name, role, org, created_at)
|
| 127 |
+
VALUES (%s, %s, %s, %s, %s, %s, %s)""",
|
| 128 |
+
(username, email, hashed_password, name, role, organization_id, datetime.now())
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
if user_result:
|
| 132 |
+
logging.info(f"User created successfully: {username}")
|
| 133 |
+
return True
|
| 134 |
+
else:
|
| 135 |
+
logging.error(f"Failed to create user: {username}")
|
| 136 |
+
return False
|
| 137 |
+
|
| 138 |
+
except Exception as e:
|
| 139 |
+
logging.error(f"User creation error: {e}")
|
| 140 |
+
return False
|
| 141 |
+
|
| 142 |
+
def get_user_by_id(self, user_id):
|
| 143 |
+
"""Get user by ID"""
|
| 144 |
+
try:
|
| 145 |
+
user = self.db.execute_query_one(
|
| 146 |
+
"SELECT id, username, email, name, role, org FROM users WHERE id = %s",
|
| 147 |
+
(user_id,)
|
| 148 |
+
)
|
| 149 |
+
return user
|
| 150 |
+
except Exception as e:
|
| 151 |
+
logging.error(f"Error fetching user by ID: {e}")
|
| 152 |
+
return None
|
| 153 |
+
|
| 154 |
+
def update_user_last_login(self, user_id):
|
| 155 |
+
"""Update user's last login timestamp"""
|
| 156 |
+
try:
|
| 157 |
+
result = self.db.execute_query(
|
| 158 |
+
"UPDATE users SET updated_at = %s WHERE id = %s",
|
| 159 |
+
(datetime.now(), user_id)
|
| 160 |
+
)
|
| 161 |
+
return bool(result)
|
| 162 |
+
except Exception as e:
|
| 163 |
+
logging.error(f"Error updating last login: {e}")
|
| 164 |
+
return False
|
src/auth_manager.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from src.database_manager import DatabaseManager
|
| 4 |
+
|
| 5 |
+
class AuthManager:
|
| 6 |
+
def __init__(self):
|
| 7 |
+
self.db_manager = DatabaseManager()
|
| 8 |
+
|
| 9 |
+
def authenticate_user(self, username, password):
|
| 10 |
+
"""Authenticate user and return user data"""
|
| 11 |
+
try:
|
| 12 |
+
# Find user by username
|
| 13 |
+
user = self.db_manager.execute_query_one(
|
| 14 |
+
"SELECT id, username, email, name, role, org FROM users WHERE username = %s",
|
| 15 |
+
(username,)
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
if not user:
|
| 19 |
+
return None
|
| 20 |
+
|
| 21 |
+
# Check password (plaintext comparison as per requirements)
|
| 22 |
+
user_password = self.db_manager.execute_query_one(
|
| 23 |
+
"SELECT password FROM users WHERE username = %s",
|
| 24 |
+
(username,)
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
if not user_password or user_password['password'] != password:
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
# Update last login
|
| 31 |
+
self.db_manager.execute_query(
|
| 32 |
+
"UPDATE users SET last_login = %s WHERE id = %s",
|
| 33 |
+
(datetime.now(), user['id'])
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
return user
|
| 37 |
+
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logging.error(f"Authentication error: {e}")
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
def create_user(self, username, email, password, name, role, org_name="", phone="",
|
| 43 |
+
country_code="", department="", location="", organization_id=None):
|
| 44 |
+
"""Create a new user account"""
|
| 45 |
+
try:
|
| 46 |
+
# Check if username or email already exists
|
| 47 |
+
existing_user = self.db_manager.execute_query_one(
|
| 48 |
+
"SELECT id FROM users WHERE username = %s OR email = %s",
|
| 49 |
+
(username, email)
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
if existing_user:
|
| 53 |
+
return False
|
| 54 |
+
|
| 55 |
+
# Handle organization creation
|
| 56 |
+
if role == 'organization':
|
| 57 |
+
# Create organization entry first
|
| 58 |
+
org_result = self.db_manager.execute_query(
|
| 59 |
+
"""INSERT INTO organizations (name, email, phone, country_code, department, location, created_at)
|
| 60 |
+
VALUES (%s, %s, %s, %s, %s, %s, %s)""",
|
| 61 |
+
(org_name or name, email, phone, country_code, department, location, datetime.now())
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
if not org_result:
|
| 65 |
+
return False
|
| 66 |
+
|
| 67 |
+
# Get the organization ID
|
| 68 |
+
org_id = self.db_manager.execute_query_one(
|
| 69 |
+
"SELECT id FROM organizations WHERE email = %s ORDER BY created_at DESC LIMIT 1",
|
| 70 |
+
(email,)
|
| 71 |
+
)
|
| 72 |
+
organization_id = org_id['id'] if org_id else None
|
| 73 |
+
|
| 74 |
+
# Create user entry
|
| 75 |
+
user_result = self.db_manager.execute_query(
|
| 76 |
+
"""INSERT INTO users (username, email, password, name, role, org, created_at)
|
| 77 |
+
VALUES (%s, %s, %s, %s, %s, %s, %s)""",
|
| 78 |
+
(username, email, password, name, role, organization_id, datetime.now())
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
return bool(user_result)
|
| 82 |
+
|
| 83 |
+
except Exception as e:
|
| 84 |
+
logging.error(f"User creation error: {e}")
|
| 85 |
+
return False
|
| 86 |
+
|
| 87 |
+
def get_organizations(self):
|
| 88 |
+
"""Get list of all organizations"""
|
| 89 |
+
try:
|
| 90 |
+
organizations = self.db_manager.execute_query(
|
| 91 |
+
"SELECT id, name FROM organizations ORDER BY name",
|
| 92 |
+
fetch=True
|
| 93 |
+
)
|
| 94 |
+
return organizations or []
|
| 95 |
+
except Exception as e:
|
| 96 |
+
logging.error(f"Error fetching organizations: {e}")
|
| 97 |
+
return []
|
| 98 |
+
|
| 99 |
+
def get_organization_practitioners(self, organization_id):
|
| 100 |
+
"""Get practitioners for an organization"""
|
| 101 |
+
try:
|
| 102 |
+
practitioners = self.db_manager.execute_query(
|
| 103 |
+
"""SELECT id, username, name, email, created_at, last_login
|
| 104 |
+
FROM users WHERE org = %s AND role = 'practitioner'
|
| 105 |
+
ORDER BY created_at DESC""",
|
| 106 |
+
(organization_id,),
|
| 107 |
+
fetch=True
|
| 108 |
+
)
|
| 109 |
+
return practitioners or []
|
| 110 |
+
except Exception as e:
|
| 111 |
+
logging.error(f"Error fetching practitioners: {e}")
|
| 112 |
+
return []
|
| 113 |
+
|
| 114 |
+
def update_user_profile(self, user_id, name=None, email=None):
|
| 115 |
+
"""Update user profile information"""
|
| 116 |
+
try:
|
| 117 |
+
if name:
|
| 118 |
+
self.db_manager.execute_query(
|
| 119 |
+
"UPDATE users SET name = %s WHERE id = %s",
|
| 120 |
+
(name, user_id)
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
if email:
|
| 124 |
+
# Check if email is already taken by another user
|
| 125 |
+
existing = self.db_manager.execute_query_one(
|
| 126 |
+
"SELECT id FROM users WHERE email = %s AND id != %s",
|
| 127 |
+
(email, user_id)
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
if not existing:
|
| 131 |
+
self.db_manager.execute_query(
|
| 132 |
+
"UPDATE users SET email = %s WHERE id = %s",
|
| 133 |
+
(email, user_id)
|
| 134 |
+
)
|
| 135 |
+
return True
|
| 136 |
+
else:
|
| 137 |
+
return False
|
| 138 |
+
|
| 139 |
+
return True
|
| 140 |
+
|
| 141 |
+
except Exception as e:
|
| 142 |
+
logging.error(f"Error updating user profile: {e}")
|
| 143 |
+
return False
|
src/best.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dc7b01e2bf4e70eca6321059de6f0965612ad5cca55154e8adb0ea15c77e350d
|
| 3 |
+
size 136689065
|
src/config.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import logging
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
class Config:
|
| 6 |
+
"""Configuration management for SmartHeal application"""
|
| 7 |
+
|
| 8 |
+
def __init__(self):
|
| 9 |
+
"""Initialize configuration with environment variables and defaults"""
|
| 10 |
+
|
| 11 |
+
# Database configuration
|
| 12 |
+
self.MYSQL_CONFIG = {
|
| 13 |
+
"host": os.getenv("MYSQL_HOST", "sg-nme-web545.main-hosting.eu"),
|
| 14 |
+
"user": os.getenv("MYSQL_USER", "u124249738_SmartHealApp"),
|
| 15 |
+
"password": os.getenv("MYSQL_PASSWORD", "I^4y1b12y"),
|
| 16 |
+
"database": os.getenv("MYSQL_DATABASE", "u124249738_SmartHealAppDB"),
|
| 17 |
+
"port": int(os.getenv("MYSQL_PORT", 3306))
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
# Application directories
|
| 21 |
+
self.UPLOADS_DIR = os.getenv("UPLOADS_DIR", "uploads")
|
| 22 |
+
self.MODELS_DIR = os.getenv("MODELS_DIR", "models")
|
| 23 |
+
|
| 24 |
+
# AI/ML configuration
|
| 25 |
+
self.HF_TOKEN = os.getenv("HF_TOKEN")
|
| 26 |
+
self.OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 27 |
+
|
| 28 |
+
# AI Model Configuration
|
| 29 |
+
# AI Model Configuration
|
| 30 |
+
self.YOLO_MODEL_PATH = os.getenv("YOLO_MODEL_PATH", "src/best.pt")
|
| 31 |
+
self.SEG_MODEL_PATH = os.getenv("SEG_MODEL_PATH", "src/segmentation_model.h5")
|
| 32 |
+
|
| 33 |
+
self.MEDGEMMA_MODEL = os.getenv("MEDGEMMA_MODEL", "google/medgemma-4b-it")
|
| 34 |
+
self.WOUND_CLASSIFICATION_MODEL = os.getenv("WOUND_CLASSIFICATION_MODEL", "Hemg/Wound-classification")
|
| 35 |
+
self.EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
|
| 36 |
+
|
| 37 |
+
# AI Processing Configuration
|
| 38 |
+
self.PIXELS_PER_CM = int(os.getenv("PIXELS_PER_CM", "37"))
|
| 39 |
+
self.MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "512"))
|
| 40 |
+
self.DATASET_ID = os.getenv("DATASET_ID", "")
|
| 41 |
+
self.GUIDELINE_PDFS = [
|
| 42 |
+
os.path.join("src", "eHealth in Wound Care.pdf"),
|
| 43 |
+
os.path.join("src", "IWGDF Guideline.pdf"),
|
| 44 |
+
os.path.join("src", "evaluation.pdf")
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# Application settings
|
| 49 |
+
self.DEBUG = os.getenv("DEBUG", "False").lower() == "true"
|
| 50 |
+
self.LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
| 51 |
+
|
| 52 |
+
# Create required directories
|
| 53 |
+
self._create_directories()
|
| 54 |
+
|
| 55 |
+
logging.info("✅ Configuration initialized")
|
| 56 |
+
|
| 57 |
+
def _create_directories(self):
|
| 58 |
+
"""Create required directories if they don't exist"""
|
| 59 |
+
directories = [self.UPLOADS_DIR, self.MODELS_DIR]
|
| 60 |
+
|
| 61 |
+
for directory in directories:
|
| 62 |
+
if not os.path.exists(directory):
|
| 63 |
+
os.makedirs(directory, exist_ok=True)
|
| 64 |
+
logging.info(f"Created directory: {directory}")
|
| 65 |
+
|
| 66 |
+
def get_mysql_config(self):
|
| 67 |
+
"""Get MySQL configuration dictionary"""
|
| 68 |
+
return self.MYSQL_CONFIG.copy()
|
| 69 |
+
|
| 70 |
+
def get_upload_path(self, filename):
|
| 71 |
+
"""Get full path for uploaded file"""
|
| 72 |
+
return os.path.join(self.UPLOADS_DIR, filename)
|
| 73 |
+
|
| 74 |
+
def get_model_path(self, model_name):
|
| 75 |
+
"""Get full path for model file"""
|
| 76 |
+
return os.path.join(self.MODELS_DIR, model_name)
|
src/dashboard_api.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, jsonify, request
|
| 2 |
+
from flask_cors import CORS
|
| 3 |
+
import logging
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
import json
|
| 6 |
+
from typing import Dict, Any, List, Optional
|
| 7 |
+
import threading
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
from .dashboard_database_manager import DashboardDatabaseManager
|
| 11 |
+
|
| 12 |
+
class DashboardAPI:
|
| 13 |
+
"""API layer for dashboard integration"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, database_manager: DashboardDatabaseManager, port: int = 5001):
|
| 16 |
+
self.database_manager = database_manager
|
| 17 |
+
self.port = port
|
| 18 |
+
self.app = Flask(__name__)
|
| 19 |
+
CORS(self.app) # Enable CORS for all routes
|
| 20 |
+
|
| 21 |
+
# Configure logging
|
| 22 |
+
logging.basicConfig(level=logging.INFO)
|
| 23 |
+
self.logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
# Setup routes
|
| 26 |
+
self._setup_routes()
|
| 27 |
+
|
| 28 |
+
# Background thread for API server
|
| 29 |
+
self.api_thread = None
|
| 30 |
+
self.running = False
|
| 31 |
+
|
| 32 |
+
def _setup_routes(self):
|
| 33 |
+
"""Setup API routes for dashboard integration"""
|
| 34 |
+
|
| 35 |
+
@self.app.route('/api/health', methods=['GET'])
|
| 36 |
+
def health_check():
|
| 37 |
+
"""Health check endpoint"""
|
| 38 |
+
return jsonify({
|
| 39 |
+
'status': 'healthy',
|
| 40 |
+
'timestamp': datetime.now().isoformat(),
|
| 41 |
+
'service': 'SmartHeal Bot API'
|
| 42 |
+
})
|
| 43 |
+
|
| 44 |
+
@self.app.route('/api/bot/analytics', methods=['GET'])
|
| 45 |
+
def get_bot_analytics():
|
| 46 |
+
"""Get comprehensive bot analytics for dashboard"""
|
| 47 |
+
try:
|
| 48 |
+
analytics_data = self.database_manager.get_analytics_data()
|
| 49 |
+
|
| 50 |
+
# Add trend data for charts
|
| 51 |
+
analytics_data.update(self._get_trend_data())
|
| 52 |
+
|
| 53 |
+
return jsonify({
|
| 54 |
+
'success': True,
|
| 55 |
+
'data': analytics_data,
|
| 56 |
+
'timestamp': datetime.now().isoformat()
|
| 57 |
+
})
|
| 58 |
+
|
| 59 |
+
except Exception as e:
|
| 60 |
+
self.logger.error(f"Error getting bot analytics: {e}")
|
| 61 |
+
return jsonify({
|
| 62 |
+
'success': False,
|
| 63 |
+
'error': str(e),
|
| 64 |
+
'timestamp': datetime.now().isoformat()
|
| 65 |
+
}), 500
|
| 66 |
+
|
| 67 |
+
@self.app.route('/api/bot/analytics/details/<int:analysis_id>', methods=['GET'])
|
| 68 |
+
def get_analysis_details(analysis_id):
|
| 69 |
+
"""Get detailed analysis information"""
|
| 70 |
+
try:
|
| 71 |
+
query = "SELECT * FROM ai_analyses WHERE id = %s"
|
| 72 |
+
analysis = self.database_manager.execute_query_one(query, (analysis_id,))
|
| 73 |
+
|
| 74 |
+
if not analysis:
|
| 75 |
+
return jsonify({
|
| 76 |
+
'success': False,
|
| 77 |
+
'error': 'Analysis not found'
|
| 78 |
+
}), 404
|
| 79 |
+
|
| 80 |
+
# Convert datetime objects to strings for JSON serialization
|
| 81 |
+
if analysis.get('created_at'):
|
| 82 |
+
analysis['created_at'] = analysis['created_at'].isoformat()
|
| 83 |
+
|
| 84 |
+
return jsonify({
|
| 85 |
+
'success': True,
|
| 86 |
+
'data': analysis,
|
| 87 |
+
'timestamp': datetime.now().isoformat()
|
| 88 |
+
})
|
| 89 |
+
|
| 90 |
+
except Exception as e:
|
| 91 |
+
self.logger.error(f"Error getting analysis details: {e}")
|
| 92 |
+
return jsonify({
|
| 93 |
+
'success': False,
|
| 94 |
+
'error': str(e)
|
| 95 |
+
}), 500
|
| 96 |
+
|
| 97 |
+
@self.app.route('/api/bot/interactions', methods=['GET'])
|
| 98 |
+
def get_bot_interactions():
|
| 99 |
+
"""Get bot interaction history"""
|
| 100 |
+
try:
|
| 101 |
+
limit = request.args.get('limit', 50, type=int)
|
| 102 |
+
interactions = self.database_manager.get_interaction_history(limit)
|
| 103 |
+
|
| 104 |
+
# Convert datetime objects for JSON serialization
|
| 105 |
+
for interaction in interactions:
|
| 106 |
+
if interaction.get('interacted_at'):
|
| 107 |
+
interaction['interacted_at'] = interaction['interacted_at'].isoformat()
|
| 108 |
+
|
| 109 |
+
return jsonify({
|
| 110 |
+
'success': True,
|
| 111 |
+
'data': interactions,
|
| 112 |
+
'count': len(interactions),
|
| 113 |
+
'timestamp': datetime.now().isoformat()
|
| 114 |
+
})
|
| 115 |
+
|
| 116 |
+
except Exception as e:
|
| 117 |
+
self.logger.error(f"Error getting bot interactions: {e}")
|
| 118 |
+
return jsonify({
|
| 119 |
+
'success': False,
|
| 120 |
+
'error': str(e)
|
| 121 |
+
}), 500
|
| 122 |
+
|
| 123 |
+
@self.app.route('/api/bot/sessions', methods=['GET'])
|
| 124 |
+
def get_session_analytics():
|
| 125 |
+
"""Get session analytics"""
|
| 126 |
+
try:
|
| 127 |
+
session_data = self.database_manager.get_session_analytics()
|
| 128 |
+
|
| 129 |
+
return jsonify({
|
| 130 |
+
'success': True,
|
| 131 |
+
'data': session_data,
|
| 132 |
+
'timestamp': datetime.now().isoformat()
|
| 133 |
+
})
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
self.logger.error(f"Error getting session analytics: {e}")
|
| 137 |
+
return jsonify({
|
| 138 |
+
'success': False,
|
| 139 |
+
'error': str(e)
|
| 140 |
+
}), 500
|
| 141 |
+
|
| 142 |
+
@self.app.route('/api/bot/stats/summary', methods=['GET'])
|
| 143 |
+
def get_summary_stats():
|
| 144 |
+
"""Get summary statistics for dashboard widgets"""
|
| 145 |
+
try:
|
| 146 |
+
# Get basic counts
|
| 147 |
+
total_analyses = self.database_manager.execute_query_one("SELECT COUNT(*) as count FROM ai_analyses")
|
| 148 |
+
total_patients = self.database_manager.execute_query_one("SELECT COUNT(DISTINCT patient_id) as count FROM bot_interactions WHERE patient_id IS NOT NULL")
|
| 149 |
+
total_sessions = self.database_manager.execute_query_one("SELECT COUNT(*) as count FROM analysis_sessions")
|
| 150 |
+
|
| 151 |
+
# Get today's activity
|
| 152 |
+
today_analyses = self.database_manager.execute_query_one("""
|
| 153 |
+
SELECT COUNT(*) as count FROM ai_analyses
|
| 154 |
+
WHERE DATE(created_at) = CURDATE()
|
| 155 |
+
""")
|
| 156 |
+
|
| 157 |
+
# Get average metrics
|
| 158 |
+
avg_processing_time = self.database_manager.execute_query_one("""
|
| 159 |
+
SELECT AVG(processing_time) as avg_time FROM ai_analyses
|
| 160 |
+
WHERE processing_time IS NOT NULL
|
| 161 |
+
""")
|
| 162 |
+
|
| 163 |
+
avg_risk_score = self.database_manager.execute_query_one("""
|
| 164 |
+
SELECT AVG(risk_score) as avg_risk FROM ai_analyses
|
| 165 |
+
WHERE risk_score IS NOT NULL
|
| 166 |
+
""")
|
| 167 |
+
|
| 168 |
+
summary = {
|
| 169 |
+
'total_analyses': total_analyses['count'] if total_analyses else 0,
|
| 170 |
+
'total_patients': total_patients['count'] if total_patients else 0,
|
| 171 |
+
'total_sessions': total_sessions['count'] if total_sessions else 0,
|
| 172 |
+
'today_analyses': today_analyses['count'] if today_analyses else 0,
|
| 173 |
+
'avg_processing_time': round(avg_processing_time['avg_time'], 2) if avg_processing_time and avg_processing_time['avg_time'] else 0,
|
| 174 |
+
'avg_risk_score': round(avg_risk_score['avg_risk'], 1) if avg_risk_score and avg_risk_score['avg_risk'] else 0
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
return jsonify({
|
| 178 |
+
'success': True,
|
| 179 |
+
'data': summary,
|
| 180 |
+
'timestamp': datetime.now().isoformat()
|
| 181 |
+
})
|
| 182 |
+
|
| 183 |
+
except Exception as e:
|
| 184 |
+
self.logger.error(f"Error getting summary stats: {e}")
|
| 185 |
+
return jsonify({
|
| 186 |
+
'success': False,
|
| 187 |
+
'error': str(e)
|
| 188 |
+
}), 500
|
| 189 |
+
|
| 190 |
+
@self.app.route('/api/bot/models/performance', methods=['GET'])
|
| 191 |
+
def get_model_performance():
|
| 192 |
+
"""Get AI model performance metrics"""
|
| 193 |
+
try:
|
| 194 |
+
performance_data = self.database_manager.execute_query("""
|
| 195 |
+
SELECT
|
| 196 |
+
model_version,
|
| 197 |
+
COUNT(*) as total_analyses,
|
| 198 |
+
AVG(processing_time) as avg_processing_time,
|
| 199 |
+
AVG(risk_score) as avg_risk_score,
|
| 200 |
+
MIN(created_at) as first_used,
|
| 201 |
+
MAX(created_at) as last_used
|
| 202 |
+
FROM ai_analyses
|
| 203 |
+
WHERE model_version IS NOT NULL
|
| 204 |
+
GROUP BY model_version
|
| 205 |
+
ORDER BY last_used DESC
|
| 206 |
+
""", fetch=True)
|
| 207 |
+
|
| 208 |
+
# Convert datetime objects for JSON serialization
|
| 209 |
+
for model in performance_data:
|
| 210 |
+
if model.get('first_used'):
|
| 211 |
+
model['first_used'] = model['first_used'].isoformat()
|
| 212 |
+
if model.get('last_used'):
|
| 213 |
+
model['last_used'] = model['last_used'].isoformat()
|
| 214 |
+
if model.get('avg_processing_time'):
|
| 215 |
+
model['avg_processing_time'] = round(model['avg_processing_time'], 2)
|
| 216 |
+
if model.get('avg_risk_score'):
|
| 217 |
+
model['avg_risk_score'] = round(model['avg_risk_score'], 1)
|
| 218 |
+
|
| 219 |
+
return jsonify({
|
| 220 |
+
'success': True,
|
| 221 |
+
'data': performance_data or [],
|
| 222 |
+
'timestamp': datetime.now().isoformat()
|
| 223 |
+
})
|
| 224 |
+
|
| 225 |
+
except Exception as e:
|
| 226 |
+
self.logger.error(f"Error getting model performance: {e}")
|
| 227 |
+
return jsonify({
|
| 228 |
+
'success': False,
|
| 229 |
+
'error': str(e)
|
| 230 |
+
}), 500
|
| 231 |
+
|
| 232 |
+
def _get_trend_data(self) -> Dict[str, Any]:
|
| 233 |
+
"""Get trend data for dashboard charts"""
|
| 234 |
+
try:
|
| 235 |
+
# Get last 30 days of analysis data
|
| 236 |
+
trend_data = self.database_manager.execute_query("""
|
| 237 |
+
SELECT
|
| 238 |
+
DATE(created_at) as analysis_date,
|
| 239 |
+
COUNT(*) as count
|
| 240 |
+
FROM ai_analyses
|
| 241 |
+
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
| 242 |
+
GROUP BY DATE(created_at)
|
| 243 |
+
ORDER BY analysis_date
|
| 244 |
+
""", fetch=True)
|
| 245 |
+
|
| 246 |
+
# Prepare data for Chart.js
|
| 247 |
+
labels = []
|
| 248 |
+
data = []
|
| 249 |
+
|
| 250 |
+
if trend_data:
|
| 251 |
+
for row in trend_data:
|
| 252 |
+
labels.append(row['analysis_date'].strftime('%Y-%m-%d'))
|
| 253 |
+
data.append(row['count'])
|
| 254 |
+
|
| 255 |
+
# Get risk level distribution
|
| 256 |
+
risk_distribution = self.database_manager.execute_query("""
|
| 257 |
+
SELECT risk_level, COUNT(*) as count
|
| 258 |
+
FROM ai_analyses
|
| 259 |
+
GROUP BY risk_level
|
| 260 |
+
""", fetch=True)
|
| 261 |
+
|
| 262 |
+
risk_labels = []
|
| 263 |
+
risk_data = []
|
| 264 |
+
|
| 265 |
+
if risk_distribution:
|
| 266 |
+
for row in risk_distribution:
|
| 267 |
+
risk_labels.append(row['risk_level'])
|
| 268 |
+
risk_data.append(row['count'])
|
| 269 |
+
|
| 270 |
+
# Get processing time distribution
|
| 271 |
+
processing_time_data = self.database_manager.execute_query("""
|
| 272 |
+
SELECT
|
| 273 |
+
CASE
|
| 274 |
+
WHEN processing_time < 1 THEN '< 1s'
|
| 275 |
+
WHEN processing_time < 2 THEN '1-2s'
|
| 276 |
+
WHEN processing_time < 5 THEN '2-5s'
|
| 277 |
+
WHEN processing_time < 10 THEN '5-10s'
|
| 278 |
+
ELSE '> 10s'
|
| 279 |
+
END as time_range,
|
| 280 |
+
COUNT(*) as count
|
| 281 |
+
FROM ai_analyses
|
| 282 |
+
WHERE processing_time IS NOT NULL
|
| 283 |
+
GROUP BY time_range
|
| 284 |
+
ORDER BY
|
| 285 |
+
CASE
|
| 286 |
+
WHEN processing_time < 1 THEN 1
|
| 287 |
+
WHEN processing_time < 2 THEN 2
|
| 288 |
+
WHEN processing_time < 5 THEN 3
|
| 289 |
+
WHEN processing_time < 10 THEN 4
|
| 290 |
+
ELSE 5
|
| 291 |
+
END
|
| 292 |
+
""", fetch=True)
|
| 293 |
+
|
| 294 |
+
processing_labels = []
|
| 295 |
+
processing_data = []
|
| 296 |
+
|
| 297 |
+
if processing_time_data:
|
| 298 |
+
for row in processing_time_data:
|
| 299 |
+
processing_labels.append(row['time_range'])
|
| 300 |
+
processing_data.append(row['count'])
|
| 301 |
+
|
| 302 |
+
return {
|
| 303 |
+
'trend_labels': labels,
|
| 304 |
+
'trend_data': data,
|
| 305 |
+
'risk_level_labels': risk_labels,
|
| 306 |
+
'risk_level_data': risk_data,
|
| 307 |
+
'processing_time_labels': processing_labels,
|
| 308 |
+
'processing_time_data': processing_data
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
except Exception as e:
|
| 312 |
+
self.logger.error(f"Error getting trend data: {e}")
|
| 313 |
+
return {
|
| 314 |
+
'trend_labels': [],
|
| 315 |
+
'trend_data': [],
|
| 316 |
+
'risk_level_labels': [],
|
| 317 |
+
'risk_level_data': [],
|
| 318 |
+
'processing_time_labels': [],
|
| 319 |
+
'processing_time_data': []
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
def start_api_server(self):
|
| 323 |
+
"""Start the API server in a background thread"""
|
| 324 |
+
if self.running:
|
| 325 |
+
self.logger.warning("API server is already running")
|
| 326 |
+
return
|
| 327 |
+
|
| 328 |
+
def run_server():
|
| 329 |
+
try:
|
| 330 |
+
self.logger.info(f"Starting SmartHeal Bot API server on port {self.port}")
|
| 331 |
+
self.app.run(host='0.0.0.0', port=self.port, debug=False, threaded=True)
|
| 332 |
+
except Exception as e:
|
| 333 |
+
self.logger.error(f"Error starting API server: {e}")
|
| 334 |
+
|
| 335 |
+
self.running = True
|
| 336 |
+
self.api_thread = threading.Thread(target=run_server, daemon=True)
|
| 337 |
+
self.api_thread.start()
|
| 338 |
+
|
| 339 |
+
# Give the server a moment to start
|
| 340 |
+
time.sleep(1)
|
| 341 |
+
self.logger.info(f"✅ SmartHeal Bot API server started on http://0.0.0.0:{self.port}")
|
| 342 |
+
|
| 343 |
+
def stop_api_server(self):
|
| 344 |
+
"""Stop the API server"""
|
| 345 |
+
self.running = False
|
| 346 |
+
if self.api_thread and self.api_thread.is_alive():
|
| 347 |
+
self.logger.info("Stopping SmartHeal Bot API server")
|
| 348 |
+
# Note: Flask development server doesn't have a clean shutdown method
|
| 349 |
+
# In production, you would use a proper WSGI server like Gunicorn
|
| 350 |
+
|
| 351 |
+
def is_running(self) -> bool:
|
| 352 |
+
"""Check if the API server is running"""
|
| 353 |
+
return self.running and self.api_thread and self.api_thread.is_alive()
|
| 354 |
+
|
| 355 |
+
class DashboardIntegrationManager:
|
| 356 |
+
"""Manager class for dashboard integration functionality"""
|
| 357 |
+
|
| 358 |
+
def __init__(self, database_manager: DashboardDatabaseManager):
|
| 359 |
+
self.database_manager = database_manager
|
| 360 |
+
self.api = DashboardAPI(database_manager)
|
| 361 |
+
self.logger = logging.getLogger(__name__)
|
| 362 |
+
|
| 363 |
+
def start_integration(self):
|
| 364 |
+
"""Start dashboard integration services"""
|
| 365 |
+
try:
|
| 366 |
+
self.api.start_api_server()
|
| 367 |
+
self.logger.info("✅ Dashboard integration started successfully")
|
| 368 |
+
except Exception as e:
|
| 369 |
+
self.logger.error(f"❌ Failed to start dashboard integration: {e}")
|
| 370 |
+
|
| 371 |
+
def stop_integration(self):
|
| 372 |
+
"""Stop dashboard integration services"""
|
| 373 |
+
try:
|
| 374 |
+
self.api.stop_api_server()
|
| 375 |
+
self.logger.info("✅ Dashboard integration stopped")
|
| 376 |
+
except Exception as e:
|
| 377 |
+
self.logger.error(f"❌ Error stopping dashboard integration: {e}")
|
| 378 |
+
|
| 379 |
+
def log_analysis_session(self, session_data: Dict[str, Any]) -> Optional[int]:
|
| 380 |
+
"""Log an analysis session for dashboard tracking"""
|
| 381 |
+
try:
|
| 382 |
+
session_id = self.database_manager.save_analysis_session(session_data)
|
| 383 |
+
if session_id:
|
| 384 |
+
self.logger.info(f"✅ Analysis session logged with ID: {session_id}")
|
| 385 |
+
return session_id
|
| 386 |
+
except Exception as e:
|
| 387 |
+
self.logger.error(f"❌ Error logging analysis session: {e}")
|
| 388 |
+
return None
|
| 389 |
+
|
| 390 |
+
def log_bot_interaction(self, interaction_data: Dict[str, Any]) -> Optional[int]:
|
| 391 |
+
"""Log a bot interaction for dashboard tracking"""
|
| 392 |
+
try:
|
| 393 |
+
interaction_id = self.database_manager.save_bot_interaction(interaction_data)
|
| 394 |
+
if interaction_id:
|
| 395 |
+
self.logger.info(f"✅ Bot interaction logged with ID: {interaction_id}")
|
| 396 |
+
return interaction_id
|
| 397 |
+
except Exception as e:
|
| 398 |
+
self.logger.error(f"❌ Error logging bot interaction: {e}")
|
| 399 |
+
return None
|
| 400 |
+
|
| 401 |
+
def get_integration_status(self) -> Dict[str, Any]:
|
| 402 |
+
"""Get the status of dashboard integration"""
|
| 403 |
+
return {
|
| 404 |
+
'api_running': self.api.is_running(),
|
| 405 |
+
'database_connected': self.database_manager.get_connection() is not None,
|
| 406 |
+
'timestamp': datetime.now().isoformat()
|
| 407 |
+
}
|
| 408 |
+
|
src/dashboard_database_manager.py
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mysql.connector
|
| 2 |
+
from mysql.connector import Error
|
| 3 |
+
import logging
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
import json
|
| 6 |
+
import uuid
|
| 7 |
+
import os
|
| 8 |
+
from PIL import Image
|
| 9 |
+
from typing import Dict, Any, Optional, List
|
| 10 |
+
|
| 11 |
+
class DashboardDatabaseManager:
|
| 12 |
+
"""Enhanced database manager for SmartHeal bot with dashboard integration"""
|
| 13 |
+
|
| 14 |
+
def __init__(self, mysql_config):
|
| 15 |
+
"""Initialize database manager with MySQL configuration"""
|
| 16 |
+
self.mysql_config = mysql_config
|
| 17 |
+
self.test_connection()
|
| 18 |
+
|
| 19 |
+
def test_connection(self):
|
| 20 |
+
"""Test database connection"""
|
| 21 |
+
try:
|
| 22 |
+
connection = self.get_connection()
|
| 23 |
+
if connection:
|
| 24 |
+
connection.close()
|
| 25 |
+
logging.info("✅ Database connection successful")
|
| 26 |
+
else:
|
| 27 |
+
logging.error("❌ Database connection failed")
|
| 28 |
+
except Exception as e:
|
| 29 |
+
logging.error(f"Database connection test failed: {e}")
|
| 30 |
+
|
| 31 |
+
def get_connection(self):
|
| 32 |
+
"""Get a database connection"""
|
| 33 |
+
try:
|
| 34 |
+
connection = mysql.connector.connect(**self.mysql_config)
|
| 35 |
+
return connection
|
| 36 |
+
except Error as e:
|
| 37 |
+
logging.error(f"Error connecting to MySQL: {e}")
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
def execute_query(self, query, params=None, fetch=False):
|
| 41 |
+
"""Execute a query and return results if fetch=True"""
|
| 42 |
+
connection = self.get_connection()
|
| 43 |
+
if not connection:
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
cursor = None
|
| 47 |
+
try:
|
| 48 |
+
cursor = connection.cursor(dictionary=True)
|
| 49 |
+
cursor.execute(query, params or ())
|
| 50 |
+
|
| 51 |
+
if fetch:
|
| 52 |
+
result = cursor.fetchall()
|
| 53 |
+
else:
|
| 54 |
+
connection.commit()
|
| 55 |
+
result = cursor.rowcount
|
| 56 |
+
|
| 57 |
+
return result
|
| 58 |
+
except Error as e:
|
| 59 |
+
logging.error(f"Error executing query: {e}")
|
| 60 |
+
if connection:
|
| 61 |
+
connection.rollback()
|
| 62 |
+
return None
|
| 63 |
+
finally:
|
| 64 |
+
if cursor:
|
| 65 |
+
cursor.close()
|
| 66 |
+
if connection and connection.is_connected():
|
| 67 |
+
connection.close()
|
| 68 |
+
|
| 69 |
+
def execute_query_one(self, query, params=None):
|
| 70 |
+
"""Execute a query and return one result"""
|
| 71 |
+
connection = self.get_connection()
|
| 72 |
+
if not connection:
|
| 73 |
+
return None
|
| 74 |
+
|
| 75 |
+
cursor = None
|
| 76 |
+
try:
|
| 77 |
+
cursor = connection.cursor(dictionary=True)
|
| 78 |
+
cursor.execute(query, params or ())
|
| 79 |
+
result = cursor.fetchone()
|
| 80 |
+
return result
|
| 81 |
+
except Error as e:
|
| 82 |
+
logging.error(f"Error executing query: {e}")
|
| 83 |
+
return None
|
| 84 |
+
finally:
|
| 85 |
+
if cursor:
|
| 86 |
+
cursor.close()
|
| 87 |
+
if connection and connection.is_connected():
|
| 88 |
+
connection.close()
|
| 89 |
+
|
| 90 |
+
def get_last_insert_id(self, connection, cursor):
|
| 91 |
+
"""Get the last inserted ID"""
|
| 92 |
+
return cursor.lastrowid
|
| 93 |
+
|
| 94 |
+
def save_questionnaire_response(self, questionnaire_data: Dict[str, Any], user_id: int) -> Optional[int]:
|
| 95 |
+
"""
|
| 96 |
+
Save questionnaire response to dashboard-compatible tables
|
| 97 |
+
"""
|
| 98 |
+
connection = None
|
| 99 |
+
cursor = None
|
| 100 |
+
try:
|
| 101 |
+
connection = self.get_connection()
|
| 102 |
+
if not connection:
|
| 103 |
+
return None
|
| 104 |
+
cursor = connection.cursor()
|
| 105 |
+
|
| 106 |
+
# (1) Create or get patient
|
| 107 |
+
patient_id = self._create_or_get_patient(cursor, questionnaire_data)
|
| 108 |
+
if not patient_id:
|
| 109 |
+
raise Exception("Failed to get or create patient")
|
| 110 |
+
|
| 111 |
+
# (2) Get or create default questionnaire
|
| 112 |
+
questionnaire_id = self._get_or_create_default_questionnaire(cursor)
|
| 113 |
+
if not questionnaire_id:
|
| 114 |
+
raise Exception("Failed to get or create questionnaire")
|
| 115 |
+
|
| 116 |
+
# (3) Prepare response_data JSON
|
| 117 |
+
response_data = {
|
| 118 |
+
'patient_info': {
|
| 119 |
+
'name': questionnaire_data.get('patient_name', ''),
|
| 120 |
+
'age': questionnaire_data.get('patient_age', 0),
|
| 121 |
+
'gender': questionnaire_data.get('patient_gender', '')
|
| 122 |
+
},
|
| 123 |
+
'wound_details': {
|
| 124 |
+
'location': questionnaire_data.get('wound_location', ''),
|
| 125 |
+
'duration': questionnaire_data.get('wound_duration', ''),
|
| 126 |
+
'pain_level': questionnaire_data.get('pain_level', 0),
|
| 127 |
+
'moisture_level': questionnaire_data.get('moisture_level', ''),
|
| 128 |
+
'infection_signs': questionnaire_data.get('infection_signs', ''),
|
| 129 |
+
'diabetic_status': questionnaire_data.get('diabetic_status', '')
|
| 130 |
+
},
|
| 131 |
+
'medical_history': {
|
| 132 |
+
'previous_treatment': questionnaire_data.get('previous_treatment', ''),
|
| 133 |
+
'medical_history': questionnaire_data.get('medical_history', ''),
|
| 134 |
+
'medications': questionnaire_data.get('medications', ''),
|
| 135 |
+
'allergies': questionnaire_data.get('allergies', ''),
|
| 136 |
+
'additional_notes': questionnaire_data.get('additional_notes', '')
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
# (4) Insert into questionnaire_responses
|
| 141 |
+
insert_resp = """
|
| 142 |
+
INSERT INTO questionnaire_responses
|
| 143 |
+
(questionnaire_id, patient_id, practitioner_id, response_data, submitted_at)
|
| 144 |
+
VALUES (%s, %s, %s, %s, %s)
|
| 145 |
+
"""
|
| 146 |
+
cursor.execute(insert_resp, (
|
| 147 |
+
questionnaire_id,
|
| 148 |
+
patient_id,
|
| 149 |
+
user_id,
|
| 150 |
+
json.dumps(response_data),
|
| 151 |
+
datetime.now()
|
| 152 |
+
))
|
| 153 |
+
response_id = cursor.lastrowid
|
| 154 |
+
|
| 155 |
+
connection.commit()
|
| 156 |
+
logging.info(f"✅ Saved questionnaire response ID {response_id}")
|
| 157 |
+
return response_id
|
| 158 |
+
|
| 159 |
+
except Exception as e:
|
| 160 |
+
logging.error(f"❌ Error saving questionnaire response: {e}")
|
| 161 |
+
if connection:
|
| 162 |
+
connection.rollback()
|
| 163 |
+
return None
|
| 164 |
+
finally:
|
| 165 |
+
if cursor:
|
| 166 |
+
cursor.close()
|
| 167 |
+
if connection:
|
| 168 |
+
connection.close()
|
| 169 |
+
|
| 170 |
+
def _get_or_create_default_questionnaire(self, cursor) -> Optional[int]:
|
| 171 |
+
"""Get or create default questionnaire"""
|
| 172 |
+
try:
|
| 173 |
+
# Check if default questionnaire exists
|
| 174 |
+
cursor.execute("SELECT id FROM questionnaires WHERE name = 'Default Patient Assessment' LIMIT 1")
|
| 175 |
+
questionnaire_row = cursor.fetchone()
|
| 176 |
+
|
| 177 |
+
if questionnaire_row:
|
| 178 |
+
return questionnaire_row[0]
|
| 179 |
+
|
| 180 |
+
# Create default questionnaire
|
| 181 |
+
cursor.execute("""
|
| 182 |
+
INSERT INTO questionnaires (name, description, created_at)
|
| 183 |
+
VALUES ('Default Patient Assessment', 'Standard patient wound assessment form', NOW())
|
| 184 |
+
""")
|
| 185 |
+
return cursor.lastrowid
|
| 186 |
+
|
| 187 |
+
except Exception as e:
|
| 188 |
+
logging.error(f"Error getting/creating questionnaire: {e}")
|
| 189 |
+
return None
|
| 190 |
+
|
| 191 |
+
def _create_or_get_patient(self, cursor, questionnaire_data: Dict[str, Any]) -> Optional[int]:
|
| 192 |
+
"""Create or get existing patient record"""
|
| 193 |
+
try:
|
| 194 |
+
# Check if patient exists by name and age
|
| 195 |
+
select_query = """
|
| 196 |
+
SELECT id FROM patients
|
| 197 |
+
WHERE name = %s AND age = %s
|
| 198 |
+
LIMIT 1
|
| 199 |
+
"""
|
| 200 |
+
cursor.execute(select_query, (
|
| 201 |
+
questionnaire_data.get('patient_name', ''),
|
| 202 |
+
questionnaire_data.get('patient_age', 0)
|
| 203 |
+
))
|
| 204 |
+
|
| 205 |
+
existing_patient = cursor.fetchone()
|
| 206 |
+
if existing_patient:
|
| 207 |
+
return existing_patient[0]
|
| 208 |
+
|
| 209 |
+
# Create new patient
|
| 210 |
+
patient_uuid = str(uuid.uuid4())
|
| 211 |
+
insert_query = """
|
| 212 |
+
INSERT INTO patients (
|
| 213 |
+
uuid, name, age, gender, illness, allergy, notes, created_at
|
| 214 |
+
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
| 215 |
+
"""
|
| 216 |
+
|
| 217 |
+
cursor.execute(insert_query, (
|
| 218 |
+
patient_uuid,
|
| 219 |
+
questionnaire_data.get('patient_name', ''),
|
| 220 |
+
questionnaire_data.get('patient_age', 0),
|
| 221 |
+
questionnaire_data.get('patient_gender', ''),
|
| 222 |
+
questionnaire_data.get('medical_history', ''),
|
| 223 |
+
questionnaire_data.get('allergies', ''),
|
| 224 |
+
questionnaire_data.get('additional_notes', ''),
|
| 225 |
+
datetime.now()
|
| 226 |
+
))
|
| 227 |
+
|
| 228 |
+
return cursor.lastrowid
|
| 229 |
+
|
| 230 |
+
except Exception as e:
|
| 231 |
+
logging.error(f"Error creating/getting patient: {e}")
|
| 232 |
+
return None
|
| 233 |
+
|
| 234 |
+
def save_wound_image(self, questionnaire_response_id: int, image, original_filename: str = None) -> Optional[int]:
|
| 235 |
+
"""Save wound image to filesystem and database"""
|
| 236 |
+
try:
|
| 237 |
+
# Generate unique filename
|
| 238 |
+
image_id = str(uuid.uuid4())
|
| 239 |
+
filename = f"wound_{image_id}.jpg"
|
| 240 |
+
file_path = os.path.join("uploads", filename)
|
| 241 |
+
|
| 242 |
+
# Ensure uploads directory exists
|
| 243 |
+
os.makedirs("uploads", exist_ok=True)
|
| 244 |
+
|
| 245 |
+
# Save image to disk
|
| 246 |
+
if hasattr(image, 'save'):
|
| 247 |
+
image.save(file_path, format='JPEG', quality=95)
|
| 248 |
+
|
| 249 |
+
# Get image dimensions and file size
|
| 250 |
+
width, height = image.size
|
| 251 |
+
file_size = os.path.getsize(file_path)
|
| 252 |
+
|
| 253 |
+
# Save to wound_images table
|
| 254 |
+
query = """
|
| 255 |
+
INSERT INTO wound_images (
|
| 256 |
+
questionnaire_id, image_url, original_filename, file_size,
|
| 257 |
+
image_width, image_height, created_at
|
| 258 |
+
) VALUES (%s, %s, %s, %s, %s, %s, %s)
|
| 259 |
+
"""
|
| 260 |
+
|
| 261 |
+
params = (
|
| 262 |
+
questionnaire_response_id,
|
| 263 |
+
file_path,
|
| 264 |
+
original_filename or filename,
|
| 265 |
+
file_size,
|
| 266 |
+
width,
|
| 267 |
+
height,
|
| 268 |
+
datetime.now()
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
result = self.execute_query(query, params)
|
| 272 |
+
if result:
|
| 273 |
+
# Get the inserted ID
|
| 274 |
+
connection = self.get_connection()
|
| 275 |
+
cursor = connection.cursor()
|
| 276 |
+
cursor.execute("SELECT LAST_INSERT_ID()")
|
| 277 |
+
image_db_id = cursor.fetchone()[0]
|
| 278 |
+
cursor.close()
|
| 279 |
+
connection.close()
|
| 280 |
+
|
| 281 |
+
logging.info(f"✅ Image saved with ID: {image_db_id}")
|
| 282 |
+
return image_db_id
|
| 283 |
+
|
| 284 |
+
except Exception as e:
|
| 285 |
+
logging.error(f"❌ Error saving wound image: {e}")
|
| 286 |
+
|
| 287 |
+
return None
|
| 288 |
+
|
| 289 |
+
def save_ai_analysis(self, analysis_data: Dict[str, Any]) -> Optional[int]:
|
| 290 |
+
"""
|
| 291 |
+
Save AI analysis results to dashboard-compatible ai_analyses table
|
| 292 |
+
"""
|
| 293 |
+
try:
|
| 294 |
+
query = """
|
| 295 |
+
INSERT INTO ai_analyses (
|
| 296 |
+
questionnaire_id, image_id, analysis_data, summary, recommendations,
|
| 297 |
+
risk_score, risk_level, wound_type, wound_dimensions, processing_time,
|
| 298 |
+
model_version, created_at
|
| 299 |
+
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
| 300 |
+
"""
|
| 301 |
+
|
| 302 |
+
# Calculate risk level based on risk score
|
| 303 |
+
risk_score = analysis_data.get('risk_score', 0)
|
| 304 |
+
if risk_score >= 70:
|
| 305 |
+
risk_level = 'High'
|
| 306 |
+
elif risk_score >= 40:
|
| 307 |
+
risk_level = 'Moderate'
|
| 308 |
+
else:
|
| 309 |
+
risk_level = 'Low'
|
| 310 |
+
|
| 311 |
+
# Format wound dimensions
|
| 312 |
+
visual_results = analysis_data.get('visual_results', {})
|
| 313 |
+
wound_dimensions = f"{visual_results.get('length_cm', 0)}x{visual_results.get('breadth_cm', 0)} cm"
|
| 314 |
+
|
| 315 |
+
params = (
|
| 316 |
+
analysis_data.get('questionnaire_id'),
|
| 317 |
+
analysis_data.get('image_id'),
|
| 318 |
+
json.dumps(analysis_data.get('analysis_data', {})),
|
| 319 |
+
analysis_data.get('summary', ''),
|
| 320 |
+
analysis_data.get('recommendations', ''),
|
| 321 |
+
risk_score,
|
| 322 |
+
risk_level,
|
| 323 |
+
visual_results.get('wound_type', 'Unknown'),
|
| 324 |
+
wound_dimensions,
|
| 325 |
+
analysis_data.get('processing_time', 0.0),
|
| 326 |
+
analysis_data.get('model_version', 'v1.0'),
|
| 327 |
+
datetime.now()
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
result = self.execute_query(query, params)
|
| 331 |
+
if result:
|
| 332 |
+
# Get the inserted ID
|
| 333 |
+
connection = self.get_connection()
|
| 334 |
+
cursor = connection.cursor()
|
| 335 |
+
cursor.execute("SELECT LAST_INSERT_ID()")
|
| 336 |
+
analysis_id = cursor.fetchone()[0]
|
| 337 |
+
cursor.close()
|
| 338 |
+
connection.close()
|
| 339 |
+
|
| 340 |
+
logging.info(f"✅ AI analysis saved with ID: {analysis_id}")
|
| 341 |
+
return analysis_id
|
| 342 |
+
|
| 343 |
+
except Exception as e:
|
| 344 |
+
logging.error(f"❌ Error saving AI analysis: {e}")
|
| 345 |
+
|
| 346 |
+
return None
|
| 347 |
+
|
| 348 |
+
def save_analysis_session(self, session_data: Dict[str, Any]) -> Optional[int]:
|
| 349 |
+
"""
|
| 350 |
+
Save analysis session data for dashboard analytics
|
| 351 |
+
"""
|
| 352 |
+
try:
|
| 353 |
+
query = """
|
| 354 |
+
INSERT INTO analysis_sessions (
|
| 355 |
+
user_id, questionnaire_id, image_id, analysis_id,
|
| 356 |
+
session_duration, created_at
|
| 357 |
+
) VALUES (%s, %s, %s, %s, %s, %s)
|
| 358 |
+
"""
|
| 359 |
+
|
| 360 |
+
params = (
|
| 361 |
+
session_data.get('user_id'),
|
| 362 |
+
session_data.get('questionnaire_id'),
|
| 363 |
+
session_data.get('image_id'),
|
| 364 |
+
session_data.get('analysis_id'),
|
| 365 |
+
session_data.get('session_duration', 0.0),
|
| 366 |
+
datetime.now()
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
result = self.execute_query(query, params)
|
| 370 |
+
if result:
|
| 371 |
+
connection = self.get_connection()
|
| 372 |
+
cursor = connection.cursor()
|
| 373 |
+
cursor.execute("SELECT LAST_INSERT_ID()")
|
| 374 |
+
session_id = cursor.fetchone()[0]
|
| 375 |
+
cursor.close()
|
| 376 |
+
connection.close()
|
| 377 |
+
|
| 378 |
+
logging.info(f"✅ Analysis session saved with ID: {session_id}")
|
| 379 |
+
return session_id
|
| 380 |
+
|
| 381 |
+
except Exception as e:
|
| 382 |
+
logging.error(f"❌ Error saving analysis session: {e}")
|
| 383 |
+
|
| 384 |
+
return None
|
| 385 |
+
|
| 386 |
+
def save_bot_interaction(self, interaction_data: Dict[str, Any]) -> Optional[int]:
|
| 387 |
+
"""
|
| 388 |
+
Save bot interaction data for dashboard analytics
|
| 389 |
+
"""
|
| 390 |
+
try:
|
| 391 |
+
query = """
|
| 392 |
+
INSERT INTO bot_interactions (
|
| 393 |
+
patient_id, practitioner_id, input_text, output_text,
|
| 394 |
+
wound_image_url, interaction_type, interacted_at
|
| 395 |
+
) VALUES (%s, %s, %s, %s, %s, %s, %s)
|
| 396 |
+
"""
|
| 397 |
+
|
| 398 |
+
params = (
|
| 399 |
+
interaction_data.get('patient_id'),
|
| 400 |
+
interaction_data.get('practitioner_id'),
|
| 401 |
+
interaction_data.get('input_text', ''),
|
| 402 |
+
interaction_data.get('output_text', ''),
|
| 403 |
+
interaction_data.get('wound_image_url', ''),
|
| 404 |
+
interaction_data.get('interaction_type', 'analysis'),
|
| 405 |
+
datetime.now()
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
result = self.execute_query(query, params)
|
| 409 |
+
if result:
|
| 410 |
+
connection = self.get_connection()
|
| 411 |
+
cursor = connection.cursor()
|
| 412 |
+
cursor.execute("SELECT LAST_INSERT_ID()")
|
| 413 |
+
interaction_id = cursor.fetchone()[0]
|
| 414 |
+
cursor.close()
|
| 415 |
+
connection.close()
|
| 416 |
+
|
| 417 |
+
logging.info(f"✅ Bot interaction saved with ID: {interaction_id}")
|
| 418 |
+
return interaction_id
|
| 419 |
+
|
| 420 |
+
except Exception as e:
|
| 421 |
+
logging.error(f"❌ Error saving bot interaction: {e}")
|
| 422 |
+
|
| 423 |
+
return None
|
| 424 |
+
|
| 425 |
+
def get_analytics_data(self) -> Dict[str, Any]:
|
| 426 |
+
"""
|
| 427 |
+
Get comprehensive analytics data for dashboard
|
| 428 |
+
"""
|
| 429 |
+
try:
|
| 430 |
+
analytics = {}
|
| 431 |
+
|
| 432 |
+
# Total analyses
|
| 433 |
+
total_analyses = self.execute_query_one("SELECT COUNT(*) as count FROM ai_analyses")
|
| 434 |
+
analytics['total_analyses'] = total_analyses['count'] if total_analyses else 0
|
| 435 |
+
|
| 436 |
+
# Average processing time
|
| 437 |
+
avg_time = self.execute_query_one("SELECT AVG(processing_time) as avg_time FROM ai_analyses WHERE processing_time IS NOT NULL")
|
| 438 |
+
analytics['avg_processing_time'] = round(avg_time['avg_time'], 2) if avg_time and avg_time['avg_time'] else 0
|
| 439 |
+
|
| 440 |
+
# High risk count
|
| 441 |
+
high_risk = self.execute_query_one("SELECT COUNT(*) as count FROM ai_analyses WHERE risk_level = 'High'")
|
| 442 |
+
analytics['high_risk_count'] = high_risk['count'] if high_risk else 0
|
| 443 |
+
|
| 444 |
+
# Average risk score
|
| 445 |
+
avg_risk = self.execute_query_one("SELECT AVG(risk_score) as avg_risk FROM ai_analyses WHERE risk_score IS NOT NULL")
|
| 446 |
+
analytics['avg_risk_score'] = round(avg_risk['avg_risk'], 1) if avg_risk and avg_risk['avg_risk'] else 0
|
| 447 |
+
|
| 448 |
+
# Risk level distribution
|
| 449 |
+
risk_distribution = self.execute_query("""
|
| 450 |
+
SELECT risk_level, COUNT(*) as count
|
| 451 |
+
FROM ai_analyses
|
| 452 |
+
GROUP BY risk_level
|
| 453 |
+
""", fetch=True)
|
| 454 |
+
|
| 455 |
+
analytics['risk_level_distribution'] = {}
|
| 456 |
+
if risk_distribution:
|
| 457 |
+
for row in risk_distribution:
|
| 458 |
+
analytics['risk_level_distribution'][row['risk_level']] = row['count']
|
| 459 |
+
|
| 460 |
+
# Recent analyses
|
| 461 |
+
recent_analyses = self.execute_query("""
|
| 462 |
+
SELECT * FROM ai_analyses
|
| 463 |
+
ORDER BY created_at DESC
|
| 464 |
+
LIMIT 10
|
| 465 |
+
""", fetch=True)
|
| 466 |
+
analytics['recent_analyses'] = recent_analyses or []
|
| 467 |
+
|
| 468 |
+
# Model performance
|
| 469 |
+
model_performance = self.execute_query("""
|
| 470 |
+
SELECT
|
| 471 |
+
model_version,
|
| 472 |
+
COUNT(*) as count,
|
| 473 |
+
AVG(processing_time) as avg_processing_time,
|
| 474 |
+
AVG(risk_score) as avg_risk_score
|
| 475 |
+
FROM ai_analyses
|
| 476 |
+
WHERE model_version IS NOT NULL
|
| 477 |
+
GROUP BY model_version
|
| 478 |
+
""", fetch=True)
|
| 479 |
+
analytics['model_performance'] = model_performance or []
|
| 480 |
+
|
| 481 |
+
# Today's analyses
|
| 482 |
+
today_analyses = self.execute_query_one("""
|
| 483 |
+
SELECT COUNT(*) as count
|
| 484 |
+
FROM ai_analyses
|
| 485 |
+
WHERE DATE(created_at) = CURDATE()
|
| 486 |
+
""")
|
| 487 |
+
analytics['analyses_today'] = today_analyses['count'] if today_analyses else 0
|
| 488 |
+
|
| 489 |
+
# This week's analyses
|
| 490 |
+
week_analyses = self.execute_query_one("""
|
| 491 |
+
SELECT COUNT(*) as count
|
| 492 |
+
FROM ai_analyses
|
| 493 |
+
WHERE YEARWEEK(created_at) = YEARWEEK(NOW())
|
| 494 |
+
""")
|
| 495 |
+
analytics['analyses_this_week'] = week_analyses['count'] if week_analyses else 0
|
| 496 |
+
|
| 497 |
+
# Unique questionnaires
|
| 498 |
+
unique_questionnaires = self.execute_query_one("""
|
| 499 |
+
SELECT COUNT(DISTINCT questionnaire_id) as count
|
| 500 |
+
FROM ai_analyses
|
| 501 |
+
""")
|
| 502 |
+
analytics['unique_questionnaires'] = unique_questionnaires['count'] if unique_questionnaires else 0
|
| 503 |
+
|
| 504 |
+
# Analyses with images
|
| 505 |
+
with_images = self.execute_query_one("""
|
| 506 |
+
SELECT COUNT(*) as count
|
| 507 |
+
FROM ai_analyses
|
| 508 |
+
WHERE image_id IS NOT NULL
|
| 509 |
+
""")
|
| 510 |
+
analytics['analyses_with_images'] = with_images['count'] if with_images else 0
|
| 511 |
+
|
| 512 |
+
return analytics
|
| 513 |
+
|
| 514 |
+
except Exception as e:
|
| 515 |
+
logging.error(f"❌ Error getting analytics data: {e}")
|
| 516 |
+
return {}
|
| 517 |
+
|
| 518 |
+
def get_interaction_history(self, limit: int = 50) -> List[Dict[str, Any]]:
|
| 519 |
+
"""
|
| 520 |
+
Get bot interaction history for dashboard
|
| 521 |
+
"""
|
| 522 |
+
try:
|
| 523 |
+
query = """
|
| 524 |
+
SELECT bi.*, p.name as patient_name, u.name as practitioner_name
|
| 525 |
+
FROM bot_interactions bi
|
| 526 |
+
LEFT JOIN patients p ON bi.patient_id = p.id
|
| 527 |
+
LEFT JOIN users u ON bi.practitioner_id = u.id
|
| 528 |
+
ORDER BY bi.interacted_at DESC
|
| 529 |
+
LIMIT %s
|
| 530 |
+
"""
|
| 531 |
+
|
| 532 |
+
interactions = self.execute_query(query, (limit,), fetch=True)
|
| 533 |
+
return interactions or []
|
| 534 |
+
|
| 535 |
+
except Exception as e:
|
| 536 |
+
logging.error(f"❌ Error getting interaction history: {e}")
|
| 537 |
+
return []
|
| 538 |
+
|
| 539 |
+
def get_session_analytics(self) -> Dict[str, Any]:
|
| 540 |
+
"""
|
| 541 |
+
Get session analytics for dashboard
|
| 542 |
+
"""
|
| 543 |
+
try:
|
| 544 |
+
analytics = {}
|
| 545 |
+
|
| 546 |
+
# Total sessions
|
| 547 |
+
total_sessions = self.execute_query_one("SELECT COUNT(*) as count FROM analysis_sessions")
|
| 548 |
+
analytics['total_sessions'] = total_sessions['count'] if total_sessions else 0
|
| 549 |
+
|
| 550 |
+
# Average session duration
|
| 551 |
+
avg_duration = self.execute_query_one("""
|
| 552 |
+
SELECT AVG(session_duration) as avg_duration
|
| 553 |
+
FROM analysis_sessions
|
| 554 |
+
WHERE session_duration IS NOT NULL
|
| 555 |
+
""")
|
| 556 |
+
analytics['avg_session_duration'] = round(avg_duration['avg_duration'], 2) if avg_duration and avg_duration['avg_duration'] else 0
|
| 557 |
+
|
| 558 |
+
# Sessions today
|
| 559 |
+
today_sessions = self.execute_query_one("""
|
| 560 |
+
SELECT COUNT(*) as count
|
| 561 |
+
FROM analysis_sessions
|
| 562 |
+
WHERE DATE(created_at) = CURDATE()
|
| 563 |
+
""")
|
| 564 |
+
analytics['sessions_today'] = today_sessions['count'] if today_sessions else 0
|
| 565 |
+
|
| 566 |
+
return analytics
|
| 567 |
+
|
| 568 |
+
except Exception as e:
|
| 569 |
+
logging.error(f"❌ Error getting session analytics: {e}")
|
| 570 |
+
return {}
|
| 571 |
+
|
src/eHealth in Wound Care.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dde88c1c1d7d295c7f2c53375b9cd69c0cef029e273c89374d92f1375c2c5038
|
| 3 |
+
size 907639
|
src/enhanced_ai_processor.py
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import logging
|
| 3 |
+
import cv2
|
| 4 |
+
import numpy as np
|
| 5 |
+
from PIL import Image
|
| 6 |
+
import torch
|
| 7 |
+
import json
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
import tensorflow as tf
|
| 10 |
+
from transformers import pipeline
|
| 11 |
+
from ultralytics import YOLO
|
| 12 |
+
from tensorflow.keras.models import load_model
|
| 13 |
+
from langchain_community.document_loaders import PyPDFLoader
|
| 14 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 15 |
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
| 16 |
+
from langchain_community.vectorstores import FAISS
|
| 17 |
+
from huggingface_hub import HfApi, HfFolder
|
| 18 |
+
import spaces
|
| 19 |
+
import time
|
| 20 |
+
from typing import Dict, Any, Optional, Tuple
|
| 21 |
+
|
| 22 |
+
from .config import Config
|
| 23 |
+
|
| 24 |
+
class EnhancedAIProcessor:
|
| 25 |
+
"""Enhanced AI processor with dashboard integration and analytics tracking"""
|
| 26 |
+
|
| 27 |
+
def __init__(self):
|
| 28 |
+
self.models_cache = {}
|
| 29 |
+
self.knowledge_base_cache = {}
|
| 30 |
+
self.config = Config()
|
| 31 |
+
self.px_per_cm = self.config.PIXELS_PER_CM
|
| 32 |
+
self.model_version = "v1.2.0" # Version for tracking
|
| 33 |
+
self._initialize_models()
|
| 34 |
+
|
| 35 |
+
def _initialize_models(self):
|
| 36 |
+
"""Initialize all AI models including real-time models"""
|
| 37 |
+
try:
|
| 38 |
+
# Set HuggingFace token
|
| 39 |
+
if self.config.HF_TOKEN:
|
| 40 |
+
HfFolder.save_token(self.config.HF_TOKEN)
|
| 41 |
+
logging.info("HuggingFace token set successfully")
|
| 42 |
+
|
| 43 |
+
# Initialize MedGemma pipeline for medical text generation
|
| 44 |
+
try:
|
| 45 |
+
self.models_cache["medgemma_pipe"] = pipeline(
|
| 46 |
+
"image-text-to-text",
|
| 47 |
+
model="google/medgemma-4b-it",
|
| 48 |
+
torch_dtype=torch.bfloat16,
|
| 49 |
+
offload_folder="offload",
|
| 50 |
+
device_map="auto",
|
| 51 |
+
token=self.config.HF_TOKEN
|
| 52 |
+
)
|
| 53 |
+
logging.info("✅ MedGemma pipeline loaded successfully")
|
| 54 |
+
except Exception as e:
|
| 55 |
+
logging.warning(f"MedGemma pipeline not available: {e}")
|
| 56 |
+
|
| 57 |
+
# Initialize YOLO model for wound detection
|
| 58 |
+
try:
|
| 59 |
+
self.models_cache["det"] = YOLO(self.config.YOLO_MODEL_PATH)
|
| 60 |
+
logging.info("✅ YOLO detection model loaded successfully")
|
| 61 |
+
except Exception as e:
|
| 62 |
+
logging.warning(f"YOLO model not available: {e}")
|
| 63 |
+
|
| 64 |
+
# Initialize segmentation model
|
| 65 |
+
try:
|
| 66 |
+
self.models_cache["seg"] = load_model(self.config.SEG_MODEL_PATH, compile=False)
|
| 67 |
+
logging.info("✅ Segmentation model loaded successfully")
|
| 68 |
+
except Exception as e:
|
| 69 |
+
logging.warning(f"Segmentation model not available: {e}")
|
| 70 |
+
|
| 71 |
+
# Initialize wound classification model
|
| 72 |
+
try:
|
| 73 |
+
self.models_cache["cls"] = pipeline(
|
| 74 |
+
"image-classification",
|
| 75 |
+
model="Hemg/Wound-classification",
|
| 76 |
+
token=self.config.HF_TOKEN,
|
| 77 |
+
device="cpu"
|
| 78 |
+
)
|
| 79 |
+
logging.info("✅ Wound classification model loaded successfully")
|
| 80 |
+
except Exception as e:
|
| 81 |
+
logging.warning(f"Wound classification model not available: {e}")
|
| 82 |
+
|
| 83 |
+
# Initialize embedding model for knowledge base
|
| 84 |
+
try:
|
| 85 |
+
self.models_cache["embedding_model"] = HuggingFaceEmbeddings(
|
| 86 |
+
model_name="sentence-transformers/all-MiniLM-L6-v2",
|
| 87 |
+
model_kwargs={'device': 'cpu'}
|
| 88 |
+
)
|
| 89 |
+
logging.info("✅ Embedding model loaded successfully")
|
| 90 |
+
except Exception as e:
|
| 91 |
+
logging.warning(f"Embedding model not available: {e}")
|
| 92 |
+
|
| 93 |
+
logging.info("✅ All models loaded.")
|
| 94 |
+
self._load_knowledge_base()
|
| 95 |
+
|
| 96 |
+
except Exception as e:
|
| 97 |
+
logging.error(f"Error initializing AI models: {e}")
|
| 98 |
+
|
| 99 |
+
def _load_knowledge_base(self):
|
| 100 |
+
"""Load knowledge base from PDF guidelines"""
|
| 101 |
+
try:
|
| 102 |
+
documents = []
|
| 103 |
+
for pdf_path in self.config.GUIDELINE_PDFS:
|
| 104 |
+
if os.path.exists(pdf_path):
|
| 105 |
+
loader = PyPDFLoader(pdf_path)
|
| 106 |
+
docs = loader.load()
|
| 107 |
+
documents.extend(docs)
|
| 108 |
+
logging.info(f"Loaded PDF: {pdf_path}")
|
| 109 |
+
|
| 110 |
+
if documents and 'embedding_model' in self.models_cache:
|
| 111 |
+
# Split documents into chunks
|
| 112 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
| 113 |
+
chunk_size=1000,
|
| 114 |
+
chunk_overlap=100
|
| 115 |
+
)
|
| 116 |
+
chunks = text_splitter.split_documents(documents)
|
| 117 |
+
|
| 118 |
+
# Create vector store
|
| 119 |
+
vectorstore = FAISS.from_documents(chunks, self.models_cache['embedding_model'])
|
| 120 |
+
self.knowledge_base_cache['vectorstore'] = vectorstore
|
| 121 |
+
logging.info(f"✅ Knowledge base loaded with {len(chunks)} chunks")
|
| 122 |
+
else:
|
| 123 |
+
self.knowledge_base_cache['vectorstore'] = None
|
| 124 |
+
logging.warning("Knowledge base not available - no PDFs found or embedding model unavailable")
|
| 125 |
+
|
| 126 |
+
except Exception as e:
|
| 127 |
+
logging.warning(f"Knowledge base loading error: {e}")
|
| 128 |
+
self.knowledge_base_cache['vectorstore'] = None
|
| 129 |
+
|
| 130 |
+
def perform_comprehensive_analysis(self, image_pil: Image.Image, patient_info: Dict[str, Any]) -> Dict[str, Any]:
|
| 131 |
+
"""
|
| 132 |
+
Perform comprehensive analysis with enhanced tracking for dashboard integration
|
| 133 |
+
"""
|
| 134 |
+
start_time = time.time()
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
# Perform visual analysis
|
| 138 |
+
visual_results = self.perform_visual_analysis(image_pil)
|
| 139 |
+
|
| 140 |
+
# Query guidelines for context
|
| 141 |
+
guideline_query = f"wound care {visual_results.get('wound_type', 'general')} treatment recommendations"
|
| 142 |
+
guideline_context = self.query_guidelines(guideline_query)
|
| 143 |
+
|
| 144 |
+
# Generate comprehensive report
|
| 145 |
+
report = self.generate_final_report(patient_info, visual_results, guideline_context, image_pil)
|
| 146 |
+
|
| 147 |
+
# Calculate processing time
|
| 148 |
+
processing_time = round(time.time() - start_time, 2)
|
| 149 |
+
|
| 150 |
+
# Calculate risk score based on multiple factors
|
| 151 |
+
risk_score = self._calculate_risk_score(visual_results, patient_info)
|
| 152 |
+
|
| 153 |
+
# Prepare comprehensive analysis data
|
| 154 |
+
analysis_data = {
|
| 155 |
+
'visual_results': visual_results,
|
| 156 |
+
'patient_info': patient_info,
|
| 157 |
+
'guideline_context': guideline_context,
|
| 158 |
+
'report': report,
|
| 159 |
+
'processing_time': processing_time,
|
| 160 |
+
'risk_score': risk_score,
|
| 161 |
+
'model_version': self.model_version,
|
| 162 |
+
'analysis_timestamp': datetime.now().isoformat(),
|
| 163 |
+
'analysis_metadata': {
|
| 164 |
+
'models_used': list(self.models_cache.keys()),
|
| 165 |
+
'image_dimensions': image_pil.size,
|
| 166 |
+
'guideline_sources': len(guideline_context.split('\n\n')) if guideline_context else 0
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
logging.info(f"✅ Comprehensive analysis completed in {processing_time}s with risk score {risk_score}")
|
| 171 |
+
return analysis_data
|
| 172 |
+
|
| 173 |
+
except Exception as e:
|
| 174 |
+
processing_time = round(time.time() - start_time, 2)
|
| 175 |
+
logging.error(f"❌ Analysis failed after {processing_time}s: {e}")
|
| 176 |
+
|
| 177 |
+
# Return error analysis data
|
| 178 |
+
return {
|
| 179 |
+
'error': str(e),
|
| 180 |
+
'processing_time': processing_time,
|
| 181 |
+
'risk_score': 0,
|
| 182 |
+
'model_version': self.model_version,
|
| 183 |
+
'analysis_timestamp': datetime.now().isoformat()
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
def perform_visual_analysis(self, image_pil: Image.Image) -> Dict[str, Any]:
|
| 187 |
+
"""Perform comprehensive visual analysis of wound image with enhanced tracking"""
|
| 188 |
+
try:
|
| 189 |
+
# Convert PIL to OpenCV format
|
| 190 |
+
image_cv = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
|
| 191 |
+
|
| 192 |
+
# YOLO detection
|
| 193 |
+
if 'det' not in self.models_cache:
|
| 194 |
+
raise ValueError("YOLO detection model not available.")
|
| 195 |
+
|
| 196 |
+
results = self.models_cache['det'].predict(image_cv, verbose=False, device="cpu")
|
| 197 |
+
|
| 198 |
+
if not results or not results[0].boxes:
|
| 199 |
+
raise ValueError("No wound detected in the image.")
|
| 200 |
+
|
| 201 |
+
# Extract bounding box
|
| 202 |
+
box = results[0].boxes[0].xyxy[0].cpu().numpy().astype(int)
|
| 203 |
+
x1, y1, x2, y2 = box
|
| 204 |
+
region_cv = image_cv[y1:y2, x1:x2]
|
| 205 |
+
|
| 206 |
+
# Save detection image with timestamp
|
| 207 |
+
detection_image_cv = image_cv.copy()
|
| 208 |
+
cv2.rectangle(detection_image_cv, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
| 209 |
+
os.makedirs(os.path.join(self.config.UPLOADS_DIR, "analysis"), exist_ok=True)
|
| 210 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 211 |
+
detection_image_path = os.path.join(self.config.UPLOADS_DIR, "analysis", f"detection_{timestamp}.png")
|
| 212 |
+
cv2.imwrite(detection_image_path, detection_image_cv)
|
| 213 |
+
detection_image_pil = Image.fromarray(cv2.cvtColor(detection_image_cv, cv2.COLOR_BGR2RGB))
|
| 214 |
+
|
| 215 |
+
# Initialize outputs
|
| 216 |
+
length = breadth = area = 0
|
| 217 |
+
segmentation_image_pil = None
|
| 218 |
+
segmentation_image_path = None
|
| 219 |
+
segmentation_confidence = 0.0
|
| 220 |
+
|
| 221 |
+
# Segmentation (optional)
|
| 222 |
+
if 'seg' in self.models_cache:
|
| 223 |
+
input_size = self.models_cache['seg'].input_shape[1:3] # (height, width)
|
| 224 |
+
resized_region = cv2.resize(region_cv, (input_size[1], input_size[0]))
|
| 225 |
+
|
| 226 |
+
seg_input = np.expand_dims(resized_region / 255.0, 0)
|
| 227 |
+
mask_pred = self.models_cache['seg'].predict(seg_input, verbose=0)[0]
|
| 228 |
+
mask_np = (mask_pred[:, :, 0] > 0.5).astype(np.uint8)
|
| 229 |
+
|
| 230 |
+
# Calculate segmentation confidence
|
| 231 |
+
segmentation_confidence = float(np.mean(mask_pred[:, :, 0]))
|
| 232 |
+
|
| 233 |
+
# Resize mask back to original region size
|
| 234 |
+
mask_resized = cv2.resize(mask_np, (region_cv.shape[1], region_cv.shape[0]), interpolation=cv2.INTER_NEAREST)
|
| 235 |
+
|
| 236 |
+
# Overlay mask on region for visualization
|
| 237 |
+
overlay = region_cv.copy()
|
| 238 |
+
overlay[mask_resized == 1] = [0, 0, 255] # Red overlay
|
| 239 |
+
|
| 240 |
+
# Blend overlay for final output
|
| 241 |
+
segmented_visual = cv2.addWeighted(region_cv, 0.7, overlay, 0.3, 0)
|
| 242 |
+
|
| 243 |
+
# Save segmentation image
|
| 244 |
+
segmentation_image_path = os.path.join(self.config.UPLOADS_DIR, "analysis", f"segmentation_{timestamp}.png")
|
| 245 |
+
cv2.imwrite(segmentation_image_path, segmented_visual)
|
| 246 |
+
segmentation_image_pil = Image.fromarray(cv2.cvtColor(segmented_visual, cv2.COLOR_BGR2RGB))
|
| 247 |
+
|
| 248 |
+
# Wound measurements from resized mask
|
| 249 |
+
contours, _ = cv2.findContours(mask_resized, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 250 |
+
if contours:
|
| 251 |
+
cnt = max(contours, key=cv2.contourArea)
|
| 252 |
+
x, y, w, h = cv2.boundingRect(cnt)
|
| 253 |
+
length = round(h / self.px_per_cm, 2)
|
| 254 |
+
breadth = round(w / self.px_per_cm, 2)
|
| 255 |
+
area = round(cv2.contourArea(cnt) / (self.px_per_cm ** 2), 2)
|
| 256 |
+
|
| 257 |
+
# Classification with confidence tracking
|
| 258 |
+
wound_type = "Unknown"
|
| 259 |
+
classification_confidence = 0.0
|
| 260 |
+
classification_scores = []
|
| 261 |
+
|
| 262 |
+
if 'cls' in self.models_cache:
|
| 263 |
+
try:
|
| 264 |
+
region_pil = Image.fromarray(cv2.cvtColor(region_cv, cv2.COLOR_BGR2RGB))
|
| 265 |
+
cls_result = self.models_cache['cls'](region_pil)
|
| 266 |
+
|
| 267 |
+
if cls_result:
|
| 268 |
+
best_result = max(cls_result, key=lambda x: x['score'])
|
| 269 |
+
wound_type = best_result['label']
|
| 270 |
+
classification_confidence = float(best_result['score'])
|
| 271 |
+
classification_scores = [{'label': r['label'], 'score': float(r['score'])} for r in cls_result]
|
| 272 |
+
|
| 273 |
+
except Exception as e:
|
| 274 |
+
logging.warning(f"Wound classification error: {e}")
|
| 275 |
+
|
| 276 |
+
return {
|
| 277 |
+
'wound_type': wound_type,
|
| 278 |
+
'length_cm': length,
|
| 279 |
+
'breadth_cm': breadth,
|
| 280 |
+
'surface_area_cm2': area,
|
| 281 |
+
'detection_confidence': float(results[0].boxes[0].conf.cpu().item()),
|
| 282 |
+
'segmentation_confidence': segmentation_confidence,
|
| 283 |
+
'classification_confidence': classification_confidence,
|
| 284 |
+
'classification_scores': classification_scores,
|
| 285 |
+
'bounding_box': box.tolist(),
|
| 286 |
+
'detection_image_path': detection_image_path,
|
| 287 |
+
'detection_image_pil': detection_image_pil,
|
| 288 |
+
'segmentation_image_path': segmentation_image_path,
|
| 289 |
+
'segmentation_image_pil': segmentation_image_pil,
|
| 290 |
+
'analysis_quality': {
|
| 291 |
+
'detection_quality': 'high' if float(results[0].boxes[0].conf.cpu().item()) > 0.8 else 'medium',
|
| 292 |
+
'segmentation_quality': 'high' if segmentation_confidence > 0.7 else 'medium',
|
| 293 |
+
'classification_quality': 'high' if classification_confidence > 0.8 else 'medium'
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
except Exception as e:
|
| 298 |
+
logging.error(f"Visual analysis error: {e}")
|
| 299 |
+
raise ValueError(f"Visual analysis failed: {str(e)}")
|
| 300 |
+
|
| 301 |
+
def _calculate_risk_score(self, visual_results: Dict[str, Any], patient_info: Dict[str, Any]) -> int:
|
| 302 |
+
"""
|
| 303 |
+
Calculate comprehensive risk score (0-100) based on visual analysis and patient data
|
| 304 |
+
"""
|
| 305 |
+
try:
|
| 306 |
+
risk_score = 0
|
| 307 |
+
|
| 308 |
+
# Wound size risk (0-25 points)
|
| 309 |
+
area = visual_results.get('surface_area_cm2', 0)
|
| 310 |
+
if area > 10:
|
| 311 |
+
risk_score += 25
|
| 312 |
+
elif area > 5:
|
| 313 |
+
risk_score += 15
|
| 314 |
+
elif area > 2:
|
| 315 |
+
risk_score += 10
|
| 316 |
+
else:
|
| 317 |
+
risk_score += 5
|
| 318 |
+
|
| 319 |
+
# Wound type risk (0-20 points)
|
| 320 |
+
wound_type = visual_results.get('wound_type', '').lower()
|
| 321 |
+
high_risk_types = ['ulcer', 'necrotic', 'infected', 'diabetic']
|
| 322 |
+
medium_risk_types = ['pressure', 'venous', 'arterial']
|
| 323 |
+
|
| 324 |
+
if any(risk_type in wound_type for risk_type in high_risk_types):
|
| 325 |
+
risk_score += 20
|
| 326 |
+
elif any(risk_type in wound_type for risk_type in medium_risk_types):
|
| 327 |
+
risk_score += 15
|
| 328 |
+
else:
|
| 329 |
+
risk_score += 10
|
| 330 |
+
|
| 331 |
+
# Patient factors (0-30 points)
|
| 332 |
+
age = patient_info.get('patient_age', 0)
|
| 333 |
+
if age > 70:
|
| 334 |
+
risk_score += 15
|
| 335 |
+
elif age > 50:
|
| 336 |
+
risk_score += 10
|
| 337 |
+
else:
|
| 338 |
+
risk_score += 5
|
| 339 |
+
|
| 340 |
+
# Diabetic status
|
| 341 |
+
diabetic_status = patient_info.get('diabetic_status', '').lower()
|
| 342 |
+
if 'yes' in diabetic_status or 'diabetic' in diabetic_status:
|
| 343 |
+
risk_score += 15
|
| 344 |
+
|
| 345 |
+
# Pain level (0-10 points)
|
| 346 |
+
pain_level = patient_info.get('pain_level', 0)
|
| 347 |
+
if pain_level > 7:
|
| 348 |
+
risk_score += 10
|
| 349 |
+
elif pain_level > 4:
|
| 350 |
+
risk_score += 7
|
| 351 |
+
else:
|
| 352 |
+
risk_score += 3
|
| 353 |
+
|
| 354 |
+
# Infection signs (0-15 points)
|
| 355 |
+
infection_signs = patient_info.get('infection_signs', '').lower()
|
| 356 |
+
if 'yes' in infection_signs or 'present' in infection_signs:
|
| 357 |
+
risk_score += 15
|
| 358 |
+
elif 'possible' in infection_signs or 'mild' in infection_signs:
|
| 359 |
+
risk_score += 10
|
| 360 |
+
else:
|
| 361 |
+
risk_score += 5
|
| 362 |
+
|
| 363 |
+
# Ensure score is within 0-100 range
|
| 364 |
+
risk_score = min(max(risk_score, 0), 100)
|
| 365 |
+
|
| 366 |
+
logging.info(f"Calculated risk score: {risk_score}")
|
| 367 |
+
return risk_score
|
| 368 |
+
|
| 369 |
+
except Exception as e:
|
| 370 |
+
logging.error(f"Error calculating risk score: {e}")
|
| 371 |
+
return 50 # Default medium risk
|
| 372 |
+
|
| 373 |
+
def query_guidelines(self, query: str) -> str:
|
| 374 |
+
"""Query the knowledge base for relevant guidelines with enhanced tracking"""
|
| 375 |
+
try:
|
| 376 |
+
vector_store = self.knowledge_base_cache.get("vectorstore")
|
| 377 |
+
if not vector_store:
|
| 378 |
+
return "Knowledge base unavailable - clinical guidelines not loaded"
|
| 379 |
+
|
| 380 |
+
# Retrieve relevant documents
|
| 381 |
+
retriever = vector_store.as_retriever(search_kwargs={"k": 10})
|
| 382 |
+
docs = retriever.invoke(query)
|
| 383 |
+
|
| 384 |
+
if not docs:
|
| 385 |
+
return "No relevant guidelines found for the query"
|
| 386 |
+
|
| 387 |
+
# Format the results with enhanced metadata
|
| 388 |
+
formatted_results = []
|
| 389 |
+
for i, doc in enumerate(docs):
|
| 390 |
+
source = doc.metadata.get('source', 'Unknown')
|
| 391 |
+
page = doc.metadata.get('page', 'N/A')
|
| 392 |
+
content = doc.page_content.strip()
|
| 393 |
+
|
| 394 |
+
# Add relevance indicator
|
| 395 |
+
relevance = f"Result {i+1}/10"
|
| 396 |
+
formatted_results.append(f"[{relevance}] Source: {source}, Page: {page}\nContent: {content}")
|
| 397 |
+
|
| 398 |
+
guideline_text = "\n\n".join(formatted_results)
|
| 399 |
+
logging.info(f"Retrieved {len(docs)} guideline documents for query: {query[:50]}...")
|
| 400 |
+
return guideline_text
|
| 401 |
+
|
| 402 |
+
except Exception as e:
|
| 403 |
+
logging.error(f"Guidelines query error: {e}")
|
| 404 |
+
return f"Error querying guidelines: {str(e)}"
|
| 405 |
+
|
| 406 |
+
def generate_final_report(self, patient_info: Dict[str, Any], visual_results: Dict[str, Any],
|
| 407 |
+
guideline_context: str, image_pil: Image.Image, max_new_tokens: int = None) -> str:
|
| 408 |
+
"""Generate comprehensive medical report using MedGemma with enhanced tracking"""
|
| 409 |
+
try:
|
| 410 |
+
if 'medgemma_pipe' not in self.models_cache:
|
| 411 |
+
return self._generate_fallback_report(patient_info, visual_results, guideline_context)
|
| 412 |
+
|
| 413 |
+
max_tokens = max_new_tokens or self.config.MAX_NEW_TOKENS
|
| 414 |
+
|
| 415 |
+
# Get detection and segmentation images if available
|
| 416 |
+
detection_image = visual_results.get('detection_image_pil', None)
|
| 417 |
+
segmentation_image = visual_results.get('segmentation_image_pil', None)
|
| 418 |
+
|
| 419 |
+
# Create enhanced prompt with quality indicators
|
| 420 |
+
analysis_quality = visual_results.get('analysis_quality', {})
|
| 421 |
+
prompt = f"""
|
| 422 |
+
# SmartHeal AI Wound Care Report
|
| 423 |
+
|
| 424 |
+
## Patient Information
|
| 425 |
+
{self._format_patient_info(patient_info)}
|
| 426 |
+
|
| 427 |
+
## Visual Analysis Summary
|
| 428 |
+
- Wound Type: {visual_results.get('wound_type', 'Unknown')} (Confidence: {visual_results.get('classification_confidence', 0):.2f})
|
| 429 |
+
- Dimensions: {visual_results.get('length_cm', 0)} × {visual_results.get('breadth_cm', 0)} cm
|
| 430 |
+
- Surface Area: {visual_results.get('surface_area_cm2', 0)} cm²
|
| 431 |
+
- Detection Quality: {analysis_quality.get('detection_quality', 'medium')}
|
| 432 |
+
- Segmentation Quality: {analysis_quality.get('segmentation_quality', 'medium')}
|
| 433 |
+
|
| 434 |
+
## Clinical Reference Guidelines
|
| 435 |
+
{guideline_context[:2000]}...
|
| 436 |
+
|
| 437 |
+
## Analysis Request
|
| 438 |
+
You are SmartHeal-AI Agent, a specialized wound care AI with expertise in clinical assessment and evidence-based treatment planning.
|
| 439 |
+
|
| 440 |
+
Based on the comprehensive data provided (patient information, precise wound measurements, clinical guidelines, and visual analysis), generate a structured clinical report with the following sections:
|
| 441 |
+
|
| 442 |
+
### 1. Clinical Assessment
|
| 443 |
+
- Detailed wound characterization based on visual analysis
|
| 444 |
+
- Tissue type assessment (granulation, slough, necrotic, epithelializing)
|
| 445 |
+
- Peri-wound skin condition evaluation
|
| 446 |
+
- Infection risk assessment
|
| 447 |
+
|
| 448 |
+
### 2. Treatment Recommendations
|
| 449 |
+
- Specific wound care dressing recommendations based on wound characteristics
|
| 450 |
+
- Topical treatments if indicated
|
| 451 |
+
- Debridement recommendations if needed
|
| 452 |
+
- Pressure offloading strategies if applicable
|
| 453 |
+
|
| 454 |
+
### 3. Risk Stratification
|
| 455 |
+
- Patient-specific risk factors analysis
|
| 456 |
+
- Healing prognosis assessment
|
| 457 |
+
- Complications to monitor
|
| 458 |
+
|
| 459 |
+
### 4. Follow-up Plan
|
| 460 |
+
- Recommended assessment frequency
|
| 461 |
+
- Key monitoring parameters
|
| 462 |
+
- Escalation criteria for specialist referral
|
| 463 |
+
|
| 464 |
+
Generate a concise, evidence-based report suitable for clinical documentation.
|
| 465 |
+
"""
|
| 466 |
+
|
| 467 |
+
# Prepare messages for MedGemma with all available images
|
| 468 |
+
content_list = [{"type": "text", "text": prompt}]
|
| 469 |
+
|
| 470 |
+
# Add images in order of importance
|
| 471 |
+
if image_pil:
|
| 472 |
+
content_list.insert(0, {"type": "image", "image": image_pil})
|
| 473 |
+
|
| 474 |
+
if detection_image:
|
| 475 |
+
content_list.insert(1, {"type": "image", "image": detection_image})
|
| 476 |
+
|
| 477 |
+
if segmentation_image:
|
| 478 |
+
content_list.insert(2, {"type": "image", "image": segmentation_image})
|
| 479 |
+
|
| 480 |
+
messages = [
|
| 481 |
+
{
|
| 482 |
+
"role": "system",
|
| 483 |
+
"content": [{"type": "text", "text": "You are a specialized medical AI assistant for wound care with expertise in clinical assessment, treatment planning, and evidence-based recommendations. Provide structured, actionable clinical reports."}],
|
| 484 |
+
},
|
| 485 |
+
{
|
| 486 |
+
"role": "user",
|
| 487 |
+
"content": content_list
|
| 488 |
+
}
|
| 489 |
+
]
|
| 490 |
+
|
| 491 |
+
# Generate report using MedGemma
|
| 492 |
+
output = self.models_cache['medgemma_pipe'](
|
| 493 |
+
text=messages,
|
| 494 |
+
max_new_tokens=max_tokens,
|
| 495 |
+
do_sample=False,
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
generated_content = output[0]['generated_text']
|
| 499 |
+
|
| 500 |
+
# Extract the assistant's response
|
| 501 |
+
if isinstance(generated_content, list):
|
| 502 |
+
for message in generated_content:
|
| 503 |
+
if message.get('role') == 'assistant':
|
| 504 |
+
report_content = message.get('content', '')
|
| 505 |
+
if isinstance(report_content, list):
|
| 506 |
+
report_text = ''.join([item.get('text', '') for item in report_content if item.get('type') == 'text'])
|
| 507 |
+
else:
|
| 508 |
+
report_text = str(report_content)
|
| 509 |
+
break
|
| 510 |
+
else:
|
| 511 |
+
report_text = str(generated_content)
|
| 512 |
+
else:
|
| 513 |
+
report_text = str(generated_content)
|
| 514 |
+
|
| 515 |
+
# Add metadata to report
|
| 516 |
+
report_with_metadata = f"""
|
| 517 |
+
{report_text}
|
| 518 |
+
|
| 519 |
+
---
|
| 520 |
+
**Report Metadata:**
|
| 521 |
+
- Generated by: SmartHeal AI v{self.model_version}
|
| 522 |
+
- Analysis Quality: Detection ({analysis_quality.get('detection_quality', 'medium')}), Segmentation ({analysis_quality.get('segmentation_quality', 'medium')})
|
| 523 |
+
- Generated at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
| 524 |
+
"""
|
| 525 |
+
|
| 526 |
+
logging.info("✅ MedGemma report generated successfully")
|
| 527 |
+
return report_with_metadata
|
| 528 |
+
|
| 529 |
+
except Exception as e:
|
| 530 |
+
logging.error(f"MedGemma report generation error: {e}")
|
| 531 |
+
return self._generate_fallback_report(patient_info, visual_results, guideline_context)
|
| 532 |
+
|
| 533 |
+
def _format_patient_info(self, patient_info: Dict[str, Any]) -> str:
|
| 534 |
+
"""Format patient information for report"""
|
| 535 |
+
formatted = f"""
|
| 536 |
+
- Name: {patient_info.get('patient_name', 'N/A')}
|
| 537 |
+
- Age: {patient_info.get('patient_age', 'N/A')} years
|
| 538 |
+
- Gender: {patient_info.get('patient_gender', 'N/A')}
|
| 539 |
+
- Wound Location: {patient_info.get('wound_location', 'N/A')}
|
| 540 |
+
- Wound Duration: {patient_info.get('wound_duration', 'N/A')}
|
| 541 |
+
- Pain Level: {patient_info.get('pain_level', 'N/A')}/10
|
| 542 |
+
- Diabetic Status: {patient_info.get('diabetic_status', 'N/A')}
|
| 543 |
+
- Infection Signs: {patient_info.get('infection_signs', 'N/A')}
|
| 544 |
+
- Previous Treatment: {patient_info.get('previous_treatment', 'N/A')}
|
| 545 |
+
- Medical History: {patient_info.get('medical_history', 'N/A')}
|
| 546 |
+
- Current Medications: {patient_info.get('medications', 'N/A')}
|
| 547 |
+
- Known Allergies: {patient_info.get('allergies', 'N/A')}
|
| 548 |
+
"""
|
| 549 |
+
return formatted.strip()
|
| 550 |
+
|
| 551 |
+
def _generate_fallback_report(self, patient_info: Dict[str, Any], visual_results: Dict[str, Any],
|
| 552 |
+
guideline_context: str) -> str:
|
| 553 |
+
"""Generate fallback report when MedGemma is not available"""
|
| 554 |
+
|
| 555 |
+
wound_type = visual_results.get('wound_type', 'Unknown')
|
| 556 |
+
length = visual_results.get('length_cm', 0)
|
| 557 |
+
breadth = visual_results.get('breadth_cm', 0)
|
| 558 |
+
area = visual_results.get('surface_area_cm2', 0)
|
| 559 |
+
|
| 560 |
+
# Basic risk assessment
|
| 561 |
+
risk_factors = []
|
| 562 |
+
if patient_info.get('patient_age', 0) > 65:
|
| 563 |
+
risk_factors.append("Advanced age")
|
| 564 |
+
if 'yes' in str(patient_info.get('diabetic_status', '')).lower():
|
| 565 |
+
risk_factors.append("Diabetes mellitus")
|
| 566 |
+
if patient_info.get('pain_level', 0) > 6:
|
| 567 |
+
risk_factors.append("High pain level")
|
| 568 |
+
if area > 5:
|
| 569 |
+
risk_factors.append("Large wound size")
|
| 570 |
+
|
| 571 |
+
report = f"""
|
| 572 |
+
# SmartHeal AI Wound Assessment Report
|
| 573 |
+
|
| 574 |
+
## Clinical Summary
|
| 575 |
+
**Patient:** {patient_info.get('patient_name', 'N/A')}, {patient_info.get('patient_age', 'N/A')} years old {patient_info.get('patient_gender', '')}
|
| 576 |
+
|
| 577 |
+
**Wound Characteristics:**
|
| 578 |
+
- Type: {wound_type}
|
| 579 |
+
- Location: {patient_info.get('wound_location', 'N/A')}
|
| 580 |
+
- Dimensions: {length} × {breadth} cm (Area: {area} cm²)
|
| 581 |
+
- Duration: {patient_info.get('wound_duration', 'N/A')}
|
| 582 |
+
- Pain Level: {patient_info.get('pain_level', 'N/A')}/10
|
| 583 |
+
|
| 584 |
+
## Risk Assessment
|
| 585 |
+
**Identified Risk Factors:**
|
| 586 |
+
{chr(10).join(f'- {factor}' for factor in risk_factors) if risk_factors else '- No significant risk factors identified'}
|
| 587 |
+
|
| 588 |
+
## Treatment Recommendations
|
| 589 |
+
**Wound Care:**
|
| 590 |
+
- Regular wound assessment and documentation
|
| 591 |
+
- Appropriate dressing selection based on wound characteristics
|
| 592 |
+
- Maintain moist wound environment
|
| 593 |
+
- Monitor for signs of infection
|
| 594 |
+
|
| 595 |
+
**Patient Management:**
|
| 596 |
+
- Pain management as indicated
|
| 597 |
+
- Nutritional assessment and optimization
|
| 598 |
+
- Patient education on wound care
|
| 599 |
+
|
| 600 |
+
## Follow-up Plan
|
| 601 |
+
- Reassess wound in 1-2 weeks
|
| 602 |
+
- Monitor for signs of healing or deterioration
|
| 603 |
+
- Consider specialist referral if no improvement in 4 weeks
|
| 604 |
+
|
| 605 |
+
---
|
| 606 |
+
**Report Generated by:** SmartHeal AI Fallback System v{self.model_version}
|
| 607 |
+
**Generated at:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
| 608 |
+
**Note:** This is a basic assessment. For comprehensive analysis, ensure all AI models are properly loaded.
|
| 609 |
+
"""
|
| 610 |
+
|
| 611 |
+
logging.info("✅ Fallback report generated")
|
| 612 |
+
return report
|
| 613 |
+
|
src/enhanced_ui_components.py
ADDED
|
@@ -0,0 +1,890 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import logging
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import time
|
| 5 |
+
from typing import Dict, Any, Optional, Tuple
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
from PIL import Image
|
| 9 |
+
|
| 10 |
+
from .enhanced_ai_processor import EnhancedAIProcessor
|
| 11 |
+
from .dashboard_database_manager import DashboardDatabaseManager
|
| 12 |
+
from .dashboard_api import DashboardIntegrationManager
|
| 13 |
+
from .auth import AuthManager
|
| 14 |
+
|
| 15 |
+
class EnhancedUIComponents:
|
| 16 |
+
"""Enhanced UI components with dashboard integration and analytics tracking"""
|
| 17 |
+
|
| 18 |
+
def __init__(self, auth_manager: AuthManager, database_manager: DashboardDatabaseManager,
|
| 19 |
+
ai_processor: EnhancedAIProcessor):
|
| 20 |
+
"""Initialize enhanced UI components"""
|
| 21 |
+
self.auth_manager = auth_manager
|
| 22 |
+
self.database_manager = database_manager
|
| 23 |
+
self.ai_processor = ai_processor
|
| 24 |
+
self.dashboard_integration = DashboardIntegrationManager(database_manager)
|
| 25 |
+
|
| 26 |
+
# Start dashboard integration
|
| 27 |
+
self.dashboard_integration.start_integration()
|
| 28 |
+
|
| 29 |
+
# UI styling
|
| 30 |
+
self.theme = gr.themes.Soft()
|
| 31 |
+
self.custom_css = self._load_custom_css()
|
| 32 |
+
|
| 33 |
+
# Session tracking
|
| 34 |
+
self.current_session = {}
|
| 35 |
+
|
| 36 |
+
logging.info("✅ Enhanced UI Components initialized with dashboard integration")
|
| 37 |
+
|
| 38 |
+
def _load_custom_css(self):
|
| 39 |
+
"""Load custom CSS for the application"""
|
| 40 |
+
return """
|
| 41 |
+
/* Enhanced SmartHeal Application Styling */
|
| 42 |
+
.main-header {
|
| 43 |
+
text-align: center;
|
| 44 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 45 |
+
color: white;
|
| 46 |
+
padding: 2rem;
|
| 47 |
+
border-radius: 10px;
|
| 48 |
+
margin-bottom: 2rem;
|
| 49 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.main-header h1 {
|
| 53 |
+
margin: 0;
|
| 54 |
+
font-size: 2.5rem;
|
| 55 |
+
font-weight: bold;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.main-header p {
|
| 59 |
+
margin: 0.5rem 0 0 0;
|
| 60 |
+
font-size: 1.1rem;
|
| 61 |
+
opacity: 0.9;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.integration-status {
|
| 65 |
+
background-color: #e8f5e8;
|
| 66 |
+
border: 1px solid #4caf50;
|
| 67 |
+
border-radius: 8px;
|
| 68 |
+
padding: 1rem;
|
| 69 |
+
margin: 1rem 0;
|
| 70 |
+
color: #2e7d32;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.analytics-info {
|
| 74 |
+
background-color: #e3f2fd;
|
| 75 |
+
border: 1px solid #2196f3;
|
| 76 |
+
border-radius: 8px;
|
| 77 |
+
padding: 1rem;
|
| 78 |
+
margin: 1rem 0;
|
| 79 |
+
color: #1565c0;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.session-info {
|
| 83 |
+
background-color: #fff3e0;
|
| 84 |
+
border: 1px solid #ff9800;
|
| 85 |
+
border-radius: 8px;
|
| 86 |
+
padding: 1rem;
|
| 87 |
+
margin: 1rem 0;
|
| 88 |
+
color: #ef6c00;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
/* Section Headers */
|
| 92 |
+
.section-header {
|
| 93 |
+
background-color: #f8f9fa;
|
| 94 |
+
padding: 1rem;
|
| 95 |
+
border-radius: 8px;
|
| 96 |
+
border-left: 4px solid #007bff;
|
| 97 |
+
margin: 1rem 0;
|
| 98 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.section-header h2 {
|
| 102 |
+
margin: 0;
|
| 103 |
+
color: #495057;
|
| 104 |
+
font-size: 1.3rem;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.section-header h3 {
|
| 108 |
+
margin: 0;
|
| 109 |
+
color: #6c757d;
|
| 110 |
+
font-size: 1.1rem;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/* Result Boxes */
|
| 114 |
+
.result-box {
|
| 115 |
+
background-color: #e7f3ff;
|
| 116 |
+
border: 1px solid #b3d9ff;
|
| 117 |
+
border-radius: 8px;
|
| 118 |
+
padding: 1rem;
|
| 119 |
+
margin: 1rem 0;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.success-box {
|
| 123 |
+
background-color: #d4edda;
|
| 124 |
+
border: 1px solid #c3e6cb;
|
| 125 |
+
border-radius: 8px;
|
| 126 |
+
padding: 1rem;
|
| 127 |
+
margin: 1rem 0;
|
| 128 |
+
color: #155724;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.warning-box {
|
| 132 |
+
background-color: #fff3cd;
|
| 133 |
+
border: 1px solid #ffeaa7;
|
| 134 |
+
border-radius: 8px;
|
| 135 |
+
padding: 1rem;
|
| 136 |
+
margin: 1rem 0;
|
| 137 |
+
color: #856404;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.error-box {
|
| 141 |
+
background-color: #ffe7e7;
|
| 142 |
+
border: 1px solid #ffb3b3;
|
| 143 |
+
border-radius: 8px;
|
| 144 |
+
padding: 1rem 0;
|
| 145 |
+
margin: 1rem 0;
|
| 146 |
+
color: #721c24;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/* Enhanced form styling */
|
| 150 |
+
.form-group {
|
| 151 |
+
margin-bottom: 1rem;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.metrics-display {
|
| 155 |
+
background-color: #f8f9fa;
|
| 156 |
+
border-radius: 8px;
|
| 157 |
+
padding: 1rem;
|
| 158 |
+
margin: 1rem 0;
|
| 159 |
+
border: 1px solid #dee2e6;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.processing-indicator {
|
| 163 |
+
background-color: #fff8e1;
|
| 164 |
+
border: 1px solid #ffcc02;
|
| 165 |
+
border-radius: 8px;
|
| 166 |
+
padding: 1rem;
|
| 167 |
+
margin: 1rem 0;
|
| 168 |
+
color: #f57f17;
|
| 169 |
+
text-align: center;
|
| 170 |
+
}
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
def create_interface(self):
|
| 174 |
+
"""Create the enhanced Gradio interface with dashboard integration"""
|
| 175 |
+
|
| 176 |
+
with gr.Blocks(theme=self.theme, css=self.custom_css, title="SmartHeal AI - Enhanced") as interface:
|
| 177 |
+
|
| 178 |
+
# Header
|
| 179 |
+
gr.HTML("""
|
| 180 |
+
<div class="main-header">
|
| 181 |
+
<h1>🏥 SmartHeal AI - Enhanced Edition</h1>
|
| 182 |
+
<p>Advanced Wound Care Analysis with Real-time Dashboard Integration</p>
|
| 183 |
+
</div>
|
| 184 |
+
""")
|
| 185 |
+
|
| 186 |
+
# Integration status display
|
| 187 |
+
integration_status = gr.HTML(self._get_integration_status_html())
|
| 188 |
+
|
| 189 |
+
# Session info
|
| 190 |
+
session_info = gr.HTML(self._get_session_info_html())
|
| 191 |
+
|
| 192 |
+
with gr.Tabs():
|
| 193 |
+
|
| 194 |
+
# Authentication Tab
|
| 195 |
+
with gr.Tab("🔐 Authentication"):
|
| 196 |
+
with gr.Row():
|
| 197 |
+
with gr.Column():
|
| 198 |
+
gr.HTML("""
|
| 199 |
+
<div class="section-header">
|
| 200 |
+
<h2>User Authentication</h2>
|
| 201 |
+
<h3>Login to access SmartHeal AI analysis features</h3>
|
| 202 |
+
</div>
|
| 203 |
+
""")
|
| 204 |
+
|
| 205 |
+
username_input = gr.Textbox(
|
| 206 |
+
label="Username",
|
| 207 |
+
placeholder="Enter your username",
|
| 208 |
+
interactive=True
|
| 209 |
+
)
|
| 210 |
+
password_input = gr.Textbox(
|
| 211 |
+
label="Password",
|
| 212 |
+
type="password",
|
| 213 |
+
placeholder="Enter your password",
|
| 214 |
+
interactive=True
|
| 215 |
+
)
|
| 216 |
+
login_btn = gr.Button("Login", variant="primary")
|
| 217 |
+
logout_btn = gr.Button("Logout", variant="secondary")
|
| 218 |
+
|
| 219 |
+
auth_status = gr.HTML(value="<div class='warning-box'>Please login to continue</div>")
|
| 220 |
+
|
| 221 |
+
# Enhanced Analysis Tab
|
| 222 |
+
with gr.Tab("🔬 Wound Analysis"):
|
| 223 |
+
with gr.Row():
|
| 224 |
+
with gr.Column(scale=1):
|
| 225 |
+
gr.HTML("""
|
| 226 |
+
<div class="section-header">
|
| 227 |
+
<h2>Patient Information</h2>
|
| 228 |
+
<h3>Complete patient details for comprehensive analysis</h3>
|
| 229 |
+
</div>
|
| 230 |
+
""")
|
| 231 |
+
|
| 232 |
+
# Patient Information
|
| 233 |
+
patient_name = gr.Textbox(label="Patient Name", placeholder="Enter patient name")
|
| 234 |
+
patient_age = gr.Number(label="Patient Age", value=0, minimum=0, maximum=120)
|
| 235 |
+
patient_gender = gr.Dropdown(
|
| 236 |
+
label="Gender",
|
| 237 |
+
choices=["Male", "Female", "Other"],
|
| 238 |
+
value="Male"
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# Wound Details
|
| 242 |
+
gr.HTML("""
|
| 243 |
+
<div class="section-header">
|
| 244 |
+
<h2>Wound Information</h2>
|
| 245 |
+
</div>
|
| 246 |
+
""")
|
| 247 |
+
|
| 248 |
+
wound_location = gr.Textbox(label="Wound Location", placeholder="e.g., Left heel, Right forearm")
|
| 249 |
+
wound_duration = gr.Textbox(label="Wound Duration", placeholder="e.g., 2 weeks, 1 month")
|
| 250 |
+
pain_level = gr.Slider(label="Pain Level (0-10)", minimum=0, maximum=10, value=0, step=1)
|
| 251 |
+
|
| 252 |
+
# Clinical Assessment
|
| 253 |
+
moisture_level = gr.Dropdown(
|
| 254 |
+
label="Moisture Level",
|
| 255 |
+
choices=["Dry", "Moist", "Wet", "Macerated"],
|
| 256 |
+
value="Moist"
|
| 257 |
+
)
|
| 258 |
+
infection_signs = gr.Dropdown(
|
| 259 |
+
label="Signs of Infection",
|
| 260 |
+
choices=["None", "Mild", "Moderate", "Severe"],
|
| 261 |
+
value="None"
|
| 262 |
+
)
|
| 263 |
+
diabetic_status = gr.Dropdown(
|
| 264 |
+
label="Diabetic Status",
|
| 265 |
+
choices=["No", "Type 1", "Type 2", "Unknown"],
|
| 266 |
+
value="No"
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
# Medical History
|
| 270 |
+
gr.HTML("""
|
| 271 |
+
<div class="section-header">
|
| 272 |
+
<h2>Medical History</h2>
|
| 273 |
+
</div>
|
| 274 |
+
""")
|
| 275 |
+
|
| 276 |
+
previous_treatment = gr.Textbox(
|
| 277 |
+
label="Previous Treatment",
|
| 278 |
+
placeholder="Describe any previous treatments",
|
| 279 |
+
lines=2
|
| 280 |
+
)
|
| 281 |
+
medical_history = gr.Textbox(
|
| 282 |
+
label="Medical History",
|
| 283 |
+
placeholder="Relevant medical conditions",
|
| 284 |
+
lines=2
|
| 285 |
+
)
|
| 286 |
+
medications = gr.Textbox(
|
| 287 |
+
label="Current Medications",
|
| 288 |
+
placeholder="List current medications",
|
| 289 |
+
lines=2
|
| 290 |
+
)
|
| 291 |
+
allergies = gr.Textbox(
|
| 292 |
+
label="Known Allergies",
|
| 293 |
+
placeholder="List any known allergies",
|
| 294 |
+
lines=2
|
| 295 |
+
)
|
| 296 |
+
additional_notes = gr.Textbox(
|
| 297 |
+
label="Additional Notes",
|
| 298 |
+
placeholder="Any additional relevant information",
|
| 299 |
+
lines=3
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
with gr.Column(scale=1):
|
| 303 |
+
gr.HTML("""
|
| 304 |
+
<div class="section-header">
|
| 305 |
+
<h2>Wound Image Analysis</h2>
|
| 306 |
+
<h3>Upload wound image for AI analysis</h3>
|
| 307 |
+
</div>
|
| 308 |
+
""")
|
| 309 |
+
|
| 310 |
+
# Image Upload
|
| 311 |
+
wound_image = gr.Image(
|
| 312 |
+
label="Wound Image",
|
| 313 |
+
type="pil",
|
| 314 |
+
height=400
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
# Analysis Controls
|
| 318 |
+
analyze_btn = gr.Button("🔍 Analyze Wound", variant="primary", size="lg")
|
| 319 |
+
|
| 320 |
+
# Processing indicator
|
| 321 |
+
processing_status = gr.HTML(visible=False)
|
| 322 |
+
|
| 323 |
+
# Analysis Metrics
|
| 324 |
+
analysis_metrics = gr.HTML(visible=False)
|
| 325 |
+
|
| 326 |
+
# Results Section
|
| 327 |
+
with gr.Row():
|
| 328 |
+
with gr.Column():
|
| 329 |
+
gr.HTML("""
|
| 330 |
+
<div class="section-header">
|
| 331 |
+
<h2>Analysis Results</h2>
|
| 332 |
+
<h3>Comprehensive AI-powered wound assessment</h3>
|
| 333 |
+
</div>
|
| 334 |
+
""")
|
| 335 |
+
|
| 336 |
+
# Visual Analysis Results
|
| 337 |
+
with gr.Row():
|
| 338 |
+
detection_image = gr.Image(label="Wound Detection", visible=False)
|
| 339 |
+
segmentation_image = gr.Image(label="Wound Segmentation", visible=False)
|
| 340 |
+
|
| 341 |
+
# Analysis Report
|
| 342 |
+
analysis_report = gr.Markdown(visible=False)
|
| 343 |
+
|
| 344 |
+
# Download Options
|
| 345 |
+
with gr.Row():
|
| 346 |
+
download_report = gr.File(label="Download Report", visible=False)
|
| 347 |
+
download_images = gr.File(label="Download Analysis Images", visible=False)
|
| 348 |
+
|
| 349 |
+
# Dashboard Integration Tab
|
| 350 |
+
with gr.Tab("📊 Dashboard Integration"):
|
| 351 |
+
gr.HTML("""
|
| 352 |
+
<div class="section-header">
|
| 353 |
+
<h2>Dashboard Integration Status</h2>
|
| 354 |
+
<h3>Real-time connection to SmartHeal Dashboard</h3>
|
| 355 |
+
</div>
|
| 356 |
+
""")
|
| 357 |
+
|
| 358 |
+
dashboard_status = gr.HTML()
|
| 359 |
+
|
| 360 |
+
with gr.Row():
|
| 361 |
+
refresh_status_btn = gr.Button("🔄 Refresh Status", variant="secondary")
|
| 362 |
+
view_analytics_btn = gr.Button("📈 View Analytics", variant="primary")
|
| 363 |
+
|
| 364 |
+
# Analytics Summary
|
| 365 |
+
analytics_summary = gr.HTML()
|
| 366 |
+
|
| 367 |
+
# Recent Activity
|
| 368 |
+
recent_activity = gr.HTML()
|
| 369 |
+
|
| 370 |
+
# Event Handlers
|
| 371 |
+
|
| 372 |
+
# Authentication
|
| 373 |
+
login_btn.click(
|
| 374 |
+
fn=self._handle_login,
|
| 375 |
+
inputs=[username_input, password_input],
|
| 376 |
+
outputs=[auth_status, session_info]
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
logout_btn.click(
|
| 380 |
+
fn=self._handle_logout,
|
| 381 |
+
outputs=[auth_status, session_info]
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
# Analysis
|
| 385 |
+
analyze_btn.click(
|
| 386 |
+
fn=self._start_analysis,
|
| 387 |
+
inputs=[],
|
| 388 |
+
outputs=[processing_status, analysis_metrics]
|
| 389 |
+
).then(
|
| 390 |
+
fn=self._perform_enhanced_analysis,
|
| 391 |
+
inputs=[
|
| 392 |
+
patient_name, patient_age, patient_gender, wound_location, wound_duration,
|
| 393 |
+
pain_level, moisture_level, infection_signs, diabetic_status,
|
| 394 |
+
previous_treatment, medical_history, medications, allergies,
|
| 395 |
+
additional_notes, wound_image
|
| 396 |
+
],
|
| 397 |
+
outputs=[
|
| 398 |
+
analysis_report, detection_image, segmentation_image,
|
| 399 |
+
download_report, download_images, processing_status,
|
| 400 |
+
analysis_metrics, session_info
|
| 401 |
+
]
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
# Dashboard Integration
|
| 405 |
+
refresh_status_btn.click(
|
| 406 |
+
fn=self._refresh_dashboard_status,
|
| 407 |
+
outputs=[dashboard_status, analytics_summary]
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
view_analytics_btn.click(
|
| 411 |
+
fn=self._get_analytics_summary,
|
| 412 |
+
outputs=[analytics_summary, recent_activity]
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
# Auto-refresh integration status on load
|
| 416 |
+
interface.load(
|
| 417 |
+
fn=self._refresh_dashboard_status,
|
| 418 |
+
outputs=[dashboard_status, analytics_summary]
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
return interface
|
| 422 |
+
|
| 423 |
+
def _get_integration_status_html(self) -> str:
|
| 424 |
+
"""Get HTML for integration status display"""
|
| 425 |
+
status = self.dashboard_integration.get_integration_status()
|
| 426 |
+
|
| 427 |
+
if status['api_running'] and status['database_connected']:
|
| 428 |
+
return """
|
| 429 |
+
<div class="integration-status">
|
| 430 |
+
✅ <strong>Dashboard Integration Active</strong><br>
|
| 431 |
+
API Server: Running | Database: Connected | Real-time Analytics: Enabled
|
| 432 |
+
</div>
|
| 433 |
+
"""
|
| 434 |
+
else:
|
| 435 |
+
return """
|
| 436 |
+
<div class="error-box">
|
| 437 |
+
❌ <strong>Dashboard Integration Issues</strong><br>
|
| 438 |
+
Please check API server and database connection
|
| 439 |
+
</div>
|
| 440 |
+
"""
|
| 441 |
+
|
| 442 |
+
def _get_session_info_html(self) -> str:
|
| 443 |
+
"""Get HTML for session information display"""
|
| 444 |
+
if self.current_session:
|
| 445 |
+
user_info = self.current_session.get('user_info', {})
|
| 446 |
+
return f"""
|
| 447 |
+
<div class="session-info">
|
| 448 |
+
👤 <strong>Active Session</strong><br>
|
| 449 |
+
User: {user_info.get('name', 'Unknown')} |
|
| 450 |
+
Role: {user_info.get('role', 'Unknown')} |
|
| 451 |
+
Session Started: {self.current_session.get('start_time', 'Unknown')}
|
| 452 |
+
</div>
|
| 453 |
+
"""
|
| 454 |
+
else:
|
| 455 |
+
return """
|
| 456 |
+
<div class="warning-box">
|
| 457 |
+
⚠️ <strong>No Active Session</strong><br>
|
| 458 |
+
Please login to start tracking your analysis session
|
| 459 |
+
</div>
|
| 460 |
+
"""
|
| 461 |
+
|
| 462 |
+
def _handle_login(self, username: str, password: str) -> Tuple[str, str]:
|
| 463 |
+
"""Handle user login with session tracking"""
|
| 464 |
+
try:
|
| 465 |
+
if not username or not password:
|
| 466 |
+
return (
|
| 467 |
+
"<div class='error-box'>❌ Please enter both username and password</div>",
|
| 468 |
+
self._get_session_info_html()
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
# Authenticate user
|
| 472 |
+
user_info = self.auth_manager.authenticate_user(username, password)
|
| 473 |
+
|
| 474 |
+
if user_info:
|
| 475 |
+
# Start session tracking
|
| 476 |
+
self.current_session = {
|
| 477 |
+
'user_info': user_info,
|
| 478 |
+
'start_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 479 |
+
'session_id': f"session_{int(time.time())}",
|
| 480 |
+
'analyses_count': 0
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
return (
|
| 484 |
+
f"<div class='success-box'>✅ Welcome, {user_info.get('name', username)}! You are now logged in.</div>",
|
| 485 |
+
self._get_session_info_html()
|
| 486 |
+
)
|
| 487 |
+
else:
|
| 488 |
+
return (
|
| 489 |
+
"<div class='error-box'>❌ Invalid username or password</div>",
|
| 490 |
+
self._get_session_info_html()
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
except Exception as e:
|
| 494 |
+
logging.error(f"Login error: {e}")
|
| 495 |
+
return (
|
| 496 |
+
f"<div class='error-box'>❌ Login failed: {str(e)}</div>",
|
| 497 |
+
self._get_session_info_html()
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
def _handle_logout(self) -> Tuple[str, str]:
|
| 501 |
+
"""Handle user logout"""
|
| 502 |
+
try:
|
| 503 |
+
if self.current_session:
|
| 504 |
+
# Log session end
|
| 505 |
+
session_duration = time.time() - datetime.strptime(
|
| 506 |
+
self.current_session['start_time'], '%Y-%m-%d %H:%M:%S'
|
| 507 |
+
).timestamp()
|
| 508 |
+
|
| 509 |
+
session_data = {
|
| 510 |
+
'user_id': self.current_session['user_info'].get('id'),
|
| 511 |
+
'session_duration': round(session_duration / 60, 2), # Convert to minutes
|
| 512 |
+
'analyses_count': self.current_session.get('analyses_count', 0)
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
# Clear session
|
| 516 |
+
self.current_session = {}
|
| 517 |
+
|
| 518 |
+
return (
|
| 519 |
+
"<div class='warning-box'>👋 You have been logged out successfully</div>",
|
| 520 |
+
self._get_session_info_html()
|
| 521 |
+
)
|
| 522 |
+
else:
|
| 523 |
+
return (
|
| 524 |
+
"<div class='warning-box'>⚠️ No active session to logout</div>",
|
| 525 |
+
self._get_session_info_html()
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
except Exception as e:
|
| 529 |
+
logging.error(f"Logout error: {e}")
|
| 530 |
+
return (
|
| 531 |
+
f"<div class='error-box'>❌ Logout error: {str(e)}</div>",
|
| 532 |
+
self._get_session_info_html()
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
def _start_analysis(self) -> Tuple[str, str]:
|
| 536 |
+
"""Start analysis process with status indicators"""
|
| 537 |
+
return (
|
| 538 |
+
"""
|
| 539 |
+
<div class="processing-indicator" style="display: block;">
|
| 540 |
+
🔄 <strong>Analysis in Progress...</strong><br>
|
| 541 |
+
Please wait while we process your wound image and patient data
|
| 542 |
+
</div>
|
| 543 |
+
""",
|
| 544 |
+
"""
|
| 545 |
+
<div class="metrics-display">
|
| 546 |
+
<strong>Analysis Metrics:</strong><br>
|
| 547 |
+
Status: Initializing...<br>
|
| 548 |
+
Processing Time: 0.0s<br>
|
| 549 |
+
Models Loading: ⏳
|
| 550 |
+
</div>
|
| 551 |
+
"""
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
def _perform_enhanced_analysis(self, patient_name: str, patient_age: int, patient_gender: str,
|
| 555 |
+
wound_location: str, wound_duration: str, pain_level: int,
|
| 556 |
+
moisture_level: str, infection_signs: str, diabetic_status: str,
|
| 557 |
+
previous_treatment: str, medical_history: str, medications: str,
|
| 558 |
+
allergies: str, additional_notes: str, wound_image) -> Tuple:
|
| 559 |
+
"""Perform enhanced analysis with dashboard integration"""
|
| 560 |
+
|
| 561 |
+
start_time = time.time()
|
| 562 |
+
|
| 563 |
+
try:
|
| 564 |
+
# Check authentication
|
| 565 |
+
if not self.current_session:
|
| 566 |
+
return (
|
| 567 |
+
"❌ **Authentication Required**\n\nPlease login before performing analysis.",
|
| 568 |
+
None, None, None, None,
|
| 569 |
+
"<div class='error-box'>❌ Authentication required</div>",
|
| 570 |
+
"<div class='error-box'>Please login to continue</div>",
|
| 571 |
+
self._get_session_info_html()
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
# Validate inputs
|
| 575 |
+
if not wound_image:
|
| 576 |
+
return (
|
| 577 |
+
"❌ **Image Required**\n\nPlease upload a wound image for analysis.",
|
| 578 |
+
None, None, None, None,
|
| 579 |
+
"<div class='error-box'>❌ Wound image required</div>",
|
| 580 |
+
"<div class='error-box'>Please upload an image</div>",
|
| 581 |
+
self._get_session_info_html()
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
if not patient_name.strip():
|
| 585 |
+
return (
|
| 586 |
+
"❌ **Patient Name Required**\n\nPlease enter the patient's name.",
|
| 587 |
+
None, None, None, None,
|
| 588 |
+
"<div class='error-box'>❌ Patient name required</div>",
|
| 589 |
+
"<div class='error-box'>Please enter patient name</div>",
|
| 590 |
+
self._get_session_info_html()
|
| 591 |
+
)
|
| 592 |
+
|
| 593 |
+
# Prepare patient information
|
| 594 |
+
patient_info = {
|
| 595 |
+
'patient_name': patient_name,
|
| 596 |
+
'patient_age': patient_age,
|
| 597 |
+
'patient_gender': patient_gender,
|
| 598 |
+
'wound_location': wound_location,
|
| 599 |
+
'wound_duration': wound_duration,
|
| 600 |
+
'pain_level': pain_level,
|
| 601 |
+
'moisture_level': moisture_level,
|
| 602 |
+
'infection_signs': infection_signs,
|
| 603 |
+
'diabetic_status': diabetic_status,
|
| 604 |
+
'previous_treatment': previous_treatment,
|
| 605 |
+
'medical_history': medical_history,
|
| 606 |
+
'medications': medications,
|
| 607 |
+
'allergies': allergies,
|
| 608 |
+
'additional_notes': additional_notes
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
# Save questionnaire response to dashboard database
|
| 612 |
+
user_id = self.current_session['user_info'].get('id')
|
| 613 |
+
questionnaire_id = self.database_manager.save_questionnaire_response(patient_info, user_id)
|
| 614 |
+
|
| 615 |
+
if not questionnaire_id:
|
| 616 |
+
logging.warning("Failed to save questionnaire response")
|
| 617 |
+
|
| 618 |
+
# Save wound image
|
| 619 |
+
image_id = None
|
| 620 |
+
if questionnaire_id:
|
| 621 |
+
image_id = self.database_manager.save_wound_image(questionnaire_id, wound_image, "wound_analysis.jpg")
|
| 622 |
+
|
| 623 |
+
# Perform comprehensive AI analysis
|
| 624 |
+
analysis_results = self.ai_processor.perform_comprehensive_analysis(wound_image, patient_info)
|
| 625 |
+
|
| 626 |
+
processing_time = analysis_results.get('processing_time', 0)
|
| 627 |
+
|
| 628 |
+
# Save AI analysis results to dashboard database
|
| 629 |
+
analysis_data = {
|
| 630 |
+
'questionnaire_id': questionnaire_id,
|
| 631 |
+
'image_id': image_id,
|
| 632 |
+
'analysis_data': analysis_results,
|
| 633 |
+
'summary': analysis_results.get('report', '')[:1000], # First 1000 chars as summary
|
| 634 |
+
'recommendations': analysis_results.get('report', ''),
|
| 635 |
+
'risk_score': analysis_results.get('risk_score', 0),
|
| 636 |
+
'processing_time': processing_time,
|
| 637 |
+
'model_version': analysis_results.get('model_version', 'v1.0'),
|
| 638 |
+
'visual_results': analysis_results.get('visual_results', {})
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
analysis_id = self.database_manager.save_ai_analysis(analysis_data)
|
| 642 |
+
|
| 643 |
+
# Log analysis session
|
| 644 |
+
session_data = {
|
| 645 |
+
'user_id': user_id,
|
| 646 |
+
'questionnaire_id': questionnaire_id,
|
| 647 |
+
'image_id': image_id,
|
| 648 |
+
'analysis_id': analysis_id,
|
| 649 |
+
'session_duration': processing_time
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
self.dashboard_integration.log_analysis_session(session_data)
|
| 653 |
+
|
| 654 |
+
# Log bot interaction
|
| 655 |
+
interaction_data = {
|
| 656 |
+
'patient_id': None, # Would need to get from patients table
|
| 657 |
+
'practitioner_id': user_id,
|
| 658 |
+
'input_text': f"Wound analysis for {patient_name}",
|
| 659 |
+
'output_text': analysis_results.get('report', '')[:500], # First 500 chars
|
| 660 |
+
'wound_image_url': f"uploads/wound_analysis_{int(time.time())}.jpg",
|
| 661 |
+
'interaction_type': 'wound_analysis'
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
self.dashboard_integration.log_bot_interaction(interaction_data)
|
| 665 |
+
|
| 666 |
+
# Update session count
|
| 667 |
+
self.current_session['analyses_count'] = self.current_session.get('analyses_count', 0) + 1
|
| 668 |
+
|
| 669 |
+
# Prepare results for display
|
| 670 |
+
visual_results = analysis_results.get('visual_results', {})
|
| 671 |
+
report = analysis_results.get('report', 'Analysis completed but no report generated.')
|
| 672 |
+
|
| 673 |
+
# Get analysis images
|
| 674 |
+
detection_image = visual_results.get('detection_image_pil')
|
| 675 |
+
segmentation_image = visual_results.get('segmentation_image_pil')
|
| 676 |
+
|
| 677 |
+
# Create downloadable report
|
| 678 |
+
report_file = self._create_report_file(analysis_results, patient_info)
|
| 679 |
+
|
| 680 |
+
# Create metrics display
|
| 681 |
+
metrics_html = f"""
|
| 682 |
+
<div class="metrics-display">
|
| 683 |
+
<strong>Analysis Completed Successfully!</strong><br>
|
| 684 |
+
Processing Time: {processing_time}s<br>
|
| 685 |
+
Risk Score: {analysis_results.get('risk_score', 0)}/100<br>
|
| 686 |
+
Wound Type: {visual_results.get('wound_type', 'Unknown')}<br>
|
| 687 |
+
Surface Area: {visual_results.get('surface_area_cm2', 0)} cm²<br>
|
| 688 |
+
Model Version: {analysis_results.get('model_version', 'v1.0')}<br>
|
| 689 |
+
Dashboard Integration: ✅ Active
|
| 690 |
+
</div>
|
| 691 |
+
"""
|
| 692 |
+
|
| 693 |
+
success_status = f"""
|
| 694 |
+
<div class="success-box">
|
| 695 |
+
✅ <strong>Analysis Completed Successfully!</strong><br>
|
| 696 |
+
Processing Time: {processing_time}s | Risk Score: {analysis_results.get('risk_score', 0)}/100<br>
|
| 697 |
+
Results saved to dashboard for real-time analytics
|
| 698 |
+
</div>
|
| 699 |
+
"""
|
| 700 |
+
|
| 701 |
+
return (
|
| 702 |
+
report,
|
| 703 |
+
detection_image,
|
| 704 |
+
segmentation_image,
|
| 705 |
+
report_file,
|
| 706 |
+
None, # Images download placeholder
|
| 707 |
+
success_status,
|
| 708 |
+
metrics_html,
|
| 709 |
+
self._get_session_info_html()
|
| 710 |
+
)
|
| 711 |
+
|
| 712 |
+
except Exception as e:
|
| 713 |
+
processing_time = time.time() - start_time
|
| 714 |
+
error_message = str(e)
|
| 715 |
+
logging.error(f"Analysis error: {error_message}")
|
| 716 |
+
|
| 717 |
+
error_status = f"""
|
| 718 |
+
<div class="error-box">
|
| 719 |
+
❌ <strong>Analysis Failed</strong><br>
|
| 720 |
+
Error: {error_message}<br>
|
| 721 |
+
Processing Time: {processing_time:.2f}s
|
| 722 |
+
</div>
|
| 723 |
+
"""
|
| 724 |
+
|
| 725 |
+
error_metrics = f"""
|
| 726 |
+
<div class="error-box">
|
| 727 |
+
<strong>Analysis Error:</strong><br>
|
| 728 |
+
Status: Failed<br>
|
| 729 |
+
Processing Time: {processing_time:.2f}s<br>
|
| 730 |
+
Error: {error_message}
|
| 731 |
+
</div>
|
| 732 |
+
"""
|
| 733 |
+
|
| 734 |
+
return (
|
| 735 |
+
f"❌ **Analysis Failed**\n\n**Error:** {error_message}\n\nPlease check your inputs and try again.",
|
| 736 |
+
None, None, None, None,
|
| 737 |
+
error_status,
|
| 738 |
+
error_metrics,
|
| 739 |
+
self._get_session_info_html()
|
| 740 |
+
)
|
| 741 |
+
|
| 742 |
+
def _create_report_file(self, analysis_results: Dict[str, Any], patient_info: Dict[str, Any]) -> str:
|
| 743 |
+
"""Create downloadable report file"""
|
| 744 |
+
try:
|
| 745 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 746 |
+
filename = f"wound_analysis_report_{timestamp}.md"
|
| 747 |
+
filepath = os.path.join("uploads", filename)
|
| 748 |
+
|
| 749 |
+
# Ensure uploads directory exists
|
| 750 |
+
os.makedirs("uploads", exist_ok=True)
|
| 751 |
+
|
| 752 |
+
# Create comprehensive report
|
| 753 |
+
report_content = f"""# SmartHeal AI Wound Analysis Report
|
| 754 |
+
|
| 755 |
+
**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
| 756 |
+
**Patient:** {patient_info.get('patient_name', 'N/A')}
|
| 757 |
+
**Analysis ID:** {timestamp}
|
| 758 |
+
|
| 759 |
+
## Patient Information
|
| 760 |
+
- **Name:** {patient_info.get('patient_name', 'N/A')}
|
| 761 |
+
- **Age:** {patient_info.get('patient_age', 'N/A')} years
|
| 762 |
+
- **Gender:** {patient_info.get('patient_gender', 'N/A')}
|
| 763 |
+
- **Wound Location:** {patient_info.get('wound_location', 'N/A')}
|
| 764 |
+
- **Wound Duration:** {patient_info.get('wound_duration', 'N/A')}
|
| 765 |
+
- **Pain Level:** {patient_info.get('pain_level', 'N/A')}/10
|
| 766 |
+
|
| 767 |
+
## Analysis Results
|
| 768 |
+
{analysis_results.get('report', 'No report generated')}
|
| 769 |
+
|
| 770 |
+
## Technical Details
|
| 771 |
+
- **Processing Time:** {analysis_results.get('processing_time', 0)}s
|
| 772 |
+
- **Risk Score:** {analysis_results.get('risk_score', 0)}/100
|
| 773 |
+
- **Model Version:** {analysis_results.get('model_version', 'Unknown')}
|
| 774 |
+
- **Analysis Timestamp:** {analysis_results.get('analysis_timestamp', 'Unknown')}
|
| 775 |
+
|
| 776 |
+
---
|
| 777 |
+
*Generated by SmartHeal AI Enhanced Edition with Dashboard Integration*
|
| 778 |
+
"""
|
| 779 |
+
|
| 780 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 781 |
+
f.write(report_content)
|
| 782 |
+
|
| 783 |
+
return filepath
|
| 784 |
+
|
| 785 |
+
except Exception as e:
|
| 786 |
+
logging.error(f"Error creating report file: {e}")
|
| 787 |
+
return None
|
| 788 |
+
|
| 789 |
+
def _refresh_dashboard_status(self) -> Tuple[str, str]:
|
| 790 |
+
"""Refresh dashboard integration status"""
|
| 791 |
+
try:
|
| 792 |
+
status = self.dashboard_integration.get_integration_status()
|
| 793 |
+
analytics_data = self.database_manager.get_analytics_data()
|
| 794 |
+
|
| 795 |
+
if status['api_running'] and status['database_connected']:
|
| 796 |
+
status_html = f"""
|
| 797 |
+
<div class="integration-status">
|
| 798 |
+
✅ <strong>Dashboard Integration Active</strong><br>
|
| 799 |
+
API Server: Running on port 5001<br>
|
| 800 |
+
Database: Connected<br>
|
| 801 |
+
Last Updated: {status['timestamp']}<br>
|
| 802 |
+
<a href="http://localhost:5001/api/health" target="_blank">🔗 Test API Health</a>
|
| 803 |
+
</div>
|
| 804 |
+
"""
|
| 805 |
+
else:
|
| 806 |
+
status_html = f"""
|
| 807 |
+
<div class="error-box">
|
| 808 |
+
❌ <strong>Dashboard Integration Issues</strong><br>
|
| 809 |
+
API Running: {status['api_running']}<br>
|
| 810 |
+
Database Connected: {status['database_connected']}<br>
|
| 811 |
+
Last Checked: {status['timestamp']}
|
| 812 |
+
</div>
|
| 813 |
+
"""
|
| 814 |
+
|
| 815 |
+
analytics_html = f"""
|
| 816 |
+
<div class="analytics-info">
|
| 817 |
+
📊 <strong>Analytics Summary</strong><br>
|
| 818 |
+
Total Analyses: {analytics_data.get('total_analyses', 0)}<br>
|
| 819 |
+
Average Processing Time: {analytics_data.get('avg_processing_time', 0)}s<br>
|
| 820 |
+
High Risk Cases: {analytics_data.get('high_risk_count', 0)}<br>
|
| 821 |
+
Average Risk Score: {analytics_data.get('avg_risk_score', 0)}<br>
|
| 822 |
+
Analyses Today: {analytics_data.get('analyses_today', 0)}
|
| 823 |
+
</div>
|
| 824 |
+
"""
|
| 825 |
+
|
| 826 |
+
return status_html, analytics_html
|
| 827 |
+
|
| 828 |
+
except Exception as e:
|
| 829 |
+
logging.error(f"Error refreshing dashboard status: {e}")
|
| 830 |
+
return (
|
| 831 |
+
f"<div class='error-box'>❌ Error refreshing status: {str(e)}</div>",
|
| 832 |
+
"<div class='error-box'>❌ Unable to load analytics</div>"
|
| 833 |
+
)
|
| 834 |
+
|
| 835 |
+
def _get_analytics_summary(self) -> Tuple[str, str]:
|
| 836 |
+
"""Get comprehensive analytics summary"""
|
| 837 |
+
try:
|
| 838 |
+
analytics_data = self.database_manager.get_analytics_data()
|
| 839 |
+
interaction_history = self.database_manager.get_interaction_history(10)
|
| 840 |
+
|
| 841 |
+
# Create detailed analytics HTML
|
| 842 |
+
analytics_html = f"""
|
| 843 |
+
<div class="analytics-info">
|
| 844 |
+
<h3>📈 Comprehensive Analytics</h3>
|
| 845 |
+
<strong>Analysis Statistics:</strong><br>
|
| 846 |
+
• Total Analyses: {analytics_data.get('total_analyses', 0)}<br>
|
| 847 |
+
• Analyses Today: {analytics_data.get('analyses_today', 0)}<br>
|
| 848 |
+
• Analyses This Week: {analytics_data.get('analyses_this_week', 0)}<br>
|
| 849 |
+
• Average Processing Time: {analytics_data.get('avg_processing_time', 0)}s<br>
|
| 850 |
+
• Average Risk Score: {analytics_data.get('avg_risk_score', 0)}/100<br>
|
| 851 |
+
<br>
|
| 852 |
+
<strong>Risk Distribution:</strong><br>
|
| 853 |
+
• High Risk Cases: {analytics_data.get('high_risk_count', 0)}<br>
|
| 854 |
+
• Unique Questionnaires: {analytics_data.get('unique_questionnaires', 0)}<br>
|
| 855 |
+
• Analyses with Images: {analytics_data.get('analyses_with_images', 0)}<br>
|
| 856 |
+
</div>
|
| 857 |
+
"""
|
| 858 |
+
|
| 859 |
+
# Create recent activity HTML
|
| 860 |
+
activity_html = "<div class='result-box'><h3>🕒 Recent Activity</h3>"
|
| 861 |
+
|
| 862 |
+
if interaction_history:
|
| 863 |
+
activity_html += "<ul>"
|
| 864 |
+
for interaction in interaction_history[:5]:
|
| 865 |
+
timestamp = interaction.get('interacted_at', 'Unknown')
|
| 866 |
+
if isinstance(timestamp, str):
|
| 867 |
+
try:
|
| 868 |
+
timestamp = datetime.fromisoformat(timestamp.replace('Z', '+00:00')).strftime('%Y-%m-%d %H:%M')
|
| 869 |
+
except:
|
| 870 |
+
pass
|
| 871 |
+
|
| 872 |
+
activity_html += f"""
|
| 873 |
+
<li><strong>{timestamp}</strong> - {interaction.get('interaction_type', 'Unknown')}
|
| 874 |
+
(Patient: {interaction.get('patient_name', 'Unknown')})</li>
|
| 875 |
+
"""
|
| 876 |
+
activity_html += "</ul>"
|
| 877 |
+
else:
|
| 878 |
+
activity_html += "<p>No recent activity found.</p>"
|
| 879 |
+
|
| 880 |
+
activity_html += "</div>"
|
| 881 |
+
|
| 882 |
+
return analytics_html, activity_html
|
| 883 |
+
|
| 884 |
+
except Exception as e:
|
| 885 |
+
logging.error(f"Error getting analytics summary: {e}")
|
| 886 |
+
return (
|
| 887 |
+
f"<div class='error-box'>❌ Error loading analytics: {str(e)}</div>",
|
| 888 |
+
"<div class='error-box'>❌ Unable to load recent activity</div>"
|
| 889 |
+
)
|
| 890 |
+
|
src/evaluation.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:990fcd18367f71c1c4729b492d21c18a5b86108a6c86faa98ba63945f29f5816
|
| 3 |
+
size 651978
|
src/segmentation_model.h5
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cd892723d03aa943b719f97f659bddbaf02ba2670606e2a7b0b553ce661d2f62
|
| 3 |
+
size 65345400
|
static/style.css
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ================= FORCE LIGHT MODE AND TEXT VISIBILITY ================= */
|
| 2 |
+
/* Override Gradio's root variables and force light mode */
|
| 3 |
+
:root {
|
| 4 |
+
--background-fill-primary: #FFFFFF !important;
|
| 5 |
+
--background-fill-secondary: #F7FAFC !important;
|
| 6 |
+
--text-color-primary: #1A202C !important;
|
| 7 |
+
--text-color-secondary: #4A5568 !important;
|
| 8 |
+
--color-accent: #E53E3E !important;
|
| 9 |
+
--color-accent-soft: #FED7D7 !important;
|
| 10 |
+
--block-background-fill: #FFFFFF !important;
|
| 11 |
+
--block-border-color: #E2E8F0 !important;
|
| 12 |
+
--block-title-text-color: #1A202C !important;
|
| 13 |
+
--block-label-text-color: #1A202C !important;
|
| 14 |
+
--block-info-text-color: #4A5568 !important;
|
| 15 |
+
--input-background-fill: #FFFFFF !important;
|
| 16 |
+
--input-border-color: #CBD5E0 !important;
|
| 17 |
+
--input-text-color: #1A202C !important;
|
| 18 |
+
--button-primary-background-fill: #E53E3E !important;
|
| 19 |
+
--button-primary-text-color: #FFFFFF !important;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/* Force light theme on the entire application */
|
| 23 |
+
.gradio-container,
|
| 24 |
+
.gradio-container *,
|
| 25 |
+
.gr-app,
|
| 26 |
+
.gr-app * {
|
| 27 |
+
color-scheme: light !important;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* ------------------- AGGRESSIVE TEXT VISIBILITY ------------------- */
|
| 31 |
+
/* Force text color on ALL elements */
|
| 32 |
+
.gradio-container,
|
| 33 |
+
.gradio-container *:not(.gr-button):not(.medical-header *),
|
| 34 |
+
.gr-app,
|
| 35 |
+
.gr-app *:not(.gr-button):not(.medical-header *) {
|
| 36 |
+
color: #1A202C !important;
|
| 37 |
+
background-color: transparent !important;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* Specific overrides for common Gradio elements */
|
| 41 |
+
.gr-block,
|
| 42 |
+
.gr-form,
|
| 43 |
+
.gr-box,
|
| 44 |
+
.gr-panel {
|
| 45 |
+
background-color: #FFFFFF !important;
|
| 46 |
+
color: #1A202C !important;
|
| 47 |
+
border: 1px solid #E2E8F0 !important;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/* Input elements */
|
| 51 |
+
.gr-textbox,
|
| 52 |
+
.gr-textbox input,
|
| 53 |
+
.gr-dropdown,
|
| 54 |
+
.gr-dropdown select,
|
| 55 |
+
.gr-number,
|
| 56 |
+
.gr-number input,
|
| 57 |
+
.gr-file,
|
| 58 |
+
.gr-radio,
|
| 59 |
+
.gr-checkbox {
|
| 60 |
+
background-color: #FFFFFF !important;
|
| 61 |
+
color: #1A202C !important;
|
| 62 |
+
border: 2px solid #CBD5E0 !important;
|
| 63 |
+
border-radius: 8px !important;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* Labels and text */
|
| 67 |
+
label,
|
| 68 |
+
.gr-input-label,
|
| 69 |
+
.gr-radio label,
|
| 70 |
+
.gr-checkbox label,
|
| 71 |
+
[data-testid="block-label"],
|
| 72 |
+
.gr-form label,
|
| 73 |
+
.gr-markdown,
|
| 74 |
+
.gr-markdown *,
|
| 75 |
+
.gr-output,
|
| 76 |
+
.gr-output * {
|
| 77 |
+
color: #1A202C !important;
|
| 78 |
+
background-color: transparent !important;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* Buttons */
|
| 82 |
+
button.gr-button,
|
| 83 |
+
button.gr-button-primary {
|
| 84 |
+
background: linear-gradient(135deg, #E53E3E 0%, #C53030 100%) !important;
|
| 85 |
+
color: #FFFFFF !important;
|
| 86 |
+
border: none !important;
|
| 87 |
+
border-radius: 8px !important;
|
| 88 |
+
font-weight: 600 !important;
|
| 89 |
+
padding: 12px 24px !important;
|
| 90 |
+
min-height: 44px !important;
|
| 91 |
+
font-size: 1rem !important;
|
| 92 |
+
letter-spacing: 0.3px !important;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
button.gr-button:hover,
|
| 96 |
+
button.gr-button-primary:hover {
|
| 97 |
+
background: linear-gradient(135deg, #C53030 0%, #B91C1C 100%) !important;
|
| 98 |
+
transform: translateY(-1px) !important;
|
| 99 |
+
box-shadow: 0 4px 12px rgba(229, 62, 62, 0.3) !important;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
button.gr-button:disabled {
|
| 103 |
+
background: #CBD5E0 !important;
|
| 104 |
+
color: #1A202C !important;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/* ------------------- GENERAL LAYOUT ------------------- */
|
| 108 |
+
.gradio-container {
|
| 109 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif !important;
|
| 110 |
+
background: #FFFFFF !important;
|
| 111 |
+
color: #1A202C !important;
|
| 112 |
+
line-height: 1.6 !important;
|
| 113 |
+
font-size: 16px !important;
|
| 114 |
+
text-rendering: optimizeLegibility !important;
|
| 115 |
+
-webkit-font-smoothing: antialiased !important;
|
| 116 |
+
-moz-osx-font-smoothing: grayscale !important;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/* Hide default footer */
|
| 120 |
+
footer { display: none !important; }
|
| 121 |
+
|
| 122 |
+
/* ------------------- HEADER ------------------- */
|
| 123 |
+
.medical-header {
|
| 124 |
+
background: linear-gradient(135deg, #1b5fc1 0%, #174ea6 100%) !important;
|
| 125 |
+
padding: 24px !important;
|
| 126 |
+
text-align: left !important;
|
| 127 |
+
border-radius: 16px !important;
|
| 128 |
+
margin-bottom: 32px !important;
|
| 129 |
+
box-shadow: 0 8px 24px rgba(27, 95, 193, 0.2) !important;
|
| 130 |
+
display: flex !important;
|
| 131 |
+
align-items: center !important;
|
| 132 |
+
flex-wrap: wrap !important;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.medical-header h1 {
|
| 136 |
+
color: #FFFFFF !important;
|
| 137 |
+
font-size: 2.75rem !important;
|
| 138 |
+
font-weight: 700 !important;
|
| 139 |
+
margin: 0 !important;
|
| 140 |
+
line-height: 1.2 !important;
|
| 141 |
+
letter-spacing: -0.025em !important;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.medical-header p {
|
| 145 |
+
color: #FFFFFF !important;
|
| 146 |
+
font-size: 1.125rem !important;
|
| 147 |
+
margin: 8px 0 0 0 !important;
|
| 148 |
+
font-weight: 400 !important;
|
| 149 |
+
line-height: 1.5 !important;
|
| 150 |
+
opacity: 0.95 !important;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.logo-container {
|
| 154 |
+
display: flex !important;
|
| 155 |
+
align-items: center !important;
|
| 156 |
+
justify-content: flex-start !important;
|
| 157 |
+
flex-wrap: wrap !important;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.logo {
|
| 161 |
+
width: 64px !important;
|
| 162 |
+
height: 64px !important;
|
| 163 |
+
margin-right: 24px !important;
|
| 164 |
+
border-radius: 16px !important;
|
| 165 |
+
background: white !important;
|
| 166 |
+
padding: 8px !important;
|
| 167 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1) !important;
|
| 168 |
+
flex-shrink: 0 !important;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
/* ------------------- DISCLAIMER BOX ------------------- */
|
| 172 |
+
.disclaimer-box {
|
| 173 |
+
border: 2px solid #FEB2B2 !important;
|
| 174 |
+
background-color: #FFF5F5 !important;
|
| 175 |
+
padding: 24px !important;
|
| 176 |
+
border-radius: 16px !important;
|
| 177 |
+
margin: 24px 0 !important;
|
| 178 |
+
box-shadow: 0 2px 8px rgba(254, 178, 178, 0.1) !important;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.disclaimer-box h3 {
|
| 182 |
+
margin-top: 0 !important;
|
| 183 |
+
margin-bottom: 12px !important;
|
| 184 |
+
color: #C53030 !important;
|
| 185 |
+
font-size: 1.25rem !important;
|
| 186 |
+
font-weight: 700 !important;
|
| 187 |
+
line-height: 1.4 !important;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.disclaimer-box p {
|
| 191 |
+
color: #1A202C !important;
|
| 192 |
+
margin-bottom: 0 !important;
|
| 193 |
+
font-size: 1rem !important;
|
| 194 |
+
line-height: 1.6 !important;
|
| 195 |
+
font-weight: 400 !important;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/* ------------------- CARDS & TITLES ------------------- */
|
| 199 |
+
.medical-card-title {
|
| 200 |
+
background: white !important;
|
| 201 |
+
border-radius: 16px !important;
|
| 202 |
+
padding: 24px !important;
|
| 203 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.08) !important;
|
| 204 |
+
border: 1px solid #E2E8F0 !important;
|
| 205 |
+
margin: 20px 0 !important;
|
| 206 |
+
color: #1A202C !important;
|
| 207 |
+
font-size: 1.5rem !important;
|
| 208 |
+
font-weight: 600 !important;
|
| 209 |
+
line-height: 1.4 !important;
|
| 210 |
+
text-align: center !important;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/* ------------------- MARKDOWN OUTPUT ------------------- */
|
| 214 |
+
.gr-markdown {
|
| 215 |
+
background-color: #FFFFFF !important;
|
| 216 |
+
color: #1A202C !important;
|
| 217 |
+
padding: 16px !important;
|
| 218 |
+
border-radius: 8px !important;
|
| 219 |
+
border: 1px solid #E2E8F0 !important;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.gr-markdown h1,
|
| 223 |
+
.gr-markdown h2,
|
| 224 |
+
.gr-markdown h3,
|
| 225 |
+
.gr-markdown h4,
|
| 226 |
+
.gr-markdown h5,
|
| 227 |
+
.gr-markdown h6 {
|
| 228 |
+
color: #1A202C !important;
|
| 229 |
+
font-weight: 700 !important;
|
| 230 |
+
line-height: 1.3 !important;
|
| 231 |
+
margin-top: 1.5em !important;
|
| 232 |
+
margin-bottom: 0.5em !important;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.gr-markdown h1 { font-size: 2rem !important; }
|
| 236 |
+
.gr-markdown h2 { font-size: 1.75rem !important; }
|
| 237 |
+
.gr-markdown h3 { font-size: 1.5rem !important; }
|
| 238 |
+
.gr-markdown h4 { font-size: 1.25rem !important; }
|
| 239 |
+
|
| 240 |
+
.gr-markdown p,
|
| 241 |
+
.gr-markdown li,
|
| 242 |
+
.gr-markdown span,
|
| 243 |
+
.gr-markdown div {
|
| 244 |
+
color: #1A202C !important;
|
| 245 |
+
font-size: 1rem !important;
|
| 246 |
+
line-height: 1.7 !important;
|
| 247 |
+
margin-bottom: 1em !important;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.gr-markdown ul,
|
| 251 |
+
.gr-markdown ol {
|
| 252 |
+
padding-left: 1.5em !important;
|
| 253 |
+
margin-bottom: 1em !important;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.gr-markdown li {
|
| 257 |
+
margin-bottom: 0.5em !important;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.gr-markdown strong,
|
| 261 |
+
.gr-markdown b {
|
| 262 |
+
color: #1A202C !important;
|
| 263 |
+
font-weight: 700 !important;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.gr-markdown em,
|
| 267 |
+
.gr-markdown i {
|
| 268 |
+
color: #1A202C !important;
|
| 269 |
+
font-style: italic !important;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/* ------------------- IMAGE UPLOAD ------------------- */
|
| 273 |
+
.gr-image {
|
| 274 |
+
border: 3px dashed #CBD5E0 !important;
|
| 275 |
+
border-radius: 16px !important;
|
| 276 |
+
background-color: #F7FAFC !important;
|
| 277 |
+
transition: all 0.2s ease !important;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.gr-image:hover {
|
| 281 |
+
border-color: #E53E3E !important;
|
| 282 |
+
background-color: #FFF5F5 !important;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/* ------------------- RADIO BUTTONS ------------------- */
|
| 286 |
+
.gr-radio input[type="radio"] {
|
| 287 |
+
margin-right: 8px !important;
|
| 288 |
+
transform: scale(1.2) !important;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.gr-radio label {
|
| 292 |
+
display: flex !important;
|
| 293 |
+
align-items: center !important;
|
| 294 |
+
padding: 8px 0 !important;
|
| 295 |
+
font-size: 1rem !important;
|
| 296 |
+
line-height: 1.5 !important;
|
| 297 |
+
cursor: pointer !important;
|
| 298 |
+
color: #1A202C !important;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
/* ------------------- TABS ------------------- */
|
| 302 |
+
.gr-tab {
|
| 303 |
+
color: #1A202C !important;
|
| 304 |
+
font-weight: 500 !important;
|
| 305 |
+
font-size: 1rem !important;
|
| 306 |
+
padding: 12px 20px !important;
|
| 307 |
+
background-color: #F7FAFC !important;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.gr-tab.selected {
|
| 311 |
+
color: #E53E3E !important;
|
| 312 |
+
font-weight: 600 !important;
|
| 313 |
+
border-bottom: 2px solid #E53E3E !important;
|
| 314 |
+
background-color: #FFFFFF !important;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
/* ------------------- FOOTER ------------------- */
|
| 318 |
+
.medical-footer {
|
| 319 |
+
text-align: center !important;
|
| 320 |
+
padding: 24px !important;
|
| 321 |
+
color: #4A5568 !important;
|
| 322 |
+
font-size: 0.95rem !important;
|
| 323 |
+
line-height: 1.6 !important;
|
| 324 |
+
border-top: 1px solid #E2E8F0 !important;
|
| 325 |
+
margin-top: 40px !important;
|
| 326 |
+
background-color: #F7FAFC !important;
|
| 327 |
+
border-radius: 12px !important;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
/* ------------------- RESPONSIVE DESIGN ------------------- */
|
| 331 |
+
@media (max-width: 768px) {
|
| 332 |
+
.medical-header {
|
| 333 |
+
padding: 20px !important;
|
| 334 |
+
text-align: center !important;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.medical-header h1 {
|
| 338 |
+
font-size: 2.25rem !important;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.medical-header p {
|
| 342 |
+
font-size: 1rem !important;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.logo {
|
| 346 |
+
margin-right: 16px !important;
|
| 347 |
+
margin-bottom: 16px !important;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.medical-card-title {
|
| 351 |
+
font-size: 1.25rem !important;
|
| 352 |
+
padding: 20px !important;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.gr-button {
|
| 356 |
+
padding: 12px 24px !important;
|
| 357 |
+
font-size: 0.95rem !important;
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
/* ------------------- ACCESSIBILITY IMPROVEMENTS ------------------- */
|
| 362 |
+
.gr-button:focus {
|
| 363 |
+
outline: 3px solid rgba(229, 62, 62, 0.5) !important;
|
| 364 |
+
outline-offset: 2px !important;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.gr-textbox:focus,
|
| 368 |
+
.gr-dropdown:focus,
|
| 369 |
+
.gr-file:focus,
|
| 370 |
+
.gr-number:focus {
|
| 371 |
+
outline: 3px solid rgba(229, 62, 62, 0.3) !important;
|
| 372 |
+
outline-offset: 2px !important;
|
| 373 |
+
border-color: #E53E3E !important;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
/* Success/Error status styling */
|
| 377 |
+
.status-success {
|
| 378 |
+
color: #22C55E !important;
|
| 379 |
+
background-color: #F0FDF4 !important;
|
| 380 |
+
border: 1px solid #BBF7D0 !important;
|
| 381 |
+
padding: 12px !important;
|
| 382 |
+
border-radius: 8px !important;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.status-error {
|
| 386 |
+
color: #EF4444 !important;
|
| 387 |
+
background-color: #FEF2F2 !important;
|
| 388 |
+
border: 1px solid #FECACA !important;
|
| 389 |
+
padding: 12px !important;
|
| 390 |
+
border-radius: 8px !important;
|
| 391 |
+
}
|