# -*- 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 = """ """ 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 '

πŸ“‚ No documents indexed yet.

' rows = [] for source in sources: esc = html.escape(source, quote=True) label = html.escape(source) rows.append( f'
' f'{label}' f'' f"
" ) return f'
{"".join(rows)}
' 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 ( "

" "Sign in with an active care profile to see today's health brief." "

" ) 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"
{line}
" for line in appt_lines ) med_html = "".join( f"
{line}
" for line in med_lines ) return f"""
{name} β€’ {today_label}
πŸ“… Upcoming Appointments
{appt_html}
πŸ’Š Active Medications
{med_html}
""" _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'πŸ“§ Open in Email Client' # ══════════════════════════════════════════════════════════════════════════════ # 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'
' f'
{title}
' f'
πŸ”’
' f'
Locked
' f'
' f'No Active Care Profile Selected
' f'
{sub}
' f'
' ) 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("""

πŸ₯ AidAiLine

AI-powered medical document assistant for caregivers

🟒 Offline Mode β€” No data leaves this computer
""") 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( '
πŸ’¬ AidAi-Line Chat Assistant
' ) 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(""" """) # ── 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(""" """) 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("""
Built for the Build Small Hackathon β€” Backyard AI Track  |  πŸ”’ All data stays on your device  |  Powered by Qwen 2.5 14B + FAISS + Gradio
""") # ── 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, )