Spaces:
Running
Running
Update src/ui_components_original.py
Browse files- src/ui_components_original.py +357 -346
src/ui_components_original.py
CHANGED
|
@@ -376,18 +376,87 @@ button.gr-button:hover, button.gr-button-primary:hover {
|
|
| 376 |
"""
|
| 377 |
|
| 378 |
def create_interface(self):
|
| 379 |
-
"""
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
import gradio as gr
|
| 382 |
-
import
|
| 383 |
-
import logging
|
| 384 |
-
from datetime import datetime
|
| 385 |
|
| 386 |
-
|
| 387 |
|
| 388 |
-
|
| 389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
gr.HTML(f"""
|
| 392 |
<div class="medical-header">
|
| 393 |
<img src="{logo_url}" class="logo" alt="SmartHeal Logo">
|
|
@@ -400,403 +469,344 @@ button.gr-button:hover, button.gr-button-primary:hover {
|
|
| 400 |
|
| 401 |
# Disclaimer
|
| 402 |
gr.HTML("""
|
| 403 |
-
<div style="border:
|
| 404 |
-
<h3 style="color
|
| 405 |
-
<p><strong>This
|
| 406 |
-
<p>Information generated may be inaccurate. Always consult a qualified healthcare provider for medical concerns.</p>
|
| 407 |
</div>
|
| 408 |
""")
|
| 409 |
|
| 410 |
-
#
|
| 411 |
with gr.Row():
|
| 412 |
-
# ---------------------- AUTH PANEL -----------------------
|
| 413 |
with gr.Column(visible=True) as auth_panel:
|
| 414 |
-
gr.HTML("""
|
| 415 |
-
<div style="text-align: center; margin: 40px 0;">
|
| 416 |
-
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px; border-radius: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); max-width: 500px; margin: 0 auto;">
|
| 417 |
-
<h2 style="color: white; font-size: 2.2rem; margin-bottom: 10px; font-weight: 700;">π₯ SmartHeal Access</h2>
|
| 418 |
-
<p style="color: rgba(255,255,255,0.95); font-size: 1rem; margin-bottom: 0;">Secure Healthcare Professional Portal</p>
|
| 419 |
-
</div>
|
| 420 |
-
</div>
|
| 421 |
-
""")
|
| 422 |
-
|
| 423 |
with gr.Tabs():
|
| 424 |
with gr.Tab("π Professional Login"):
|
| 425 |
-
login_username = gr.Textbox(label="π€ Username"
|
| 426 |
-
login_password = gr.Textbox(label="π Password", type="password"
|
| 427 |
-
login_btn = gr.Button("π Sign In
|
| 428 |
-
login_status = gr.HTML("<div
|
| 429 |
|
| 430 |
with gr.Tab("π New Registration"):
|
| 431 |
-
signup_username = gr.Textbox(label="π€ Username"
|
| 432 |
-
signup_email = gr.Textbox(label="π§ Email
|
| 433 |
-
signup_password = gr.Textbox(label="π Password", type="password"
|
| 434 |
-
signup_name = gr.Textbox(label="π¨ββοΈ Full Name"
|
| 435 |
-
signup_role = gr.Radio(["practitioner", "organization"], label="
|
| 436 |
|
| 437 |
with gr.Group(visible=False) as org_fields:
|
| 438 |
-
gr.
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
location = gr.Textbox(label="Location", placeholder="City, State/Province, Country")
|
| 444 |
|
| 445 |
with gr.Group(visible=True) as prac_fields:
|
| 446 |
-
gr.
|
| 447 |
-
organization_dropdown = gr.Dropdown(choices=self.get_organizations_dropdown(), label="Select Your Organization")
|
| 448 |
|
| 449 |
-
signup_btn = gr.Button("β¨ Create
|
| 450 |
-
signup_status = gr.HTML(
|
| 451 |
|
| 452 |
-
# ------------------- PRACTITIONER PANEL -------------------
|
| 453 |
with gr.Column(visible=False) as practitioner_panel:
|
| 454 |
-
gr.HTML('<div class="medical-card-title" style="font-weight:800;font-size:20px;">π©ββοΈ Practitioner Dashboard</div>')
|
| 455 |
user_info = gr.HTML("")
|
| 456 |
logout_btn_prac = gr.Button("πͺ Logout", variant="secondary")
|
| 457 |
|
| 458 |
with gr.Tabs():
|
| 459 |
-
#
|
| 460 |
with gr.Tab("π¬ Wound Analysis"):
|
| 461 |
with gr.Row():
|
| 462 |
with gr.Column(scale=1):
|
| 463 |
-
gr.HTML("<h3>π Patient
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
|
| 468 |
gr.HTML("<h3>π©Ή Wound Information</h3>")
|
| 469 |
-
wound_location = gr.Textbox(label="Wound Location", placeholder="e.g., Left ankle
|
| 470 |
-
wound_duration = gr.Textbox(label="Wound Duration", placeholder="e.g., 2 weeks
|
| 471 |
-
pain_level = gr.Slider(
|
| 472 |
|
| 473 |
gr.HTML("<h3>βοΈ Clinical Assessment</h3>")
|
| 474 |
-
moisture_level = gr.Dropdown(
|
| 475 |
-
infection_signs = gr.Dropdown(
|
| 476 |
-
diabetic_status = gr.Dropdown(
|
| 477 |
|
| 478 |
with gr.Column(scale=1):
|
| 479 |
-
gr.HTML("<h3>πΈ Wound Image
|
| 480 |
wound_image = gr.Image(label="Upload Wound Image", type="filepath")
|
| 481 |
-
|
| 482 |
gr.HTML("<h3>π Medical History</h3>")
|
| 483 |
-
previous_treatment = gr.Textbox(label="Previous Treatment",
|
| 484 |
-
medical_history = gr.Textbox(label="Medical History",
|
| 485 |
-
medications = gr.Textbox(label="Current Medications",
|
| 486 |
-
allergies = gr.Textbox(label="Known Allergies",
|
| 487 |
-
additional_notes = gr.Textbox(label="Additional Notes",
|
| 488 |
|
| 489 |
-
analyze_btn = gr.Button("π¬ Analyze Wound", variant="primary",
|
| 490 |
analysis_output = gr.HTML("")
|
| 491 |
|
| 492 |
-
#
|
| 493 |
with gr.Tab("π Patient History"):
|
| 494 |
with gr.Row():
|
| 495 |
with gr.Column(scale=2):
|
| 496 |
-
gr.
|
| 497 |
-
history_btn = gr.Button("π Load Patient History", variant="primary")
|
| 498 |
patient_history_output = gr.HTML("")
|
| 499 |
-
|
| 500 |
with gr.Column(scale=1):
|
| 501 |
-
|
| 502 |
-
gr.
|
| 503 |
-
patient_selector = gr.Dropdown(
|
| 504 |
-
choices=[],
|
| 505 |
-
label="Select Patient",
|
| 506 |
-
interactive=True,
|
| 507 |
-
allow_custom_value=False,
|
| 508 |
-
info="Pick a patient to view wound progression"
|
| 509 |
-
)
|
| 510 |
-
reload_patients_btn = gr.Button("π Refresh Patient List", variant="secondary")
|
| 511 |
-
view_details_btn = gr.Button("π View Details", variant="primary")
|
| 512 |
-
patient_details_output = gr.HTML("")
|
| 513 |
-
|
| 514 |
-
# Search Specific Patient (restored)
|
| 515 |
-
gr.HTML("<h3 style='margin-top:20px;'>π Search Specific Patient</h3>")
|
| 516 |
-
search_patient_name = gr.Textbox(label="Patient Name", placeholder="Enter patient name to searchβ¦")
|
| 517 |
-
search_patient_btn = gr.Button("π Search Patient History", variant="secondary")
|
| 518 |
specific_patient_output = gr.HTML("")
|
| 519 |
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
if rl.startswith("low"):
|
| 526 |
-
bg, fg = "#d4edda", "#155724"
|
| 527 |
-
elif rl.startswith("moderate"):
|
| 528 |
-
bg, fg = "#fff3cd", "#856404"
|
| 529 |
-
elif rl.startswith("high"):
|
| 530 |
-
bg, fg = "#f8d7da", "#721c24"
|
| 531 |
-
return f"<span style='background:{bg};color:{fg};padding:6px 10px;border-radius:999px;font-weight:700;font-size:12px;letter-spacing:.4px;text-transform:uppercase;'>{html.escape(level or 'Unknown')}</span>"
|
| 532 |
-
|
| 533 |
-
def _render_progression_timeline(rows):
|
| 534 |
-
if not rows:
|
| 535 |
-
return "<div class='status-warning'>No wound progression data found for this patient.</div>"
|
| 536 |
-
|
| 537 |
-
head = """
|
| 538 |
-
<div style="border:1px solid #e2e8f0;border-radius:16px;overflow:hidden;background:white;box-shadow:0 8px 24px rgba(0,0,0,.06);">
|
| 539 |
-
<div style="background:linear-gradient(135deg,#2563eb 0%,#1e3a8a 100%);color:white;padding:24px 28px;">
|
| 540 |
-
<h2 style="margin:0;font-size:22px;font-weight:800;letter-spacing:.2px;">π©Ί Wound Progression</h2>
|
| 541 |
-
<p style="margin:6px 0 0 0;opacity:.95;">Chronological clinical snapshots with AI risk assessment</p>
|
| 542 |
-
</div>
|
| 543 |
-
<div style="padding:22px 22px 6px 22px">
|
| 544 |
-
"""
|
| 545 |
-
items = []
|
| 546 |
-
for i, r in enumerate(rows, start=1):
|
| 547 |
-
dt = r.get("visit_date")
|
| 548 |
-
try:
|
| 549 |
-
dt_str = dt.strftime('%b %d, %Y β’ %I:%M %p') if hasattr(dt, "strftime") else str(dt)
|
| 550 |
-
except Exception:
|
| 551 |
-
dt_str = str(dt)
|
| 552 |
-
|
| 553 |
-
risk = _risk_chip_ui(r.get("risk_level"))
|
| 554 |
-
wound_loc = r.get("wound_location") or "N/A"
|
| 555 |
-
moisture = r.get("moisture") or "β"
|
| 556 |
-
infection = r.get("infection") or "β"
|
| 557 |
-
pain = r.get("pain_level", "N/A")
|
| 558 |
-
summary = r.get("summary")
|
| 559 |
-
img = r.get("image_url")
|
| 560 |
-
|
| 561 |
-
summary_block = (
|
| 562 |
-
f"<div style='margin-top:12px;color:#0f172a;background:white;border:1px solid #e2e8f0;border-radius:10px;padding:12px;'><strong>Summary:</strong> {html.escape(str(summary))}</div>"
|
| 563 |
-
if summary else ""
|
| 564 |
-
)
|
| 565 |
-
img_block = (
|
| 566 |
-
f"<div style='margin-top:12px;'><img src='{html.escape(str(img))}' style='max-width:360px;border-radius:12px;border:1px solid #e2e8f0;box-shadow:0 6px 18px rgba(0,0,0,.06)'></div>"
|
| 567 |
-
if img else ""
|
| 568 |
-
)
|
| 569 |
|
| 570 |
-
|
| 571 |
-
<div
|
| 572 |
-
|
| 573 |
-
<div style="width:12px;height:12px;background:#2563eb;border:2px solid white;border-radius:999px;box-shadow:0 0 0 3px rgba(37,99,235,.15)"></div>
|
| 574 |
-
<div style="position:absolute;left:6px;top:24px;bottom:-10px;width:2px;background:linear-gradient(180deg, rgba(203,213,225,1), rgba(203,213,225,0));"></div>
|
| 575 |
-
</div>
|
| 576 |
-
<div style="border:1px solid #e2e8f0;border-radius:12px;padding:16px;background:#f9fafb;">
|
| 577 |
-
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;justify-content:space-between;">
|
| 578 |
-
<div style="font-weight:800;color:#0f172a;letter-spacing:.2px;">Visit #{i}</div>
|
| 579 |
-
<div style="color:#475569;font-weight:600;">{html.escape(dt_str)}</div>
|
| 580 |
-
</div>
|
| 581 |
-
<div style="margin-top:10px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;">
|
| 582 |
-
<div style="background:white;border:1px solid #e2e8f0;border-radius:10px;padding:12px;">
|
| 583 |
-
<div style="font-size:12px;color:#64748b;font-weight:700;letter-spacing:.4px;text-transform:uppercase;">Location</div>
|
| 584 |
-
<div style="font-weight:700;color:#0f172a;">{html.escape(str(wound_loc))}</div>
|
| 585 |
-
</div>
|
| 586 |
-
<div style="background:white;border:1px solid #e2e8f0;border-radius:10px;padding:12px;">
|
| 587 |
-
<div style="font-size:12px;color:#64748b;font-weight:700;letter-spacing:.4px;text-transform:uppercase;">Pain</div>
|
| 588 |
-
<div style="font-weight:700;color:#0f172a;">{html.escape(str(pain))} / 10</div>
|
| 589 |
-
</div>
|
| 590 |
-
<div style="background:white;border:1px solid #e2e8f0;border-radius:10px;padding:12px;">
|
| 591 |
-
<div style="font-size:12px;color:#64748b;font-weight:700;letter-spacing:.4px;text-transform:uppercase;">Moisture</div>
|
| 592 |
-
<div style="font-weight:700;color:#0f172a;">{html.escape(str(moisture))}</div>
|
| 593 |
-
</div>
|
| 594 |
-
<div style="background:white;border:1px solid #e2e8f0;border-radius:10px;padding:12px;">
|
| 595 |
-
<div style="font-size:12px;color:#64748b;font-weight:700;letter-spacing:.4px;text-transform:uppercase;">Infection</div>
|
| 596 |
-
<div style="font-weight:700;color:#0f172a;">{html.escape(str(infection))}</div>
|
| 597 |
-
</div>
|
| 598 |
-
<div style="background:white;border:1px solid #e2e8f0;border-radius:10px;padding:12px;">
|
| 599 |
-
<div style="font-size:12px;color:#64748b;font-weight:700;letter-spacing:.4px;text-transform:uppercase;">AI Risk</div>
|
| 600 |
-
<div>{risk}</div>
|
| 601 |
-
</div>
|
| 602 |
-
</div>
|
| 603 |
-
{summary_block}
|
| 604 |
-
{img_block}
|
| 605 |
-
</div>
|
| 606 |
-
</div>
|
| 607 |
-
"""
|
| 608 |
-
items.append(card)
|
| 609 |
|
| 610 |
-
|
| 611 |
-
return head + "".join(items) + tail
|
| 612 |
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
patient_update = _load_patient_names()
|
| 619 |
-
return {
|
| 620 |
-
auth_panel: gr.update(visible=False),
|
| 621 |
-
practitioner_panel: gr.update(visible=True),
|
| 622 |
-
login_status: "<div class='status-success'>β
Login successful! Welcome to SmartHeal</div>",
|
| 623 |
-
patient_selector: patient_update
|
| 624 |
-
}
|
| 625 |
-
else:
|
| 626 |
-
return {login_status: "<div class='status-error'>β Invalid credentials. Please try again.</div>"}
|
| 627 |
|
| 628 |
-
def handle_signup(username, email, password, name, role,
|
| 629 |
try:
|
| 630 |
if role == "organization":
|
| 631 |
org_data = {
|
| 632 |
-
'org_name':
|
| 633 |
'email': email,
|
| 634 |
-
'phone':
|
| 635 |
-
'country_code':
|
| 636 |
-
'department':
|
| 637 |
-
'location':
|
| 638 |
}
|
| 639 |
org_id = self.database_manager.create_organization(org_data)
|
| 640 |
-
user_data = {'username': username, 'email': email, 'password': password, 'name': name, 'role': role, 'org_id': org_id}
|
| 641 |
else:
|
|
|
|
| 642 |
org_id = 1
|
| 643 |
-
user_data = {'username': username, 'email': email, 'password': password, 'name': name, 'role': role, 'org_id': org_id}
|
| 644 |
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
except Exception as e:
|
| 650 |
-
return
|
| 651 |
|
| 652 |
-
def
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
|
| 657 |
def handle_logout():
|
| 658 |
self.current_user = {}
|
| 659 |
-
return {
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 667 |
try:
|
| 668 |
-
if not
|
| 669 |
-
return "<div class='status-error'>β Please upload a wound image
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
'
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
}
|
| 688 |
|
| 689 |
-
|
|
|
|
| 690 |
if not response_id:
|
| 691 |
-
return "<div class='status-error'>β Could not save questionnaire
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 692 |
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
'date_of_injury': 'Unknown',
|
| 698 |
'professional_care': 'Yes',
|
| 699 |
-
'oozing_bleeding': 'Minor Oozing' if
|
| 700 |
-
'infection': 'Yes' if
|
| 701 |
-
'moisture':
|
| 702 |
-
'patient_name':
|
| 703 |
-
'patient_gender':
|
| 704 |
-
'wound_location':
|
| 705 |
-
'wound_duration':
|
| 706 |
-
'pain_level':
|
| 707 |
-
'previous_treatment':
|
| 708 |
-
'medical_history':
|
| 709 |
-
'medications':
|
| 710 |
-
'additional_notes':
|
| 711 |
}
|
| 712 |
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
|
|
|
| 717 |
|
|
|
|
| 718 |
try:
|
| 719 |
-
self.database_manager.
|
| 720 |
-
except Exception as
|
| 721 |
-
logging.error(f"
|
| 722 |
|
| 723 |
-
|
| 724 |
-
|
|
|
|
| 725 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
except Exception as e:
|
| 727 |
-
logging.
|
| 728 |
-
return f"<div class='status-error'>β System
|
| 729 |
|
| 730 |
-
|
| 731 |
-
def load_patient_history():
|
| 732 |
try:
|
| 733 |
-
|
| 734 |
-
if not
|
| 735 |
return "<div class='status-error'>β Please login first.</div>"
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
return
|
| 739 |
-
except Exception as e:
|
| 740 |
-
logging.error(f"Error loading patient history: {e}")
|
| 741 |
-
return f"<div class='status-error'>β Error loading history: {html.escape(str(e))}</div>"
|
| 742 |
-
|
| 743 |
-
def _load_patient_names():
|
| 744 |
-
try:
|
| 745 |
-
uid = self.current_user.get('id', 1)
|
| 746 |
-
plist = self.patient_history_manager.get_patient_list(uid) or []
|
| 747 |
-
names, seen = [], set()
|
| 748 |
-
for row in plist:
|
| 749 |
-
nm = row.get("patient_name")
|
| 750 |
-
if nm and nm not in seen:
|
| 751 |
-
names.append(nm); seen.add(nm)
|
| 752 |
-
if not names:
|
| 753 |
-
names = ["β No patients yet β"]
|
| 754 |
-
return gr.update(choices=names, value=(names[0] if names else None))
|
| 755 |
except Exception as e:
|
| 756 |
-
logging.error(f"
|
| 757 |
-
return
|
| 758 |
|
| 759 |
-
def
|
| 760 |
try:
|
| 761 |
-
uid = self.current_user.get(
|
| 762 |
-
if not
|
| 763 |
-
return "<div class='status-
|
| 764 |
-
|
| 765 |
-
|
|
|
|
|
|
|
|
|
|
| 766 |
except Exception as e:
|
| 767 |
-
logging.error(f"
|
| 768 |
-
return f"<div class='status-error'
|
| 769 |
|
| 770 |
-
def
|
| 771 |
try:
|
| 772 |
-
uid = self.current_user.get(
|
| 773 |
if not uid:
|
| 774 |
return "<div class='status-error'>β Please login first.</div>"
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
return self.patient_history_manager.
|
| 781 |
except Exception as e:
|
| 782 |
-
logging.error(f"
|
| 783 |
-
return f"<div class='status-error'>β Error
|
| 784 |
|
| 785 |
-
#
|
| 786 |
-
login_btn.click(
|
| 787 |
-
handle_login,
|
| 788 |
-
inputs=[login_username, login_password],
|
| 789 |
-
outputs=[auth_panel, practitioner_panel, login_status, patient_selector]
|
| 790 |
-
)
|
| 791 |
-
|
| 792 |
-
signup_btn.click(
|
| 793 |
-
handle_signup,
|
| 794 |
-
inputs=[
|
| 795 |
-
signup_username, signup_email, signup_password, signup_name, signup_role,
|
| 796 |
-
org_name, phone, country_code, department, location, organization_dropdown
|
| 797 |
-
],
|
| 798 |
-
outputs=[signup_status]
|
| 799 |
-
)
|
| 800 |
|
| 801 |
signup_role.change(
|
| 802 |
toggle_role_fields,
|
|
@@ -804,42 +814,43 @@ button.gr-button:hover, button.gr-button-primary:hover {
|
|
| 804 |
outputs=[org_fields, prac_fields]
|
| 805 |
)
|
| 806 |
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
inputs=[
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
medical_history, medications, allergies, additional_notes, wound_image
|
| 813 |
-
],
|
| 814 |
-
outputs=[analysis_output]
|
| 815 |
)
|
| 816 |
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
|
|
|
|
|
|
| 820 |
)
|
| 821 |
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
outputs=[patient_history_output]
|
| 825 |
-
)
|
| 826 |
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
|
|
|
| 830 |
)
|
| 831 |
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
inputs=[
|
| 835 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
)
|
| 837 |
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
outputs=[specific_patient_output]
|
| 842 |
-
)
|
| 843 |
|
| 844 |
return app
|
| 845 |
|
|
|
|
| 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">
|
|
|
|
| 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,
|
|
|
|
| 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 |
|