"""Home tab — greeting, checklist, metrics, and daily check-in.""" from datetime import date, datetime import gradio as gr from db import queries from vital_types.db import DailyLogSchemaField, MedicationLogEntry def _period_label(now: datetime) -> str: """Return morning, afternoon, or evening for the current hour.""" if now.hour < 12: return "morning" if now.hour < 17: return "afternoon" return "evening" def _greeting_text() -> str: """Build the dashboard greeting line.""" profile = queries.get_profile() name = profile.name if profile and profile.name.strip() else "there" today = date.today().strftime("%A, %B %d, %Y") period = _period_label(datetime.now()) return f"## Good {period}, {name}\n{today}" def _briefing_text() -> str: """Return the cached morning briefing or a short fallback.""" cached = queries.get_morning_briefing_cache() if cached: return cached profile = queries.get_profile() if profile is None: return "Your coach briefing will appear here after the morning greeting." return ( f"Welcome back, {profile.name}. Use today's check-in below to log how you're feeling." ) def _metrics_markdown() -> str: """Build the three-column metrics summary as markdown.""" today = date.today() logs = {entry.field_id: entry.value for entry in queries.get_daily_logs_for_date(today)} medications = queries.get_medications_for_date(today) taken_count = sum(1 for med in medications if med.taken) total_meds = len(medications) primary_metric = "—" schema = queries.get_daily_log_schema() if schema: first_field = schema[0] primary_metric = logs.get(first_field.field_id, "—") water_value = logs.get("water_cups", "0") return ( f"| Health metric | Water (cups) | Meds taken |\n" f"|---|---|---|\n" f"| **{primary_metric}** | **{water_value}** | **{taken_count}/{total_meds}** |" ) def _schedule_markdown() -> str: """Build today's LLM-generated schedule summary.""" plan = queries.get_daily_plan(date.today()) if plan is None: return "_Today's schedule will appear after your wake time._" lines = [f"_{plan.summary}_\n"] for job in plan.jobs: extra = "" if job.volume_ml: extra = f" ({job.volume_ml}ml)" if job.exercise_type: extra = f" — {job.exercise_type}, {job.duration_minutes} min" lines.append(f"- **{job.time}** [{job.type}] {job.message}{extra}") return "\n".join(lines) def _medication_choice_label(medication: MedicationLogEntry) -> str: """Format one medication row for the interactive checklist.""" return ( f"{medication.scheduled_time} — " f"{medication.medication_name} ({medication.dose})" ) def empty_medication_checklist_update() -> dict[str, object]: """Return a safe CheckboxGroup update when the dashboard is hidden.""" return { "choices": [], "value": [], "label": "Medications today", "info": "", } def medication_checklist_update() -> dict[str, object]: """Build a gr.update payload for today's medication checklist.""" medications = queries.get_medications_for_date(date.today()) if not medications: return { "choices": [], "value": [], "label": "Medications today", "info": "No medications scheduled today.", } choices = [_medication_choice_label(med) for med in medications] selected = [ label for med, label in zip(medications, choices, strict=True) if med.taken ] return { "choices": choices, "value": selected, "label": "Medications today", "info": "Check each dose when you take it.", } def get_dashboard_updates() -> tuple[str, str, str, dict[str, object]]: """Return fresh dashboard content from the database.""" return ( _greeting_text(), _briefing_text(), _metrics_markdown(), medication_checklist_update(), ) def _render_checkin_field(field: DailyLogSchemaField) -> gr.components.Component: """Render one dynamic check-in field from schema.""" if field.type == "scale_1_10": return gr.Slider( minimum=1, maximum=10, step=1, value=5, label=field.label, info=field.reason, ) if field.type == "number": return gr.Number(label=field.label, value=0, precision=0, info=field.reason) if field.type == "select": return gr.Radio( choices=field.options, label=field.label, info=field.reason, ) if field.type == "boolean": return gr.Checkbox(label=field.label, value=False, info=field.reason) return gr.Textbox(label=field.label, info=field.reason) def build_dashboard_tab(initial_onboarded: bool = False) -> dict[str, object]: """Build the home dashboard tab components.""" greeting_text, briefing_text, metrics_text, checklist_payload = ( get_dashboard_updates() if initial_onboarded else ("", "", "", {}) ) greeting = gr.Markdown(greeting_text) briefing = gr.Markdown(briefing_text, elem_classes=["coach-bubble"]) gr.Markdown("### Today's schedule") schedule = gr.Markdown(_schedule_markdown() if initial_onboarded else "") metrics = gr.Markdown(metrics_text) gr.Markdown("### Medication checklist") med_checklist = gr.CheckboxGroup( choices=checklist_payload.get("choices", []), value=checklist_payload.get("value", []), label=str(checklist_payload.get("label", "Medications today")), info=str(checklist_payload.get("info", "")), ) med_status = gr.Markdown("") gr.Markdown("### Daily check-in") schema = queries.get_daily_log_schema() if initial_onboarded else [] checkin_fields: list[gr.components.Component] = [] checkin_field_ids: list[str] = [] with gr.Row(): for field in schema: checkin_fields.append(_render_checkin_field(field)) checkin_field_ids.append(field.field_id) save_status = gr.Markdown("") save_btn = gr.Button("Save check-in", variant="primary") refresh_btn = gr.Button("Refresh") def sync_medications(selected_labels: list[str]) -> tuple[str, str]: """Persist medication checklist checkbox changes to the database.""" today = date.today() medications = queries.get_medications_for_date(today) selected_set = set(selected_labels) for medication in medications: label = _medication_choice_label(medication) should_be_taken = label in selected_set if medication.taken != should_be_taken: queries.set_medication_taken_status( today, medication.medication_name, medication.scheduled_time, should_be_taken, ) return _metrics_markdown(), "**Medication checklist updated.**" def save_checkin(*values: object) -> str: """Persist dynamic check-in values to the database.""" today = date.today() for index, field_id in enumerate(checkin_field_ids): if index >= len(values): continue raw_value = values[index] if isinstance(raw_value, bool): text_value = "true" if raw_value else "false" else: text_value = str(raw_value) queries.upsert_daily_log(today, field_id, text_value) return "**Check-in saved.**" def refresh_dashboard() -> tuple: """Reload dashboard summaries from the database.""" greeting_value, briefing_value, metrics_value, checklist_payload = ( get_dashboard_updates() ) return ( gr.update(value=greeting_value), gr.update(value=briefing_value), gr.update(value=_schedule_markdown()), gr.update(value=metrics_value), gr.update(**checklist_payload), "", ) med_checklist.change( sync_medications, inputs=[med_checklist], outputs=[metrics, med_status], ) if checkin_fields: save_btn.click( save_checkin, inputs=checkin_fields, outputs=[save_status], ) refresh_btn.click( refresh_dashboard, outputs=[greeting, briefing, schedule, metrics, med_checklist, med_status], ) return { "greeting": greeting, "briefing": briefing, "schedule": schedule, "metrics": metrics, "checklist": med_checklist, "refresh_dashboard": refresh_dashboard, }