Spaces:
Sleeping
Sleeping
| # -*- coding: utf-8 -*- | |
| """ | |
| 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 | |
| # ββ Internal modules ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 | |
| # RAG is imported lazily in handlers to survive missing deps at startup | |
| def _safe_import_rag(): | |
| try: | |
| import rag_engine | |
| return rag_engine | |
| except ImportError as e: | |
| return None | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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; | |
| } | |
| """ # style.css disabled β was causing invisible text on Windows. Inline rules only. | |
| _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() | |
| # Unix timestamp as string | |
| if val.isdigit() or (val.startswith("-") and val[1:].isdigit()): | |
| return datetime.fromtimestamp(int(val)).strftime("%Y-%m-%d") | |
| # ISO string β just take the date portion | |
| return val[:10] | |
| # datetime or date object | |
| 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 "" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Tab 1: Documents | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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) | |
| # Append source citations | |
| 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}" | |
| ) | |
| # ββ Profile Documents (Settings tab) βββββββββββββββββββββββββββββββββββββββββ | |
| 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"], # hidden for selection | |
| ]) | |
| 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 | |
| # ββ Edit Entry helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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, # move_cat reset | |
| ) | |
| internal = _to_internal(category) | |
| return ( | |
| gr.update(choices=_food_choices(internal, profile_id), value=None), | |
| "", # clear name | |
| "", # clear notes | |
| gr.update(visible=(internal == "allergies")), | |
| "mild", # reset severity | |
| None, # move_cat reset | |
| ) | |
| 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, # pre-select move category to current 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 visibility follows the DESTINATION category | |
| severity_vis = gr.update(visible=(move_internal == "allergies")) | |
| # ββ Validation (before any data mutation) ββ | |
| 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, | |
| ) | |
| # ββ Execute the data operation: move or same-category edit ββ | |
| 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) | |
| # ββ AFTER mutation: re-compute ALL rows and refresh ALL dropdowns ββ | |
| 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)), | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Tab 3: Appointments | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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) | |
| # Tag completed items so the status column shows β | |
| 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") | |
| # Show β for completed injections, normal status for active | |
| 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)), | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Tab 4: Food Chart | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Maps UI display names (with emojis) β internal category keys | |
| _CATEGORY_MAP = { | |
| "β οΈ Allergies": "allergies", | |
| "β Preferred Foods": "liked_foods", | |
| "β Food Aversions": "disliked_foods", | |
| } | |
| _CATEGORY_CHOICES = list(_CATEGORY_MAP.keys()) | |
| # Reverse: internal key β display name | |
| _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) | |
| # ββ Date helpers for appointment date dropdowns ββββββββββββββββββββββββββββββ | |
| _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=[]), | |
| "", "", | |
| ) | |
| # Cross-category conflict check | |
| 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) | |
| # Shared refreshes | |
| 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)), | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Tab 5: Doctor Visit | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _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, | |
| ): | |
| # Resolve Reason: if "Other" selected, use the free-text value | |
| resolved_reason = (reason_other or "").strip() if reason == "Other" else (reason or "") | |
| # Care Mode and Caregiver Name are now read from the profile | |
| # (moved off the appointment form β they belong to the patient, not the visit) | |
| 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], # load into input | |
| idx, | |
| gr.update(choices=q_choices, value=None), | |
| gr.update(choices=q_choices, value=idx), | |
| gr.update(visible=False), # add btn hidden | |
| gr.update(visible=True), # cancel btn visible | |
| _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)) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Export (Doctor Visit tab β bottom panel) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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>' | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Tab 7: Settings | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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) | |
| # Reset RAG engine so new model path is picked up | |
| 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 dropdown state βββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Map each top-tier category to its sub-choice list. | |
| _RELATIONSHIP_DETAIL_CHOICES = { | |
| "Parent": ["Mom", "Dad", "Guardian"], | |
| "Sibling": ["Brother", "Sister"], | |
| # "Self", "Child", "Grandchild", "Care Provider", "Other" β no sub-dropdown | |
| } | |
| # Reverse-map a stored label string back to (category, detail, custom) so we | |
| # can re-populate the cascading dropdowns when a profile is loaded. | |
| _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]), # pre-select first valid choice | |
| 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) | |
| # Number of form fields in the profile manager (used by the load/clear/delete | |
| # handlers). Layout order: | |
| # 1-2 : care_mode / caregiver_name (NEW β moved from appointment) | |
| # 3-5 : relationship category / detail / custom | |
| # 6-10 : full_name / dob / street_address / phone_number / email | |
| # 11-13 : insurance_provider / policy_id / group_id | |
| # 14-17 : pcp_name / pcp_phone / pcp_clinic / pcp_address | |
| # 18-23 : contact1 (name/phone/relationship) + contact2 (name/phone/relationship) | |
| _PROFILE_FIELD_COUNT = 23 | |
| # Valid defaults for hidden relationship-detail dropdown (must stay in choices list). | |
| _RELATIONSHIP_DETAIL_DEFAULT = "Mom" | |
| _RELATIONSHIP_CATEGORY_CHOICES = [ | |
| "Parent", "Child", "Sibling", "Grandchild", "Care Provider", "Other", | |
| ] | |
| _RELATIONSHIP_DEFAULT_CATEGORY = "Parent" | |
| # Sentinel value for the unified profile dropdown's "Create New Profile" option. | |
| _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 | |
| # ββ Active session (one profile at a time) ββββββββββββββββββββββββββββββββββββ | |
| _HOME_TAB_INDEX = 0 | |
| _SETTINGS_TAB_INDEX = 5 # βοΈ Settings & Profile (0-based tab index) | |
| 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"), # 1 care_mode | |
| gr.update(value=""), # 2 caregiver_name | |
| gr.update(value=""), # 3 caregiver_phone | |
| gr.update(value=""), # 4 caregiver_email | |
| gr.update(value=_RELATIONSHIP_DEFAULT_CATEGORY), # 5 relationship_category | |
| gr.update(visible=False, value=_RELATIONSHIP_DETAIL_DEFAULT), # 6 relationship_detail | |
| gr.update(visible=False, value=""), # 7 relationship_custom | |
| gr.update(value=""), # 8 full_name | |
| gr.update(value=""), # 9 dob | |
| gr.update(value=""), # 10 address_line1 | |
| gr.update(value=""), # 11 address_line2 | |
| gr.update(value=""), # 12 address_city | |
| gr.update(value=""), # 13 address_state | |
| gr.update(value=""), # 14 address_zip | |
| gr.update(value=""), # 15 phone_number | |
| gr.update(value=""), # 16 email | |
| gr.update(value=""), # 17 insurance | |
| gr.update(value=""), # 18 policy | |
| gr.update(value=""), # 19 group | |
| gr.update(value=""), # 20 pcp_name | |
| gr.update(value=""), # 21 pcp_phone | |
| gr.update(value=""), # 22 pcp_clinic | |
| gr.update(value=""), # 23 pcp_address_line1 | |
| gr.update(value=""), # 24 pcp_address_line2 | |
| gr.update(value=""), # 25 pcp_address_city | |
| gr.update(value=""), # 26 pcp_address_state | |
| gr.update(value=""), # 27 pcp_address_zip | |
| gr.update(value=""), # 28 contact1_name | |
| gr.update(value=""), # 29 contact1_phone | |
| gr.update(value=""), # 30 contact1_relationship | |
| gr.update(value=""), # 31 contact2_name | |
| gr.update(value=""), # 32 contact2_phone | |
| gr.update(value=""), # 33 contact2_relationship | |
| ] | |
| 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 (with backward-compat default for profiles saved before this field existed) | |
| 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) | |
| # Resolve up to 2 emergency contacts (from new list or legacy single fields) | |
| 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), # 1 care_mode | |
| gr.update(value=p.get("caregiver_name", "")), # 2 caregiver_name | |
| gr.update(value=_format_us_phone(p.get("caregiver_phone", ""))), # 3 caregiver_phone | |
| gr.update(value=p.get("caregiver_email", "")), # 4 caregiver_email | |
| gr.update(value=category, visible=managing_other), # 5 relationship_category | |
| detail_update, # 6 relationship_detail | |
| custom_update, # 7 relationship_custom | |
| gr.update(value=p.get("full_name", "")), # 8 | |
| gr.update(value=p.get("dob", "")), # 9 | |
| gr.update(value=addr1), # 10 address_line1 | |
| gr.update(value=addr2), # 11 address_line2 | |
| gr.update(value=addr_city), # 12 address_city | |
| gr.update(value=addr_state), # 13 address_state | |
| gr.update(value=addr_zip), # 14 address_zip | |
| gr.update(value=_format_us_phone(p.get("phone_number", ""))), # 15 | |
| gr.update(value=p.get("email", "")), # 16 | |
| gr.update(value=p.get("insurance_provider", "")), # 17 | |
| gr.update(value=p.get("policy_id", "")), # 18 | |
| gr.update(value=p.get("group_id", "")), # 19 | |
| gr.update(value=p.get("pcp_name", "")), # 20 | |
| gr.update(value=_format_us_phone(p.get("pcp_phone", ""))), # 21 | |
| gr.update(value=p.get("pcp_clinic", "")), # 22 | |
| gr.update(value=pcp1), # 23 pcp_address_line1 | |
| gr.update(value=pcp2), # 24 pcp_address_line2 | |
| gr.update(value=pcp_city), # 25 pcp_address_city | |
| gr.update(value=pcp_state), # 26 pcp_address_state | |
| gr.update(value=pcp_zip), # 27 pcp_address_zip | |
| gr.update(value=c1.get("name", "")), # 28 | |
| gr.update(value=_format_us_phone(c1.get("phone", ""))), # 29 | |
| gr.update(value=c1.get("relationship", "")), # 30 | |
| gr.update(value=c2.get("name", "")), # 31 | |
| gr.update(value=_format_us_phone(c2.get("phone", ""))), # 32 | |
| gr.update(value=c2.get("relationship", "")), # 33 | |
| ] + 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 | |
| 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") | |
| # Index | |
| 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") | |
| # Data files | |
| 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) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Build the Gradio UI | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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: | |
| # ββ Top banner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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: | |
| # ββ Tab 1: Home (AI Assistant) βββββββββββββββββββββββββββββββββββββββ | |
| 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], | |
| ) | |
| # ββ Tab 2: Medications βββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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, | |
| ], | |
| ) | |
| # ββ Tab 3: Diet & Allergies ββββββββββββββββββββββββββββββββββββββββββββ | |
| 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, | |
| ], | |
| ) | |
| # ββ Tab 4: Appointments ββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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, | |
| ], | |
| ) | |
| # ββ Tab 5: Doctors Visit βββββββββββββββββββββββββββββββββββββββββββββ | |
| 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", | |
| ) | |
| # Pre-Visit Brief Settings β below grid, above Generate | |
| 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, | |
| ) | |
| # ββ Action buttons βββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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> | |
| """) | |
| # ββ Output panel ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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", | |
| ) | |
| # ββ Export & Share (below brief preview) βββββββββββββββββββββββββ | |
| 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, | |
| ) | |
| # ββ Wiring ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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], | |
| ) | |
| # Reason for Visit β show/hide "Other" textbox + refresh summary | |
| 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 / Clear | |
| 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, | |
| ) | |
| # Questions list β Add (also handles Update) / Delete / Edit / Cancel / Clear | |
| 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, | |
| ) | |
| # ββ Active session wiring ββββββββββββββββββββββββββββββββββββββββββββ | |
| _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], | |
| ) | |
| # ββ Tab 6: Settings & Profile ββββββββββββββββββββββββββββββββββββββββ | |
| 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"], | |
| ) | |
| # ββ New-user welcome banner βββββββββββββββββββββββββββββββββββ | |
| _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") | |
| # ββ Selection command bar βββββββββββββββββββββββββββββββββββ | |
| _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", | |
| ) | |
| # ββ Workspace: View / Edit ββββββββββββββββββββββββββββββββββββ | |
| 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="", | |
| ) | |
| # ββ Workspace: Delete βββββββββββββββββββββββββββββββββββββββββββ | |
| 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="", | |
| ) | |
| # ββ Workspace: Create (intro chrome) ββββββββββββββββββββββββββββ | |
| 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"], | |
| ) | |
| # Hidden canonical profile id for save handlers | |
| 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, | |
| ) | |
| # ββ Shared profile form (Create + View/Edit) ββββββββββββββββββββ | |
| 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"], | |
| ) | |
| # ββ Profile event wiring βββββββββββββββββββββββββββββββββββββββββ | |
| _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, | |
| ], | |
| ) | |
| # Relationship category β reveal detail/custom (user action only) β reveal detail/custom (user action only) | |
| prof_relationship_category.input( | |
| fn=_on_relationship_category_change, | |
| inputs=[prof_relationship_category], | |
| outputs=[prof_relationship_detail, prof_relationship_custom], | |
| ) | |
| # Contact 2 reveal / remove | |
| 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, | |
| ], | |
| ) | |
| # ββ Per-section save handlers (edit existing profile) βββββββββββββ | |
| 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, | |
| ) | |
| # ββ Add New Profile: one-button create from all fields βββββββββββ | |
| 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], | |
| ) | |
| # ββ Profile Documents (active care profile) βββββββββββββββββββββ | |
| 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], | |
| ) | |
| # ββ Data Integrations & Imports βββββββββββββββββββββββββββββββββββ | |
| 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, | |
| ) | |
| # ββ AI Model β last section, collapsed by default βββββββββββββββββ | |
| 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), | |
| ) | |
| # ββ Footer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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> | |
| """) | |
| # ββ Launch ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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, | |
| ) | |