# -*- 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,
)