SmartHeal commited on
Commit
00831ad
Β·
verified Β·
1 Parent(s): e8aecf7

Update src/ui_components_original.py

Browse files
Files changed (1) hide show
  1. src/ui_components_original.py +496 -460
src/ui_components_original.py CHANGED
@@ -376,483 +376,519 @@ button.gr-button:hover, button.gr-button-primary:hover {
376
  """
377
 
378
  def create_interface(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  """
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
-
388
- # ----------------------- helpers (inner) -----------------------
389
-
390
- self._patient_choices = [] # list[str] rendered in dropdown
391
- self._patient_map = {} # label -> patient_id (int)
392
-
393
- def _to_data_url_if_local(path_or_url: str) -> str:
394
- if not path_or_url:
395
- return ""
396
- try:
397
- if os.path.exists(path_or_url):
398
- return self.image_to_base64(path_or_url) or ""
399
- return path_or_url # already a URL
400
- except Exception:
401
- return ""
402
-
403
- def _refresh_patient_dropdown(user_id: int):
404
- """Query patient's list and prepare dropdown choices."""
405
- self._patient_choices = []
406
- self._patient_map = {}
407
- try:
408
- rows = self.patient_history_manager.get_patient_list(user_id) or []
409
- # label starts with id -> stable parse
410
- for r in rows:
411
- pid = int(r.get("id") or 0)
412
- nm = r.get("patient_name") or "Unknown"
413
- age = r.get("patient_age") or ""
414
- gen = r.get("patient_gender") or ""
415
- v = int(r.get("total_visits") or 0)
416
- label = f"{pid} β€’ {nm} ({age}y {gen}) β€” visits: {v}"
417
- self._patient_choices.append(label)
418
- self._patient_map[label] = pid
419
- except Exception as e:
420
- logging.error(f"refresh dropdown error: {e}")
421
-
422
- def _label_to_id(label: str) -> Optional[int]:
423
- if not label: return None
424
  try:
425
- return int(str(label).split("β€’", 1)[0].strip())
426
  except Exception:
427
- return None
428
-
429
- def _fetch_patient_core(pid: int):
430
- """Get name/age/gender for an existing patient id."""
431
- row = self.database_manager.execute_query_one(
432
- "SELECT id, name, age, gender FROM patients WHERE id=%s LIMIT 1", (pid,)
433
- )
434
- return row or {}
435
-
436
- def _response_to_patient_id(resp_id: int) -> Optional[int]:
437
- row = self.database_manager.execute_query_one(
438
- "SELECT patient_id FROM questionnaire_responses WHERE id=%s LIMIT 1", (resp_id,)
439
- )
440
  try:
441
- return int(row["patient_id"]) if row and "patient_id" in row else None
442
  except Exception:
443
- return None
444
-
445
- def _rows_with_inline_images(rows: list[dict]) -> list[dict]:
446
- """Convert local file paths to data URLs so HTML displays them anywhere."""
447
- out = []
448
- for r in rows or []:
449
- r = dict(r)
450
- if r.get("image_url"):
451
- r["image_url"] = _to_data_url_if_local(r["image_url"])
452
- out.append(r)
453
- return out
454
-
455
- # ----------------------- Blocks UI -----------------------
456
-
457
- with gr.Blocks(css=self.get_custom_css(), title="SmartHeal - AI Wound Care Assistant") as app:
458
- # Header
459
- logo_url = "https://scontent.fccu31-2.fna.fbcdn.net/v/t39.30808-6/275933824_102121829111657_3325198727201325354_n.jpg?_nc_cat=104&ccb=1-7&_nc_sid=6ee11a&_nc_ohc=45krrEUpcSUQ7kNvwGVdiMW&_nc_oc=AdkTdxEC_TkYGiyDkEtTJZ_DFZELW17XKFmWpswmFqGB7JSdvTyWtnrQyLS0USngEiY&_nc_zt=23&_nc_ht=scontent.fccu31-2.fna&_nc_gid=ufAA4Hj5gTRwON5POYzz0Q&oh=00_AfW1-jLEN5RGeggqOvGgEaK_gdg0EDgxf_VhKbZwFLUO0Q&oe=6897A98B"
460
- gr.HTML(f"""
461
- <div class="medical-header">
462
- <img src="{logo_url}" class="logo" alt="SmartHeal Logo">
463
- <div>
464
- <h1>SmartHeal AI</h1>
465
- <p>Advanced Wound Care Analysis & Clinical Support System</p>
466
- </div>
467
- </div>
468
- """)
469
-
470
- # Disclaimer
471
- gr.HTML("""
472
- <div style="border:2px solid #FF6B6B;background:#FFE5E5;padding:15px;border-radius:12px;margin:10px 0;">
473
- <h3 style="color:#D63031;margin:0 0 8px 0;">⚠️ IMPORTANT DISCLAIMER</h3>
474
- <p><strong>This system is for testing/education and not a substitute for clinical judgment.</strong></p>
 
 
 
475
  </div>
476
- """)
477
-
478
- # Panels: auth vs practitioner vs organization
479
- with gr.Row():
480
- with gr.Column(visible=True) as auth_panel:
481
- with gr.Tabs():
482
- with gr.Tab("πŸ” Professional Login"):
483
- login_username = gr.Textbox(label="πŸ‘€ Username")
484
- login_password = gr.Textbox(label="πŸ”’ Password", type="password")
485
- login_btn = gr.Button("πŸš€ Sign In", variant="primary")
486
- login_status = gr.HTML("<div class='status-warning'>Please sign in.</div>")
487
-
488
- with gr.Tab("πŸ“ New Registration"):
489
- signup_username = gr.Textbox(label="πŸ‘€ Username")
490
- signup_email = gr.Textbox(label="πŸ“§ Email")
491
- signup_password = gr.Textbox(label="πŸ”’ Password", type="password")
492
- signup_name = gr.Textbox(label="πŸ‘¨β€βš•οΈ Full Name")
493
- signup_role = gr.Radio(["practitioner", "organization"], label="Account Type", value="practitioner")
494
-
495
- with gr.Group(visible=False) as org_fields:
496
- org_name = gr.Textbox(label="Organization Name")
497
- phone = gr.Textbox(label="Phone")
498
- country_code = gr.Textbox(label="Country Code")
499
- department = gr.Textbox(label="Department")
500
- location = gr.Textbox(label="Location")
501
-
502
- with gr.Group(visible=True) as prac_fields:
503
- organization_dropdown = gr.Dropdown(choices=self.get_organizations_dropdown(), label="Select Organization")
504
-
505
- signup_btn = gr.Button("✨ Create Account", variant="primary")
506
- signup_status = gr.HTML()
507
-
508
- with gr.Column(visible=False) as practitioner_panel:
509
- user_info = gr.HTML("")
510
- logout_btn_prac = gr.Button("πŸšͺ Logout", variant="secondary")
511
-
512
- with gr.Tabs():
513
- # ------------------- WOUND ANALYSIS -------------------
514
- with gr.Tab("πŸ”¬ Wound Analysis"):
515
- with gr.Row():
516
- with gr.Column(scale=1):
517
- gr.HTML("<h3>πŸ“‹ Patient Selection</h3>")
518
- patient_mode = gr.Radio(
519
- ["Existing patient", "New patient"],
520
- label="Patient mode",
521
- value="Existing patient"
522
- )
523
- existing_patient_dd = gr.Dropdown(
524
- choices=[],
525
- label="Select existing patient (ID β€’ Name)",
526
- interactive=True
527
- )
528
- with gr.Group(visible=False) as new_patient_group:
529
- new_patient_name = gr.Textbox(label="Patient Name")
530
- new_patient_age = gr.Number(label="Age", value=30, minimum=0, maximum=120)
531
- new_patient_gender = gr.Dropdown(choices=["Male", "Female", "Other"], value="Male", label="Gender")
532
-
533
- gr.HTML("<h3>🩹 Wound Information</h3>")
534
- wound_location = gr.Textbox(label="Wound Location", placeholder="e.g., Left ankle")
535
- wound_duration = gr.Textbox(label="Wound Duration", placeholder="e.g., 2 weeks")
536
- pain_level = gr.Slider(0, 10, value=5, step=1, label="Pain Level (0-10)")
537
-
538
- gr.HTML("<h3>βš•οΈ Clinical Assessment</h3>")
539
- moisture_level = gr.Dropdown(["Dry", "Moist", "Wet", "Saturated"], value="Moist", label="Moisture Level")
540
- infection_signs = gr.Dropdown(["None", "Mild", "Moderate", "Severe"], value="None", label="Signs of Infection")
541
- diabetic_status = gr.Dropdown(["Non-diabetic", "Type 1", "Type 2", "Gestational"], value="Non-diabetic", label="Diabetic Status")
542
-
543
- with gr.Column(scale=1):
544
- gr.HTML("<h3>πŸ“Έ Wound Image</h3>")
545
- wound_image = gr.Image(label="Upload Wound Image", type="filepath")
546
- gr.HTML("<h3>πŸ“ Medical History</h3>")
547
- previous_treatment = gr.Textbox(label="Previous Treatment", lines=3)
548
- medical_history = gr.Textbox(label="Medical History", lines=3)
549
- medications = gr.Textbox(label="Current Medications", lines=2)
550
- allergies = gr.Textbox(label="Known Allergies", lines=2)
551
- additional_notes = gr.Textbox(label="Additional Notes", lines=3)
552
-
553
- analyze_btn = gr.Button("πŸ”¬ Analyze Wound", variant="primary", elem_id="analyze-btn")
554
- analysis_output = gr.HTML("")
555
-
556
- # ------------------- PATIENT HISTORY -------------------
557
- with gr.Tab("πŸ“‹ Patient History"):
558
- with gr.Row():
559
- with gr.Column(scale=2):
560
- history_btn = gr.Button("πŸ“„ Load Patient History", variant="primary")
561
- patient_history_output = gr.HTML("")
562
- with gr.Column(scale=1):
563
- search_patient_name = gr.Textbox(label="Search patient by name")
564
- search_patient_btn = gr.Button("πŸ” Search", variant="secondary")
565
- specific_patient_output = gr.HTML("")
566
-
567
- gr.HTML("<hr style='margin:10px 0 6px 0;border:none;border-top:1px solid #e2e8f0'>")
568
- with gr.Row():
569
- view_details_dd = gr.Dropdown(choices=[], label="Select patient to view details")
570
- view_details_btn = gr.Button("πŸ“ˆ View Details (Timeline)", variant="primary")
571
- view_details_output = gr.HTML("")
572
-
573
- with gr.Column(visible=False) as organization_panel:
574
- gr.HTML("<div class='status-warning'>Organization dashboard coming soon.</div>")
575
- logout_btn_org = gr.Button("πŸšͺ Logout", variant="secondary")
576
-
577
- # ----------------------- handlers -----------------------
578
-
579
- def toggle_role_fields(role):
580
- return {
581
- org_fields: gr.update(visible=(role == "organization")),
582
- prac_fields: gr.update(visible=(role != "organization"))
583
- }
584
-
585
- def handle_signup(username, email, password, name, role, org_name_v, phone_v, cc_v, dept_v, loc_v, org_dropdown):
586
- try:
587
- if role == "organization":
588
- org_data = {
589
- 'org_name': org_name_v,
590
- 'email': email,
591
- 'phone': phone_v,
592
- 'country_code': cc_v,
593
- 'department': dept_v,
594
- 'location': loc_v
595
- }
596
- org_id = self.database_manager.create_organization(org_data)
597
- else:
598
- # For now pick first org (or default)
599
- org_id = 1
600
-
601
- user_data = {
602
- 'username': username, 'email': email, 'password': password,
603
- 'name': name, 'role': role, 'org_id': org_id
604
- }
605
- ok = self.auth_manager.create_user(user_data)
606
- if ok:
607
- return "<div class='status-success'>βœ… Account created. Please log in.</div>"
608
- return "<div class='status-error'>❌ Could not create account. Username/email may exist.</div>"
609
- except Exception as e:
610
- return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
611
-
612
- def handle_login(username, password):
613
- user = self.auth_manager.authenticate_user(username, password)
614
- if not user:
615
- return {
616
- login_status: "<div class='status-error'>❌ Invalid credentials.</div>"
617
  }
618
- self.current_user = user
619
- uid = int(user.get("id"))
620
- role = user.get("role")
621
-
622
- # Preload patient dropdowns for practitioners
623
- if role == "practitioner":
624
- _refresh_patient_dropdown(uid)
625
-
626
- info = f"<div class='status-success'>Welcome, <strong>{html.escape(user.get('name','User'))}</strong> β€” {html.escape(role)}</div>"
627
- updates = {login_status: info}
628
-
629
- if role == "practitioner":
630
- updates.update({
631
- auth_panel: gr.update(visible=False),
632
- practitioner_panel: gr.update(visible=True),
633
- user_info: info,
634
- existing_patient_dd: gr.update(choices=self._patient_choices),
635
- view_details_dd: gr.update(choices=self._patient_choices),
636
- })
637
  else:
638
- updates.update({
639
- auth_panel: gr.update(visible=False),
640
- organization_panel: gr.update(visible=True),
641
- })
642
- return updates
643
-
644
- def handle_logout():
645
- self.current_user = {}
646
- return {
647
- auth_panel: gr.update(visible=True),
648
- practitioner_panel: gr.update(visible=False),
649
- organization_panel: gr.update(visible=False)
650
  }
651
-
652
- def on_patient_mode_change(mode):
 
 
 
 
 
 
 
 
653
  return {
654
- new_patient_group: gr.update(visible=(mode == "New patient")),
655
- existing_patient_dd: gr.update(interactive=(mode == "Existing patient"))
656
  }
657
-
658
- def run_analysis(mode, existing_label,
659
- np_name, np_age, np_gender,
660
- w_loc, w_dur, pain, moist, infect, diabetic,
661
- prev_tx, med_hist, meds, alls, notes, img_path):
662
- try:
663
- if not img_path:
664
- return "<div class='status-error'>❌ Please upload a wound image.</div>"
665
-
666
- user_id = int(self.current_user.get("id", 0) or 0)
667
- if not user_id:
668
- return "<div class='status-error'>❌ Please login first.</div>"
669
-
670
- # Determine patient core fields (ensures same patient_id for existing)
671
- if mode == "Existing patient":
672
- pid = _label_to_id(existing_label)
673
- if not pid:
674
- return "<div class='status-warning'>⚠️ Select an existing patient.</div>"
675
- pcore = _fetch_patient_core(pid)
676
- patient_name_v = pcore.get("name")
677
- patient_age_v = pcore.get("age")
678
- patient_gender_v = pcore.get("gender")
679
- else:
680
- patient_name_v = np_name
681
- patient_age_v = np_age
682
- patient_gender_v = np_gender
683
-
684
- # Build questionnaire payload
685
- q_payload = {
686
- 'user_id': user_id,
687
- 'patient_name': patient_name_v,
688
- 'patient_age': patient_age_v,
689
- 'patient_gender': patient_gender_v,
690
- 'wound_location': w_loc,
691
- 'wound_duration': w_dur,
692
- 'pain_level': pain,
693
- 'moisture_level': moist,
694
- 'infection_signs': infect,
695
- 'diabetic_status': diabetic,
696
- 'previous_treatment': prev_tx,
697
- 'medical_history': med_hist,
698
- 'medications': meds,
699
- 'allergies': alls,
700
- 'additional_notes': notes
701
- }
702
-
703
- # Save questionnaire -> response_id
704
- response_id = self.database_manager.save_questionnaire(q_payload)
705
- if not response_id:
706
- return "<div class='status-error'>❌ Could not save questionnaire.</div>"
707
-
708
- # Resolve patient_id from response (works for new or existing)
709
- patient_id = _response_to_patient_id(response_id)
710
- if not patient_id:
711
- return "<div class='status-error'>❌ Could not resolve patient ID.</div>"
712
-
713
- # Save wound image to DB
714
- try:
715
- with Image.open(img_path) as pil:
716
- pil = pil.convert("RGB")
717
- img_meta = self.database_manager.save_wound_image(patient_id, pil)
718
- image_db_id = img_meta["id"] if img_meta else None
719
- except Exception as e:
720
- logging.error(f"save_wound_image error: {e}")
721
- image_db_id = None
722
-
723
- # Prepare AI analyzer questionnaire dict
724
- q_for_ai = {
725
- 'age': patient_age_v,
726
- 'diabetic': 'Yes' if diabetic != 'Non-diabetic' else 'No',
727
- 'allergies': alls,
728
- 'date_of_injury': 'Unknown',
729
- 'professional_care': 'Yes',
730
- 'oozing_bleeding': 'Minor Oozing' if infect != 'None' else 'None',
731
- 'infection': 'Yes' if infect != 'None' else 'No',
732
- 'moisture': moist,
733
- 'patient_name': patient_name_v,
734
- 'patient_gender': patient_gender_v,
735
- 'wound_location': w_loc,
736
- 'wound_duration': w_dur,
737
- 'pain_level': pain,
738
- 'previous_treatment': prev_tx,
739
- 'medical_history': med_hist,
740
- 'medications': meds,
741
- 'additional_notes': notes
742
- }
743
-
744
- # Run AI
745
- analysis_result = self.wound_analyzer.analyze_wound(img_path, q_for_ai)
746
- if not analysis_result or not analysis_result.get("success"):
747
- err = (analysis_result or {}).get("error", "Unknown analysis error")
748
- return f"<div class='status-error'>❌ AI Analysis failed: {html.escape(str(err))}</div>"
749
-
750
- # Persist AI analysis (ties back to template via response->questionnaire_id)
751
- try:
752
- self.database_manager.save_analysis(response_id, image_db_id, analysis_result)
753
- except Exception as e:
754
- logging.error(f"save_analysis error: {e}")
755
-
756
- # If a new patient was created, refresh dropdowns
757
- if mode == "New patient":
758
- _refresh_patient_dropdown(user_id)
759
-
760
- # Render fancy results (this method already converts file paths to data URLs)
761
- return self._format_comprehensive_analysis_results(
762
- analysis_result, img_path, q_for_ai
763
- )
764
- except Exception as e:
765
- logging.exception("run_analysis exception")
766
- return f"<div class='status-error'>❌ System error: {html.escape(str(e))}</div>"
767
-
768
- def load_history():
769
  try:
770
- uid = int(self.current_user.get("id", 0) or 0)
771
- if not uid:
772
- return "<div class='status-error'>❌ Please login first.</div>"
773
- rows = self.patient_history_manager.get_user_patient_history(uid) or []
774
- rows = _rows_with_inline_images(rows)
775
- return self.patient_history_manager.format_history_for_display(rows)
776
- except Exception as e:
777
- logging.error(f"load_history error: {e}")
778
- return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
779
-
780
- def do_search(name):
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
- if not (name or "").strip():
786
- return "<div class='status-warning'>⚠️ Enter a name to search.</div>"
787
- rows = self.patient_history_manager.search_patient_by_name(uid, name.strip()) or []
788
- rows = _rows_with_inline_images(rows)
789
- return self.patient_history_manager.format_patient_data_for_display(rows)
790
  except Exception as e:
791
- logging.error(f"search error: {e}")
792
- return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
793
-
794
- def view_details(existing_label):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
795
  try:
796
- uid = int(self.current_user.get("id", 0) or 0)
797
- if not uid:
798
- return "<div class='status-error'>❌ Please login first.</div>"
799
- pid = _label_to_id(existing_label)
800
- if not pid:
801
- return "<div class='status-warning'>⚠️ Select a patient.</div>"
802
- rows = self.patient_history_manager.get_wound_progression_by_id(uid, pid) or []
803
- rows = _rows_with_inline_images(rows)
804
- return self.patient_history_manager.format_patient_progress_for_display(rows)
805
  except Exception as e:
806
- logging.error(f"view_details error: {e}")
807
- return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
808
-
809
- # ----------------------- wiring -----------------------
810
-
811
- signup_role.change(
812
- toggle_role_fields,
813
- inputs=[signup_role],
814
- outputs=[org_fields, prac_fields]
815
- )
816
-
817
- signup_btn.click(
818
- handle_signup,
819
- inputs=[signup_username, signup_email, signup_password, signup_name, signup_role,
820
- org_name, phone, country_code, department, location, organization_dropdown],
821
- outputs=[signup_status]
822
- )
823
-
824
- login_btn.click(
825
- handle_login,
826
- inputs=[login_username, login_password],
827
- outputs=[login_status, auth_panel, practitioner_panel, organization_panel,
828
- user_info, existing_patient_dd, view_details_dd]
829
- )
830
-
831
- logout_btn_prac.click(handle_logout, outputs=[auth_panel, practitioner_panel, organization_panel])
832
- logout_btn_org.click(handle_logout, outputs=[auth_panel, practitioner_panel, organization_panel])
833
-
834
- patient_mode.change(
835
- on_patient_mode_change,
836
- inputs=[patient_mode],
837
- outputs=[new_patient_group, existing_patient_dd]
838
- )
839
-
840
- analyze_btn.click(
841
- run_analysis,
842
- inputs=[
843
- patient_mode, existing_patient_dd,
844
- new_patient_name, new_patient_age, new_patient_gender,
845
- wound_location, wound_duration, pain_level, moisture_level, infection_signs, diabetic_status,
846
- previous_treatment, medical_history, medications, allergies, additional_notes, wound_image
847
- ],
848
- outputs=[analysis_output]
849
- )
850
-
851
- history_btn.click(load_history, outputs=[patient_history_output])
852
- search_patient_btn.click(do_search, inputs=[search_patient_name], outputs=[specific_patient_output])
853
- view_details_btn.click(view_details, inputs=[view_details_dd], outputs=[view_details_output])
854
-
855
- return app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
856
 
857
 
858
  def _format_comprehensive_analysis_results(self, analysis_result, image_url=None, questionnaire_data=None):
 
376
  """
377
 
378
  def create_interface(self):
379
+ """
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 = []
407
+ self._patient_map = {}
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"
414
+ age = r.get("patient_age") or ""
415
+ gen = r.get("patient_gender") or ""
416
+ v = int(r.get("total_visits") or 0)
417
+ label = f"{pid} β€’ {nm} ({age}y {gen}) β€” visits: {v}"
418
+ self._patient_choices.append(label)
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
+ # ----------------------- Blocks UI -----------------------
486
+
487
+ with gr.Blocks(css=self.get_custom_css(), title="SmartHeal - AI Wound Care Assistant") as app:
488
+ # Header
489
+ logo_url = "https://scontent.fccu31-2.fna.fbcdn.net/v/t39.30808-6/275933824_102121829111657_3325198727201325354_n.jpg?_nc_cat=104&ccb=1-7&_nc_sid=6ee11a&_nc_ohc=45krrEUpcSUQ7kNvwGVdiMW&_nc_oc=AdkTdxEC_TkYGiyDkEtTJZ_DFZELW17XKFmWpswmFqGB7JSdvTyWtnrQyLS0USngEiY&_nc_zt=23&_nc_ht=scontent.fccu31-2.fna&_nc_gid=ufAA4Hj5gTRwON5POYzz0Q&oh=00_AfW1-jLEN5RGeggqOvGgEaK_gdg0EDgxf_VhKbZwFLUO0Q&oe=6897A98B"
490
+ gr.HTML(f"""
491
+ <div class="medical-header">
492
+ <img src="{logo_url}" class="logo" alt="SmartHeal Logo">
493
+ <div>
494
+ <h1>SmartHeal AI</h1>
495
+ <p>Advanced Wound Care Analysis & Clinical Support System</p>
496
  </div>
497
+ </div>
498
+ """)
499
+
500
+ # Disclaimer
501
+ gr.HTML("""
502
+ <div style="border:2px solid #FF6B6B;background:#FFE5E5;padding:15px;border-radius:12px;margin:10px 0;">
503
+ <h3 style="color:#D63031;margin:0 0 8px 0;">⚠️ IMPORTANT DISCLAIMER</h3>
504
+ <p><strong>This system is for testing/education and not a substitute for clinical judgment.</strong></p>
505
+ </div>
506
+ """)
507
+
508
+ # Panels: auth vs practitioner vs organization
509
+ with gr.Row():
510
+ with gr.Column(visible=True) as auth_panel:
511
+ with gr.Tabs():
512
+ with gr.Tab("πŸ” Professional Login"):
513
+ login_username = gr.Textbox(label="πŸ‘€ Username")
514
+ login_password = gr.Textbox(label="πŸ”’ Password", type="password")
515
+ login_btn = gr.Button("πŸš€ Sign In", variant="primary")
516
+ login_status = gr.HTML("<div class='status-warning'>Please sign in.</div>")
517
+
518
+ with gr.Tab("πŸ“ New Registration"):
519
+ signup_username = gr.Textbox(label="πŸ‘€ Username")
520
+ signup_email = gr.Textbox(label="πŸ“§ Email")
521
+ signup_password = gr.Textbox(label="πŸ”’ Password", type="password")
522
+ signup_name = gr.Textbox(label="πŸ‘¨β€βš•οΈ Full Name")
523
+ signup_role = gr.Radio(["practitioner", "organization"], label="Account Type", value="practitioner")
524
+
525
+ with gr.Group(visible=False) as org_fields:
526
+ org_name = gr.Textbox(label="Organization Name")
527
+ phone = gr.Textbox(label="Phone")
528
+ country_code = gr.Textbox(label="Country Code")
529
+ department = gr.Textbox(label="Department")
530
+ location = gr.Textbox(label="Location")
531
+
532
+ with gr.Group(visible=True) as prac_fields:
533
+ organization_dropdown = gr.Dropdown(choices=self.get_organizations_dropdown(), label="Select Organization")
534
+
535
+ signup_btn = gr.Button("✨ Create Account", variant="primary")
536
+ signup_status = gr.HTML()
537
+
538
+ with gr.Column(visible=False) as practitioner_panel:
539
+ user_info = gr.HTML("")
540
+ logout_btn_prac = gr.Button("πŸšͺ Logout", variant="secondary")
541
+
542
+ with gr.Tabs():
543
+ # ------------------- WOUND ANALYSIS -------------------
544
+ with gr.Tab("πŸ”¬ Wound Analysis"):
545
+ with gr.Row():
546
+ with gr.Column(scale=1):
547
+ gr.HTML("<h3>πŸ“‹ Patient Selection</h3>")
548
+ patient_mode = gr.Radio(
549
+ ["Existing patient", "New patient"],
550
+ label="Patient mode",
551
+ value="Existing patient"
552
+ )
553
+ existing_patient_dd = gr.Dropdown(
554
+ choices=[],
555
+ label="Select existing patient (ID β€’ Name)",
556
+ interactive=True
557
+ )
558
+ with gr.Group(visible=False) as new_patient_group:
559
+ new_patient_name = gr.Textbox(label="Patient Name")
560
+ new_patient_age = gr.Number(label="Age", value=30, minimum=0, maximum=120)
561
+ new_patient_gender = gr.Dropdown(choices=["Male", "Female", "Other"], value="Male", label="Gender")
562
+
563
+ gr.HTML("<h3>🩹 Wound Information</h3>")
564
+ wound_location = gr.Textbox(label="Wound Location", placeholder="e.g., Left ankle")
565
+ wound_duration = gr.Textbox(label="Wound Duration", placeholder="e.g., 2 weeks")
566
+ pain_level = gr.Slider(0, 10, value=5, step=1, label="Pain Level (0-10)")
567
+
568
+ gr.HTML("<h3>βš•οΈ Clinical Assessment</h3>")
569
+ moisture_level = gr.Dropdown(["Dry", "Moist", "Wet", "Saturated"], value="Moist", label="Moisture Level")
570
+ infection_signs = gr.Dropdown(["None", "Mild", "Moderate", "Severe"], value="None", label="Signs of Infection")
571
+ diabetic_status = gr.Dropdown(["Non-diabetic", "Type 1", "Type 2", "Gestational"], value="Non-diabetic", label="Diabetic Status")
572
+
573
+ with gr.Column(scale=1):
574
+ gr.HTML("<h3>πŸ“Έ Wound Image</h3>")
575
+ wound_image = gr.Image(label="Upload Wound Image", type="filepath")
576
+ gr.HTML("<h3>πŸ“ Medical History</h3>")
577
+ previous_treatment = gr.Textbox(label="Previous Treatment", lines=3)
578
+ medical_history = gr.Textbox(label="Medical History", lines=3)
579
+ medications = gr.Textbox(label="Current Medications", lines=2)
580
+ allergies = gr.Textbox(label="Known Allergies", lines=2)
581
+ additional_notes = gr.Textbox(label="Additional Notes", lines=3)
582
+
583
+ analyze_btn = gr.Button("πŸ”¬ Analyze Wound", variant="primary", elem_id="analyze-btn")
584
+ analysis_output = gr.HTML("")
585
+
586
+ # ------------------- PATIENT HISTORY -------------------
587
+ with gr.Tab("πŸ“‹ Patient History"):
588
+ with gr.Row():
589
+ with gr.Column(scale=2):
590
+ history_btn = gr.Button("πŸ“„ Load Patient History", variant="primary")
591
+ patient_history_output = gr.HTML("")
592
+ with gr.Column(scale=1):
593
+ search_patient_name = gr.Textbox(label="Search patient by name")
594
+ search_patient_btn = gr.Button("πŸ” Search", variant="secondary")
595
+ specific_patient_output = gr.HTML("")
596
+
597
+ gr.HTML("<hr style='margin:10px 0 6px 0;border:none;border-top:1px solid #e2e8f0'>")
598
+ with gr.Row():
599
+ view_details_dd = gr.Dropdown(choices=[], label="Select patient to view details")
600
+ view_details_btn = gr.Button("πŸ“ˆ View Details (Timeline)", variant="primary")
601
+ view_details_output = gr.HTML("")
602
+
603
+ with gr.Column(visible=False) as organization_panel:
604
+ gr.HTML("<div class='status-warning'>Organization dashboard coming soon.</div>")
605
+ logout_btn_org = gr.Button("πŸšͺ Logout", variant="secondary")
606
+
607
+ # ----------------------- handlers -----------------------
608
+
609
+ def toggle_role_fields(role):
610
+ return {
611
+ org_fields: gr.update(visible=(role == "organization")),
612
+ prac_fields: gr.update(visible=(role != "organization"))
613
+ }
614
+
615
+ def handle_signup(username, email, password, name, role, org_name_v, phone_v, cc_v, dept_v, loc_v, org_dropdown):
616
+ try:
617
+ if role == "organization":
618
+ org_data = {
619
+ 'org_name': org_name_v,
620
+ 'email': email,
621
+ 'phone': phone_v,
622
+ 'country_code': cc_v,
623
+ 'department': dept_v,
624
+ 'location': loc_v
 
 
 
 
 
 
 
 
 
 
 
 
 
625
  }
626
+ org_id = self.database_manager.create_organization(org_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  else:
628
+ # For now pick first org (or default)
629
+ org_id = 1
630
+
631
+ user_data = {
632
+ 'username': username, 'email': email, 'password': password,
633
+ 'name': name, 'role': role, 'org_id': org_id
 
 
 
 
 
 
634
  }
635
+ ok = self.auth_manager.create_user(user_data)
636
+ if ok:
637
+ return "<div class='status-success'>βœ… Account created. Please log in.</div>"
638
+ return "<div class='status-error'>❌ Could not create account. Username/email may exist.</div>"
639
+ except Exception as e:
640
+ return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
641
+
642
+ def handle_login(username, password):
643
+ user = self.auth_manager.authenticate_user(username, password)
644
+ if not user:
645
  return {
646
+ login_status: "<div class='status-error'>❌ Invalid credentials.</div>"
 
647
  }
648
+ self.current_user = user
649
+ uid = int(user.get("id"))
650
+ role = user.get("role")
651
+
652
+ # Preload patient dropdowns for practitioners
653
+ if role == "practitioner":
654
+ _refresh_patient_dropdown(uid)
655
+
656
+ info = f"<div class='status-success'>Welcome, <strong>{html.escape(user.get('name','User'))}</strong> β€” {html.escape(role)}</div>"
657
+ updates = {login_status: info}
658
+
659
+ if role == "practitioner":
660
+ updates.update({
661
+ auth_panel: gr.update(visible=False),
662
+ practitioner_panel: gr.update(visible=True),
663
+ user_info: info,
664
+ existing_patient_dd: gr.update(choices=self._patient_choices),
665
+ view_details_dd: gr.update(choices=self._patient_choices),
666
+ })
667
+ else:
668
+ updates.update({
669
+ auth_panel: gr.update(visible=False),
670
+ organization_panel: gr.update(visible=True),
671
+ })
672
+ return updates
673
+
674
+ def handle_logout():
675
+ self.current_user = {}
676
+ return {
677
+ auth_panel: gr.update(visible=True),
678
+ practitioner_panel: gr.update(visible=False),
679
+ organization_panel: gr.update(visible=False)
680
+ }
681
+
682
+ def on_patient_mode_change(mode):
683
+ return {
684
+ new_patient_group: gr.update(visible=(mode == "New patient")),
685
+ existing_patient_dd: gr.update(interactive=(mode == "Existing patient"))
686
+ }
687
+
688
+ def run_analysis(mode, existing_label,
689
+ np_name, np_age, np_gender,
690
+ w_loc, w_dur, pain, moist, infect, diabetic,
691
+ prev_tx, med_hist, meds, alls, notes, img_path):
692
+ try:
693
+ if not img_path:
694
+ return "<div class='status-error'>❌ Please upload a wound image.</div>"
695
+
696
+ user_id = int(self.current_user.get("id", 0) or 0)
697
+ if not user_id:
698
+ return "<div class='status-error'>❌ Please login first.</div>"
699
+
700
+ # Determine patient core fields (ensures same patient_id for existing)
701
+ if mode == "Existing patient":
702
+ pid = _label_to_id(existing_label)
703
+ if not pid:
704
+ return "<div class='status-warning'>⚠️ Select an existing patient.</div>"
705
+ pcore = _fetch_patient_core(pid)
706
+ patient_name_v = pcore.get("name")
707
+ patient_age_v = pcore.get("age")
708
+ patient_gender_v = pcore.get("gender")
709
+ else:
710
+ patient_name_v = np_name
711
+ patient_age_v = np_age
712
+ patient_gender_v = np_gender
713
+
714
+ # Build questionnaire payload
715
+ q_payload = {
716
+ 'user_id': user_id,
717
+ 'patient_name': patient_name_v,
718
+ 'patient_age': patient_age_v,
719
+ 'patient_gender': patient_gender_v,
720
+ 'wound_location': w_loc,
721
+ 'wound_duration': w_dur,
722
+ 'pain_level': pain,
723
+ 'moisture_level': moist,
724
+ 'infection_signs': infect,
725
+ 'diabetic_status': diabetic,
726
+ 'previous_treatment': prev_tx,
727
+ 'medical_history': med_hist,
728
+ 'medications': meds,
729
+ 'allergies': alls,
730
+ 'additional_notes': notes
731
+ }
732
+
733
+ # Save questionnaire -> response_id
734
+ response_id = self.database_manager.save_questionnaire(q_payload)
735
+
736
+ # πŸ”’ Normalize in case an older/alternate version returns a dict
737
+ if isinstance(response_id, dict):
738
+ response_id = response_id.get("response_id") or response_id.get("id")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
739
  try:
740
+ response_id = int(response_id)
741
+ except Exception:
742
+ return "<div class='status-error'>❌ Could not resolve response ID.</div>"
743
+
744
+ # Resolve patient_id from response (works for new or existing)
745
+ patient_id = _response_to_patient_id(response_id)
746
+ if not patient_id:
747
+ return "<div class='status-error'>❌ Could not resolve patient ID.</div>"
748
+
749
+ # Save wound image to DB
 
750
  try:
751
+ with Image.open(img_path) as pil:
752
+ pil = pil.convert("RGB")
753
+ img_meta = self.database_manager.save_wound_image(patient_id, pil)
754
+ image_db_id = img_meta["id"] if img_meta else None
 
 
 
 
755
  except Exception as e:
756
+ logging.error(f"save_wound_image error: {e}")
757
+ image_db_id = None
758
+
759
+ # Prepare AI analyzer questionnaire dict
760
+ q_for_ai = {
761
+ 'age': patient_age_v,
762
+ 'diabetic': 'Yes' if diabetic != 'Non-diabetic' else 'No',
763
+ 'allergies': alls,
764
+ 'date_of_injury': 'Unknown',
765
+ 'professional_care': 'Yes',
766
+ 'oozing_bleeding': 'Minor Oozing' if infect != 'None' else 'None',
767
+ 'infection': 'Yes' if infect != 'None' else 'No',
768
+ 'moisture': moist,
769
+ 'patient_name': patient_name_v,
770
+ 'patient_gender': patient_gender_v,
771
+ 'wound_location': w_loc,
772
+ 'wound_duration': w_dur,
773
+ 'pain_level': pain,
774
+ 'previous_treatment': prev_tx,
775
+ 'medical_history': med_hist,
776
+ 'medications': meds,
777
+ 'additional_notes': notes
778
+ }
779
+
780
+ # Run AI
781
+ analysis_result = self.wound_analyzer.analyze_wound(img_path, q_for_ai)
782
+ if not analysis_result or not analysis_result.get("success"):
783
+ err = (analysis_result or {}).get("error", "Unknown analysis error")
784
+ return f"<div class='status-error'>❌ AI Analysis failed: {html.escape(str(err))}</div>"
785
+
786
+ # Persist AI analysis (ties back to template via response->questionnaire_id)
787
  try:
788
+ self.database_manager.save_analysis(response_id, image_db_id, analysis_result)
 
 
 
 
 
 
 
 
789
  except Exception as e:
790
+ logging.error(f"save_analysis error: {e}")
791
+
792
+ # If a new patient was created, refresh dropdowns
793
+ if mode == "New patient":
794
+ _refresh_patient_dropdown(user_id)
795
+
796
+ # Render fancy results
797
+ return self._format_comprehensive_analysis_results(
798
+ analysis_result, img_path, q_for_ai
799
+ )
800
+ except Exception as e:
801
+ logging.exception("run_analysis exception")
802
+ return f"<div class='status-error'>❌ System error: {html.escape(str(e))}</div>"
803
+
804
+ def load_history():
805
+ try:
806
+ uid = int(self.current_user.get("id", 0) or 0)
807
+ if not uid:
808
+ return "<div class='status-error'>❌ Please login first.</div>"
809
+ rows = self.patient_history_manager.get_user_patient_history(uid) or []
810
+ rows = _rows_with_inline_images(rows)
811
+ return self.patient_history_manager.format_history_for_display(rows)
812
+ except Exception as e:
813
+ logging.error(f"load_history error: {e}")
814
+ return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
815
+
816
+ def do_search(name):
817
+ try:
818
+ uid = int(self.current_user.get("id", 0) or 0)
819
+ if not uid:
820
+ return "<div class='status-error'>❌ Please login first.</div>"
821
+ if not (name or "").strip():
822
+ return "<div class='status-warning'>⚠️ Enter a name to search.</div>"
823
+ rows = self.patient_history_manager.search_patient_by_name(uid, name.strip()) or []
824
+ rows = _rows_with_inline_images(rows)
825
+ return self.patient_history_manager.format_patient_data_for_display(rows)
826
+ except Exception as e:
827
+ logging.error(f"search error: {e}")
828
+ return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
829
+
830
+ def view_details(existing_label):
831
+ try:
832
+ uid = int(self.current_user.get("id", 0) or 0)
833
+ if not uid:
834
+ return "<div class='status-error'>❌ Please login first.</div>"
835
+ pid = _label_to_id(existing_label)
836
+ if not pid:
837
+ return "<div class='status-warning'>⚠️ Select a patient.</div>"
838
+ rows = self.patient_history_manager.get_wound_progression_by_id(uid, pid) or []
839
+ rows = _rows_with_inline_images(rows)
840
+ return self.patient_history_manager.format_patient_progress_for_display(rows)
841
+ except Exception as e:
842
+ logging.error(f"view_details error: {e}")
843
+ return f"<div class='status-error'>❌ Error: {html.escape(str(e))}</div>"
844
+
845
+ # ----------------------- wiring -----------------------
846
+
847
+ signup_role.change(
848
+ toggle_role_fields,
849
+ inputs=[signup_role],
850
+ outputs=[org_fields, prac_fields]
851
+ )
852
+
853
+ signup_btn.click(
854
+ handle_signup,
855
+ inputs=[signup_username, signup_email, signup_password, signup_name, signup_role,
856
+ org_name, phone, country_code, department, location, organization_dropdown],
857
+ outputs=[signup_status]
858
+ )
859
+
860
+ login_btn.click(
861
+ handle_login,
862
+ inputs=[login_username, login_password],
863
+ outputs=[login_status, auth_panel, practitioner_panel, organization_panel,
864
+ user_info, existing_patient_dd, view_details_dd]
865
+ )
866
+
867
+ logout_btn_prac.click(handle_logout, outputs=[auth_panel, practitioner_panel, organization_panel])
868
+ logout_btn_org.click(handle_logout, outputs=[auth_panel, practitioner_panel, organization_panel])
869
+
870
+ patient_mode.change(
871
+ on_patient_mode_change,
872
+ inputs=[patient_mode],
873
+ outputs=[new_patient_group, existing_patient_dd]
874
+ )
875
+
876
+ analyze_btn.click(
877
+ run_analysis,
878
+ inputs=[
879
+ patient_mode, existing_patient_dd,
880
+ new_patient_name, new_patient_age, new_patient_gender,
881
+ wound_location, wound_duration, pain_level, moisture_level, infection_signs, diabetic_status,
882
+ previous_treatment, medical_history, medications, allergies, additional_notes, wound_image
883
+ ],
884
+ outputs=[analysis_output]
885
+ )
886
+
887
+ history_btn.click(load_history, outputs=[patient_history_output])
888
+ search_patient_btn.click(do_search, inputs=[search_patient_name], outputs=[specific_patient_output])
889
+ view_details_btn.click(view_details, inputs=[view_details_dd], outputs=[view_details_output])
890
+
891
+ return app
892
 
893
 
894
  def _format_comprehensive_analysis_results(self, analysis_result, image_url=None, questionnaire_data=None):