Spaces:
Sleeping
Sleeping
Update src/ui_components_original.py
Browse files- src/ui_components_original.py +343 -410
src/ui_components_original.py
CHANGED
|
@@ -376,12 +376,18 @@ button.gr-button:hover, button.gr-button-primary:hover {
|
|
| 376 |
"""
|
| 377 |
|
| 378 |
def create_interface(self):
|
| 379 |
-
"""Create the main Gradio interface with
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
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"
|
| 384 |
-
|
| 385 |
gr.HTML(f"""
|
| 386 |
<div class="medical-header">
|
| 387 |
<img src="{logo_url}" class="logo" alt="SmartHeal Logo">
|
|
@@ -391,525 +397,452 @@ button.gr-button:hover, button.gr-button-primary:hover {
|
|
| 391 |
</div>
|
| 392 |
</div>
|
| 393 |
""")
|
| 394 |
-
|
| 395 |
-
#
|
| 396 |
gr.HTML("""
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
# Main interface with conditional visibility
|
| 406 |
with gr.Row():
|
| 407 |
-
#
|
| 408 |
with gr.Column(visible=True) as auth_panel:
|
| 409 |
gr.HTML("""
|
| 410 |
<div style="text-align: center; margin: 40px 0;">
|
| 411 |
<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;">
|
| 412 |
-
<h2 style="color: white; font-size: 2.
|
| 413 |
-
<p style="color: rgba(255,255,255,0.
|
| 414 |
</div>
|
| 415 |
</div>
|
| 416 |
""")
|
| 417 |
-
|
| 418 |
with gr.Tabs():
|
| 419 |
-
with gr.Tab("π Professional Login")
|
| 420 |
-
gr.
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
""")
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
)
|
| 433 |
-
login_password = gr.Textbox(
|
| 434 |
-
label="π Password",
|
| 435 |
-
type="password",
|
| 436 |
-
placeholder="Enter your secure password"
|
| 437 |
-
)
|
| 438 |
-
|
| 439 |
-
login_btn = gr.Button(
|
| 440 |
-
"π Sign In to Dashboard",
|
| 441 |
-
variant="primary",
|
| 442 |
-
size="lg"
|
| 443 |
-
)
|
| 444 |
-
|
| 445 |
-
login_status = gr.HTML(
|
| 446 |
-
value="<div style='text-align: center; color: #718096; font-size: 0.9rem; margin-top: 15px;'>Enter your credentials to access the system</div>"
|
| 447 |
-
)
|
| 448 |
-
|
| 449 |
-
with gr.Tab("π New Registration") as signup_tab:
|
| 450 |
-
gr.HTML("""
|
| 451 |
-
<div style="background: white; padding: 40px; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); margin: 20px auto; max-width: 450px;">
|
| 452 |
-
<div style="text-align: center; margin-bottom: 30px;">
|
| 453 |
-
<h3 style="color: #2d3748; font-size: 1.8rem; margin-bottom: 8px;">Create Account</h3>
|
| 454 |
-
<p style="color: #718096; font-size: 1rem;">Join the SmartHeal healthcare network</p>
|
| 455 |
-
</div>
|
| 456 |
-
</div>
|
| 457 |
-
""")
|
| 458 |
-
|
| 459 |
-
signup_username = gr.Textbox(
|
| 460 |
-
label="π€ Username",
|
| 461 |
-
placeholder="Choose a unique username"
|
| 462 |
-
)
|
| 463 |
-
signup_email = gr.Textbox(
|
| 464 |
-
label="π§ Email Address",
|
| 465 |
-
placeholder="Enter your professional email"
|
| 466 |
-
)
|
| 467 |
-
signup_password = gr.Textbox(
|
| 468 |
-
label="π Password",
|
| 469 |
-
type="password",
|
| 470 |
-
placeholder="Create a strong password"
|
| 471 |
-
)
|
| 472 |
-
signup_name = gr.Textbox(
|
| 473 |
-
label="π¨ββοΈ Full Name",
|
| 474 |
-
placeholder="Enter your full professional name"
|
| 475 |
-
)
|
| 476 |
-
signup_role = gr.Radio(
|
| 477 |
-
["practitioner", "organization"],
|
| 478 |
-
label="π₯ Account Type",
|
| 479 |
-
value="practitioner"
|
| 480 |
-
)
|
| 481 |
-
|
| 482 |
-
# Organization-specific fields
|
| 483 |
with gr.Group(visible=False) as org_fields:
|
| 484 |
-
gr.HTML("<h4 style='color
|
| 485 |
org_name = gr.Textbox(label="Organization Name", placeholder="Enter organization name")
|
| 486 |
phone = gr.Textbox(label="Phone Number", placeholder="Enter contact number")
|
| 487 |
country_code = gr.Textbox(label="Country Code", placeholder="e.g., +1, +44")
|
| 488 |
department = gr.Textbox(label="Department", placeholder="e.g., Emergency, Surgery")
|
| 489 |
location = gr.Textbox(label="Location", placeholder="City, State/Province, Country")
|
| 490 |
-
|
| 491 |
-
# Practitioner-specific fields
|
| 492 |
with gr.Group(visible=True) as prac_fields:
|
| 493 |
-
gr.HTML("<h4 style='color
|
| 494 |
-
organization_dropdown = gr.Dropdown(
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
"β¨ Create Professional Account",
|
| 501 |
-
variant="primary",
|
| 502 |
-
size="lg"
|
| 503 |
-
)
|
| 504 |
-
|
| 505 |
-
signup_status = gr.HTML(
|
| 506 |
-
value="<div style='text-align: center; color: #718096; font-size: 0.9rem; margin-top: 15px;'>Fill in your details to create an account</div>"
|
| 507 |
-
)
|
| 508 |
-
|
| 509 |
-
# Practitioner Interface (hidden initially)
|
| 510 |
with gr.Column(visible=False) as practitioner_panel:
|
| 511 |
-
gr.HTML('<div class="medical-card-title">π©ββοΈ Practitioner Dashboard</div>')
|
| 512 |
-
|
| 513 |
user_info = gr.HTML("")
|
| 514 |
logout_btn_prac = gr.Button("πͺ Logout", variant="secondary")
|
| 515 |
-
|
| 516 |
-
# Main tabs for different functions
|
| 517 |
with gr.Tabs():
|
| 518 |
-
# WOUND ANALYSIS TAB
|
| 519 |
with gr.Tab("π¬ Wound Analysis"):
|
| 520 |
with gr.Row():
|
| 521 |
with gr.Column(scale=1):
|
| 522 |
gr.HTML("<h3>π Patient Information</h3>")
|
| 523 |
patient_name = gr.Textbox(label="Patient Name", placeholder="Enter patient's full name")
|
| 524 |
patient_age = gr.Number(label="Age", value=30, minimum=0, maximum=120)
|
| 525 |
-
patient_gender = gr.Dropdown(
|
| 526 |
-
|
| 527 |
-
label="Gender",
|
| 528 |
-
value="Male"
|
| 529 |
-
)
|
| 530 |
-
|
| 531 |
gr.HTML("<h3>π©Ή Wound Information</h3>")
|
| 532 |
wound_location = gr.Textbox(label="Wound Location", placeholder="e.g., Left ankle, Right arm")
|
| 533 |
wound_duration = gr.Textbox(label="Wound Duration", placeholder="e.g., 2 weeks, 1 month")
|
| 534 |
-
pain_level = gr.Slider(
|
| 535 |
-
|
| 536 |
-
label="Pain Level (0-10)"
|
| 537 |
-
)
|
| 538 |
-
|
| 539 |
gr.HTML("<h3>βοΈ Clinical Assessment</h3>")
|
| 540 |
-
moisture_level = gr.Dropdown(
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
)
|
| 545 |
-
infection_signs = gr.Dropdown(
|
| 546 |
-
choices=["None", "Mild", "Moderate", "Severe"],
|
| 547 |
-
label="Signs of Infection",
|
| 548 |
-
value="None"
|
| 549 |
-
)
|
| 550 |
-
diabetic_status = gr.Dropdown(
|
| 551 |
-
choices=["Non-diabetic", "Type 1", "Type 2", "Gestational"],
|
| 552 |
-
label="Diabetic Status",
|
| 553 |
-
value="Non-diabetic"
|
| 554 |
-
)
|
| 555 |
-
|
| 556 |
with gr.Column(scale=1):
|
| 557 |
gr.HTML("<h3>πΈ Wound Image Upload</h3>")
|
| 558 |
-
wound_image = gr.Image(
|
| 559 |
-
|
| 560 |
-
type="filepath"
|
| 561 |
-
)
|
| 562 |
-
|
| 563 |
gr.HTML("<h3>π Medical History</h3>")
|
| 564 |
-
previous_treatment = gr.Textbox(
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
)
|
| 569 |
-
|
| 570 |
-
label="Medical History",
|
| 571 |
-
placeholder="Relevant medical conditions, surgeries, etc...",
|
| 572 |
-
lines=3
|
| 573 |
-
)
|
| 574 |
-
medications = gr.Textbox(
|
| 575 |
-
label="Current Medications",
|
| 576 |
-
placeholder="List current medications...",
|
| 577 |
-
lines=2
|
| 578 |
-
)
|
| 579 |
-
allergies = gr.Textbox(
|
| 580 |
-
label="Known Allergies",
|
| 581 |
-
placeholder="List any known allergies...",
|
| 582 |
-
lines=2
|
| 583 |
-
)
|
| 584 |
-
additional_notes = gr.Textbox(
|
| 585 |
-
label="Additional Notes",
|
| 586 |
-
placeholder="Any additional clinical observations...",
|
| 587 |
-
lines=3
|
| 588 |
-
)
|
| 589 |
-
|
| 590 |
analyze_btn = gr.Button("π¬ Analyze Wound", variant="primary", size="lg", elem_id="analyze-btn")
|
| 591 |
analysis_output = gr.HTML("")
|
| 592 |
-
|
| 593 |
-
# PATIENT HISTORY TAB
|
| 594 |
with gr.Tab("π Patient History"):
|
| 595 |
with gr.Row():
|
| 596 |
with gr.Column(scale=2):
|
| 597 |
gr.HTML("<h3>π Patient History Dashboard</h3>")
|
| 598 |
history_btn = gr.Button("π Load Patient History", variant="primary")
|
| 599 |
patient_history_output = gr.HTML("")
|
| 600 |
-
|
| 601 |
with gr.Column(scale=1):
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
)
|
| 607 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
specific_patient_output = gr.HTML("")
|
| 609 |
-
|
| 610 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
def handle_login(username, password):
|
| 612 |
user_data = self.auth_manager.authenticate_user(username, password)
|
| 613 |
if user_data:
|
| 614 |
self.current_user = user_data
|
|
|
|
| 615 |
return {
|
| 616 |
auth_panel: gr.update(visible=False),
|
| 617 |
practitioner_panel: gr.update(visible=True),
|
| 618 |
-
login_status: "<div class='status-success'>β
Login successful! Welcome to SmartHeal</div>"
|
|
|
|
| 619 |
}
|
| 620 |
else:
|
| 621 |
-
return {
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
def handle_signup(username, email, password, name, role, org_name, phone, country_code, department, location, organization_dropdown):
|
| 626 |
try:
|
| 627 |
if role == "organization":
|
| 628 |
org_data = {
|
| 629 |
-
'org_name':
|
| 630 |
-
'phone': phone,
|
| 631 |
-
'country_code': country_code,
|
| 632 |
-
'department': department,
|
| 633 |
-
'location': location
|
| 634 |
-
}
|
| 635 |
-
org_id = self.database_manager.create_organization(org_data)
|
| 636 |
-
|
| 637 |
-
user_data = {
|
| 638 |
-
'username': username,
|
| 639 |
'email': email,
|
| 640 |
-
'
|
| 641 |
-
'
|
| 642 |
-
'
|
| 643 |
-
'
|
| 644 |
}
|
|
|
|
|
|
|
| 645 |
else:
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
'username': username,
|
| 650 |
-
'email': email,
|
| 651 |
-
'password': password,
|
| 652 |
-
'name': name,
|
| 653 |
-
'role': role,
|
| 654 |
-
'org_id': org_id
|
| 655 |
-
}
|
| 656 |
-
|
| 657 |
if self.auth_manager.create_user(user_data):
|
| 658 |
-
return {
|
| 659 |
-
signup_status: "<div class='status-success'>β
Account created successfully! Please login.</div>"
|
| 660 |
-
}
|
| 661 |
else:
|
| 662 |
-
return {
|
| 663 |
-
signup_status: "<div class='status-error'>β Failed to create account. Username or email may already exist.</div>"
|
| 664 |
-
}
|
| 665 |
except Exception as e:
|
| 666 |
-
return {
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 673 |
try:
|
| 674 |
-
if not
|
| 675 |
return "<div class='status-error'>β Please upload a wound image for analysis.</div>"
|
| 676 |
-
|
| 677 |
-
# Show loading state first
|
| 678 |
-
loading_html = """
|
| 679 |
-
<div style="text-align:center; padding: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 16px; color: white; margin: 20px 0;">
|
| 680 |
-
<div style="display:inline-block; border:4px solid rgba(255,255,255,0.3); border-radius:50%; border-top-color:white; width:50px; height:50px; animation:spin 1s linear infinite; margin-bottom: 20px;"></div>
|
| 681 |
-
<h3 style="margin: 0; font-size: 1.5rem;">π¬ SmartHeal AI Processing</h3>
|
| 682 |
-
<p style="margin: 10px 0 0 0; font-size: 1rem; opacity: 0.9;">Analyzing wound image with advanced computer vision...</p>
|
| 683 |
-
<div style="background: rgba(255,255,255,0.1); border-radius: 8px; padding: 10px; margin-top: 15px;">
|
| 684 |
-
<p style="margin: 0; font-size: 0.9rem;">β‘ Detection β π Segmentation β π€ AI Report</p>
|
| 685 |
-
</div>
|
| 686 |
-
<style>@keyframes spin {0% {transform:rotate(0deg)} 100% {transform:rotate(360deg)}}</style>
|
| 687 |
-
</div>
|
| 688 |
-
"""
|
| 689 |
-
|
| 690 |
-
# 1. Save questionnaire FIRST to get valid ID
|
| 691 |
questionnaire_data_for_db = {
|
| 692 |
-
'user_id': self.current_user.get('id', 1),
|
| 693 |
-
'patient_name':
|
| 694 |
-
'patient_age':
|
| 695 |
-
'patient_gender':
|
| 696 |
-
'wound_location':
|
| 697 |
-
'wound_duration':
|
| 698 |
-
'pain_level':
|
| 699 |
-
'moisture_level':
|
| 700 |
-
'infection_signs':
|
| 701 |
-
'diabetic_status':
|
| 702 |
-
'previous_treatment':
|
| 703 |
-
'medical_history':
|
| 704 |
-
'medications':
|
| 705 |
-
'allergies':
|
| 706 |
-
'additional_notes':
|
| 707 |
}
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
# 2. Create AIProcessor questionnaire format
|
| 714 |
questionnaire_data_for_ai = {
|
| 715 |
-
'age':
|
| 716 |
-
'diabetic': 'Yes' if
|
| 717 |
-
'allergies':
|
| 718 |
-
'date_of_injury': 'Unknown',
|
| 719 |
-
'professional_care': 'Yes',
|
| 720 |
-
'oozing_bleeding': 'Minor Oozing' if
|
| 721 |
-
'infection': 'Yes' if
|
| 722 |
-
'moisture':
|
| 723 |
-
|
| 724 |
-
'
|
| 725 |
-
'
|
| 726 |
-
'
|
| 727 |
-
'
|
| 728 |
-
'
|
| 729 |
-
'
|
| 730 |
-
'
|
| 731 |
-
'
|
| 732 |
-
'additional_notes': additional_notes
|
| 733 |
}
|
| 734 |
-
|
| 735 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
try:
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
logging.error(f"β AI Analysis failed: {error_msg}")
|
| 745 |
-
|
| 746 |
-
return f"""
|
| 747 |
-
<div class='status-error' style='padding: 30px; background: #fff5f5; border-left: 5px solid #e53e3e; margin: 20px 0; border-radius: 12px;'>
|
| 748 |
-
<h3 style="color: #c53030; margin-top: 0;">β AI Analysis Error</h3>
|
| 749 |
-
<p style="color: #742a2a; font-size: 1.1rem;"><strong>Error Details:</strong></p>
|
| 750 |
-
<div style='background: #fed7d7; padding: 15px; border-radius: 8px; margin: 10px 0;'>
|
| 751 |
-
<code style='color: #742a2a; font-size: 0.9rem;'>{error_msg}</code>
|
| 752 |
-
</div>
|
| 753 |
-
<p style="color: #742a2a;">Please try again with a different image or contact technical support.</p>
|
| 754 |
-
<div style="margin-top: 15px; padding: 15px; background: #fff; border-radius: 8px; border: 1px solid #feb2b2;">
|
| 755 |
-
<strong>Troubleshooting Tips:</strong>
|
| 756 |
-
<ul style="margin: 10px 0; color: #742a2a;">
|
| 757 |
-
<li>Ensure image is clear and well-lit</li>
|
| 758 |
-
<li>Wound should be clearly visible in the image</li>
|
| 759 |
-
<li>Try a different image format (PNG, JPG)</li>
|
| 760 |
-
<li>Check image file size (max 10MB recommended)</li>
|
| 761 |
-
</ul>
|
| 762 |
-
</div>
|
| 763 |
-
</div>
|
| 764 |
-
"""
|
| 765 |
-
|
| 766 |
-
logging.info("β
AI Analysis completed successfully")
|
| 767 |
-
|
| 768 |
-
# 4. Save analysis result with valid questionnaire_response_id
|
| 769 |
-
try:
|
| 770 |
-
if questionnaire_response_id:
|
| 771 |
-
self.database_manager.save_analysis_result(questionnaire_response_id, analysis_result)
|
| 772 |
-
logging.info("β
Analysis result saved to database")
|
| 773 |
-
except Exception as db_error:
|
| 774 |
-
logging.error(f"β Database save error: {db_error}")
|
| 775 |
-
# Continue with display even if DB save fails
|
| 776 |
-
|
| 777 |
-
# 5. Format comprehensive analysis results with all images
|
| 778 |
-
formatted_analysis = self._format_comprehensive_analysis_results(
|
| 779 |
-
analysis_result, wound_image, questionnaire_data_for_ai
|
| 780 |
-
)
|
| 781 |
-
|
| 782 |
-
return formatted_analysis
|
| 783 |
-
|
| 784 |
-
except Exception as analysis_error:
|
| 785 |
-
logging.error(f"β AI analysis exception: {analysis_error}", exc_info=True)
|
| 786 |
-
return f"""
|
| 787 |
-
<div class='status-error' style='padding: 30px; background: #fff5f5; border-left: 5px solid #e53e3e; margin: 20px 0; border-radius: 12px;'>
|
| 788 |
-
<h3 style="color: #c53030; margin-top: 0;">β Analysis Processing Error</h3>
|
| 789 |
-
<p style="color: #742a2a;">There was an unexpected error during wound analysis:</p>
|
| 790 |
-
<div style='background: #fed7d7; padding: 15px; border-radius: 8px; margin: 15px 0;'>
|
| 791 |
-
<code style='color: #742a2a; font-size: 0.9rem; word-break: break-word;'>{str(analysis_error)}</code>
|
| 792 |
-
</div>
|
| 793 |
-
<p style="color: #742a2a;"><strong>Next Steps:</strong></p>
|
| 794 |
-
<ul style="color: #742a2a; margin: 10px 0;">
|
| 795 |
-
<li>Refresh the page and try again</li>
|
| 796 |
-
<li>Check your internet connection</li>
|
| 797 |
-
<li>Try with a different wound image</li>
|
| 798 |
-
<li>Contact system administrator if the problem persists</li>
|
| 799 |
-
</ul>
|
| 800 |
-
</div>
|
| 801 |
-
"""
|
| 802 |
-
|
| 803 |
except Exception as e:
|
| 804 |
logging.error(f"β Analysis handler error: {e}", exc_info=True)
|
| 805 |
-
return f""
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
<p style="color: #742a2a;">A system error occurred while processing your request:</p>
|
| 809 |
-
<div style='background: #fed7d7; padding: 15px; border-radius: 8px; margin: 15px 0;'>
|
| 810 |
-
<code style='color: #742a2a; font-size: 0.9rem;'>{str(e)}</code>
|
| 811 |
-
</div>
|
| 812 |
-
<p style="color: #742a2a;">Please contact technical support if this issue continues.</p>
|
| 813 |
-
</div>
|
| 814 |
-
"""
|
| 815 |
-
|
| 816 |
-
def handle_logout():
|
| 817 |
-
self.current_user = {}
|
| 818 |
-
return {
|
| 819 |
-
auth_panel: gr.update(visible=True),
|
| 820 |
-
practitioner_panel: gr.update(visible=False)
|
| 821 |
-
}
|
| 822 |
-
|
| 823 |
-
def toggle_role_fields(role):
|
| 824 |
-
if role == "organization":
|
| 825 |
-
return {
|
| 826 |
-
org_fields: gr.update(visible=True),
|
| 827 |
-
prac_fields: gr.update(visible=False)
|
| 828 |
-
}
|
| 829 |
-
else:
|
| 830 |
-
return {
|
| 831 |
-
org_fields: gr.update(visible=False),
|
| 832 |
-
prac_fields: gr.update(visible=True)
|
| 833 |
-
}
|
| 834 |
-
|
| 835 |
def load_patient_history():
|
| 836 |
try:
|
| 837 |
user_id = self.current_user.get('id', 1)
|
| 838 |
if not user_id:
|
| 839 |
return "<div class='status-error'>β Please login first.</div>"
|
| 840 |
-
|
| 841 |
history_data = self.patient_history_manager.get_user_patient_history(user_id)
|
| 842 |
-
|
| 843 |
-
return
|
| 844 |
except Exception as e:
|
| 845 |
logging.error(f"Error loading patient history: {e}")
|
| 846 |
-
return f"<div class='status-error'>β Error loading history: {str(e)}</div>"
|
| 847 |
-
|
| 848 |
-
def
|
| 849 |
try:
|
| 850 |
-
|
| 851 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 852 |
return "<div class='status-error'>β Please login first.</div>"
|
| 853 |
-
|
| 854 |
-
if not patient_name.strip():
|
| 855 |
return "<div class='status-warning'>β οΈ Please enter a patient name to search.</div>"
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
return formatted_data
|
| 861 |
-
else:
|
| 862 |
-
return f"<div class='status-warning'>β οΈ No records found for patient: {patient_name}</div>"
|
| 863 |
-
|
| 864 |
except Exception as e:
|
| 865 |
logging.error(f"Error searching patient: {e}")
|
| 866 |
-
return f"<div class='status-error'>β Error searching patient: {str(e)}</div>"
|
| 867 |
-
|
| 868 |
-
#
|
| 869 |
login_btn.click(
|
| 870 |
handle_login,
|
| 871 |
inputs=[login_username, login_password],
|
| 872 |
-
outputs=[auth_panel, practitioner_panel, login_status]
|
| 873 |
)
|
| 874 |
-
|
| 875 |
signup_btn.click(
|
| 876 |
handle_signup,
|
| 877 |
-
inputs=[
|
| 878 |
-
|
|
|
|
|
|
|
| 879 |
outputs=[signup_status]
|
| 880 |
)
|
| 881 |
-
|
| 882 |
signup_role.change(
|
| 883 |
toggle_role_fields,
|
| 884 |
inputs=[signup_role],
|
| 885 |
outputs=[org_fields, prac_fields]
|
| 886 |
)
|
| 887 |
-
|
| 888 |
analyze_btn.click(
|
| 889 |
handle_analysis,
|
| 890 |
-
inputs=[
|
| 891 |
-
|
| 892 |
-
|
|
|
|
|
|
|
| 893 |
outputs=[analysis_output]
|
| 894 |
)
|
| 895 |
-
|
| 896 |
logout_btn_prac.click(
|
| 897 |
handle_logout,
|
| 898 |
outputs=[auth_panel, practitioner_panel]
|
| 899 |
)
|
| 900 |
-
|
| 901 |
history_btn.click(
|
| 902 |
load_patient_history,
|
| 903 |
outputs=[patient_history_output]
|
| 904 |
)
|
| 905 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 906 |
search_patient_btn.click(
|
| 907 |
-
|
| 908 |
inputs=[search_patient_name],
|
| 909 |
outputs=[specific_patient_output]
|
| 910 |
)
|
| 911 |
-
|
| 912 |
-
return app
|
|
|
|
| 913 |
|
| 914 |
def _format_comprehensive_analysis_results(self, analysis_result, image_url=None, questionnaire_data=None):
|
| 915 |
"""Format comprehensive analysis results with all visualization images from AIProcessor."""
|
|
|
|
| 376 |
"""
|
| 377 |
|
| 378 |
def create_interface(self):
|
| 379 |
+
"""Create the main Gradio interface with View Details + Search Specific Patient."""
|
| 380 |
+
import html
|
| 381 |
+
import gradio as gr
|
| 382 |
+
import os
|
| 383 |
+
import logging
|
| 384 |
+
from datetime import datetime
|
| 385 |
+
|
| 386 |
+
with gr.Blocks(css=self.get_custom_css(), title="SmartHeal - SmartHeal AI Wound Care Assistant") as app:
|
| 387 |
+
|
| 388 |
+
# -------------------------- HEADER --------------------------
|
| 389 |
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"
|
| 390 |
+
|
| 391 |
gr.HTML(f"""
|
| 392 |
<div class="medical-header">
|
| 393 |
<img src="{logo_url}" class="logo" alt="SmartHeal Logo">
|
|
|
|
| 397 |
</div>
|
| 398 |
</div>
|
| 399 |
""")
|
| 400 |
+
|
| 401 |
+
# Disclaimer
|
| 402 |
gr.HTML("""
|
| 403 |
+
<div style="border: 2px solid #FF6B6B; background-color: #FFE5E5; padding: 15px; border-radius: 12px; margin: 10px 0;">
|
| 404 |
+
<h3 style="color: #D63031; margin-top: 0;">β οΈ IMPORTANT DISCLAIMER</h3>
|
| 405 |
+
<p><strong>This model is for testing and educational purposes only and is NOT a replacement for professional medical advice.</strong></p>
|
| 406 |
+
<p>Information generated may be inaccurate. Always consult a qualified healthcare provider for medical concerns.</p>
|
| 407 |
+
</div>
|
| 408 |
+
""")
|
| 409 |
+
|
| 410 |
+
# -------------------------- LAYOUT --------------------------
|
|
|
|
| 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", placeholder="Enter your username")
|
| 426 |
+
login_password = gr.Textbox(label="π Password", type="password", placeholder="Enter your secure password")
|
| 427 |
+
login_btn = gr.Button("π Sign In to Dashboard", variant="primary")
|
| 428 |
+
login_status = gr.HTML("<div style='text-align:center;color:#718096;font-size:0.9rem;margin-top:8px;'>Enter your credentials to access the system</div>")
|
| 429 |
+
|
| 430 |
+
with gr.Tab("π New Registration"):
|
| 431 |
+
signup_username = gr.Textbox(label="π€ Username", placeholder="Choose a unique username")
|
| 432 |
+
signup_email = gr.Textbox(label="π§ Email Address", placeholder="Enter your professional email")
|
| 433 |
+
signup_password = gr.Textbox(label="π Password", type="password", placeholder="Create a strong password")
|
| 434 |
+
signup_name = gr.Textbox(label="π¨ββοΈ Full Name", placeholder="Enter your full professional name")
|
| 435 |
+
signup_role = gr.Radio(["practitioner", "organization"], label="π₯ Account Type", value="practitioner")
|
| 436 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
with gr.Group(visible=False) as org_fields:
|
| 438 |
+
gr.HTML("<h4 style='color:#2d3748;margin: 12px 0 8px 0;'>π’ Organization Details</h4>")
|
| 439 |
org_name = gr.Textbox(label="Organization Name", placeholder="Enter organization name")
|
| 440 |
phone = gr.Textbox(label="Phone Number", placeholder="Enter contact number")
|
| 441 |
country_code = gr.Textbox(label="Country Code", placeholder="e.g., +1, +44")
|
| 442 |
department = gr.Textbox(label="Department", placeholder="e.g., Emergency, Surgery")
|
| 443 |
location = gr.Textbox(label="Location", placeholder="City, State/Province, Country")
|
| 444 |
+
|
|
|
|
| 445 |
with gr.Group(visible=True) as prac_fields:
|
| 446 |
+
gr.HTML("<h4 style='color:#2d3748;margin: 12px 0 8px 0;'>π₯ Affiliation</h4>")
|
| 447 |
+
organization_dropdown = gr.Dropdown(choices=self.get_organizations_dropdown(), label="Select Your Organization")
|
| 448 |
+
|
| 449 |
+
signup_btn = gr.Button("β¨ Create Professional Account", variant="primary")
|
| 450 |
+
signup_status = gr.HTML("<div style='text-align:center;color:#718096;font-size:0.9rem;margin-top:8px;'>Fill in your details to create an account</div>")
|
| 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 |
+
# ------------- WOUND ANALYSIS TAB -------------
|
| 460 |
with gr.Tab("π¬ Wound Analysis"):
|
| 461 |
with gr.Row():
|
| 462 |
with gr.Column(scale=1):
|
| 463 |
gr.HTML("<h3>π Patient Information</h3>")
|
| 464 |
patient_name = gr.Textbox(label="Patient Name", placeholder="Enter patient's full name")
|
| 465 |
patient_age = gr.Number(label="Age", value=30, minimum=0, maximum=120)
|
| 466 |
+
patient_gender = gr.Dropdown(choices=["Male", "Female", "Other"], label="Gender", value="Male")
|
| 467 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
gr.HTML("<h3>π©Ή Wound Information</h3>")
|
| 469 |
wound_location = gr.Textbox(label="Wound Location", placeholder="e.g., Left ankle, Right arm")
|
| 470 |
wound_duration = gr.Textbox(label="Wound Duration", placeholder="e.g., 2 weeks, 1 month")
|
| 471 |
+
pain_level = gr.Slider(minimum=0, maximum=10, value=5, step=1, label="Pain Level (0-10)")
|
| 472 |
+
|
|
|
|
|
|
|
|
|
|
| 473 |
gr.HTML("<h3>βοΈ Clinical Assessment</h3>")
|
| 474 |
+
moisture_level = gr.Dropdown(choices=["Dry", "Moist", "Wet", "Saturated"], label="Moisture Level", value="Moist")
|
| 475 |
+
infection_signs = gr.Dropdown(choices=["None", "Mild", "Moderate", "Severe"], label="Signs of Infection", value="None")
|
| 476 |
+
diabetic_status = gr.Dropdown(choices=["Non-diabetic", "Type 1", "Type 2", "Gestational"], label="Diabetic Status", value="Non-diabetic")
|
| 477 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
with gr.Column(scale=1):
|
| 479 |
gr.HTML("<h3>πΈ Wound Image Upload</h3>")
|
| 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", placeholder="Describe any previous treatments...", lines=3)
|
| 484 |
+
medical_history = gr.Textbox(label="Medical History", placeholder="Relevant medical conditions, surgeries, etc...", lines=3)
|
| 485 |
+
medications = gr.Textbox(label="Current Medications", placeholder="List current medications...", lines=2)
|
| 486 |
+
allergies = gr.Textbox(label="Known Allergies", placeholder="List any known allergies...", lines=2)
|
| 487 |
+
additional_notes = gr.Textbox(label="Additional Notes", placeholder="Any additional clinical observations...", lines=3)
|
| 488 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
analyze_btn = gr.Button("π¬ Analyze Wound", variant="primary", size="lg", elem_id="analyze-btn")
|
| 490 |
analysis_output = gr.HTML("")
|
| 491 |
+
|
| 492 |
+
# ------------- PATIENT HISTORY TAB -------------
|
| 493 |
with gr.Tab("π Patient History"):
|
| 494 |
with gr.Row():
|
| 495 |
with gr.Column(scale=2):
|
| 496 |
gr.HTML("<h3>π Patient History Dashboard</h3>")
|
| 497 |
history_btn = gr.Button("π Load Patient History", variant="primary")
|
| 498 |
patient_history_output = gr.HTML("")
|
| 499 |
+
|
| 500 |
with gr.Column(scale=1):
|
| 501 |
+
# View Details (dropdown)
|
| 502 |
+
gr.HTML("<h3>π View Details</h3>")
|
| 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 |
+
# --------------------- HELPERS & HANDLERS ---------------------
|
| 521 |
+
|
| 522 |
+
def _risk_chip_ui(level: str) -> str:
|
| 523 |
+
rl = (level or "Unknown").strip().lower()
|
| 524 |
+
bg, fg = "#f0f0f0", "#333333"
|
| 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 |
+
card = f"""
|
| 571 |
+
<div style="display:grid;grid-template-columns:32px 1fr;gap:16px;position:relative;margin-bottom:22px;">
|
| 572 |
+
<div style="display:flex;align-items:center;justify-content:center;">
|
| 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 |
+
tail = "</div></div>"
|
| 611 |
+
return head + "".join(items) + tail
|
| 612 |
+
|
| 613 |
+
# ---------- AUTH HANDLERS ----------
|
| 614 |
def handle_login(username, password):
|
| 615 |
user_data = self.auth_manager.authenticate_user(username, password)
|
| 616 |
if user_data:
|
| 617 |
self.current_user = user_data
|
| 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, org_name_val, phone_val, cc_val, dept_val, loc_val, org_dropdown_val):
|
|
|
|
|
|
|
| 629 |
try:
|
| 630 |
if role == "organization":
|
| 631 |
org_data = {
|
| 632 |
+
'org_name': org_name_val,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
'email': email,
|
| 634 |
+
'phone': phone_val,
|
| 635 |
+
'country_code': cc_val,
|
| 636 |
+
'department': dept_val,
|
| 637 |
+
'location': loc_val
|
| 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 |
if self.auth_manager.create_user(user_data):
|
| 646 |
+
return {signup_status: "<div class='status-success'>β
Account created successfully! Please login.</div>"}
|
|
|
|
|
|
|
| 647 |
else:
|
| 648 |
+
return {signup_status: "<div class='status-error'>β Failed to create account. Username or email may already exist.</div>"}
|
|
|
|
|
|
|
| 649 |
except Exception as e:
|
| 650 |
+
return {signup_status: f"<div class='status-error'>β Error: {html.escape(str(e))}</div>"}
|
| 651 |
+
|
| 652 |
+
def toggle_role_fields(role):
|
| 653 |
+
if role == "organization":
|
| 654 |
+
return {org_fields: gr.update(visible=True), prac_fields: gr.update(visible=False)}
|
| 655 |
+
return {org_fields: gr.update(visible=False), prac_fields: gr.update(visible=True)}
|
| 656 |
+
|
| 657 |
+
def handle_logout():
|
| 658 |
+
self.current_user = {}
|
| 659 |
+
return {auth_panel: gr.update(visible=True), practitioner_panel: gr.update(visible=False)}
|
| 660 |
+
|
| 661 |
+
# ---------- ANALYSIS HANDLER ----------
|
| 662 |
+
def handle_analysis(
|
| 663 |
+
_patient_name, _patient_age, _patient_gender, _wound_location, _wound_duration,
|
| 664 |
+
_pain_level, _moisture_level, _infection_signs, _diabetic_status, _previous_treatment,
|
| 665 |
+
_medical_history, _medications, _allergies, _additional_notes, _wound_image
|
| 666 |
+
):
|
| 667 |
try:
|
| 668 |
+
if not _wound_image:
|
| 669 |
return "<div class='status-error'>β Please upload a wound image for analysis.</div>"
|
| 670 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
questionnaire_data_for_db = {
|
| 672 |
+
'user_id': self.current_user.get('id', 1),
|
| 673 |
+
'patient_name': _patient_name,
|
| 674 |
+
'patient_age': _patient_age,
|
| 675 |
+
'patient_gender': _patient_gender,
|
| 676 |
+
'wound_location': _wound_location,
|
| 677 |
+
'wound_duration': _wound_duration,
|
| 678 |
+
'pain_level': _pain_level,
|
| 679 |
+
'moisture_level': _moisture_level,
|
| 680 |
+
'infection_signs': _infection_signs,
|
| 681 |
+
'diabetic_status': _diabetic_status,
|
| 682 |
+
'previous_treatment': _previous_treatment,
|
| 683 |
+
'medical_history': _medical_history,
|
| 684 |
+
'medications': _medications,
|
| 685 |
+
'allergies': _allergies,
|
| 686 |
+
'additional_notes': _additional_notes
|
| 687 |
}
|
| 688 |
+
|
| 689 |
+
response_id = self.database_manager.save_questionnaire(questionnaire_data_for_db)
|
| 690 |
+
if not response_id:
|
| 691 |
+
return "<div class='status-error'>β Could not save questionnaire (patient/profile). Check logs.</div>"
|
| 692 |
+
|
|
|
|
| 693 |
questionnaire_data_for_ai = {
|
| 694 |
+
'age': _patient_age,
|
| 695 |
+
'diabetic': 'Yes' if _diabetic_status != 'Non-diabetic' else 'No',
|
| 696 |
+
'allergies': _allergies,
|
| 697 |
+
'date_of_injury': 'Unknown',
|
| 698 |
+
'professional_care': 'Yes',
|
| 699 |
+
'oozing_bleeding': 'Minor Oozing' if _infection_signs != 'None' else 'None',
|
| 700 |
+
'infection': 'Yes' if _infection_signs != 'None' else 'No',
|
| 701 |
+
'moisture': _moisture_level,
|
| 702 |
+
'patient_name': _patient_name,
|
| 703 |
+
'patient_gender': _patient_gender,
|
| 704 |
+
'wound_location': _wound_location,
|
| 705 |
+
'wound_duration': _wound_duration,
|
| 706 |
+
'pain_level': _pain_level,
|
| 707 |
+
'previous_treatment': _previous_treatment,
|
| 708 |
+
'medical_history': _medical_history,
|
| 709 |
+
'medications': _medications,
|
| 710 |
+
'additional_notes': _additional_notes
|
|
|
|
| 711 |
}
|
| 712 |
+
|
| 713 |
+
analysis_result = self.wound_analyzer.analyze_wound(_wound_image, questionnaire_data_for_ai)
|
| 714 |
+
if not analysis_result.get('success'):
|
| 715 |
+
err = analysis_result.get('error', 'Analysis failed')
|
| 716 |
+
return f"<div class='status-error'>β AI Analysis Error: {html.escape(str(err))}</div>"
|
| 717 |
+
|
| 718 |
try:
|
| 719 |
+
self.database_manager.save_analysis_result(response_id, analysis_result)
|
| 720 |
+
except Exception as db_error:
|
| 721 |
+
logging.error(f"DB save (analysis_result) error: {db_error}")
|
| 722 |
+
|
| 723 |
+
formatted = self._format_comprehensive_analysis_results(analysis_result, _wound_image, questionnaire_data_for_ai)
|
| 724 |
+
return formatted
|
| 725 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
except Exception as e:
|
| 727 |
logging.error(f"β Analysis handler error: {e}", exc_info=True)
|
| 728 |
+
return f"<div class='status-error'>β System Error: {html.escape(str(e))}</div>"
|
| 729 |
+
|
| 730 |
+
# ---------- HISTORY HELPERS ----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 731 |
def load_patient_history():
|
| 732 |
try:
|
| 733 |
user_id = self.current_user.get('id', 1)
|
| 734 |
if not user_id:
|
| 735 |
return "<div class='status-error'>β Please login first.</div>"
|
|
|
|
| 736 |
history_data = self.patient_history_manager.get_user_patient_history(user_id)
|
| 737 |
+
formatted = self.patient_history_manager.format_history_for_display(history_data)
|
| 738 |
+
return formatted
|
| 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"load patients error: {e}")
|
| 757 |
+
return gr.update(choices=["β Error loading β"], value=None)
|
| 758 |
+
|
| 759 |
+
def view_details_action(selected_name):
|
| 760 |
+
try:
|
| 761 |
+
uid = self.current_user.get('id', 1)
|
| 762 |
+
if not selected_name or selected_name.startswith("β"):
|
| 763 |
+
return "<div class='status-warning'>Please select a valid patient.</div>"
|
| 764 |
+
rows = self.patient_history_manager.get_wound_progression(uid, selected_name)
|
| 765 |
+
return _render_progression_timeline(rows)
|
| 766 |
+
except Exception as e:
|
| 767 |
+
logging.error(f"view details error: {e}")
|
| 768 |
+
return f"<div class='status-error'>Error loading details: {html.escape(str(e))}</div>"
|
| 769 |
+
|
| 770 |
+
def search_specific_patient_action(name_text):
|
| 771 |
+
try:
|
| 772 |
+
uid = self.current_user.get('id', 1)
|
| 773 |
+
if not uid:
|
| 774 |
return "<div class='status-error'>β Please login first.</div>"
|
| 775 |
+
if not name_text or not name_text.strip():
|
|
|
|
| 776 |
return "<div class='status-warning'>β οΈ Please enter a patient name to search.</div>"
|
| 777 |
+
rows = self.patient_history_manager.search_patient_by_name(uid, name_text.strip())
|
| 778 |
+
if not rows:
|
| 779 |
+
return f"<div class='status-warning'>β οΈ No records found for patient: {html.escape(name_text.strip())}</div>"
|
| 780 |
+
return self.patient_history_manager.format_patient_data_for_display(rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
except Exception as e:
|
| 782 |
logging.error(f"Error searching patient: {e}")
|
| 783 |
+
return f"<div class='status-error'>β Error searching patient: {html.escape(str(e))}</div>"
|
| 784 |
+
|
| 785 |
+
# --------------------------- BINDINGS ---------------------------
|
| 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,
|
| 803 |
inputs=[signup_role],
|
| 804 |
outputs=[org_fields, prac_fields]
|
| 805 |
)
|
| 806 |
+
|
| 807 |
analyze_btn.click(
|
| 808 |
handle_analysis,
|
| 809 |
+
inputs=[
|
| 810 |
+
patient_name, patient_age, patient_gender, wound_location, wound_duration,
|
| 811 |
+
pain_level, moisture_level, infection_signs, diabetic_status, previous_treatment,
|
| 812 |
+
medical_history, medications, allergies, additional_notes, wound_image
|
| 813 |
+
],
|
| 814 |
outputs=[analysis_output]
|
| 815 |
)
|
| 816 |
+
|
| 817 |
logout_btn_prac.click(
|
| 818 |
handle_logout,
|
| 819 |
outputs=[auth_panel, practitioner_panel]
|
| 820 |
)
|
| 821 |
+
|
| 822 |
history_btn.click(
|
| 823 |
load_patient_history,
|
| 824 |
outputs=[patient_history_output]
|
| 825 |
)
|
| 826 |
+
|
| 827 |
+
reload_patients_btn.click(
|
| 828 |
+
_load_patient_names,
|
| 829 |
+
outputs=[patient_selector]
|
| 830 |
+
)
|
| 831 |
+
|
| 832 |
+
view_details_btn.click(
|
| 833 |
+
view_details_action,
|
| 834 |
+
inputs=[patient_selector],
|
| 835 |
+
outputs=[patient_details_output]
|
| 836 |
+
)
|
| 837 |
+
|
| 838 |
search_patient_btn.click(
|
| 839 |
+
search_specific_patient_action,
|
| 840 |
inputs=[search_patient_name],
|
| 841 |
outputs=[specific_patient_output]
|
| 842 |
)
|
| 843 |
+
|
| 844 |
+
return app
|
| 845 |
+
|
| 846 |
|
| 847 |
def _format_comprehensive_analysis_results(self, analysis_result, image_url=None, questionnaire_data=None):
|
| 848 |
"""Format comprehensive analysis results with all visualization images from AIProcessor."""
|