SmartHeal commited on
Commit
4470362
·
verified ·
1 Parent(s): 5d2a233

Update src/ui_components_original.py

Browse files
Files changed (1) hide show
  1. src/ui_components_original.py +343 -399
src/ui_components_original.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import gradio as gr
2
  import os
3
  import re
@@ -6,23 +8,36 @@ import base64
6
  from datetime import datetime
7
  from PIL import Image
8
  import html
9
- from typing import Optional
10
- from .patient_history import PatientHistoryManager, ReportGenerator
11
- import spaces
12
- def pil_to_base64(pil_image):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  """Convert PIL Image to base64 data URL"""
14
  import io
15
- import base64
16
- from PIL import Image
17
-
18
  if pil_image is None:
19
  return None
20
-
21
  try:
22
- # Convert image to RGB if it's not already
23
  if pil_image.mode != 'RGB':
24
  pil_image = pil_image.convert('RGB')
25
-
26
  buffer = io.BytesIO()
27
  pil_image.save(buffer, format='PNG')
28
  img_str = base64.b64encode(buffer.getvalue()).decode()
@@ -31,6 +46,167 @@ def pil_to_base64(pil_image):
31
  logging.error(f"Error converting PIL image to base64: {e}")
32
  return None
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  class UIComponents:
35
  def __init__(self, auth_manager, database_manager, wound_analyzer):
36
  self.auth_manager = auth_manager
@@ -39,7 +215,7 @@ class UIComponents:
39
  self.current_user = {}
40
  self.patient_history_manager = PatientHistoryManager(database_manager)
41
  self.report_generator = ReportGenerator()
42
-
43
  # Ensure uploads directory exists
44
  if not os.path.exists("uploads"):
45
  os.makedirs("uploads", exist_ok=True)
@@ -48,12 +224,10 @@ class UIComponents:
48
  """Convert image to base64 data URL for embedding in HTML"""
49
  if not image_path or not os.path.exists(image_path):
50
  return None
51
-
52
  try:
53
  with open(image_path, "rb") as image_file:
54
  encoded_string = base64.b64encode(image_file.read()).decode()
55
-
56
- # Determine image format
57
  image_ext = os.path.splitext(image_path)[1].lower()
58
  if image_ext in [".jpg", ".jpeg"]:
59
  mime_type = "image/jpeg"
@@ -62,8 +236,8 @@ class UIComponents:
62
  elif image_ext == ".gif":
63
  mime_type = "image/gif"
64
  else:
65
- mime_type = "image/png" # Default to PNG
66
-
67
  return f"data:{mime_type};base64,{encoded_string}"
68
  except Exception as e:
69
  logging.error(f"Error converting image to base64: {e}")
@@ -73,40 +247,37 @@ class UIComponents:
73
  """Convert markdown text to proper HTML format with enhanced support"""
74
  if not markdown_text:
75
  return ""
76
-
77
- # Escape HTML entities first to prevent issues with special characters
78
  html_text = html.escape(markdown_text)
79
 
80
- # Convert headers
81
  html_text = re.sub(r"^### (.*?)$", r"<h3>\1</h3>", html_text, flags=re.MULTILINE)
82
  html_text = re.sub(r"^## (.*?)$", r"<h2>\1</h2>", html_text, flags=re.MULTILINE)
83
  html_text = re.sub(r"^# (.*?)$", r"<h1>\1</h1>", html_text, flags=re.MULTILINE)
84
-
85
- # Convert bold text
86
  html_text = re.sub(r"\*\*(.*?)\*\*", r"<strong>\1</strong>", html_text)
87
-
88
- # Convert italic text
89
  html_text = re.sub(r"\*(.*?)\*", r"<em>\1</em>", html_text)
90
 
91
- # Convert code blocks (triple backticks)
92
  html_text = re.sub(r"```(.*?)```", r"<pre><code>\1</code></pre>", html_text, flags=re.DOTALL)
93
- # Convert inline code (single backticks)
94
  html_text = re.sub(r"`(.*?)`", r"<code>\1</code>", html_text)
95
 
96
- # Convert blockquotes
97
  html_text = re.sub(r"^> (.*?)$", r"<blockquote>\1</blockquote>", html_text, flags=re.MULTILINE)
98
 
99
- # Convert links
100
  html_text = re.sub(r"\[(.*?)\]\((.*?)\)", r"<a href=\"\2\">\1</a>", html_text)
101
 
102
- # Convert horizontal rules
103
  html_text = re.sub(r"^\s*[-*_]{3,}\s*$", r"<hr>", html_text, flags=re.MULTILINE)
104
 
105
- # Convert bullet points
106
  lines = html_text.split("\n")
107
  in_list = False
108
  result_lines = []
109
-
110
  for line in lines:
111
  stripped = line.strip()
112
  if stripped.startswith("- "):
@@ -122,10 +293,8 @@ class UIComponents:
122
  result_lines.append(f"<p>{stripped}</p>")
123
  else:
124
  result_lines.append("<br>")
125
-
126
  if in_list:
127
  result_lines.append("</ul>")
128
-
129
  return "\n".join(result_lines)
130
 
131
  def get_organizations_dropdown(self):
@@ -292,42 +461,19 @@ button.gr-button:hover, button.gr-button-primary:hover {
292
  backdrop-filter: blur(10px) !important;
293
  }
294
 
295
- /* Image gallery styling for better visualization */
296
  .image-gallery {
297
  display: grid;
298
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
299
  gap: 20px;
300
  margin: 20px 0;
301
  }
 
 
 
 
302
 
303
- .image-item {
304
- background: #f8f9fa;
305
- border-radius: 12px;
306
- padding: 15px;
307
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
308
- text-align: center;
309
- }
310
-
311
- .image-item img {
312
- max-width: 100%;
313
- height: auto;
314
- border-radius: 8px;
315
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
316
- }
317
-
318
- .image-item h4 {
319
- margin: 15px 0 5px 0;
320
- color: #2d3748;
321
- font-weight: 600;
322
- }
323
-
324
- .image-item p {
325
- margin: 0;
326
- color: #666;
327
- font-size: 0.9em;
328
- }
329
-
330
- /* Analyze button special styling */
331
  #analyze-btn {
332
  background: linear-gradient(135deg, #1B5CF3 0%, #1E3A8A 100%) !important;
333
  color: #FFFFFF !important;
@@ -340,38 +486,19 @@ button.gr-button:hover, button.gr-button-primary:hover {
340
  text-align: center !important;
341
  transition: all 0.2s ease-in-out !important;
342
  }
343
-
344
  #analyze-btn:hover {
345
  background: linear-gradient(135deg, #174ea6 0%, #123b82 100%) !important;
346
  box-shadow: 0 4px 14px rgba(27, 95, 193, 0.4) !important;
347
  transform: translateY(-2px) !important;
348
  }
349
 
350
- /* Responsive design */
351
  @media (max-width: 768px) {
352
- .medical-header {
353
- padding: 16px !important;
354
- text-align: center !important;
355
- }
356
-
357
- .medical-header h1 {
358
- font-size: 2rem !important;
359
- }
360
-
361
- .logo {
362
- width: 48px !important;
363
- height: 48px !important;
364
- margin-right: 16px !important;
365
- }
366
-
367
- .gr-form {
368
- padding: 16px !important;
369
- margin: 8px 0 !important;
370
- }
371
-
372
- .image-gallery {
373
- grid-template-columns: 1fr;
374
- }
375
  }
376
  """
377
 
@@ -380,34 +507,31 @@ button.gr-button:hover, button.gr-button-primary:hover {
380
  SmartHeal UI – aligned with current DB + history manager:
381
  • Login (practitioner / organization)
382
  • Practitioner: Wound Analysis (existing vs new patient), Patient History, View Details
383
- • Images from disk are shown via data URLs for reliable rendering
384
  """
385
  import gradio as gr
386
  from PIL import Image
387
  import os, html, logging
388
-
389
  # ----------------------- helpers (inner) -----------------------
390
-
391
- self._patient_choices = [] # list[str] rendered in dropdown
392
- self._patient_map = {} # label -> patient_id (int)
393
-
394
  def _to_data_url_if_local(path_or_url: str) -> str:
395
  if not path_or_url:
396
  return ""
397
  try:
398
  if os.path.exists(path_or_url):
399
  return self.image_to_base64(path_or_url) or ""
400
- return path_or_url # already a URL
401
  except Exception:
402
  return ""
403
-
404
  def _refresh_patient_dropdown(user_id: int):
405
  """Query patient's list and prepare dropdown choices."""
406
  self._patient_choices.clear()
407
  self._patient_map.clear()
408
  try:
409
  rows = self.patient_history_manager.get_patient_list(user_id) or []
410
- # label starts with id -> stable parse
411
  for r in rows:
412
  pid = int(r.get("id") or 0)
413
  nm = r.get("patient_name") or "Unknown"
@@ -419,69 +543,15 @@ button.gr-button:hover, button.gr-button-primary:hover {
419
  self._patient_map[label] = pid
420
  except Exception as e:
421
  logging.error(f"refresh dropdown error: {e}")
422
-
423
  def _label_to_id(label: str):
424
- if not label: return None
425
- try:
426
- return int(str(label).split("•", 1)[0].strip())
427
- except Exception:
428
- return None
429
-
430
- def _fetch_patient_core(pid: int):
431
- """Get name/age/gender for an existing patient id."""
432
- row = self.database_manager.execute_query_one(
433
- "SELECT id, name, age, gender FROM patients WHERE id=%s LIMIT 1", (pid,)
434
- )
435
- return row or {}
436
-
437
- def _response_to_patient_id(resp_ref):
438
- """
439
- Accepts either a response_id int or a dict like
440
- {'response_id': 70, 'patient_id': 346, ...}.
441
- Returns patient_id (int) by preferring any direct dict field,
442
- otherwise queries by response id.
443
- """
444
- # If the caller already gave us a dict, take the shortcut
445
- if isinstance(resp_ref, dict):
446
- pid = resp_ref.get("patient_id")
447
- if pid is not None:
448
- try:
449
- return int(pid)
450
- except Exception:
451
- pass
452
- resp_id = resp_ref.get("response_id") or resp_ref.get("id")
453
- try:
454
- resp_id = int(resp_id) if resp_id is not None else None
455
- except Exception:
456
- resp_id = None
457
- else:
458
- try:
459
- resp_id = int(resp_ref) if resp_ref is not None else None
460
- except Exception:
461
- resp_id = None
462
-
463
- if not resp_id:
464
  return None
465
-
466
- row = self.database_manager.execute_query_one(
467
- "SELECT patient_id FROM questionnaire_responses WHERE id=%s LIMIT 1",
468
- (resp_id,) # <-- ensure scalar param
469
- )
470
  try:
471
- return int(row["patient_id"]) if row and "patient_id" in row else None
472
  except Exception:
473
  return None
474
-
475
- def _rows_with_inline_images(rows: list[dict]) -> list[dict]:
476
- """Convert local file paths to data URLs so HTML displays them anywhere."""
477
- out = []
478
- for r in rows or []:
479
- r = dict(r)
480
- if r.get("image_url"):
481
- r["image_url"] = _to_data_url_if_local(r["image_url"])
482
- out.append(r)
483
- return out
484
-
485
  def _resolve_org_id_from_dropdown(label: str) -> Optional[int]:
486
  """
487
  Dropdown items look like: 'Org Name - Location'.
@@ -499,7 +569,6 @@ button.gr-button:hover, button.gr-button-primary:hover {
499
  if row and "id" in row:
500
  return int(row["id"])
501
  else:
502
- # fallback: match by name only
503
  row = self.database_manager.execute_query_one(
504
  "SELECT id FROM organizations WHERE name=%s ORDER BY id DESC LIMIT 1",
505
  (label.strip(),)
@@ -509,9 +578,8 @@ button.gr-button:hover, button.gr-button-primary:hover {
509
  except Exception as e:
510
  logging.error(f"resolve org id error: {e}")
511
  return None
512
-
513
  # ----------------------- Blocks UI -----------------------
514
-
515
  with gr.Blocks(css=self.get_custom_css(), title="SmartHeal - AI Wound Care Assistant") as app:
516
  # Header
517
  logo_url = "https://scontent.fccu31-2.fna.fbcdn.net/v/t39.30808-6/275933824_102121829111657_3325198727201325354_n.jpg?_nc_cat=104&ccb=1-7&_nc_sid=6ee11a&_nc_ohc=UIBKBXaPiSsQ7kNvwHy41Wj&_nc_oc=Adm8WwTOq--itjR7UgI7mUy57nDeZQ8zZSh4YxQ6F0iq8gmoUxtQ4-nZV7vTAWlYJxY&_nc_zt=23&_nc_ht=scontent.fccu31-2.fna&_nc_gid=1Em7m4EFZJCDDExF5Mmlfg&oh=00_AfXodrOfPdY61Bes8PY2s9o0Z2kXAVdmkk0Yd4dNOo8mgw&oe=68A2358B"
@@ -524,7 +592,7 @@ button.gr-button:hover, button.gr-button-primary:hover {
524
  </div>
525
  </div>
526
  """)
527
-
528
  # Disclaimer
529
  gr.HTML("""
530
  <div style="border:2px solid #FF6B6B;background:#FFE5E5;padding:15px;border-radius:12px;margin:10px 0;">
@@ -532,7 +600,7 @@ button.gr-button:hover, button.gr-button-primary:hover {
532
  <p><strong>This system is for testing/education and not a substitute for clinical judgment.</strong></p>
533
  </div>
534
  """)
535
-
536
  # Panels: auth vs practitioner vs organization
537
  with gr.Row():
538
  with gr.Column(visible=True) as auth_panel:
@@ -542,31 +610,31 @@ button.gr-button:hover, button.gr-button-primary:hover {
542
  login_password = gr.Textbox(label="🔒 Password", type="password")
543
  login_btn = gr.Button("🚀 Sign In", variant="primary")
544
  login_status = gr.HTML("<div class='status-warning'>Please sign in.</div>")
545
-
546
  with gr.Tab("📝 New Registration"):
547
  signup_username = gr.Textbox(label="👤 Username")
548
  signup_email = gr.Textbox(label="📧 Email")
549
  signup_password = gr.Textbox(label="🔒 Password", type="password")
550
  signup_name = gr.Textbox(label="👨‍⚕️ Full Name")
551
  signup_role = gr.Radio(["practitioner", "organization"], label="Account Type", value="practitioner")
552
-
553
  with gr.Group(visible=False) as org_fields:
554
  org_name = gr.Textbox(label="Organization Name")
555
  phone = gr.Textbox(label="Phone")
556
  country_code = gr.Textbox(label="Country Code")
557
  department = gr.Textbox(label="Department")
558
  location = gr.Textbox(label="Location")
559
-
560
  with gr.Group(visible=True) as prac_fields:
561
  organization_dropdown = gr.Dropdown(choices=self.get_organizations_dropdown(), label="Select Organization")
562
-
563
  signup_btn = gr.Button("✨ Create Account", variant="primary")
564
  signup_status = gr.HTML()
565
-
566
  with gr.Column(visible=False) as practitioner_panel:
567
  user_info = gr.HTML("")
568
  logout_btn_prac = gr.Button("🚪 Logout", variant="secondary")
569
-
570
  with gr.Tabs():
571
  # ------------------- WOUND ANALYSIS -------------------
572
  with gr.Tab("🔬 Wound Analysis"):
@@ -587,17 +655,17 @@ button.gr-button:hover, button.gr-button-primary:hover {
587
  new_patient_name = gr.Textbox(label="Patient Name")
588
  new_patient_age = gr.Number(label="Age", value=30, minimum=0, maximum=120)
589
  new_patient_gender = gr.Dropdown(choices=["Male", "Female", "Other"], value="Male", label="Gender")
590
-
591
  gr.HTML("<h3>🩹 Wound Information</h3>")
592
  wound_location = gr.Textbox(label="Wound Location", placeholder="e.g., Left ankle")
593
  wound_duration = gr.Textbox(label="Wound Duration", placeholder="e.g., 2 weeks")
594
  pain_level = gr.Slider(0, 10, value=5, step=1, label="Pain Level (0-10)")
595
-
596
  gr.HTML("<h3>⚕️ Clinical Assessment</h3>")
597
  moisture_level = gr.Dropdown(["Dry", "Moist", "Wet", "Saturated"], value="Moist", label="Moisture Level")
598
  infection_signs = gr.Dropdown(["None", "Mild", "Moderate", "Severe"], value="None", label="Signs of Infection")
599
  diabetic_status = gr.Dropdown(["Non-diabetic", "Type 1", "Type 2", "Gestational"], value="Non-diabetic", label="Diabetic Status")
600
-
601
  with gr.Column(scale=1):
602
  gr.HTML("<h3>📸 Wound Image</h3>")
603
  wound_image = gr.Image(label="Upload Wound Image", type="filepath")
@@ -607,10 +675,10 @@ button.gr-button:hover, button.gr-button-primary:hover {
607
  medications = gr.Textbox(label="Current Medications", lines=2)
608
  allergies = gr.Textbox(label="Known Allergies", lines=2)
609
  additional_notes = gr.Textbox(label="Additional Notes", lines=3)
610
-
611
  analyze_btn = gr.Button("🔬 Analyze Wound", variant="primary", elem_id="analyze-btn")
612
  analysis_output = gr.HTML("")
613
-
614
  # ------------------- PATIENT HISTORY -------------------
615
  with gr.Tab("📋 Patient History"):
616
  with gr.Row():
@@ -621,33 +689,30 @@ button.gr-button:hover, button.gr-button-primary:hover {
621
  search_patient_name = gr.Textbox(label="Search patient by name")
622
  search_patient_btn = gr.Button("🔍 Search", variant="secondary")
623
  specific_patient_output = gr.HTML("")
624
-
625
  gr.HTML("<hr style='margin:10px 0 6px 0;border:none;border-top:1px solid #e2e8f0'>")
626
  with gr.Row():
627
  view_details_dd = gr.Dropdown(choices=[], label="Select patient to view details")
628
  view_details_btn = gr.Button("📈 View Details (Timeline)", variant="primary")
629
  view_details_output = gr.HTML("")
630
-
631
  with gr.Column(visible=False) as organization_panel:
632
  gr.HTML("<div class='status-warning'>Organization dashboard coming soon.</div>")
633
  logout_btn_org = gr.Button("🚪 Logout", variant="secondary")
634
-
635
  # ----------------------- handlers -----------------------
636
-
637
  def toggle_role_fields(role):
638
  return {
639
  org_fields: gr.update(visible=(role == "organization")),
640
  prac_fields: gr.update(visible=(role != "organization"))
641
  }
642
-
643
  def handle_signup(username, email, password, name, role, org_name_v, phone_v, cc_v, dept_v, loc_v, org_dropdown):
644
  try:
645
- # Resolve org_id for practitioner (based on dropdown selection)
646
  organization_id = None
647
  if role == "practitioner":
648
  organization_id = _resolve_org_id_from_dropdown(org_dropdown)
649
-
650
- # Create user using AuthManager's signature (it will handle org creation if role=='organization')
651
  ok = self.auth_manager.create_user(
652
  username=username,
653
  email=email,
@@ -661,13 +726,12 @@ button.gr-button:hover, button.gr-button-primary:hover {
661
  location=loc_v if role == "organization" else "",
662
  organization_id=organization_id
663
  )
664
-
665
  if ok:
666
  return "<div class='status-success'>✅ Account created. Please log in.</div>"
667
  return "<div class='status-error'>❌ Could not create account. Username/email may exist.</div>"
668
  except Exception as e:
669
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
670
-
671
  def handle_login(username, password):
672
  user = self.auth_manager.authenticate_user(username, password)
673
  if not user:
@@ -677,14 +741,13 @@ button.gr-button:hover, button.gr-button-primary:hover {
677
  self.current_user = user
678
  uid = int(user.get("id"))
679
  role = user.get("role")
680
-
681
- # Preload patient dropdowns for practitioners
682
  if role == "practitioner":
683
  _refresh_patient_dropdown(uid)
684
-
685
  info = f"<div class='status-success'>Welcome, <strong>{html.escape(user.get('name','User'))}</strong> — {html.escape(role)}</div>"
686
  updates = {login_status: info}
687
-
688
  if role == "practitioner":
689
  updates.update({
690
  auth_panel: gr.update(visible=False),
@@ -699,7 +762,7 @@ button.gr-button:hover, button.gr-button-primary:hover {
699
  organization_panel: gr.update(visible=True),
700
  })
701
  return updates
702
-
703
  def handle_logout():
704
  self.current_user = {}
705
  return {
@@ -707,141 +770,31 @@ button.gr-button:hover, button.gr-button-primary:hover {
707
  practitioner_panel: gr.update(visible=False),
708
  organization_panel: gr.update(visible=False)
709
  }
710
-
711
  def on_patient_mode_change(mode):
712
  return {
713
  new_patient_group: gr.update(visible=(mode == "New patient")),
714
  existing_patient_dd: gr.update(interactive=(mode == "Existing patient"))
715
  }
716
- @spaces.GPU
717
- def run_analysis(mode, existing_label,
718
- np_name, np_age, np_gender,
719
- w_loc, w_dur, pain, moist, infect, diabetic,
720
- prev_tx, med_hist, meds, alls, notes, img_path):
721
- try:
722
- if not img_path:
723
- return "<div class='status-error'>❌ Please upload a wound image.</div>"
724
-
725
- user_id = int(self.current_user.get("id", 0) or 0)
726
- if not user_id:
727
- return "<div class='status-error'>❌ Please login first.</div>"
728
-
729
- # Determine patient core fields (ensures same patient_id for existing)
730
- if mode == "Existing patient":
731
- pid = _label_to_id(existing_label)
732
- if not pid:
733
- return "<div class='status-warning'>⚠️ Select an existing patient.</div>"
734
- pcore = _fetch_patient_core(pid)
735
- patient_name_v = pcore.get("name")
736
- patient_age_v = pcore.get("age")
737
- patient_gender_v = pcore.get("gender")
738
- else:
739
- patient_name_v = np_name
740
- patient_age_v = np_age
741
- patient_gender_v = np_gender
742
-
743
- # Build questionnaire payload
744
- q_payload = {
745
- 'user_id': user_id,
746
- 'patient_name': patient_name_v,
747
- 'patient_age': patient_age_v,
748
- 'patient_gender': patient_gender_v,
749
- 'wound_location': w_loc,
750
- 'wound_duration': w_dur,
751
- 'pain_level': pain,
752
- 'moisture_level': moist,
753
- 'infection_signs': infect,
754
- 'diabetic_status': diabetic,
755
- 'previous_treatment': prev_tx,
756
- 'medical_history': med_hist,
757
- 'medications': meds,
758
- 'allergies': alls,
759
- 'additional_notes': notes
760
- }
761
-
762
- # Save questionnaire -> response_id
763
- response_id = self.database_manager.save_questionnaire(q_payload)
764
-
765
- # 🔒 Normalize in case an older/alternate version returns a dict
766
- if isinstance(response_id, dict):
767
- response_id = response_id.get("response_id") or response_id.get("id")
768
- try:
769
- response_id = int(response_id)
770
- except Exception:
771
- return "<div class='status-error'>❌ Could not resolve response ID.</div>"
772
-
773
- # Resolve patient_id from response (works for new or existing)
774
- patient_id = _response_to_patient_id(response_id)
775
- if not patient_id:
776
- return "<div class='status-error'>❌ Could not resolve patient ID.</div>"
777
-
778
- # Save wound image to DB
779
- try:
780
- with Image.open(img_path) as pil:
781
- pil = pil.convert("RGB")
782
- img_meta = self.database_manager.save_wound_image(patient_id, pil)
783
- image_db_id = img_meta["id"] if img_meta else None
784
- except Exception as e:
785
- logging.error(f"save_wound_image error: {e}")
786
- image_db_id = None
787
-
788
- # Prepare AI analyzer questionnaire dict
789
- q_for_ai = {
790
- 'age': patient_age_v,
791
- 'diabetic': 'Yes' if diabetic != 'Non-diabetic' else 'No',
792
- 'allergies': alls,
793
- 'date_of_injury': 'Unknown',
794
- 'professional_care': 'Yes',
795
- 'oozing_bleeding': 'Minor Oozing' if infect != 'None' else 'None',
796
- 'infection': 'Yes' if infect != 'None' else 'No',
797
- 'moisture': moist,
798
- 'patient_name': patient_name_v,
799
- 'patient_gender': patient_gender_v,
800
- 'wound_location': w_loc,
801
- 'wound_duration': w_dur,
802
- 'pain_level': pain,
803
- 'previous_treatment': prev_tx,
804
- 'medical_history': med_hist,
805
- 'medications': meds,
806
- 'additional_notes': notes
807
- }
808
-
809
- # Run AI
810
- analysis_result = self.wound_analyzer.analyze_wound(img_path, q_for_ai)
811
- if not analysis_result or not analysis_result.get("success"):
812
- err = (analysis_result or {}).get("error", "Unknown analysis error")
813
- return f"<div class='status-error'>❌ AI Analysis failed: {html.escape(str(err))}</div>"
814
-
815
- # Persist AI analysis (ties back to template via response->questionnaire_id)
816
- try:
817
- self.database_manager.save_analysis(response_id, image_db_id, analysis_result)
818
- except Exception as e:
819
- logging.error(f"save_analysis error: {e}")
820
-
821
- # If a new patient was created, refresh dropdowns
822
- if mode == "New patient":
823
- _refresh_patient_dropdown(user_id)
824
-
825
- # Render fancy results
826
- return self._format_comprehensive_analysis_results(
827
- analysis_result, img_path, q_for_ai
828
- )
829
- except Exception as e:
830
- logging.exception("run_analysis exception")
831
- return f"<div class='status-error'>❌ System error: {html.escape(str(e))}</div>"
832
-
833
  def load_history():
834
  try:
835
  uid = int(self.current_user.get("id", 0) or 0)
836
  if not uid:
837
  return "<div class='status-error'>❌ Please login first.</div>"
838
  rows = self.patient_history_manager.get_user_patient_history(uid) or []
839
- rows = _rows_with_inline_images(rows)
840
- return self.patient_history_manager.format_history_for_display(rows)
 
 
 
 
 
 
841
  except Exception as e:
842
  logging.error(f"load_history error: {e}")
843
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
844
-
845
  def do_search(name):
846
  try:
847
  uid = int(self.current_user.get("id", 0) or 0)
@@ -850,12 +803,17 @@ button.gr-button:hover, button.gr-button-primary:hover {
850
  if not (name or "").strip():
851
  return "<div class='status-warning'>⚠️ Enter a name to search.</div>"
852
  rows = self.patient_history_manager.search_patient_by_name(uid, name.strip()) or []
853
- rows = _rows_with_inline_images(rows)
854
- return self.patient_history_manager.format_patient_data_for_display(rows)
 
 
 
 
 
855
  except Exception as e:
856
  logging.error(f"search error: {e}")
857
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
858
-
859
  def view_details(existing_label):
860
  try:
861
  uid = int(self.current_user.get("id", 0) or 0)
@@ -865,45 +823,54 @@ button.gr-button:hover, button.gr-button-primary:hover {
865
  if not pid:
866
  return "<div class='status-warning'>⚠️ Select a patient.</div>"
867
  rows = self.patient_history_manager.get_wound_progression_by_id(uid, pid) or []
868
- rows = _rows_with_inline_images(rows)
869
- return self.patient_history_manager.format_patient_progress_for_display(rows)
 
 
 
 
 
870
  except Exception as e:
871
  logging.error(f"view_details error: {e}")
872
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
873
-
874
  # ----------------------- wiring -----------------------
875
-
876
  signup_role.change(
877
  toggle_role_fields,
878
  inputs=[signup_role],
879
  outputs=[org_fields, prac_fields]
880
  )
881
-
882
  signup_btn.click(
883
  handle_signup,
884
  inputs=[signup_username, signup_email, signup_password, signup_name, signup_role,
885
  org_name, phone, country_code, department, location, organization_dropdown],
886
  outputs=[signup_status]
887
  )
888
-
889
  login_btn.click(
890
  handle_login,
891
  inputs=[login_username, login_password],
892
  outputs=[login_status, auth_panel, practitioner_panel, organization_panel,
893
  user_info, existing_patient_dd, view_details_dd]
894
  )
895
-
896
  logout_btn_prac.click(handle_logout, outputs=[auth_panel, practitioner_panel, organization_panel])
897
  logout_btn_org.click(handle_logout, outputs=[auth_panel, practitioner_panel, organization_panel])
898
-
899
  patient_mode.change(
900
  on_patient_mode_change,
901
  inputs=[patient_mode],
902
  outputs=[new_patient_group, existing_patient_dd]
903
  )
904
-
 
905
  analyze_btn.click(
906
- run_analysis,
 
 
 
 
907
  inputs=[
908
  patient_mode, existing_patient_dd,
909
  new_patient_name, new_patient_age, new_patient_gender,
@@ -912,82 +879,69 @@ button.gr-button:hover, button.gr-button-primary:hover {
912
  ],
913
  outputs=[analysis_output]
914
  )
915
-
916
  history_btn.click(load_history, outputs=[patient_history_output])
917
  search_patient_btn.click(do_search, inputs=[search_patient_name], outputs=[specific_patient_output])
918
  view_details_btn.click(view_details, inputs=[view_details_dd], outputs=[view_details_output])
919
-
920
- return app
921
 
 
922
 
923
-
924
  def _format_comprehensive_analysis_results(self, analysis_result, image_url=None, questionnaire_data=None):
925
  """Format comprehensive analysis results with all visualization images from AIProcessor."""
926
  try:
927
- # Extract the core analysis results from AIProcessor
928
  success = analysis_result.get('success', False)
929
  if not success:
930
  error_msg = analysis_result.get('error', 'Unknown error')
931
  return f"<div class='status-error'>❌ Analysis failed: {error_msg}</div>"
932
-
933
  visual_analysis = analysis_result.get('visual_analysis', {})
934
  report = analysis_result.get('report', '')
935
  saved_image_path = analysis_result.get('saved_image_path', '')
936
-
937
- # Extract wound metrics
938
  wound_type = visual_analysis.get('wound_type', 'Unknown')
939
  length_cm = visual_analysis.get('length_cm', 0)
940
  breadth_cm = visual_analysis.get('breadth_cm', 0)
941
  area_cm2 = visual_analysis.get('surface_area_cm2', 0)
942
  detection_confidence = visual_analysis.get('detection_confidence', 0)
943
-
944
- # Get image paths for visualizations
945
  detection_image_path = visual_analysis.get('detection_image_path', '')
946
  segmentation_image_path = visual_analysis.get('segmentation_image_path', '')
947
  original_image_path = visual_analysis.get('original_image_path', '')
948
-
949
- # Convert images to base64 for embedding
950
  original_image_base64 = None
951
  detection_image_base64 = None
952
  segmentation_image_base64 = None
953
-
954
- # Original uploaded image
955
  if image_url and os.path.exists(image_url):
956
  original_image_base64 = self.image_to_base64(image_url)
957
  elif original_image_path and os.path.exists(original_image_path):
958
  original_image_base64 = self.image_to_base64(original_image_path)
959
  elif saved_image_path and os.path.exists(saved_image_path):
960
  original_image_base64 = self.image_to_base64(saved_image_path)
961
-
962
- # Detection visualization
963
  if detection_image_path and os.path.exists(detection_image_path):
964
  detection_image_base64 = self.image_to_base64(detection_image_path)
965
-
966
- # Segmentation visualization
967
  if segmentation_image_path and os.path.exists(segmentation_image_path):
968
  segmentation_image_base64 = self.image_to_base64(segmentation_image_path)
969
-
970
- # Generate risk assessment from questionnaire data
971
  risk_assessment = self._generate_risk_assessment(questionnaire_data)
972
  risk_level = risk_assessment['risk_level']
973
  risk_score = risk_assessment['risk_score']
974
  risk_factors = risk_assessment['risk_factors']
975
-
976
- # Set risk class for styling
977
  risk_class = "low"
978
  if risk_level.lower() == "moderate":
979
  risk_class = "moderate"
980
  elif risk_level.lower() in ["high", "very high"]:
981
  risk_class = "high"
982
-
983
- # Format risk factors
984
  risk_factors_html = "<ul>" + "".join(f"<li>{factor}</li>" for factor in risk_factors) + "</ul>" if risk_factors else "<p>No specific risk factors identified.</p>"
985
-
986
- # Create image gallery
987
  image_gallery_html = ""
988
  if original_image_base64 or detection_image_base64 or segmentation_image_base64:
989
  image_gallery_html = '<div class="image-gallery">'
990
-
991
  if original_image_base64:
992
  image_gallery_html += f'''
993
  <div class="image-item">
@@ -996,7 +950,6 @@ button.gr-button:hover, button.gr-button-primary:hover {
996
  <p>Uploaded image for analysis</p>
997
  </div>
998
  '''
999
-
1000
  if detection_image_base64:
1001
  image_gallery_html += f'''
1002
  <div class="image-item">
@@ -1005,7 +958,6 @@ button.gr-button:hover, button.gr-button-primary:hover {
1005
  <p>AI-detected wound boundaries with {detection_confidence:.1%} confidence</p>
1006
  </div>
1007
  '''
1008
-
1009
  if segmentation_image_base64:
1010
  image_gallery_html += f'''
1011
  <div class="image-item">
@@ -1014,43 +966,38 @@ button.gr-button:hover, button.gr-button-primary:hover {
1014
  <p>Detailed wound area measurement and analysis</p>
1015
  </div>
1016
  '''
1017
-
1018
  image_gallery_html += '</div>'
1019
-
1020
- # Convert markdown report to HTML
1021
- report_html = ""
1022
- if report:
1023
- report_html = self.markdown_to_html(report)
1024
-
1025
- # Final comprehensive HTML output
1026
  html_output = f"""
1027
  <div style="max-width: 1200px; margin: 0 auto; background: white; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); overflow: hidden;">
1028
  <div style="background: linear-gradient(135deg, #3182ce 0%, #2c5aa0 100%); color: white; padding: 40px; text-align: center;">
1029
  <h1 style="margin: 0; font-size: 32px; font-weight: 700;">🔬 SmartHeal AI Comprehensive Analysis</h1>
1030
  <p style="margin: 15px 0 0 0; opacity: 0.9; font-size: 18px;">Advanced Computer Vision & Medical AI Assessment</p>
1031
  <div style="background: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px; margin-top: 20px;">
1032
- <p style="margin: 0; font-size: 16px;"><strong>Patient:</strong> {questionnaire_data.get('patient_name', 'Unknown')} | <strong>Analysis Date:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
1033
  </div>
1034
  </div>
1035
-
1036
  <div style="padding: 40px;">
1037
  <div class="status-success" style="margin-bottom: 30px;">
1038
  <strong>✅ Analysis Status:</strong> Analysis completed successfully with comprehensive wound assessment
1039
  </div>
1040
-
1041
  <!-- Image Gallery Section -->
1042
  <div style="margin-bottom: 40px;">
1043
  <h2 style="color: #2d3748; font-size: 24px; margin-bottom: 20px; border-bottom: 2px solid #e53e3e; padding-bottom: 10px;">🖼️ Visual Analysis Gallery</h2>
1044
  {image_gallery_html}
1045
  </div>
1046
-
1047
  <!-- Wound Detection & Classification -->
1048
  <div style="background: #f8f9fa; padding: 30px; border-radius: 12px; margin-bottom: 30px;">
1049
  <h2 style="color: #2d3748; margin-top: 0;">🔍 Wound Detection & Classification</h2>
1050
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0;">
1051
  <div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
1052
  <h3 style="color: #3182ce; margin: 0 0 10px 0;">Wound Type</h3>
1053
- <p style="font-weight: 600; font-size: 18px; color: #2d3748; margin: 0;">{wound_type}</p>
1054
  </div>
1055
  <div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
1056
  <h3 style="color: #3182ce; margin: 0 0 10px 0;">Detection Confidence</h3>
@@ -1058,11 +1005,11 @@ button.gr-button:hover, button.gr-button-primary:hover {
1058
  </div>
1059
  <div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
1060
  <h3 style="color: #3182ce; margin: 0 0 10px 0;">Location</h3>
1061
- <p style="font-weight: 600; font-size: 18px; color: #2d3748; margin: 0;">{questionnaire_data.get('wound_location', 'Not specified')}</p>
1062
  </div>
1063
  </div>
1064
  </div>
1065
-
1066
  <!-- Wound Measurements -->
1067
  <div style="background: #e7f5ff; padding: 30px; border-radius: 12px; margin-bottom: 30px;">
1068
  <h2 style="color: #2d3748; margin-top: 0;">📏 Wound Measurements</h2>
@@ -1081,7 +1028,7 @@ button.gr-button:hover, button.gr-button-primary:hover {
1081
  </div>
1082
  </div>
1083
  </div>
1084
-
1085
  <!-- Risk Assessment -->
1086
  <div style="background: #fff4e6; padding: 30px; border-radius: 12px; margin-bottom: 30px;">
1087
  <h2 style="color: #2d3748; margin-top: 0;">⚠️ Risk Assessment</h2>
@@ -1107,26 +1054,26 @@ button.gr-button:hover, button.gr-button-primary:hover {
1107
  {risk_factors_html}
1108
  </div>
1109
  </div>
1110
-
1111
  <!-- Patient Information Summary -->
1112
  <div style="background: #f0f8f0; padding: 30px; border-radius: 12px; margin-bottom: 30px;">
1113
  <h2 style="color: #2d3748; margin-top: 0;">👤 Patient Information Summary</h2>
1114
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px;">
1115
- <div><strong>Age:</strong> {questionnaire_data.get('age', 'Not specified')} years</div>
1116
- <div><strong>Gender:</strong> {questionnaire_data.get('patient_gender', 'Not specified')}</div>
1117
- <div><strong>Diabetic Status:</strong> {questionnaire_data.get('diabetic', 'Unknown')}</div>
1118
- <div><strong>Pain Level:</strong> {questionnaire_data.get('pain_level', 'Not assessed')}/10</div>
1119
- <div><strong>Wound Duration:</strong> {questionnaire_data.get('wound_duration', 'Not specified')}</div>
1120
- <div><strong>Moisture Level:</strong> {questionnaire_data.get('moisture', 'Not assessed')}</div>
1121
  </div>
1122
- {f"<div style='margin-top: 20px;'><strong>Medical History:</strong> {questionnaire_data.get('medical_history', 'None provided')}</div>" if questionnaire_data.get('medical_history') else ""}
1123
- {f"<div style='margin-top: 10px;'><strong>Current Medications:</strong> {questionnaire_data.get('medications', 'None listed')}</div>" if questionnaire_data.get('medications') else ""}
1124
- {f"<div style='margin-top: 10px;'><strong>Known Allergies:</strong> {questionnaire_data.get('allergies', 'None listed')}</div>" if questionnaire_data.get('allergies') else ""}
1125
  </div>
1126
-
1127
  <!-- AI Generated Report -->
1128
  {f'<div style="background: #f8f9fa; padding: 30px; border-radius: 12px; margin-bottom: 30px;"><h2 style="color: #2d3748; margin-top: 0;">🤖 AI-Generated Clinical Report</h2><div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">{report_html}</div></div>' if report_html else ''}
1129
-
1130
  <!-- Important Disclaimers -->
1131
  <div style="background: #fff5f5; border: 2px solid #feb2b2; padding: 25px; border-radius: 12px; margin: 30px 0;">
1132
  <h3 style="color: #c53030; margin-top: 0;">⚠️ Important Medical Disclaimers</h3>
@@ -1137,7 +1084,7 @@ button.gr-button:hover, button.gr-button-primary:hover {
1137
  <li><strong>Risk Assessment Limitations:</strong> Risk factors are based on provided information and may not reflect the complete clinical picture.</li>
1138
  </ul>
1139
  </div>
1140
-
1141
  <!-- Footer -->
1142
  <div style="text-align: center; padding: 30px 0; border-top: 2px solid #e2e8f0; margin-top: 30px;">
1143
  <p style="color: #6c757d; font-style: italic; font-size: 16px; margin: 0;">
@@ -1148,77 +1095,74 @@ button.gr-button:hover, button.gr-button-primary:hover {
1148
  </div>
1149
  </div>
1150
  """
1151
-
1152
  return html_output
1153
 
1154
  except Exception as e:
1155
  logging.error(f"Error formatting comprehensive results: {e}")
1156
  return f"<div class='status-error'>❌ Error displaying results: {str(e)}</div>"
1157
-
1158
  def _generate_risk_assessment(self, questionnaire_data):
1159
  """Generate risk assessment based on questionnaire data"""
1160
  if not questionnaire_data:
1161
  return {'risk_level': 'Unknown', 'risk_score': 0, 'risk_factors': []}
1162
-
1163
  risk_factors = []
1164
  risk_score = 0
1165
-
1166
  try:
1167
- # Age assessment
1168
  age = questionnaire_data.get('age', 0)
1169
  if isinstance(age, str):
1170
  try:
1171
  age = int(age)
1172
  except ValueError:
1173
  age = 0
1174
-
1175
  if age > 65:
1176
  risk_factors.append("Advanced age (>65 years)")
1177
  risk_score += 2
1178
  elif age > 50:
1179
  risk_factors.append("Older adult (50-65 years)")
1180
  risk_score += 1
1181
-
1182
- # Diabetic status
1183
  diabetic_status = str(questionnaire_data.get('diabetic', '')).lower()
1184
  if 'yes' in diabetic_status:
1185
  risk_factors.append("Diabetes mellitus")
1186
  risk_score += 3
1187
-
1188
- # Infection signs
1189
  infection = str(questionnaire_data.get('infection', '')).lower()
1190
  if 'yes' in infection:
1191
  risk_factors.append("Signs of infection present")
1192
  risk_score += 3
1193
-
1194
- # Pain level
1195
  pain_level = questionnaire_data.get('pain_level', 0)
1196
  if isinstance(pain_level, str):
1197
  try:
1198
  pain_level = float(pain_level)
1199
  except ValueError:
1200
  pain_level = 0
1201
-
1202
  if pain_level >= 7:
1203
  risk_factors.append("High pain level (≥7/10)")
1204
  risk_score += 2
1205
  elif pain_level >= 5:
1206
  risk_factors.append("Moderate pain level (5-6/10)")
1207
  risk_score += 1
1208
-
1209
- # Wound duration
1210
  duration = str(questionnaire_data.get('wound_duration', '')).lower()
1211
  if any(term in duration for term in ['month', 'months', 'year', 'years']):
1212
  risk_factors.append("Chronic wound (>4 weeks)")
1213
  risk_score += 3
1214
-
1215
- # Moisture level
1216
  moisture = str(questionnaire_data.get('moisture', '')).lower()
1217
  if any(term in moisture for term in ['wet', 'saturated']):
1218
  risk_factors.append("Excessive wound exudate")
1219
  risk_score += 1
1220
-
1221
- # Medical history analysis
1222
  medical_history = str(questionnaire_data.get('medical_history', '')).lower()
1223
  if any(term in medical_history for term in ['vascular', 'circulation', 'heart']):
1224
  risk_factors.append("Cardiovascular disease")
@@ -1229,8 +1173,8 @@ button.gr-button:hover, button.gr-button-primary:hover {
1229
  if any(term in medical_history for term in ['smoking', 'tobacco']):
1230
  risk_factors.append("Smoking history")
1231
  risk_score += 2
1232
-
1233
- # Determine risk level
1234
  if risk_score >= 8:
1235
  risk_level = "Very High"
1236
  elif risk_score >= 6:
@@ -1239,13 +1183,13 @@ button.gr-button:hover, button.gr-button-primary:hover {
1239
  risk_level = "Moderate"
1240
  else:
1241
  risk_level = "Low"
1242
-
1243
  return {
1244
  'risk_score': risk_score,
1245
  'risk_level': risk_level,
1246
  'risk_factors': risk_factors
1247
  }
1248
-
1249
  except Exception as e:
1250
  logging.error(f"Risk assessment error: {e}")
1251
  return {
 
1
+ # src/ui_components_original.py
2
+
3
  import gradio as gr
4
  import os
5
  import re
 
8
  from datetime import datetime
9
  from PIL import Image
10
  import html
11
+ from typing import Optional, Dict, Any
12
+
13
+ # ---- Safe imports for local vs package execution ----
14
+ try:
15
+ from .patient_history import PatientHistoryManager, ReportGenerator
16
+ except Exception:
17
+ from patient_history import PatientHistoryManager, ReportGenerator # local dev
18
+
19
+ # ---- Optional spaces.GPU fallback (local dev) ----
20
+ try:
21
+ import spaces
22
+ def _SPACES_GPU(*args, **kwargs):
23
+ return spaces.GPU(*args, **kwargs)
24
+ except Exception:
25
+ def _SPACES_GPU(*_args, **_kwargs):
26
+ def deco(f):
27
+ return f
28
+ return deco
29
+
30
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
31
+
32
+
33
+ def pil_to_base64(pil_image: Image.Image) -> Optional[str]:
34
  """Convert PIL Image to base64 data URL"""
35
  import io
 
 
 
36
  if pil_image is None:
37
  return None
 
38
  try:
 
39
  if pil_image.mode != 'RGB':
40
  pil_image = pil_image.convert('RGB')
 
41
  buffer = io.BytesIO()
42
  pil_image.save(buffer, format='PNG')
43
  img_str = base64.b64encode(buffer.getvalue()).decode()
 
46
  logging.error(f"Error converting PIL image to base64: {e}")
47
  return None
48
 
49
+
50
+ # =============================================================================
51
+ # GPU-DECORATED FUNCTION (STANDALONE)
52
+ # =============================================================================
53
+ @_SPACES_GPU(enable_queue=True)
54
+ def standalone_run_analysis(
55
+ # instance/context
56
+ ui_instance,
57
+ current_user: Dict[str, Any],
58
+ database_manager,
59
+ wound_analyzer,
60
+ # UI inputs
61
+ mode, existing_label,
62
+ np_name, np_age, np_gender,
63
+ w_loc, w_dur, pain, moist, infect, diabetic,
64
+ prev_tx, med_hist, meds, alls, notes, img_path
65
+ ):
66
+ """Runs in the ZeroGPU worker; returns HTML for the UI."""
67
+ def _label_to_id(label: str):
68
+ if not label:
69
+ return None
70
+ try:
71
+ return int(str(label).split("•", 1)[0].strip())
72
+ except Exception:
73
+ return None
74
+
75
+ def _fetch_patient_core(pid: int):
76
+ row = database_manager.execute_query_one(
77
+ "SELECT id, name, age, gender FROM patients WHERE id=%s LIMIT 1", (pid,)
78
+ )
79
+ return row or {}
80
+
81
+ def _response_to_patient_id(resp_ref):
82
+ if isinstance(resp_ref, dict):
83
+ pid = resp_ref.get("patient_id")
84
+ if pid is not None:
85
+ try:
86
+ return int(pid)
87
+ except Exception:
88
+ pass
89
+ resp_id = resp_ref.get("response_id") or resp_ref.get("id")
90
+ else:
91
+ resp_id = resp_ref
92
+ if not resp_id:
93
+ return None
94
+ row = database_manager.execute_query_one(
95
+ "SELECT patient_id FROM questionnaire_responses WHERE id=%s LIMIT 1",
96
+ (int(resp_id),)
97
+ )
98
+ try:
99
+ return int(row["patient_id"]) if row and "patient_id" in row else None
100
+ except Exception:
101
+ return None
102
+
103
+ try:
104
+ if not img_path:
105
+ return "<div class='status-error'>❌ Please upload a wound image.</div>"
106
+
107
+ user_id = int(current_user.get("id", 0) or 0)
108
+ if not user_id:
109
+ return "<div class='status-error'>❌ Please login first.</div>"
110
+
111
+ # Resolve patient
112
+ if mode == "Existing patient":
113
+ pid = _label_to_id(existing_label)
114
+ if not pid:
115
+ return "<div class='status-warning'>⚠️ Select an existing patient.</div>"
116
+ pcore = _fetch_patient_core(pid)
117
+ patient_name_v = pcore.get("name")
118
+ patient_age_v = pcore.get("age")
119
+ patient_gender_v = pcore.get("gender")
120
+ else:
121
+ patient_name_v = np_name
122
+ patient_age_v = np_age
123
+ patient_gender_v = np_gender
124
+
125
+ # Save questionnaire
126
+ q_payload = {
127
+ 'user_id': user_id,
128
+ 'patient_name': patient_name_v,
129
+ 'patient_age': patient_age_v,
130
+ 'patient_gender': patient_gender_v,
131
+ 'wound_location': w_loc,
132
+ 'wound_duration': w_dur,
133
+ 'pain_level': pain,
134
+ 'moisture_level': moist,
135
+ 'infection_signs': infect,
136
+ 'diabetic_status': diabetic,
137
+ 'previous_treatment': prev_tx,
138
+ 'medical_history': med_hist,
139
+ 'medications': meds,
140
+ 'allergies': alls,
141
+ 'additional_notes': notes
142
+ }
143
+ response_id = database_manager.save_questionnaire(q_payload)
144
+ # normalize
145
+ response_id = (response_id.get("response_id") if isinstance(response_id, dict) else response_id)
146
+ try:
147
+ response_id = int(response_id)
148
+ except Exception:
149
+ return "<div class='status-error'>❌ Could not resolve response ID.</div>"
150
+
151
+ patient_id = _response_to_patient_id(response_id)
152
+ if not patient_id:
153
+ return "<div class='status-error'>❌ Could not resolve patient ID.</div>"
154
+
155
+ # Save wound image binary
156
+ try:
157
+ with Image.open(img_path) as pil:
158
+ pil = pil.convert("RGB")
159
+ img_meta = database_manager.save_wound_image(patient_id, pil)
160
+ image_db_id = img_meta["id"] if img_meta else None
161
+ except Exception as e:
162
+ logging.error(f"save_wound_image error: {e}")
163
+ image_db_id = None
164
+
165
+ # Prepare AI inputs
166
+ q_for_ai = {
167
+ 'age': patient_age_v,
168
+ 'diabetic': 'Yes' if diabetic != 'Non-diabetic' else 'No',
169
+ 'allergies': alls,
170
+ 'date_of_injury': 'Unknown',
171
+ 'professional_care': 'Yes',
172
+ 'oozing_bleeding': 'Minor Oozing' if infect != 'None' else 'None',
173
+ 'infection': 'Yes' if infect != 'None' else 'No',
174
+ 'moisture': moist,
175
+ 'patient_name': patient_name_v,
176
+ 'patient_gender': patient_gender_v,
177
+ 'wound_location': w_loc,
178
+ 'wound_duration': w_dur,
179
+ 'pain_level': pain,
180
+ 'previous_treatment': prev_tx,
181
+ 'medical_history': med_hist,
182
+ 'medications': meds,
183
+ 'additional_notes': notes
184
+ }
185
+
186
+ # Run AI
187
+ analysis_result = wound_analyzer.analyze_wound(img_path, q_for_ai)
188
+ if not analysis_result or not analysis_result.get("success"):
189
+ err = (analysis_result or {}).get("error", "Unknown analysis error")
190
+ return f"<div class='status-error'>❌ AI Analysis failed: {html.escape(str(err))}</div>"
191
+
192
+ # Persist AI analysis
193
+ try:
194
+ database_manager.save_analysis(response_id, image_db_id, analysis_result)
195
+ except Exception as e:
196
+ logging.error(f"save_analysis error: {e}")
197
+
198
+ # Format via instance method to keep UI consistent
199
+ return ui_instance._format_comprehensive_analysis_results(
200
+ analysis_result, img_path, q_for_ai
201
+ )
202
+ except Exception as e:
203
+ logging.exception("standalone_run_analysis exception")
204
+ return f"<div class='status-error'>❌ System error in GPU worker: {html.escape(str(e))}</div>"
205
+
206
+
207
+ # =============================================================================
208
+ # UI CLASS DEFINITION
209
+ # =============================================================================
210
  class UIComponents:
211
  def __init__(self, auth_manager, database_manager, wound_analyzer):
212
  self.auth_manager = auth_manager
 
215
  self.current_user = {}
216
  self.patient_history_manager = PatientHistoryManager(database_manager)
217
  self.report_generator = ReportGenerator()
218
+
219
  # Ensure uploads directory exists
220
  if not os.path.exists("uploads"):
221
  os.makedirs("uploads", exist_ok=True)
 
224
  """Convert image to base64 data URL for embedding in HTML"""
225
  if not image_path or not os.path.exists(image_path):
226
  return None
 
227
  try:
228
  with open(image_path, "rb") as image_file:
229
  encoded_string = base64.b64encode(image_file.read()).decode()
230
+
 
231
  image_ext = os.path.splitext(image_path)[1].lower()
232
  if image_ext in [".jpg", ".jpeg"]:
233
  mime_type = "image/jpeg"
 
236
  elif image_ext == ".gif":
237
  mime_type = "image/gif"
238
  else:
239
+ mime_type = "image/png"
240
+
241
  return f"data:{mime_type};base64,{encoded_string}"
242
  except Exception as e:
243
  logging.error(f"Error converting image to base64: {e}")
 
247
  """Convert markdown text to proper HTML format with enhanced support"""
248
  if not markdown_text:
249
  return ""
250
+
251
+ # Escape HTML entities
252
  html_text = html.escape(markdown_text)
253
 
254
+ # Headers
255
  html_text = re.sub(r"^### (.*?)$", r"<h3>\1</h3>", html_text, flags=re.MULTILINE)
256
  html_text = re.sub(r"^## (.*?)$", r"<h2>\1</h2>", html_text, flags=re.MULTILINE)
257
  html_text = re.sub(r"^# (.*?)$", r"<h1>\1</h1>", html_text, flags=re.MULTILINE)
258
+
259
+ # Bold, italic
260
  html_text = re.sub(r"\*\*(.*?)\*\*", r"<strong>\1</strong>", html_text)
 
 
261
  html_text = re.sub(r"\*(.*?)\*", r"<em>\1</em>", html_text)
262
 
263
+ # Code blocks
264
  html_text = re.sub(r"```(.*?)```", r"<pre><code>\1</code></pre>", html_text, flags=re.DOTALL)
265
+ # Inline code
266
  html_text = re.sub(r"`(.*?)`", r"<code>\1</code>", html_text)
267
 
268
+ # Blockquotes
269
  html_text = re.sub(r"^> (.*?)$", r"<blockquote>\1</blockquote>", html_text, flags=re.MULTILINE)
270
 
271
+ # Links
272
  html_text = re.sub(r"\[(.*?)\]\((.*?)\)", r"<a href=\"\2\">\1</a>", html_text)
273
 
274
+ # Horizontal rules
275
  html_text = re.sub(r"^\s*[-*_]{3,}\s*$", r"<hr>", html_text, flags=re.MULTILINE)
276
 
277
+ # Bullet points to <ul>
278
  lines = html_text.split("\n")
279
  in_list = False
280
  result_lines = []
 
281
  for line in lines:
282
  stripped = line.strip()
283
  if stripped.startswith("- "):
 
293
  result_lines.append(f"<p>{stripped}</p>")
294
  else:
295
  result_lines.append("<br>")
 
296
  if in_list:
297
  result_lines.append("</ul>")
 
298
  return "\n".join(result_lines)
299
 
300
  def get_organizations_dropdown(self):
 
461
  backdrop-filter: blur(10px) !important;
462
  }
463
 
464
+ /* Image gallery styling */
465
  .image-gallery {
466
  display: grid;
467
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
468
  gap: 20px;
469
  margin: 20px 0;
470
  }
471
+ .image-item { background: #f8f9fa; border-radius: 12px; padding: 15px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); text-align: center; }
472
+ .image-item img { max-width: 100%; height: auto; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
473
+ .image-item h4 { margin: 15px 0 5px 0; color: #2d3748; font-weight: 600; }
474
+ .image-item p { margin: 0; color: #666; font-size: 0.9em; }
475
 
476
+ /* Analyze button */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  #analyze-btn {
478
  background: linear-gradient(135deg, #1B5CF3 0%, #1E3A8A 100%) !important;
479
  color: #FFFFFF !important;
 
486
  text-align: center !important;
487
  transition: all 0.2s ease-in-out !important;
488
  }
 
489
  #analyze-btn:hover {
490
  background: linear-gradient(135deg, #174ea6 0%, #123b82 100%) !important;
491
  box-shadow: 0 4px 14px rgba(27, 95, 193, 0.4) !important;
492
  transform: translateY(-2px) !important;
493
  }
494
 
495
+ /* Responsive */
496
  @media (max-width: 768px) {
497
+ .medical-header { padding: 16px !important; text-align: center !important; }
498
+ .medical-header h1 { font-size: 2rem !important; }
499
+ .logo { width: 48px !important; height: 48px !important; margin-right: 16px !important; }
500
+ .gr-form { padding: 16px !important; margin: 8px 0 !important; }
501
+ .image-gallery { grid-template-columns: 1fr; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
  }
503
  """
504
 
 
507
  SmartHeal UI – aligned with current DB + history manager:
508
  • Login (practitioner / organization)
509
  • Practitioner: Wound Analysis (existing vs new patient), Patient History, View Details
 
510
  """
511
  import gradio as gr
512
  from PIL import Image
513
  import os, html, logging
514
+
515
  # ----------------------- helpers (inner) -----------------------
516
+ self._patient_choices = [] # list[str] labels in dropdown
517
+ self._patient_map = {} # label -> patient_id
518
+
 
519
  def _to_data_url_if_local(path_or_url: str) -> str:
520
  if not path_or_url:
521
  return ""
522
  try:
523
  if os.path.exists(path_or_url):
524
  return self.image_to_base64(path_or_url) or ""
525
+ return path_or_url
526
  except Exception:
527
  return ""
528
+
529
  def _refresh_patient_dropdown(user_id: int):
530
  """Query patient's list and prepare dropdown choices."""
531
  self._patient_choices.clear()
532
  self._patient_map.clear()
533
  try:
534
  rows = self.patient_history_manager.get_patient_list(user_id) or []
 
535
  for r in rows:
536
  pid = int(r.get("id") or 0)
537
  nm = r.get("patient_name") or "Unknown"
 
543
  self._patient_map[label] = pid
544
  except Exception as e:
545
  logging.error(f"refresh dropdown error: {e}")
546
+
547
  def _label_to_id(label: str):
548
+ if not label:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  return None
 
 
 
 
 
550
  try:
551
+ return int(str(label).split("", 1)[0].strip())
552
  except Exception:
553
  return None
554
+
 
 
 
 
 
 
 
 
 
 
555
  def _resolve_org_id_from_dropdown(label: str) -> Optional[int]:
556
  """
557
  Dropdown items look like: 'Org Name - Location'.
 
569
  if row and "id" in row:
570
  return int(row["id"])
571
  else:
 
572
  row = self.database_manager.execute_query_one(
573
  "SELECT id FROM organizations WHERE name=%s ORDER BY id DESC LIMIT 1",
574
  (label.strip(),)
 
578
  except Exception as e:
579
  logging.error(f"resolve org id error: {e}")
580
  return None
581
+
582
  # ----------------------- Blocks UI -----------------------
 
583
  with gr.Blocks(css=self.get_custom_css(), title="SmartHeal - AI Wound Care Assistant") as app:
584
  # Header
585
  logo_url = "https://scontent.fccu31-2.fna.fbcdn.net/v/t39.30808-6/275933824_102121829111657_3325198727201325354_n.jpg?_nc_cat=104&ccb=1-7&_nc_sid=6ee11a&_nc_ohc=UIBKBXaPiSsQ7kNvwHy41Wj&_nc_oc=Adm8WwTOq--itjR7UgI7mUy57nDeZQ8zZSh4YxQ6F0iq8gmoUxtQ4-nZV7vTAWlYJxY&_nc_zt=23&_nc_ht=scontent.fccu31-2.fna&_nc_gid=1Em7m4EFZJCDDExF5Mmlfg&oh=00_AfXodrOfPdY61Bes8PY2s9o0Z2kXAVdmkk0Yd4dNOo8mgw&oe=68A2358B"
 
592
  </div>
593
  </div>
594
  """)
595
+
596
  # Disclaimer
597
  gr.HTML("""
598
  <div style="border:2px solid #FF6B6B;background:#FFE5E5;padding:15px;border-radius:12px;margin:10px 0;">
 
600
  <p><strong>This system is for testing/education and not a substitute for clinical judgment.</strong></p>
601
  </div>
602
  """)
603
+
604
  # Panels: auth vs practitioner vs organization
605
  with gr.Row():
606
  with gr.Column(visible=True) as auth_panel:
 
610
  login_password = gr.Textbox(label="🔒 Password", type="password")
611
  login_btn = gr.Button("🚀 Sign In", variant="primary")
612
  login_status = gr.HTML("<div class='status-warning'>Please sign in.</div>")
613
+
614
  with gr.Tab("📝 New Registration"):
615
  signup_username = gr.Textbox(label="👤 Username")
616
  signup_email = gr.Textbox(label="📧 Email")
617
  signup_password = gr.Textbox(label="🔒 Password", type="password")
618
  signup_name = gr.Textbox(label="👨‍⚕️ Full Name")
619
  signup_role = gr.Radio(["practitioner", "organization"], label="Account Type", value="practitioner")
620
+
621
  with gr.Group(visible=False) as org_fields:
622
  org_name = gr.Textbox(label="Organization Name")
623
  phone = gr.Textbox(label="Phone")
624
  country_code = gr.Textbox(label="Country Code")
625
  department = gr.Textbox(label="Department")
626
  location = gr.Textbox(label="Location")
627
+
628
  with gr.Group(visible=True) as prac_fields:
629
  organization_dropdown = gr.Dropdown(choices=self.get_organizations_dropdown(), label="Select Organization")
630
+
631
  signup_btn = gr.Button("✨ Create Account", variant="primary")
632
  signup_status = gr.HTML()
633
+
634
  with gr.Column(visible=False) as practitioner_panel:
635
  user_info = gr.HTML("")
636
  logout_btn_prac = gr.Button("🚪 Logout", variant="secondary")
637
+
638
  with gr.Tabs():
639
  # ------------------- WOUND ANALYSIS -------------------
640
  with gr.Tab("🔬 Wound Analysis"):
 
655
  new_patient_name = gr.Textbox(label="Patient Name")
656
  new_patient_age = gr.Number(label="Age", value=30, minimum=0, maximum=120)
657
  new_patient_gender = gr.Dropdown(choices=["Male", "Female", "Other"], value="Male", label="Gender")
658
+
659
  gr.HTML("<h3>🩹 Wound Information</h3>")
660
  wound_location = gr.Textbox(label="Wound Location", placeholder="e.g., Left ankle")
661
  wound_duration = gr.Textbox(label="Wound Duration", placeholder="e.g., 2 weeks")
662
  pain_level = gr.Slider(0, 10, value=5, step=1, label="Pain Level (0-10)")
663
+
664
  gr.HTML("<h3>⚕️ Clinical Assessment</h3>")
665
  moisture_level = gr.Dropdown(["Dry", "Moist", "Wet", "Saturated"], value="Moist", label="Moisture Level")
666
  infection_signs = gr.Dropdown(["None", "Mild", "Moderate", "Severe"], value="None", label="Signs of Infection")
667
  diabetic_status = gr.Dropdown(["Non-diabetic", "Type 1", "Type 2", "Gestational"], value="Non-diabetic", label="Diabetic Status")
668
+
669
  with gr.Column(scale=1):
670
  gr.HTML("<h3>📸 Wound Image</h3>")
671
  wound_image = gr.Image(label="Upload Wound Image", type="filepath")
 
675
  medications = gr.Textbox(label="Current Medications", lines=2)
676
  allergies = gr.Textbox(label="Known Allergies", lines=2)
677
  additional_notes = gr.Textbox(label="Additional Notes", lines=3)
678
+
679
  analyze_btn = gr.Button("🔬 Analyze Wound", variant="primary", elem_id="analyze-btn")
680
  analysis_output = gr.HTML("")
681
+
682
  # ------------------- PATIENT HISTORY -------------------
683
  with gr.Tab("📋 Patient History"):
684
  with gr.Row():
 
689
  search_patient_name = gr.Textbox(label="Search patient by name")
690
  search_patient_btn = gr.Button("🔍 Search", variant="secondary")
691
  specific_patient_output = gr.HTML("")
692
+
693
  gr.HTML("<hr style='margin:10px 0 6px 0;border:none;border-top:1px solid #e2e8f0'>")
694
  with gr.Row():
695
  view_details_dd = gr.Dropdown(choices=[], label="Select patient to view details")
696
  view_details_btn = gr.Button("📈 View Details (Timeline)", variant="primary")
697
  view_details_output = gr.HTML("")
698
+
699
  with gr.Column(visible=False) as organization_panel:
700
  gr.HTML("<div class='status-warning'>Organization dashboard coming soon.</div>")
701
  logout_btn_org = gr.Button("🚪 Logout", variant="secondary")
702
+
703
  # ----------------------- handlers -----------------------
 
704
  def toggle_role_fields(role):
705
  return {
706
  org_fields: gr.update(visible=(role == "organization")),
707
  prac_fields: gr.update(visible=(role != "organization"))
708
  }
709
+
710
  def handle_signup(username, email, password, name, role, org_name_v, phone_v, cc_v, dept_v, loc_v, org_dropdown):
711
  try:
 
712
  organization_id = None
713
  if role == "practitioner":
714
  organization_id = _resolve_org_id_from_dropdown(org_dropdown)
715
+
 
716
  ok = self.auth_manager.create_user(
717
  username=username,
718
  email=email,
 
726
  location=loc_v if role == "organization" else "",
727
  organization_id=organization_id
728
  )
 
729
  if ok:
730
  return "<div class='status-success'>✅ Account created. Please log in.</div>"
731
  return "<div class='status-error'>❌ Could not create account. Username/email may exist.</div>"
732
  except Exception as e:
733
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
734
+
735
  def handle_login(username, password):
736
  user = self.auth_manager.authenticate_user(username, password)
737
  if not user:
 
741
  self.current_user = user
742
  uid = int(user.get("id"))
743
  role = user.get("role")
744
+
 
745
  if role == "practitioner":
746
  _refresh_patient_dropdown(uid)
747
+
748
  info = f"<div class='status-success'>Welcome, <strong>{html.escape(user.get('name','User'))}</strong> — {html.escape(role)}</div>"
749
  updates = {login_status: info}
750
+
751
  if role == "practitioner":
752
  updates.update({
753
  auth_panel: gr.update(visible=False),
 
762
  organization_panel: gr.update(visible=True),
763
  })
764
  return updates
765
+
766
  def handle_logout():
767
  self.current_user = {}
768
  return {
 
770
  practitioner_panel: gr.update(visible=False),
771
  organization_panel: gr.update(visible=False)
772
  }
773
+
774
  def on_patient_mode_change(mode):
775
  return {
776
  new_patient_group: gr.update(visible=(mode == "New patient")),
777
  existing_patient_dd: gr.update(interactive=(mode == "Existing patient"))
778
  }
779
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
780
  def load_history():
781
  try:
782
  uid = int(self.current_user.get("id", 0) or 0)
783
  if not uid:
784
  return "<div class='status-error'>❌ Please login first.</div>"
785
  rows = self.patient_history_manager.get_user_patient_history(uid) or []
786
+ # inline images
787
+ out = []
788
+ for r in rows or []:
789
+ r = dict(r)
790
+ if r.get("image_url"):
791
+ r["image_url"] = _to_data_url_if_local(r["image_url"])
792
+ out.append(r)
793
+ return self.patient_history_manager.format_history_for_display(out)
794
  except Exception as e:
795
  logging.error(f"load_history error: {e}")
796
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
797
+
798
  def do_search(name):
799
  try:
800
  uid = int(self.current_user.get("id", 0) or 0)
 
803
  if not (name or "").strip():
804
  return "<div class='status-warning'>⚠️ Enter a name to search.</div>"
805
  rows = self.patient_history_manager.search_patient_by_name(uid, name.strip()) or []
806
+ out = []
807
+ for r in rows or []:
808
+ r = dict(r)
809
+ if r.get("image_url"):
810
+ r["image_url"] = _to_data_url_if_local(r["image_url"])
811
+ out.append(r)
812
+ return self.patient_history_manager.format_patient_data_for_display(out)
813
  except Exception as e:
814
  logging.error(f"search error: {e}")
815
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
816
+
817
  def view_details(existing_label):
818
  try:
819
  uid = int(self.current_user.get("id", 0) or 0)
 
823
  if not pid:
824
  return "<div class='status-warning'>⚠️ Select a patient.</div>"
825
  rows = self.patient_history_manager.get_wound_progression_by_id(uid, pid) or []
826
+ out = []
827
+ for r in rows or []:
828
+ r = dict(r)
829
+ if r.get("image_url"):
830
+ r["image_url"] = _to_data_url_if_local(r["image_url"])
831
+ out.append(r)
832
+ return self.patient_history_manager.format_patient_progress_for_display(out)
833
  except Exception as e:
834
  logging.error(f"view_details error: {e}")
835
  return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
836
+
837
  # ----------------------- wiring -----------------------
 
838
  signup_role.change(
839
  toggle_role_fields,
840
  inputs=[signup_role],
841
  outputs=[org_fields, prac_fields]
842
  )
843
+
844
  signup_btn.click(
845
  handle_signup,
846
  inputs=[signup_username, signup_email, signup_password, signup_name, signup_role,
847
  org_name, phone, country_code, department, location, organization_dropdown],
848
  outputs=[signup_status]
849
  )
850
+
851
  login_btn.click(
852
  handle_login,
853
  inputs=[login_username, login_password],
854
  outputs=[login_status, auth_panel, practitioner_panel, organization_panel,
855
  user_info, existing_patient_dd, view_details_dd]
856
  )
857
+
858
  logout_btn_prac.click(handle_logout, outputs=[auth_panel, practitioner_panel, organization_panel])
859
  logout_btn_org.click(handle_logout, outputs=[auth_panel, practitioner_panel, organization_panel])
860
+
861
  patient_mode.change(
862
  on_patient_mode_change,
863
  inputs=[patient_mode],
864
  outputs=[new_patient_group, existing_patient_dd]
865
  )
866
+
867
+ # --- IMPORTANT: call standalone GPU function via lambda to pass instance/ctx ---
868
  analyze_btn.click(
869
+ fn=lambda mode, ex_lbl, np_n, np_a, np_g, wl, wd, p, m, i, d, pt, mh, med, al, nt, img: \
870
+ standalone_run_analysis(
871
+ self, self.current_user, self.database_manager, self.wound_analyzer,
872
+ mode, ex_lbl, np_n, np_a, np_g, wl, wd, p, m, i, d, pt, mh, med, al, nt, img
873
+ ),
874
  inputs=[
875
  patient_mode, existing_patient_dd,
876
  new_patient_name, new_patient_age, new_patient_gender,
 
879
  ],
880
  outputs=[analysis_output]
881
  )
882
+
883
  history_btn.click(load_history, outputs=[patient_history_output])
884
  search_patient_btn.click(do_search, inputs=[search_patient_name], outputs=[specific_patient_output])
885
  view_details_btn.click(view_details, inputs=[view_details_dd], outputs=[view_details_output])
 
 
886
 
887
+ return app
888
 
889
+ # ----------------------- formatting & risk logic -----------------------
890
  def _format_comprehensive_analysis_results(self, analysis_result, image_url=None, questionnaire_data=None):
891
  """Format comprehensive analysis results with all visualization images from AIProcessor."""
892
  try:
 
893
  success = analysis_result.get('success', False)
894
  if not success:
895
  error_msg = analysis_result.get('error', 'Unknown error')
896
  return f"<div class='status-error'>❌ Analysis failed: {error_msg}</div>"
897
+
898
  visual_analysis = analysis_result.get('visual_analysis', {})
899
  report = analysis_result.get('report', '')
900
  saved_image_path = analysis_result.get('saved_image_path', '')
901
+
 
902
  wound_type = visual_analysis.get('wound_type', 'Unknown')
903
  length_cm = visual_analysis.get('length_cm', 0)
904
  breadth_cm = visual_analysis.get('breadth_cm', 0)
905
  area_cm2 = visual_analysis.get('surface_area_cm2', 0)
906
  detection_confidence = visual_analysis.get('detection_confidence', 0)
907
+
 
908
  detection_image_path = visual_analysis.get('detection_image_path', '')
909
  segmentation_image_path = visual_analysis.get('segmentation_image_path', '')
910
  original_image_path = visual_analysis.get('original_image_path', '')
911
+
 
912
  original_image_base64 = None
913
  detection_image_base64 = None
914
  segmentation_image_base64 = None
915
+
 
916
  if image_url and os.path.exists(image_url):
917
  original_image_base64 = self.image_to_base64(image_url)
918
  elif original_image_path and os.path.exists(original_image_path):
919
  original_image_base64 = self.image_to_base64(original_image_path)
920
  elif saved_image_path and os.path.exists(saved_image_path):
921
  original_image_base64 = self.image_to_base64(saved_image_path)
922
+
 
923
  if detection_image_path and os.path.exists(detection_image_path):
924
  detection_image_base64 = self.image_to_base64(detection_image_path)
925
+
 
926
  if segmentation_image_path and os.path.exists(segmentation_image_path):
927
  segmentation_image_base64 = self.image_to_base64(segmentation_image_path)
928
+
 
929
  risk_assessment = self._generate_risk_assessment(questionnaire_data)
930
  risk_level = risk_assessment['risk_level']
931
  risk_score = risk_assessment['risk_score']
932
  risk_factors = risk_assessment['risk_factors']
933
+
 
934
  risk_class = "low"
935
  if risk_level.lower() == "moderate":
936
  risk_class = "moderate"
937
  elif risk_level.lower() in ["high", "very high"]:
938
  risk_class = "high"
939
+
 
940
  risk_factors_html = "<ul>" + "".join(f"<li>{factor}</li>" for factor in risk_factors) + "</ul>" if risk_factors else "<p>No specific risk factors identified.</p>"
941
+
 
942
  image_gallery_html = ""
943
  if original_image_base64 or detection_image_base64 or segmentation_image_base64:
944
  image_gallery_html = '<div class="image-gallery">'
 
945
  if original_image_base64:
946
  image_gallery_html += f'''
947
  <div class="image-item">
 
950
  <p>Uploaded image for analysis</p>
951
  </div>
952
  '''
 
953
  if detection_image_base64:
954
  image_gallery_html += f'''
955
  <div class="image-item">
 
958
  <p>AI-detected wound boundaries with {detection_confidence:.1%} confidence</p>
959
  </div>
960
  '''
 
961
  if segmentation_image_base64:
962
  image_gallery_html += f'''
963
  <div class="image-item">
 
966
  <p>Detailed wound area measurement and analysis</p>
967
  </div>
968
  '''
 
969
  image_gallery_html += '</div>'
970
+
971
+ report_html = self.markdown_to_html(report) if report else ""
972
+
 
 
 
 
973
  html_output = f"""
974
  <div style="max-width: 1200px; margin: 0 auto; background: white; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); overflow: hidden;">
975
  <div style="background: linear-gradient(135deg, #3182ce 0%, #2c5aa0 100%); color: white; padding: 40px; text-align: center;">
976
  <h1 style="margin: 0; font-size: 32px; font-weight: 700;">🔬 SmartHeal AI Comprehensive Analysis</h1>
977
  <p style="margin: 15px 0 0 0; opacity: 0.9; font-size: 18px;">Advanced Computer Vision & Medical AI Assessment</p>
978
  <div style="background: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px; margin-top: 20px;">
979
+ <p style="margin: 0; font-size: 16px;"><strong>Patient:</strong> {html.escape(str(questionnaire_data.get('patient_name', 'Unknown')))} | <strong>Analysis Date:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
980
  </div>
981
  </div>
982
+
983
  <div style="padding: 40px;">
984
  <div class="status-success" style="margin-bottom: 30px;">
985
  <strong>✅ Analysis Status:</strong> Analysis completed successfully with comprehensive wound assessment
986
  </div>
987
+
988
  <!-- Image Gallery Section -->
989
  <div style="margin-bottom: 40px;">
990
  <h2 style="color: #2d3748; font-size: 24px; margin-bottom: 20px; border-bottom: 2px solid #e53e3e; padding-bottom: 10px;">🖼️ Visual Analysis Gallery</h2>
991
  {image_gallery_html}
992
  </div>
993
+
994
  <!-- Wound Detection & Classification -->
995
  <div style="background: #f8f9fa; padding: 30px; border-radius: 12px; margin-bottom: 30px;">
996
  <h2 style="color: #2d3748; margin-top: 0;">🔍 Wound Detection & Classification</h2>
997
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0;">
998
  <div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
999
  <h3 style="color: #3182ce; margin: 0 0 10px 0;">Wound Type</h3>
1000
+ <p style="font-weight: 600; font-size: 18px; color: #2d3748; margin: 0;">{html.escape(str(wound_type))}</p>
1001
  </div>
1002
  <div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
1003
  <h3 style="color: #3182ce; margin: 0 0 10px 0;">Detection Confidence</h3>
 
1005
  </div>
1006
  <div style="background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
1007
  <h3 style="color: #3182ce; margin: 0 0 10px 0;">Location</h3>
1008
+ <p style="font-weight: 600; font-size: 18px; color: #2d3748; margin: 0;">{html.escape(str(questionnaire_data.get('wound_location', 'Not specified')))}</p>
1009
  </div>
1010
  </div>
1011
  </div>
1012
+
1013
  <!-- Wound Measurements -->
1014
  <div style="background: #e7f5ff; padding: 30px; border-radius: 12px; margin-bottom: 30px;">
1015
  <h2 style="color: #2d3748; margin-top: 0;">📏 Wound Measurements</h2>
 
1028
  </div>
1029
  </div>
1030
  </div>
1031
+
1032
  <!-- Risk Assessment -->
1033
  <div style="background: #fff4e6; padding: 30px; border-radius: 12px; margin-bottom: 30px;">
1034
  <h2 style="color: #2d3748; margin-top: 0;">⚠️ Risk Assessment</h2>
 
1054
  {risk_factors_html}
1055
  </div>
1056
  </div>
1057
+
1058
  <!-- Patient Information Summary -->
1059
  <div style="background: #f0f8f0; padding: 30px; border-radius: 12px; margin-bottom: 30px;">
1060
  <h2 style="color: #2d3748; margin-top: 0;">👤 Patient Information Summary</h2>
1061
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px;">
1062
+ <div><strong>Age:</strong> {html.escape(str(questionnaire_data.get('age', 'Not specified')))} years</div>
1063
+ <div><strong>Gender:</strong> {html.escape(str(questionnaire_data.get('patient_gender', 'Not specified')))}</div>
1064
+ <div><strong>Diabetic Status:</strong> {html.escape(str(questionnaire_data.get('diabetic', 'Unknown')))}</div>
1065
+ <div><strong>Pain Level:</strong> {html.escape(str(questionnaire_data.get('pain_level', 'Not assessed')))} / 10</div>
1066
+ <div><strong>Wound Duration:</strong> {html.escape(str(questionnaire_data.get('wound_duration', 'Not specified')))}</div>
1067
+ <div><strong>Moisture Level:</strong> {html.escape(str(questionnaire_data.get('moisture', 'Not assessed')))}</div>
1068
  </div>
1069
+ {f"<div style='margin-top: 20px;'><strong>Medical History:</strong> {html.escape(str(questionnaire_data.get('medical_history', 'None provided')))}</div>" if questionnaire_data.get('medical_history') else ""}
1070
+ {f"<div style='margin-top: 10px;'><strong>Current Medications:</strong> {html.escape(str(questionnaire_data.get('medications', 'None listed')))}</div>" if questionnaire_data.get('medications') else ""}
1071
+ {f"<div style='margin-top: 10px;'><strong>Known Allergies:</strong> {html.escape(str(questionnaire_data.get('allergies', 'None listed')))}</div>" if questionnaire_data.get('allergies') else ""}
1072
  </div>
1073
+
1074
  <!-- AI Generated Report -->
1075
  {f'<div style="background: #f8f9fa; padding: 30px; border-radius: 12px; margin-bottom: 30px;"><h2 style="color: #2d3748; margin-top: 0;">🤖 AI-Generated Clinical Report</h2><div style="background: white; padding: 25px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">{report_html}</div></div>' if report_html else ''}
1076
+
1077
  <!-- Important Disclaimers -->
1078
  <div style="background: #fff5f5; border: 2px solid #feb2b2; padding: 25px; border-radius: 12px; margin: 30px 0;">
1079
  <h3 style="color: #c53030; margin-top: 0;">⚠️ Important Medical Disclaimers</h3>
 
1084
  <li><strong>Risk Assessment Limitations:</strong> Risk factors are based on provided information and may not reflect the complete clinical picture.</li>
1085
  </ul>
1086
  </div>
1087
+
1088
  <!-- Footer -->
1089
  <div style="text-align: center; padding: 30px 0; border-top: 2px solid #e2e8f0; margin-top: 30px;">
1090
  <p style="color: #6c757d; font-style: italic; font-size: 16px; margin: 0;">
 
1095
  </div>
1096
  </div>
1097
  """
 
1098
  return html_output
1099
 
1100
  except Exception as e:
1101
  logging.error(f"Error formatting comprehensive results: {e}")
1102
  return f"<div class='status-error'>❌ Error displaying results: {str(e)}</div>"
1103
+
1104
  def _generate_risk_assessment(self, questionnaire_data):
1105
  """Generate risk assessment based on questionnaire data"""
1106
  if not questionnaire_data:
1107
  return {'risk_level': 'Unknown', 'risk_score': 0, 'risk_factors': []}
1108
+
1109
  risk_factors = []
1110
  risk_score = 0
1111
+
1112
  try:
1113
+ # Age
1114
  age = questionnaire_data.get('age', 0)
1115
  if isinstance(age, str):
1116
  try:
1117
  age = int(age)
1118
  except ValueError:
1119
  age = 0
 
1120
  if age > 65:
1121
  risk_factors.append("Advanced age (>65 years)")
1122
  risk_score += 2
1123
  elif age > 50:
1124
  risk_factors.append("Older adult (50-65 years)")
1125
  risk_score += 1
1126
+
1127
+ # Diabetes
1128
  diabetic_status = str(questionnaire_data.get('diabetic', '')).lower()
1129
  if 'yes' in diabetic_status:
1130
  risk_factors.append("Diabetes mellitus")
1131
  risk_score += 3
1132
+
1133
+ # Infection
1134
  infection = str(questionnaire_data.get('infection', '')).lower()
1135
  if 'yes' in infection:
1136
  risk_factors.append("Signs of infection present")
1137
  risk_score += 3
1138
+
1139
+ # Pain
1140
  pain_level = questionnaire_data.get('pain_level', 0)
1141
  if isinstance(pain_level, str):
1142
  try:
1143
  pain_level = float(pain_level)
1144
  except ValueError:
1145
  pain_level = 0
 
1146
  if pain_level >= 7:
1147
  risk_factors.append("High pain level (≥7/10)")
1148
  risk_score += 2
1149
  elif pain_level >= 5:
1150
  risk_factors.append("Moderate pain level (5-6/10)")
1151
  risk_score += 1
1152
+
1153
+ # Duration
1154
  duration = str(questionnaire_data.get('wound_duration', '')).lower()
1155
  if any(term in duration for term in ['month', 'months', 'year', 'years']):
1156
  risk_factors.append("Chronic wound (>4 weeks)")
1157
  risk_score += 3
1158
+
1159
+ # Moisture
1160
  moisture = str(questionnaire_data.get('moisture', '')).lower()
1161
  if any(term in moisture for term in ['wet', 'saturated']):
1162
  risk_factors.append("Excessive wound exudate")
1163
  risk_score += 1
1164
+
1165
+ # Medical history
1166
  medical_history = str(questionnaire_data.get('medical_history', '')).lower()
1167
  if any(term in medical_history for term in ['vascular', 'circulation', 'heart']):
1168
  risk_factors.append("Cardiovascular disease")
 
1173
  if any(term in medical_history for term in ['smoking', 'tobacco']):
1174
  risk_factors.append("Smoking history")
1175
  risk_score += 2
1176
+
1177
+ # Risk level
1178
  if risk_score >= 8:
1179
  risk_level = "Very High"
1180
  elif risk_score >= 6:
 
1183
  risk_level = "Moderate"
1184
  else:
1185
  risk_level = "Low"
1186
+
1187
  return {
1188
  'risk_score': risk_score,
1189
  'risk_level': risk_level,
1190
  'risk_factors': risk_factors
1191
  }
1192
+
1193
  except Exception as e:
1194
  logging.error(f"Risk assessment error: {e}")
1195
  return {