| |
| """ |
| app.py β AidAiLine |
| Gradio entry point. Launch with: python app.py |
| |
| Tabs: |
| π Home β AI assistant chatbot |
| π Medications β CRUD with current/past filter |
| π
Appointments β CRUD with upcoming/today views |
| π½οΈ Diet & Allergies β Liked / Disliked / Allergies |
| π Doctors Visit β Brief builder, print, export & share |
| βοΈ Settings β Profiles, documents, model path |
| """ |
|
|
| from __future__ import annotations |
|
|
| import html |
| import re |
| import shutil |
| from datetime import date, datetime |
| from pathlib import Path |
| from typing import Optional |
|
|
| import gradio as gr |
|
|
| |
| import config as cfg |
| import med_tracker as med |
| import appointment_tracker as apt |
| import food_tracker as food |
| import doc_forms |
| import export_tool |
| import profiles |
|
|
| |
| def _safe_import_rag(): |
| try: |
| import rag_engine |
| return rag_engine |
| except ImportError as e: |
| return None |
|
|
| |
|
|
| CSS_PATH = Path(__file__).parent / "assets" / "style.css" |
| _CSS = """ |
| /* ββ Doctor Visit brief: clean word wrapping βββββββββββββββββββββββββββ */ |
| .visit-brief { |
| word-break: break-word; |
| white-space: pre-wrap; |
| overflow-wrap: break-word; |
| max-width: 100%; |
| } |
| .visit-brief * { |
| word-break: break-word; |
| overflow-wrap: break-word; |
| max-width: 100%; |
| } |
| /* DateTime picker β editable year field injected by _HEAD_JS */ |
| .dob-year-input { |
| width: 4.5em; |
| padding: 2px 4px; |
| border: 1px solid var(--input-border-color, #ccc); |
| border-radius: 4px; |
| font-weight: 600; |
| text-align: center; |
| background: var(--input-background-fill, #fff); |
| color: var(--body-text-color, inherit); |
| } |
| .month-year.svelte-12ypm2m, |
| .picker-header .month-year { |
| display: inline-flex; |
| align-items: center; |
| gap: 4px; |
| } |
| .dob-month-label { |
| font-weight: 600; |
| } |
| .app-datetime input.time, |
| .block.app-datetime input.time { |
| min-width: 11em; |
| } |
| /* Identity grid β two columns */ |
| #prof_identity_grid { |
| display: flex !important; |
| flex-wrap: nowrap !important; |
| gap: 0.75rem !important; |
| align-items: flex-start !important; |
| margin-bottom: 0.75rem !important; |
| } |
| #prof_side_a_col, |
| #prof_side_b_col { |
| flex: 1 1 0 !important; |
| min-width: 0 !important; |
| display: flex !important; |
| flex-direction: column !important; |
| gap: 0.75rem !important; |
| align-items: stretch !important; |
| } |
| #prof_side_a_col > .form, |
| #prof_side_b_col > .form, |
| #prof_side_a_col > .block, |
| #prof_side_b_col > .block { |
| display: flex !important; |
| flex-direction: column !important; |
| flex-wrap: nowrap !important; |
| width: 100% !important; |
| gap: 0.75rem !important; |
| } |
| #prof_identity_grid .profile-field-card, |
| #prof_identity_grid .profile-field-card.column, |
| #prof_identity_grid .dob-card { |
| flex: 0 0 auto !important; |
| flex-grow: 0 !important; |
| width: 100% !important; |
| min-width: 100% !important; |
| max-width: 100% !important; |
| box-sizing: border-box !important; |
| } |
| #prof_identity_grid .profile-field-card .field-card-view:not(.hidden), |
| #prof_identity_grid .profile-field-card .dob-card-view:not(.hidden) { |
| display: block !important; |
| visibility: visible !important; |
| width: 100% !important; |
| min-height: 2.25rem !important; |
| } |
| #prof_identity_grid .profile-field-card .field-card-body { |
| width: 100% !important; |
| } |
| #prof_optional_actions { |
| display: flex !important; |
| flex-wrap: wrap !important; |
| gap: 0.5rem !important; |
| margin: 0.65rem 0 0.5rem 0 !important; |
| width: 100% !important; |
| position: relative !important; |
| z-index: 4 !important; |
| } |
| #prof_optional_actions > .column, |
| #prof_optional_actions > .block { |
| flex: 1 1 10rem !important; |
| min-width: 0 !important; |
| } |
| #prof_optional_actions > button, |
| #prof_optional_actions .gr-button, |
| #prof_optional_actions button { |
| flex: 1 1 10rem !important; |
| width: 100% !important; |
| min-width: 0 !important; |
| white-space: normal !important; |
| line-height: 1.25 !important; |
| height: auto !important; |
| min-height: 2.35rem !important; |
| padding: 0.45rem 0.65rem !important; |
| pointer-events: auto !important; |
| position: relative !important; |
| z-index: 1 !important; |
| } |
| .profile-optional-panel { |
| margin-top: 0.5rem !important; |
| margin-bottom: 0.75rem !important; |
| width: 100% !important; |
| } |
| .profile-optional-panel:not(.hidden), |
| #prof-emergency-panel:not(.hidden), |
| #prof-insurance-panel:not(.hidden), |
| #prof-pcp-panel:not(.hidden) { |
| display: block !important; |
| visibility: visible !important; |
| } |
| .profile-optional-panel.hidden, |
| #prof-emergency-panel.hidden, |
| #prof-insurance-panel.hidden, |
| #prof-pcp-panel.hidden { |
| display: none !important; |
| } |
| .profile-optional-panel .profile-field-card, |
| .profile-optional-panel .dob-card { |
| margin-bottom: 0.5rem !important; |
| } |
| .profile-optional-panel .profile-optional-actions-inline { |
| gap: 0.5rem !important; |
| margin-top: 0.25rem !important; |
| } |
| .identity-col-header, |
| .identity-col-header p { |
| margin: 0 0 0.5rem 0 !important; |
| font-size: 0.9rem !important; |
| font-weight: 600 !important; |
| opacity: 0.92; |
| color: var(--body-text-color, rgba(255, 255, 255, 0.92)) !important; |
| } |
| .identity-info-grid .profile-field-card, |
| .identity-info-grid .dob-card { |
| margin-bottom: 0.5rem !important; |
| } |
| .identity-info-grid .field-card-title, |
| .identity-info-grid .field-card-title p { |
| color: var(--body-text-color, rgba(255, 255, 255, 0.92)) !important; |
| } |
| .identity-info-grid .field-card-value, |
| .identity-info-grid .field-card-value p, |
| .identity-info-grid .dob-card-date, |
| .identity-info-grid .dob-card-date p { |
| color: var(--body-text-color-subdued, rgba(255, 255, 255, 0.82)) !important; |
| } |
| /* Profile field cards β view / inline edit (DOB, Relationship, etc.) */ |
| .profile-field-card, |
| .dob-card { |
| width: 100%; |
| border: 1px solid #1e7a8f; |
| border-radius: 8px; |
| padding: 0.65rem 0.85rem; |
| box-sizing: border-box; |
| background: var(--background-fill-secondary, transparent); |
| } |
| .field-card-title, |
| .field-card-title p, |
| .dob-card-title, |
| .dob-card-title p { |
| margin: 0 0 0.35rem 0 !important; |
| font-size: 0.875rem !important; |
| font-weight: 600 !important; |
| } |
| .field-card-body, |
| .dob-card-body { |
| display: flex !important; |
| flex-wrap: nowrap !important; |
| align-items: center !important; |
| justify-content: space-between !important; |
| gap: 0.75rem !important; |
| width: 100% !important; |
| } |
| .field-card-body .field-card-value, |
| .field-card-body .field-card-value p, |
| .dob-card-body .dob-card-date, |
| .dob-card-body .dob-card-date p { |
| margin: 0 !important; |
| flex: 1 1 auto !important; |
| min-width: 0 !important; |
| font-size: 1rem !important; |
| word-break: break-word !important; |
| overflow-wrap: break-word !important; |
| } |
| .field-card-body .field-edit-link, |
| .dob-card-body .dob-edit-link { |
| flex: 0 0 auto !important; |
| margin-left: auto !important; |
| align-self: center !important; |
| } |
| .field-card-body .field-edit-link button, |
| .field-card-body button.field-edit-link, |
| .dob-card-body .dob-edit-link button, |
| .dob-card-body button.dob-edit-link { |
| min-width: unset !important; |
| box-shadow: none !important; |
| border: none !important; |
| padding: 0 0.25rem !important; |
| white-space: nowrap !important; |
| } |
| .field-card-edit-row, |
| .dob-split-row { |
| gap: 0.5rem !important; |
| flex-wrap: wrap !important; |
| align-items: stretch !important; |
| } |
| .field-card-edit-row .block, |
| .dob-split-row .block { |
| min-width: 0 !important; |
| flex: 1 1 8rem !important; |
| } |
| .field-card-actions, |
| .dob-split-actions { |
| gap: 0.5rem !important; |
| margin-top: 0.35rem !important; |
| } |
| /* Click anywhere on the card body to edit (JS wires the Edit button) */ |
| .field-card-view, |
| .dob-card-view { |
| cursor: pointer !important; |
| border-radius: 6px !important; |
| transition: background 0.15s ease, box-shadow 0.15s ease !important; |
| } |
| .field-card-view:hover, |
| .dob-card-view:hover { |
| background: rgba(42, 157, 181, 0.08) !important; |
| } |
| .profile-field-card:has(.field-card-view:hover), |
| .dob-card:has(.dob-card-view:hover) { |
| border-color: #2a9db5 !important; |
| box-shadow: 0 0 0 1px rgba(42, 157, 181, 0.2) !important; |
| } |
| .field-card-view .field-edit-link, |
| .dob-card-view .dob-edit-link { |
| pointer-events: none !important; |
| } |
| /* Structured address sub-fields inside edit mode */ |
| .address-edit-grid { |
| display: flex !important; |
| flex-direction: column !important; |
| gap: 0.5rem !important; |
| width: 100% !important; |
| } |
| .address-subfield { |
| border: 1px solid #1e7a8f !important; |
| border-radius: 8px !important; |
| padding: 0.45rem 0.65rem 0.55rem !important; |
| background: rgba(42, 157, 181, 0.06) !important; |
| box-sizing: border-box !important; |
| } |
| .address-subfield .wrap, |
| .address-subfield .block { |
| border: none !important; |
| box-shadow: none !important; |
| background: transparent !important; |
| padding: 0 !important; |
| } |
| .address-subfield label, |
| .address-subfield .label-wrap span { |
| font-size: 0.8125rem !important; |
| font-weight: 600 !important; |
| } |
| .address-subfield-row { |
| gap: 0.5rem !important; |
| align-items: stretch !important; |
| } |
| .address-subfield-row .address-subfield { |
| flex: 1 1 0 !important; |
| min-width: 0 !important; |
| } |
| #prof-dob-year input { |
| max-width: 7.5em !important; |
| font-variant-numeric: tabular-nums; |
| } |
| .care-profile-manager .profile-command-bar { |
| gap: 0.5rem !important; |
| margin: 0.5rem 0 0.75rem 0 !important; |
| flex-wrap: wrap !important; |
| } |
| .care-profile-manager .profile-command-bar button { |
| flex: 1 1 0 !important; |
| min-width: 10rem !important; |
| } |
| .care-profile-manager .profile-workspace { |
| margin-bottom: 0.75rem !important; |
| } |
| .care-profile-manager .profile-workspace:not(.hidden), |
| .care-profile-manager .profile-create-shell:not(.hidden), |
| .care-profile-manager .profile-create-submit:not(.hidden) { |
| display: block !important; |
| visibility: visible !important; |
| } |
| .care-profile-manager .profile-delete-panel { |
| border: 1px solid var(--border-color-accent-subdued, #7f1d1d); |
| border-radius: 8px; |
| padding: 0.85rem 1rem; |
| background: var(--background-fill-secondary, transparent); |
| } |
| .care-profile-manager .profile-create-shell { |
| border: 1px solid #1e7a8f !important; |
| border-radius: var(--radius-lg, 8px) !important; |
| margin-top: 0.5rem !important; |
| } |
| .care-profile-manager .profile-create-shell > .label-wrap { |
| font-weight: 600 !important; |
| } |
| /* Care Profile Manager β compact, professional controls */ |
| .care-profile-manager .profile-actions-row button { |
| min-height: 2rem !important; |
| font-size: 0.8125rem !important; |
| padding: 0.25rem 0.75rem !important; |
| } |
| .care-profile-manager .profile-btn-compact { |
| min-height: 2rem !important; |
| font-size: 0.8125rem !important; |
| } |
| .care-profile-manager .welcome-banner.gr-group { |
| background: linear-gradient(135deg, #0f3d48 0%, #145f70 55%, #1a5f72 100%) !important; |
| border: 2px solid #2a9db5 !important; |
| border-radius: 12px !important; |
| padding: 1.25rem 1.5rem !important; |
| margin-bottom: 1rem !important; |
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.28) !important; |
| outline: none !important; |
| --block-border-width: 0 !important; |
| --block-background-fill: transparent !important; |
| --background-fill-secondary: transparent !important; |
| --border-color-primary: #2a9db5 !important; |
| } |
| .care-profile-manager .welcome-banner > .form, |
| .care-profile-manager .welcome-banner .block, |
| .care-profile-manager .welcome-banner .markdown, |
| .care-profile-manager .welcome-banner .prose, |
| .care-profile-manager .welcome-banner .wrap, |
| .care-profile-manager .block:has(> .welcome-banner) { |
| background: transparent !important; |
| border: none !important; |
| box-shadow: none !important; |
| } |
| .care-profile-manager .welcome-banner h3, |
| .care-profile-manager .welcome-banner .markdown h3, |
| .care-profile-manager .welcome-banner .prose h3 { |
| margin: 0 0 0.5rem 0; |
| color: #ffffff !important; |
| font-weight: 600; |
| } |
| .care-profile-manager .welcome-banner p, |
| .care-profile-manager .welcome-banner .markdown p, |
| .care-profile-manager .welcome-banner .prose p { |
| margin: 0 0 1rem 0; |
| color: rgba(255, 255, 255, 0.82) !important; |
| line-height: 1.5; |
| } |
| .care-profile-manager .section-lead { |
| color: #4a6b78; |
| font-size: 0.9375rem; |
| margin-bottom: 0.75rem; |
| } |
| /* Active session β locked tab gateway */ |
| .locked-gateway { |
| text-align: center; |
| padding: 3rem 1.5rem; |
| margin: 1rem 0; |
| background: linear-gradient(180deg, #f0f7fa 0%, #e8f4f8 100%); |
| border: 1px dashed #1e7a8f; |
| border-radius: 16px; |
| } |
| .locked-gateway .locked-title { |
| font-size: 1.125rem; |
| font-weight: 700; |
| letter-spacing: 0.04em; |
| color: #145f70; |
| margin-bottom: 1.5rem; |
| } |
| .locked-gateway .locked-icon { |
| font-size: 2.5rem; |
| margin-bottom: 0.5rem; |
| } |
| .locked-gateway .locked-headline { |
| font-size: 1.25rem; |
| font-weight: 600; |
| color: #1a3a44; |
| margin: 0.25rem 0; |
| } |
| .locked-gateway .locked-sub { |
| color: #4a6b78; |
| max-width: 36rem; |
| margin: 0.75rem auto 1.5rem; |
| line-height: 1.6; |
| } |
| .session-bar .block.session-banner, |
| .session-bar .session-banner.block { |
| background: linear-gradient(135deg, #0f3d48 0%, #145f70 55%, #1a5f72 100%) !important; |
| border: 2px solid #2a9db5 !important; |
| border-radius: 10px !important; |
| padding: 0.65rem 1rem !important; |
| margin-bottom: 0 !important; |
| font-size: 0.9375rem; |
| flex: 1 1 auto; |
| min-width: 0; |
| box-shadow: 0 4px 16px rgba(0, 0, 0, 0.22) !important; |
| outline: none !important; |
| --block-border-width: 0 !important; |
| --block-background-fill: transparent !important; |
| --background-fill-secondary: transparent !important; |
| --border-color-primary: #2a9db5 !important; |
| } |
| .session-bar .block.session-banner.locked, |
| .session-bar .session-banner.block.locked { |
| background: linear-gradient(135deg, #122a32 0%, #0f3d48 100%) !important; |
| border-color: #1e7a8f !important; |
| } |
| .session-bar .block.session-banner > .wrap, |
| .session-bar .block.session-banner .form, |
| .session-bar .block.session-banner .markdown, |
| .session-bar .block.session-banner .prose { |
| background: transparent !important; |
| border: none !important; |
| box-shadow: none !important; |
| padding: 0 !important; |
| } |
| .session-bar .block.session-banner .markdown, |
| .session-bar .block.session-banner .prose, |
| .session-bar .block.session-banner p, |
| .session-bar .block.session-banner strong, |
| .session-bar .block.session-banner span, |
| .session-bar .block.session-banner .md { |
| color: rgba(255, 255, 255, 0.92) !important; |
| margin: 0 !important; |
| } |
| .session-bar { |
| align-items: center; |
| gap: 0.75rem; |
| margin-bottom: 0.75rem; |
| } |
| .visit-clinic-grid { |
| gap: 0.75rem !important; |
| align-items: stretch !important; |
| margin-bottom: 0.75rem !important; |
| } |
| .visit-clinic-grid > .column, |
| .visit-clinic-grid > .block { |
| flex: 1 1 0 !important; |
| min-width: 0 !important; |
| display: flex !important; |
| align-items: stretch !important; |
| } |
| /* Uniform paired cards in 50/50 visit rows */ |
| .visit-uniform-card.profile-field-card { |
| display: flex !important; |
| flex-direction: column !important; |
| flex: 1 1 0 !important; |
| width: 100% !important; |
| min-height: 17rem !important; |
| height: 100% !important; |
| margin-bottom: 0 !important; |
| box-sizing: border-box !important; |
| } |
| .visit-clinic-card.profile-field-card { |
| display: flex !important; |
| flex-direction: column !important; |
| height: 100% !important; |
| } |
| .visit-uniform-card .field-card-title, |
| .visit-uniform-card .field-card-title p { |
| flex: 0 0 auto !important; |
| margin-bottom: 0.5rem !important; |
| } |
| .visit-uniform-card .field-card-value, |
| .visit-uniform-card .field-card-value p { |
| flex: 1 1 auto !important; |
| margin: 0 !important; |
| font-size: 0.9375rem !important; |
| line-height: 1.5 !important; |
| } |
| .visit-uniform-card .block.wrap, |
| .visit-uniform-card .form { |
| flex: 1 1 auto !important; |
| display: flex !important; |
| flex-direction: column !important; |
| min-height: 0 !important; |
| } |
| .visit-clinic-card .field-card-value, |
| .visit-clinic-card .field-card-value p { |
| margin: 0 !important; |
| font-size: 0.9375rem !important; |
| line-height: 1.5 !important; |
| } |
| .visit-clinic-custom-wrap { |
| margin-top: 0.5rem !important; |
| } |
| .visit-destination-card.profile-field-card { |
| margin-bottom: 0.75rem !important; |
| } |
| .visit-destination-header { |
| display: flex !important; |
| flex-wrap: nowrap !important; |
| align-items: center !important; |
| justify-content: space-between !important; |
| gap: 0.5rem !important; |
| margin-bottom: 0.35rem !important; |
| } |
| .visit-destination-header .field-card-title, |
| .visit-destination-header .field-card-title p { |
| margin: 0 !important; |
| flex: 1 1 auto !important; |
| } |
| .visit-destination-header .field-edit-link, |
| .visit-destination-header button.field-edit-link { |
| flex: 0 0 auto !important; |
| margin-left: auto !important; |
| } |
| .visit-destination-card .field-card-value, |
| .visit-destination-card .field-card-value p { |
| margin: 0 !important; |
| font-size: 0.9375rem !important; |
| line-height: 1.5 !important; |
| } |
| .visit-brief-shell.profile-create-shell { |
| border: 1px solid #1e7a8f !important; |
| border-radius: 8px !important; |
| padding: 0.85rem 1rem !important; |
| margin-bottom: 0.75rem !important; |
| background: var(--background-fill-secondary, transparent) !important; |
| box-sizing: border-box !important; |
| } |
| .visit-brief-shell > .form, |
| .visit-brief-shell .block, |
| .visit-brief-shell .wrap, |
| .visit-brief-shell .markdown, |
| .visit-brief-shell .prose { |
| background: transparent !important; |
| border: none !important; |
| box-shadow: none !important; |
| } |
| .visit-brief-shell-title, |
| .visit-brief-shell-title p { |
| margin: 0 0 0.5rem 0 !important; |
| font-weight: 600 !important; |
| text-align: center !important; |
| } |
| .visit-brief-header, |
| .visit-brief-header p { |
| margin: 0 0 0.65rem 0 !important; |
| font-size: 0.9rem !important; |
| line-height: 1.45 !important; |
| text-align: center !important; |
| } |
| .visit-chip-grid-row { |
| gap: 0.75rem !important; |
| align-items: stretch !important; |
| } |
| .visit-chip-column { |
| flex: 1 1 0 !important; |
| min-width: 0 !important; |
| display: flex !important; |
| flex-direction: column !important; |
| gap: 0.5rem !important; |
| border: 1px solid rgba(30, 122, 143, 0.45) !important; |
| border-radius: 8px !important; |
| padding: 0.55rem 0.65rem !important; |
| background: rgba(42, 157, 181, 0.04) !important; |
| box-sizing: border-box !important; |
| } |
| .visit-chip-col-label, |
| .visit-chip-col-label p { |
| margin: 0 0 0.15rem 0 !important; |
| font-size: 0.6875rem !important; |
| font-weight: 600 !important; |
| opacity: 0.82 !important; |
| text-transform: uppercase !important; |
| letter-spacing: 0.04em !important; |
| } |
| .visit-chip-column button, |
| .visit-chip-column .visit-chip-btn { |
| width: 100% !important; |
| flex: 1 1 auto !important; |
| min-height: 2.35rem !important; |
| font-size: 0.8125rem !important; |
| padding: 0.45rem 0.65rem !important; |
| border-radius: 8px !important; |
| border: 1px solid rgba(30, 122, 143, 0.55) !important; |
| background: transparent !important; |
| white-space: normal !important; |
| line-height: 1.25 !important; |
| } |
| .visit-expansion-stack { |
| gap: 0.5rem !important; |
| margin-bottom: 0.75rem !important; |
| } |
| .visit-expansion-panel.profile-field-card { |
| margin-bottom: 0 !important; |
| padding: 0.65rem 0.85rem !important; |
| } |
| .visit-pain-row { |
| align-items: center !important; |
| gap: 0.75rem !important; |
| margin: 0.35rem 0 0.5rem 0 !important; |
| } |
| .visit-pain-row label, |
| .visit-pain-row .label-wrap span { |
| font-size: 0.8125rem !important; |
| font-weight: 600 !important; |
| white-space: nowrap !important; |
| } |
| .visit-form-card.profile-field-card { |
| margin-bottom: 0.75rem !important; |
| } |
| .visit-form-card .block, |
| .visit-form-card .wrap { |
| border: none !important; |
| box-shadow: none !important; |
| background: transparent !important; |
| } |
| .visit-form-card label, |
| .visit-form-card .label-wrap span { |
| font-size: 0.8125rem !important; |
| font-weight: 600 !important; |
| } |
| .visit-inline-actions { |
| gap: 0.5rem !important; |
| flex-wrap: wrap !important; |
| } |
| .visit-brief-output { |
| margin-top: 0.75rem !important; |
| } |
| .visit-brief-output .visit-brief, |
| .visit-brief-output .visit-brief .prose { |
| margin: 0 !important; |
| } |
| .visit-concerns-inner { |
| gap: 0.65rem !important; |
| align-items: stretch !important; |
| flex: 1 1 auto !important; |
| min-height: 10rem !important; |
| width: 100% !important; |
| } |
| .visit-concerns-inner > .column, |
| .visit-concerns-inner > .block { |
| flex: 1 1 0 !important; |
| min-width: 0 !important; |
| min-height: 9rem !important; |
| display: flex !important; |
| flex-direction: column !important; |
| } |
| .visit-concern-tokens { |
| display: flex !important; |
| flex-direction: row !important; |
| flex-wrap: wrap !important; |
| gap: 0.35rem !important; |
| margin: 0 0 0.35rem 0 !important; |
| } |
| .visit-concern-tokens > .block, |
| .visit-concern-tokens > button, |
| .visit-concern-tokens button { |
| flex: 0 0 auto !important; |
| width: auto !important; |
| max-width: none !important; |
| font-size: 0.75rem !important; |
| padding: 0.25rem 0.55rem !important; |
| min-height: 1.65rem !important; |
| white-space: nowrap !important; |
| } |
| .visit-quick-add-label, |
| .visit-quick-add-label p { |
| font-size: 0.75rem !important; |
| margin: 0 0 0.25rem 0 !important; |
| opacity: 0.88; |
| } |
| .visit-concern-actions { |
| display: flex !important; |
| flex-direction: row !important; |
| justify-content: space-between !important; |
| align-items: center !important; |
| gap: 0.5rem !important; |
| width: 100% !important; |
| margin-top: 0.25rem !important; |
| } |
| .visit-concern-actions > .block, |
| .visit-concern-actions > button, |
| .visit-concern-actions button { |
| width: auto !important; |
| flex: 0 0 auto !important; |
| min-width: 0 !important; |
| } |
| .visit-purpose-grid { |
| gap: 0.75rem !important; |
| align-items: stretch !important; |
| margin-bottom: 0.75rem !important; |
| } |
| .visit-purpose-card.profile-field-card { |
| display: flex !important; |
| flex-direction: column !important; |
| flex: 1 1 0 !important; |
| width: 100% !important; |
| min-height: 0 !important; |
| height: auto !important; |
| margin-bottom: 0 !important; |
| box-sizing: border-box !important; |
| border: 1px solid rgba(30, 122, 143, 0.55) !important; |
| background: transparent !important; |
| } |
| .visit-purpose-card-shell { |
| position: relative !important; |
| cursor: pointer !important; |
| border-radius: 6px !important; |
| transition: background 0.15s ease, box-shadow 0.15s ease !important; |
| } |
| .visit-purpose-card-shell:hover { |
| background: rgba(42, 157, 181, 0.08) !important; |
| } |
| .visit-purpose-card:has(.visit-purpose-card-shell:hover) { |
| border-color: #2a9db5 !important; |
| box-shadow: 0 0 0 1px rgba(42, 157, 181, 0.2) !important; |
| } |
| .visit-purpose-card-shell .field-card-title, |
| .visit-purpose-card-shell .field-card-title p { |
| margin: 0 0 0.25rem 0 !important; |
| pointer-events: none !important; |
| } |
| .visit-purpose-summary, |
| .visit-purpose-summary p { |
| margin: 0 !important; |
| font-size: 0.9375rem !important; |
| line-height: 1.5 !important; |
| pointer-events: none !important; |
| } |
| .visit-purpose-toggle, |
| .visit-purpose-toggle button { |
| position: absolute !important; |
| inset: 0 !important; |
| opacity: 0 !important; |
| width: 100% !important; |
| height: 100% !important; |
| min-height: 2.75rem !important; |
| margin: 0 !important; |
| padding: 0 !important; |
| z-index: 2 !important; |
| cursor: pointer !important; |
| border: none !important; |
| background: transparent !important; |
| box-shadow: none !important; |
| } |
| .visit-purpose-workspace { |
| margin-top: 0.5rem !important; |
| padding-top: 0.5rem !important; |
| border-top: 1px solid rgba(42, 157, 181, 0.25) !important; |
| } |
| .visit-purpose-workspace .wrap, |
| .visit-purpose-workspace .block { |
| border: none !important; |
| box-shadow: none !important; |
| background: transparent !important; |
| } |
| .visit-purpose-grid .visit-purpose-concerns-inner { |
| min-height: 0 !important; |
| flex: 1 1 auto !important; |
| } |
| .visit-purpose-grid .visit-concerns-list, |
| .visit-purpose-grid .visit-concerns-list p { |
| min-height: 4.25rem !important; |
| max-height: 5.75rem !important; |
| overflow-y: auto !important; |
| } |
| .visit-purpose-grid .visit-concerns-inner > .column { |
| min-height: 0 !important; |
| } |
| .visit-reason-other-wrap { |
| margin-top: 0.35rem !important; |
| } |
| .visit-concerns-list, |
| .visit-concerns-list p { |
| flex: 1 1 auto !important; |
| margin: 0 !important; |
| min-height: 7rem !important; |
| font-size: 0.9375rem !important; |
| line-height: 1.45 !important; |
| } |
| .visit-uniform-card .visit-questions-list, |
| .visit-questions-list, |
| .visit-questions-list p { |
| flex: 1 1 auto !important; |
| margin: 0 !important; |
| min-height: 4.5rem !important; |
| max-height: 8rem !important; |
| overflow-y: auto !important; |
| } |
| .visit-symptom-detail, |
| .visit-symptom-detail p { |
| margin: 0 !important; |
| font-size: 0.875rem !important; |
| line-height: 1.45 !important; |
| padding: 0.35rem 0 !important; |
| } |
| .visit-symptoms-column, |
| .visit-questions-column { |
| overflow-y: auto !important; |
| } |
| .visit-symptoms-column .visit-concerns-inner > .column:last-child { |
| border-left: 1px solid rgba(42, 157, 181, 0.35) !important; |
| padding-left: 0.65rem !important; |
| } |
| .visit-compiler-toggle, |
| .visit-brief-settings-toggle { |
| width: auto !important; |
| max-width: 22rem !important; |
| margin: 0.5rem 0 0.35rem 0 !important; |
| } |
| .visit-brief-settings-wrap { |
| margin-bottom: 0.5rem !important; |
| } |
| .visit-chip-btn-complete { |
| border-color: #2a9db5 !important; |
| background: rgba(42, 157, 181, 0.12) !important; |
| } |
| .med-manager-shell.profile-create-shell { |
| border: 1px solid #1e7a8f !important; |
| border-radius: 8px !important; |
| padding: 0.85rem 1rem !important; |
| margin-bottom: 0.75rem !important; |
| background: var(--background-fill-secondary, transparent) !important; |
| box-sizing: border-box !important; |
| } |
| .med-manager-shell > .form, |
| .med-manager-shell .block, |
| .med-manager-shell .wrap { |
| background: transparent !important; |
| border: none !important; |
| box-shadow: none !important; |
| } |
| .med-manager-shell-title, |
| .med-manager-shell-title p { |
| margin: 0 0 0.35rem 0 !important; |
| font-weight: 600 !important; |
| text-align: center !important; |
| } |
| .med-summary-line, |
| .med-summary-line p { |
| margin: 0 0 0.5rem 0 !important; |
| font-size: 0.875rem !important; |
| text-align: center !important; |
| opacity: 0.9 !important; |
| } |
| .med-table-card.profile-field-card { |
| margin-bottom: 0.65rem !important; |
| padding: 0.5rem 0.65rem !important; |
| } |
| .med-action-row { |
| gap: 0.75rem !important; |
| align-items: flex-start !important; |
| margin-top: 0.35rem !important; |
| } |
| .med-action-row > .column, |
| .med-action-row > .block { |
| flex: 1 1 0 !important; |
| min-width: 0 !important; |
| } |
| .med-catalog-card.profile-field-card { |
| margin-bottom: 0 !important; |
| background: var(--background-fill-secondary) !important; |
| cursor: pointer !important; |
| } |
| .med-catalog-header { |
| display: flex !important; |
| flex-wrap: nowrap !important; |
| align-items: center !important; |
| justify-content: space-between !important; |
| gap: 0.5rem !important; |
| width: 100% !important; |
| margin-bottom: 0.35rem !important; |
| } |
| .med-catalog-header .field-card-title, |
| .med-catalog-header .field-card-title p { |
| margin: 0 !important; |
| flex: 1 1 auto !important; |
| min-width: 0 !important; |
| } |
| .med-catalog-header .med-catalog-toggle, |
| .med-catalog-header button.med-catalog-toggle { |
| flex: 0 0 auto !important; |
| margin-left: auto !important; |
| pointer-events: auto !important; |
| } |
| .med-catalog-card .field-card-view { |
| margin-top: 0 !important; |
| padding: 0.35rem 0.45rem !important; |
| border-radius: 6px !important; |
| } |
| .med-catalog-card .field-card-value, |
| .med-catalog-card .field-card-value p { |
| margin: 0 !important; |
| font-size: 0.9375rem !important; |
| } |
| .med-filter-options { |
| gap: 0.35rem !important; |
| width: 100% !important; |
| } |
| .med-filter-options button { |
| width: 100% !important; |
| min-height: 2rem !important; |
| font-size: 0.8125rem !important; |
| padding: 0.35rem 0.55rem !important; |
| border-radius: 8px !important; |
| border: 1px solid rgba(30, 122, 143, 0.45) !important; |
| background: rgba(42, 157, 181, 0.06) !important; |
| } |
| .med-add-form .address-edit-grid, |
| .med-delete-form .address-edit-grid { |
| gap: 0.45rem !important; |
| } |
| .med-add-form .address-subfield, |
| .med-delete-form .address-subfield { |
| padding: 0.4rem 0.55rem 0.5rem !important; |
| } |
| .med-add-form .field-card-edit .wrap, |
| .med-delete-form .field-card-edit .wrap { |
| min-width: 0 !important; |
| } |
| .med-add-workspace { |
| width: 100% !important; |
| margin-top: 0.5rem !important; |
| padding: 0 !important; |
| border: none !important; |
| background: transparent !important; |
| box-shadow: none !important; |
| } |
| .med-add-workspace > .form, |
| .med-add-workspace > .block, |
| .med-add-workspace > .wrap { |
| background: transparent !important; |
| border: none !important; |
| box-shadow: none !important; |
| } |
| .med-add-matrix-shell.profile-field-card { |
| width: 100% !important; |
| margin-bottom: 0.35rem !important; |
| padding: 0.65rem 0.85rem !important; |
| box-sizing: border-box !important; |
| background: var(--background-fill-secondary) !important; |
| } |
| .med-add-matrix-shell > .form, |
| .med-add-matrix-shell > .block, |
| .med-add-matrix-shell > .wrap { |
| border: none !important; |
| box-shadow: none !important; |
| } |
| .med-add-matrix, |
| .med-add-matrix > .form, |
| .med-add-row, |
| .med-add-row > .column, |
| .med-add-row > .block { |
| background: var(--background-fill-secondary) !important; |
| } |
| .med-add-matrix .block, |
| .med-add-matrix .wrap { |
| border: none !important; |
| box-shadow: none !important; |
| padding: 0 !important; |
| } |
| .med-add-matrix label, |
| .med-add-matrix .label-wrap span { |
| font-size: 0.8125rem !important; |
| font-weight: 600 !important; |
| } |
| .med-add-row { |
| gap: 0.65rem !important; |
| align-items: stretch !important; |
| width: 100% !important; |
| } |
| .med-add-row > .column, |
| .med-add-row > .block { |
| flex: 1 1 0 !important; |
| min-width: 0 !important; |
| } |
| .med-add-matrix .med-add-notes-block { |
| width: 100% !important; |
| margin-top: 0.15rem !important; |
| background: var(--background-fill-secondary) !important; |
| } |
| .med-add-matrix .med-add-notes-block .wrap, |
| .med-add-matrix .med-add-notes-block .block { |
| width: 100% !important; |
| background: transparent !important; |
| } |
| .med-add-matrix .address-subfield { |
| height: 100% !important; |
| padding: 0.4rem 0.55rem 0.5rem !important; |
| box-sizing: border-box !important; |
| background: rgba(42, 157, 181, 0.06) !important; |
| border: 1px solid #1e7a8f !important; |
| border-radius: 8px !important; |
| } |
| /* Restore Gradio block.padded containers for split pickers (Settings DOB style) */ |
| .split-picker-row { |
| gap: 0.5rem !important; |
| align-items: stretch !important; |
| } |
| .split-picker-row > .block, |
| .split-picker-row > .column { |
| flex: 1 1 0 !important; |
| min-width: 0 !important; |
| } |
| .med-add-matrix .split-picker-row > .block, |
| .med-add-matrix .split-picker-row > .column, |
| .address-subfield .split-picker-row > .block, |
| .address-subfield .split-picker-row > .column { |
| border: 1px solid var(--border-color-primary) !important; |
| border-radius: var(--radius-lg, 8px) !important; |
| background: var(--input-background-fill, var(--background-fill-secondary)) !important; |
| padding: var(--block-padding, 0.75rem) !important; |
| box-sizing: border-box !important; |
| overflow: hidden !important; |
| box-shadow: none !important; |
| } |
| .med-add-matrix .split-picker-row > .block > .wrap, |
| .med-add-matrix .split-picker-row > .column > .wrap, |
| .address-subfield .split-picker-row > .block > .wrap, |
| .address-subfield .split-picker-row > .column > .wrap { |
| background: transparent !important; |
| box-shadow: none !important; |
| } |
| .appt-form-matrix-shell.profile-field-card { |
| width: 100% !important; |
| margin-top: 0.5rem !important; |
| margin-bottom: 0.35rem !important; |
| padding: 0.65rem 0.85rem !important; |
| box-sizing: border-box !important; |
| background: var(--background-fill-secondary) !important; |
| } |
| .appt-form-matrix-shell > .form, |
| .appt-form-matrix-shell > .block, |
| .appt-form-matrix-shell > .wrap { |
| border: none !important; |
| box-shadow: none !important; |
| } |
| .appt-form-matrix .field-subfield-label, |
| .appt-form-matrix .field-subfield-label p { |
| font-size: 0.8125rem !important; |
| font-weight: 600 !important; |
| margin: 0 0 0.35rem !important; |
| line-height: 1.25 !important; |
| } |
| .appt-form-matrix .appt-time-parts-row { |
| gap: 0.35rem !important; |
| align-items: stretch !important; |
| width: 100% !important; |
| } |
| .appt-form-matrix .appt-time-parts-row > .column, |
| .appt-form-matrix .appt-time-parts-row > .block { |
| flex: 1 1 0 !important; |
| min-width: 0 !important; |
| } |
| .appt-form-matrix .appt-load-row { |
| margin-bottom: 0.45rem !important; |
| } |
| .appt-form-matrix .appt-load-row label, |
| .appt-form-matrix .appt-load-row .label-wrap span { |
| font-size: 0.8125rem !important; |
| font-weight: 600 !important; |
| } |
| .appt-form-matrix .appt-recurrence-block .wrap, |
| .appt-form-matrix .appt-recurrence-block .block { |
| background: transparent !important; |
| border: none !important; |
| box-shadow: none !important; |
| padding: 0 !important; |
| } |
| .appt-form-matrix .appt-date-cell.address-subfield { |
| display: flex !important; |
| flex-direction: column !important; |
| gap: 0.35rem !important; |
| } |
| .appt-form-matrix .appt-date-cell .field-card-view, |
| .appt-form-matrix .appt-date-cell .field-card-edit { |
| background: transparent !important; |
| border: none !important; |
| box-shadow: none !important; |
| padding: 0 !important; |
| } |
| .appt-form-matrix .appt-date-cell .dob-card-body { |
| min-height: 2rem !important; |
| padding: 0.15rem 0 !important; |
| } |
| .appt-form-matrix .appt-date-cell .dob-card-date, |
| .appt-form-matrix .appt-date-cell .dob-card-date p { |
| font-size: 0.875rem !important; |
| } |
| .appt-form-matrix .appt-date-cell .dob-split-row { |
| margin-top: 0 !important; |
| } |
| .appt-form-matrix .appt-date-cell .dob-split-actions { |
| margin-top: 0.15rem !important; |
| } |
| .appt-today-line { |
| margin: 0.15rem 0 0.35rem 0 !important; |
| font-size: 0.875rem !important; |
| } |
| .appt-completed-label, |
| .appt-completed-label p { |
| margin: 0.35rem 0 0.15rem 0 !important; |
| font-size: 0.8125rem !important; |
| font-weight: 600 !important; |
| } |
| .appt-show-options { |
| gap: 0.4rem !important; |
| } |
| .food-action-row > .column, |
| .food-action-row > .block { |
| flex: 1 1 11rem !important; |
| } |
| .home-brief-card.profile-field-card { |
| margin-bottom: 0.75rem !important; |
| padding: 0.65rem 0.85rem !important; |
| } |
| .home-brief-card .home-brief-header { |
| margin-bottom: 0.35rem !important; |
| } |
| .home-brief-body, |
| .home-brief-body p { |
| margin: 0 !important; |
| font-size: 0.875rem !important; |
| line-height: 1.45 !important; |
| color: var(--body-text-color, rgba(255, 255, 255, 0.88)) !important; |
| } |
| .home-brief-body h4 { |
| margin: 0.55rem 0 0.2rem 0 !important; |
| font-size: 0.8125rem !important; |
| font-weight: 700 !important; |
| letter-spacing: 0.02em !important; |
| text-transform: uppercase !important; |
| color: var(--body-text-color, rgba(255, 255, 255, 0.92)) !important; |
| opacity: 0.92 !important; |
| } |
| .home-brief-body h4:first-child { |
| margin-top: 0.15rem !important; |
| } |
| .home-brief-body ul { |
| margin: 0.15rem 0 0.25rem 0 !important; |
| padding-left: 1.1rem !important; |
| } |
| .home-brief-refresh-btn { |
| min-height: 1.75rem !important; |
| } |
| .home-brief-compact { |
| width: 100%; |
| } |
| .home-brief-card .home-brief-meta, |
| .home-brief-body .home-brief-meta { |
| font-size: 0.875rem !important; |
| color: var(--body-text-color-subdued, rgba(255, 255, 255, 0.78)) !important; |
| margin: 0 !important; |
| } |
| .home-brief-card .home-brief-rule, |
| .home-brief-body .home-brief-rule { |
| border-top: 1px solid var(--border-color-primary, rgba(255, 255, 255, 0.18)); |
| margin: 0.4rem 0 0.5rem; |
| } |
| .home-brief-columns { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 0.5rem 1.25rem; |
| } |
| @media (max-width: 640px) { |
| .home-brief-columns { |
| grid-template-columns: 1fr; |
| } |
| } |
| .home-brief-card .home-brief-col-title, |
| .home-brief-body .home-brief-col-title { |
| font-size: 0.6875rem; |
| font-weight: 700; |
| letter-spacing: 0.04em; |
| text-transform: uppercase; |
| color: var(--body-text-color, rgba(255, 255, 255, 0.92)) !important; |
| opacity: 0.88; |
| margin-bottom: 0.25rem; |
| } |
| .home-brief-card .home-brief-col-line, |
| .home-brief-body .home-brief-col-line { |
| font-size: 0.8125rem; |
| line-height: 1.45; |
| color: var(--body-text-color, rgba(255, 255, 255, 0.88)) !important; |
| margin: 0.08rem 0; |
| } |
| .home-chat-shell.profile-field-card { |
| margin-top: 0.35rem !important; |
| padding: 0.65rem 0.85rem 0.75rem !important; |
| border: 1px solid var(--border-color-primary, #1e7a8f) !important; |
| } |
| .home-chat-shell .home-chat-title { |
| font-size: 0.8125rem; |
| font-weight: 700; |
| letter-spacing: 0.02em; |
| color: var(--body-text-color, rgba(255, 255, 255, 0.92)) !important; |
| margin: 0 0 0.4rem 0; |
| padding-bottom: 0.35rem; |
| border-bottom: 1px solid var(--border-color-primary, rgba(255, 255, 255, 0.18)); |
| } |
| .home-example-chips { |
| gap: 0.35rem !important; |
| flex-wrap: wrap !important; |
| margin-top: 0.35rem !important; |
| } |
| .home-example-chips > .column, |
| .home-example-chips > .block { |
| flex: 0 1 auto !important; |
| min-width: 0 !important; |
| } |
| .home-example-chips button { |
| font-size: 0.75rem !important; |
| padding: 0.2rem 0.55rem !important; |
| min-height: 1.65rem !important; |
| white-space: nowrap !important; |
| } |
| .home-chat-shell .gradio-chatbot { |
| border: none !important; |
| box-shadow: none !important; |
| } |
| .home-chat-clear-btn { |
| margin-top: 0.25rem !important; |
| } |
| .food-add-matrix .food-add-row2 > .column:only-child, |
| .food-add-matrix .food-add-row2 > .block:only-child { |
| flex: 1 1 100% !important; |
| max-width: 100% !important; |
| } |
| .settings-documents-shell.profile-field-card, |
| .visit-export-panel.profile-field-card { |
| margin-top: 0.5rem !important; |
| margin-bottom: 0.75rem !important; |
| } |
| .settings-docs-upload-row { |
| gap: 0.65rem !important; |
| align-items: stretch !important; |
| margin-bottom: 0.5rem !important; |
| } |
| .settings-docs-upload-row > .column, |
| .settings-docs-upload-row > .block { |
| min-width: 0 !important; |
| } |
| .settings-docs-upload-actions { |
| gap: 0.35rem !important; |
| display: flex !important; |
| flex-direction: column !important; |
| justify-content: stretch !important; |
| } |
| .settings-docs-upload-actions button { |
| width: 100% !important; |
| min-height: 2rem !important; |
| flex: 1 1 auto !important; |
| } |
| .settings-documents-shell .wrap.file-preview, |
| .settings-documents-shell .upload-container, |
| .settings-documents-shell .icon-wrap { |
| min-height: 2.75rem !important; |
| max-height: 3.25rem !important; |
| padding: 0.25rem 0.5rem !important; |
| } |
| .settings-documents-shell .file-preview *, |
| .settings-documents-shell .icon-wrap .icon { |
| font-size: 0.875rem !important; |
| } |
| .settings-documents-shell .file-preview .text, |
| .settings-documents-shell .icon-wrap .text { |
| font-size: 0.8125rem !important; |
| line-height: 1.25 !important; |
| } |
| .settings-docs-view-index-btn { |
| width: 100% !important; |
| margin-top: 0.15rem !important; |
| } |
| .settings-docs-index-panel { |
| margin-top: 0.45rem !important; |
| padding-top: 0.45rem !important; |
| border-top: 1px solid rgba(42, 157, 181, 0.25) !important; |
| } |
| .docs-index-empty { |
| margin: 0 !important; |
| font-size: 0.875rem !important; |
| opacity: 0.88 !important; |
| } |
| .docs-index-list { |
| display: flex !important; |
| flex-direction: column !important; |
| gap: 0.35rem !important; |
| width: 100% !important; |
| } |
| .docs-index-row { |
| display: flex !important; |
| align-items: center !important; |
| justify-content: space-between !important; |
| gap: 0.65rem !important; |
| padding: 0.45rem 0.65rem !important; |
| border: 1px solid #1e7a8f !important; |
| border-radius: 8px !important; |
| background: rgba(42, 157, 181, 0.06) !important; |
| box-sizing: border-box !important; |
| } |
| .docs-index-name { |
| flex: 1 1 auto !important; |
| min-width: 0 !important; |
| font-size: 0.875rem !important; |
| word-break: break-word !important; |
| } |
| .docs-delete-btn { |
| flex: 0 0 auto !important; |
| border: 1px solid rgba(248, 113, 113, 0.55) !important; |
| background: rgba(127, 29, 29, 0.18) !important; |
| color: #fca5a5 !important; |
| border-radius: 6px !important; |
| padding: 0.2rem 0.55rem !important; |
| font-size: 0.75rem !important; |
| cursor: pointer !important; |
| white-space: nowrap !important; |
| } |
| .docs-delete-btn:hover { |
| background: rgba(127, 29, 29, 0.32) !important; |
| } |
| """ |
|
|
| _HEAD_JS = """ |
| <script> |
| (function () { |
| var MONTHS = ["January","February","March","April","May","June", |
| "July","August","September","October","November","December"]; |
| function wirePickerYears() { |
| document.querySelectorAll(".picker-header").forEach(function (pickerHeader) { |
| if (pickerHeader.dataset.yearWired) return; |
| var monthYear = pickerHeader.querySelector(".month-year"); |
| if (!monthYear) return; |
| var txt = (monthYear.textContent || "").trim(); |
| var m = txt.match(/^([A-Za-z]+)\\s+(\\d{4})$/); |
| if (!m) return; |
| pickerHeader.dataset.yearWired = "1"; |
| |
| var monthIdx = MONTHS.indexOf(m[1]); |
| var year = parseInt(m[2], 10); |
| if (monthIdx < 0) monthIdx = new Date().getMonth(); |
| |
| monthYear.textContent = ""; |
| var monthSpan = document.createElement("span"); |
| monthSpan.className = "dob-month-label"; |
| var yearInput = document.createElement("input"); |
| yearInput.type = "number"; |
| yearInput.min = "1900"; |
| yearInput.max = "2100"; |
| yearInput.className = "dob-year-input"; |
| yearInput.setAttribute("aria-label", "Year"); |
| |
| function renderHeader() { |
| monthSpan.textContent = MONTHS[monthIdx] + " "; |
| yearInput.value = String(year); |
| } |
| renderHeader(); |
| |
| function getTimeInput() { |
| var block = pickerHeader.closest(".block") || document; |
| return block.querySelector("input.time"); |
| } |
| |
| function applyYearToField(newYear) { |
| var timeInput = getTimeInput(); |
| if (!timeInput) return; |
| year = parseInt(newYear, 10) || year; |
| var y = String(year).padStart(4, "0"); |
| var mi = String(monthIdx + 1).padStart(2, "0"); |
| var parts = (timeInput.value || "").split("-"); |
| var day = (parts.length >= 3 && parts[2]) ? parts[2] : "01"; |
| timeInput.value = y + "-" + mi + "-" + day; |
| timeInput.dispatchEvent(new Event("input", { bubbles: true })); |
| timeInput.dispatchEvent(new Event("blur", { bubbles: true })); |
| } |
| |
| function syncFromTimeInput() { |
| var timeInput = getTimeInput(); |
| if (!timeInput || !timeInput.value) return; |
| var parts = timeInput.value.split("-"); |
| if (parts.length >= 2) { |
| var y = parseInt(parts[0], 10); |
| var mi = parseInt(parts[1], 10) - 1; |
| if (!isNaN(y)) year = y; |
| if (mi >= 0 && mi < 12) monthIdx = mi; |
| renderHeader(); |
| } |
| } |
| |
| var navButtons = pickerHeader.querySelectorAll(".nav-button"); |
| if (navButtons.length >= 2) { |
| navButtons[0].addEventListener("click", function () { |
| setTimeout(function () { |
| monthIdx--; |
| if (monthIdx < 0) { monthIdx = 11; year--; } |
| renderHeader(); |
| }, 0); |
| }); |
| navButtons[1].addEventListener("click", function () { |
| setTimeout(function () { |
| monthIdx++; |
| if (monthIdx > 11) { monthIdx = 0; year++; } |
| renderHeader(); |
| }, 0); |
| }); |
| } |
| |
| yearInput.addEventListener("change", function () { |
| applyYearToField(yearInput.value); |
| }); |
| |
| var timeInput = getTimeInput(); |
| if (timeInput) { |
| timeInput.addEventListener("input", syncFromTimeInput); |
| timeInput.addEventListener("change", syncFromTimeInput); |
| } |
| |
| var picker = pickerHeader.closest(".picker"); |
| if (picker) { |
| picker.addEventListener("click", function (e) { |
| if (e.target.closest(".day")) { |
| setTimeout(syncFromTimeInput, 50); |
| } |
| }); |
| } |
| |
| monthYear.appendChild(monthSpan); |
| monthYear.appendChild(yearInput); |
| }); |
| } |
| if (typeof MutationObserver !== "undefined") { |
| var obs = new MutationObserver(wireAll); |
| obs.observe(document.documentElement, { childList: true, subtree: true }); |
| } |
| function wireSplitYearInputs() { |
| ["prof-dob-year", "appt-date-year"].forEach(function (rootId) { |
| var root = document.getElementById(rootId); |
| if (!root) return; |
| var input = root.querySelector("input"); |
| if (!input || input.dataset.dobYearWired) return; |
| input.dataset.dobYearWired = "1"; |
| input.setAttribute("maxlength", "4"); |
| input.setAttribute("inputmode", "numeric"); |
| input.setAttribute("pattern", "[0-9]*"); |
| input.setAttribute("autocomplete", "bday-year"); |
| input.addEventListener("input", function () { |
| var digits = input.value.replace(/\\D/g, "").slice(0, 4); |
| if (input.value !== digits) input.value = digits; |
| }); |
| input.addEventListener("paste", function (e) { |
| e.preventDefault(); |
| var text = (e.clipboardData || window.clipboardData).getData("text") || ""; |
| input.value = text.replace(/\\D/g, "").slice(0, 4); |
| input.dispatchEvent(new Event("input", { bubbles: true })); |
| }); |
| }); |
| } |
| function wireFieldCardClicks() { |
| document.querySelectorAll( |
| ".field-card-view:not([data-card-click-wired]), .dob-card-view:not([data-card-click-wired])" |
| ).forEach(function (view) { |
| if (view.closest(".med-catalog-card")) return; |
| view.dataset.cardClickWired = "1"; |
| view.setAttribute("role", "button"); |
| view.setAttribute("tabindex", "0"); |
| view.setAttribute("aria-label", "Click to edit this field"); |
| function openEdit() { |
| var btn = view.querySelector("button.field-edit-link, button.dob-edit-link"); |
| if (btn && !btn.disabled) btn.click(); |
| } |
| view.addEventListener("click", function () { openEdit(); }); |
| view.addEventListener("keydown", function (e) { |
| if (e.key === "Enter" || e.key === " ") { |
| e.preventDefault(); |
| openEdit(); |
| } |
| }); |
| }); |
| } |
| function wireVisitPurposeCards() { |
| document.querySelectorAll( |
| ".visit-purpose-card-shell:not([data-purpose-wired])" |
| ).forEach(function (shell) { |
| shell.dataset.purposeWired = "1"; |
| shell.setAttribute("role", "button"); |
| shell.setAttribute("tabindex", "0"); |
| shell.setAttribute("aria-label", "Click to expand or collapse this section"); |
| function togglePanel() { |
| var btn = shell.querySelector( |
| "button.visit-purpose-toggle, .visit-purpose-toggle button" |
| ); |
| if (btn && !btn.disabled) btn.click(); |
| } |
| shell.addEventListener("keydown", function (e) { |
| if (e.key === "Enter" || e.key === " ") { |
| e.preventDefault(); |
| togglePanel(); |
| } |
| }); |
| }); |
| } |
| function wireMedCatalogCards() { |
| document.querySelectorAll( |
| ".med-catalog-card:not([data-med-catalog-wired])" |
| ).forEach(function (card) { |
| card.dataset.medCatalogWired = "1"; |
| function toggleCard() { |
| var btn = card.querySelector("button.med-catalog-toggle"); |
| if (btn && !btn.disabled) btn.click(); |
| } |
| card.querySelectorAll(".field-card-view").forEach(function (view) { |
| if (view.dataset.medViewWired) return; |
| view.dataset.medViewWired = "1"; |
| view.setAttribute("role", "button"); |
| view.setAttribute("tabindex", "0"); |
| view.setAttribute("aria-label", "Click to open or close this section"); |
| view.addEventListener("click", function (e) { |
| if (e.target.closest("button, input, textarea, select, a")) return; |
| toggleCard(); |
| }); |
| view.addEventListener("keydown", function (e) { |
| if (e.key === "Enter" || e.key === " ") { |
| e.preventDefault(); |
| toggleCard(); |
| } |
| }); |
| }); |
| var header = card.querySelector(".med-catalog-header"); |
| if (header && !header.dataset.medHeaderWired) { |
| header.dataset.medHeaderWired = "1"; |
| header.addEventListener("click", function (e) { |
| if (e.target.closest("button.med-catalog-toggle")) return; |
| toggleCard(); |
| }); |
| } |
| }); |
| } |
| function wireDocsIndexDeletes() { |
| var nameRoot = document.getElementById("doc-delete-name"); |
| if (!nameRoot) return; |
| var nameInput = nameRoot.querySelector("textarea, input"); |
| var triggerBtn = document.querySelector("#docs-delete-trigger button") |
| || document.getElementById("docs-delete-trigger"); |
| if (!nameInput || !triggerBtn) return; |
| document.querySelectorAll( |
| ".settings-documents-shell .docs-delete-btn:not([data-del-wired])" |
| ).forEach(function (btn) { |
| btn.dataset.delWired = "1"; |
| btn.addEventListener("click", function (e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| var docName = btn.getAttribute("data-doc-name") || ""; |
| nameInput.value = docName; |
| nameInput.dispatchEvent(new Event("input", { bubbles: true })); |
| nameInput.dispatchEvent(new Event("change", { bubbles: true })); |
| triggerBtn.click(); |
| }); |
| }); |
| } |
| function wireMainTabTooltips() { |
| var tips = { |
| "π Home": "Today's health brief plus AI chat for the signed-in care profile.", |
| "π Medications": "Manage prescription lists, schedules, and active dosages.", |
| "π½οΈ Diet & Allergies": "Track nutritional goals, dietary restrictions, and allergic reactions.", |
| "π
Appointments": "Schedule visits, set reminders, and review upcoming or past appointments.", |
| "π Doctors Visit": "Compile pre-visit clinic forms, track symptoms, and export appointment summaries.", |
| "βοΈ Settings & Profile": "Manage family care profiles, insurance records, and static health documents." |
| }; |
| document.querySelectorAll('button[role="tab"]').forEach(function (btn) { |
| var label = (btn.textContent || "").replace(/\\s+/g, " ").trim(); |
| var tip = tips[label]; |
| if (!tip) return; |
| if (btn.dataset.tabTipWired === label) return; |
| btn.dataset.tabTipWired = label; |
| btn.setAttribute("title", tip); |
| btn.setAttribute("aria-description", tip); |
| }); |
| } |
| function wireAll() { |
| wirePickerYears(); |
| wireSplitYearInputs(); |
| wireFieldCardClicks(); |
| wireVisitPurposeCards(); |
| wireMedCatalogCards(); |
| wireMainTabTooltips(); |
| wireDocsIndexDeletes(); |
| } |
| document.addEventListener("DOMContentLoaded", wireAll); |
| wireAll(); |
| })(); |
| </script> |
| """ |
|
|
|
|
| def _fmt_date(d: str) -> str: |
| """Format YYYY-MM-DD to Month D, YYYY.""" |
| try: |
| return datetime.strptime(d, "%Y-%m-%d").strftime("%b %d, %Y") |
| except Exception: |
| return d |
|
|
|
|
| _DOB_MONTH_ABBRS = [ |
| "Jan", "Feb", "Mar", "Apr", "May", "Jun", |
| "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", |
| ] |
| _DOB_ABBR_TO_NUM = {abbr: idx + 1 for idx, abbr in enumerate(_DOB_MONTH_ABBRS)} |
| _DOB_NUM_TO_ABBR = {idx + 1: abbr for idx, abbr in enumerate(_DOB_MONTH_ABBRS)} |
| _DOB_DAY_CHOICES = [str(day) for day in range(1, 32)] |
|
|
|
|
| def _format_dob_card(iso: str) -> str: |
| """Long-form card label, e.g. June 15, 1945.""" |
| iso = _safe_str(iso) |
| if not iso: |
| return "Not set" |
| try: |
| return datetime.strptime(iso[:10], "%Y-%m-%d").strftime("%B %d, %Y") |
| except Exception: |
| return iso |
|
|
|
|
| def _split_iso_dob(iso: str) -> tuple[Optional[str], Optional[str], str]: |
| """Parse YYYY-MM-DD into (month_abbr, day_str, year_str).""" |
| iso = _safe_str(iso) |
| if not iso: |
| return None, None, "" |
| try: |
| dt = datetime.strptime(iso[:10], "%Y-%m-%d") |
| return _DOB_NUM_TO_ABBR[dt.month], str(dt.day), str(dt.year) |
| except Exception: |
| return None, None, "" |
|
|
|
|
| def _compose_iso_dob(month_abbr, day, year) -> Optional[str]: |
| """Build YYYY-MM-DD from split fields; None if invalid.""" |
| m_abbr = _safe_str(month_abbr) |
| day_s = _safe_str(day) |
| year_s = _safe_str(year) |
| if not m_abbr or not day_s or not year_s: |
| return None |
| if not re.fullmatch(r"\d{4}", year_s): |
| return None |
| month_num = _DOB_ABBR_TO_NUM.get(m_abbr) |
| if not month_num: |
| return None |
| try: |
| dt = date(int(year_s), month_num, int(day_s)) |
| return dt.strftime("%Y-%m-%d") |
| except (ValueError, OverflowError): |
| return None |
|
|
|
|
| def _sync_dob_card(iso: str): |
| """Refresh card view from canonical YYYY-MM-DD value.""" |
| month, day, year = _split_iso_dob(iso) |
| return ( |
| gr.update(value=_format_dob_card(iso)), |
| gr.update(value=month), |
| gr.update(value=day), |
| gr.update(value=year), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _dob_begin_edit(iso: str): |
| """Switch card into inline split edit mode.""" |
| month, day, year = _split_iso_dob(iso) |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=True), |
| gr.update(value=month or "Jan"), |
| gr.update(value=day or "1"), |
| gr.update(value=year), |
| ) |
|
|
|
|
| def _dob_save_edit(month, day, year, current_iso: str): |
| """Validate split fields and return to card view.""" |
| iso = _compose_iso_dob(month, day, year) |
| if not iso: |
| gr.Warning("β οΈ Enter a valid date β pick month/day and a 4-digit year.") |
| return ( |
| gr.update(value=current_iso), |
| gr.update(value=_format_dob_card(current_iso)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
| return ( |
| gr.update(value=iso), |
| gr.update(value=_format_dob_card(iso)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _dob_cancel_edit(iso: str): |
| """Discard inline edits and restore card view.""" |
| return _sync_dob_card(iso) |
|
|
|
|
| def _sync_appt_date_display(iso: str): |
| """Refresh appointment date card view from canonical YYYY-MM-DD.""" |
| month, day, year = _split_iso_dob(iso) |
| return ( |
| gr.update(value=_format_dob_card(iso)), |
| gr.update(value=month), |
| gr.update(value=day), |
| gr.update(value=year), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _appt_date_begin_edit(iso: str): |
| """Open inline month/day/year picker for appointment date.""" |
| iso = _safe_str(iso) |
| if iso: |
| month, day, year = _split_iso_dob(iso) |
| else: |
| today = date.today() |
| month = _DOB_NUM_TO_ABBR[today.month] |
| day = str(today.day) |
| year = str(today.year) |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=True), |
| gr.update(value=month or "Jan"), |
| gr.update(value=day or "1"), |
| gr.update(value=year), |
| ) |
|
|
|
|
| def _appt_date_save_edit(month, day, year, current_iso: str): |
| """Validate split fields and return appointment date to card view.""" |
| iso = _compose_iso_dob(month, day, year) |
| if not iso: |
| gr.Warning("β οΈ Enter a valid date β pick month/day and a 4-digit year.") |
| return ( |
| gr.update(value=current_iso), |
| gr.update(value=_format_dob_card(current_iso)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
| return ( |
| gr.update(value=iso), |
| gr.update(value=_format_dob_card(iso)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _appt_date_cancel_edit(iso: str): |
| """Discard inline date edits and restore card view.""" |
| return _sync_appt_date_display(iso) |
|
|
|
|
| def _format_field_card(value) -> str: |
| """Display label for generic text field cards.""" |
| value = _safe_str(value) |
| return value if value else "Not set" |
|
|
|
|
| def _normalize_phone_digits(value) -> str: |
| return re.sub(r"\D", "", _safe_str(value)) |
|
|
|
|
| def _format_us_phone(value) -> str: |
| """Format common US phone lengths; otherwise return trimmed input.""" |
| raw = _safe_str(value) |
| if not raw: |
| return "" |
| digits = _normalize_phone_digits(raw) |
| if len(digits) == 10: |
| return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}" |
| if len(digits) == 11 and digits[0] == "1": |
| return f"+1 ({digits[1:4]}) {digits[4:7]}-{digits[7:]}" |
| if len(digits) == 7: |
| return f"{digits[:3]}-{digits[3:]}" |
| return raw.strip() |
|
|
|
|
| def _format_phone_card(value) -> str: |
| formatted = _format_us_phone(value) |
| return formatted if formatted else "Not set" |
|
|
|
|
| def _sync_phone_field_card(value): |
| """Refresh a phone field card view from its canonical value.""" |
| return ( |
| gr.update(value=_format_phone_card(value)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _field_save_phone_edit(value): |
| """Save a phone field card and return to view mode.""" |
| value = _format_us_phone(value) |
| return ( |
| gr.update(value=_format_phone_card(value)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=value), |
| ) |
|
|
|
|
| def _field_cancel_phone_edit(snapshot): |
| """Discard phone field card edits.""" |
| value = _format_us_phone(_safe_str((snapshot or {}).get("value", ""))) |
| return ( |
| gr.update(value=_format_phone_card(value)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=value), |
| ) |
|
|
|
|
| def _format_address_card(line1, line2, city, state, zip_code) -> str: |
| composed = profiles.compose_address(line1, line2, city, state, zip_code) |
| return composed.replace("\n", " \n") if composed else "Not set" |
|
|
|
|
| def _sync_address_card(line1, line2, city, state, zip_code): |
| return ( |
| gr.update(value=_format_address_card(line1, line2, city, state, zip_code)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _address_begin_edit(line1, line2, city, state, zip_code): |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=True), |
| { |
| "line1": _safe_str(line1), |
| "line2": _safe_str(line2), |
| "city": _safe_str(city), |
| "state": _safe_str(state), |
| "zip": _safe_str(zip_code), |
| }, |
| ) |
|
|
|
|
| def _address_save_edit(line1, line2, city, state, zip_code): |
| line1, line2, city, state, zip_code = map( |
| _safe_str, (line1, line2, city, state, zip_code), |
| ) |
| return ( |
| gr.update(value=_format_address_card(line1, line2, city, state, zip_code)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=line1), |
| gr.update(value=line2), |
| gr.update(value=city), |
| gr.update(value=state), |
| gr.update(value=zip_code), |
| ) |
|
|
|
|
| def _address_cancel_edit(snapshot): |
| snap = snapshot or {} |
| line1 = _safe_str(snap.get("line1", "")) |
| line2 = _safe_str(snap.get("line2", "")) |
| city = _safe_str(snap.get("city", "")) |
| state = _safe_str(snap.get("state", "")) |
| zip_code = _safe_str(snap.get("zip", "")) |
| return ( |
| gr.update(value=_format_address_card(line1, line2, city, state, zip_code)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=line1), |
| gr.update(value=line2), |
| gr.update(value=city), |
| gr.update(value=state), |
| gr.update(value=zip_code), |
| ) |
|
|
|
|
| def _pcp_address_begin_edit(line1, line2, city, state, zip_code): |
| return _address_begin_edit(line1, line2, city, state, zip_code) |
|
|
|
|
| def _pcp_address_save_edit(line1, line2, city, state, zip_code): |
| return _address_save_edit(line1, line2, city, state, zip_code) |
|
|
|
|
| def _pcp_sync_address_card(line1, line2, city, state, zip_code): |
| return _sync_address_card(line1, line2, city, state, zip_code) |
|
|
|
|
| def _patient_address_profile_fields(line1, line2, city, state, zip_code): |
| line1, line2, city, state, zip_code = map( |
| _safe_str, (line1, line2, city, state, zip_code), |
| ) |
| composed = profiles.compose_address(line1, line2, city, state, zip_code) |
| return { |
| "address_line1": line1, |
| "address_line2": line2, |
| "address_city": city, |
| "address_state": state, |
| "address_zip": zip_code, |
| "street_address": composed, |
| } |
|
|
|
|
| def _pcp_address_profile_fields(line1, line2, city, state, zip_code): |
| line1, line2, city, state, zip_code = map( |
| _safe_str, (line1, line2, city, state, zip_code), |
| ) |
| composed = profiles.compose_address(line1, line2, city, state, zip_code) |
| return { |
| "pcp_address_line1": line1, |
| "pcp_address_line2": line2, |
| "pcp_address_city": city, |
| "pcp_address_state": state, |
| "pcp_address_zip": zip_code, |
| "pcp_address": composed, |
| } |
|
|
|
|
| def _sync_field_card(value): |
| """Refresh a text field card view from its canonical value.""" |
| return ( |
| gr.update(value=_format_field_card(value)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _field_begin_edit(value): |
| """Switch a text field card into inline edit mode.""" |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=True), |
| {"value": _safe_str(value)}, |
| ) |
|
|
|
|
| def _field_save_edit(value): |
| """Save a text field card and return to view mode.""" |
| value = _safe_str(value) |
| return ( |
| gr.update(value=_format_field_card(value)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=value), |
| ) |
|
|
|
|
| def _field_cancel_edit(snapshot): |
| """Discard text field card edits.""" |
| value = _safe_str((snapshot or {}).get("value", "")) |
| return ( |
| gr.update(value=_format_field_card(value)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=value), |
| ) |
|
|
|
|
| def _format_contact_card(name, phone, rel) -> str: |
| """Display label for a single emergency contact card.""" |
| name, phone, rel = _safe_str(name), _format_us_phone(phone), _safe_str(rel) |
| if not name and not phone: |
| return "Not set" |
| if phone and name: |
| if rel: |
| return f"{name} ({rel}) Β· {phone}" |
| return f"{name} Β· {phone}" |
| return phone or name |
|
|
|
|
| def _sync_contact_card(name, phone, rel): |
| """Refresh one emergency contact card view.""" |
| return ( |
| gr.update(value=_format_contact_card(name, phone, rel)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _contact_begin_edit(name, phone, rel): |
| """Switch one emergency contact card into inline edit mode.""" |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=True), |
| { |
| "name": _safe_str(name), |
| "phone": _format_us_phone(phone), |
| "rel": _safe_str(rel), |
| }, |
| ) |
|
|
|
|
| def _contact_save_edit(name, phone, rel): |
| """Save one emergency contact card and return to view mode.""" |
| name = _safe_str(name) |
| phone = _format_us_phone(phone) |
| rel = _safe_str(rel) |
| return ( |
| gr.update(value=_format_contact_card(name, phone, rel)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=name), |
| gr.update(value=phone), |
| gr.update(value=rel), |
| ) |
|
|
|
|
| def _contact_cancel_edit(snapshot): |
| """Discard one emergency contact card edit.""" |
| snap = snapshot or {} |
| name = _safe_str(snap.get("name", "")) |
| phone = _format_us_phone(snap.get("phone", "")) |
| rel = _safe_str(snap.get("rel", "")) |
| return ( |
| gr.update(value=_format_contact_card(name, phone, rel)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=name), |
| gr.update(value=phone), |
| gr.update(value=rel), |
| ) |
|
|
|
|
| def _contact2_remove(): |
| """Clear contact 2 fields and hide the second card.""" |
| return ( |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| *_sync_contact_card("", "", ""), |
| ) |
|
|
|
|
| def _format_emergency_card(c1_name, c1_phone, c1_rel, |
| c2_name, c2_phone, c2_rel) -> str: |
| """Display label for the emergency contacts card.""" |
| def _line(name, phone): |
| name, phone = _safe_str(name), _format_us_phone(phone) |
| if not name and not phone: |
| return None |
| if phone and name: |
| return f"{name} Β· {phone}" |
| return phone or name |
|
|
| lines = [] |
| first = _line(c1_name, c1_phone) |
| if first: |
| lines.append(first) |
| second = _line(c2_name, c2_phone) |
| if second: |
| lines.append(second) |
| if not lines: |
| return "Not set" |
| if len(lines) == 1: |
| return lines[0] |
| return f"{lines[0]} (+{len(lines) - 1} more)" |
|
|
|
|
| def _sync_emergency_card(c1_name, c1_phone, c1_rel, |
| c2_name, c2_phone, c2_rel): |
| """Refresh emergency contacts card view from field values.""" |
| return ( |
| gr.update(value=_format_emergency_card( |
| c1_name, c1_phone, c1_rel, c2_name, c2_phone, c2_rel, |
| )), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _emergency_begin_edit(c1_name, c1_phone, c1_rel, |
| c2_name, c2_phone, c2_rel): |
| """Switch emergency contacts card into inline edit mode.""" |
| c2_name = _safe_str(c2_name) |
| c2_phone = _safe_str(c2_phone) |
| c2_rel = _safe_str(c2_rel) |
| has_c2 = bool(c2_name or c2_phone or c2_rel) |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=True), |
| { |
| "c1_name": _safe_str(c1_name), |
| "c1_phone": _safe_str(c1_phone), |
| "c1_rel": _safe_str(c1_rel), |
| "c2_name": c2_name, |
| "c2_phone": c2_phone, |
| "c2_rel": c2_rel, |
| "c2_visible": has_c2, |
| }, |
| gr.update(visible=has_c2), |
| gr.update(visible=has_c2), |
| ) |
|
|
|
|
| def _emergency_save_edit(c1_name, c1_phone, c1_rel, |
| c2_name, c2_phone, c2_rel): |
| """Save emergency contacts card and return to view mode.""" |
| c1_name = _safe_str(c1_name) |
| c1_phone = _format_us_phone(c1_phone) |
| c1_rel = _safe_str(c1_rel) |
| c2_name = _safe_str(c2_name) |
| c2_phone = _format_us_phone(c2_phone) |
| c2_rel = _safe_str(c2_rel) |
| has_c2 = bool(c2_name or c2_phone or c2_rel) |
| return ( |
| gr.update(value=_format_emergency_card( |
| c1_name, c1_phone, c1_rel, c2_name, c2_phone, c2_rel, |
| )), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=c1_name), |
| gr.update(value=c1_phone), |
| gr.update(value=c1_rel), |
| gr.update(value=c2_name), |
| gr.update(value=c2_phone), |
| gr.update(value=c2_rel), |
| gr.update(visible=has_c2), |
| gr.update(visible=has_c2), |
| ) |
|
|
|
|
| def _emergency_cancel_edit(snapshot): |
| """Discard emergency contacts card edits.""" |
| snap = snapshot or {} |
| c1_name = _safe_str(snap.get("c1_name", "")) |
| c1_phone = _format_us_phone(snap.get("c1_phone", "")) |
| c1_rel = _safe_str(snap.get("c1_rel", "")) |
| c2_name = _safe_str(snap.get("c2_name", "")) |
| c2_phone = _format_us_phone(snap.get("c2_phone", "")) |
| c2_rel = _safe_str(snap.get("c2_rel", "")) |
| c2_visible = bool(snap.get("c2_visible", False)) |
| return ( |
| gr.update(value=_format_emergency_card( |
| c1_name, c1_phone, c1_rel, c2_name, c2_phone, c2_rel, |
| )), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=c1_name), |
| gr.update(value=c1_phone), |
| gr.update(value=c1_rel), |
| gr.update(value=c2_name), |
| gr.update(value=c2_phone), |
| gr.update(value=c2_rel), |
| gr.update(visible=c2_visible), |
| gr.update(visible=c2_visible), |
| ) |
|
|
|
|
| def _build_emergency_contacts(c1_name, c1_phone, c1_rel, |
| c2_name, c2_phone, c2_rel): |
| """Normalize contact fields into the profile emergency_contacts list.""" |
| contacts = [] |
| if _safe_str(c1_name) or _safe_str(c1_phone): |
| contacts.append({ |
| "name": _safe_str(c1_name), |
| "phone": _format_us_phone(c1_phone), |
| "relationship": _safe_str(c1_rel), |
| }) |
| if _safe_str(c2_name) or _safe_str(c2_phone): |
| contacts.append({ |
| "name": _safe_str(c2_name), |
| "phone": _format_us_phone(c2_phone), |
| "relationship": _safe_str(c2_rel), |
| }) |
| return contacts |
|
|
|
|
| def _format_caregiver_card(name) -> str: |
| """Display label for the caregiver name card.""" |
| return _format_field_card(name) |
|
|
|
|
| def _sync_caregiver_card(name): |
| """Refresh caregiver card view from field value.""" |
| return ( |
| gr.update(value=_format_caregiver_card(name)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _caregiver_begin_edit(name): |
| """Switch caregiver card into inline edit mode.""" |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=True), |
| {"name": _safe_str(name)}, |
| ) |
|
|
|
|
| def _caregiver_save_edit(name): |
| """Save caregiver name and return to card view.""" |
| name = _safe_str(name) |
| return ( |
| gr.update(value=_format_caregiver_card(name)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=name), |
| ) |
|
|
|
|
| def _caregiver_cancel_edit(snapshot): |
| """Discard caregiver edits and restore card view.""" |
| name = _safe_str((snapshot or {}).get("name", "")) |
| return ( |
| gr.update(value=_format_caregiver_card(name)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=name), |
| ) |
|
|
|
|
| def _format_relationship_card(category, detail, custom) -> str: |
| """Display label for the relationship card.""" |
| if _safe_str(category) == "Self": |
| return "Not set" |
| label = _compute_label(_safe_str(category), _safe_str(detail), _safe_str(custom)) |
| return label if label else "Not set" |
|
|
|
|
| def _relationship_edit_fields(category, detail, custom): |
| """Dropdown visibility/values when opening the relationship card editor.""" |
| category = _safe_str(category) or _RELATIONSHIP_DEFAULT_CATEGORY |
| detail = _safe_str(detail) |
| custom = _safe_str(custom) |
| if category in _RELATIONSHIP_DETAIL_CHOICES: |
| sub_choices = _RELATIONSHIP_DETAIL_CHOICES[category] |
| det_val = detail if detail in sub_choices else sub_choices[0] |
| return ( |
| gr.update(value=category), |
| gr.update(visible=True, choices=sub_choices, value=det_val), |
| gr.update(visible=False, value=custom), |
| ) |
| if category == "Other": |
| return ( |
| gr.update(value=category), |
| gr.update(visible=False, value=_RELATIONSHIP_DETAIL_DEFAULT), |
| gr.update(visible=True, value=custom), |
| ) |
| return ( |
| gr.update(value=category), |
| gr.update(visible=False, value=_RELATIONSHIP_DETAIL_DEFAULT), |
| gr.update(visible=False, value=custom), |
| ) |
|
|
|
|
| def _sync_relationship_card(category, detail, custom): |
| """Refresh relationship card view from field values.""" |
| return ( |
| gr.update(value=_format_relationship_card(category, detail, custom)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _relationship_begin_edit(category, detail, custom): |
| """Switch relationship card into inline edit mode.""" |
| cat_up, det_up, cus_up = _relationship_edit_fields(category, detail, custom) |
| snapshot = { |
| "category": _safe_str(category) or _RELATIONSHIP_DEFAULT_CATEGORY, |
| "detail": _safe_str(detail), |
| "custom": _safe_str(custom), |
| } |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=True), |
| cat_up, |
| det_up, |
| cus_up, |
| snapshot, |
| ) |
|
|
|
|
| def _relationship_save_edit(category, detail, custom): |
| """Validate and close relationship card editor.""" |
| if not _compute_label(_safe_str(category), _safe_str(detail), _safe_str(custom)): |
| gr.Warning("β οΈ Pick a relationship category.") |
| return ( |
| gr.update(), |
| gr.update(visible=False), |
| gr.update(visible=True), |
| ) |
| return ( |
| gr.update(value=_format_relationship_card(category, detail, custom)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _relationship_cancel_edit(snapshot): |
| """Discard relationship edits and restore card view.""" |
| if not snapshot: |
| return ( |
| gr.update(value="Not set"), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(value=_RELATIONSHIP_DEFAULT_CATEGORY), |
| gr.update(visible=False, value=_RELATIONSHIP_DETAIL_DEFAULT), |
| gr.update(visible=False, value=""), |
| ) |
| cat_up, det_up, cus_up = _relationship_edit_fields( |
| snapshot.get("category"), |
| snapshot.get("detail"), |
| snapshot.get("custom"), |
| ) |
| return ( |
| gr.update(value=_format_relationship_card( |
| snapshot.get("category"), |
| snapshot.get("detail"), |
| snapshot.get("custom"), |
| )), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| cat_up, |
| det_up, |
| cus_up, |
| ) |
|
|
|
|
| def _parse_date_from_picker(val) -> str: |
| """Convert gr.DateTime output to YYYY-MM-DD string. |
| |
| Gradio returns different formats depending on version/widget config: |
| - Unix timestamp (int/float): 1781931600 |
| - ISO string: "2026-06-11" or "2026-06-11T00:00:00" |
| - datetime object |
| """ |
| if not val: |
| return "" |
| if isinstance(val, (int, float)): |
| return datetime.fromtimestamp(val).strftime("%Y-%m-%d") |
| if isinstance(val, str): |
| val = val.strip() |
| |
| if val.isdigit() or (val.startswith("-") and val[1:].isdigit()): |
| return datetime.fromtimestamp(int(val)).strftime("%Y-%m-%d") |
| |
| return val[:10] |
| |
| if hasattr(val, "strftime"): |
| return val.strftime("%Y-%m-%d") |
| return str(val)[:10] |
|
|
|
|
| def _time_to_parts(t: str) -> tuple: |
| """Parse stored time string into (hour, minute, ampm).""" |
| if not t: |
| return ("12", "00", "PM") |
| t = t.strip() |
| m = re.match(r"(\d{1,2}):(\d{2})\s*(AM|PM)", t, re.IGNORECASE) |
| if m: |
| return (m.group(1), m.group(2), m.group(3).upper()) |
| m = re.match(r"(\d{1,2}):(\d{2})$", t) |
| if m: |
| h, mn = int(m.group(1)), m.group(2) |
| if h == 0: |
| return ("12", mn, "AM") |
| elif h < 12: |
| return (str(h), mn, "AM") |
| elif h == 12: |
| return ("12", mn, "PM") |
| else: |
| return (str(h - 12), mn, "PM") |
| return ("12", "00", "PM") |
|
|
|
|
| def _parts_to_time(hour: str, minute: str, ampm: str) -> str: |
| """Combine dropdown values into a time string.""" |
| return f"{hour}:{minute} {ampm}" if hour and minute and ampm else "" |
|
|
|
|
| def _days_label(d: str) -> str: |
| try: |
| days = (date.fromisoformat(d) - date.today()).days |
| if days == 0: |
| return "Today" |
| elif days == 1: |
| return "Tomorrow" |
| elif days < 0: |
| return f"{abs(days)}d ago" |
| else: |
| return f"In {days}d" |
| except Exception: |
| return "" |
|
|
|
|
| |
| |
| |
|
|
| def handle_pdf_upload(files, profile_id=None) -> str: |
| """Ingest uploaded PDFs and return status.""" |
| rag = _safe_import_rag() |
| if rag is None: |
| return "β rag_engine could not be loaded. Check your dependencies." |
| if not profile_id: |
| return "β Select an active care profile in Settings & Profiles first." |
|
|
| if not files: |
| return "No files received." |
|
|
| messages = [] |
| for file in (files if isinstance(files, list) else [files]): |
| try: |
| src = Path(file.name) |
| dest = cfg.DOCUMENTS_DIR / src.name |
| shutil.copy2(str(src), str(dest)) |
| status = rag.index_document(dest, profile_id=profile_id) |
| messages.append(status) |
| except Exception as e: |
| messages.append(f"β Error processing {Path(file.name).name}: {e}") |
|
|
| return "\n\n".join(messages) |
|
|
|
|
| def handle_chat(message: str, history: list, profile_id=None) -> tuple[str, list]: |
| """RAG chat handler.""" |
| rag = _safe_import_rag() |
| if rag is None: |
| reply = "β AI features unavailable. Install dependencies from requirements.txt." |
| history.append({"role": "user", "content": message}) |
| history.append({"role": "assistant", "content": reply}) |
| return "", history |
|
|
| if not profile_id: |
| reply = ( |
| "π Sign in with an active care profile (Settings & Profiles) to use " |
| "the assistant β it reads that profile's data and documents." |
| ) |
| history.append({"role": "user", "content": message}) |
| history.append({"role": "assistant", "content": reply}) |
| return "", history |
|
|
| if not message.strip(): |
| return "", history |
|
|
| answer, sources = rag.answer_question(message, profile_id=profile_id) |
|
|
| |
| if sources: |
| src_lines = "\n\n---\nπ **Sources:**" |
| seen = set() |
| for s in sources: |
| key = f"{s['source']}, p.{s['page']}" |
| if key not in seen: |
| src_lines += f"\nβ’ {key}" |
| seen.add(key) |
| answer += src_lines |
|
|
| history.append({"role": "user", "content": message}) |
| history.append({"role": "assistant", "content": answer}) |
| return "", history |
|
|
|
|
| def get_indexed_docs_text(profile_id=None) -> str: |
| rag = _safe_import_rag() |
| if rag is None: |
| return "β οΈ RAG engine not available." |
| status = rag.get_document_status(profile_id=profile_id or "") |
| if not status["indexed_sources"]: |
| return "π No documents indexed yet. Upload a PDF above." |
| sources_list = "\n".join(f" β’ {s}" for s in status["indexed_sources"]) |
| return ( |
| f"β
{status['num_documents']} document(s), " |
| f"{status['total_chunks']} chunks indexed:\n{sources_list}" |
| ) |
|
|
|
|
| |
|
|
| def _indexed_sources_list(profile_id=None) -> list[str]: |
| """Return indexed document source names for the active profile.""" |
| rag = _safe_import_rag() |
| if rag is None: |
| return [] |
| status = rag.get_document_status(profile_id=profile_id or "") |
| return status.get("indexed_sources", []) |
|
|
|
|
| def _docs_index_html(profile_id=None) -> str: |
| """HTML list of indexed documents with per-row delete controls.""" |
| sources = _indexed_sources_list(profile_id) |
| if not sources: |
| return '<p class="docs-index-empty">π No documents indexed yet.</p>' |
| rows = [] |
| for source in sources: |
| esc = html.escape(source, quote=True) |
| label = html.escape(source) |
| rows.append( |
| f'<div class="docs-index-row">' |
| f'<span class="docs-index-name">{label}</span>' |
| f'<button type="button" class="docs-delete-btn" ' |
| f'data-doc-name="{esc}">ποΈ Delete</button>' |
| f"</div>" |
| ) |
| return f'<div class="docs-index-list">{"".join(rows)}</div>' |
|
|
|
|
| def _docs_upload_toast(files, profile_id): |
| """Upload files and refresh the indexed-documents panel.""" |
| html_up = gr.update(value=_docs_index_html(profile_id)) |
| if not files: |
| return gr.Warning("β οΈ Upload failed, please try again."), html_up, None |
| result = handle_pdf_upload(files, profile_id) |
| if "β
" in result and "β" not in result: |
| return gr.Info("β Document uploaded successfully"), html_up, None |
| return gr.Warning("β οΈ Upload failed, please try again."), html_up, None |
|
|
|
|
| def _docs_delete_toast(source_name: str, profile_id): |
| """Delete one indexed document and refresh the list.""" |
| html_up = gr.update(value=_docs_index_html(profile_id)) |
| if not (source_name or "").strip(): |
| return gr.Warning("β οΈ Delete failed, please try again."), html_up |
| rag = _safe_import_rag() |
| if rag is None: |
| return gr.Warning("β οΈ Delete failed, please try again."), html_up |
| result = rag.delete_document(source_name.strip()) |
| if result.startswith("β
"): |
| return gr.Info("β Document deleted successfully"), html_up |
| return gr.Warning("β οΈ Delete failed, please try again."), html_up |
|
|
|
|
| def _docs_toggle_index_panel(is_open: bool, profile_id): |
| """Show or hide the indexed-documents directory panel.""" |
| new_open = not bool(is_open) |
| label = "π Hide Indexed Documents" if new_open else "π View Indexed Documents" |
| return ( |
| gr.update(visible=new_open), |
| new_open, |
| gr.update(value=label), |
| gr.update(value=_docs_index_html(profile_id)), |
| ) |
|
|
|
|
| def _docs_refresh_index(profile_id): |
| """Refresh indexed-documents HTML.""" |
| return gr.update(value=_docs_index_html(profile_id)) |
|
|
|
|
| def _med_table_rows(filter_val: str = "all", profile_id=None): |
| """Return rows for the medication dataframe.""" |
| meds = med.get_medications(filter=filter_val, profile_id=profile_id) |
| rows = [] |
| for m in meds: |
| rows.append([ |
| m["name"], |
| m.get("dosage", ""), |
| m.get("frequency", ""), |
| m.get("status", "").capitalize(), |
| m.get("category", ""), |
| m.get("side_effects", "")[:60] + ("β¦" if len(m.get("side_effects","")) > 60 else ""), |
| m["id"], |
| ]) |
| return rows |
|
|
|
|
| def refresh_med_table(filter_val: str = "all", profile_id=None): |
| rows = _med_table_rows(filter_val, profile_id) |
| return gr.Dataframe( |
| value=rows, |
| headers=["Name", "Dosage", "Frequency", "Status", "Category", "Side Effects", "ID"], |
| visible=True, |
| ) |
|
|
|
|
| def _med_choices(profile_id=None) -> list[str]: |
| """Return display labels for the medication delete dropdown.""" |
| meds = med.get_medications(filter="all", profile_id=profile_id) |
| return [f"{m['name']} ({m.get('dosage', '')}) β {m.get('status', 'current')}" for m in meds] |
|
|
|
|
| def _find_med_id_by_label(label: str, profile_id=None) -> str | None: |
| """Find a medication ID by its display label.""" |
| for m in med.get_medications(filter="all", profile_id=profile_id): |
| expected = f"{m['name']} ({m.get('dosage', '')}) β {m.get('status', 'current')}" |
| if expected == label: |
| return m["id"] |
| return None |
|
|
|
|
| def _appt_choices(profile_id=None) -> list[tuple[str, str]]: |
| """Return (display_label, uuid) tuples for appointment dropdowns.""" |
| appts = apt.get_all_appointments(include_past=True, profile_id=profile_id) |
| return [(f"{a['title']} β {a.get('date', '')}", a["id"]) for a in appts] |
|
|
|
|
| def _food_choices(category: str, profile_id=None) -> list[tuple[str, str]]: |
| """Return (display_name, id) tuples β unique IDs prevent duplicate-name checkbox issues.""" |
| data = food.get_food(profile_id=profile_id) |
| entries = data.get(category, []) |
| return [(e["name"], e["id"]) for e in entries] |
|
|
|
|
| def _find_food_id_by_label(category: str, label: str, profile_id=None) -> str | None: |
| """Find a food entry ID by its display label (name).""" |
| data = food.get_food(profile_id=profile_id) |
| for e in data.get(category, []): |
| if e["name"] == label: |
| return e["id"] |
| return None |
|
|
|
|
| def _find_food_entry_by_name(category: str, name: str, profile_id=None) -> dict | None: |
| """Find a full food entry dict by category + name.""" |
| data = food.get_food(profile_id=profile_id) |
| for e in data.get(category, []): |
| if e["name"] == name: |
| return e |
| return None |
|
|
|
|
| def _find_food_entry_by_id(category: str, food_id: str, profile_id=None) -> dict | None: |
| """Find a full food entry dict by category + UUID.""" |
| data = food.get_food(profile_id=profile_id) |
| for e in data.get(category, []): |
| if e["id"] == food_id: |
| return e |
| return None |
|
|
|
|
| |
|
|
|
|
| def _populate_edit_dropdown(category: str, profile_id=None): |
| """Populate the edit selection dropdown when edit category changes.""" |
| if not category: |
| return ( |
| gr.update(choices=[], value=None), |
| "", "", |
| gr.update(visible=False), |
| "mild", |
| None, |
| ) |
| internal = _to_internal(category) |
| return ( |
| gr.update(choices=_food_choices(internal, profile_id), value=None), |
| "", |
| "", |
| gr.update(visible=(internal == "allergies")), |
| "mild", |
| None, |
| ) |
|
|
|
|
| def _populate_edit_fields(category: str, selected_id: str, profile_id=None): |
| """Pre-fill edit form when an entry is selected (selected_id is a UUID from tuple choices).""" |
| if not category or not selected_id: |
| internal = _to_internal(category) if category else None |
| return ( |
| "", "", |
| gr.update(visible=(internal == "allergies") if internal else False), |
| "mild", |
| None, |
| ) |
| internal = _to_internal(category) |
| entry = _find_food_entry_by_id(internal, selected_id, profile_id) |
| if not entry: |
| return ("", "", gr.update(visible=(internal == "allergies")), "mild", None) |
| return ( |
| entry.get("name", ""), |
| entry.get("notes", ""), |
| gr.update(visible=(internal == "allergies")), |
| entry.get("severity", "mild"), |
| category, |
| ) |
|
|
|
|
| def edit_food_handler(category, selected_id, name, notes, severity, del_category, move_category, profile_id): |
| """Save/edit/move a food entry. Execute operation FIRST, THEN compute all returns.""" |
| internal_cat = _to_internal(category) |
| food_id = selected_id |
| move_internal = _to_internal(move_category) if move_category else internal_cat |
| move_label = _cat_label(move_internal) |
|
|
| |
| severity_vis = gr.update(visible=(move_internal == "allergies")) |
|
|
| |
| if not food_id or not name.strip(): |
| return ( |
| gr.Warning("Select an entry and enter a name."), |
| _food_rows("liked_foods", profile_id), _food_rows("disliked_foods", profile_id), _food_rows("allergies", profile_id), |
| gr.update(choices=_food_choices(internal_cat, profile_id), value=None), |
| "", "", severity_vis, "mild", |
| gr.update(), None, |
| ) |
|
|
| |
| if move_internal != internal_cat: |
| food.delete_food(internal_cat, food_id) |
| food.add_food(move_internal, name, notes, severity=severity, profile_id=profile_id) |
| msg = gr.Info(f"β
Moved '{name}' to {move_label}") |
| edit_choices = gr.update(choices=_food_choices(internal_cat, profile_id), value=None) |
| else: |
| food.edit_food(internal_cat, food_id, name, notes, severity) |
| msg = gr.Info(f"β
Updated: {name}") |
| edit_choices = gr.update(choices=_food_choices(internal_cat, profile_id), value=None) |
|
|
| |
| del_internal = _to_internal(del_category) if del_category else None |
| del_choices = gr.update(choices=_food_choices(del_internal, profile_id), value=[]) if del_internal else gr.update(choices=[], value=[]) |
|
|
| return ( |
| msg, |
| _food_rows("liked_foods", profile_id), |
| _food_rows("disliked_foods", profile_id), |
| _food_rows("allergies", profile_id), |
| edit_choices, |
| "", |
| "", |
| severity_vis, |
| "mild", |
| del_choices, |
| None, |
| ) |
|
|
|
|
| def _med_list_summary_md(filter_val: str = "all", profile_id=None) -> str: |
| """One-line medication counts for the list header.""" |
| all_meds = med.get_medications(filter="all", profile_id=profile_id) |
| current_n = len(med.get_medications(filter="current", profile_id=profile_id)) |
| past_n = len(med.get_medications(filter="past", profile_id=profile_id)) |
| if not all_meds: |
| return "*No medications on file yet β use **β Add Medication** below.*" |
| filt = (filter_val or "all").lower() |
| if filt == "current": |
| return f"**{current_n}** current medication(s) shown" |
| if filt == "past": |
| return f"**{past_n}** past medication(s) shown" |
| return f"**{len(all_meds)}** total Β· **{current_n}** current Β· **{past_n}** past" |
|
|
|
|
| def _med_show_display_md(filter_val: str) -> str: |
| labels = { |
| "all": "All medications", |
| "current": "Current medications only", |
| "past": "Past medications only", |
| } |
| return labels.get((filter_val or "all").lower(), "All medications") |
|
|
|
|
| def _med_add_display_md() -> str: |
| return "Tap to add a new medication" |
|
|
|
|
| def _med_delete_display_md(profile_id=None) -> str: |
| n = len(med.get_medications(filter="all", profile_id=profile_id)) |
| if n == 0: |
| return "No medications on file" |
| if n == 1: |
| return "1 medication on file" |
| return f"{n} medications on file" |
|
|
|
|
| def _med_catalog_toggle_label(is_open: bool) -> str: |
| return "βοΈ Close" if is_open else "βοΈ Open" |
|
|
|
|
| def _med_view_edit_updates(show_open: bool, add_open: bool, del_open: bool): |
| """View visible when edit panel closed β matches Settings field cards.""" |
| return ( |
| gr.update(visible=not show_open), |
| gr.update(visible=not add_open), |
| gr.update(visible=not del_open), |
| ) |
|
|
|
|
| def _med_table_refresh(filter_val, profile_id): |
| """Refresh table rows and delete dropdown choices.""" |
| return ( |
| _med_table_rows(filter_val, profile_id), |
| gr.update(choices=_med_choices(profile_id), value=None), |
| ) |
|
|
|
|
| def _med_toggle_panel(which: str, show_open: bool, add_open: bool, del_open: bool): |
| """One medication workspace open at a time.""" |
| opens = { |
| "show": bool(show_open), |
| "add": bool(add_open), |
| "delete": bool(del_open), |
| } |
| if opens[which]: |
| new_opens = {k: False for k in opens} |
| else: |
| new_opens = {k: (k == which) for k in opens} |
| return ( |
| gr.update(visible=new_opens["show"]), |
| new_opens["show"], |
| gr.update(visible=new_opens["add"]), |
| new_opens["add"], |
| gr.update(visible=new_opens["delete"]), |
| new_opens["delete"], |
| ) |
|
|
|
|
| def _med_apply_filter(filter_val, profile_id): |
| rows, del_up = _med_table_refresh(filter_val, profile_id) |
| return ( |
| filter_val, |
| rows, |
| del_up, |
| gr.update(value=_med_list_summary_md(filter_val, profile_id)), |
| gr.update(visible=False), |
| False, |
| gr.update(visible=True), |
| gr.update(value=_med_show_display_md(filter_val)), |
| gr.update(value=_med_delete_display_md(profile_id)), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
|
|
| def _med_chip_show_click(show_open, add_open, del_open, filter_val, profile_id): |
| vis_show, st_show, vis_add, st_add, vis_del, st_del = _med_toggle_panel( |
| "show", show_open, add_open, del_open, |
| ) |
| show_view, add_view, del_view = _med_view_edit_updates(st_show, st_add, st_del) |
| return ( |
| vis_show, st_show, vis_add, st_add, vis_del, st_del, |
| show_view, add_view, del_view, |
| gr.update(value=_med_show_display_md(filter_val)), |
| gr.update(value=_med_delete_display_md(profile_id)), |
| gr.update(value=_med_catalog_toggle_label(st_show)), |
| gr.update(value=_med_catalog_toggle_label(st_add)), |
| gr.update(value=_med_catalog_toggle_label(st_del)), |
| ) |
|
|
|
|
| def _med_chip_add_click(show_open, add_open, del_open, filter_val, profile_id): |
| vis_show, st_show, vis_add, st_add, vis_del, st_del = _med_toggle_panel( |
| "add", show_open, add_open, del_open, |
| ) |
| show_view, add_view, del_view = _med_view_edit_updates(st_show, st_add, st_del) |
| return ( |
| vis_show, st_show, vis_add, st_add, vis_del, st_del, |
| show_view, add_view, del_view, |
| gr.update(value=_med_show_display_md(filter_val)), |
| gr.update(value=_med_delete_display_md(profile_id)), |
| gr.update(value=_med_catalog_toggle_label(st_show)), |
| gr.update(value=_med_catalog_toggle_label(st_add)), |
| gr.update(value=_med_catalog_toggle_label(st_del)), |
| ) |
|
|
|
|
| def _med_chip_delete_click(show_open, add_open, del_open, filter_val, profile_id): |
| vis_show, st_show, vis_add, st_add, vis_del, st_del = _med_toggle_panel( |
| "delete", show_open, add_open, del_open, |
| ) |
| show_view, add_view, del_view = _med_view_edit_updates(st_show, st_add, st_del) |
| return ( |
| vis_show, st_show, vis_add, st_add, vis_del, st_del, |
| show_view, add_view, del_view, |
| gr.update(value=_med_show_display_md(filter_val)), |
| gr.update(value=_med_delete_display_md(profile_id)), |
| gr.update(value=_med_catalog_toggle_label(st_show)), |
| gr.update(value=_med_catalog_toggle_label(st_add)), |
| gr.update(value=_med_catalog_toggle_label(st_del)), |
| ) |
|
|
|
|
| def _med_cancel_show(): |
| return ( |
| gr.update(visible=False), False, gr.update(visible=True), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
|
|
| def _med_cancel_add(): |
| return ( |
| gr.update(visible=False), False, gr.update(visible=True), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
|
|
| def _med_cancel_delete(): |
| return ( |
| gr.update(visible=False), False, gr.update(visible=True), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
|
|
| def add_med_handler(name, dosage, frequency, status, side_effects, notes, category, profile_id, filter_val): |
| _noop_fields = (gr.update(),) * 7 |
| _close_add = ( |
| gr.update(visible=False), False, gr.update(visible=True), |
| gr.update(value=_med_delete_display_md(profile_id)), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
| if not profile_id: |
| return ( |
| gr.Warning("Select an active care profile first."), |
| *_med_table_refresh(filter_val, profile_id), |
| *_noop_fields, |
| *_close_add, |
| gr.update(value=_med_list_summary_md(filter_val, profile_id)), |
| ) |
| if not (name or "").strip(): |
| return ( |
| gr.Warning("Medication name is required."), |
| *_med_table_refresh(filter_val, profile_id), |
| *_noop_fields, |
| *_close_add, |
| gr.update(value=_med_list_summary_md(filter_val, profile_id)), |
| ) |
| try: |
| med.add_medication( |
| name, dosage, frequency, status, side_effects, notes, category, |
| profile_id=profile_id, |
| ) |
| return ( |
| gr.Info(f"β
Added: {name}"), |
| *_med_table_refresh(filter_val, profile_id), |
| "", "", "", "current", "", "", "", |
| *_close_add, |
| gr.update(value=_med_list_summary_md(filter_val, profile_id)), |
| ) |
| except Exception as e: |
| return ( |
| gr.Warning(str(e)), |
| *_med_table_refresh(filter_val, profile_id), |
| *_noop_fields, |
| *_close_add, |
| gr.update(value=_med_list_summary_md(filter_val, profile_id)), |
| ) |
|
|
|
|
| def delete_med_handler(med_id: str, profile_id, filter_val): |
| """Delete handler β returns notification, table, dropdown, panel state, labels.""" |
| _close_del = ( |
| gr.update(visible=False), False, gr.update(visible=True), |
| gr.update(value=_med_delete_display_md(profile_id)), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
| if not med_id: |
| return ( |
| gr.Warning("Select a medication to delete."), |
| *_med_table_refresh(filter_val, profile_id), |
| *_close_del, |
| gr.update(value=_med_list_summary_md(filter_val, profile_id)), |
| ) |
| real_id = _find_med_id_by_label(med_id.strip(), profile_id) or med_id.strip() |
| success = med.delete_medication(real_id) |
| if success: |
| return ( |
| gr.Info("ποΈ Medication deleted."), |
| *_med_table_refresh(filter_val, profile_id), |
| *_close_del, |
| gr.update(value=_med_list_summary_md(filter_val, profile_id)), |
| ) |
| return ( |
| gr.Warning("Medication not found."), |
| *_med_table_refresh(filter_val, profile_id), |
| *_close_del, |
| gr.update(value=_med_list_summary_md(filter_val, profile_id)), |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def _appt_table_rows(include_past: bool = False, profile_id=None) -> list: |
| if include_past: |
| active = apt.get_all_appointments(include_past=True, profile_id=profile_id) |
| completed = apt.get_recent_completed(days=30, profile_id=profile_id) |
| |
| for c in completed: |
| c["_is_completed_injection"] = True |
| merged = active + completed |
| merged.sort(key=lambda a: (a.get("date", ""), a.get("time", ""))) |
| source = merged |
| else: |
| source = apt.get_all_appointments(include_past=False, profile_id=profile_id) |
| rows = [] |
| for a in source: |
| rec = a.get("recurrence", {}) |
| freq = rec.get("frequency", "none") |
| |
| if a.get("_is_completed_injection"): |
| status_display = "β
Completed" |
| else: |
| status_display = a.get("status", "scheduled").capitalize() |
| rows.append([ |
| a.get("title", ""), |
| _fmt_date(a.get("date", "")), |
| a.get("time", ""), |
| status_display, |
| freq.capitalize() if freq != "none" else "β", |
| a.get("location", ""), |
| a.get("notes", "")[:40], |
| a["id"], |
| ]) |
| return rows |
|
|
|
|
| def _appt_completed_rows(profile_id=None) -> list: |
| """30-day rolling window β always shows recent completions regardless of filters.""" |
| rows = [] |
| for a in apt.get_recent_completed(days=30, profile_id=profile_id): |
| ca = a.get("completed_at", "") |
| try: |
| ca_display = datetime.fromisoformat(ca).strftime("%b %d, %Y %I:%M %p") if ca else "" |
| except Exception: |
| ca_display = ca |
| rows.append([a.get("title", ""), _fmt_date(a.get("date", "")), |
| a.get("time", ""), ca_display, a.get("location", "")]) |
| return rows |
|
|
|
|
| def _today_summary(profile_id=None) -> str: |
| today = apt.get_today(profile_id=profile_id) |
| if not today: |
| summary = "π
No appointments scheduled for today." |
| else: |
| parts = ["π
**Today's Appointments:**"] |
| for a in today: |
| t = f" at {a['time']}" if a.get("time") else "" |
| parts.append(f"β’ **{a['title']}**{t}") |
| if a.get("location"): |
| parts.append(f" π {a['location']}") |
| summary = "\n".join(parts) |
| due = apt.get_due_reminders(profile_id=profile_id) |
| if due: |
| notes = [] |
| for d in due: |
| days = d.get("days_until", 0) |
| when = "today" if days == 0 else f"in {days} days" |
| notes.append(f"π **{d['title']}** β {when}"[:100]) |
| summary += "\n\n---\n" + "\n".join(notes) |
| return summary |
|
|
|
|
| def _home_health_brief_md(profile_id=None) -> str: |
| """Compact two-column daily snapshot for the Home tab.""" |
| if not profile_id or not _session_active(profile_id): |
| return ( |
| "<p class='home-brief-col-line'>" |
| "Sign in with an active care profile to see today's health brief." |
| "</p>" |
| ) |
|
|
| try: |
| apt.run_lifecycle_check(profile_id) |
| except Exception: |
| pass |
|
|
| profile = profiles.get_profile(profile_id) or {} |
| name = html.escape(profiles.profile_display_name(profile)) |
| today_label = html.escape(datetime.now().strftime("%A, %B %d, %Y")) |
|
|
| appt_lines: list[str] = [] |
| today_appts = apt.get_today(profile_id=profile_id) |
| if today_appts: |
| for appt in today_appts[:4]: |
| title = html.escape(appt.get("title", "Appointment")) |
| when = _fmt_date(appt.get("date", "")) |
| time_bit = f" @ {html.escape(appt['time'])}" if appt.get("time") else "" |
| appt_lines.append(f"β’ {title} β {when}{time_bit}") |
| else: |
| for appt in apt.get_upcoming(profile_id=profile_id)[:4]: |
| title = html.escape(appt.get("title", "Appointment")) |
| when = _fmt_date(appt.get("date", "")) |
| time_bit = f" @ {html.escape(appt['time'])}" if appt.get("time") else "" |
| appt_lines.append(f"β’ {title} β {when}{time_bit}") |
| if not appt_lines: |
| appt_lines.append("β’ None scheduled") |
|
|
| med_lines: list[str] = [] |
| for m in med.get_medications(filter="current", profile_id=profile_id)[:4]: |
| med_name = html.escape(m.get("name", "Medication")) |
| dose = html.escape(m.get("dosage", "")) |
| freq = html.escape(m.get("frequency", "")) |
| if dose and freq: |
| med_lines.append(f"β’ {med_name} β {dose} ({freq})") |
| elif dose: |
| med_lines.append(f"β’ {med_name} β {dose}") |
| elif freq: |
| med_lines.append(f"β’ {med_name} ({freq})") |
| else: |
| med_lines.append(f"β’ {med_name}") |
| if not med_lines: |
| med_lines.append("β’ None on file") |
|
|
| appt_html = "".join( |
| f"<div class='home-brief-col-line'>{line}</div>" for line in appt_lines |
| ) |
| med_html = "".join( |
| f"<div class='home-brief-col-line'>{line}</div>" for line in med_lines |
| ) |
|
|
| return f""" |
| <div class="home-brief-compact"> |
| <div class="home-brief-meta">{name} β’ {today_label}</div> |
| <div class="home-brief-rule"></div> |
| <div class="home-brief-columns"> |
| <div class="home-brief-col"> |
| <div class="home-brief-col-title">π
Upcoming Appointments</div> |
| {appt_html} |
| </div> |
| <div class="home-brief-col"> |
| <div class="home-brief-col-title">π Active Medications</div> |
| {med_html} |
| </div> |
| </div> |
| </div> |
| """ |
|
|
|
|
| _HOME_CHAT_FLOWS = { |
| "add_med": [ |
| ("name", "What is the name of the medication?"), |
| ("dosage", "What is the dosage? (e.g. 10mg, or type skip)"), |
| ("frequency", "How often is it taken? (e.g. once daily, or skip)"), |
| ], |
| "add_appt": [ |
| ("title", "What is the appointment title?"), |
| ("date", "What date? (YYYY-MM-DD or e.g. Jan 15, 2026)"), |
| ("time", "What time? (e.g. 2:30 PM, or skip)"), |
| ("location", "Where is it? (or skip)"), |
| ], |
| "log_symptom": [ |
| ("symptom", "What symptom would you like to log?"), |
| ("pain", "Pain level 1β10? (or skip)"), |
| ("notes", "Any notes? (or skip)"), |
| ], |
| } |
|
|
| _HOME_INTENT_PATTERNS = { |
| "add_med": ( |
| "add medication", "add med", "new medication", "new med", |
| "add a medication", "add a med", |
| ), |
| "add_appt": ( |
| "new appointment", "add appointment", "schedule appointment", |
| "schedule an appointment", "add an appointment", |
| ), |
| "log_symptom": ( |
| "log symptom", "add symptom", "track symptom", "log a symptom", |
| ), |
| } |
|
|
| _HOME_FLOW_GREETINGS = { |
| "add_med": "I can help with that! What is the name of the medication?", |
| "add_appt": "Let's schedule it. What is the appointment title?", |
| "log_symptom": "What symptom would you like to log?", |
| } |
|
|
|
|
| def _blank_home_chat_state() -> dict: |
| return {"flow": None, "step": 0, "data": {}} |
|
|
|
|
| def _is_skip_value(text: str) -> bool: |
| return text.lower().strip() in ("skip", "none", "n/a", "na", "-", "no") |
|
|
|
|
| def _is_cancel_value(text: str) -> bool: |
| return text.lower().strip() in ("cancel", "never mind", "nevermind", "stop", "exit") |
|
|
|
|
| def _detect_home_intent(message: str) -> Optional[str]: |
| lowered = message.lower() |
| for intent, patterns in _HOME_INTENT_PATTERNS.items(): |
| if any(p in lowered for p in patterns): |
| return intent |
| return None |
|
|
|
|
| def _parse_flexible_date(text: str) -> str: |
| raw = text.strip() |
| if not raw: |
| return "" |
| try: |
| return date.fromisoformat(raw[:10]).isoformat() |
| except ValueError: |
| pass |
| for fmt in ("%b %d, %Y", "%B %d, %Y", "%b %d %Y", "%B %d %Y", "%m/%d/%Y", "%m/%d/%y"): |
| try: |
| return datetime.strptime(raw, fmt).date().isoformat() |
| except ValueError: |
| continue |
| year = date.today().year |
| for fmt in ("%b %d", "%B %d"): |
| try: |
| return datetime.strptime(f"{raw} {year}", f"{fmt} %Y").date().isoformat() |
| except ValueError: |
| continue |
| return "" |
|
|
|
|
| def _normalize_time_input(text: str) -> str: |
| if _is_skip_value(text): |
| return "" |
| return _parts_to_time(*_time_to_parts(text.strip())) |
|
|
|
|
| def _home_wizard_parse_field(flow: str, field_key: str, message: str): |
| if field_key == "date": |
| if _is_skip_value(message): |
| return None |
| parsed = _parse_flexible_date(message) |
| return parsed or None |
| if field_key == "time": |
| return _normalize_time_input(message) |
| if field_key == "pain": |
| if _is_skip_value(message): |
| return None |
| match = re.search(r"\d+", message) |
| if not match: |
| return None |
| level = int(match.group()) |
| return level if 1 <= level <= 10 else None |
| if field_key in ("dosage", "frequency", "location", "notes"): |
| return "" if _is_skip_value(message) else message.strip() |
| value = message.strip() |
| return value if value else None |
|
|
|
|
| def _home_commit_medication(profile_id: str, data: dict) -> str: |
| name = data.get("name", "").strip() |
| if not name: |
| raise ValueError("Medication name is required.") |
| dosage = data.get("dosage", "") |
| frequency = data.get("frequency", "") |
| med.add_medication( |
| name, dosage, frequency, "current", "", "", "", |
| profile_id=profile_id, |
| ) |
| detail_bits = [b for b in [dosage, frequency] if b] |
| detail = f" ({', '.join(detail_bits)})" if detail_bits else "" |
| return f"β
Added **{name}**{detail} to your medications." |
|
|
|
|
| def _home_commit_appointment(profile_id: str, data: dict) -> str: |
| title = data.get("title", "").strip() |
| appt_date = data.get("date", "").strip() |
| if not title: |
| raise ValueError("Appointment title is required.") |
| if not appt_date: |
| raise ValueError("Appointment date is required.") |
| appt_time = data.get("time", "") |
| location = data.get("location", "") |
| apt.add_appointment( |
| title, appt_date, appt_time, location, "", [], None, |
| profile_id=profile_id, |
| ) |
| when = _fmt_date(appt_date) |
| time_bit = f" at {appt_time}" if appt_time else "" |
| loc_bit = f" Β· π {location}" if location else "" |
| return f"β
Scheduled **{title}** β {when}{time_bit}{loc_bit}." |
|
|
|
|
| def _home_commit_symptom(profile_id: str, data: dict) -> str: |
| symptom = data.get("symptom", "").strip() |
| if not symptom: |
| raise ValueError("Symptom description is required.") |
| profile = profiles.get_profile(profile_id) or {} |
| journal = list(profile.get("symptom_journal") or []) |
| journal.append({ |
| "symptom": symptom, |
| "pain": data.get("pain"), |
| "notes": data.get("notes", ""), |
| "logged_at": datetime.now().strftime("%Y-%m-%d %H:%M"), |
| }) |
| profiles.update_profile(profile_id, {"symptom_journal": journal[-100:]}) |
| pain = data.get("pain") |
| pain_bit = f" (pain {pain}/10)" if pain is not None else "" |
| return f"β
Logged symptom: **{symptom}**{pain_bit}." |
|
|
|
|
| def _home_commit_flow(flow: str, profile_id: str, data: dict) -> str: |
| if flow == "add_med": |
| return _home_commit_medication(profile_id, data) |
| if flow == "add_appt": |
| return _home_commit_appointment(profile_id, data) |
| if flow == "log_symptom": |
| return _home_commit_symptom(profile_id, data) |
| raise ValueError(f"Unknown flow: {flow}") |
|
|
|
|
| def _home_cross_tab_refresh(profile_id, med_filter="all", appt_include_past=False): |
| """Refresh Home brief plus Medications / Appointments tab data.""" |
| brief = _home_health_brief_md(profile_id) |
| med_rows, med_del = _med_table_refresh(med_filter, profile_id) |
| appt_rows, appt_edit, appt_completed, today = _appt_table_refresh( |
| appt_include_past, profile_id, |
| ) |
| return ( |
| brief, |
| med_rows, |
| med_del, |
| gr.update(value=_med_list_summary_md(med_filter, profile_id)), |
| appt_rows, |
| appt_edit, |
| appt_completed, |
| today, |
| gr.update(value=_appt_list_summary_md(appt_include_past, profile_id)), |
| ) |
|
|
|
|
| def _home_cross_tab_noop(): |
| return (gr.update(),) * 9 |
|
|
|
|
| def _home_rag_reply(message: str, profile_id: str) -> str: |
| rag = _safe_import_rag() |
| if rag is None: |
| return "β AI features unavailable. Install dependencies from requirements.txt." |
| answer, sources = rag.answer_question(message, profile_id=profile_id) |
| if sources: |
| src_lines = "\n\n---\nπ **Sources:**" |
| seen = set() |
| for s in sources: |
| key = f"{s['source']}, p.{s['page']}" |
| if key not in seen: |
| src_lines += f"\nβ’ {key}" |
| seen.add(key) |
| answer += src_lines |
| return answer |
|
|
|
|
| def handle_home_chat( |
| message: str, |
| history: list, |
| profile_id=None, |
| chat_state=None, |
| med_filter="all", |
| appt_include_past=False, |
| ): |
| """Home chat: data-entry wizard or RAG fallback, with cross-tab refresh on save.""" |
| history = list(history or []) |
| chat_state = dict(chat_state or _blank_home_chat_state()) |
|
|
| if not message or not message.strip(): |
| return ("", history, chat_state, *_home_cross_tab_noop()) |
|
|
| msg = message.strip() |
|
|
| if not profile_id: |
| history.append({"role": "user", "content": msg}) |
| history.append({ |
| "role": "assistant", |
| "content": ( |
| "π Sign in with an active care profile (Settings & Profile) to use " |
| "the assistant β it reads that profile's data and documents." |
| ), |
| }) |
| return ("", history, chat_state, *_home_cross_tab_noop()) |
|
|
| if chat_state.get("flow"): |
| if _is_cancel_value(msg): |
| chat_state = _blank_home_chat_state() |
| history.append({"role": "user", "content": msg}) |
| history.append({ |
| "role": "assistant", |
| "content": "Okay, cancelled. What else can I help with today?", |
| }) |
| return ("", history, chat_state, *_home_cross_tab_noop()) |
|
|
| flow = chat_state["flow"] |
| step_idx = int(chat_state.get("step", 0)) |
| steps = _HOME_CHAT_FLOWS[flow] |
| field_key, field_prompt = steps[step_idx] |
| parsed = _home_wizard_parse_field(flow, field_key, msg) |
| history.append({"role": "user", "content": msg}) |
|
|
| if parsed is None: |
| if field_key in ("dosage", "frequency", "location", "notes", "time") and _is_skip_value(msg): |
| parsed = "" |
| elif field_key == "pain" and _is_skip_value(msg): |
| parsed = None |
| else: |
| history.append({ |
| "role": "assistant", |
| "content": f"I didn't catch that. {field_prompt}", |
| }) |
| return ("", history, chat_state, *_home_cross_tab_noop()) |
| chat_state["data"][field_key] = parsed |
| step_idx += 1 |
| chat_state["step"] = step_idx |
| refresh_tabs = False |
|
|
| if step_idx >= len(steps): |
| try: |
| reply = _home_commit_flow(flow, profile_id, chat_state["data"]) |
| chat_state = _blank_home_chat_state() |
| history.append({"role": "assistant", "content": reply}) |
| refresh_tabs = True |
| except Exception as exc: |
| chat_state = _blank_home_chat_state() |
| history.append({ |
| "role": "assistant", |
| "content": f"Sorry, I couldn't save that: {exc}", |
| }) |
| else: |
| _, next_prompt = steps[step_idx] |
| history.append({"role": "assistant", "content": next_prompt}) |
|
|
| if refresh_tabs: |
| return ( |
| "", history, chat_state, |
| *_home_cross_tab_refresh(profile_id, med_filter, appt_include_past), |
| ) |
| return ("", history, chat_state, *_home_cross_tab_noop()) |
|
|
| intent = _detect_home_intent(msg) |
| history.append({"role": "user", "content": msg}) |
| if intent: |
| chat_state = {"flow": intent, "step": 0, "data": {}} |
| history.append({ |
| "role": "assistant", |
| "content": _HOME_FLOW_GREETINGS[intent], |
| }) |
| return ("", history, chat_state, *_home_cross_tab_noop()) |
|
|
| history.append({"role": "assistant", "content": _home_rag_reply(msg, profile_id)}) |
| return ("", history, chat_state, *_home_cross_tab_noop()) |
|
|
|
|
| def _home_tab_on_select(profile_id, history, chat_state): |
| """Refresh brief and seed greeting when chat is empty.""" |
| brief = _home_health_brief_md(profile_id) |
| if history: |
| return brief, gr.update(), gr.update() |
| if not _session_active(profile_id): |
| return brief, gr.update(), gr.update() |
| profile = profiles.get_profile(profile_id) or {} |
| display = profiles.profile_display_name(profile) |
| first = display.split()[0] if display else "there" |
| greeting = [{ |
| "role": "assistant", |
| "content": f"Hello {first}. I can help update your records today.", |
| }] |
| return brief, greeting, _blank_home_chat_state() |
|
|
|
|
| def _home_clear_chat(): |
| return [], "", _blank_home_chat_state() |
|
|
|
|
| def _appt_list_summary_md(include_past: bool = False, profile_id=None) -> str: |
| """One-line appointment counts for the list header.""" |
| upcoming = apt.get_all_appointments(include_past=False, profile_id=profile_id) |
| if not upcoming and not include_past: |
| completed = apt.get_recent_completed(days=30, profile_id=profile_id) |
| if not completed: |
| return "*No appointments on file β use **β Add** below.*" |
| if include_past: |
| shown = len(_appt_table_rows(True, profile_id)) |
| return f"**{shown}** appointment(s) shown (includes past 30 days)" |
| today_n = len(apt.get_today(profile_id=profile_id)) |
| n = len(upcoming) |
| if today_n: |
| return f"**{n}** upcoming Β· **{today_n}** today" |
| return f"**{n}** upcoming appointment(s)" |
|
|
|
|
| def _appt_show_display_md(include_past: bool) -> str: |
| return ( |
| "Including past appointments (30 days)" |
| if include_past else "Upcoming appointments only" |
| ) |
|
|
|
|
| def _appt_add_display_md() -> str: |
| return "Tap to add or edit an appointment" |
|
|
|
|
| def _appt_delete_display_md(profile_id=None) -> str: |
| n = len(apt.get_all_appointments(include_past=True, profile_id=profile_id)) |
| if n == 0: |
| return "No appointments on file" |
| if n == 1: |
| return "1 appointment on file" |
| return f"{n} appointments on file" |
|
|
|
|
| def _appt_view_edit_updates(show_open: bool, add_open: bool, del_open: bool): |
| return ( |
| gr.update(visible=not show_open), |
| gr.update(visible=not add_open), |
| gr.update(visible=not del_open), |
| ) |
|
|
|
|
| def _appt_table_refresh(include_past: bool, profile_id): |
| return ( |
| _appt_table_rows(include_past, profile_id), |
| gr.update(choices=_appt_choices(profile_id), value=None), |
| _appt_completed_rows(profile_id), |
| _today_summary(profile_id), |
| ) |
|
|
|
|
| def _appt_toggle_panel(which: str, show_open: bool, add_open: bool, del_open: bool): |
| opens = { |
| "show": bool(show_open), |
| "add": bool(add_open), |
| "delete": bool(del_open), |
| } |
| if opens[which]: |
| new_opens = {k: False for k in opens} |
| else: |
| new_opens = {k: (k == which) for k in opens} |
| return ( |
| gr.update(visible=new_opens["show"]), |
| new_opens["show"], |
| gr.update(visible=new_opens["add"]), |
| new_opens["add"], |
| gr.update(visible=new_opens["delete"]), |
| new_opens["delete"], |
| ) |
|
|
|
|
| def _appt_apply_show_filter(include_past_val: bool, profile_id): |
| rows, choices_up, completed, today = _appt_table_refresh( |
| include_past_val, profile_id, |
| ) |
| return ( |
| include_past_val, |
| rows, |
| choices_up, |
| choices_up, |
| completed, |
| today, |
| gr.update(value=_appt_list_summary_md(include_past_val, profile_id)), |
| gr.update(visible=False), |
| False, |
| gr.update(visible=True), |
| gr.update(value=_appt_show_display_md(include_past_val)), |
| gr.update(value=_appt_delete_display_md(profile_id)), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
|
|
| def _appt_chip_show_click(show_open, add_open, del_open, include_past, profile_id): |
| vis_show, st_show, vis_add, st_add, vis_del, st_del = _appt_toggle_panel( |
| "show", show_open, add_open, del_open, |
| ) |
| show_view, add_view, del_view = _appt_view_edit_updates(st_show, st_add, st_del) |
| return ( |
| vis_show, st_show, vis_add, st_add, vis_del, st_del, |
| show_view, add_view, del_view, |
| gr.update(value=_appt_show_display_md(include_past)), |
| gr.update(value=_appt_delete_display_md(profile_id)), |
| gr.update(value=_med_catalog_toggle_label(st_show)), |
| gr.update(value=_med_catalog_toggle_label(st_add)), |
| gr.update(value=_med_catalog_toggle_label(st_del)), |
| ) |
|
|
|
|
| def _appt_chip_add_click(show_open, add_open, del_open, include_past, profile_id): |
| vis_show, st_show, vis_add, st_add, vis_del, st_del = _appt_toggle_panel( |
| "add", show_open, add_open, del_open, |
| ) |
| show_view, add_view, del_view = _appt_view_edit_updates(st_show, st_add, st_del) |
| return ( |
| vis_show, st_show, vis_add, st_add, vis_del, st_del, |
| show_view, add_view, del_view, |
| gr.update(value=_appt_show_display_md(include_past)), |
| gr.update(value=_appt_delete_display_md(profile_id)), |
| gr.update(value=_med_catalog_toggle_label(st_show)), |
| gr.update(value=_med_catalog_toggle_label(st_add)), |
| gr.update(value=_med_catalog_toggle_label(st_del)), |
| ) |
|
|
|
|
| def _appt_chip_delete_click(show_open, add_open, del_open, include_past, profile_id): |
| vis_show, st_show, vis_add, st_add, vis_del, st_del = _appt_toggle_panel( |
| "delete", show_open, add_open, del_open, |
| ) |
| show_view, add_view, del_view = _appt_view_edit_updates(st_show, st_add, st_del) |
| return ( |
| vis_show, st_show, vis_add, st_add, vis_del, st_del, |
| show_view, add_view, del_view, |
| gr.update(value=_appt_show_display_md(include_past)), |
| gr.update(value=_appt_delete_display_md(profile_id)), |
| gr.update(value=_med_catalog_toggle_label(st_show)), |
| gr.update(value=_med_catalog_toggle_label(st_add)), |
| gr.update(value=_med_catalog_toggle_label(st_del)), |
| ) |
|
|
|
|
| def _appt_cancel_show(): |
| return ( |
| gr.update(visible=False), False, |
| gr.update(visible=True), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
|
|
| def _appt_cancel_add(): |
| return ( |
| gr.update(visible=False), False, |
| gr.update(visible=True), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
|
|
| def _appt_cancel_delete(): |
| return ( |
| gr.update(visible=False), False, |
| gr.update(visible=True), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
|
|
| def _appt_open_add_panel(): |
| """Open add/edit workspace (e.g. after row click).""" |
| show_view, add_view, del_view = _appt_view_edit_updates(False, True, False) |
| return ( |
| gr.update(visible=False), False, |
| gr.update(visible=True), True, |
| gr.update(visible=False), False, |
| show_view, add_view, del_view, |
| gr.update(), |
| gr.update(), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| gr.update(value=_med_catalog_toggle_label(True)), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
|
|
| def _appt_panel_close_after_save(include_past, profile_id): |
| return ( |
| gr.update(visible=False), |
| False, |
| gr.update(visible=True), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| gr.update(value=_appt_list_summary_md(include_past, profile_id)), |
| gr.update(value=_appt_delete_display_md(profile_id)), |
| ) |
|
|
|
|
| def _recurrence_to_ui(rec: dict) -> tuple: |
| """Unpack recurrence dict β 9 UI field values.""" |
| fm = {"none": "None", "daily": "Daily", "weekly": "Weekly", |
| "monthly": "Monthly", "yearly": "Yearly"} |
| freq = fm.get(rec.get("frequency", "none"), "None") |
| interval = rec.get("interval", 1) |
| active = freq != "None" |
| return ( |
| freq, interval, |
| gr.update(visible=active), |
| ) |
|
|
|
|
| def _ui_to_recurrence(freq_ui, interval) -> dict: |
| fm = {"None": "none", "Daily": "daily", "Weekly": "weekly", |
| "Monthly": "monthly", "Yearly": "yearly"} |
| return { |
| "frequency": fm.get(freq_ui or "None", "none"), |
| "interval": int(interval) if interval else 1, |
| } |
|
|
|
|
| def _populate_appt_edit_fields(appt_id: str): |
| """Pre-fill edit form from dropdown selection (appt_id is now a UUID). Returns 12 values.""" |
| if not appt_id: |
| return ("", "", "12", "00", "PM", "", "", [], "None", 1, gr.update(visible=False)) |
| appt = apt.get_appointment_by_id(appt_id) |
| if not appt: |
| return ("", "", "12", "00", "PM", "", "", [], "None", 1, gr.update(visible=False)) |
| h, mn, ap = _time_to_parts(appt.get("time", "")) |
| return ( |
| appt.get("title", ""), appt.get("date", ""), |
| h, mn, ap, |
| appt.get("location", ""), appt.get("notes", ""), |
| appt.get("reminders", []), |
| *_recurrence_to_ui(appt.get("recurrence", {})), |
| ) |
|
|
|
|
| def _appt_select_handler(evt: gr.SelectData, include_past: bool, profile_id): |
| """Row click on appt_table β populate edit form + set delete target. 14 outputs.""" |
| try: |
| rows = _appt_table_rows(include_past, profile_id) |
| appt_id = rows[evt.index[0]][-1] |
| except Exception: |
| return (gr.update(),) * 14 |
| appt = apt.get_appointment_by_id(appt_id) |
| if not appt: |
| return (gr.update(),) * 14 |
| h, mn, ap = _time_to_parts(appt.get("time", "")) |
| return ( |
| appt_id, |
| appt.get("title", ""), appt.get("date", ""), |
| h, mn, ap, |
| appt.get("location", ""), appt.get("notes", ""), |
| appt.get("reminders", []), |
| *_recurrence_to_ui(appt.get("recurrence", {})), |
| gr.update(value=appt_id, choices=_appt_choices(profile_id)), |
| gr.update(value=appt_id, choices=_appt_choices(profile_id)), |
| ) |
|
|
|
|
| def _appt_form_blank_updates(): |
| """Clear unified add/edit form fields after a successful save.""" |
| return ( |
| "", |
| gr.update(value=""), gr.update(value=""), |
| gr.update(value="12"), gr.update(value="00"), gr.update(value="PM"), |
| gr.update(value=""), gr.update(value=""), |
| gr.update(value=[]), |
| gr.update(value="None"), gr.update(value=1), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def add_appt_handler( |
| title, add_date, add_hour, add_minute, add_ampm, location, notes, |
| reminders_val, freq_ui, interval, profile_id, include_past=False, |
| ): |
| _choices_none = gr.update(choices=_appt_choices(profile_id), value=None) |
| _blank = ( |
| "", |
| gr.update(value=""), gr.update(value=""), |
| gr.update(value="12"), gr.update(value="00"), gr.update(value="PM"), |
| gr.update(value=""), gr.update(value=""), |
| gr.update(value=[]), |
| gr.update(value="None"), gr.update(value=1), |
| gr.update(visible=False), |
| ) |
| _rows, _, _completed, _today = _appt_table_refresh(include_past, profile_id) |
| if not profile_id: |
| return (gr.Warning("Select an active care profile first."), |
| _rows, _completed, _today, _choices_none, _choices_none, *_blank) |
| if not title.strip(): |
| return (gr.Info("π‘ Please add a title to your appointment to save it."), |
| _rows, _completed, _today, _choices_none, _choices_none, *_blank) |
| if not add_date: |
| return (gr.Info("Please pick an appointment date β tap Edit Date, then Save."), |
| _rows, _completed, _today, _choices_none, _choices_none, *_blank) |
| try: |
| parsed_date = _parse_date_from_picker(add_date) |
| parsed_time = _parts_to_time(add_hour, add_minute, add_ampm) |
| if apt.has_conflicting_appointment(parsed_date, parsed_time, profile_id=profile_id): |
| _rows, _, _completed, _today = _appt_table_refresh(include_past, profile_id) |
| return (gr.Info("β οΈ Note: There is already an appointment scheduled for this time."), |
| _rows, _completed, _today, _choices_none, _choices_none, *_blank) |
| apt.add_appointment( |
| title, _parse_date_from_picker(add_date), |
| _parts_to_time(add_hour, add_minute, add_ampm), |
| location, notes, |
| list(reminders_val) if reminders_val else [], |
| _ui_to_recurrence(freq_ui, interval), |
| profile_id=profile_id, |
| ) |
| _rows, _, _completed, _today = _appt_table_refresh(include_past, profile_id) |
| return (gr.Info(f"β
Added: {title}"), |
| _rows, _completed, _today, _choices_none, _choices_none, *_blank) |
| except Exception as e: |
| _rows, _, _completed, _today = _appt_table_refresh(include_past, profile_id) |
| return (gr.Warning(str(e)), |
| _rows, _completed, _today, _choices_none, _choices_none, *_blank) |
|
|
|
|
| def edit_appt_handler( |
| appt_id, title, edit_date, edit_hour, edit_minute, edit_ampm, |
| location, notes, reminders_val, |
| freq_ui, interval, profile_id, include_past=False, |
| ): |
| _choices_none = gr.update(choices=_appt_choices(profile_id), value=None) |
| _blank = _appt_form_blank_updates() |
| _keep = ( |
| appt_id, |
| gr.update(value=title), |
| gr.update(value=edit_date), |
| gr.update(value=edit_hour), |
| gr.update(value=edit_minute), |
| gr.update(value=edit_ampm), |
| gr.update(value=location), |
| gr.update(value=notes), |
| gr.update(value=reminders_val or []), |
| gr.update(value=freq_ui), |
| gr.update(value=interval), |
| gr.update(), |
| ) |
| _rows, _, _completed, _today = _appt_table_refresh(include_past, profile_id) |
| if not appt_id: |
| return (gr.Info("π‘ Please select an appointment from the list above to begin making changes."), |
| _rows, _completed, _today, _choices_none, _choices_none, *_keep) |
| if not title.strip(): |
| return (gr.Info("π‘ Please add a title to your appointment to save it."), |
| _rows, _completed, _today, |
| gr.update(choices=_appt_choices(profile_id), value=appt_id), _choices_none, *_keep) |
| if not edit_date: |
| return (gr.Info("Please pick an appointment date β tap Edit Date, then Save."), |
| _rows, _completed, _today, |
| gr.update(choices=_appt_choices(profile_id), value=appt_id), _choices_none, *_keep) |
| result = apt.edit_appointment( |
| appt_id, title=title, |
| appt_date=_parse_date_from_picker(edit_date), |
| appt_time=_parts_to_time(edit_hour, edit_minute, edit_ampm), |
| location=location, notes=notes, |
| reminders=list(reminders_val) if reminders_val else [], |
| recurrence=_ui_to_recurrence(freq_ui, interval), |
| ) |
| _rows, _, _completed, _today = _appt_table_refresh(include_past, profile_id) |
| if result: |
| return (gr.Info(f"β
Updated: {title}"), |
| _rows, _completed, _today, _choices_none, _choices_none, *_blank) |
| return (gr.Warning("Appointment not found."), |
| _rows, _completed, _today, |
| gr.update(choices=_appt_choices(profile_id), value=appt_id), _choices_none, *_keep) |
|
|
|
|
| def delete_appt_handler(appt_id: str, profile_id, include_past=False): |
| _choices_none = gr.update(choices=_appt_choices(profile_id), value=None) |
| _rows, _, _completed, _today = _appt_table_refresh(include_past, profile_id) |
| if not appt_id: |
| return (gr.Info("π‘ Please select which appointment you would like to remove from the list."), |
| _rows, _completed, _today, _choices_none, _choices_none) |
| if apt.delete_appointment(appt_id.strip()): |
| _rows, _, _completed, _today = _appt_table_refresh(include_past, profile_id) |
| return (gr.Info("ποΈ Appointment deleted."), |
| _rows, _completed, _today, _choices_none, _choices_none) |
| return (gr.Warning("Appointment not found."), |
| _rows, _completed, _today, _choices_none, _choices_none) |
|
|
|
|
| def _appt_clear_form_handler(profile_id): |
| """Reset form and selection (Clear Form button).""" |
| blank_id, *blank_fields = _appt_form_blank_updates() |
| return ( |
| blank_id, |
| *blank_fields, |
| gr.update(value=None, choices=_appt_choices(profile_id)), |
| gr.update(value=None, choices=_appt_choices(profile_id)), |
| ) |
|
|
|
|
| def appt_save_handler( |
| appt_id, title, form_date, hour, minute, ampm, location, notes, |
| reminders_val, freq_ui, interval, profile_id, include_past=False, |
| ): |
| """Save new appointment or update selected one via unified form.""" |
| if appt_id: |
| return edit_appt_handler( |
| appt_id, title, form_date, hour, minute, ampm, |
| location, notes, reminders_val, freq_ui, interval, profile_id, |
| include_past, |
| ) |
| return add_appt_handler( |
| title, form_date, hour, minute, ampm, location, notes, |
| reminders_val, freq_ui, interval, profile_id, include_past, |
| ) |
|
|
|
|
| def appt_save_with_ui( |
| appt_id, title, form_date, hour, minute, ampm, location, notes, |
| reminders_val, freq_ui, interval, profile_id, include_past, |
| ): |
| core = appt_save_handler( |
| appt_id, title, form_date, hour, minute, ampm, location, notes, |
| reminders_val, freq_ui, interval, profile_id, include_past, |
| ) |
| return (*core, *_appt_panel_close_after_save(include_past, profile_id)) |
|
|
|
|
| def delete_appt_with_ui(appt_id, profile_id, include_past): |
| core = delete_appt_handler(appt_id, profile_id, include_past) |
| return ( |
| *core, |
| gr.update(visible=False), |
| False, |
| gr.update(visible=True), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| gr.update(value=_appt_list_summary_md(include_past, profile_id)), |
| gr.update(value=_appt_delete_display_md(profile_id)), |
| ) |
|
|
| |
| |
| |
|
|
| |
| _CATEGORY_MAP = { |
| "β οΈ Allergies": "allergies", |
| "β
Preferred Foods": "liked_foods", |
| "β Food Aversions": "disliked_foods", |
| } |
| _CATEGORY_CHOICES = list(_CATEGORY_MAP.keys()) |
|
|
| |
| _CATEGORY_LABELS = {v: k for k, v in _CATEGORY_MAP.items()} |
|
|
|
|
| def _cat_label(category: str) -> str: |
| """Convert internal category name to display label.""" |
| return _CATEGORY_LABELS.get(category, category.replace("_", " ").title()) |
|
|
|
|
| def _to_internal(ui_category: str) -> str: |
| """Convert UI display name to internal category key.""" |
| return _CATEGORY_MAP.get(ui_category, ui_category) |
|
|
|
|
| |
|
|
| _MONTHS = [ |
| ("January", "01"), ("February", "02"), ("March", "03"), |
| ("April", "04"), ("May", "05"), ("June", "06"), |
| ("July", "07"), ("August", "08"), ("September", "09"), |
| ("October", "10"), ("November", "11"), ("December", "12"), |
| ] |
|
|
| _DAYS = [(str(d), f"{d:02d}") for d in range(1, 32)] |
|
|
| _CURRENT_YEAR = datetime.now().year |
| _YEARS = [str(y) for y in range(_CURRENT_YEAR, _CURRENT_YEAR + 3)] |
|
|
|
|
| def _date_to_parts(date_str: str) -> tuple: |
| """Split YYYY-MM-DD into (year_display, month_display, day_display).""" |
| if not date_str: |
| return ("", "", "") |
| parts = date_str.split("-") |
| if len(parts) == 3: |
| y, m, d = parts |
| month_name = next((name for name, val in _MONTHS if val == m), m) |
| return (y, month_name, str(int(d))) |
| return ("", "", "") |
|
|
|
|
| def _parts_to_date(year: str, month_val: str, day_val: str) -> str: |
| """Combine dropdown values into YYYY-MM-DD string.""" |
| if year and month_val and day_val: |
| try: |
| return f"{int(year):04d}-{month_val}-{int(day_val):02d}" |
| except (ValueError, TypeError): |
| return "" |
| return "" |
|
|
|
|
| def _food_rows(category: str, profile_id=None): |
| """Return table rows for a food category. Allergies include severity column.""" |
| data = food.get_food(profile_id=profile_id) |
| entries = data.get(category, []) |
| if category == "allergies": |
| return [[e["name"], e.get("severity", "β"), e.get("notes", ""), e["id"]] for e in entries] |
| return [[e["name"], e.get("notes", ""), e["id"]] for e in entries] |
|
|
|
|
| def add_food_handler(category, name, notes, severity, profile_id): |
| internal_cat = _to_internal(category) |
| if not profile_id: |
| return ( |
| gr.Warning("Select an active care profile first."), |
| _food_rows("liked_foods", profile_id), _food_rows("disliked_foods", profile_id), _food_rows("allergies", profile_id), |
| gr.update(choices=_food_choices(internal_cat, profile_id), value=[]), |
| "", "", |
| ) |
| if not name.strip(): |
| return ( |
| gr.Warning("Food name is required."), |
| _food_rows("liked_foods", profile_id), _food_rows("disliked_foods", profile_id), _food_rows("allergies", profile_id), |
| gr.update(choices=_food_choices(internal_cat, profile_id), value=[]), |
| "", "", |
| ) |
| |
| try: |
| conflict_cat = food.check_conflict(name, internal_cat, profile_id=profile_id) |
| if conflict_cat: |
| label = _cat_label(conflict_cat) |
| return ( |
| gr.Warning(f"β οΈ Conflict: '{name}' is already listed under {label}."), |
| _food_rows("liked_foods", profile_id), _food_rows("disliked_foods", profile_id), _food_rows("allergies", profile_id), |
| gr.update(choices=_food_choices(internal_cat, profile_id), value=[]), |
| "", "", |
| ) |
| food.add_food(internal_cat, name, notes, severity=severity, profile_id=profile_id) |
| return ( |
| gr.Info(f"β
Added to {_cat_label(internal_cat)}: {name}"), |
| _food_rows("liked_foods", profile_id), _food_rows("disliked_foods", profile_id), _food_rows("allergies", profile_id), |
| gr.update(choices=_food_choices(internal_cat, profile_id), value=[]), |
| "", "", |
| ) |
| except Exception as e: |
| return ( |
| gr.Warning(str(e)), |
| _food_rows("liked_foods", profile_id), _food_rows("disliked_foods", profile_id), _food_rows("allergies", profile_id), |
| gr.update(choices=_food_choices(internal_cat, profile_id), value=[]), |
| "", "", |
| ) |
|
|
|
|
| def delete_food_handler(category, selected_ids, confirm, profile_id): |
| """Bulk delete handler β accepts list of ID values from CheckboxGroup tuples.""" |
| internal_cat = _to_internal(category) |
|
|
| |
| liked_rows = _food_rows("liked_foods", profile_id) |
| disliked_rows = _food_rows("disliked_foods", profile_id) |
| allergy_rows = _food_rows("allergies", profile_id) |
| del_choices = gr.update(choices=_food_choices(internal_cat, profile_id), value=[]) |
| edit_choices = gr.update(choices=_food_choices(internal_cat, profile_id), value=None) |
| confirm_hide = gr.update(visible=False) |
| confirm_reset = gr.update(value=False) |
|
|
| if not selected_ids: |
| return ( |
| gr.Warning("Select entries to delete."), |
| liked_rows, disliked_rows, allergy_rows, |
| del_choices, edit_choices, confirm_hide, confirm_reset, |
| ) |
|
|
| if not confirm: |
| return ( |
| gr.Warning("Check the confirmation box before deleting."), |
| liked_rows, disliked_rows, allergy_rows, |
| del_choices, edit_choices, confirm_hide, confirm_reset, |
| ) |
|
|
| ids_to_delete = [str(i) for i in selected_ids if i] |
| if not ids_to_delete: |
| return ( |
| gr.Warning("Selected entries not found."), |
| liked_rows, disliked_rows, allergy_rows, |
| del_choices, edit_choices, confirm_hide, confirm_reset, |
| ) |
|
|
| count = food.delete_food(internal_cat, ids_to_delete) |
| if count > 0: |
| return ( |
| gr.Info(f"ποΈ Deleted {count} entry/entries from {_cat_label(internal_cat)}."), |
| _food_rows("liked_foods", profile_id), |
| _food_rows("disliked_foods", profile_id), |
| _food_rows("allergies", profile_id), |
| del_choices, edit_choices, confirm_hide, confirm_reset, |
| ) |
| return ( |
| gr.Warning("Entries not found."), |
| _food_rows("liked_foods", profile_id), |
| _food_rows("disliked_foods", profile_id), |
| _food_rows("allergies", profile_id), |
| del_choices, edit_choices, confirm_hide, confirm_reset, |
| ) |
|
|
|
|
| def refresh_food_tables(profile_id=None): |
| return ( |
| _food_rows("liked_foods", profile_id), |
| _food_rows("disliked_foods", profile_id), |
| _food_rows("allergies", profile_id), |
| ) |
|
|
|
|
| _FOOD_VIEW_INTERNAL = { |
| "allergies": "allergies", |
| "preferred": "liked_foods", |
| "aversions": "disliked_foods", |
| } |
|
|
|
|
| def _food_internal_view(view: str) -> str: |
| return _FOOD_VIEW_INTERNAL.get(view or "allergies", "allergies") |
|
|
|
|
| def _food_show_display_md(view: str) -> str: |
| labels = { |
| "allergies": "β οΈ Allergies", |
| "preferred": "β
Preferred Foods", |
| "aversions": "β Food Aversions", |
| } |
| return labels.get(view or "allergies", "β οΈ Allergies") |
|
|
|
|
| def _food_list_summary_md(view: str, profile_id=None) -> str: |
| data = food.get_food(profile_id=profile_id) |
| a_n = len(data.get("allergies", [])) |
| p_n = len(data.get("liked_foods", [])) |
| v_n = len(data.get("disliked_foods", [])) |
| if a_n + p_n + v_n == 0: |
| return "*No diet entries yet β use **β Add** below.*" |
| shown = len(data.get(_food_internal_view(view), [])) |
| return ( |
| f"**{shown}** in {_food_show_display_md(view).lower()} Β· " |
| f"**{a_n}** allergies Β· **{p_n}** preferred Β· **{v_n}** aversions" |
| ) |
|
|
|
|
| def _food_add_display_md() -> str: |
| return "Tap to add a new diet or allergy entry" |
|
|
|
|
| def _food_edit_display_md() -> str: |
| return "Tap to edit or move an existing entry" |
|
|
|
|
| def _food_delete_display_md(profile_id=None) -> str: |
| data = food.get_food(profile_id=profile_id) |
| total = sum(len(data.get(k, [])) for k in ("allergies", "liked_foods", "disliked_foods")) |
| if total == 0: |
| return "No entries on file" |
| if total == 1: |
| return "1 entry on file" |
| return f"{total} entries on file" |
|
|
|
|
| def _food_view_edit_updates(show_open, add_open, edit_open, del_open): |
| return ( |
| gr.update(visible=not show_open), |
| gr.update(visible=not add_open), |
| gr.update(visible=not edit_open), |
| gr.update(visible=not del_open), |
| ) |
|
|
|
|
| def _food_toggle_panel(which, show_open, add_open, edit_open, del_open): |
| opens = { |
| "show": bool(show_open), |
| "add": bool(add_open), |
| "edit": bool(edit_open), |
| "delete": bool(del_open), |
| } |
| if opens[which]: |
| new_opens = {k: False for k in opens} |
| else: |
| new_opens = {k: (k == which) for k in opens} |
| return ( |
| gr.update(visible=new_opens["show"]), new_opens["show"], |
| gr.update(visible=new_opens["add"]), new_opens["add"], |
| gr.update(visible=new_opens["edit"]), new_opens["edit"], |
| gr.update(visible=new_opens["delete"]), new_opens["delete"], |
| ) |
|
|
|
|
| def _food_chip_click(which, show_open, add_open, edit_open, del_open, view, profile_id): |
| vis = _food_toggle_panel(which, show_open, add_open, edit_open, del_open) |
| views = _food_view_edit_updates(vis[1], vis[3], vis[5], vis[7]) |
| return ( |
| *vis, |
| *views, |
| gr.update(value=_food_show_display_md(view)), |
| gr.update(value=_food_delete_display_md(profile_id)), |
| gr.update(value=_food_edit_display_md()), |
| gr.update(value=_med_catalog_toggle_label(vis[1])), |
| gr.update(value=_med_catalog_toggle_label(vis[3])), |
| gr.update(value=_med_catalog_toggle_label(vis[5])), |
| gr.update(value=_med_catalog_toggle_label(vis[7])), |
| ) |
|
|
|
|
| def _food_cancel_panel(): |
| return ( |
| gr.update(visible=False), False, |
| gr.update(visible=True), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
|
|
| def _food_apply_view(view: str, profile_id): |
| return ( |
| view, |
| gr.update(visible=(view == "allergies")), |
| gr.update(visible=(view == "preferred")), |
| gr.update(visible=(view == "aversions")), |
| gr.update(value=_food_list_summary_md(view, profile_id)), |
| gr.update(value=_food_show_display_md(view)), |
| gr.update(visible=False), |
| False, |
| gr.update(visible=True), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
|
|
| |
| |
| |
|
|
| _VISIT_CLINIC_CUSTOM_LABEL = "β Use Custom Destination" |
| _VISIT_CLINIC_UNSET_LABEL = "(Not set in profile β add in Settings)" |
|
|
|
|
| def _format_profile_clinic_label(profile_id) -> str: |
| """Display label for profile PCP + clinic (Doctor Visit dropdown default).""" |
| if not profile_id: |
| return "" |
| p = profiles.get_profile(profile_id) or {} |
| name = (p.get("pcp_name") or "").strip() |
| clinic = (p.get("pcp_clinic") or "").strip() |
| if name and clinic: |
| return f"{name} β {clinic}" |
| return name or clinic |
|
|
|
|
| def _visit_clinic_dropdown_choices(profile_id) -> list[str]: |
| label = _format_profile_clinic_label(profile_id) |
| choices = [label if label else _VISIT_CLINIC_UNSET_LABEL] |
| choices.append(_VISIT_CLINIC_CUSTOM_LABEL) |
| return choices |
|
|
|
|
| def _visit_destination_summary_text(profile_id, selection, custom) -> str: |
| """One-line summary for the compact destination card.""" |
| text = _resolve_visit_clinic_destination(profile_id, selection, custom) |
| return text if text else "*Not set β click βοΈ Edit Destination*" |
|
|
|
|
| def _visit_clinic_sync_updates(profile_id): |
| """Reset destination from active profile (sign-in / profile change / page load).""" |
| choices = _visit_clinic_dropdown_choices(profile_id) |
| label = _format_profile_clinic_label(profile_id) |
| default = label if label else _VISIT_CLINIC_UNSET_LABEL |
| summary = _visit_destination_summary_text(profile_id, default, "") |
| header = _visit_brief_header_md(profile_id, default, "") |
| sym_choices = profiles.get_tracked_symptoms(profile_id) |
| chip_labels = _visit_all_chip_label_updates( |
| profile_id, default, "", None, "", [], [], [], |
| ) |
| return ( |
| gr.update(choices=choices, value=default), |
| gr.update(value=summary), |
| gr.update(value="", visible=False), |
| gr.update(visible=False), |
| False, |
| gr.update(value=header), |
| gr.update(visible=False), |
| False, |
| gr.update(visible=False), |
| False, |
| gr.update(visible=False), |
| False, |
| gr.update(choices=sym_choices, value=[]), |
| *chip_labels, |
| ) |
|
|
|
|
| def _visit_destination_save(profile_id, selection, custom): |
| summary = _visit_destination_summary_text(profile_id, selection, custom) |
| header = _visit_brief_header_md(profile_id, selection, custom) |
| chip_dest = _visit_chip_destination_label(profile_id, selection, custom) |
| return ( |
| gr.update(value=summary), |
| gr.update(value=header), |
| gr.update(visible=False), |
| False, |
| gr.update(value=chip_dest), |
| ) |
|
|
|
|
| def _visit_destination_cancel(profile_id, snapshot): |
| snap = snapshot or {} |
| sel = snap.get("select") |
| custom = snap.get("custom", "") |
| choices = _visit_clinic_dropdown_choices(profile_id) |
| if sel not in choices: |
| sel = choices[0] if choices else None |
| summary = _visit_destination_summary_text(profile_id, sel, custom) |
| header = _visit_brief_header_md(profile_id, sel, custom) |
| chip_dest = _visit_chip_destination_label(profile_id, sel, custom) |
| return ( |
| gr.update(value=summary), |
| gr.update(value=header), |
| gr.update(choices=choices, value=sel), |
| gr.update( |
| value=custom, |
| visible=(sel == _VISIT_CLINIC_CUSTOM_LABEL), |
| ), |
| gr.update(visible=False), |
| False, |
| gr.update(value=chip_dest), |
| ) |
|
|
|
|
| def _on_visit_clinic_select_change(selection): |
| """Reveal custom destination field when user opts out of profile PCP.""" |
| return gr.update(visible=(selection == _VISIT_CLINIC_CUSTOM_LABEL)) |
|
|
|
|
| def _resolve_visit_clinic_destination(profile_id, selection, custom_text) -> str: |
| if selection == _VISIT_CLINIC_CUSTOM_LABEL: |
| return (custom_text or "").strip() |
| if selection and selection != _VISIT_CLINIC_UNSET_LABEL: |
| return selection.strip() |
| return _format_profile_clinic_label(profile_id) |
|
|
|
|
| def generate_visit(reason: str) -> str: |
| return doc_forms.generate_visit_template(reason) |
|
|
|
|
| def generate_visit_brief( |
| profile_id, clinic_doctor, reason, reason_other, concerns, |
| symptoms, symptom_other, symptom_notes, questions, |
| inc_meds, inc_allergies, inc_history, inc_insurance, |
| ): |
| |
| resolved_reason = (reason_other or "").strip() if reason == "Other" else (reason or "") |
| |
| |
| return doc_forms.generate_visit_brief_markdown( |
| profile_id=profile_id or "", |
| clinic_doctor=clinic_doctor or "", |
| reason=resolved_reason, |
| concerns=concerns or "", |
| symptoms=symptoms or [], |
| symptom_other=symptom_other or "", |
| symptom_notes=symptom_notes or "", |
| questions=questions or [], |
| inc_meds=bool(inc_meds), |
| inc_allergies=bool(inc_allergies), |
| inc_history=bool(inc_history), |
| inc_insurance=bool(inc_insurance), |
| ) |
|
|
|
|
| def generate_visit_brief_resolved( |
| profile_id, clinic_select, clinic_custom, reason, reason_other, concerns, |
| symptom_entries, questions, |
| inc_meds, inc_allergies, inc_history, inc_insurance, |
| ): |
| clinic_doctor = _resolve_visit_clinic_destination( |
| profile_id, clinic_select, clinic_custom, |
| ) |
| symptoms, symptom_other, symptom_notes = _flatten_symptom_entries( |
| symptom_entries or [], |
| ) |
| return generate_visit_brief( |
| profile_id, clinic_doctor, reason, reason_other, |
| _concerns_for_brief(concerns), |
| symptoms, symptom_other, symptom_notes, questions, |
| inc_meds, inc_allergies, inc_history, inc_insurance, |
| ) |
|
|
|
|
| def _visit_reason_summary_text(reason, reason_other) -> str: |
| """Compact summary for the Reason for Visit card shell.""" |
| if reason == "Other": |
| text = (reason_other or "").strip() |
| return text if text else "*Other β specify when expanded*" |
| if reason: |
| return reason |
| return "*Not set β click to set reason*" |
|
|
|
|
| def _visit_concerns_summary_text(clist) -> str: |
| """Compact summary for the Primary Concerns card shell.""" |
| clist = clist or [] |
| if not clist: |
| return "*No concerns added β click to add*" |
| if len(clist) == 1: |
| return clist[0] |
| preview = "; ".join(clist[:2]) |
| extra = len(clist) - 2 |
| if extra > 0: |
| preview += f" (+{extra} more)" |
| return preview |
|
|
|
|
| def _visit_brief_header_md(profile_id, clinic_select, clinic_custom) -> str: |
| """Appointment brief strip β patient, DOB, date, clinic.""" |
| from datetime import date |
|
|
| p = profiles.get_profile(profile_id or "") or {} |
| name = (p.get("full_name") or "").strip() or "_(patient not set)_" |
| dob_raw = (p.get("dob") or "").strip() |
| if dob_raw and len(dob_raw) >= 10 and dob_raw[4:5] == "-": |
| parts = dob_raw.split("-") |
| if len(parts) >= 3: |
| dob_show = f"{parts[1]}/{parts[2]}/{parts[0]}" |
| else: |
| dob_show = dob_raw |
| else: |
| dob_show = dob_raw or "_(DOB not set)_" |
| clinic = _resolve_visit_clinic_destination( |
| profile_id or "", clinic_select, clinic_custom, |
| ) or "_(clinic not set)_" |
| today = date.today().strftime("%B %d, %Y") |
| return ( |
| f"**PATIENT:** {name} Β· **DOB:** {dob_show} Β· **DATE:** {today}\n\n" |
| f"**CLINIC:** {clinic}" |
| ) |
|
|
|
|
| def _visit_header_update(profile_id, clinic_select, clinic_custom): |
| return gr.update( |
| value=_visit_brief_header_md(profile_id, clinic_select, clinic_custom), |
| ) |
|
|
|
|
| def _visit_symptoms_summary_text(entries) -> str: |
| entries = entries or [] |
| if not entries: |
| return "*None added for this visit*" |
| if len(entries) == 1: |
| return _symptom_entry_summary(entries[0], 48) |
| return f"{len(entries)} symptom(s) for this visit" |
|
|
|
|
| def _visit_questions_summary_text(qlist) -> str: |
| qlist = qlist or [] |
| if not qlist: |
| return "*No questions added yet*" |
| if len(qlist) == 1: |
| q = qlist[0] |
| return q if len(q) <= 48 else q[:47] + "β¦" |
| return f"{len(qlist)} question(s) ready" |
|
|
|
|
| _VISIT_CHIP_DEST_BASE = "π©Ί Choose Destination" |
| _VISIT_CHIP_PURPOSE_BASE = "π― Visit Purpose" |
| _VISIT_CHIP_SYMPTOMS_BASE = "π©Ί Select Symptoms" |
| _VISIT_CHIP_QUESTIONS_BASE = "β Add Questions" |
|
|
|
|
| def _visit_chip_destination_label(profile_id, selection, custom) -> str: |
| text = _resolve_visit_clinic_destination(profile_id, selection, custom) |
| if not text or text == _VISIT_CLINIC_UNSET_LABEL: |
| return _VISIT_CHIP_DEST_BASE |
| short = text if len(text) <= 26 else text[:25] + "β¦" |
| return f"π©Ί Destination (β) β {short}" |
|
|
|
|
| def _visit_chip_purpose_label(reason, reason_other, concerns) -> str: |
| has_reason = bool((reason or "").strip()) |
| has_concerns = bool(concerns) |
| if not has_reason and not has_concerns: |
| return _VISIT_CHIP_PURPOSE_BASE |
| parts = [] |
| if has_reason: |
| resolved = ( |
| (reason_other or "").strip() |
| if reason == "Other" |
| else (reason or "").strip() |
| ) |
| if resolved: |
| parts.append(resolved) |
| if has_concerns: |
| parts.append(f"{len(concerns)} concern(s)") |
| preview = "; ".join(parts) |
| if len(preview) > 30: |
| preview = preview[:29] + "β¦" |
| return f"π― Visit Purpose (β) β {preview}" |
|
|
|
|
| def _visit_chip_symptoms_label(entries) -> str: |
| n = len(entries or []) |
| if n == 0: |
| return _VISIT_CHIP_SYMPTOMS_BASE |
| if n == 1: |
| return "π©Ί Symptoms Selected (β)" |
| return f"π©Ί Symptoms Selected (β) β {n} logged" |
|
|
|
|
| def _visit_chip_questions_label(qlist) -> str: |
| n = len(qlist or []) |
| if n == 0: |
| return _VISIT_CHIP_QUESTIONS_BASE |
| return f"β Questions ({n}) (β)" |
|
|
|
|
| def _visit_all_chip_label_updates( |
| profile_id, clinic_select, clinic_custom, |
| reason, reason_other, concerns, symptoms, questions, |
| ): |
| return ( |
| gr.update(value=_visit_chip_destination_label( |
| profile_id, clinic_select, clinic_custom, |
| )), |
| gr.update(value=_visit_chip_purpose_label(reason, reason_other, concerns)), |
| gr.update(value=_visit_chip_symptoms_label(symptoms)), |
| gr.update(value=_visit_chip_questions_label(questions)), |
| ) |
|
|
|
|
| def _visit_exclusive_panel_toggle( |
| which, |
| dest_open, |
| purpose_open, |
| symptoms_open, |
| questions_open, |
| profile_id, |
| selection, |
| custom, |
| snapshot, |
| ): |
| """Open one visit workspace at a time; clicking again collapses it.""" |
| opens = { |
| "dest": bool(dest_open), |
| "purpose": bool(purpose_open), |
| "symptoms": bool(symptoms_open), |
| "questions": bool(questions_open), |
| } |
| if opens[which]: |
| new_opens = {k: False for k in opens} |
| else: |
| new_opens = {k: (k == which) for k in opens} |
|
|
| snap = snapshot |
| if new_opens["dest"]: |
| snap = {"select": selection, "custom": (custom or "")} |
|
|
| sym_pick = gr.update() |
| if new_opens["symptoms"]: |
| sym_pick = gr.update( |
| choices=profiles.get_tracked_symptoms(profile_id or ""), |
| value=[], |
| ) |
|
|
| return ( |
| gr.update(visible=new_opens["dest"]), |
| new_opens["dest"], |
| gr.update(visible=new_opens["purpose"]), |
| new_opens["purpose"], |
| gr.update(visible=new_opens["symptoms"]), |
| new_opens["symptoms"], |
| sym_pick, |
| gr.update(visible=new_opens["questions"]), |
| new_opens["questions"], |
| snap, |
| ) |
|
|
|
|
| def clear_visit_form(profile_id=None): |
| """Reset Doctor Visit form fields; keeps active profile when signed in.""" |
| label = _format_profile_clinic_label(profile_id or "") |
| default = label if label else _VISIT_CLINIC_UNSET_LABEL |
| summary = _visit_destination_summary_text(profile_id or "", default, "") |
| header = _visit_brief_header_md(profile_id or "", default, "") |
| sym_choices = profiles.get_tracked_symptoms(profile_id or "") |
| chip_labels = _visit_all_chip_label_updates( |
| profile_id or "", default, "", None, "", [], [], [], |
| ) |
| return [ |
| profile_id, |
| gr.update( |
| choices=_visit_clinic_dropdown_choices(profile_id or ""), |
| value=default, |
| ), |
| gr.update(value="", visible=False), |
| None, |
| "", |
| [], |
| [], |
| [], |
| True, |
| True, |
| True, |
| True, |
| gr.update(value=_render_concerns_md([])), |
| gr.update(value=""), |
| gr.update(value=_render_symptoms_list_md([])), |
| gr.update(choices=[], value=None), |
| gr.update(value=_SYMPTOM_VIEW_PLACEHOLDER), |
| gr.update(value=_visit_reason_summary_text(None, "")), |
| gr.update(value=_visit_concerns_summary_text([])), |
| gr.update(value=summary), |
| gr.update(value=header), |
| gr.update(visible=False), |
| False, |
| gr.update(visible=False), |
| False, |
| gr.update(visible=False), |
| False, |
| gr.update(visible=False), |
| False, |
| gr.update(choices=sym_choices, value=[]), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=5), |
| *chip_labels, |
| ] |
|
|
|
|
| def _render_concerns_md(clist): |
| """Render primary concerns as a numbered list.""" |
| clist = clist or [] |
| if not clist: |
| return "*No concerns added yet β type one on the left and click β Add.*" |
| lines = [] |
| for i, item in enumerate(clist, start=1): |
| lines.append(f"{i}. {item}") |
| return "\n".join(lines) |
|
|
|
|
| def _add_visit_concern(current_list, new_text): |
| current_list = current_list or [] |
| new_text = (new_text or "").strip() |
| if not new_text: |
| summary = _visit_concerns_summary_text(current_list) |
| return current_list, _render_concerns_md(current_list), "", gr.update(value=summary) |
| new_list = list(current_list) + [new_text] |
| summary = _visit_concerns_summary_text(new_list) |
| return new_list, _render_concerns_md(new_list), "", gr.update(value=summary) |
|
|
|
|
| def _append_concern_token(current_list, token): |
| """Quick-add a preset concern token to the visit list.""" |
| new_list, md, _, summary_up = _add_visit_concern(current_list, token) |
| return new_list, md, summary_up |
|
|
|
|
| _VISIT_CONCERN_QUICK_TOKENS = ( |
| "Review Meds", |
| "Discuss Lab Results", |
| "Renew Prescription", |
| ) |
|
|
|
|
| def _clear_visit_concerns(): |
| return ( |
| [], |
| _render_concerns_md([]), |
| "", |
| gr.update(value=_visit_concerns_summary_text([])), |
| ) |
|
|
|
|
| _SYMPTOM_VIEW_PLACEHOLDER = "*Select an entry in the list to view full details.*" |
|
|
|
|
| def _symptom_entry_summary(entry: dict, max_len: int = 42) -> str: |
| parts = [] |
| syms = entry.get("symptoms") or [] |
| if syms: |
| labels = [s for s in syms if s != "Other"] |
| if labels: |
| parts.append(", ".join(labels)) |
| if "Other" in syms and (entry.get("symptom_other") or "").strip(): |
| parts.append(f"Other: {entry['symptom_other'].strip()}") |
| pain = entry.get("pain_level") |
| if pain is not None and str(pain).strip() != "": |
| parts.append(f"Pain {pain}/10") |
| notes = (entry.get("notes") or "").strip() |
| if notes: |
| parts.append(notes) |
| text = " β ".join(parts) if parts else "Empty entry" |
| if len(text) > max_len: |
| return text[: max_len - 1] + "β¦" |
| return text |
|
|
|
|
| def _render_symptoms_list_md(entries) -> str: |
| entries = entries or [] |
| if not entries: |
| return "*No symptoms logged yet β use β Add on the left.*" |
| return "\n".join( |
| f"{i}. {_symptom_entry_summary(e)}" for i, e in enumerate(entries, start=1) |
| ) |
|
|
|
|
| def _symptom_view_choices(entries) -> list[tuple[str, int]]: |
| return [ |
| (f"{i + 1}. {_symptom_entry_summary(e, 36)}", i) |
| for i, e in enumerate(entries or []) |
| ] |
|
|
|
|
| def _view_symptom_entry(entries, idx): |
| entries = entries or [] |
| if idx is None or not (0 <= idx < len(entries)): |
| return _SYMPTOM_VIEW_PLACEHOLDER |
| e = entries[idx] |
| lines = [] |
| syms = e.get("symptoms") or [] |
| if syms: |
| lines.append("**Symptoms reported:** " + ", ".join(syms)) |
| if (e.get("symptom_other") or "").strip(): |
| lines.append(f"**Other detail:** {e['symptom_other'].strip()}") |
| if e.get("pain_level") is not None: |
| lines.append(f"**Pain level:** {e['pain_level']}/10") |
| if (e.get("notes") or "").strip(): |
| lines.append(f"**Symptom notes:**\n{e['notes'].strip()}") |
| return "\n\n".join(lines) if lines else "_(Empty entry)_" |
|
|
|
|
| def _flatten_symptom_entries(entries): |
| """Convert logged symptom entries to brief generator fields.""" |
| if not entries: |
| return [], "", "" |
| if not isinstance(entries[0], dict): |
| return list(entries or []), "", "" |
| all_symptoms: list[str] = [] |
| other_texts: list[str] = [] |
| note_lines: list[str] = [] |
| for e in entries: |
| entry_syms = [] |
| for s in e.get("symptoms") or []: |
| if s == "Other": |
| if (e.get("symptom_other") or "").strip(): |
| other_texts.append(e["symptom_other"].strip()) |
| entry_syms.append(e["symptom_other"].strip()) |
| else: |
| all_symptoms.append(s) |
| entry_syms.append(s) |
| if (e.get("notes") or "").strip(): |
| note_lines.append(e["notes"].strip()) |
| if e.get("pain_level") is not None: |
| label = ", ".join(entry_syms) if entry_syms else "Symptom" |
| note_lines.append(f"{label}: pain {e['pain_level']}/10") |
| seen: set[str] = set() |
| uniq: list[str] = [] |
| for s in all_symptoms: |
| if s not in seen: |
| seen.add(s) |
| uniq.append(s) |
| other = ( |
| other_texts[0] if len(other_texts) == 1 else "\n".join(other_texts) |
| ) |
| return uniq, other, "\n".join(note_lines) |
|
|
|
|
| def _add_visit_symptoms_to_report(entries, picked, pain_level, custom_text, notes): |
| """Add profile-picked and/or custom symptoms to the visit report.""" |
| entries = list(entries or []) |
| picked = list(picked or []) |
| custom_text = (custom_text or "").strip() |
| notes = (notes or "").strip() |
| pain = int(pain_level) if pain_level is not None else None |
|
|
| added = False |
| for label in picked: |
| label = (label or "").strip() |
| if not label: |
| continue |
| entries.append({ |
| "symptoms": [label], |
| "symptom_other": "", |
| "notes": notes, |
| "pain_level": pain, |
| "source": "profile", |
| }) |
| added = True |
| notes = "" |
|
|
| if custom_text: |
| entries.append({ |
| "symptoms": ["Other"], |
| "symptom_other": custom_text, |
| "notes": notes, |
| "pain_level": pain, |
| "source": "custom", |
| }) |
| added = True |
|
|
| if not added: |
| return ( |
| entries, |
| _render_symptoms_list_md(entries), |
| gr.update(choices=_symptom_view_choices(entries), value=None), |
| gr.update(value=_SYMPTOM_VIEW_PLACEHOLDER), |
| gr.update(value=[]), |
| gr.update(value=""), |
| gr.update(value=""), |
| ) |
|
|
| return ( |
| entries, |
| _render_symptoms_list_md(entries), |
| gr.update(choices=_symptom_view_choices(entries), value=None), |
| gr.update(value=_SYMPTOM_VIEW_PLACEHOLDER), |
| gr.update(value=[]), |
| gr.update(value=""), |
| gr.update(value=""), |
| ) |
|
|
|
|
| def _add_symptom_entry(entries, symptoms, symptom_other, notes): |
| entries = list(entries or []) |
| symptoms = list(symptoms or []) |
| symptom_other = (symptom_other or "").strip() |
| notes = (notes or "").strip() |
| if not symptoms and not symptom_other and not notes: |
| return ( |
| entries, |
| _render_symptoms_list_md(entries), |
| gr.update(choices=_symptom_view_choices(entries), value=None), |
| gr.update(value=_SYMPTOM_VIEW_PLACEHOLDER), |
| symptoms, |
| symptom_other, |
| notes, |
| ) |
| entries.append({ |
| "symptoms": symptoms, |
| "symptom_other": symptom_other, |
| "notes": notes, |
| }) |
| return ( |
| entries, |
| _render_symptoms_list_md(entries), |
| gr.update(choices=_symptom_view_choices(entries), value=None), |
| gr.update(value=_SYMPTOM_VIEW_PLACEHOLDER), |
| [], |
| "", |
| "", |
| ) |
|
|
|
|
| def _clear_visit_symptoms(): |
| return ( |
| [], |
| _render_symptoms_list_md([]), |
| gr.update(choices=[], value=None), |
| gr.update(value=_SYMPTOM_VIEW_PLACEHOLDER), |
| gr.update(value=[]), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=5), |
| ) |
|
|
|
|
| def _concerns_for_brief(concerns) -> str: |
| """Accept list (new UI) or legacy string for the brief generator.""" |
| if isinstance(concerns, list): |
| return "\n".join(concerns) |
| return (concerns or "").strip() |
|
|
|
|
| def _build_q_choices(qlist): |
| """Build (label, idx) dropdown choices for the delete/edit menus.""" |
| qlist = qlist or [] |
| return [ |
| (f"{i+1}. {q[:60]}{'β¦' if len(q) > 60 else ''}", i) |
| for i, q in enumerate(qlist) |
| ] |
|
|
|
|
| def _render_questions_md(qlist): |
| """Render the dynamic questions list as a markdown panel.""" |
| if not qlist: |
| return "*No questions added yet. Type a question above and click β Add.*" |
| lines = [] |
| for i, q in enumerate(qlist, start=1): |
| lines.append(f"{i}. {q}") |
| return "\n".join(lines) |
|
|
|
|
| def _render_mode_md(editing_idx): |
| """Indicator banner above the question input.""" |
| if editing_idx is not None: |
| return ( |
| f"βοΈ **Editing question #{editing_idx + 1}** β modify the text above " |
| f"and click β Add Question to save (or Cancel Edit to discard)." |
| ) |
| return "β **Adding new question** β type above and click β Add Question." |
|
|
|
|
| def _add_or_update_visit_question(current_list, new_q, editing_idx): |
| """Single button serves both Add (default) and Update (when editing_idx is set).""" |
| new_q = (new_q or "").strip() |
| current_list = current_list or [] |
| q_choices = _build_q_choices(current_list) |
|
|
| if not new_q: |
| return ( |
| current_list, _render_questions_md(current_list), |
| "", editing_idx, |
| gr.update(choices=q_choices, value=None), |
| gr.update(choices=q_choices, value=None), |
| gr.update(visible=(editing_idx is None)), |
| gr.update(visible=(editing_idx is not None)), |
| _render_mode_md(editing_idx), |
| ) |
|
|
| if editing_idx is not None and 0 <= editing_idx < len(current_list): |
| new_list = list(current_list) |
| new_list[editing_idx] = new_q |
| else: |
| new_list = current_list + [new_q] |
| new_choices = _build_q_choices(new_list) |
|
|
| return ( |
| new_list, _render_questions_md(new_list), |
| "", None, |
| gr.update(choices=new_choices, value=None), |
| gr.update(choices=new_choices, value=None), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| _render_mode_md(None), |
| ) |
|
|
|
|
| def _delete_visit_question(current_list, idx): |
| current_list = current_list or [] |
| if idx is None or not (0 <= idx < len(current_list)): |
| q_choices = _build_q_choices(current_list) |
| return ( |
| current_list, _render_questions_md(current_list), |
| gr.update(choices=q_choices, value=None), |
| gr.update(choices=q_choices, value=None), |
| ) |
| new_list = list(current_list) |
| new_list.pop(idx) |
| new_choices = _build_q_choices(new_list) |
| return ( |
| new_list, _render_questions_md(new_list), |
| gr.update(choices=new_choices, value=None), |
| gr.update(choices=new_choices, value=None), |
| ) |
|
|
|
|
| def _clear_visit_questions(): |
| return ( |
| [], _render_questions_md([]), |
| gr.update(choices=[], value=None), |
| gr.update(choices=[], value=None), |
| gr.update(visible=True), gr.update(visible=False), |
| _render_mode_md(None), |
| ) |
|
|
|
|
| def _init_edit_visit_question(current_list, idx): |
| """Load question at idx into input and enter edit mode.""" |
| current_list = current_list or [] |
| q_choices = _build_q_choices(current_list) |
| if idx is None or not (0 <= idx < len(current_list)): |
| return ( |
| current_list, _render_questions_md(current_list), |
| "", None, |
| gr.update(choices=q_choices, value=None), |
| gr.update(choices=q_choices, value=None), |
| gr.update(visible=True), gr.update(visible=False), |
| _render_mode_md(None), |
| ) |
| return ( |
| current_list, _render_questions_md(current_list), |
| current_list[idx], |
| idx, |
| gr.update(choices=q_choices, value=None), |
| gr.update(choices=q_choices, value=idx), |
| gr.update(visible=False), |
| gr.update(visible=True), |
| _render_mode_md(idx), |
| ) |
|
|
|
|
| def _cancel_edit_visit_question(current_list): |
| """Exit edit mode, clear input, reset buttons.""" |
| current_list = current_list or [] |
| q_choices = _build_q_choices(current_list) |
| return ( |
| current_list, _render_questions_md(current_list), |
| "", None, |
| gr.update(choices=q_choices, value=None), |
| gr.update(choices=q_choices, value=None), |
| gr.update(visible=True), gr.update(visible=False), |
| _render_mode_md(None), |
| ) |
|
|
|
|
| def _on_symptoms_change(sel): |
| """Show the 'Other' textbox only when 'Other' is checked.""" |
| return gr.update(visible=("Other" in (sel or []))) |
|
|
|
|
| def _on_reason_change(reason, reason_other): |
| """Show 'Other' field and refresh the reason card summary.""" |
| return ( |
| gr.update(visible=(reason == "Other")), |
| gr.update(value=_visit_reason_summary_text(reason, reason_other)), |
| ) |
|
|
|
|
| def _on_reason_other_change(reason, reason_other): |
| return gr.update(value=_visit_reason_summary_text(reason, reason_other)) |
|
|
|
|
| |
| |
| |
|
|
| def generate_export(inc_meds, inc_appts, inc_food, custom_note, profile_id): |
| return export_tool.assemble_export( |
| include_meds=inc_meds, |
| include_appointments=inc_appts, |
| include_food=inc_food, |
| custom_note=custom_note, |
| profile_id=profile_id, |
| ) |
|
|
|
|
| def build_mailto(export_text: str, email_to: str) -> str: |
| link = export_tool.build_mailto_link( |
| to=email_to, |
| body=export_text, |
| ) |
| return f'<a href="{link}" target="_blank" style="color:#1e7a8f;font-weight:600;">π§ Open in Email Client</a>' |
|
|
|
|
| |
| |
| |
|
|
| def load_settings(): |
| """Return the saved model path for the AI Model field.""" |
| c = cfg.load_patient_config() |
| return c.get("model_path", str(cfg.MODEL_PATH)) |
|
|
|
|
| def save_settings(model_path): |
| """Persist the model path. Patient data is now stored in profiles.py.""" |
| c = cfg.load_patient_config() |
| c["model_path"] = model_path |
| cfg.save_patient_config(c) |
|
|
| |
| rag = _safe_import_rag() |
| if rag: |
| rag.reset_engine() |
|
|
| return gr.Info("β
AI model path saved.") |
|
|
|
|
| def _load_profiles_into_dropdown(preselect_id=None): |
| choices = _profile_dropdown_choices() |
| if preselect_id is not None: |
| value = preselect_id |
| elif not profiles.get_profiles(): |
| value = _CREATE_NEW_PROFILE_ID |
| else: |
| value = None |
| return gr.update(choices=choices, value=value) |
|
|
|
|
| |
|
|
| |
| _RELATIONSHIP_DETAIL_CHOICES = { |
| "Parent": ["Mom", "Dad", "Guardian"], |
| "Sibling": ["Brother", "Sister"], |
| |
| } |
|
|
| |
| |
| _LABEL_REVERSE_MAP = { |
| "Self": ("Self", None, ""), |
| "Mom": ("Parent", "Mom", ""), |
| "Dad": ("Parent", "Dad", ""), |
| "Guardian": ("Parent", "Guardian", ""), |
| "Child": ("Child", None, ""), |
| "Brother": ("Sibling", "Brother", ""), |
| "Sister": ("Sibling", "Sister", ""), |
| "Grandchild": ("Grandchild", None, ""), |
| "Care Provider": ("Care Provider", None, ""), |
| } |
|
|
|
|
| def _parse_label(label_str: str): |
| """Reverse-map a stored label to (category, detail, custom).""" |
| if not label_str: |
| return None, None, "" |
| if label_str in _LABEL_REVERSE_MAP: |
| return _LABEL_REVERSE_MAP[label_str] |
| return "Other", None, label_str |
|
|
|
|
| def _compute_label(category, detail, custom) -> str: |
| """Forward-map dropdown state to a single label string for storage.""" |
| if not category: |
| return "" |
| if category == "Other": |
| return (custom or "").strip() |
| if category in ("Self", "Child", "Grandchild", "Care Provider"): |
| return category |
| return (detail or "").strip() |
|
|
|
|
| def _on_relationship_category_change(category): |
| """Reveal the Specific dropdown for categories that have sub-choices; |
| reveal the Custom textbox for 'Other'; hide both otherwise. |
| |
| The Specific dropdown's value is always set to a VALID choice (first |
| in the list, or kept from before) so Gradio 5.x's strict validation |
| at page-load time never sees value=None/'' against the choices list. |
| """ |
| if category in _RELATIONSHIP_DETAIL_CHOICES: |
| sub_choices = _RELATIONSHIP_DETAIL_CHOICES[category] |
| return ( |
| gr.update(visible=True, |
| choices=sub_choices, |
| value=sub_choices[0]), |
| gr.update(visible=False, value=""), |
| ) |
| if category == "Other": |
| return ( |
| gr.update(visible=False, value=_RELATIONSHIP_DETAIL_DEFAULT), |
| gr.update(visible=True, value=""), |
| ) |
| return ( |
| gr.update(visible=False, value=_RELATIONSHIP_DETAIL_DEFAULT), |
| gr.update(visible=False, value=""), |
| ) |
|
|
|
|
| def _safe_str(val) -> str: |
| """Normalize a field value to a stripped string. Handles None and |
| non-str (e.g. gr.DateTime may pass a datetime-like object).""" |
| if val is None: |
| return "" |
| if isinstance(val, str): |
| return val.strip() |
| return str(val).strip() |
|
|
|
|
| def _resolve_profile_label(care_mode, rel_cat, rel_det, rel_cus) -> str: |
| """Own-care profiles are always labelled 'Self'; otherwise use relationship fields.""" |
| if _safe_str(care_mode) == "Managing my own care": |
| return "Self" |
| return _compute_label(rel_cat, rel_det, rel_cus) |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| _PROFILE_FIELD_COUNT = 23 |
|
|
| |
| _RELATIONSHIP_DETAIL_DEFAULT = "Mom" |
| _RELATIONSHIP_CATEGORY_CHOICES = [ |
| "Parent", "Child", "Sibling", "Grandchild", "Care Provider", "Other", |
| ] |
| _RELATIONSHIP_DEFAULT_CATEGORY = "Parent" |
|
|
| |
| _CREATE_NEW_PROFILE_ID = "__create_new__" |
| _CREATE_NEW_LABEL = "β Create New Profile" |
|
|
|
|
| def _app_datetime(label: str, *, calendar_only: bool = False, **kwargs) -> gr.DateTime: |
| """Program-wide DateTime with keyboard-friendly date entry.""" |
| classes = list(kwargs.pop("elem_classes", ["app-datetime"])) |
| if calendar_only: |
| classes.append("app-datetime-calendar-only") |
| default_info = ( |
| None |
| if calendar_only |
| else "Type YYYY-MM-DD (e.g. 1952-03-15) in the field, or type the year in the calendar header" |
| ) |
| return gr.DateTime( |
| label=label, |
| include_time=kwargs.pop("include_time", False), |
| type=kwargs.pop("type", "string"), |
| value=kwargs.pop("value", ""), |
| elem_classes=classes, |
| info=kwargs.pop("info", default_info), |
| scale=kwargs.pop("scale", 0 if calendar_only else None), |
| min_width=kwargs.pop("min_width", 96 if calendar_only else 160), |
| **kwargs, |
| ) |
|
|
|
|
| def _profile_dropdown_choices() -> list[tuple[str, str]]: |
| return [(_CREATE_NEW_LABEL, _CREATE_NEW_PROFILE_ID)] + profiles.get_profile_choices() |
|
|
|
|
| def _hidden_profile_selector_update(value): |
| """Sync hidden prof_selector choices + value (Gradio 5 strict dropdown validation).""" |
| return gr.update(choices=_profile_dropdown_choices(), value=value) |
|
|
|
|
| def _existing_profile_choices() -> list[tuple[str, str]]: |
| return profiles.get_profile_choices() |
|
|
|
|
| def _profile_view_summary(profile_id) -> str: |
| """Read-only summary text for the View/Edit workspace.""" |
| if not profile_id: |
| return "Select a saved profile above to view or edit." |
| p = profiles.get_profile(profile_id) |
| if not p: |
| return "Profile not found." |
| lines = [ |
| f"**{p.get('full_name') or 'Unnamed'}** Β· {p.get('label', '')}", |
| f"DOB: {p.get('dob') or 'β'} Β· Phone: {_format_us_phone(p.get('phone_number')) or 'β'}", |
| f"Email: {p.get('email') or 'β'}", |
| f"Care mode: {p.get('care_mode') or 'β'}", |
| f"Insurance: {p.get('insurance_provider') or 'β'}", |
| f"PCP: {p.get('pcp_name') or 'β'}", |
| ] |
| addr = profiles.compose_address(*profiles.address_parts(p)) if p else "" |
| if addr: |
| lines.append(f"Address: {addr.replace(chr(10), ', ')}") |
| return "\n".join(lines) |
|
|
|
|
| def _cmd_bar_button_updates(active: str): |
| """Highlight the active command-bar button.""" |
| return ( |
| gr.update(variant="primary" if active == "view_edit" else "secondary"), |
| gr.update(variant="primary" if active == "delete" else "secondary"), |
| gr.update(variant="primary" if active == "create" else "secondary"), |
| ) |
|
|
|
|
| def _workspace_visibility(active: str, *, show_form: bool = True): |
| """Show exactly one workflow workspace.""" |
| form_visible = show_form and active in ("view_edit", "create") |
| return ( |
| gr.update(visible=(active == "view_edit")), |
| gr.update(visible=(active == "delete")), |
| gr.update(visible=(active == "create")), |
| gr.update(visible=form_visible), |
| gr.update(visible=(active == "create")), |
| gr.update(visible=(active == "view_edit" and show_form)), |
| ) |
|
|
|
|
| def _open_view_edit_workspace(preselect_id=None): |
| """Command bar β View/Edit workspace.""" |
| existing = _existing_profile_choices() |
| pid = preselect_id |
| if pid and not any(p[1] == pid for p in existing): |
| pid = None |
| if pid is None and existing: |
| pid = existing[0][1] |
| if pid: |
| loaded = _on_profile_select(pid) |
| status = _profile_view_summary(pid) |
| view_sel = gr.update(choices=existing, value=pid) |
| hidden_sel = _hidden_profile_selector_update(pid) |
| else: |
| loaded = _blank_profile_form_updates() + list( |
| _profile_route_suffix("Managing someone else's care"), |
| ) |
| status = "No saved profiles yet. Use **β Create New Profile** to add one." |
| view_sel = gr.update(choices=[], value=None) |
| hidden_sel = _hidden_profile_selector_update(_CREATE_NEW_PROFILE_ID) |
| return ( |
| *_cmd_bar_button_updates("view_edit"), |
| *_workspace_visibility("view_edit", show_form=bool(pid)), |
| view_sel, |
| hidden_sel, |
| gr.update(value=status), |
| *loaded, |
| gr.update(choices=existing, value=pid), |
| ) |
|
|
|
|
| def _open_delete_workspace(): |
| """Command bar β Delete workspace.""" |
| existing = _existing_profile_choices() |
| pid = existing[0][1] if existing else None |
| status = ( |
| "Select a profile below, then confirm deletion." |
| if pid |
| else "No saved profiles to delete." |
| ) |
| return ( |
| *_cmd_bar_button_updates("delete"), |
| *_workspace_visibility("delete", show_form=False), |
| gr.update(choices=existing, value=pid), |
| gr.update(value=status), |
| ) |
|
|
|
|
| def _open_create_workspace(): |
| """Command bar β Create New Profile workspace.""" |
| existing = _existing_profile_choices() |
| form = _blank_profile_form_updates() |
| return ( |
| *_cmd_bar_button_updates("create"), |
| *_workspace_visibility("create"), |
| gr.update(choices=existing, value=None), |
| _hidden_profile_selector_update(_CREATE_NEW_PROFILE_ID), |
| gr.update(value=""), |
| *form, |
| *_profile_route_suffix("Managing someone else's care"), |
| gr.update(choices=existing, value=None), |
| ) |
|
|
|
|
| def _after_profile_created(): |
| """After Add New Profile β open View/Edit on the newest profile.""" |
| existing = _existing_profile_choices() |
| return _open_view_edit_workspace(existing[-1][1] if existing else None) |
|
|
|
|
| def _is_create_selection(selection) -> bool: |
| return not selection or selection == _CREATE_NEW_PROFILE_ID |
|
|
|
|
| |
|
|
| _HOME_TAB_INDEX = 0 |
| _SETTINGS_TAB_INDEX = 5 |
|
|
|
|
| def _session_active(profile_id) -> bool: |
| return bool(profile_id) and not _is_create_selection(profile_id) |
|
|
|
|
| def _session_banner_text(profile_id): |
| if not _session_active(profile_id): |
| return ( |
| "π **No active care profile** β click **Sign In** to open the app, " |
| "or use **Settings & Profiles** to manage profiles." |
| ) |
| p = profiles.get_profile(profile_id) |
| name = profiles.profile_display_name(p) if p else "Care Profile" |
| return f"π€ **Active care profile:** {name}" |
|
|
|
|
| def _session_banner_update(profile_id): |
| """Markdown + CSS class for the session banner.""" |
| active = _session_active(profile_id) |
| classes = ["session-banner"] if active else ["session-banner", "locked"] |
| return gr.update(value=_session_banner_text(profile_id), elem_classes=classes) |
|
|
|
|
| def _persist_session(profile_id): |
| """Write active profile id to disk; ignore create-placeholder selections.""" |
| if _session_active(profile_id): |
| cfg.save_active_profile_id(profile_id) |
| else: |
| cfg.save_active_profile_id(None) |
|
|
|
|
| def _load_persisted_session_id(): |
| """Restore session from disk if the profile still exists.""" |
| pid = cfg.load_active_profile_id() |
| if pid and profiles.get_profile(pid): |
| return pid |
| if pid: |
| cfg.save_active_profile_id(None) |
| return None |
|
|
|
|
| def _session_auth_btn_update(profile_id): |
| """Single Sign In / Sign Out toggle label.""" |
| if _session_active(profile_id): |
| return gr.update(value="π Sign Out", variant="secondary") |
| return gr.update(value="π Sign In", variant="primary") |
|
|
|
|
| def _session_gate_updates(profile_id): |
| """Five operational tabs: locked visible when signed out, active when signed in.""" |
| active = _session_active(profile_id) |
| locked = gr.update(visible=not active) |
| unlocked = gr.update(visible=active) |
| return (locked, unlocked) * 5 |
|
|
|
|
| def _session_main_tabs_update(profile_id): |
| """Home when signed in; Settings when signed out.""" |
| if _session_active(profile_id): |
| return gr.update(selected=_HOME_TAB_INDEX) |
| return gr.update(selected=_SETTINGS_TAB_INDEX) |
|
|
|
|
| def _apply_session_from_selector(selection): |
| aid = selection if _session_active(selection) else None |
| _persist_session(aid) |
| return ( |
| aid, |
| _session_banner_update(aid), |
| *_session_gate_updates(aid), |
| _session_main_tabs_update(aid), |
| ) |
|
|
|
|
| def _init_active_session(): |
| pid = _load_persisted_session_id() |
| return ( |
| pid, |
| _session_banner_update(pid), |
| *_session_gate_updates(pid), |
| _session_main_tabs_update(pid), |
| _session_auth_btn_update(pid), |
| ) |
|
|
|
|
| def _sign_out_session(): |
| """Phase C: explicit sign-out β locks tabs and clears persisted session.""" |
| _persist_session(None) |
| return ( |
| None, |
| _session_banner_update(None), |
| *_session_gate_updates(None), |
| _session_main_tabs_update(None), |
| gr.update(value=None, choices=profiles.get_profile_choices()), |
| ) |
|
|
|
|
| def _resolve_sign_in_profile_id(): |
| """Choose a profile to activate when the user clicks Sign In.""" |
| all_p = profiles.get_profiles() |
| if not all_p: |
| return None |
| if len(all_p) == 1: |
| return all_p[0]["id"] |
| saved = cfg.load_active_profile_id() |
| if saved and profiles.get_profile(saved): |
| return saved |
| return all_p[0]["id"] |
|
|
|
|
| def _sign_in_session(): |
| """Activate a care profile and open Home β no Settings form changes.""" |
| pid = _resolve_sign_in_profile_id() |
| if not pid: |
| gr.Warning( |
| "No care profiles yet β open **Settings & Profiles** and use " |
| "**β Create New Profile** first." |
| ) |
| return ( |
| *_apply_session_from_selector(None), |
| gr.update(value=None, choices=profiles.get_profile_choices()), |
| _session_auth_btn_update(None), |
| *_visit_clinic_sync_updates(None), |
| ) |
| p = profiles.get_profile(pid) or {} |
| gr.Info(f"Signed in as {profiles.profile_display_name(p)}") |
| return ( |
| *_apply_session_from_selector(pid), |
| gr.update(value=pid, choices=profiles.get_profile_choices()), |
| _session_auth_btn_update(pid), |
| *_visit_clinic_sync_updates(pid), |
| ) |
|
|
|
|
| def _session_auth_toggle(profile_id): |
| """One button: sign in when logged out, sign out when logged in.""" |
| if _session_active(profile_id): |
| gr.Info("Signed out β care profile deactivated.") |
| return ( |
| *_sign_out_session(), |
| _session_auth_btn_update(None), |
| *_visit_clinic_sync_updates(None), |
| ) |
| return _sign_in_session() |
|
|
|
|
| def _restore_profile_workspace_on_load(profile_id, output_count: int): |
| """Re-open View/Edit after browser refresh when a session was persisted.""" |
| if not _session_active(profile_id): |
| return tuple(gr.update() for _ in range(output_count)) |
| return _open_view_edit_workspace(profile_id) |
|
|
|
|
| def _go_to_settings_tab(): |
| return gr.update(selected=_SETTINGS_TAB_INDEX) |
|
|
|
|
| def _locked_gateway_md(title: str, subtitle: str = "") -> str: |
| sub = subtitle or ( |
| "To view, add, or manage data in this section, please select or create " |
| "an active profile in the **Settings & Profiles** tab first." |
| ) |
| return ( |
| f'<div class="locked-gateway">' |
| f'<div class="locked-title">{title}</div>' |
| f'<div class="locked-icon">π</div>' |
| f'<div class="locked-headline">Locked</div>' |
| f'<div class="locked-headline" style="font-size:1rem;font-weight:500;">' |
| f'No Active Care Profile Selected</div>' |
| f'<div class="locked-sub">{sub}</div>' |
| f'</div>' |
| ) |
|
|
|
|
| def _blank_profile_form_updates(): |
| """Return gr.update() list for all 31 profile fields with Gradio-safe defaults.""" |
| return [ |
| gr.update(value="Managing someone else's care"), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=_RELATIONSHIP_DEFAULT_CATEGORY), |
| gr.update(visible=False, value=_RELATIONSHIP_DETAIL_DEFAULT), |
| gr.update(visible=False, value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| gr.update(value=""), |
| ] |
|
|
|
|
| def _identity_layout_updates(care_mode): |
| """Grid class + Side A/B headers for own-care vs caregiver layout.""" |
| own = care_mode == "Managing my own care" |
| layout = "identity-layout-own" if own else "identity-layout-other" |
| if own: |
| a_hdr = "π **Personal Information (Side A)**" |
| b_hdr = "π **Contact Information (Side B)**" |
| else: |
| a_hdr = "π **Patient Information (Side A)**" |
| b_hdr = "π₯ **Caregiver Contact Info (Side B)**" |
| return ( |
| gr.update(elem_classes=["identity-info-grid"]), |
| gr.update(value=a_hdr), |
| gr.update(value=b_hdr), |
| ) |
|
|
|
|
| def _on_care_mode_change_profile(care_mode): |
| """Show Caregiver Name and Relationship fields only when managing someone |
| else's care. Own-care profiles always store label 'Self'.""" |
| managing_other = care_mode != "Managing my own care" |
| layout_grid, layout_a_hdr, layout_b_hdr = _identity_layout_updates(care_mode) |
| if managing_other: |
| return ( |
| gr.update(visible=True), |
| gr.update(visible=True), |
| gr.update(value=_RELATIONSHIP_DEFAULT_CATEGORY, visible=True), |
| gr.update(visible=False, value=_RELATIONSHIP_DETAIL_DEFAULT), |
| gr.update(visible=False, value=""), |
| layout_grid, |
| layout_a_hdr, |
| layout_b_hdr, |
| gr.update(visible=True), |
| gr.update(visible=True), |
| ) |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(value="Self", visible=False), |
| gr.update(visible=False, value=_RELATIONSHIP_DETAIL_DEFAULT), |
| gr.update(visible=False, value=""), |
| layout_grid, |
| layout_a_hdr, |
| layout_b_hdr, |
| gr.update(visible=True), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def _on_profile_select(profile_id): |
| """Load all 23 form fields from the active profile, plus reset the |
| Contact 2 group visibility (hidden by default; only revealed if the |
| saved profile actually has a 2nd contact).""" |
| blank = _blank_profile_form_updates() |
| if _is_create_selection(profile_id): |
| return blank + list(_profile_route_suffix("Managing someone else's care")) |
| p = profiles.get_profile(profile_id) |
| if not p: |
| return blank + list(_profile_route_suffix("Managing someone else's care")) |
|
|
| |
| care_mode = p.get("care_mode") or "Managing someone else's care" |
| managing_other = care_mode != "Managing my own care" |
|
|
| category, detail, custom = _parse_label(p.get("label", "")) |
| if not managing_other: |
| category = "Self" |
| detail_update = gr.update(visible=False, value=_RELATIONSHIP_DETAIL_DEFAULT) |
| custom_update = gr.update(visible=False, value="") |
| elif category == "Self": |
| category = _RELATIONSHIP_DEFAULT_CATEGORY |
| detail = _RELATIONSHIP_DETAIL_DEFAULT |
| detail_update = gr.update( |
| visible=True, |
| choices=_RELATIONSHIP_DETAIL_CHOICES[category], |
| value=detail, |
| ) |
| custom_update = gr.update(visible=False, value=custom) |
| elif category in _RELATIONSHIP_DETAIL_CHOICES: |
| detail_update = gr.update( |
| visible=True, |
| choices=_RELATIONSHIP_DETAIL_CHOICES[category], |
| value=detail, |
| ) |
| custom_update = gr.update(visible=False, value=custom) |
| else: |
| detail_update = gr.update(visible=False, value=_RELATIONSHIP_DETAIL_DEFAULT) |
| custom_update = gr.update(visible=(category == "Other"), value=custom) |
|
|
| |
| raw = p.get("emergency_contacts") or [] |
| contacts = list(raw) if isinstance(raw, list) else [] |
| if not contacts and (p.get("emergency_contact_name") or p.get("emergency_contact_phone")): |
| contacts = [{"name": p.get("emergency_contact_name", ""), |
| "phone": p.get("emergency_contact_phone", ""), |
| "relationship": ""}] |
| c1 = contacts[0] if len(contacts) >= 1 else {"name": "", "phone": "", "relationship": ""} |
| c2 = contacts[1] if len(contacts) >= 2 else {"name": "", "phone": "", "relationship": ""} |
| c2_visible = len(contacts) >= 2 |
| addr1, addr2, addr_city, addr_state, addr_zip = profiles.address_parts(p) |
| pcp1, pcp2, pcp_city, pcp_state, pcp_zip = profiles.address_parts(p, pcp=True) |
|
|
| return [ |
| gr.update(value=care_mode), |
| gr.update(value=p.get("caregiver_name", "")), |
| gr.update(value=_format_us_phone(p.get("caregiver_phone", ""))), |
| gr.update(value=p.get("caregiver_email", "")), |
| gr.update(value=category, visible=managing_other), |
| detail_update, |
| custom_update, |
| gr.update(value=p.get("full_name", "")), |
| gr.update(value=p.get("dob", "")), |
| gr.update(value=addr1), |
| gr.update(value=addr2), |
| gr.update(value=addr_city), |
| gr.update(value=addr_state), |
| gr.update(value=addr_zip), |
| gr.update(value=_format_us_phone(p.get("phone_number", ""))), |
| gr.update(value=p.get("email", "")), |
| gr.update(value=p.get("insurance_provider", "")), |
| gr.update(value=p.get("policy_id", "")), |
| gr.update(value=p.get("group_id", "")), |
| gr.update(value=p.get("pcp_name", "")), |
| gr.update(value=_format_us_phone(p.get("pcp_phone", ""))), |
| gr.update(value=p.get("pcp_clinic", "")), |
| gr.update(value=pcp1), |
| gr.update(value=pcp2), |
| gr.update(value=pcp_city), |
| gr.update(value=pcp_state), |
| gr.update(value=pcp_zip), |
| gr.update(value=c1.get("name", "")), |
| gr.update(value=_format_us_phone(c1.get("phone", ""))), |
| gr.update(value=c1.get("relationship", "")), |
| gr.update(value=c2.get("name", "")), |
| gr.update(value=_format_us_phone(c2.get("phone", ""))), |
| gr.update(value=c2.get("relationship", "")), |
| ] + list(_profile_route_suffix(care_mode, c2_visible=c2_visible)) |
|
|
|
|
| def _profile_card_sync_values(profile_id): |
| """Raw field values for _sync_cards_from_form (matches _card_sync_inputs order).""" |
| empty = ( |
| "", "", "", |
| _RELATIONSHIP_DEFAULT_CATEGORY, _RELATIONSHIP_DETAIL_DEFAULT, "", |
| "", "", "", "", "", |
| "", "", |
| "", "", "", "", "", "", |
| "", "", "", |
| "", "", |
| "", "", "", |
| "", "", "", "", "", |
| ) |
| if _is_create_selection(profile_id) or not profile_id: |
| return empty |
| p = profiles.get_profile(profile_id) |
| if not p: |
| return empty |
|
|
| care_mode = p.get("care_mode") or "Managing someone else's care" |
| managing_other = care_mode != "Managing my own care" |
| category, detail, custom = _parse_label(p.get("label", "")) |
| if not managing_other: |
| category = "Self" |
| elif category == "Self": |
| category = _RELATIONSHIP_DEFAULT_CATEGORY |
| detail = _RELATIONSHIP_DETAIL_DEFAULT |
|
|
| raw = p.get("emergency_contacts") or [] |
| contacts = list(raw) if isinstance(raw, list) else [] |
| if not contacts and (p.get("emergency_contact_name") or p.get("emergency_contact_phone")): |
| contacts = [{ |
| "name": p.get("emergency_contact_name", ""), |
| "phone": p.get("emergency_contact_phone", ""), |
| "relationship": "", |
| }] |
| c1 = contacts[0] if len(contacts) >= 1 else {"name": "", "phone": "", "relationship": ""} |
| c2 = contacts[1] if len(contacts) >= 2 else {"name": "", "phone": "", "relationship": ""} |
| addr1, addr2, addr_city, addr_state, addr_zip = profiles.address_parts(p) |
| pcp1, pcp2, pcp_city, pcp_state, pcp_zip = profiles.address_parts(p, pcp=True) |
|
|
| return ( |
| p.get("full_name", ""), |
| p.get("dob", ""), |
| p.get("caregiver_name", ""), |
| category, detail, custom, |
| addr1, addr2, addr_city, addr_state, addr_zip, |
| _format_us_phone(p.get("phone_number", "")), |
| p.get("email", ""), |
| c1.get("name", ""), |
| _format_us_phone(c1.get("phone", "")), |
| c1.get("relationship", ""), |
| c2.get("name", ""), |
| _format_us_phone(c2.get("phone", "")), |
| c2.get("relationship", ""), |
| p.get("insurance_provider", ""), |
| p.get("policy_id", ""), |
| p.get("group_id", ""), |
| _format_us_phone(p.get("caregiver_phone", "")), |
| p.get("caregiver_email", ""), |
| p.get("pcp_name", ""), |
| p.get("pcp_clinic", ""), |
| _format_us_phone(p.get("pcp_phone", "")), |
| pcp1, pcp2, pcp_city, pcp_state, pcp_zip, |
| ) |
|
|
|
|
| def _profile_route_suffix(care_mode, c2_visible=False, managing_other=None): |
| """Trailing View/Edit route outputs: contact2, groups, grid, headers, actions.""" |
| if managing_other is None: |
| managing_other = care_mode != "Managing my own care" |
| layout_grid, layout_a_hdr, layout_b_hdr = _identity_layout_updates(care_mode) |
| return ( |
| gr.update(visible=c2_visible), |
| gr.update(visible=managing_other), |
| gr.update(visible=managing_other), |
| layout_grid, |
| layout_a_hdr, |
| layout_b_hdr, |
| gr.update(visible=True), |
| ) |
|
|
|
|
| def _on_view_selector_change(selection): |
| """Load profile fields when the View/Edit workspace selection changes.""" |
| existing = _existing_profile_choices() |
| if not selection: |
| blank = _blank_profile_form_updates() |
| return ( |
| gr.update(value=None), |
| _hidden_profile_selector_update(_CREATE_NEW_PROFILE_ID), |
| gr.update(value="Select a saved profile above to view or edit."), |
| *blank, |
| *_profile_route_suffix("Managing someone else's care"), |
| gr.update(choices=existing, value=None), |
| ) |
| loaded = _on_profile_select(selection) |
| return ( |
| gr.update(value=selection), |
| _hidden_profile_selector_update(selection), |
| gr.update(value=_profile_view_summary(selection)), |
| *loaded, |
| gr.update(choices=existing, value=selection), |
| ) |
|
|
|
|
| def _refresh_profile_selectors(preselect_id=None): |
| """Refresh all profile dropdowns after create/delete.""" |
| existing = _existing_profile_choices() |
| pid = preselect_id |
| if pid and not any(p[1] == pid for p in existing): |
| pid = existing[0][1] if existing else None |
| if pid is None and existing: |
| pid = existing[0][1] |
| return ( |
| _hidden_profile_selector_update(pid or _CREATE_NEW_PROFILE_ID), |
| gr.update(choices=existing, value=pid), |
| gr.update(choices=existing, value=pid), |
| ) |
|
|
|
|
| def _view_profile(profile_id): |
| """Read-only summary for legacy callers.""" |
| return gr.update(value=_profile_view_summary(profile_id)) |
|
|
|
|
| def _show_edit_panel(): |
| return gr.update(visible=True) |
|
|
|
|
| def _toggle_optional_panel(is_open: bool): |
| """Open or close optional Settings sections (Insurance, PCP, Emergency).""" |
| new_open = not bool(is_open) |
| return gr.update(visible=new_open), new_open |
|
|
|
|
| def _toggle_profile_optional_panel(panel_key: str, active_panel): |
| """Toggle one optional profile section; close the others.""" |
| opening = active_panel != panel_key |
| new_active = panel_key if opening else "" |
| labels = { |
| "emergency": "Emergency contacts", |
| "insurance": "Insurance details", |
| "pcp": "Primary care doctor", |
| } |
| if opening: |
| gr.Info(f"{labels[panel_key]} opened β see the section below the buttons.") |
| return ( |
| gr.update(visible=(new_active == "emergency")), |
| gr.update(visible=(new_active == "insurance")), |
| gr.update(visible=(new_active == "pcp")), |
| new_active, |
| ) |
|
|
|
|
| def _close_all_optional_panels(): |
| """Hide every optional profile section.""" |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| "", |
| ) |
|
|
|
|
| def _toggle_emergency_panel(active_panel): |
| return _toggle_profile_optional_panel("emergency", active_panel) |
|
|
|
|
| def _toggle_insurance_panel(active_panel): |
| return _toggle_profile_optional_panel("insurance", active_panel) |
|
|
|
|
| def _toggle_pcp_panel(active_panel): |
| return _toggle_profile_optional_panel("pcp", active_panel) |
|
|
|
|
| def _active_profile_id(view_id, create_id): |
| """Resolve profile id from View/Edit or Create workspace selectors.""" |
| return view_id or create_id |
|
|
|
|
| def _welcome_banner_visible(): |
| """Show setup prompt whenever no profiles exist yet.""" |
| if profiles.get_profiles(): |
| return gr.update(visible=False) |
| return gr.update(visible=True) |
|
|
|
|
| def _welcome_dismiss(): |
| """Hide for now; returns on next Settings visit if still no profiles.""" |
| return gr.update(visible=False) |
|
|
|
|
| def _welcome_go(): |
| """Open the Create New Profile workspace.""" |
| opened = _open_create_workspace() |
| return ( |
| gr.update(visible=False), |
| *opened, |
| ) |
|
|
|
|
| def _init_settings_profile_tab(): |
| """First visit: welcome banner visibility only.""" |
| return _welcome_banner_visible() |
|
|
|
|
| def _save_identity(care_mode, caregiver_name, caregiver_phone, caregiver_email, |
| rel_cat, rel_det, rel_cus, |
| full_name, dob, |
| address_line1, address_line2, address_city, address_state, address_zip, |
| phone_number, email, |
| c1_name, c1_phone, c1_rel, |
| c2_name, c2_phone, c2_rel, |
| selected_id): |
| """Save identity + caregiver contact + emergency contacts.""" |
| label = _resolve_profile_label(care_mode, rel_cat, rel_det, rel_cus) |
| if not label: |
| return gr.Warning("β οΈ Relationship is required. Pick one from the dropdown.") |
|
|
| fields = { |
| "care_mode": _safe_str(care_mode) or "Managing someone else's care", |
| "caregiver_name": _safe_str(caregiver_name), |
| "caregiver_phone": _format_us_phone(caregiver_phone), |
| "caregiver_email": _safe_str(caregiver_email), |
| "label": label, |
| "full_name": _safe_str(full_name), |
| "dob": _safe_str(dob), |
| **_patient_address_profile_fields( |
| address_line1, address_line2, address_city, address_state, address_zip, |
| ), |
| "phone_number": _format_us_phone(phone_number), |
| "email": _safe_str(email), |
| "emergency_contacts": _build_emergency_contacts( |
| c1_name, c1_phone, c1_rel, c2_name, c2_phone, c2_rel, |
| ), |
| } |
| if not selected_id or _is_create_selection(selected_id): |
| profiles.create_profile(label=label, template=fields) |
| return gr.Info( |
| f"β
Profile '{label}' created. Select it from the dropdown above " |
| "to save Insurance or Primary Care Doctor." |
| ) |
| profiles.update_profile(selected_id, fields) |
| return gr.Info(f"β
Identity saved for '{label}'.") |
|
|
|
|
| def _add_new_profile(care_mode, caregiver_name, caregiver_phone, caregiver_email, |
| rel_cat, rel_det, rel_cus, |
| full_name, dob, |
| address_line1, address_line2, address_city, address_state, address_zip, |
| phone_number, email, |
| insurance, policy, group, |
| pcp_name, pcp_clinic, pcp_phone, |
| pcp_address_line1, pcp_address_line2, pcp_address_city, |
| pcp_address_state, pcp_address_zip, |
| c1_name, c1_phone, c1_rel, |
| c2_name, c2_phone, c2_rel): |
| """Create a new profile from ALL form fields at once. Wired to the |
| 'β
Save New Profile' button (Create workspace only). Required: |
| Relationship + Full Legal Name. Other sections optional. |
| """ |
| label = _resolve_profile_label(care_mode, rel_cat, rel_det, rel_cus) |
| if not label: |
| return gr.Warning("β οΈ Relationship is required. Pick one from the dropdown.") |
| if not _safe_str(full_name): |
| return gr.Warning("β οΈ Full Legal Name is required.") |
|
|
| contacts = [] |
| if _safe_str(c1_name) or _safe_str(c1_phone): |
| contacts.append({ |
| "name": _safe_str(c1_name), |
| "phone": _format_us_phone(c1_phone), |
| "relationship": _safe_str(c1_rel), |
| }) |
| if _safe_str(c2_name) or _safe_str(c2_phone): |
| contacts.append({ |
| "name": _safe_str(c2_name), |
| "phone": _format_us_phone(c2_phone), |
| "relationship": _safe_str(c2_rel), |
| }) |
|
|
| fields = { |
| "care_mode": _safe_str(care_mode) or "Managing someone else's care", |
| "caregiver_name": _safe_str(caregiver_name), |
| "caregiver_phone": _format_us_phone(caregiver_phone), |
| "caregiver_email": _safe_str(caregiver_email), |
| "label": label, |
| "full_name": _safe_str(full_name), |
| "dob": _safe_str(dob), |
| **_patient_address_profile_fields( |
| address_line1, address_line2, address_city, address_state, address_zip, |
| ), |
| "phone_number": _format_us_phone(phone_number), |
| "email": _safe_str(email), |
| "insurance_provider": _safe_str(insurance), |
| "policy_id": _safe_str(policy), |
| "group_id": _safe_str(group), |
| "pcp_name": _safe_str(pcp_name), |
| "pcp_clinic": _safe_str(pcp_clinic), |
| "pcp_phone": _format_us_phone(pcp_phone), |
| **_pcp_address_profile_fields( |
| pcp_address_line1, pcp_address_line2, pcp_address_city, |
| pcp_address_state, pcp_address_zip, |
| ), |
| "emergency_contacts": contacts, |
| } |
|
|
| profiles.create_profile(label=label, template=fields) |
| return gr.Info( |
| f"β
Profile '{label}' created with all sections. Select it from the " |
| "dropdown above to edit later." |
| ) |
|
|
|
|
| def _save_insurance(provider, policy, group, selected_id): |
| if _is_create_selection(selected_id): |
| return gr.Warning("β οΈ Select a profile first, or use β Create New Profile.") |
| profiles.update_profile(selected_id, { |
| "insurance_provider": _safe_str(provider), |
| "policy_id": _safe_str(policy), |
| "group_id": _safe_str(group), |
| }) |
| return gr.Info("β
Insurance saved.") |
|
|
|
|
| def _save_pcp(name, clinic, phone, |
| address_line1, address_line2, address_city, address_state, address_zip, |
| selected_id): |
| if _is_create_selection(selected_id): |
| return gr.Warning("β οΈ Select a profile first, or use β Create New Profile.") |
| profiles.update_profile(selected_id, { |
| "pcp_name": _safe_str(name), |
| "pcp_clinic": _safe_str(clinic), |
| "pcp_phone": _format_us_phone(phone), |
| **_pcp_address_profile_fields( |
| address_line1, address_line2, address_city, address_state, address_zip, |
| ), |
| }) |
| return gr.Info("β
Primary Care Doctor saved.") |
|
|
|
|
| def _save_contacts(c1_name, c1_phone, c1_rel, |
| c2_name, c2_phone, c2_rel, |
| selected_id): |
| if _is_create_selection(selected_id): |
| return gr.Warning("β οΈ Select a profile first, or use β Create New Profile.") |
| contacts = [] |
| if _safe_str(c1_name) or _safe_str(c1_phone): |
| contacts.append({ |
| "name": _safe_str(c1_name), |
| "phone": _format_us_phone(c1_phone), |
| "relationship": _safe_str(c1_rel), |
| }) |
| if _safe_str(c2_name) or _safe_str(c2_phone): |
| contacts.append({ |
| "name": _safe_str(c2_name), |
| "phone": _format_us_phone(c2_phone), |
| "relationship": _safe_str(c2_rel), |
| }) |
| profiles.update_profile(selected_id, {"emergency_contacts": contacts}) |
| if not contacts: |
| return gr.Info("π§Ή Emergency contacts cleared.") |
| return gr.Info(f"β
{len(contacts)} emergency contact(s) saved.") |
|
|
|
|
| def _clear_profile_form(): |
| """Reset create workspace form without touching saved data.""" |
| return _open_create_workspace() |
|
|
|
|
| def _delete_profile(selected_id): |
| noop = [gr.update()] * 33 |
| group_noop = [gr.update()] * len(_profile_route_suffix("Managing someone else's care")) |
| if not selected_id: |
| return ( |
| gr.update(value="β οΈ Select a saved profile to delete."), |
| gr.update(), |
| *_cmd_bar_button_updates("delete"), |
| *_workspace_visibility("delete", show_form=False), |
| gr.update(), gr.update(), gr.update(), |
| *noop, |
| *group_noop, |
| gr.update(), |
| ) |
| label = (profiles.get_profile(selected_id) or {}).get("label", "Unknown") |
| profiles.delete_profile_data(selected_id) |
| profiles.delete_profile(selected_id) |
| remaining = _existing_profile_choices() |
| if remaining: |
| pid = remaining[0][1] |
| return ( |
| gr.update(value=f"ποΈ Profile '{label}' deleted."), |
| gr.update(choices=remaining, value=pid), |
| *_cmd_bar_button_updates("delete"), |
| *_workspace_visibility("delete", show_form=False), |
| gr.update(choices=remaining, value=pid), |
| _hidden_profile_selector_update(pid), |
| gr.update(value=""), |
| *noop, |
| *group_noop, |
| gr.update(choices=remaining, value=pid), |
| ) |
| msg = f"ποΈ Profile '{label}' deleted. No profiles remain β create a new one below." |
| opened = _open_create_workspace() |
| return ( |
| gr.update(value=msg), |
| gr.update(choices=[], value=None), |
| *opened, |
| ) |
|
|
|
|
| def get_system_status() -> str: |
| rag = _safe_import_rag() |
| lines = [] |
|
|
| |
| model_path = cfg.get_model_path() |
| if Path(model_path).exists(): |
| size_gb = Path(model_path).stat().st_size / (1024**3) |
| lines.append(f"π’ **Model:** {Path(model_path).name} ({size_gb:.1f} GB)") |
| else: |
| lines.append(f"π΄ **Model:** Not found at `{model_path}`") |
| lines.append(" β Download Qwen 2.5 14B Q4_K_M from:") |
| lines.append(" https://huggingface.co/Qwen/Qwen2.5-14B-Instruct-GGUF") |
|
|
| |
| if rag: |
| status = rag.get_document_status() |
| if status["total_chunks"] > 0: |
| lines.append(f"π’ **Index:** {status['total_chunks']} chunks, {status['num_documents']} document(s)") |
| else: |
| lines.append("βͺ **Index:** Empty β upload PDFs in Settings & Profiles") |
| else: |
| lines.append("π΄ **RAG Engine:** Could not load β check requirements.txt") |
|
|
| |
| for label, path in [ |
| ("Medications", cfg.MEDICATIONS_JSON), |
| ("Appointments", cfg.APPOINTMENTS_JSON), |
| ("Food Chart", cfg.FOOD_CHART_JSON), |
| ]: |
| if path.exists(): |
| lines.append(f"π’ **{label}:** {path.name}") |
| else: |
| lines.append(f"βͺ **{label}:** No data yet") |
|
|
| return "\n".join(lines) |
|
|
|
|
| |
| |
| |
|
|
| EXAMPLE_QUESTIONS = [ |
| "What did the doctor note about my blood work?", |
| "How much did insurance pay on this claim?", |
| "What medications does the patient take?", |
| "When is the next follow-up appointment?", |
| ] |
|
|
| HOME_EXAMPLE_CHIPS = [ |
| "Add a new medication", |
| "Schedule an appointment", |
| "Log a symptom", |
| "What medications am I on?", |
| ] |
|
|
| with gr.Blocks( |
| title="AidAiLine", |
| css=_CSS, |
| head=_HEAD_JS, |
| theme=gr.themes.Soft( |
| primary_hue=gr.themes.colors.teal, |
| secondary_hue=gr.themes.colors.cyan, |
| neutral_hue=gr.themes.colors.slate, |
| font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], |
| ), |
| ) as demo: |
|
|
| |
| gr.HTML(""" |
| <div style=" |
| background: linear-gradient(135deg, #145f70, #1e7a8f, #2a9db5); |
| border-radius: 12px; |
| padding: 20px 28px; |
| margin-bottom: 16px; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| flex-wrap: wrap; |
| gap: 12px; |
| box-shadow: 0 4px 20px rgba(30,122,143,0.25); |
| "> |
| <div> |
| <h1 style="margin:0; color:#fff; font-size:22px; font-weight:700; letter-spacing:-0.3px;"> |
| π₯ AidAiLine |
| </h1> |
| <p style="margin:4px 0 0; color:rgba(255,255,255,0.85); font-size:13px;"> |
| AI-powered medical document assistant for caregivers |
| </p> |
| </div> |
| <div style=" |
| background: rgba(255,255,255,0.15); |
| border: 1px solid rgba(255,255,255,0.3); |
| border-radius: 8px; |
| padding: 8px 16px; |
| color: #fff; |
| font-size: 13px; |
| font-weight: 500; |
| white-space: nowrap; |
| "> |
| π’ Offline Mode β No data leaves this computer |
| </div> |
| </div> |
| """) |
|
|
| active_profile_id = gr.State(None) |
| _bootstrap_session_id = _load_persisted_session_id() |
| _bootstrap_signed_in = _session_active(_bootstrap_session_id) |
| _initial_tab_index = ( |
| _HOME_TAB_INDEX if _bootstrap_signed_in else _SETTINGS_TAB_INDEX |
| ) |
| with gr.Row(elem_classes=["session-bar"]): |
| session_banner = gr.Markdown( |
| value=_session_banner_text(_bootstrap_session_id), |
| elem_classes=["session-banner", "locked"] if not _bootstrap_signed_in else ["session-banner"], |
| ) |
| session_auth_btn = gr.Button( |
| "π Sign Out" if _bootstrap_signed_in else "π Sign In", |
| variant="secondary" if _bootstrap_signed_in else "primary", |
| size="sm", |
| elem_classes=["session-auth-btn"], |
| ) |
|
|
| with gr.Tabs(selected=_initial_tab_index) as main_tabs: |
|
|
| |
| with gr.Tab( |
| "π Home", |
| elem_id="home-tab", |
| ) as home_tab: |
| with gr.Group(visible=True, elem_classes=["locked-gateway-wrap"]) as home_locked_view: |
| gr.HTML(_locked_gateway_md("π HOME CONTROL")) |
| home_go_settings_btn = gr.Button( |
| "βοΈ Go to Settings & Profiles", variant="primary", |
| ) |
| with gr.Group(visible=False) as home_active_view: |
| with gr.Group( |
| elem_classes=["profile-field-card", "home-brief-card"], |
| ): |
| with gr.Row(elem_classes=["med-catalog-header", "home-brief-header"]): |
| gr.Markdown( |
| "π **Today's Health Brief**", |
| elem_classes=["field-card-title"], |
| ) |
| home_brief_refresh_btn = gr.Button( |
| "π Refresh", |
| size="sm", |
| variant="secondary", |
| elem_classes=["home-brief-refresh-btn"], |
| ) |
| home_brief_md = gr.HTML( |
| _home_health_brief_md( |
| _bootstrap_session_id if _bootstrap_signed_in else None |
| ), |
| elem_classes=["home-brief-body"], |
| ) |
| home_chat_state = gr.State(_blank_home_chat_state()) |
| with gr.Group( |
| elem_classes=["profile-field-card", "home-chat-shell"], |
| ): |
| gr.HTML( |
| '<div class="home-chat-title">π¬ AidAi-Line Chat Assistant</div>' |
| ) |
| chatbot = gr.Chatbot( |
| type="messages", |
| height=288, |
| show_label=False, |
| avatar_images=(None, "π₯"), |
| ) |
| with gr.Row(): |
| chat_input = gr.Textbox( |
| placeholder="Type your message or command hereβ¦", |
| show_label=False, |
| lines=1, |
| scale=5, |
| ) |
| send_btn = gr.Button("Send", variant="primary", scale=1) |
| home_example_btns = [] |
| with gr.Row(elem_classes=["home-example-chips"]): |
| for q in HOME_EXAMPLE_CHIPS: |
| home_example_btns.append( |
| gr.Button(q, size="sm", variant="secondary") |
| ) |
| clear_btn = gr.Button( |
| "ποΈ Clear Chat", variant="secondary", size="sm", |
| elem_classes=["home-chat-clear-btn"], |
| ) |
|
|
| home_brief_refresh_btn.click( |
| fn=_home_health_brief_md, |
| inputs=[active_profile_id], |
| outputs=[home_brief_md], |
| ) |
| home_tab.select( |
| fn=_home_tab_on_select, |
| inputs=[active_profile_id, chatbot, home_chat_state], |
| outputs=[home_brief_md, chatbot, home_chat_state], |
| ) |
|
|
| |
| with gr.Tab( |
| "π Medications", |
| elem_id="medications-tab", |
| ) as med_tab: |
| with gr.Group(visible=True, elem_classes=["locked-gateway-wrap"]) as med_locked_view: |
| gr.HTML(_locked_gateway_md("π MEDICATIONS CONTROL")) |
| med_go_settings_btn = gr.Button( |
| "βοΈ Go to Settings & Profiles", variant="primary", |
| ) |
| with gr.Group(visible=False) as med_active_view: |
| gr.Markdown( |
| "Track **current and past medications** for the signed-in care profile." |
| ) |
|
|
| med_add_panel_open = gr.State(False) |
| med_delete_panel_open = gr.State(False) |
| med_filter_panel_open = gr.State(False) |
| med_filter = gr.State("all") |
|
|
| with gr.Group( |
| elem_classes=["profile-create-shell", "med-manager-shell"], |
| ): |
| gr.Markdown( |
| "##### π Medication List", |
| elem_classes=["med-manager-shell-title"], |
| ) |
| med_summary_md = gr.Markdown( |
| value=_med_list_summary_md("all"), |
| elem_classes=["med-summary-line"], |
| ) |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-table-card"], |
| ): |
| med_table = gr.Dataframe( |
| value=_med_table_rows("all"), |
| headers=[ |
| "Name", "Dosage", "Frequency", "Status", |
| "Category", "Side Effects", "ID", |
| ], |
| interactive=False, |
| wrap=True, |
| elem_id="med-table", |
| ) |
| with gr.Row( |
| elem_classes=["med-action-row"], |
| ): |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-catalog-card"], |
| ): |
| with gr.Row(elem_classes=["med-catalog-header"]): |
| gr.Markdown( |
| "π **Show**", |
| elem_classes=["field-card-title"], |
| ) |
| med_chip_show_btn = gr.Button( |
| "βοΈ Open", |
| variant="link", |
| size="sm", |
| elem_classes=[ |
| "field-edit-link", "med-catalog-toggle", |
| ], |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["field-card-view"], |
| ) as med_show_card_view: |
| med_show_display = gr.Markdown( |
| _med_show_display_md("all"), |
| elem_classes=["field-card-value"], |
| ) |
| with gr.Group( |
| visible=False, |
| elem_classes=["field-card-edit"], |
| ) as med_filter_panel: |
| with gr.Column( |
| elem_classes=["address-edit-grid", "med-filter-options"], |
| ): |
| med_filter_all_btn = gr.Button( |
| "All medications", |
| size="sm", |
| variant="secondary", |
| ) |
| med_filter_current_btn = gr.Button( |
| "Current medications only", |
| size="sm", |
| variant="secondary", |
| ) |
| med_filter_past_btn = gr.Button( |
| "Past medications only", |
| size="sm", |
| variant="secondary", |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| med_filter_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-catalog-card"], |
| ): |
| with gr.Row(elem_classes=["med-catalog-header"]): |
| gr.Markdown( |
| "β **Add Medication**", |
| elem_classes=["field-card-title"], |
| ) |
| med_chip_add_btn = gr.Button( |
| "βοΈ Open", |
| variant="link", |
| size="sm", |
| elem_classes=[ |
| "field-edit-link", "med-catalog-toggle", |
| ], |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["field-card-view"], |
| ) as med_add_card_view: |
| med_add_display = gr.Markdown( |
| _med_add_display_md(), |
| elem_classes=["field-card-value"], |
| ) |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-catalog-card"], |
| ): |
| with gr.Row(elem_classes=["med-catalog-header"]): |
| gr.Markdown( |
| "ποΈ **Remove Medication**", |
| elem_classes=["field-card-title"], |
| ) |
| med_chip_delete_btn = gr.Button( |
| "βοΈ Open", |
| variant="link", |
| size="sm", |
| elem_classes=[ |
| "field-edit-link", "med-catalog-toggle", |
| ], |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["field-card-view"], |
| ) as med_delete_card_view: |
| med_delete_display = gr.Markdown( |
| _med_delete_display_md(), |
| elem_classes=["field-card-value"], |
| ) |
| with gr.Group( |
| visible=False, |
| elem_classes=["field-card-edit", "med-delete-form"], |
| ) as med_delete_panel: |
| with gr.Column(elem_classes=["address-edit-grid"]): |
| with gr.Group(elem_classes=["address-subfield"]): |
| med_del_id = gr.Dropdown( |
| choices=_med_choices(), |
| value=None, |
| allow_custom_value=True, |
| label="Select medication", |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| del_med_btn = gr.Button( |
| "ποΈ Delete", variant="stop", size="sm", |
| ) |
| med_delete_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Group( |
| visible=False, |
| elem_classes=["med-add-workspace"], |
| ) as med_add_panel: |
| with gr.Group( |
| elem_classes=["profile-field-card", "med-add-matrix-shell"], |
| ): |
| gr.Markdown( |
| "β **Add / Edit Medication Record**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Column(elem_classes=["med-add-matrix"]): |
| with gr.Row(elem_classes=["med-add-row"]): |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield"]): |
| med_name = gr.Textbox( |
| label="Name *", |
| placeholder="e.g. Lisinopril", |
| ) |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield"]): |
| med_dosage = gr.Textbox( |
| label="Dosage", |
| placeholder="e.g. 10mg", |
| ) |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield"]): |
| med_freq = gr.Textbox( |
| label="Frequency", |
| placeholder="e.g. once daily", |
| ) |
| with gr.Row(elem_classes=["med-add-row"]): |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield"]): |
| med_category = gr.Textbox( |
| label="Category", |
| placeholder="e.g. Blood Pressure", |
| ) |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield"]): |
| med_status = gr.Radio( |
| choices=["current", "past"], |
| value="current", |
| label="Status", |
| ) |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield"]): |
| med_side_fx = gr.Textbox( |
| label="Side Effects", |
| placeholder="e.g. may cause dizziness", |
| ) |
| with gr.Group( |
| elem_classes=["address-subfield", "med-add-notes-block"], |
| ): |
| med_notes = gr.Textbox( |
| label="Personal Notes", |
| placeholder=( |
| 'e.g. "Take with evening mealβ¦"' |
| ), |
| lines=3, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| add_med_btn = gr.Button( |
| "β
Save Medication", variant="primary", size="sm", |
| ) |
| med_add_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
|
|
| _med_filter_outputs = [ |
| med_filter, med_table, med_del_id, med_summary_md, |
| med_filter_panel, med_filter_panel_open, med_show_card_view, |
| med_show_display, med_delete_display, med_chip_show_btn, |
| ] |
| _med_panel_toggle_outputs = [ |
| med_filter_panel, med_filter_panel_open, |
| med_add_panel, med_add_panel_open, |
| med_delete_panel, med_delete_panel_open, |
| med_show_card_view, med_add_card_view, med_delete_card_view, |
| med_show_display, med_delete_display, |
| med_chip_show_btn, med_chip_add_btn, med_chip_delete_btn, |
| ] |
| for _filt_btn, _filt_val in ( |
| (med_filter_all_btn, "all"), |
| (med_filter_current_btn, "current"), |
| (med_filter_past_btn, "past"), |
| ): |
| _filt_btn.click( |
| fn=lambda pid, fv=_filt_val: _med_apply_filter(fv, pid), |
| inputs=[active_profile_id], |
| outputs=_med_filter_outputs, |
| ) |
| med_chip_show_btn.click( |
| fn=_med_chip_show_click, |
| inputs=[ |
| med_filter_panel_open, med_add_panel_open, |
| med_delete_panel_open, med_filter, active_profile_id, |
| ], |
| outputs=_med_panel_toggle_outputs, |
| ) |
| med_chip_add_btn.click( |
| fn=_med_chip_add_click, |
| inputs=[ |
| med_filter_panel_open, med_add_panel_open, |
| med_delete_panel_open, med_filter, active_profile_id, |
| ], |
| outputs=_med_panel_toggle_outputs, |
| ) |
| med_chip_delete_btn.click( |
| fn=_med_chip_delete_click, |
| inputs=[ |
| med_filter_panel_open, med_add_panel_open, |
| med_delete_panel_open, med_filter, active_profile_id, |
| ], |
| outputs=_med_panel_toggle_outputs, |
| ) |
| med_filter_cancel_btn.click( |
| fn=_med_cancel_show, |
| inputs=None, |
| outputs=[ |
| med_filter_panel, med_filter_panel_open, |
| med_show_card_view, med_chip_show_btn, |
| ], |
| ) |
| med_add_cancel_btn.click( |
| fn=_med_cancel_add, |
| inputs=None, |
| outputs=[ |
| med_add_panel, med_add_panel_open, |
| med_add_card_view, med_chip_add_btn, |
| ], |
| ) |
| med_delete_cancel_btn.click( |
| fn=_med_cancel_delete, |
| inputs=None, |
| outputs=[ |
| med_delete_panel, med_delete_panel_open, |
| med_delete_card_view, med_chip_delete_btn, |
| ], |
| ) |
| add_med_btn.click( |
| fn=add_med_handler, |
| inputs=[ |
| med_name, med_dosage, med_freq, med_status, |
| med_side_fx, med_notes, med_category, |
| active_profile_id, med_filter, |
| ], |
| outputs=[ |
| gr.Textbox(visible=False), |
| med_table, med_del_id, |
| med_name, med_dosage, med_freq, med_status, |
| med_side_fx, med_notes, med_category, |
| med_add_panel, med_add_panel_open, med_add_card_view, |
| med_delete_display, med_chip_add_btn, med_summary_md, |
| ], |
| ) |
| del_med_btn.click( |
| fn=delete_med_handler, |
| inputs=[med_del_id, active_profile_id, med_filter], |
| outputs=[ |
| gr.Textbox(visible=False), |
| med_table, med_del_id, |
| med_delete_panel, med_delete_panel_open, |
| med_delete_card_view, med_delete_display, |
| med_chip_delete_btn, med_summary_md, |
| ], |
| ) |
|
|
| def _med_tab_refresh(profile_id): |
| return ( |
| "all", |
| _med_table_rows("all", profile_id), |
| gr.update(choices=_med_choices(profile_id), value=None), |
| gr.update(value=_med_list_summary_md("all", profile_id)), |
| gr.update(value=_med_show_display_md("all")), |
| gr.update(value=_med_delete_display_md(profile_id)), |
| gr.update(visible=True), |
| gr.update(visible=False), False, |
| gr.update(visible=True), |
| gr.update(visible=False), False, |
| gr.update(visible=True), |
| gr.update(visible=False), False, |
| gr.update(value=_med_catalog_toggle_label(False)), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
| med_tab.select( |
| fn=_med_tab_refresh, |
| inputs=[active_profile_id], |
| outputs=[ |
| med_filter, med_table, med_del_id, med_summary_md, |
| med_show_display, med_delete_display, |
| med_show_card_view, |
| med_filter_panel, med_filter_panel_open, |
| med_add_card_view, |
| med_add_panel, med_add_panel_open, |
| med_delete_card_view, |
| med_delete_panel, med_delete_panel_open, |
| med_chip_show_btn, med_chip_add_btn, med_chip_delete_btn, |
| ], |
| ) |
|
|
| |
| with gr.Tab( |
| "π½οΈ Diet & Allergies", |
| elem_id="food-tab", |
| ) as food_tab: |
| with gr.Group(visible=True, elem_classes=["locked-gateway-wrap"]) as food_locked_view: |
| gr.HTML(_locked_gateway_md("π½οΈ DIET & ALLERGIES CONTROL")) |
| food_go_settings_btn = gr.Button( |
| "βοΈ Go to Settings & Profiles", variant="primary", |
| ) |
| with gr.Group(visible=False) as food_active_view: |
| gr.Markdown( |
| "Track **allergies, preferred foods, and aversions** for the signed-in care profile." |
| ) |
|
|
| food_view_state = gr.State("allergies") |
| food_show_panel_open = gr.State(False) |
| food_add_panel_open = gr.State(False) |
| food_edit_panel_open = gr.State(False) |
| food_delete_panel_open = gr.State(False) |
|
|
| with gr.Group( |
| elem_classes=["profile-create-shell", "med-manager-shell"], |
| ): |
| gr.Markdown( |
| "##### π½οΈ Diet & Allergies", |
| elem_classes=["med-manager-shell-title"], |
| ) |
| food_summary_md = gr.Markdown( |
| value=_food_list_summary_md("allergies"), |
| elem_classes=["med-summary-line"], |
| ) |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-table-card"], |
| ): |
| with gr.Column(visible=True) as allergies_view: |
| allergy_table = gr.Dataframe( |
| value=_food_rows("allergies"), |
| headers=["Name", "Severity", "Notes", "ID"], |
| elem_classes="allergy-table", |
| interactive=False, |
| wrap=True, |
| ) |
| with gr.Column(visible=False) as preferred_view: |
| liked_table = gr.Dataframe( |
| value=_food_rows("liked_foods"), |
| headers=["Name", "Notes", "ID"], |
| elem_classes="preferred-table", |
| interactive=False, |
| wrap=True, |
| ) |
| with gr.Column(visible=False) as aversions_view: |
| disliked_table = gr.Dataframe( |
| value=_food_rows("disliked_foods"), |
| headers=["Name", "Notes", "ID"], |
| elem_classes="aversion-table", |
| interactive=False, |
| wrap=True, |
| ) |
| with gr.Row(elem_classes=["med-action-row", "food-action-row"]): |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-catalog-card"], |
| ): |
| with gr.Row(elem_classes=["med-catalog-header"]): |
| gr.Markdown( |
| "π **Show**", |
| elem_classes=["field-card-title"], |
| ) |
| food_chip_show_btn = gr.Button( |
| "βοΈ Open", |
| variant="link", |
| size="sm", |
| elem_classes=[ |
| "field-edit-link", "med-catalog-toggle", |
| ], |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["field-card-view"], |
| ) as food_show_card_view: |
| food_show_display = gr.Markdown( |
| _food_show_display_md("allergies"), |
| elem_classes=["field-card-value"], |
| ) |
| with gr.Group( |
| visible=False, |
| elem_classes=["field-card-edit", "food-show-panel"], |
| ) as food_show_panel: |
| with gr.Column(elem_classes=["address-edit-grid", "food-show-options"]): |
| food_show_allergies_btn = gr.Button( |
| "β οΈ Allergies", |
| size="sm", |
| variant="secondary", |
| ) |
| food_show_preferred_btn = gr.Button( |
| "β
Preferred Foods", |
| size="sm", |
| variant="secondary", |
| ) |
| food_show_aversions_btn = gr.Button( |
| "β Food Aversions", |
| size="sm", |
| variant="secondary", |
| ) |
| refresh_food_btn = gr.Button( |
| "π Refresh tables", |
| size="sm", |
| variant="secondary", |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| food_show_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-catalog-card"], |
| ): |
| with gr.Row(elem_classes=["med-catalog-header"]): |
| gr.Markdown( |
| "β **Add**", |
| elem_classes=["field-card-title"], |
| ) |
| food_chip_add_btn = gr.Button( |
| "βοΈ Open", |
| variant="link", |
| size="sm", |
| elem_classes=[ |
| "field-edit-link", "med-catalog-toggle", |
| ], |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["field-card-view"], |
| ) as food_add_card_view: |
| food_add_display = gr.Markdown( |
| _food_add_display_md(), |
| elem_classes=["field-card-value"], |
| ) |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-catalog-card"], |
| ): |
| with gr.Row(elem_classes=["med-catalog-header"]): |
| gr.Markdown( |
| "βοΈ **Edit**", |
| elem_classes=["field-card-title"], |
| ) |
| food_chip_edit_btn = gr.Button( |
| "βοΈ Open", |
| variant="link", |
| size="sm", |
| elem_classes=[ |
| "field-edit-link", "med-catalog-toggle", |
| ], |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["field-card-view"], |
| ) as food_edit_card_view: |
| food_edit_display = gr.Markdown( |
| _food_edit_display_md(), |
| elem_classes=["field-card-value"], |
| ) |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-catalog-card"], |
| ): |
| with gr.Row(elem_classes=["med-catalog-header"]): |
| gr.Markdown( |
| "ποΈ **Remove**", |
| elem_classes=["field-card-title"], |
| ) |
| food_chip_delete_btn = gr.Button( |
| "βοΈ Open", |
| variant="link", |
| size="sm", |
| elem_classes=[ |
| "field-edit-link", "med-catalog-toggle", |
| ], |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["field-card-view"], |
| ) as food_delete_card_view: |
| food_delete_display = gr.Markdown( |
| _food_delete_display_md(), |
| elem_classes=["field-card-value"], |
| ) |
| with gr.Group( |
| visible=False, |
| elem_classes=["field-card-edit", "food-delete-form"], |
| ) as food_delete_panel: |
| with gr.Column(elem_classes=["address-edit-grid"]): |
| food_del_cat = gr.Dropdown( |
| choices=_CATEGORY_CHOICES, |
| value=None, |
| label="Select a category first", |
| ) |
| food_del_ids = gr.CheckboxGroup( |
| choices=[], |
| label="Select entries to delete", |
| visible=False, |
| ) |
| with gr.Column(visible=False) as delete_confirm_col: |
| del_confirm_cb = gr.Checkbox( |
| label="I confirm β delete the selected entries", |
| value=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| del_food_btn = gr.Button( |
| "ποΈ Delete Selected", variant="stop", size="sm", |
| ) |
| food_delete_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Group( |
| visible=False, |
| elem_classes=["med-add-workspace"], |
| ) as food_add_panel: |
| with gr.Group( |
| elem_classes=["profile-field-card", "med-add-matrix-shell"], |
| ): |
| gr.Markdown( |
| "β **Add Entry**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Column(elem_classes=["med-add-matrix", "food-add-matrix"]): |
| with gr.Row(elem_classes=["med-add-row"]): |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield"]): |
| food_category = gr.Dropdown( |
| choices=_CATEGORY_CHOICES, |
| value=_CATEGORY_CHOICES[0], |
| label="Category", |
| ) |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield"]): |
| food_name = gr.Textbox( |
| label="Allergen / Trigger *", |
| placeholder="e.g. Penicillin, Peanuts, Latex", |
| ) |
| with gr.Row(elem_classes=["med-add-row", "food-add-row2"]): |
| with gr.Column(min_width=0, visible=True) as severity_col: |
| with gr.Group(elem_classes=["address-subfield"]): |
| food_severity = gr.Dropdown( |
| choices=["mild", "moderate", "severe", "anaphylactic"], |
| value="mild", |
| label="Severity", |
| ) |
| with gr.Column(min_width=0) as food_notes_col: |
| with gr.Group(elem_classes=["address-subfield"]): |
| food_notes = gr.Textbox( |
| label="Reaction Notes", |
| placeholder="e.g. Causes rash and hives", |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| add_food_btn = gr.Button( |
| "β
Add Entry", variant="primary", size="sm", |
| ) |
| food_add_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Group( |
| visible=False, |
| elem_classes=["med-add-workspace"], |
| ) as food_edit_panel: |
| with gr.Group( |
| elem_classes=["profile-field-card", "med-add-matrix-shell"], |
| ): |
| gr.Markdown( |
| "βοΈ **Edit Entry**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Column(elem_classes=["med-add-matrix"]): |
| with gr.Row(elem_classes=["med-add-row"]): |
| with gr.Group(elem_classes=["address-subfield"]): |
| food_edit_cat = gr.Dropdown( |
| choices=_CATEGORY_CHOICES, |
| value=None, |
| label="Category", |
| ) |
| with gr.Group(elem_classes=["address-subfield"]): |
| food_edit_select = gr.Dropdown( |
| choices=[], |
| value=None, |
| label="Entry Name", |
| ) |
| with gr.Row(elem_classes=["med-add-row"]): |
| with gr.Group(elem_classes=["address-subfield"]): |
| food_edit_move_cat = gr.Dropdown( |
| choices=_CATEGORY_CHOICES, |
| value=None, |
| label="Move to Category", |
| ) |
| with gr.Group(elem_classes=["address-subfield"]): |
| food_edit_name = gr.Textbox( |
| label="Name *", |
| placeholder="e.g. Strawberries", |
| ) |
| with gr.Group(elem_classes=["address-subfield"]): |
| food_edit_notes = gr.Textbox( |
| label="Notes (optional)", |
| placeholder="e.g. causes rash", |
| ) |
| with gr.Column(visible=False) as edit_severity_col: |
| with gr.Group(elem_classes=["address-subfield"]): |
| food_edit_severity = gr.Dropdown( |
| choices=["mild", "moderate", "severe", "anaphylactic"], |
| value="mild", |
| label="Allergy Severity", |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| edit_food_btn = gr.Button( |
| "πΎ Save Changes", variant="primary", size="sm", |
| ) |
| food_edit_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
|
|
| def _food_add_category_ui(cat: str): |
| """Switch add form between allergy 2Γ2 grid and food 2-row layout.""" |
| internal = _to_internal(cat) |
| is_allergy = internal == "allergies" |
| if is_allergy: |
| return ( |
| gr.update(visible=True), |
| gr.update( |
| label="Allergen / Trigger *", |
| placeholder="e.g. Penicillin, Peanuts, Latex", |
| value="", |
| ), |
| gr.update( |
| label="Reaction Notes", |
| placeholder="e.g. Causes rash and hives", |
| value="", |
| ), |
| ) |
| name_meta = { |
| "liked_foods": ( |
| "Food Name *", |
| "e.g. Grilled salmon, oatmeal", |
| ), |
| "disliked_foods": ( |
| "Food Name *", |
| "e.g. Cilantro, mushrooms", |
| ), |
| } |
| name_label, name_ph = name_meta.get( |
| internal, |
| ("Food Name *", "e.g. Strawberries"), |
| ) |
| return ( |
| gr.update(visible=False), |
| gr.update( |
| label=name_label, |
| placeholder=name_ph, |
| value="", |
| ), |
| gr.update( |
| label="Notes (optional)", |
| placeholder="e.g. preferred at dinner", |
| value="", |
| ), |
| ) |
|
|
| food_category.change( |
| fn=_food_add_category_ui, |
| inputs=food_category, |
| outputs=[severity_col, food_name, food_notes], |
| ) |
|
|
| def _populate_delete_checkboxes(cat: str, pid): |
| if not cat: |
| return ( |
| gr.update(choices=[], value=[], visible=False), |
| gr.update(visible=False), |
| ) |
| internal = _to_internal(cat) |
| choices = _food_choices(internal, pid) |
| return ( |
| gr.update(choices=choices, value=[], visible=bool(choices)), |
| gr.update(visible=bool(choices)), |
| ) |
|
|
| food_del_cat.change( |
| _populate_delete_checkboxes, |
| inputs=[food_del_cat, active_profile_id], |
| outputs=[food_del_ids, delete_confirm_col], |
| ) |
|
|
| _food_panel_toggle_outputs = [ |
| food_show_panel, food_show_panel_open, |
| food_add_panel, food_add_panel_open, |
| food_edit_panel, food_edit_panel_open, |
| food_delete_panel, food_delete_panel_open, |
| food_show_card_view, food_add_card_view, |
| food_edit_card_view, food_delete_card_view, |
| food_show_display, food_delete_display, food_edit_display, |
| food_chip_show_btn, food_chip_add_btn, |
| food_chip_edit_btn, food_chip_delete_btn, |
| ] |
| _food_view_apply_outputs = [ |
| food_view_state, |
| allergies_view, preferred_view, aversions_view, |
| food_summary_md, food_show_display, |
| food_show_panel, food_show_panel_open, |
| food_show_card_view, food_chip_show_btn, |
| ] |
|
|
| food_chip_show_btn.click( |
| fn=lambda s, a, e, d, v, pid: _food_chip_click( |
| "show", s, a, e, d, v, pid, |
| ), |
| inputs=[ |
| food_show_panel_open, food_add_panel_open, |
| food_edit_panel_open, food_delete_panel_open, |
| food_view_state, active_profile_id, |
| ], |
| outputs=_food_panel_toggle_outputs, |
| ) |
| food_chip_add_btn.click( |
| fn=lambda s, a, e, d, v, pid: _food_chip_click( |
| "add", s, a, e, d, v, pid, |
| ), |
| inputs=[ |
| food_show_panel_open, food_add_panel_open, |
| food_edit_panel_open, food_delete_panel_open, |
| food_view_state, active_profile_id, |
| ], |
| outputs=_food_panel_toggle_outputs, |
| ) |
| food_chip_edit_btn.click( |
| fn=lambda s, a, e, d, v, pid: _food_chip_click( |
| "edit", s, a, e, d, v, pid, |
| ), |
| inputs=[ |
| food_show_panel_open, food_add_panel_open, |
| food_edit_panel_open, food_delete_panel_open, |
| food_view_state, active_profile_id, |
| ], |
| outputs=_food_panel_toggle_outputs, |
| ) |
| food_chip_delete_btn.click( |
| fn=lambda s, a, e, d, v, pid: _food_chip_click( |
| "delete", s, a, e, d, v, pid, |
| ), |
| inputs=[ |
| food_show_panel_open, food_add_panel_open, |
| food_edit_panel_open, food_delete_panel_open, |
| food_view_state, active_profile_id, |
| ], |
| outputs=_food_panel_toggle_outputs, |
| ) |
| food_show_cancel_btn.click( |
| fn=_food_cancel_panel, |
| outputs=[food_show_panel, food_show_panel_open, food_show_card_view, food_chip_show_btn], |
| ) |
| food_add_cancel_btn.click( |
| fn=_food_cancel_panel, |
| outputs=[food_add_panel, food_add_panel_open, food_add_card_view, food_chip_add_btn], |
| ) |
| food_edit_cancel_btn.click( |
| fn=_food_cancel_panel, |
| outputs=[food_edit_panel, food_edit_panel_open, food_edit_card_view, food_chip_edit_btn], |
| ) |
| food_delete_cancel_btn.click( |
| fn=_food_cancel_panel, |
| outputs=[food_delete_panel, food_delete_panel_open, food_delete_card_view, food_chip_delete_btn], |
| ) |
|
|
| for _btn, _view in [ |
| (food_show_allergies_btn, "allergies"), |
| (food_show_preferred_btn, "preferred"), |
| (food_show_aversions_btn, "aversions"), |
| ]: |
| _btn.click( |
| fn=lambda pid, v=_view: _food_apply_view(v, pid), |
| inputs=[active_profile_id], |
| outputs=_food_view_apply_outputs, |
| ) |
|
|
| add_food_btn.click( |
| add_food_handler, |
| inputs=[food_category, food_name, food_notes, food_severity, active_profile_id], |
| outputs=[gr.Textbox(visible=False), liked_table, disliked_table, allergy_table, food_del_ids, food_name, food_notes], |
| ) |
|
|
| def _toggle_move_severity(move_cat: str): |
| internal = _to_internal(move_cat) if move_cat else None |
| return gr.update(visible=(internal == "allergies")) |
|
|
| food_edit_cat.change( |
| _populate_edit_dropdown, |
| inputs=[food_edit_cat, active_profile_id], |
| outputs=[food_edit_select, food_edit_name, food_edit_notes, edit_severity_col, food_edit_severity, food_edit_move_cat], |
| ) |
| food_edit_select.change( |
| _populate_edit_fields, |
| inputs=[food_edit_cat, food_edit_select, active_profile_id], |
| outputs=[food_edit_name, food_edit_notes, edit_severity_col, food_edit_severity, food_edit_move_cat], |
| ) |
| food_edit_move_cat.change( |
| _toggle_move_severity, |
| inputs=food_edit_move_cat, |
| outputs=edit_severity_col, |
| ) |
| edit_food_btn.click( |
| edit_food_handler, |
| inputs=[food_edit_cat, food_edit_select, food_edit_name, food_edit_notes, food_edit_severity, food_del_cat, food_edit_move_cat, active_profile_id], |
| outputs=[ |
| gr.Textbox(visible=False), |
| liked_table, disliked_table, allergy_table, |
| food_edit_select, |
| food_edit_name, food_edit_notes, |
| edit_severity_col, food_edit_severity, |
| food_del_ids, food_edit_move_cat, |
| ], |
| ) |
| del_food_btn.click( |
| delete_food_handler, |
| inputs=[food_del_cat, food_del_ids, del_confirm_cb, active_profile_id], |
| outputs=[ |
| gr.Textbox(visible=False), |
| liked_table, disliked_table, allergy_table, |
| food_del_ids, food_edit_select, |
| delete_confirm_col, del_confirm_cb, |
| ], |
| ) |
| refresh_food_btn.click( |
| lambda pid: ( |
| _food_rows("liked_foods", pid), _food_rows("disliked_foods", pid), _food_rows("allergies", pid), |
| gr.update(choices=_food_choices(_to_internal(_CATEGORY_CHOICES[0]), pid), value=None), |
| "", "", |
| gr.update(visible=(_to_internal(_CATEGORY_CHOICES[0]) == "allergies")), |
| "mild", |
| None, |
| ), |
| inputs=[active_profile_id], |
| outputs=[liked_table, disliked_table, allergy_table, food_edit_select, food_edit_name, food_edit_notes, edit_severity_col, food_edit_severity, food_edit_move_cat], |
| ) |
|
|
| def _food_tab_refresh(profile_id): |
| return ( |
| "allergies", |
| _food_rows("liked_foods", profile_id), |
| _food_rows("disliked_foods", profile_id), |
| _food_rows("allergies", profile_id), |
| gr.update(choices=[], value=[], visible=False), |
| gr.update(choices=_CATEGORY_CHOICES, value=None), |
| gr.update(visible=True), |
| gr.update(choices=[], value=None), |
| "", "", |
| gr.update(visible=False), |
| "mild", |
| None, |
| gr.update(value=_food_list_summary_md("allergies", profile_id)), |
| gr.update(value=_food_show_display_md("allergies")), |
| gr.update(value=_food_delete_display_md(profile_id)), |
| gr.update(value=_food_edit_display_md()), |
| gr.update(visible=True), |
| gr.update(visible=False), False, |
| gr.update(visible=True), |
| gr.update(visible=False), False, |
| gr.update(visible=True), |
| gr.update(visible=False), False, |
| gr.update(visible=True), |
| gr.update(visible=False), False, |
| gr.update(value=_med_catalog_toggle_label(False)), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ) |
|
|
| food_tab.select( |
| fn=_food_tab_refresh, |
| inputs=[active_profile_id], |
| outputs=[ |
| food_view_state, |
| liked_table, disliked_table, allergy_table, |
| food_del_ids, food_del_cat, severity_col, |
| food_edit_select, food_edit_name, food_edit_notes, |
| edit_severity_col, food_edit_severity, food_edit_move_cat, |
| food_summary_md, food_show_display, food_delete_display, food_edit_display, |
| food_show_card_view, |
| food_show_panel, food_show_panel_open, |
| food_add_card_view, food_add_panel, food_add_panel_open, |
| food_edit_card_view, food_edit_panel, food_edit_panel_open, |
| food_delete_card_view, food_delete_panel, food_delete_panel_open, |
| food_chip_show_btn, food_chip_add_btn, food_chip_edit_btn, food_chip_delete_btn, |
| allergies_view, preferred_view, aversions_view, |
| ], |
| ) |
|
|
| |
| with gr.Tab( |
| "π
Appointments", |
| elem_id="appointments-tab", |
| ) as appt_tab: |
| with gr.Group(visible=True, elem_classes=["locked-gateway-wrap"]) as appt_locked_view: |
| gr.HTML(_locked_gateway_md("π
APPOINTMENTS CONTROL")) |
| appt_go_settings_btn = gr.Button( |
| "βοΈ Go to Settings & Profiles", variant="primary", |
| ) |
| with gr.Group(visible=False) as appt_active_view: |
| gr.Markdown( |
| "Track **upcoming and past appointments** for the signed-in care profile." |
| ) |
|
|
| appt_selected_id = gr.State("") |
| appt_include_past = gr.State(False) |
| appt_show_panel_open = gr.State(False) |
| appt_add_panel_open = gr.State(False) |
| appt_delete_panel_open = gr.State(False) |
|
|
| with gr.Group( |
| elem_classes=["profile-create-shell", "med-manager-shell"], |
| ): |
| gr.Markdown( |
| "##### π
Appointments", |
| elem_classes=["med-manager-shell-title"], |
| ) |
| today_md = gr.Markdown( |
| _today_summary(), |
| elem_classes=["appt-today-line"], |
| ) |
| appt_summary_md = gr.Markdown( |
| value=_appt_list_summary_md(False), |
| elem_classes=["med-summary-line"], |
| ) |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-table-card"], |
| ): |
| appt_table = gr.Dataframe( |
| value=_appt_table_rows(False), |
| headers=[ |
| "Title", "Date", "Time", "Status", "Recurrence", |
| "Location", "Notes", "ID", |
| ], |
| datatype=[ |
| "str", "str", "str", "str", "str", |
| "str", "str", "str", |
| ], |
| interactive=False, |
| wrap=True, |
| label="Appointments (click a row to edit)", |
| ) |
| with gr.Row(elem_classes=["med-action-row"]): |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-catalog-card"], |
| ): |
| with gr.Row(elem_classes=["med-catalog-header"]): |
| gr.Markdown( |
| "π **Show**", |
| elem_classes=["field-card-title"], |
| ) |
| appt_chip_show_btn = gr.Button( |
| "βοΈ Open", |
| variant="link", |
| size="sm", |
| elem_classes=[ |
| "field-edit-link", "med-catalog-toggle", |
| ], |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["field-card-view"], |
| ) as appt_show_card_view: |
| appt_show_display = gr.Markdown( |
| _appt_show_display_md(False), |
| elem_classes=["field-card-value"], |
| ) |
| with gr.Group( |
| visible=False, |
| elem_classes=["field-card-edit", "appt-show-panel"], |
| ) as appt_show_panel: |
| with gr.Column( |
| elem_classes=["address-edit-grid", "appt-show-options"], |
| ): |
| appt_show_upcoming_btn = gr.Button( |
| "Upcoming appointments only", |
| size="sm", |
| variant="secondary", |
| ) |
| appt_show_past_btn = gr.Button( |
| "Include past appointments (30 days)", |
| size="sm", |
| variant="secondary", |
| ) |
| refresh_appt_btn = gr.Button( |
| "π Refresh list", |
| size="sm", |
| variant="secondary", |
| ) |
| gr.Markdown( |
| "**Recently completed**", |
| elem_classes=["appt-completed-label"], |
| ) |
| appt_completed_table = gr.Dataframe( |
| value=_appt_completed_rows(), |
| headers=[ |
| "Title", "Date", "Time", |
| "Completed At", "Location", |
| ], |
| datatype=["str", "str", "str", "str", "str"], |
| interactive=False, |
| wrap=True, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| appt_show_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-catalog-card"], |
| ): |
| with gr.Row(elem_classes=["med-catalog-header"]): |
| gr.Markdown( |
| "β **Add**", |
| elem_classes=["field-card-title"], |
| ) |
| appt_chip_add_btn = gr.Button( |
| "βοΈ Open", |
| variant="link", |
| size="sm", |
| elem_classes=[ |
| "field-edit-link", "med-catalog-toggle", |
| ], |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["field-card-view"], |
| ) as appt_add_card_view: |
| appt_add_display = gr.Markdown( |
| _appt_add_display_md(), |
| elem_classes=["field-card-value"], |
| ) |
| with gr.Column( |
| elem_classes=["profile-field-card", "med-catalog-card"], |
| ): |
| with gr.Row(elem_classes=["med-catalog-header"]): |
| gr.Markdown( |
| "ποΈ **Remove**", |
| elem_classes=["field-card-title"], |
| ) |
| appt_chip_delete_btn = gr.Button( |
| "βοΈ Open", |
| variant="link", |
| size="sm", |
| elem_classes=[ |
| "field-edit-link", "med-catalog-toggle", |
| ], |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["field-card-view"], |
| ) as appt_delete_card_view: |
| appt_delete_display = gr.Markdown( |
| _appt_delete_display_md(), |
| elem_classes=["field-card-value"], |
| ) |
| with gr.Group( |
| visible=False, |
| elem_classes=["field-card-edit", "appt-delete-form"], |
| ) as appt_delete_panel: |
| with gr.Column(elem_classes=["address-edit-grid"]): |
| with gr.Group(elem_classes=["address-subfield"]): |
| appt_del_id = gr.Dropdown( |
| choices=_appt_choices(), |
| value=None, |
| label="Select appointment to delete", |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| appt_del_btn = gr.Button( |
| "ποΈ Delete", variant="stop", size="sm", |
| ) |
| appt_delete_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Group( |
| visible=False, |
| elem_classes=["med-add-workspace"], |
| ) as appt_add_panel: |
| with gr.Group( |
| elem_classes=["profile-field-card", "appt-form-matrix-shell"], |
| ): |
| gr.Markdown( |
| "β **Add / Edit Appointment**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Column(elem_classes=["med-add-matrix", "appt-form-matrix"]): |
| with gr.Row(elem_classes=["appt-load-row"]): |
| appt_edit_select = gr.Dropdown( |
| choices=_appt_choices(), |
| label="Select to edit (or click a row above)", |
| scale=1, |
| ) |
| with gr.Row(elem_classes=["med-add-row"]): |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield"]): |
| appt_title = gr.Textbox( |
| label="Title *", |
| placeholder="e.g. Cardiology follow-up", |
| ) |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield", "appt-date-cell"]): |
| gr.Markdown( |
| "**Date**", |
| elem_classes=["field-subfield-label"], |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["field-card-view", "dob-card-view"], |
| ) as appt_date_card_view: |
| with gr.Row(elem_classes=["field-card-body", "dob-card-body"]): |
| appt_date_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value", "dob-card-date"], |
| ) |
| appt_date_edit_btn = gr.Button( |
| "Edit Date", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link", "dob-edit-link"], |
| ) |
| with gr.Group( |
| visible=False, |
| elem_classes=["field-card-edit", "dob-card-edit"], |
| ) as appt_date_edit_view: |
| with gr.Row(elem_classes=["field-card-edit-row", "dob-split-row", "split-picker-row"]): |
| appt_date_month = gr.Dropdown( |
| choices=_DOB_MONTH_ABBRS, |
| value=None, |
| label="Month", |
| show_label=True, |
| allow_custom_value=False, |
| ) |
| appt_date_day = gr.Dropdown( |
| choices=_DOB_DAY_CHOICES, |
| value=None, |
| label="Day", |
| show_label=True, |
| allow_custom_value=False, |
| ) |
| appt_date_year = gr.Textbox( |
| value="", |
| label="Year", |
| show_label=True, |
| placeholder="2026 (Type Year)", |
| max_length=4, |
| elem_id="appt-date-year", |
| ) |
| with gr.Row(elem_classes=["field-card-actions", "dob-split-actions"]): |
| appt_date_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| appt_date_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| appt_add_date = gr.Textbox(value="", visible=False, label="Date") |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield"]): |
| gr.Markdown( |
| "Time", |
| elem_classes=["field-subfield-label"], |
| ) |
| with gr.Row(elem_classes=["appt-time-parts-row", "split-picker-row"]): |
| appt_hour = gr.Dropdown( |
| [str(h) for h in range(1, 13)], |
| value="12", |
| label="Hour", |
| show_label=True, |
| ) |
| appt_minute = gr.Dropdown( |
| [f"{m:02d}" for m in range(0, 60)], |
| value="00", |
| label="Minute", |
| show_label=True, |
| ) |
| appt_ampm = gr.Radio( |
| ["AM", "PM"], |
| value="PM", |
| label="AM/PM", |
| show_label=True, |
| ) |
| with gr.Row(elem_classes=["med-add-row"]): |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield"]): |
| appt_location = gr.Textbox( |
| label="Location", |
| placeholder="e.g. General Clinic", |
| ) |
| with gr.Column(min_width=0): |
| with gr.Group(elem_classes=["address-subfield"]): |
| appt_reminders = gr.CheckboxGroup( |
| choices=[ |
| ("Day of", 0), |
| ("1 day before", 1), |
| ("3 days before", 3), |
| ("1 week before", 7), |
| ("2 weeks before", 14), |
| ], |
| label="Reminders", |
| ) |
| with gr.Group( |
| elem_classes=["address-subfield", "med-add-notes-block"], |
| ): |
| appt_notes = gr.Textbox( |
| label="Notes", |
| placeholder=( |
| 'e.g. "Fasting required for labs"' |
| ), |
| lines=3, |
| ) |
| with gr.Group( |
| elem_classes=["address-subfield", "appt-recurrence-block"], |
| ): |
| appt_rec_freq = gr.Radio( |
| ["None", "Daily", "Weekly", "Monthly", "Yearly"], |
| value="None", |
| label="Recurrence", |
| ) |
| with gr.Row(visible=False) as appt_rec_interval_row: |
| appt_rec_interval = gr.Number( |
| value=1, |
| minimum=1, |
| label="Repeat every", |
| precision=0, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| appt_save_btn = gr.Button( |
| "β
Save Appointment", |
| variant="primary", |
| size="sm", |
| interactive=False, |
| ) |
| appt_clear_form_btn = gr.Button( |
| "Clear Form", |
| size="sm", |
| ) |
| appt_add_cancel_btn = gr.Button( |
| "Cancel", |
| size="sm", |
| ) |
|
|
| appt_title.change( |
| fn=lambda t: gr.update(interactive=bool(t and t.strip())), |
| inputs=[appt_title], |
| outputs=[appt_save_btn], |
| ) |
|
|
| _appt_date_display_outputs = [ |
| appt_date_display, appt_date_month, appt_date_day, appt_date_year, |
| appt_date_card_view, appt_date_edit_view, |
| ] |
| _appt_date_edit_outputs = [ |
| appt_date_card_view, appt_date_edit_view, |
| appt_date_month, appt_date_day, appt_date_year, |
| ] |
| _appt_date_save_outputs = [ |
| appt_add_date, appt_date_display, appt_date_card_view, appt_date_edit_view, |
| ] |
| _appt_panel_toggle_outputs = [ |
| appt_show_panel, appt_show_panel_open, |
| appt_add_panel, appt_add_panel_open, |
| appt_delete_panel, appt_delete_panel_open, |
| appt_show_card_view, appt_add_card_view, appt_delete_card_view, |
| appt_show_display, appt_delete_display, |
| appt_chip_show_btn, appt_chip_add_btn, appt_chip_delete_btn, |
| ] |
| _appt_show_filter_outputs = [ |
| appt_include_past, |
| appt_table, appt_edit_select, appt_del_id, |
| appt_completed_table, today_md, |
| appt_summary_md, |
| appt_show_panel, appt_show_panel_open, |
| appt_show_card_view, |
| appt_show_display, appt_delete_display, |
| appt_chip_show_btn, |
| ] |
|
|
| appt_date_edit_btn.click( |
| fn=_appt_date_begin_edit, |
| inputs=[appt_add_date], |
| outputs=_appt_date_edit_outputs, |
| ) |
| appt_date_save_btn.click( |
| fn=_appt_date_save_edit, |
| inputs=[appt_date_month, appt_date_day, appt_date_year, appt_add_date], |
| outputs=_appt_date_save_outputs, |
| ) |
| appt_date_cancel_btn.click( |
| fn=_appt_date_cancel_edit, |
| inputs=[appt_add_date], |
| outputs=_appt_date_display_outputs, |
| ) |
|
|
| appt_rec_freq.change( |
| fn=lambda f: { |
| "None": gr.update(visible=False), |
| "Daily": gr.update(visible=True, label="Repeat every X Day(s)"), |
| "Weekly": gr.update(visible=True, label="Repeat every X Week(s)"), |
| "Monthly": gr.update(visible=True, label="Repeat every X Month(s)"), |
| "Yearly": gr.update(visible=True, label="Repeat every X Year(s)"), |
| }.get(f, gr.update(visible=False)), |
| inputs=[appt_rec_freq], |
| outputs=[appt_rec_interval_row], |
| ) |
|
|
| appt_chip_show_btn.click( |
| fn=_appt_chip_show_click, |
| inputs=[ |
| appt_show_panel_open, appt_add_panel_open, |
| appt_delete_panel_open, appt_include_past, active_profile_id, |
| ], |
| outputs=_appt_panel_toggle_outputs, |
| ) |
| appt_chip_add_btn.click( |
| fn=_appt_chip_add_click, |
| inputs=[ |
| appt_show_panel_open, appt_add_panel_open, |
| appt_delete_panel_open, appt_include_past, active_profile_id, |
| ], |
| outputs=_appt_panel_toggle_outputs, |
| ) |
| appt_chip_delete_btn.click( |
| fn=_appt_chip_delete_click, |
| inputs=[ |
| appt_show_panel_open, appt_add_panel_open, |
| appt_delete_panel_open, appt_include_past, active_profile_id, |
| ], |
| outputs=_appt_panel_toggle_outputs, |
| ) |
| appt_show_cancel_btn.click( |
| fn=_appt_cancel_show, |
| outputs=[appt_show_panel, appt_show_panel_open, appt_show_card_view, appt_chip_show_btn], |
| ) |
| appt_add_cancel_btn.click( |
| fn=_appt_cancel_add, |
| outputs=[appt_add_panel, appt_add_panel_open, appt_add_card_view, appt_chip_add_btn], |
| ) |
| appt_delete_cancel_btn.click( |
| fn=_appt_cancel_delete, |
| outputs=[appt_delete_panel, appt_delete_panel_open, appt_delete_card_view, appt_chip_delete_btn], |
| ) |
|
|
| for _show_btn, _past_val in [ |
| (appt_show_upcoming_btn, False), |
| (appt_show_past_btn, True), |
| ]: |
| _show_btn.click( |
| fn=lambda pid, v=_past_val: _appt_apply_show_filter(v, pid), |
| inputs=[active_profile_id], |
| outputs=_appt_show_filter_outputs, |
| ) |
|
|
| def _appt_refresh_click(profile_id, include_past): |
| apt.run_lifecycle_check(profile_id) |
| rows, choices_up, completed, today = _appt_table_refresh( |
| include_past, profile_id, |
| ) |
| return rows, completed, today, choices_up, choices_up |
|
|
| refresh_appt_btn.click( |
| fn=_appt_refresh_click, |
| inputs=[active_profile_id, appt_include_past], |
| outputs=[appt_table, appt_completed_table, today_md, appt_edit_select, appt_del_id], |
| ) |
|
|
| appt_table.select( |
| fn=_appt_select_handler, |
| inputs=[appt_include_past, active_profile_id], |
| outputs=[ |
| appt_selected_id, |
| appt_title, appt_add_date, |
| appt_hour, appt_minute, appt_ampm, |
| appt_location, appt_notes, appt_reminders, |
| appt_rec_freq, appt_rec_interval, |
| appt_rec_interval_row, |
| appt_edit_select, appt_del_id, |
| ], |
| ).then( |
| fn=_sync_appt_date_display, |
| inputs=[appt_add_date], |
| outputs=_appt_date_display_outputs, |
| ).then( |
| fn=_appt_open_add_panel, |
| outputs=_appt_panel_toggle_outputs, |
| ) |
|
|
| def _edit_dropdown_handler(selected_appt_id): |
| form_values = _populate_appt_edit_fields(selected_appt_id) |
| return (selected_appt_id, *form_values) |
|
|
| appt_edit_select.change( |
| fn=_edit_dropdown_handler, |
| inputs=[appt_edit_select], |
| outputs=[ |
| appt_selected_id, |
| appt_title, appt_add_date, |
| appt_hour, appt_minute, appt_ampm, |
| appt_location, appt_notes, appt_reminders, |
| appt_rec_freq, appt_rec_interval, |
| appt_rec_interval_row, |
| ], |
| ).then( |
| fn=_sync_appt_date_display, |
| inputs=[appt_add_date], |
| outputs=_appt_date_display_outputs, |
| ) |
|
|
| _appt_save_outputs = [ |
| gr.Textbox(visible=False), |
| appt_table, appt_completed_table, today_md, |
| appt_edit_select, appt_del_id, |
| appt_selected_id, |
| appt_title, appt_add_date, |
| appt_hour, appt_minute, appt_ampm, |
| appt_location, appt_notes, appt_reminders, |
| appt_rec_freq, appt_rec_interval, appt_rec_interval_row, |
| appt_add_panel, appt_add_panel_open, appt_add_card_view, |
| appt_chip_add_btn, appt_summary_md, appt_delete_display, |
| ] |
|
|
| appt_save_btn.click( |
| fn=appt_save_with_ui, |
| inputs=[ |
| appt_selected_id, |
| appt_title, appt_add_date, |
| appt_hour, appt_minute, appt_ampm, |
| appt_location, appt_notes, appt_reminders, |
| appt_rec_freq, appt_rec_interval, |
| active_profile_id, appt_include_past, |
| ], |
| outputs=_appt_save_outputs, |
| ).then( |
| fn=_sync_appt_date_display, |
| inputs=[appt_add_date], |
| outputs=_appt_date_display_outputs, |
| ) |
|
|
| appt_clear_form_btn.click( |
| fn=_appt_clear_form_handler, |
| inputs=[active_profile_id], |
| outputs=[ |
| appt_selected_id, |
| appt_title, appt_add_date, |
| appt_hour, appt_minute, appt_ampm, |
| appt_location, appt_notes, appt_reminders, |
| appt_rec_freq, appt_rec_interval, appt_rec_interval_row, |
| appt_edit_select, appt_del_id, |
| ], |
| ).then( |
| fn=_sync_appt_date_display, |
| inputs=[appt_add_date], |
| outputs=_appt_date_display_outputs, |
| ) |
|
|
| appt_del_btn.click( |
| fn=delete_appt_with_ui, |
| inputs=[appt_del_id, active_profile_id, appt_include_past], |
| outputs=[ |
| gr.Textbox(visible=False), |
| appt_table, appt_completed_table, today_md, |
| appt_edit_select, appt_del_id, |
| appt_delete_panel, appt_delete_panel_open, |
| appt_delete_card_view, appt_chip_delete_btn, |
| appt_summary_md, appt_delete_display, |
| ], |
| ) |
|
|
| def _appt_tab_refresh(profile_id): |
| rows, choices_up, completed, today = _appt_table_refresh( |
| False, profile_id, |
| ) |
| return ( |
| False, |
| rows, choices_up, choices_up, completed, today, |
| gr.update(value=_appt_list_summary_md(False, profile_id)), |
| gr.update(value=_appt_show_display_md(False)), |
| gr.update(value=_appt_delete_display_md(profile_id)), |
| gr.update(visible=True), |
| gr.update(visible=False), False, |
| gr.update(visible=True), |
| gr.update(visible=False), False, |
| gr.update(visible=True), |
| gr.update(visible=False), False, |
| gr.update(value=_med_catalog_toggle_label(False)), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| gr.update(value=_med_catalog_toggle_label(False)), |
| ) |
|
|
| def _appt_tab_activate(profile_id): |
| apt.run_lifecycle_check(profile_id) |
| return _appt_tab_refresh(profile_id) |
|
|
| appt_tab.select( |
| fn=_appt_tab_activate, |
| inputs=[active_profile_id], |
| outputs=[ |
| appt_include_past, |
| appt_table, appt_edit_select, appt_del_id, |
| appt_completed_table, today_md, |
| appt_summary_md, |
| appt_show_display, appt_delete_display, |
| appt_show_card_view, |
| appt_show_panel, appt_show_panel_open, |
| appt_add_card_view, |
| appt_add_panel, appt_add_panel_open, |
| appt_delete_card_view, |
| appt_delete_panel, appt_delete_panel_open, |
| appt_chip_show_btn, appt_chip_add_btn, appt_chip_delete_btn, |
| ], |
| ) |
|
|
| |
| with gr.Tab( |
| "π Doctors Visit", |
| elem_id="doctor-visit-tab", |
| ) as visit_tab: |
| with gr.Group(visible=True, elem_classes=["locked-gateway-wrap"]) as visit_locked_view: |
| gr.HTML(_locked_gateway_md("π DOCTOR VISIT CONTROL")) |
| visit_go_settings_btn = gr.Button( |
| "βοΈ Go to Settings & Profiles", variant="primary", |
| ) |
| with gr.Group(visible=False) as visit_active_view: |
| gr.Markdown( |
| "Build a printable **appointment brief** β tap a section below " |
| "to fill it in, then **Generate**." |
| ) |
|
|
| _visit_clinic_bootstrap = _format_profile_clinic_label(_bootstrap_session_id) |
| _visit_clinic_default = ( |
| _visit_clinic_bootstrap |
| if _visit_clinic_bootstrap |
| else _VISIT_CLINIC_UNSET_LABEL |
| ) |
| visit_profile_select = gr.Dropdown( |
| choices=profiles.get_profile_choices(), |
| value=_bootstrap_session_id or None, |
| visible=False, |
| ) |
| visit_destination_snapshot = gr.State({ |
| "select": _visit_clinic_default, |
| "custom": "", |
| }) |
|
|
| visit_dest_panel_open = gr.State(False) |
| visit_purpose_panel_open = gr.State(False) |
| visit_symptoms_panel_open = gr.State(False) |
| visit_questions_panel_open = gr.State(False) |
|
|
| visit_destination_display = gr.Markdown( |
| value=_visit_destination_summary_text( |
| _bootstrap_session_id, _visit_clinic_default, "", |
| ), |
| visible=False, |
| ) |
|
|
| with gr.Group( |
| elem_classes=["profile-create-shell", "visit-brief-shell"], |
| ): |
| gr.Markdown( |
| "##### π AIDAI-LINE APPOINTMENT BRIEF", |
| elem_classes=["visit-brief-shell-title"], |
| ) |
| visit_brief_header_md = gr.Markdown( |
| value=_visit_brief_header_md( |
| _bootstrap_session_id, _visit_clinic_default, "", |
| ), |
| elem_classes=["visit-brief-header"], |
| ) |
| with gr.Row( |
| equal_height=True, |
| elem_classes=["visit-chip-grid-row"], |
| ): |
| with gr.Column(elem_classes=["visit-chip-column", "visit-chip-logistics"]): |
| gr.Markdown( |
| "The Logistics", |
| elem_classes=["visit-chip-col-label"], |
| ) |
| visit_chip_destination_btn = gr.Button( |
| "π©Ί Choose Destination", |
| variant="secondary", |
| size="sm", |
| elem_classes=["visit-chip-btn"], |
| ) |
| visit_chip_purpose_btn = gr.Button( |
| "π― Visit Purpose", |
| variant="secondary", |
| size="sm", |
| elem_classes=["visit-chip-btn"], |
| ) |
| with gr.Column(elem_classes=["visit-chip-column", "visit-chip-medical"]): |
| gr.Markdown( |
| "The Medical", |
| elem_classes=["visit-chip-col-label"], |
| ) |
| visit_chip_symptoms_btn = gr.Button( |
| "π©Ί Select Symptoms", |
| variant="secondary", |
| size="sm", |
| elem_classes=["visit-chip-btn"], |
| ) |
| visit_chip_questions_btn = gr.Button( |
| "β Add Questions", |
| variant="secondary", |
| size="sm", |
| elem_classes=["visit-chip-btn"], |
| ) |
|
|
| with gr.Column(elem_classes=["visit-expansion-stack"]): |
| with gr.Group( |
| visible=False, |
| elem_classes=["profile-field-card", "visit-expansion-panel"], |
| ) as visit_panel_destination: |
| gr.Markdown( |
| "π©Ί **Destination Clinic & Doctor**", |
| elem_classes=["field-card-title"], |
| ) |
| visit_clinic_select = gr.Dropdown( |
| choices=_visit_clinic_dropdown_choices(_bootstrap_session_id), |
| value=_visit_clinic_default, |
| label="Saved doctor from profile", |
| info="Defaults to primary care on the active profile.", |
| ) |
| visit_clinic_custom = gr.Textbox( |
| label="Custom Clinic Name", |
| placeholder="e.g. Dr. Smith at City Cardiology", |
| lines=1, |
| visible=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| visit_destination_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| visit_destination_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
|
|
| with gr.Group( |
| visible=False, |
| elem_classes=["profile-field-card", "visit-expansion-panel"], |
| ) as visit_panel_purpose: |
| gr.Markdown( |
| "π― **Visit Purpose & Goals**", |
| elem_classes=["field-card-title"], |
| ) |
| visit_reason_summary = gr.Markdown(visible=False) |
| visit_concerns_summary = gr.Markdown(visible=False) |
| with gr.Row( |
| equal_height=True, |
| elem_classes=["visit-clinic-grid", "visit-purpose-grid"], |
| ): |
| with gr.Column(elem_classes=["visit-purpose-reason-col"]): |
| gr.Markdown( |
| "**Reason for Visit**", |
| elem_classes=["field-card-title"], |
| ) |
| visit_reason = gr.Dropdown( |
| choices=[ |
| "Routine Checkup", |
| "Follow-up", |
| "New Symptom Exploration", |
| "Urgent Care", |
| "Specialist Consultation", |
| "Other", |
| ], |
| value=None, |
| label="Select a reason", |
| ) |
| visit_reason_other = gr.Textbox( |
| label="Specify Custom Reason", |
| placeholder=( |
| "e.g. Insurance physical, post-op follow-upβ¦" |
| ), |
| lines=2, |
| visible=False, |
| ) |
| with gr.Column(elem_classes=["visit-purpose-concerns-col"]): |
| gr.Markdown( |
| "**Primary Concerns / Goals**", |
| elem_classes=["field-card-title"], |
| ) |
| visit_concerns_state = gr.State([]) |
| with gr.Row( |
| equal_height=True, |
| elem_classes=[ |
| "visit-concerns-inner", |
| "visit-purpose-concerns-inner", |
| ], |
| ): |
| with gr.Column(): |
| gr.Markdown( |
| "*Quick add:*", |
| elem_classes=[ |
| "field-card-title", "visit-quick-add-label", |
| ], |
| ) |
| with gr.Row(elem_classes=["visit-concern-tokens"]): |
| visit_concern_token_btns = [ |
| gr.Button(_token, variant="secondary", size="sm") |
| for _token in _VISIT_CONCERN_QUICK_TOKENS |
| ] |
| visit_concern_input = gr.Textbox( |
| placeholder=( |
| "e.g. Review BP medication side effects" |
| ), |
| lines=2, |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["visit-concern-actions"]): |
| visit_add_concern_btn = gr.Button( |
| "β Add", variant="secondary", size="sm", |
| ) |
| visit_clear_concerns_btn = gr.Button( |
| "Clear All", variant="secondary", size="sm", |
| ) |
| with gr.Column(): |
| gr.Markdown( |
| "**Added for this visit**", |
| elem_classes=["field-card-title"], |
| ) |
| visit_concerns_list_md = gr.Markdown( |
| value=_render_concerns_md([]), |
| elem_classes=["visit-concerns-list"], |
| ) |
|
|
| visit_symptoms_state = gr.State([]) |
| with gr.Group( |
| visible=False, |
| elem_classes=["profile-field-card", "visit-expansion-panel"], |
| ) as visit_panel_symptoms: |
| gr.Markdown( |
| "π©Ί **Active Symptoms Tracker**", |
| elem_classes=["field-card-title"], |
| ) |
| gr.Markdown( |
| "*Your tracked symptoms (from profile β select to add " |
| "to this visit's report):*", |
| elem_classes=["visit-quick-add-label"], |
| ) |
| visit_profile_symptom_pick = gr.CheckboxGroup( |
| choices=profiles.get_tracked_symptoms(_bootstrap_session_id), |
| label="Profile symptoms", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["visit-pain-row"]): |
| visit_pain_level = gr.Slider( |
| minimum=1, |
| maximum=10, |
| step=1, |
| value=5, |
| label="Selected symptom pain level", |
| info="Muted 1 β Severe 10", |
| ) |
| visit_symptom_custom_input = gr.Textbox( |
| label="Or add a one-off symptom", |
| placeholder="e.g. Sudden dizziness this week", |
| lines=1, |
| ) |
| visit_symptom_notes = gr.Textbox( |
| label="Notes (duration, triggers, context)", |
| placeholder="Optional notes for the selected symptom(s)", |
| lines=2, |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["visit-inline-actions"]): |
| visit_add_symptoms_report_btn = gr.Button( |
| "β Add to visit report", |
| variant="secondary", |
| size="sm", |
| ) |
| visit_clear_symptoms_btn = gr.Button( |
| "Clear All", variant="secondary", size="sm", |
| ) |
| gr.Markdown( |
| "**On this visit's brief**", |
| elem_classes=["field-card-title"], |
| ) |
| visit_symptoms_list_md = gr.Markdown( |
| value=_render_symptoms_list_md([]), |
| elem_classes=["visit-concerns-list"], |
| ) |
| visit_symptom_view_select = gr.Dropdown( |
| choices=[], |
| value=None, |
| label="View full entry", |
| ) |
| visit_symptom_view_md = gr.Markdown( |
| value=_SYMPTOM_VIEW_PLACEHOLDER, |
| elem_classes=["visit-symptom-detail"], |
| ) |
|
|
| with gr.Group( |
| visible=False, |
| elem_classes=["profile-field-card", "visit-expansion-panel"], |
| ) as visit_panel_questions: |
| gr.Markdown( |
| "β **Questions for the Doctor**", |
| elem_classes=["field-card-title"], |
| ) |
| visit_questions_state = gr.State([]) |
| visit_editing_state = gr.State(None) |
| visit_mode_md = gr.Markdown(value=_render_mode_md(None)) |
| visit_question_input = gr.Textbox( |
| placeholder="Type a question for the doctorβ¦", |
| lines=2, |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["visit-inline-actions"]): |
| visit_add_question_btn = gr.Button( |
| "β Add Question", variant="secondary", size="sm", |
| ) |
| visit_cancel_edit_btn = gr.Button( |
| "Cancel Edit", variant="secondary", size="sm", |
| visible=False, |
| ) |
| visit_clear_questions_btn = gr.Button( |
| "Clear All", variant="secondary", size="sm", |
| ) |
| visit_questions_list_md = gr.Markdown( |
| value=_render_questions_md([]), |
| elem_classes=["visit-questions-list"], |
| ) |
| with gr.Accordion("Manage questions", open=False): |
| gr.Markdown("**Delete a question**") |
| visit_delete_select = gr.Dropdown( |
| choices=[], |
| label="Select question to delete", |
| ) |
| visit_delete_question_btn = gr.Button( |
| "ποΈ Delete Selected", variant="stop", size="sm", |
| ) |
| gr.Markdown("**Edit a question**") |
| visit_edit_select = gr.Dropdown( |
| choices=[], |
| label="Select question to edit", |
| ) |
| visit_edit_btn = gr.Button( |
| "βοΈ Edit Selected", variant="primary", size="sm", |
| ) |
|
|
| |
| visit_compiler_panel_open = gr.State(False) |
| with gr.Group(elem_classes=["visit-brief-settings-wrap"]): |
| visit_compiler_toggle_btn = gr.Button( |
| "βοΈ Pre-Visit Brief Settings", |
| variant="secondary", |
| size="sm", |
| elem_classes=["visit-brief-settings-toggle"], |
| ) |
| with gr.Group( |
| visible=False, elem_classes=["profile-optional-panel"], |
| ) as visit_compiler_panel: |
| with gr.Column(elem_classes=["profile-field-card", "visit-form-card"]): |
| gr.Markdown( |
| "#### βοΈ Pre-Visit Brief Settings", |
| elem_classes=["field-card-title"], |
| ) |
| gr.Markdown( |
| "*Choose what to auto-include when generating the brief:*", |
| elem_classes=["visit-quick-add-label"], |
| ) |
| with gr.Row(): |
| visit_inc_meds = gr.Checkbox( |
| label="Auto-include Current Medications", value=True, |
| ) |
| visit_inc_allergies = gr.Checkbox( |
| label="Auto-include Allergies & Food Aversions", value=True, |
| ) |
| visit_inc_history = gr.Checkbox( |
| label="Auto-include Recent Vitals / History", value=True, |
| ) |
| visit_inc_insurance = gr.Checkbox( |
| label="Include Insurance Policy at Top of Brief (fast check-in)", |
| value=True, |
| ) |
|
|
| |
| with gr.Row(): |
| generate_visit_btn = gr.Button( |
| "π Generate Pre-Visit Brief", variant="primary" |
| ) |
| clear_visit_btn = gr.Button("π Clear Form", variant="secondary") |
| gr.HTML(""" |
| <button onclick="window.print()" style=" |
| background: linear-gradient(135deg,#145f70,#1e7a8f); |
| color:#fff; border:none; border-radius:6px; |
| padding:10px 22px; font-size:14px; font-weight:600; |
| cursor:pointer; font-family:Inter,sans-serif; |
| box-shadow:0 2px 8px rgba(30,122,143,0.3); |
| ">π¨οΈ Print Brief</button> |
| """) |
|
|
| |
| gr.Markdown("---") |
| with gr.Column(elem_classes=["profile-field-card", "visit-form-card", "visit-brief-output"]): |
| gr.Markdown( |
| "### π Patient Pre-Visit Intake Brief", |
| elem_classes=["field-card-title"], |
| ) |
| visit_output = gr.Markdown( |
| value=( |
| "*Click **π Generate Pre-Visit Brief** above to compile your " |
| "intake summary here. Then click π¨οΈ Print Brief to take it to " |
| "the clinic.*" |
| ), |
| elem_classes="visit-brief", |
| ) |
|
|
| |
| gr.Markdown("---") |
| with gr.Column( |
| elem_classes=["profile-field-card", "visit-form-card", "visit-export-panel"], |
| ): |
| gr.Markdown( |
| "### π€ Export & Share Caregiver Summary", |
| elem_classes=["field-card-title"], |
| ) |
| gr.Markdown( |
| "*Generate a shareable summary from the active care profile β " |
| "medications, appointments, and food notes.*", |
| elem_classes=["visit-quick-add-label"], |
| ) |
| with gr.Row(): |
| inc_meds = gr.Checkbox(label="Include Medications", value=True) |
| inc_appts = gr.Checkbox(label="Include Appointments", value=True) |
| inc_food = gr.Checkbox(label="Include Food Chart", value=True) |
|
|
| custom_note_inp = gr.Textbox( |
| label="Additional Notes (optional)", |
| placeholder="Add any extra context for the recipientβ¦", |
| lines=3, |
| ) |
| with gr.Row(): |
| gen_export_btn = gr.Button( |
| "π Generate Export", variant="primary", |
| ) |
| clear_export_btn = gr.Button( |
| "ποΈ Clear Export", variant="secondary", |
| ) |
| gr.HTML(""" |
| <button onclick="window.print()" style=" |
| background: linear-gradient(135deg,#145f70,#1e7a8f); |
| color:#fff; border:none; border-radius:6px; |
| padding:10px 22px; font-size:14px; font-weight:600; |
| cursor:pointer; font-family:Inter,sans-serif; |
| box-shadow:0 2px 8px rgba(30,122,143,0.3); |
| ">π¨οΈ Print / Share</button> |
| """) |
|
|
| export_output = gr.Textbox( |
| label="Export Summary", |
| lines=16, |
| interactive=False, |
| ) |
|
|
| with gr.Accordion("π§ Email Export", open=False): |
| email_to_inp = gr.Textbox( |
| label="Send To (email address, optional)", |
| placeholder="doctor@clinic.com", |
| ) |
| email_link_output = gr.HTML() |
| email_btn = gr.Button( |
| "π§ Generate Email Link", variant="secondary", |
| ) |
|
|
| gen_export_btn.click( |
| generate_export, |
| inputs=[inc_meds, inc_appts, inc_food, custom_note_inp, active_profile_id], |
| outputs=export_output, |
| ) |
| clear_export_btn.click( |
| lambda: "", |
| inputs=None, |
| outputs=export_output, |
| ) |
| email_btn.click( |
| build_mailto, |
| inputs=[export_output, email_to_inp], |
| outputs=email_link_output, |
| ) |
|
|
| |
| visit_form_inputs = [ |
| visit_profile_select, |
| visit_clinic_select, visit_clinic_custom, |
| visit_reason, |
| visit_reason_other, |
| visit_concerns_state, visit_symptoms_state, |
| visit_questions_state, |
| visit_inc_meds, visit_inc_allergies, visit_inc_history, |
| visit_inc_insurance, |
| ] |
| visit_form_outputs = visit_form_inputs + [ |
| visit_concerns_list_md, visit_concern_input, |
| visit_symptoms_list_md, visit_symptom_view_select, visit_symptom_view_md, |
| visit_reason_summary, visit_concerns_summary, |
| visit_destination_display, visit_brief_header_md, |
| visit_panel_destination, visit_dest_panel_open, |
| visit_panel_purpose, visit_purpose_panel_open, |
| visit_panel_symptoms, visit_symptoms_panel_open, |
| visit_panel_questions, visit_questions_panel_open, |
| visit_profile_symptom_pick, visit_symptom_custom_input, |
| visit_symptom_notes, visit_pain_level, |
| visit_chip_destination_btn, visit_chip_purpose_btn, |
| visit_chip_symptoms_btn, visit_chip_questions_btn, |
| ] |
|
|
| _visit_panel_toggle_inputs = [ |
| visit_dest_panel_open, visit_purpose_panel_open, |
| visit_symptoms_panel_open, visit_questions_panel_open, |
| active_profile_id, visit_clinic_select, visit_clinic_custom, |
| visit_destination_snapshot, |
| ] |
| _visit_panel_toggle_outputs = [ |
| visit_panel_destination, visit_dest_panel_open, |
| visit_panel_purpose, visit_purpose_panel_open, |
| visit_panel_symptoms, visit_symptoms_panel_open, |
| visit_profile_symptom_pick, |
| visit_panel_questions, visit_questions_panel_open, |
| visit_destination_snapshot, |
| ] |
| _visit_chip_label_inputs = [ |
| active_profile_id, visit_clinic_select, visit_clinic_custom, |
| visit_reason, visit_reason_other, visit_concerns_state, |
| visit_symptoms_state, visit_questions_state, |
| ] |
| _visit_chip_label_outputs = [ |
| visit_chip_destination_btn, visit_chip_purpose_btn, |
| visit_chip_symptoms_btn, visit_chip_questions_btn, |
| ] |
|
|
| for _which, _chip_btn in ( |
| ("dest", visit_chip_destination_btn), |
| ("purpose", visit_chip_purpose_btn), |
| ("symptoms", visit_chip_symptoms_btn), |
| ("questions", visit_chip_questions_btn), |
| ): |
| _chip_btn.click( |
| fn=lambda *a, w=_which: _visit_exclusive_panel_toggle(w, *a), |
| inputs=_visit_panel_toggle_inputs, |
| outputs=_visit_panel_toggle_outputs, |
| ) |
|
|
| visit_destination_save_btn.click( |
| fn=_visit_destination_save, |
| inputs=[active_profile_id, visit_clinic_select, visit_clinic_custom], |
| outputs=[ |
| visit_destination_display, visit_brief_header_md, |
| visit_panel_destination, visit_dest_panel_open, |
| visit_chip_destination_btn, |
| ], |
| ) |
| visit_destination_cancel_btn.click( |
| fn=_visit_destination_cancel, |
| inputs=[active_profile_id, visit_destination_snapshot], |
| outputs=[ |
| visit_destination_display, visit_brief_header_md, |
| visit_clinic_select, visit_clinic_custom, |
| visit_panel_destination, visit_dest_panel_open, |
| visit_chip_destination_btn, |
| ], |
| ) |
|
|
| visit_clinic_select.change( |
| fn=_on_visit_clinic_select_change, |
| inputs=[visit_clinic_select], |
| outputs=[visit_clinic_custom], |
| ) |
|
|
| |
| visit_reason.change( |
| fn=_on_reason_change, |
| inputs=[visit_reason, visit_reason_other], |
| outputs=[visit_reason_other, visit_reason_summary], |
| ).then( |
| fn=_visit_all_chip_label_updates, |
| inputs=_visit_chip_label_inputs, |
| outputs=_visit_chip_label_outputs, |
| ) |
| visit_reason_other.change( |
| fn=_on_reason_other_change, |
| inputs=[visit_reason, visit_reason_other], |
| outputs=[visit_reason_summary], |
| ).then( |
| fn=_visit_all_chip_label_updates, |
| inputs=_visit_chip_label_inputs, |
| outputs=_visit_chip_label_outputs, |
| ) |
|
|
| visit_add_symptoms_report_btn.click( |
| fn=_add_visit_symptoms_to_report, |
| inputs=[ |
| visit_symptoms_state, visit_profile_symptom_pick, |
| visit_pain_level, visit_symptom_custom_input, visit_symptom_notes, |
| ], |
| outputs=[ |
| visit_symptoms_state, visit_symptoms_list_md, |
| visit_symptom_view_select, visit_symptom_view_md, |
| visit_profile_symptom_pick, visit_symptom_custom_input, |
| visit_symptom_notes, |
| ], |
| ).then( |
| fn=_visit_all_chip_label_updates, |
| inputs=_visit_chip_label_inputs, |
| outputs=_visit_chip_label_outputs, |
| ) |
| visit_symptom_view_select.change( |
| fn=_view_symptom_entry, |
| inputs=[visit_symptoms_state, visit_symptom_view_select], |
| outputs=[visit_symptom_view_md], |
| ) |
| visit_clear_symptoms_btn.click( |
| fn=_clear_visit_symptoms, |
| inputs=None, |
| outputs=[ |
| visit_symptoms_state, visit_symptoms_list_md, |
| visit_symptom_view_select, visit_symptom_view_md, |
| visit_profile_symptom_pick, visit_symptom_custom_input, |
| visit_symptom_notes, visit_pain_level, |
| ], |
| ).then( |
| fn=_visit_all_chip_label_updates, |
| inputs=_visit_chip_label_inputs, |
| outputs=_visit_chip_label_outputs, |
| ) |
|
|
| visit_compiler_toggle_btn.click( |
| fn=_toggle_optional_panel, |
| inputs=[visit_compiler_panel_open], |
| outputs=[visit_compiler_panel, visit_compiler_panel_open], |
| ) |
|
|
| visit_add_concern_btn.click( |
| fn=_add_visit_concern, |
| inputs=[visit_concerns_state, visit_concern_input], |
| outputs=[ |
| visit_concerns_state, visit_concerns_list_md, |
| visit_concern_input, visit_concerns_summary, |
| ], |
| ).then( |
| fn=_visit_all_chip_label_updates, |
| inputs=_visit_chip_label_inputs, |
| outputs=_visit_chip_label_outputs, |
| ) |
| visit_concern_input.submit( |
| fn=_add_visit_concern, |
| inputs=[visit_concerns_state, visit_concern_input], |
| outputs=[ |
| visit_concerns_state, visit_concerns_list_md, |
| visit_concern_input, visit_concerns_summary, |
| ], |
| ).then( |
| fn=_visit_all_chip_label_updates, |
| inputs=_visit_chip_label_inputs, |
| outputs=_visit_chip_label_outputs, |
| ) |
| visit_clear_concerns_btn.click( |
| fn=_clear_visit_concerns, |
| inputs=None, |
| outputs=[ |
| visit_concerns_state, visit_concerns_list_md, |
| visit_concern_input, visit_concerns_summary, |
| ], |
| ).then( |
| fn=_visit_all_chip_label_updates, |
| inputs=_visit_chip_label_inputs, |
| outputs=_visit_chip_label_outputs, |
| ) |
| for _btn, _token in zip( |
| visit_concern_token_btns, _VISIT_CONCERN_QUICK_TOKENS, |
| ): |
| _btn.click( |
| fn=lambda lst, t=_token: _append_concern_token(lst, t), |
| inputs=[visit_concerns_state], |
| outputs=[ |
| visit_concerns_state, visit_concerns_list_md, |
| visit_concerns_summary, |
| ], |
| ).then( |
| fn=_visit_all_chip_label_updates, |
| inputs=_visit_chip_label_inputs, |
| outputs=_visit_chip_label_outputs, |
| ) |
|
|
| |
| generate_visit_btn.click( |
| fn=generate_visit_brief_resolved, |
| inputs=visit_form_inputs, |
| outputs=visit_output, |
| ) |
| clear_visit_btn.click( |
| fn=clear_visit_form, |
| inputs=[active_profile_id], |
| outputs=visit_form_outputs, |
| ) |
|
|
| |
| add_outputs = [ |
| visit_questions_state, visit_questions_list_md, |
| visit_question_input, visit_editing_state, |
| visit_delete_select, visit_edit_select, |
| visit_add_question_btn, visit_cancel_edit_btn, |
| visit_mode_md, |
| ] |
| visit_add_question_btn.click( |
| fn=_add_or_update_visit_question, |
| inputs=[visit_questions_state, visit_question_input, visit_editing_state], |
| outputs=add_outputs, |
| ).then( |
| fn=_visit_all_chip_label_updates, |
| inputs=_visit_chip_label_inputs, |
| outputs=_visit_chip_label_outputs, |
| ) |
| visit_cancel_edit_btn.click( |
| fn=_cancel_edit_visit_question, |
| inputs=[visit_questions_state], |
| outputs=add_outputs, |
| ) |
| visit_delete_question_btn.click( |
| fn=_delete_visit_question, |
| inputs=[visit_questions_state, visit_delete_select], |
| outputs=[ |
| visit_questions_state, visit_questions_list_md, |
| visit_delete_select, visit_edit_select, |
| ], |
| ).then( |
| fn=_visit_all_chip_label_updates, |
| inputs=_visit_chip_label_inputs, |
| outputs=_visit_chip_label_outputs, |
| ) |
| visit_edit_btn.click( |
| fn=_init_edit_visit_question, |
| inputs=[visit_questions_state, visit_edit_select], |
| outputs=add_outputs, |
| ) |
| visit_clear_questions_btn.click( |
| fn=_clear_visit_questions, |
| inputs=None, |
| outputs=[ |
| visit_questions_state, visit_questions_list_md, |
| visit_delete_select, visit_edit_select, |
| visit_add_question_btn, visit_cancel_edit_btn, |
| visit_mode_md, |
| ], |
| ).then( |
| fn=_visit_all_chip_label_updates, |
| inputs=_visit_chip_label_inputs, |
| outputs=_visit_chip_label_outputs, |
| ) |
|
|
| |
| _session_gate_outputs = [ |
| home_locked_view, home_active_view, |
| med_locked_view, med_active_view, |
| appt_locked_view, appt_active_view, |
| food_locked_view, food_active_view, |
| visit_locked_view, visit_active_view, |
| ] |
| def _apply_session_with_visit(selection): |
| updates = _apply_session_from_selector(selection) |
| aid = updates[0] |
| visit_up = gr.update( |
| value=aid, |
| choices=profiles.get_profile_choices(), |
| ) if aid else gr.update(value=None, choices=profiles.get_profile_choices()) |
| clinic_sync = _visit_clinic_sync_updates(aid) |
| return ( |
| *updates, visit_up, _session_auth_btn_update(aid), *clinic_sync, |
| ) |
|
|
| def _apply_session_settings_profile(selection): |
| """Update active session from Settings without leaving the tab.""" |
| aid = selection if _session_active(selection) else None |
| _persist_session(aid) |
| visit_up = gr.update( |
| value=aid, |
| choices=profiles.get_profile_choices(), |
| ) if aid else gr.update(value=None, choices=profiles.get_profile_choices()) |
| clinic_sync = _visit_clinic_sync_updates(aid) |
| return ( |
| aid, |
| _session_banner_update(aid), |
| *_session_gate_updates(aid), |
| visit_up, |
| _session_auth_btn_update(aid), |
| *clinic_sync, |
| ) |
|
|
| _session_visit_outputs = [ |
| visit_profile_select, session_auth_btn, |
| visit_clinic_select, visit_destination_display, visit_clinic_custom, |
| visit_panel_destination, visit_dest_panel_open, |
| visit_brief_header_md, |
| visit_panel_purpose, visit_purpose_panel_open, |
| visit_panel_symptoms, visit_symptoms_panel_open, |
| visit_panel_questions, visit_questions_panel_open, |
| visit_profile_symptom_pick, |
| visit_chip_destination_btn, visit_chip_purpose_btn, |
| visit_chip_symptoms_btn, visit_chip_questions_btn, |
| ] |
| _session_apply_outputs = [ |
| active_profile_id, session_banner, *_session_gate_outputs, |
| main_tabs, *_session_visit_outputs, |
| ] |
| _session_apply_settings_outputs = [ |
| active_profile_id, session_banner, *_session_gate_outputs, |
| *_session_visit_outputs, |
| ] |
|
|
| _go_settings_btns = [ |
| home_go_settings_btn, med_go_settings_btn, appt_go_settings_btn, |
| food_go_settings_btn, visit_go_settings_btn, |
| ] |
| for _btn in _go_settings_btns: |
| _btn.click(_go_to_settings_tab, outputs=[main_tabs]) |
|
|
| session_auth_btn.click( |
| fn=_session_auth_toggle, |
| inputs=[active_profile_id], |
| outputs=_session_apply_outputs, |
| ).then( |
| fn=_home_tab_on_select, |
| inputs=[active_profile_id, chatbot, home_chat_state], |
| outputs=[home_brief_md, chatbot, home_chat_state], |
| ) |
|
|
| |
| with gr.Tab( |
| "βοΈ Settings & Profile", |
| elem_id="settings-tab", |
| ) as settings_tab: |
|
|
| with gr.Column(elem_classes=["care-profile-manager"]): |
| gr.Markdown("### π€ Care Profile Manager") |
| gr.Markdown( |
| "*Manage care profiles. Each profile stores the data used in " |
| "the Doctor Visit brief.*", |
| elem_classes=["section-lead"], |
| ) |
|
|
| |
| _has_profiles = bool(profiles.get_profiles()) |
| with gr.Group( |
| visible=not _has_profiles, |
| elem_classes=["welcome-banner"], |
| ) as prof_welcome_banner: |
| gr.Markdown( |
| "### Welcome to AidAiLine β let's set up your first profile\n" |
| "Create a care profile so the app can pre-fill doctor visits, " |
| "insurance, and emergency contacts for you." |
| ) |
| with gr.Row(): |
| prof_welcome_go_btn = gr.Button("Go", variant="primary", size="sm") |
| prof_welcome_close_btn = gr.Button("Close", size="sm") |
|
|
| |
| _show_create_on_load = not _has_profiles |
| with gr.Row(elem_classes=["profile-command-bar"]): |
| prof_cmd_view_edit_btn = gr.Button( |
| "View/Edit Profile", |
| variant="secondary" if _show_create_on_load else "primary", |
| size="sm", |
| ) |
| prof_cmd_delete_btn = gr.Button( |
| "ποΈ Delete Profile", |
| variant="secondary", |
| size="sm", |
| ) |
| prof_cmd_create_btn = gr.Button( |
| "β Create New Profile", |
| variant="primary" if _show_create_on_load else "secondary", |
| size="sm", |
| ) |
|
|
| |
| with gr.Group(visible=False, elem_classes=["profile-workspace"]) as prof_view_edit_workspace: |
| prof_view_selector = gr.Dropdown( |
| choices=_existing_profile_choices(), |
| label="Select profile", |
| value=None, |
| info="Pick a saved profile to view or edit the sections below.", |
| allow_custom_value=False, |
| ) |
| prof_status = gr.Textbox( |
| label="Profile summary", |
| lines=4, |
| interactive=False, |
| value="", |
| ) |
|
|
| |
| with gr.Group(visible=False, elem_classes=["profile-workspace"]) as prof_delete_workspace: |
| with gr.Column(elem_classes=["profile-delete-panel"]): |
| prof_delete_selector = gr.Dropdown( |
| choices=_existing_profile_choices(), |
| label="Profile to delete", |
| value=None, |
| allow_custom_value=False, |
| ) |
| gr.Markdown("*This permanently removes the profile and cannot be undone.*") |
| prof_delete_execute_btn = gr.Button( |
| "ποΈ Delete Selected Profile", |
| variant="stop", |
| size="sm", |
| elem_classes=["profile-btn-compact"], |
| ) |
| prof_delete_status = gr.Textbox( |
| label="Status", |
| lines=2, |
| interactive=False, |
| value="", |
| ) |
|
|
| |
| with gr.Group( |
| visible=_show_create_on_load, |
| elem_classes=["profile-workspace"], |
| ) as prof_create_workspace: |
| gr.Markdown( |
| "*Fill in each section below, then click **β
Save New Profile** " |
| "at the bottom to create your profile with all entered details.*", |
| elem_classes=["section-lead"], |
| ) |
|
|
| |
| prof_selector = gr.Dropdown( |
| choices=_profile_dropdown_choices(), |
| value=_CREATE_NEW_PROFILE_ID if _show_create_on_load else ( |
| _existing_profile_choices()[0][1] if _has_profiles else None |
| ), |
| visible=False, |
| allow_custom_value=False, |
| ) |
|
|
| |
| with gr.Group( |
| visible=_show_create_on_load, |
| elem_classes=["profile-create-shell"], |
| ) as prof_form_panel: |
| prof_care_mode = gr.Radio( |
| choices=["Managing someone else's care", "Managing my own care"], |
| value="Managing someone else's care", |
| label="Care Mode", |
| ) |
|
|
| gr.Markdown("##### Care Profile Identity Summary") |
| prof_full_name_snapshot = gr.State(None) |
| prof_phone_snapshot = gr.State(None) |
| prof_address_snapshot = gr.State(None) |
| prof_cg_phone_snapshot = gr.State(None) |
| prof_cg_email_snapshot = gr.State(None) |
| prof_email_snapshot = gr.State(None) |
| prof_caregiver_snapshot = gr.State(None) |
| prof_rel_snapshot = gr.State(None) |
| prof_active_optional_panel = gr.State("") |
| prof_dob = gr.Textbox(value="", visible=False, label="Date of Birth") |
| with gr.Row( |
| elem_id="prof_identity_grid", |
| elem_classes=["identity-info-grid"], |
| ) as prof_identity_grid: |
| with gr.Column( |
| elem_id="prof_side_a_col", |
| elem_classes=["identity-side-col"], |
| scale=1, |
| ): |
| prof_side_a_header = gr.Markdown( |
| "π **Patient Information (Side A)**", |
| elem_classes=["identity-col-header"], |
| ) |
| with gr.Group( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-full-name-card", |
| ): |
| gr.Markdown( |
| "**Full Legal Name**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_full_name_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_full_name_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_full_name_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_full_name_edit_view: |
| prof_full_name = gr.Textbox( |
| value="", |
| label="Full Legal Name", |
| placeholder="e.g. Eleanor Rigby", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_full_name_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_full_name_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Group( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-address-card", |
| ): |
| gr.Markdown( |
| "**Address**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_address_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_address_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_address_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_address_edit_view: |
| prof_address_line1 = gr.Textbox( |
| value="", |
| label="Street Address 1", |
| placeholder="123 Main St", |
| show_label=False, |
| ) |
| prof_address_line2 = gr.Textbox( |
| value="", |
| label="Street Address 2", |
| placeholder="Apt 4B (optional)", |
| show_label=False, |
| ) |
| prof_address_city = gr.Textbox( |
| value="", |
| label="City", |
| placeholder="Springfield", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-edit-row"]): |
| prof_address_state = gr.Textbox( |
| value="", |
| label="State", |
| placeholder="IL", |
| show_label=False, |
| ) |
| prof_address_zip = gr.Textbox( |
| value="", |
| label="ZIP Code", |
| placeholder="62701", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_address_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_address_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Group( |
| elem_classes=["profile-field-card", "dob-card"], |
| elem_id="prof-dob-card", |
| ): |
| gr.Markdown( |
| "**Date of Birth**", |
| elem_classes=["field-card-title", "dob-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view", "dob-card-view"]) as prof_dob_card_view: |
| with gr.Row(elem_classes=["field-card-body", "dob-card-body"]): |
| prof_dob_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value", "dob-card-date"], |
| ) |
| prof_dob_edit_btn = gr.Button( |
| "Edit Date", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link", "dob-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit", "dob-card-edit"]) as prof_dob_edit_view: |
| with gr.Row(elem_classes=["field-card-edit-row", "dob-split-row"]): |
| prof_dob_month = gr.Dropdown( |
| choices=_DOB_MONTH_ABBRS, |
| value=None, |
| label="Month", |
| show_label=False, |
| allow_custom_value=False, |
| ) |
| prof_dob_day = gr.Dropdown( |
| choices=_DOB_DAY_CHOICES, |
| value=None, |
| label="Day", |
| show_label=False, |
| allow_custom_value=False, |
| ) |
| prof_dob_year = gr.Textbox( |
| value="", |
| label="Year", |
| show_label=False, |
| placeholder="1945 (Type Year)", |
| max_length=4, |
| elem_id="prof-dob-year", |
| ) |
| with gr.Row(elem_classes=["field-card-actions", "dob-split-actions"]): |
| prof_dob_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_dob_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Group( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-phone-card", |
| ): |
| gr.Markdown( |
| "**Phone Number**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_phone_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_phone_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_phone_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_phone_edit_view: |
| prof_phone_number = gr.Textbox( |
| value="", |
| label="Phone Number", |
| placeholder="(555) 123-4567", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_phone_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_phone_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Group( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-email-card", |
| ): |
| gr.Markdown( |
| "**Email Address**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_email_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_email_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_email_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_email_edit_view: |
| prof_email = gr.Textbox( |
| value="", |
| label="Email Address", |
| placeholder="eleanor@example.com", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_email_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_email_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Column( |
| elem_id="prof_side_b_col", |
| elem_classes=["identity-side-col"], |
| scale=1, |
| ) as prof_side_b_col: |
| prof_side_b_header = gr.Markdown( |
| "π₯ **Caregiver Contact Info (Side B)**", |
| elem_classes=["identity-col-header"], |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["profile-field-card"], |
| elem_id="prof-caregiver-slot", |
| ) as prof_caregiver_group: |
| gr.Markdown( |
| "**Caregiver Name**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_caregiver_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_caregiver_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_caregiver_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_caregiver_edit_view: |
| prof_caregiver_name = gr.Textbox( |
| value="", |
| label="Caregiver Name", |
| placeholder="Your name (e.g. Jane β daughter)", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_caregiver_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_caregiver_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Group( |
| visible=True, |
| elem_classes=["profile-field-card"], |
| elem_id="prof-relationship-slot", |
| ) as prof_relationship_group: |
| gr.Markdown( |
| "**Relationship**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_rel_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_rel_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_rel_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_rel_edit_view: |
| prof_relationship_category = gr.Dropdown( |
| choices=_RELATIONSHIP_CATEGORY_CHOICES, |
| label="Relationship", |
| value=_RELATIONSHIP_DEFAULT_CATEGORY, |
| allow_custom_value=True, |
| ) |
| prof_relationship_detail = gr.Dropdown( |
| choices=["Mom", "Dad", "Guardian", "Brother", "Sister"], |
| label="Specific relationship", |
| value="Mom", |
| visible=False, |
| allow_custom_value=True, |
| ) |
| prof_relationship_custom = gr.Textbox( |
| value="", |
| label="Custom relationship label", |
| placeholder="e.g. Spouse, Friend, Neighbor", |
| visible=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_rel_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_rel_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Group( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-caregiver-phone-card", |
| ): |
| gr.Markdown( |
| "**Phone Number**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_cg_phone_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_cg_phone_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_cg_phone_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_cg_phone_edit_view: |
| prof_caregiver_phone = gr.Textbox( |
| value="", |
| label="Caregiver Phone", |
| placeholder="(555) 123-4567", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_cg_phone_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_cg_phone_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Group( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-caregiver-email-card", |
| ): |
| gr.Markdown( |
| "**Email Address**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_cg_email_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_cg_email_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_cg_email_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_cg_email_edit_view: |
| prof_caregiver_email = gr.Textbox( |
| value="", |
| label="Caregiver Email", |
| placeholder="caregiver@example.com", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_cg_email_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_cg_email_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
|
|
| with gr.Row( |
| elem_id="prof_optional_actions", |
| elem_classes=["profile-optional-actions"], |
| ): |
| prof_add_emergency_btn = gr.Button( |
| "β Add Emergency Contacts", |
| variant="secondary", |
| size="sm", |
| ) |
| prof_add_insurance_btn = gr.Button( |
| "β Add Insurance Details", |
| variant="secondary", |
| size="sm", |
| ) |
| prof_add_pcp_btn = gr.Button( |
| "β Add Primary Care Doctor", |
| variant="secondary", |
| size="sm", |
| ) |
|
|
| with gr.Group( |
| visible=False, |
| elem_id="prof-emergency-panel", |
| elem_classes=["profile-optional-panel"], |
| ) as prof_emergency_panel: |
| prof_contact1_snapshot = gr.State(None) |
| with gr.Column( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-contact1-card", |
| ): |
| gr.Markdown( |
| "**Emergency Contact 1**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_contact1_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_contact1_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_contact1_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_contact1_edit_view: |
| prof_contact1_name = gr.Textbox( |
| value="", |
| label="Name", |
| placeholder="Jane (daughter)", |
| show_label=False, |
| ) |
| prof_contact1_phone = gr.Textbox( |
| value="", |
| label="Phone", |
| placeholder="(555) 123-4567", |
| show_label=False, |
| ) |
| prof_contact1_relationship = gr.Textbox( |
| value="", |
| label="Relationship", |
| placeholder="Daughter, Son, Spouse, Friend, etc.", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_contact1_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_contact1_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Group(visible=False) as prof_contact2_group: |
| prof_contact2_snapshot = gr.State(None) |
| with gr.Column( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-contact2-card", |
| ): |
| gr.Markdown( |
| "**Emergency Contact 2**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_contact2_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_contact2_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_contact2_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_contact2_edit_view: |
| prof_contact2_name = gr.Textbox( |
| value="", label="Name", show_label=False, |
| ) |
| prof_contact2_phone = gr.Textbox( |
| value="", label="Phone", show_label=False, |
| placeholder="(555) 123-4567", |
| ) |
| prof_contact2_relationship = gr.Textbox( |
| value="", label="Relationship", show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_contact2_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_contact2_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Row(elem_classes=["profile-optional-actions-inline"]): |
| prof_contact_add_btn = gr.Button( |
| "β Add another contact", variant="secondary", size="sm", |
| ) |
| prof_contact_remove_btn = gr.Button( |
| "ποΈ Remove Contact 2", variant="stop", size="sm", |
| visible=False, |
| ) |
|
|
| with gr.Group( |
| visible=False, |
| elem_id="prof-insurance-panel", |
| elem_classes=["profile-optional-panel"], |
| ) as prof_insurance_panel: |
| prof_insurance_snapshot = gr.State(None) |
| with gr.Column( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-insurance-card", |
| ): |
| gr.Markdown( |
| "**Insurance Provider**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_insurance_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_insurance_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_insurance_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_insurance_edit_view: |
| prof_insurance = gr.Textbox( |
| value="", |
| label="Insurance Provider", |
| placeholder="e.g. Medicare, Blue Cross", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_insurance_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_insurance_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Row(equal_height=True): |
| prof_policy_snapshot = gr.State(None) |
| with gr.Column( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-policy-card", |
| ): |
| gr.Markdown( |
| "**Policy ID**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_policy_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_policy_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_policy_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_policy_edit_view: |
| prof_policy = gr.Textbox( |
| value="", |
| label="Policy ID", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_policy_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_policy_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| prof_group_snapshot = gr.State(None) |
| with gr.Column( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-group-card", |
| ): |
| gr.Markdown( |
| "**Group ID**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_group_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_group_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_group_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_group_edit_view: |
| prof_group = gr.Textbox( |
| value="", |
| label="Group ID", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_group_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_group_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| prof_save_insurance_btn = gr.Button( |
| "πΎ Save Insurance", variant="primary", size="sm", |
| elem_classes=["profile-btn-compact"], |
| ) |
|
|
| with gr.Group( |
| visible=False, |
| elem_id="prof-pcp-panel", |
| elem_classes=["profile-optional-panel"], |
| ) as prof_pcp_panel: |
| prof_pcp_name_snapshot = gr.State(None) |
| with gr.Column( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-pcp-name-card", |
| ): |
| gr.Markdown( |
| "**Primary Care Doctor Name**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_pcp_name_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_pcp_name_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_pcp_name_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_pcp_name_edit_view: |
| prof_pcp_name = gr.Textbox( |
| value="", |
| label="Primary Care Doctor Name", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_pcp_name_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_pcp_name_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| with gr.Row(equal_height=True): |
| prof_pcp_clinic_snapshot = gr.State(None) |
| with gr.Column( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-pcp-clinic-card", |
| ): |
| gr.Markdown( |
| "**Clinic / Office Name**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_pcp_clinic_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_pcp_clinic_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_pcp_clinic_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_pcp_clinic_edit_view: |
| prof_pcp_clinic = gr.Textbox( |
| value="", |
| label="Clinic / Office Name", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_pcp_clinic_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_pcp_clinic_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| prof_pcp_phone_snapshot = gr.State(None) |
| with gr.Column( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-pcp-phone-card", |
| ): |
| gr.Markdown( |
| "**PCP Phone**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_pcp_phone_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_pcp_phone_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_pcp_phone_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_pcp_phone_edit_view: |
| prof_pcp_phone = gr.Textbox( |
| value="", |
| label="PCP Phone", |
| placeholder="(555) 123-4567", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_pcp_phone_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_pcp_phone_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| prof_pcp_address_snapshot = gr.State(None) |
| with gr.Column( |
| elem_classes=["profile-field-card"], |
| elem_id="prof-pcp-address-card", |
| ): |
| gr.Markdown( |
| "**Clinic Address**", |
| elem_classes=["field-card-title"], |
| ) |
| with gr.Group(visible=True, elem_classes=["field-card-view"]) as prof_pcp_address_card_view: |
| with gr.Row(elem_classes=["field-card-body"]): |
| prof_pcp_address_display = gr.Markdown( |
| "Not set", |
| elem_classes=["field-card-value"], |
| ) |
| prof_pcp_address_edit_btn = gr.Button( |
| "Edit", |
| variant="link", |
| size="sm", |
| elem_classes=["field-edit-link"], |
| ) |
| with gr.Group(visible=False, elem_classes=["field-card-edit"]) as prof_pcp_address_edit_view: |
| prof_pcp_address_line1 = gr.Textbox( |
| value="", |
| label="Street Address 1", |
| placeholder="456 Clinic Rd", |
| show_label=False, |
| ) |
| prof_pcp_address_line2 = gr.Textbox( |
| value="", |
| label="Street Address 2", |
| placeholder="Suite 200 (optional)", |
| show_label=False, |
| ) |
| prof_pcp_address_city = gr.Textbox( |
| value="", |
| label="City", |
| placeholder="Springfield", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-edit-row"]): |
| prof_pcp_address_state = gr.Textbox( |
| value="", |
| label="State", |
| placeholder="IL", |
| show_label=False, |
| ) |
| prof_pcp_address_zip = gr.Textbox( |
| value="", |
| label="ZIP Code", |
| placeholder="62701", |
| show_label=False, |
| ) |
| with gr.Row(elem_classes=["field-card-actions"]): |
| prof_pcp_address_save_btn = gr.Button( |
| "Save", variant="primary", size="sm", |
| ) |
| prof_pcp_address_cancel_btn = gr.Button( |
| "Cancel", size="sm", |
| ) |
| prof_save_pcp_btn = gr.Button( |
| "πΎ Save Primary Care Doctor", variant="primary", size="sm", |
| elem_classes=["profile-btn-compact"], |
| ) |
|
|
| _optional_panel_outputs = [ |
| prof_emergency_panel, prof_insurance_panel, prof_pcp_panel, |
| prof_active_optional_panel, |
| ] |
| prof_add_emergency_btn.click( |
| fn=_toggle_emergency_panel, |
| inputs=[prof_active_optional_panel], |
| outputs=_optional_panel_outputs, |
| ) |
| prof_add_insurance_btn.click( |
| fn=_toggle_insurance_panel, |
| inputs=[prof_active_optional_panel], |
| outputs=_optional_panel_outputs, |
| ) |
| prof_add_pcp_btn.click( |
| fn=_toggle_pcp_panel, |
| inputs=[prof_active_optional_panel], |
| outputs=_optional_panel_outputs, |
| ) |
|
|
| prof_save_identity_btn = gr.Button( |
| "πΎ Save Identity", variant="primary", size="sm", |
| elem_classes=["profile-btn-compact"], |
| visible=not _show_create_on_load, |
| ) |
|
|
| gr.Markdown("---") |
| with gr.Group( |
| visible=_show_create_on_load, |
| elem_classes=["profile-create-submit"], |
| ) as prof_create_submit_group: |
| prof_add_new_btn = gr.Button( |
| "β
Save New Profile", variant="primary", |
| elem_classes=["profile-btn-compact"], |
| ) |
| gr.Markdown( |
| "*Saves a **new** profile from all fields above in one step. " |
| "**Required:** Full Legal Name. Relationship is required only when " |
| "managing someone else's care.*", |
| elem_classes=["section-lead"], |
| ) |
|
|
| |
| _prof_form_fields = [ |
| prof_care_mode, prof_caregiver_name, prof_caregiver_phone, prof_caregiver_email, |
| prof_relationship_category, prof_relationship_detail, prof_relationship_custom, |
| prof_full_name, prof_dob, |
| prof_address_line1, prof_address_line2, prof_address_city, prof_address_state, prof_address_zip, |
| prof_phone_number, prof_email, |
| prof_insurance, prof_policy, prof_group, |
| prof_pcp_name, prof_pcp_phone, prof_pcp_clinic, |
| prof_pcp_address_line1, prof_pcp_address_line2, prof_pcp_address_city, |
| prof_pcp_address_state, prof_pcp_address_zip, |
| prof_contact1_name, prof_contact1_phone, prof_contact1_relationship, |
| prof_contact2_name, prof_contact2_phone, prof_contact2_relationship, |
| ] |
| _prof_cmd_outputs = [ |
| prof_cmd_view_edit_btn, prof_cmd_delete_btn, prof_cmd_create_btn, |
| ] |
| _prof_workspace_outputs = [ |
| prof_view_edit_workspace, prof_delete_workspace, |
| prof_create_workspace, prof_form_panel, prof_create_submit_group, |
| prof_save_identity_btn, |
| ] |
| _prof_route_body_outputs = [ |
| prof_view_selector, prof_selector, prof_status, |
| *_prof_form_fields, |
| prof_contact2_group, prof_relationship_group, prof_caregiver_group, |
| prof_identity_grid, prof_side_a_header, prof_side_b_header, |
| prof_add_emergency_btn, |
| prof_delete_selector, |
| ] |
| _prof_open_view_edit_outputs = ( |
| _prof_cmd_outputs + _prof_workspace_outputs + _prof_route_body_outputs |
| ) |
|
|
| def _restore_profile_workspace_on_load_ui(profile_id): |
| return _restore_profile_workspace_on_load( |
| profile_id, len(_prof_open_view_edit_outputs), |
| ) |
|
|
| _prof_open_create_outputs = _prof_open_view_edit_outputs |
| _prof_open_delete_outputs = ( |
| _prof_cmd_outputs + _prof_workspace_outputs + [ |
| prof_delete_selector, prof_delete_status, |
| ] |
| ) |
| _prof_delete_outputs = [ |
| prof_delete_status, prof_delete_selector, |
| *_prof_open_create_outputs, |
| ] |
| _prof_view_select_outputs = _prof_route_body_outputs |
| _dob_card_outputs = [ |
| prof_dob_display, prof_dob_month, prof_dob_day, prof_dob_year, |
| prof_dob_card_view, prof_dob_edit_view, |
| ] |
| _dob_save_outputs = [prof_dob, prof_dob_display, prof_dob_card_view, prof_dob_edit_view] |
| _dob_edit_outputs = [ |
| prof_dob_card_view, prof_dob_edit_view, |
| prof_dob_month, prof_dob_day, prof_dob_year, |
| ] |
| _rel_card_outputs = [prof_rel_display, prof_rel_card_view, prof_rel_edit_view] |
| _rel_begin_outputs = [ |
| prof_rel_card_view, prof_rel_edit_view, |
| prof_relationship_category, prof_relationship_detail, prof_relationship_custom, |
| prof_rel_snapshot, |
| ] |
| _rel_save_outputs = [prof_rel_display, prof_rel_card_view, prof_rel_edit_view] |
| _rel_cancel_outputs = [ |
| prof_rel_display, prof_rel_card_view, prof_rel_edit_view, |
| prof_relationship_category, prof_relationship_detail, prof_relationship_custom, |
| ] |
|
|
| _caregiver_card_outputs = [ |
| prof_caregiver_display, prof_caregiver_card_view, prof_caregiver_edit_view, |
| ] |
| _caregiver_begin_outputs = [ |
| prof_caregiver_card_view, prof_caregiver_edit_view, prof_caregiver_snapshot, |
| ] |
| _caregiver_save_outputs = [ |
| prof_caregiver_display, prof_caregiver_card_view, prof_caregiver_edit_view, |
| prof_caregiver_name, |
| ] |
| _caregiver_cancel_outputs = [ |
| prof_caregiver_display, prof_caregiver_card_view, prof_caregiver_edit_view, |
| prof_caregiver_name, |
| ] |
| _full_name_card_outputs = [ |
| prof_full_name_display, prof_full_name_card_view, prof_full_name_edit_view, |
| ] |
| _full_name_begin_outputs = [ |
| prof_full_name_card_view, prof_full_name_edit_view, prof_full_name_snapshot, |
| ] |
| _full_name_save_outputs = [ |
| prof_full_name_display, prof_full_name_card_view, prof_full_name_edit_view, |
| prof_full_name, |
| ] |
| _full_name_cancel_outputs = [ |
| prof_full_name_display, prof_full_name_card_view, prof_full_name_edit_view, |
| prof_full_name, |
| ] |
| _address_card_outputs = [ |
| prof_address_display, prof_address_card_view, prof_address_edit_view, |
| ] |
| _address_begin_outputs = [ |
| prof_address_card_view, prof_address_edit_view, prof_address_snapshot, |
| ] |
| _address_save_outputs = [ |
| prof_address_display, prof_address_card_view, prof_address_edit_view, |
| prof_address_line1, prof_address_line2, prof_address_city, prof_address_state, prof_address_zip, |
| ] |
| _address_cancel_outputs = [ |
| prof_address_display, prof_address_card_view, prof_address_edit_view, |
| prof_address_line1, prof_address_line2, prof_address_city, prof_address_state, prof_address_zip, |
| ] |
| _phone_card_outputs = [ |
| prof_phone_display, prof_phone_card_view, prof_phone_edit_view, |
| ] |
| _phone_begin_outputs = [ |
| prof_phone_card_view, prof_phone_edit_view, prof_phone_snapshot, |
| ] |
| _phone_save_outputs = [ |
| prof_phone_display, prof_phone_card_view, prof_phone_edit_view, |
| prof_phone_number, |
| ] |
| _phone_cancel_outputs = [ |
| prof_phone_display, prof_phone_card_view, prof_phone_edit_view, |
| prof_phone_number, |
| ] |
| _email_card_outputs = [ |
| prof_email_display, prof_email_card_view, prof_email_edit_view, |
| ] |
| _email_begin_outputs = [ |
| prof_email_card_view, prof_email_edit_view, prof_email_snapshot, |
| ] |
| _email_save_outputs = [ |
| prof_email_display, prof_email_card_view, prof_email_edit_view, |
| prof_email, |
| ] |
| _email_cancel_outputs = [ |
| prof_email_display, prof_email_card_view, prof_email_edit_view, |
| prof_email, |
| ] |
| _insurance_card_outputs = [ |
| prof_insurance_display, prof_insurance_card_view, prof_insurance_edit_view, |
| ] |
| _insurance_begin_outputs = [ |
| prof_insurance_card_view, prof_insurance_edit_view, prof_insurance_snapshot, |
| ] |
| _insurance_save_outputs = [ |
| prof_insurance_display, prof_insurance_card_view, prof_insurance_edit_view, |
| prof_insurance, |
| ] |
| _insurance_cancel_outputs = [ |
| prof_insurance_display, prof_insurance_card_view, prof_insurance_edit_view, |
| prof_insurance, |
| ] |
| _policy_card_outputs = [ |
| prof_policy_display, prof_policy_card_view, prof_policy_edit_view, |
| ] |
| _policy_begin_outputs = [ |
| prof_policy_card_view, prof_policy_edit_view, prof_policy_snapshot, |
| ] |
| _policy_save_outputs = [ |
| prof_policy_display, prof_policy_card_view, prof_policy_edit_view, |
| prof_policy, |
| ] |
| _policy_cancel_outputs = [ |
| prof_policy_display, prof_policy_card_view, prof_policy_edit_view, |
| prof_policy, |
| ] |
| _group_card_outputs = [ |
| prof_group_display, prof_group_card_view, prof_group_edit_view, |
| ] |
| _group_begin_outputs = [ |
| prof_group_card_view, prof_group_edit_view, prof_group_snapshot, |
| ] |
| _group_save_outputs = [ |
| prof_group_display, prof_group_card_view, prof_group_edit_view, |
| prof_group, |
| ] |
| _group_cancel_outputs = [ |
| prof_group_display, prof_group_card_view, prof_group_edit_view, |
| prof_group, |
| ] |
| _cg_phone_card_outputs = [ |
| prof_cg_phone_display, prof_cg_phone_card_view, prof_cg_phone_edit_view, |
| ] |
| _cg_phone_begin_outputs = [ |
| prof_cg_phone_card_view, prof_cg_phone_edit_view, prof_cg_phone_snapshot, |
| ] |
| _cg_phone_save_outputs = [ |
| prof_cg_phone_display, prof_cg_phone_card_view, prof_cg_phone_edit_view, |
| prof_caregiver_phone, |
| ] |
| _cg_phone_cancel_outputs = [ |
| prof_cg_phone_display, prof_cg_phone_card_view, prof_cg_phone_edit_view, |
| prof_caregiver_phone, |
| ] |
| _cg_email_card_outputs = [ |
| prof_cg_email_display, prof_cg_email_card_view, prof_cg_email_edit_view, |
| ] |
| _cg_email_begin_outputs = [ |
| prof_cg_email_card_view, prof_cg_email_edit_view, prof_cg_email_snapshot, |
| ] |
| _cg_email_save_outputs = [ |
| prof_cg_email_display, prof_cg_email_card_view, prof_cg_email_edit_view, |
| prof_caregiver_email, |
| ] |
| _cg_email_cancel_outputs = [ |
| prof_cg_email_display, prof_cg_email_card_view, prof_cg_email_edit_view, |
| prof_caregiver_email, |
| ] |
| _contact1_card_outputs = [ |
| prof_contact1_display, prof_contact1_card_view, prof_contact1_edit_view, |
| ] |
| _contact1_begin_outputs = [ |
| prof_contact1_card_view, prof_contact1_edit_view, prof_contact1_snapshot, |
| ] |
| _contact1_save_outputs = [ |
| prof_contact1_display, prof_contact1_card_view, prof_contact1_edit_view, |
| prof_contact1_name, prof_contact1_phone, prof_contact1_relationship, |
| ] |
| _contact1_cancel_outputs = [ |
| prof_contact1_display, prof_contact1_card_view, prof_contact1_edit_view, |
| prof_contact1_name, prof_contact1_phone, prof_contact1_relationship, |
| ] |
| _contact2_card_outputs = [ |
| prof_contact2_display, prof_contact2_card_view, prof_contact2_edit_view, |
| ] |
| _contact2_begin_outputs = [ |
| prof_contact2_card_view, prof_contact2_edit_view, prof_contact2_snapshot, |
| ] |
| _contact2_save_outputs = [ |
| prof_contact2_display, prof_contact2_card_view, prof_contact2_edit_view, |
| prof_contact2_name, prof_contact2_phone, prof_contact2_relationship, |
| ] |
| _contact2_cancel_outputs = [ |
| prof_contact2_display, prof_contact2_card_view, prof_contact2_edit_view, |
| prof_contact2_name, prof_contact2_phone, prof_contact2_relationship, |
| ] |
| _pcp_name_card_outputs = [ |
| prof_pcp_name_display, prof_pcp_name_card_view, prof_pcp_name_edit_view, |
| ] |
| _pcp_name_begin_outputs = [ |
| prof_pcp_name_card_view, prof_pcp_name_edit_view, prof_pcp_name_snapshot, |
| ] |
| _pcp_name_save_outputs = [ |
| prof_pcp_name_display, prof_pcp_name_card_view, prof_pcp_name_edit_view, |
| prof_pcp_name, |
| ] |
| _pcp_name_cancel_outputs = [ |
| prof_pcp_name_display, prof_pcp_name_card_view, prof_pcp_name_edit_view, |
| prof_pcp_name, |
| ] |
| _pcp_clinic_card_outputs = [ |
| prof_pcp_clinic_display, prof_pcp_clinic_card_view, prof_pcp_clinic_edit_view, |
| ] |
| _pcp_clinic_begin_outputs = [ |
| prof_pcp_clinic_card_view, prof_pcp_clinic_edit_view, prof_pcp_clinic_snapshot, |
| ] |
| _pcp_clinic_save_outputs = [ |
| prof_pcp_clinic_display, prof_pcp_clinic_card_view, prof_pcp_clinic_edit_view, |
| prof_pcp_clinic, |
| ] |
| _pcp_clinic_cancel_outputs = [ |
| prof_pcp_clinic_display, prof_pcp_clinic_card_view, prof_pcp_clinic_edit_view, |
| prof_pcp_clinic, |
| ] |
| _pcp_phone_card_outputs = [ |
| prof_pcp_phone_display, prof_pcp_phone_card_view, prof_pcp_phone_edit_view, |
| ] |
| _pcp_phone_begin_outputs = [ |
| prof_pcp_phone_card_view, prof_pcp_phone_edit_view, prof_pcp_phone_snapshot, |
| ] |
| _pcp_phone_save_outputs = [ |
| prof_pcp_phone_display, prof_pcp_phone_card_view, prof_pcp_phone_edit_view, |
| prof_pcp_phone, |
| ] |
| _pcp_phone_cancel_outputs = [ |
| prof_pcp_phone_display, prof_pcp_phone_card_view, prof_pcp_phone_edit_view, |
| prof_pcp_phone, |
| ] |
| _pcp_address_card_outputs = [ |
| prof_pcp_address_display, prof_pcp_address_card_view, prof_pcp_address_edit_view, |
| ] |
| _pcp_address_begin_outputs = [ |
| prof_pcp_address_card_view, prof_pcp_address_edit_view, prof_pcp_address_snapshot, |
| ] |
| _pcp_address_save_outputs = [ |
| prof_pcp_address_display, prof_pcp_address_card_view, prof_pcp_address_edit_view, |
| prof_pcp_address_line1, prof_pcp_address_line2, prof_pcp_address_city, |
| prof_pcp_address_state, prof_pcp_address_zip, |
| ] |
| _pcp_address_cancel_outputs = [ |
| prof_pcp_address_display, prof_pcp_address_card_view, prof_pcp_address_edit_view, |
| prof_pcp_address_line1, prof_pcp_address_line2, prof_pcp_address_city, |
| prof_pcp_address_state, prof_pcp_address_zip, |
| ] |
|
|
| def _sync_cards_from_form(full_name, dob, caregiver, rel_cat, rel_det, rel_cus, |
| addr1, addr2, addr_city, addr_state, addr_zip, |
| phone, email, |
| c1_name, c1_phone, c1_rel, |
| c2_name, c2_phone, c2_rel, |
| insurance, policy, group, |
| cg_phone, cg_email, |
| pcp_name, pcp_clinic, pcp_phone, |
| pcp_addr1, pcp_addr2, pcp_addr_city, pcp_addr_state, pcp_addr_zip): |
| return ( |
| *_sync_field_card(full_name), |
| *_sync_dob_card(dob), |
| *_sync_address_card(addr1, addr2, addr_city, addr_state, addr_zip), |
| *_sync_phone_field_card(phone), |
| *_sync_caregiver_card(caregiver), |
| *_sync_relationship_card(rel_cat, rel_det, rel_cus), |
| *_sync_phone_field_card(cg_phone), |
| *_sync_field_card(cg_email), |
| *_sync_field_card(email), |
| *_sync_contact_card(c1_name, c1_phone, c1_rel), |
| *_sync_contact_card(c2_name, c2_phone, c2_rel), |
| *_sync_field_card(insurance), |
| *_sync_field_card(policy), |
| *_sync_field_card(group), |
| *_sync_field_card(pcp_name), |
| *_sync_field_card(pcp_clinic), |
| *_sync_phone_field_card(pcp_phone), |
| *_sync_address_card(pcp_addr1, pcp_addr2, pcp_addr_city, pcp_addr_state, pcp_addr_zip), |
| ) |
|
|
| def _sync_cards_from_profile_id(profile_id): |
| """Refresh cards from saved profile (safe in Gradio 5 .then() chains).""" |
| return _sync_cards_from_form(*_profile_card_sync_values(profile_id)) |
|
|
| _card_sync_inputs = [ |
| prof_full_name, prof_dob, prof_caregiver_name, |
| prof_relationship_category, prof_relationship_detail, prof_relationship_custom, |
| prof_address_line1, prof_address_line2, prof_address_city, prof_address_state, prof_address_zip, |
| prof_phone_number, prof_email, |
| prof_contact1_name, prof_contact1_phone, prof_contact1_relationship, |
| prof_contact2_name, prof_contact2_phone, prof_contact2_relationship, |
| prof_insurance, prof_policy, prof_group, |
| prof_caregiver_phone, prof_caregiver_email, |
| prof_pcp_name, prof_pcp_clinic, prof_pcp_phone, |
| prof_pcp_address_line1, prof_pcp_address_line2, prof_pcp_address_city, |
| prof_pcp_address_state, prof_pcp_address_zip, |
| ] |
| _card_sync_outputs = ( |
| _full_name_card_outputs + _dob_card_outputs + _address_card_outputs |
| + _phone_card_outputs + _caregiver_card_outputs + _rel_card_outputs |
| + _cg_phone_card_outputs + _cg_email_card_outputs + _email_card_outputs |
| + _contact1_card_outputs + _contact2_card_outputs |
| + _insurance_card_outputs + _policy_card_outputs + _group_card_outputs |
| + _pcp_name_card_outputs + _pcp_clinic_card_outputs |
| + _pcp_phone_card_outputs + _pcp_address_card_outputs |
| ) |
|
|
| _optional_panel_outputs = [ |
| prof_emergency_panel, prof_insurance_panel, prof_pcp_panel, |
| prof_active_optional_panel, |
| ] |
| _optional_panel_toggle_inputs = [prof_active_optional_panel] |
|
|
| prof_cmd_view_edit_btn.click( |
| fn=_open_view_edit_workspace, |
| inputs=None, |
| outputs=_prof_open_view_edit_outputs, |
| ).then( |
| fn=_sync_cards_from_profile_id, |
| inputs=[prof_view_selector], |
| outputs=_card_sync_outputs, |
| ).then( |
| fn=_apply_session_settings_profile, |
| inputs=[prof_view_selector], |
| outputs=_session_apply_settings_outputs, |
| ) |
| prof_cmd_delete_btn.click( |
| fn=_open_delete_workspace, |
| inputs=None, |
| outputs=_prof_open_delete_outputs, |
| ) |
| prof_cmd_create_btn.click( |
| fn=_open_create_workspace, |
| inputs=None, |
| outputs=_prof_open_create_outputs, |
| ).then( |
| fn=_sync_cards_from_profile_id, |
| inputs=[prof_selector], |
| outputs=_card_sync_outputs, |
| ).then( |
| fn=_apply_session_settings_profile, |
| inputs=[prof_selector], |
| outputs=_session_apply_settings_outputs, |
| ) |
|
|
| prof_welcome_go_btn.click( |
| fn=_welcome_go, |
| inputs=None, |
| outputs=[prof_welcome_banner, *_prof_open_create_outputs], |
| ).then( |
| fn=_sync_cards_from_profile_id, |
| inputs=[prof_selector], |
| outputs=_card_sync_outputs, |
| ) |
| prof_welcome_close_btn.click( |
| fn=_welcome_dismiss, |
| inputs=None, |
| outputs=[prof_welcome_banner], |
| ) |
|
|
| prof_view_selector.input( |
| fn=_on_view_selector_change, |
| inputs=[prof_view_selector], |
| outputs=_prof_view_select_outputs, |
| ).then( |
| fn=_sync_cards_from_profile_id, |
| inputs=[prof_view_selector], |
| outputs=_card_sync_outputs, |
| ).then( |
| fn=_close_all_optional_panels, |
| inputs=None, |
| outputs=_optional_panel_outputs, |
| ).then( |
| fn=_apply_session_settings_profile, |
| inputs=[prof_view_selector], |
| outputs=_session_apply_settings_outputs, |
| ) |
|
|
| prof_dob_edit_btn.click( |
| fn=_dob_begin_edit, |
| inputs=[prof_dob], |
| outputs=_dob_edit_outputs, |
| ) |
| prof_dob_save_btn.click( |
| fn=_dob_save_edit, |
| inputs=[prof_dob_month, prof_dob_day, prof_dob_year, prof_dob], |
| outputs=_dob_save_outputs, |
| ) |
| prof_dob_cancel_btn.click( |
| fn=_dob_cancel_edit, |
| inputs=[prof_dob], |
| outputs=_dob_card_outputs, |
| ) |
|
|
| prof_rel_edit_btn.click( |
| fn=_relationship_begin_edit, |
| inputs=[ |
| prof_relationship_category, |
| prof_relationship_detail, |
| prof_relationship_custom, |
| ], |
| outputs=_rel_begin_outputs, |
| ) |
| prof_rel_save_btn.click( |
| fn=_relationship_save_edit, |
| inputs=[ |
| prof_relationship_category, |
| prof_relationship_detail, |
| prof_relationship_custom, |
| ], |
| outputs=_rel_save_outputs, |
| ) |
| prof_rel_cancel_btn.click( |
| fn=_relationship_cancel_edit, |
| inputs=[prof_rel_snapshot], |
| outputs=_rel_cancel_outputs, |
| ) |
|
|
| prof_caregiver_edit_btn.click( |
| fn=_caregiver_begin_edit, |
| inputs=[prof_caregiver_name], |
| outputs=_caregiver_begin_outputs, |
| ) |
| prof_caregiver_save_btn.click( |
| fn=_caregiver_save_edit, |
| inputs=[prof_caregiver_name], |
| outputs=_caregiver_save_outputs, |
| ) |
| prof_caregiver_cancel_btn.click( |
| fn=_caregiver_cancel_edit, |
| inputs=[prof_caregiver_snapshot], |
| outputs=_caregiver_cancel_outputs, |
| ) |
|
|
| prof_full_name_edit_btn.click( |
| fn=_field_begin_edit, |
| inputs=[prof_full_name], |
| outputs=_full_name_begin_outputs, |
| ) |
| prof_full_name_save_btn.click( |
| fn=_field_save_edit, |
| inputs=[prof_full_name], |
| outputs=_full_name_save_outputs, |
| ) |
| prof_full_name_cancel_btn.click( |
| fn=_field_cancel_edit, |
| inputs=[prof_full_name_snapshot], |
| outputs=_full_name_cancel_outputs, |
| ) |
|
|
| prof_address_edit_btn.click( |
| fn=_address_begin_edit, |
| inputs=[ |
| prof_address_line1, prof_address_line2, prof_address_city, |
| prof_address_state, prof_address_zip, |
| ], |
| outputs=_address_begin_outputs, |
| ) |
| prof_address_save_btn.click( |
| fn=_address_save_edit, |
| inputs=[ |
| prof_address_line1, prof_address_line2, prof_address_city, |
| prof_address_state, prof_address_zip, |
| ], |
| outputs=_address_save_outputs, |
| ) |
| prof_address_cancel_btn.click( |
| fn=_address_cancel_edit, |
| inputs=[prof_address_snapshot], |
| outputs=_address_cancel_outputs, |
| ) |
|
|
| prof_phone_edit_btn.click( |
| fn=_field_begin_edit, |
| inputs=[prof_phone_number], |
| outputs=_phone_begin_outputs, |
| ) |
| prof_phone_save_btn.click( |
| fn=_field_save_phone_edit, |
| inputs=[prof_phone_number], |
| outputs=_phone_save_outputs, |
| ) |
| prof_phone_cancel_btn.click( |
| fn=_field_cancel_phone_edit, |
| inputs=[prof_phone_snapshot], |
| outputs=_phone_cancel_outputs, |
| ) |
|
|
| prof_email_edit_btn.click( |
| fn=_field_begin_edit, |
| inputs=[prof_email], |
| outputs=_email_begin_outputs, |
| ) |
| prof_email_save_btn.click( |
| fn=_field_save_edit, |
| inputs=[prof_email], |
| outputs=_email_save_outputs, |
| ) |
| prof_email_cancel_btn.click( |
| fn=_field_cancel_edit, |
| inputs=[prof_email_snapshot], |
| outputs=_email_cancel_outputs, |
| ) |
|
|
| prof_insurance_edit_btn.click( |
| fn=_field_begin_edit, |
| inputs=[prof_insurance], |
| outputs=_insurance_begin_outputs, |
| ) |
| prof_insurance_save_btn.click( |
| fn=_field_save_edit, |
| inputs=[prof_insurance], |
| outputs=_insurance_save_outputs, |
| ) |
| prof_insurance_cancel_btn.click( |
| fn=_field_cancel_edit, |
| inputs=[prof_insurance_snapshot], |
| outputs=_insurance_cancel_outputs, |
| ) |
|
|
| prof_policy_edit_btn.click( |
| fn=_field_begin_edit, |
| inputs=[prof_policy], |
| outputs=_policy_begin_outputs, |
| ) |
| prof_policy_save_btn.click( |
| fn=_field_save_edit, |
| inputs=[prof_policy], |
| outputs=_policy_save_outputs, |
| ) |
| prof_policy_cancel_btn.click( |
| fn=_field_cancel_edit, |
| inputs=[prof_policy_snapshot], |
| outputs=_policy_cancel_outputs, |
| ) |
|
|
| prof_group_edit_btn.click( |
| fn=_field_begin_edit, |
| inputs=[prof_group], |
| outputs=_group_begin_outputs, |
| ) |
| prof_group_save_btn.click( |
| fn=_field_save_edit, |
| inputs=[prof_group], |
| outputs=_group_save_outputs, |
| ) |
| prof_group_cancel_btn.click( |
| fn=_field_cancel_edit, |
| inputs=[prof_group_snapshot], |
| outputs=_group_cancel_outputs, |
| ) |
|
|
| prof_cg_phone_edit_btn.click( |
| fn=_field_begin_edit, |
| inputs=[prof_caregiver_phone], |
| outputs=_cg_phone_begin_outputs, |
| ) |
| prof_cg_phone_save_btn.click( |
| fn=_field_save_phone_edit, |
| inputs=[prof_caregiver_phone], |
| outputs=_cg_phone_save_outputs, |
| ) |
| prof_cg_phone_cancel_btn.click( |
| fn=_field_cancel_phone_edit, |
| inputs=[prof_cg_phone_snapshot], |
| outputs=_cg_phone_cancel_outputs, |
| ) |
|
|
| prof_cg_email_edit_btn.click( |
| fn=_field_begin_edit, |
| inputs=[prof_caregiver_email], |
| outputs=_cg_email_begin_outputs, |
| ) |
| prof_cg_email_save_btn.click( |
| fn=_field_save_edit, |
| inputs=[prof_caregiver_email], |
| outputs=_cg_email_save_outputs, |
| ) |
| prof_cg_email_cancel_btn.click( |
| fn=_field_cancel_edit, |
| inputs=[prof_cg_email_snapshot], |
| outputs=_cg_email_cancel_outputs, |
| ) |
|
|
| prof_contact1_edit_btn.click( |
| fn=_contact_begin_edit, |
| inputs=[ |
| prof_contact1_name, prof_contact1_phone, prof_contact1_relationship, |
| ], |
| outputs=_contact1_begin_outputs, |
| ) |
| prof_contact1_save_btn.click( |
| fn=_contact_save_edit, |
| inputs=[ |
| prof_contact1_name, prof_contact1_phone, prof_contact1_relationship, |
| ], |
| outputs=_contact1_save_outputs, |
| ) |
| prof_contact1_cancel_btn.click( |
| fn=_contact_cancel_edit, |
| inputs=[prof_contact1_snapshot], |
| outputs=_contact1_cancel_outputs, |
| ) |
|
|
| prof_contact2_edit_btn.click( |
| fn=_contact_begin_edit, |
| inputs=[ |
| prof_contact2_name, prof_contact2_phone, prof_contact2_relationship, |
| ], |
| outputs=_contact2_begin_outputs, |
| ) |
| prof_contact2_save_btn.click( |
| fn=_contact_save_edit, |
| inputs=[ |
| prof_contact2_name, prof_contact2_phone, prof_contact2_relationship, |
| ], |
| outputs=_contact2_save_outputs, |
| ) |
| prof_contact2_cancel_btn.click( |
| fn=_contact_cancel_edit, |
| inputs=[prof_contact2_snapshot], |
| outputs=_contact2_cancel_outputs, |
| ) |
|
|
| prof_pcp_name_edit_btn.click( |
| fn=_field_begin_edit, |
| inputs=[prof_pcp_name], |
| outputs=_pcp_name_begin_outputs, |
| ) |
| prof_pcp_name_save_btn.click( |
| fn=_field_save_edit, |
| inputs=[prof_pcp_name], |
| outputs=_pcp_name_save_outputs, |
| ) |
| prof_pcp_name_cancel_btn.click( |
| fn=_field_cancel_edit, |
| inputs=[prof_pcp_name_snapshot], |
| outputs=_pcp_name_cancel_outputs, |
| ) |
|
|
| prof_pcp_clinic_edit_btn.click( |
| fn=_field_begin_edit, |
| inputs=[prof_pcp_clinic], |
| outputs=_pcp_clinic_begin_outputs, |
| ) |
| prof_pcp_clinic_save_btn.click( |
| fn=_field_save_edit, |
| inputs=[prof_pcp_clinic], |
| outputs=_pcp_clinic_save_outputs, |
| ) |
| prof_pcp_clinic_cancel_btn.click( |
| fn=_field_cancel_edit, |
| inputs=[prof_pcp_clinic_snapshot], |
| outputs=_pcp_clinic_cancel_outputs, |
| ) |
|
|
| prof_pcp_phone_edit_btn.click( |
| fn=_field_begin_edit, |
| inputs=[prof_pcp_phone], |
| outputs=_pcp_phone_begin_outputs, |
| ) |
| prof_pcp_phone_save_btn.click( |
| fn=_field_save_phone_edit, |
| inputs=[prof_pcp_phone], |
| outputs=_pcp_phone_save_outputs, |
| ) |
| prof_pcp_phone_cancel_btn.click( |
| fn=_field_cancel_phone_edit, |
| inputs=[prof_pcp_phone_snapshot], |
| outputs=_pcp_phone_cancel_outputs, |
| ) |
|
|
| prof_pcp_address_edit_btn.click( |
| fn=_pcp_address_begin_edit, |
| inputs=[ |
| prof_pcp_address_line1, prof_pcp_address_line2, prof_pcp_address_city, |
| prof_pcp_address_state, prof_pcp_address_zip, |
| ], |
| outputs=_pcp_address_begin_outputs, |
| ) |
| prof_pcp_address_save_btn.click( |
| fn=_pcp_address_save_edit, |
| inputs=[ |
| prof_pcp_address_line1, prof_pcp_address_line2, prof_pcp_address_city, |
| prof_pcp_address_state, prof_pcp_address_zip, |
| ], |
| outputs=_pcp_address_save_outputs, |
| ) |
| prof_pcp_address_cancel_btn.click( |
| fn=_address_cancel_edit, |
| inputs=[prof_pcp_address_snapshot], |
| outputs=_pcp_address_cancel_outputs, |
| ) |
|
|
| prof_care_mode.input( |
| fn=_on_care_mode_change_profile, |
| inputs=[prof_care_mode], |
| outputs=[ |
| prof_caregiver_group, |
| prof_relationship_group, |
| prof_relationship_category, |
| prof_relationship_detail, |
| prof_relationship_custom, |
| prof_identity_grid, |
| prof_side_a_header, |
| prof_side_b_header, |
| prof_add_emergency_btn, |
| prof_side_b_col, |
| ], |
| ) |
|
|
| |
| prof_relationship_category.input( |
| fn=_on_relationship_category_change, |
| inputs=[prof_relationship_category], |
| outputs=[prof_relationship_detail, prof_relationship_custom], |
| ) |
|
|
| |
| prof_contact_add_btn.click( |
| fn=lambda: ( |
| gr.update(visible=True), |
| gr.update(visible=True), |
| ), |
| inputs=[], |
| outputs=[prof_contact2_group, prof_contact_remove_btn], |
| ) |
| prof_contact_remove_btn.click( |
| fn=_contact2_remove, |
| inputs=[], |
| outputs=[ |
| prof_contact2_name, prof_contact2_phone, prof_contact2_relationship, |
| prof_contact2_group, prof_contact_remove_btn, |
| prof_contact2_display, prof_contact2_card_view, prof_contact2_edit_view, |
| ], |
| ) |
|
|
| |
| prof_save_identity_btn.click( |
| fn=_save_identity, |
| inputs=[ |
| prof_care_mode, prof_caregiver_name, prof_caregiver_phone, prof_caregiver_email, |
| prof_relationship_category, prof_relationship_detail, prof_relationship_custom, |
| prof_full_name, prof_dob, |
| prof_address_line1, prof_address_line2, prof_address_city, prof_address_state, prof_address_zip, |
| prof_phone_number, prof_email, |
| prof_contact1_name, prof_contact1_phone, prof_contact1_relationship, |
| prof_contact2_name, prof_contact2_phone, prof_contact2_relationship, |
| prof_selector, |
| ], |
| outputs=None, |
| ) |
| prof_save_insurance_btn.click( |
| fn=_save_insurance, |
| inputs=[prof_insurance, prof_policy, prof_group, prof_selector], |
| outputs=None, |
| ) |
| prof_save_pcp_btn.click( |
| fn=_save_pcp, |
| inputs=[ |
| prof_pcp_name, prof_pcp_phone, prof_pcp_clinic, |
| prof_pcp_address_line1, prof_pcp_address_line2, prof_pcp_address_city, |
| prof_pcp_address_state, prof_pcp_address_zip, |
| prof_selector, |
| ], |
| outputs=None, |
| ) |
| |
| prof_add_new_btn.click( |
| fn=_add_new_profile, |
| inputs=[ |
| prof_care_mode, prof_caregiver_name, prof_caregiver_phone, prof_caregiver_email, |
| prof_relationship_category, prof_relationship_detail, prof_relationship_custom, |
| prof_full_name, prof_dob, |
| prof_address_line1, prof_address_line2, prof_address_city, prof_address_state, prof_address_zip, |
| prof_phone_number, prof_email, |
| prof_insurance, prof_policy, prof_group, |
| prof_pcp_name, prof_pcp_phone, prof_pcp_clinic, |
| prof_pcp_address_line1, prof_pcp_address_line2, prof_pcp_address_city, |
| prof_pcp_address_state, prof_pcp_address_zip, |
| prof_contact1_name, prof_contact1_phone, prof_contact1_relationship, |
| prof_contact2_name, prof_contact2_phone, prof_contact2_relationship, |
| ], |
| outputs=None, |
| ).then( |
| fn=_after_profile_created, |
| inputs=None, |
| outputs=_prof_open_view_edit_outputs, |
| ).then( |
| fn=_sync_cards_from_profile_id, |
| inputs=[prof_view_selector], |
| outputs=_card_sync_outputs, |
| ).then( |
| fn=_apply_session_settings_profile, |
| inputs=[prof_view_selector], |
| outputs=_session_apply_settings_outputs, |
| ).then( |
| fn=_welcome_banner_visible, |
| inputs=None, |
| outputs=[prof_welcome_banner], |
| ) |
|
|
| prof_delete_execute_btn.click( |
| fn=_delete_profile, |
| inputs=[prof_delete_selector], |
| outputs=_prof_delete_outputs, |
| ).then( |
| fn=_sync_cards_from_profile_id, |
| inputs=[prof_view_selector], |
| outputs=_card_sync_outputs, |
| ).then( |
| fn=_apply_session_settings_profile, |
| inputs=[prof_selector], |
| outputs=_session_apply_settings_outputs, |
| ).then( |
| fn=_welcome_banner_visible, |
| inputs=None, |
| outputs=[prof_welcome_banner], |
| ) |
|
|
| |
| gr.Markdown("---") |
| docs_index_panel_open = gr.State(False) |
| with gr.Group( |
| elem_id="settings-documents-shell", |
| elem_classes=["profile-field-card", "settings-documents-shell"], |
| ): |
| gr.Markdown( |
| "### π Profile Documents", |
| elem_classes=["field-card-title"], |
| ) |
| gr.Markdown( |
| "*Upload PDFs and text files for the **signed-in active care profile**. " |
| "The Home assistant uses these when answering questions.*", |
| elem_classes=["section-lead"], |
| ) |
| with gr.Row(elem_classes=["settings-docs-upload-row"]): |
| with gr.Column( |
| scale=3, |
| min_width=0, |
| elem_classes=["settings-docs-upload-cell"], |
| ): |
| pdf_upload = gr.File( |
| label="Upload Documents", |
| file_types=[".pdf", ".txt", ".md", ".csv"], |
| file_count="multiple", |
| elem_id="pdf-upload", |
| ) |
| with gr.Column( |
| scale=1, |
| min_width=0, |
| elem_classes=["settings-docs-upload-actions"], |
| ): |
| upload_btn = gr.Button( |
| "π₯ Upload Documents", |
| variant="primary", |
| size="sm", |
| ) |
| refresh_docs_btn = gr.Button( |
| "π Refresh Index", |
| variant="secondary", |
| size="sm", |
| ) |
| docs_view_index_btn = gr.Button( |
| "π View Indexed Documents", |
| variant="secondary", |
| size="sm", |
| elem_classes=["settings-docs-view-index-btn"], |
| ) |
| with gr.Group( |
| visible=False, |
| elem_classes=["settings-docs-index-panel"], |
| ) as docs_index_panel: |
| docs_index_html = gr.HTML( |
| value=_docs_index_html(), |
| ) |
| doc_delete_name = gr.Textbox( |
| visible=False, |
| elem_id="doc-delete-name", |
| ) |
| doc_delete_trigger = gr.Button( |
| "Delete", |
| visible=False, |
| elem_id="docs-delete-trigger", |
| ) |
| docs_toast_sink = gr.Textbox(visible=False) |
|
|
| upload_btn.click( |
| _docs_upload_toast, |
| inputs=[pdf_upload, active_profile_id], |
| outputs=[docs_toast_sink, docs_index_html, pdf_upload], |
| ) |
| refresh_docs_btn.click( |
| _docs_refresh_index, |
| inputs=[active_profile_id], |
| outputs=[docs_index_html], |
| ) |
| docs_view_index_btn.click( |
| _docs_toggle_index_panel, |
| inputs=[docs_index_panel_open, active_profile_id], |
| outputs=[ |
| docs_index_panel, docs_index_panel_open, |
| docs_view_index_btn, docs_index_html, |
| ], |
| ) |
| doc_delete_trigger.click( |
| _docs_delete_toast, |
| inputs=[doc_delete_name, active_profile_id], |
| outputs=[docs_toast_sink, docs_index_html], |
| ) |
|
|
| def _settings_tab_refresh(profile_id): |
| return ( |
| _init_settings_profile_tab(), |
| gr.update(value=_docs_index_html(profile_id)), |
| ) |
|
|
| settings_tab.select( |
| fn=_settings_tab_refresh, |
| inputs=[active_profile_id], |
| outputs=[prof_welcome_banner, docs_index_html], |
| ) |
|
|
| |
| gr.Markdown("---") |
| gr.Markdown("### Data Integrations & Imports") |
|
|
| with gr.Accordion("π Secure Portal Integrations (Privacy-First)", open=False): |
| gr.Markdown( |
| "**β οΈ Future feature β not active in this build.**\n\n" |
| "Direct portal sync (Epic MyChart, Cerner HealtheIntent / Oracle Health, " |
| "VA My HealtheVet) requires a healthcare-organization partnership, BAA, " |
| "and API access granted by the portal vendor. This build does not " |
| "hold any of those credentials.\n\n" |
| "**What's actually available right now:** upload clinical summaries, " |
| "lab reports, and C-CDA documents as PDF or text files under " |
| "**π Profile Documents** in Settings & Profiles β the Home " |
| "assistant will use them for context.\n\n" |
| "When this feature ships, the consent toggle below will gate every " |
| "connection attempt and the OAuth flow will run client-side only β " |
| "no password ever leaves your browser, and any refresh tokens will " |
| "stay encrypted in the local app data folder." |
| ) |
|
|
| portal_provider = gr.Radio( |
| choices=["Epic MyChart", "Cerner HealtheIntent (Oracle Health)", "VA My HealtheVet", "Manual Upload Only"], |
| value="Manual Upload Only", |
| label="Select Provider Portal", |
| ) |
|
|
| portal_consent = gr.Checkbox( |
| label=("I understand portal sync is a future feature, and I authorize " |
| "this assistant to connect to my selected provider *when* this " |
| "feature is available in a future release."), |
| value=False, |
| ) |
|
|
| portal_sync_btn = gr.Button( |
| "π Authenticate & Sync Portal Data", |
| variant="secondary", |
| interactive=False, |
| ) |
|
|
| def _on_consent_change(consent: bool): |
| return gr.update(interactive=bool(consent)) |
|
|
| portal_consent.change( |
| fn=_on_consent_change, |
| inputs=[portal_consent], |
| outputs=[portal_sync_btn], |
| ) |
|
|
| def _on_portal_sync_click(provider: str): |
| return gr.Info( |
| f"π Portal sync for **{provider}** is a future feature. " |
| f"For now, upload clinical summaries as PDF under " |
| f"**π Profile Documents** in Settings & Profiles." |
| ) |
|
|
| portal_sync_btn.click( |
| fn=_on_portal_sync_click, |
| inputs=[portal_provider], |
| outputs=None, |
| ) |
|
|
| |
| gr.Markdown("---") |
| with gr.Accordion("βοΈ Advanced β AI Model", open=False): |
| gr.Markdown( |
| "Download **Qwen 2.5 14B Q4_K_M** from " |
| "[HuggingFace](https://huggingface.co/Qwen/Qwen2.5-14B-Instruct-GGUF) " |
| "and place in the `models/` directory. " |
| "If `pip install llama-cpp-python` fails on Windows, use a pre-built wheel from " |
| "[llama-cpp-python Releases](https://github.com/abetlen/llama-cpp-python/releases)." |
| ) |
| s_model_path = gr.Textbox( |
| label="Model Path", |
| placeholder="models/qwen-2.5-14b-q4_k_m.gguf", |
| ) |
| save_settings_btn = gr.Button( |
| "πΎ Save Model Path", variant="primary", size="sm", |
| ) |
|
|
| _home_chat_outputs = [ |
| chat_input, chatbot, home_chat_state, home_brief_md, |
| med_table, med_del_id, med_summary_md, |
| appt_table, appt_edit_select, appt_completed_table, today_md, |
| appt_summary_md, |
| ] |
| _home_chat_inputs = [ |
| chat_input, chatbot, active_profile_id, home_chat_state, |
| med_filter, appt_include_past, |
| ] |
|
|
| send_btn.click( |
| fn=handle_home_chat, |
| inputs=_home_chat_inputs, |
| outputs=_home_chat_outputs, |
| ) |
| chat_input.submit( |
| fn=handle_home_chat, |
| inputs=_home_chat_inputs, |
| outputs=_home_chat_outputs, |
| ) |
| clear_btn.click( |
| fn=_home_clear_chat, |
| inputs=None, |
| outputs=[chatbot, chat_input, home_chat_state], |
| ) |
| for chip_btn, chip_msg in zip(home_example_btns, HOME_EXAMPLE_CHIPS): |
| chip_btn.click( |
| fn=lambda h, pid, cs, mf, ap, m=chip_msg: handle_home_chat( |
| m, h, pid, cs, mf, ap, |
| ), |
| inputs=[ |
| chatbot, active_profile_id, home_chat_state, |
| med_filter, appt_include_past, |
| ], |
| outputs=_home_chat_outputs, |
| ) |
|
|
| demo.load( |
| fn=_init_active_session, |
| inputs=None, |
| outputs=[ |
| active_profile_id, session_banner, *_session_gate_outputs, |
| main_tabs, session_auth_btn, |
| ], |
| ).then( |
| fn=_restore_profile_workspace_on_load_ui, |
| inputs=[active_profile_id], |
| outputs=_prof_open_view_edit_outputs, |
| ).then( |
| fn=_init_settings_profile_tab, |
| inputs=None, |
| outputs=[prof_welcome_banner], |
| ).then( |
| fn=_sync_cards_from_profile_id, |
| inputs=[active_profile_id], |
| outputs=_card_sync_outputs, |
| ).then( |
| fn=_visit_clinic_sync_updates, |
| inputs=[active_profile_id], |
| outputs=[ |
| visit_clinic_select, |
| visit_destination_display, |
| visit_clinic_custom, |
| visit_panel_destination, |
| visit_dest_panel_open, |
| visit_brief_header_md, |
| visit_panel_purpose, |
| visit_purpose_panel_open, |
| visit_panel_symptoms, |
| visit_symptoms_panel_open, |
| visit_panel_questions, |
| visit_questions_panel_open, |
| visit_profile_symptom_pick, |
| visit_chip_destination_btn, |
| visit_chip_purpose_btn, |
| visit_chip_symptoms_btn, |
| visit_chip_questions_btn, |
| ], |
| ).then( |
| load_settings, |
| inputs=None, |
| outputs=[s_model_path], |
| ).then( |
| fn=_home_tab_on_select, |
| inputs=[active_profile_id, chatbot, home_chat_state], |
| outputs=[home_brief_md, chatbot, home_chat_state], |
| ) |
|
|
| save_settings_btn.click( |
| save_settings, |
| inputs=[s_model_path], |
| outputs=gr.Textbox(visible=False), |
| ) |
|
|
| |
| gr.HTML(""" |
| <div style=" |
| text-align:center; padding:16px; color:#7a9aaa; |
| font-size:12px; border-top:1px solid #c8dfe8; margin-top:20px; |
| font-family:Inter,sans-serif; |
| "> |
| Built for the <strong>Build Small Hackathon β Backyard AI Track</strong> | |
| π All data stays on your device | |
| Powered by Qwen 2.5 14B + FAISS + Gradio |
| </div> |
| """) |
|
|
| |
|
|
| if __name__ == "__main__": |
| print("\n" + "=" * 60) |
| print(" AidAiLine") |
| print(" Build Small Hackathon β Backyard AI Track") |
| print("=" * 60) |
| print(f"\n Data directory: {cfg.DATA_DIR}") |
| print(f" Model directory: {cfg.MODELS_DIR}") |
| print(f" Cache directory: {cfg.CACHE_DIR}") |
|
|
| rag = _safe_import_rag() |
| if rag: |
| status = rag.get_document_status() |
| if status["model_found"]: |
| print(f"\n β
Model found: {Path(status['model_path']).name}") |
| else: |
| print(f"\n β οΈ Model not found at: {status['model_path']}") |
| print(" Download Qwen 2.5 14B Q4_K_M from:") |
| print(" https://huggingface.co/Qwen/Qwen2.5-14B-Instruct-GGUF") |
| else: |
| print("\n β οΈ Some AI dependencies are missing.") |
| print(" Run: pip install -r requirements.txt") |
|
|
| print("\n Starting Gradio serverβ¦\n") |
|
|
| demo.launch( |
| server_name="0.0.0.0", |
| server_port=7860, |
| inbrowser=True, |
| show_error=True, |
| favicon_path=None, |
| ) |
|
|