""" Orbura — Gradio App (dark "off-brand" edition) Quantifies physiological debt from lifestyle stressors and provides AI-backed recovery prescriptions using a local small model. """ from __future__ import annotations import html import time from datetime import datetime import gradio as gr import numpy as np from scoring import ( Stressor, compute_live_score, compute_system_scores, compute_counterfactual, STRESSOR_DEFS, SYSTEM_META, ) from face_scan import run_face_scan, features_to_array from stress_model import predict_stress_score from health_coach import stream_advice, stream_plan, _fallback_advice, _fallback_plan # ─── Design tokens (mirrors src/lib/design-tokens.ts) ──────────────────────── BRAND_PRIMARY = "#EA580C" BRAND_SECONDARY = "#F59E0B" RECOVERY_GREEN = "#4ADE80" # Light mode tokens LT_BG_BASE = "#FAFAF9" LT_BG_SURFACE = "#FFFFFF" LT_BG_ELEVATED = "#F5F5F4" LT_BORDER = "rgba(0, 0, 0, 0.08)" LT_BORDER_SOFT = "rgba(0, 0, 0, 0.04)" LT_TEXT_PRIMARY = "#1C1917" LT_TEXT_SECONDARY = "#57534E" LT_TEXT_MUTED = "#78716C" LT_TEXT_FAINT = "#A8A29E" SYSTEM_ACCENTS = { "cardiovascular": ("#F43F5E", "rgba(244, 63, 94, 0.18)", "rgba(244, 63, 94, 0.40)"), "brain": ("#22D3EE", "rgba(34, 211, 238, 0.18)", "rgba(34, 211, 238, 0.40)"), "liver": ("#EAB308", "rgba(234, 179, 8, 0.18)", "rgba(234, 179, 8, 0.40)"), "muscular": ("#A78BFA", "rgba(167, 139, 250, 0.18)","rgba(167, 139, 250, 0.40)"), "gut": ("#2DD4BF", "rgba(45, 212, 191, 0.18)", "rgba(45, 212, 191, 0.40)"), } SYSTEM_GLYPHS = { "cardiovascular": "C", "brain": "N", "liver": "L", "muscular": "M", "gut": "G", } DEBT_TIERS = [ (0, 20, "#4ADE80", "You're clear. Minimal debt.", "low"), (20, 40, "#F59E0B", "Low debt. Minor adjustments needed.", "low"), (40, 60, "#EA580C", "Moderate debt. Recovery recommended.","moderate"), (60, 80, "#DC2626", "High debt. Prioritize recovery.", "high"), (80, 101,"#991B1B", "Critical debt. Full rest mode.", "critical"), ] TIME_OPTIONS = [] for h in range(12): for m in ["00", "30"]: if h == 0: TIME_OPTIONS.append(f"12:{m} AM") else: TIME_OPTIONS.append(f"{h}:{m} AM") for h in range(12): for m in ["00", "30"]: if h == 0: TIME_OPTIONS.append(f"12:{m} PM") else: TIME_OPTIONS.append(f"{h}:{m} PM") WINDOW_COLORS = { "RIGHT NOW": "#DC2626", "THIS MORNING": "#EA580C", "TODAY": "#F59E0B", "AVOID": "#A78BFA", } def debt_tier(score: int) -> tuple[str, str, str]: for lo, hi, color, verdict, _ in DEBT_TIERS: if lo <= score < hi: return color, verdict, DEBT_TIERS[DEBT_TIERS.index((lo, hi, color, verdict, _))][4] return "#4ADE80", "You're clear. Minimal debt.", "low" APP_THEME = gr.themes.Soft( primary_hue="orange", secondary_hue="amber", neutral_hue="stone", font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"], ).set( body_background_fill=LT_BG_BASE, body_background_fill_dark=LT_BG_BASE, body_text_color=LT_TEXT_PRIMARY, body_text_color_dark=LT_TEXT_PRIMARY, body_text_color_subdued=LT_TEXT_SECONDARY, body_text_color_subdued_dark=LT_TEXT_SECONDARY, block_background_fill=LT_BG_SURFACE, block_background_fill_dark=LT_BG_SURFACE, block_border_color=LT_BORDER, block_border_color_dark=LT_BORDER, block_title_text_color=LT_TEXT_PRIMARY, block_title_text_color_dark=LT_TEXT_PRIMARY, input_background_fill=LT_BG_SURFACE, input_background_fill_dark=LT_BG_SURFACE, input_border_color=LT_BORDER, input_border_color_dark=LT_BORDER, input_placeholder_color=LT_TEXT_MUTED, input_placeholder_color_dark=LT_TEXT_MUTED, button_primary_background_fill=BRAND_PRIMARY, button_primary_background_fill_dark=BRAND_PRIMARY, button_primary_text_color="#FFFFFF", button_primary_text_color_dark="#FFFFFF", button_secondary_background_fill=LT_BG_SURFACE, button_secondary_background_fill_dark=LT_BG_SURFACE, button_secondary_text_color=LT_TEXT_SECONDARY, button_secondary_text_color_dark=LT_TEXT_SECONDARY, ) # ─── Custom CSS (off-brand dark theme) ─────────────────────────────────────── CUSTOM_CSS = f""" @import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600;700&display=swap'); :root {{ --bg-base: {LT_BG_BASE}; --bg-surface: {LT_BG_SURFACE}; --bg-elevated: {LT_BG_ELEVATED}; --border: {LT_BORDER}; --border-soft: {LT_BORDER_SOFT}; --text-primary: {LT_TEXT_PRIMARY}; --text-secondary: {LT_TEXT_SECONDARY}; --text-muted: {LT_TEXT_MUTED}; --text-faint: {LT_TEXT_FAINT}; --brand: {BRAND_PRIMARY}; --brand-secondary: {BRAND_SECONDARY}; --recovery-green: {RECOVERY_GREEN}; --surface-glint: rgba(0, 0, 0, 0.02); --surface-wash: rgba(234, 88, 12, 0.08); --bar-track: rgba(28, 25, 23, 0.10); }} html, body, .gradio-container {{ background: var(--bg-base) !important; color: var(--text-primary) !important; font-family: 'Inter', system-ui, sans-serif !important; }} .gradio-container {{ max-width: 1180px !important; margin: 0 auto !important; padding: 0 24px 60px !important; }} footer {{ display: none !important; }} /* Hide default Gradio chrome we don't need */ .block.padded, .panel, .gap, .form {{ background: transparent !important; border: none !important; }} /* The giant debt score number */ .debt-hero {{ font-family: 'DM Serif Display', Georgia, serif; font-size: clamp(7rem, 22vw, 11rem); font-weight: 400; line-height: 0.9; letter-spacing: -0.04em; margin: 0; text-shadow: 0 0 60px currentColor; transition: color 0.6s ease; animation: heroDrop 0.7s cubic-bezier(0.22, 1, 0.36, 1) backwards; }} .debt-hero-label {{ font-family: 'Inter', sans-serif; font-size: 10px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-secondary); margin-bottom: 8px; animation: fadeUp 0.6s cubic-bezier(0.22, 1, 0.36, 1) 0.05s backwards; }} .debt-verdict {{ font-family: 'Inter', sans-serif; font-size: 14px; font-weight: 500; color: var(--text-secondary); margin-top: 6px; animation: fadeUp 0.6s cubic-bezier(0.22, 1, 0.36, 1) 0.25s backwards; }} /* The breathing orb behind the score */ .orb-wrap {{ position: relative; display: inline-block; padding: 40px 60px 30px; }} .orb-wrap::before {{ content: ''; position: absolute; inset: -30px; background: radial-gradient(circle, currentColor 0%, transparent 65%); opacity: 0.18; border-radius: 50%; animation: orbBreath 4s ease-in-out infinite; z-index: -2; }} .orb-wrap::after {{ content: ''; position: absolute; inset: -50px; background: radial-gradient(circle, currentColor 0%, transparent 70%); opacity: 0.06; border-radius: 50%; animation: orbBreath 4s ease-in-out infinite 0.5s; z-index: -3; filter: blur(8px); }} @keyframes orbBreath {{ 0%, 100% {{ transform: scale(1); }} 50% {{ transform: scale(1.10); }} }} @keyframes heroDrop {{ 0% {{ opacity: 0; transform: scale(0.6) translateY(8px); filter: blur(8px); }} 100% {{ opacity: 1; transform: scale(1) translateY(0); filter: blur(0); }} }} @keyframes fadeUp {{ 0% {{ opacity: 0; transform: translateY(6px); }} 100% {{ opacity: 1; transform: translateY(0); }} }} /* Section labels */ .section-label {{ font-family: 'Inter', sans-serif; font-size: 11px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-secondary); margin: 0 0 12px; display: flex; align-items: center; gap: 10px; }} .section-label::after {{ content: ''; flex: 1; height: 1px; background: var(--border-soft); }} /* System meter */ .sys-meter {{ padding: 10px 14px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px; margin-bottom: 8px; display: flex; align-items: center; gap: 12px; transition: border-color 0.2s, background 0.2s; animation: fadeUp 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards; }} .sys-meter:nth-child(1) {{ animation-delay: 0.05s; }} .sys-meter:nth-child(2) {{ animation-delay: 0.10s; }} .sys-meter:nth-child(3) {{ animation-delay: 0.15s; }} .sys-meter:nth-child(4) {{ animation-delay: 0.20s; }} .sys-meter:nth-child(5) {{ animation-delay: 0.25s; }} .sys-meter.is-primary {{ background: linear-gradient(180deg, var(--surface-glint), transparent); }} .sys-glyph {{ width: 28px; height: 28px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-family: 'JetBrains Mono', monospace; font-size: 12px; font-weight: 700; flex-shrink: 0; }} .sys-body {{ flex: 1; min-width: 0; }} .sys-row {{ display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }} .sys-label {{ font-size: 13px; font-weight: 600; color: var(--text-primary); }} .sys-time {{ font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text-muted); }} .sys-bar {{ margin-top: 8px; height: 3px; background: var(--bar-track); border-radius: 2px; overflow: hidden; }} .sys-bar-fill {{ height: 100%; border-radius: 2px; transition: width 0.7s cubic-bezier(0.22, 1, 0.36, 1); }} .sys-cause {{ font-size: 11px; color: var(--text-muted); margin-top: 6px; line-height: 1.4; }} /* Protocol step */ .proto-step {{ display: flex; gap: 12px; padding: 8px 0; }} .proto-rail {{ display: flex; flex-direction: column; align-items: center; width: 30px; flex-shrink: 0; }} .proto-num {{ width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 700; border: 1px solid currentColor; background: var(--surface-glint); }} .proto-conn {{ flex: 1; width: 1px; background: var(--border); min-height: 18px; margin-top: 4px; }} .proto-window {{ font-family: 'JetBrains Mono', monospace; font-size: 9px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; margin-bottom: 4px; }} .proto-action {{ font-size: 13px; color: var(--text-primary); line-height: 1.55; font-weight: 500; }} /* Science cards */ .sci-card {{ padding: 12px 14px; background: var(--bg-elevated); border-left: 2px solid currentColor; border-radius: 0 8px 8px 0; margin-bottom: 8px; }} .sci-fact {{ font-size: 13px; color: var(--text-secondary); line-height: 1.55; margin: 0 0 4px; }} .sci-cite {{ font-size: 11px; color: var(--text-muted); font-style: italic; margin: 0; font-family: 'JetBrains Mono', monospace; }} /* Face scan pill */ .face-pill {{ padding: 14px 16px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px; display: flex; align-items: center; gap: 16px; margin-bottom: 16px; }} .face-pill-placeholder {{ padding: 20px; background: var(--bg-surface); border: 1.5px dashed var(--border); border-radius: 12px; text-align: center; margin-bottom: 16px; transition: border-color 0.2s; }} .face-pill-placeholder:hover {{ border-color: var(--brand); }} .face-pill-placeholder .icon {{ font-size: 28px; opacity: 0.4; margin-bottom: 6px; }} .face-pill-placeholder .label {{ font-size: 11px; color: var(--text-muted); font-weight: 500; }} .face-pill-placeholder .sub {{ font-size: 9px; color: var(--text-faint); margin-top: 2px; font-family: 'JetBrains Mono', monospace; }} .face-num {{ font-family: 'DM Serif Display', serif; font-size: 36px; line-height: 1; }} .face-label {{ font-size: 9px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--text-muted); }} .face-status {{ font-family: 'JetBrains Mono', monospace; font-size: 10px; font-weight: 700; padding: 3px 8px; border-radius: 4px; }} .face-meta {{ font-family: 'JetBrains Mono', monospace; font-size: 9px; color: var(--text-faint); margin-top: 4px; }} .face-bar {{ margin-top: 8px; height: 4px; background: var(--bar-track); border-radius: 2px; overflow: hidden; }} .face-bar-fill {{ height: 100%; border-radius: 2px; transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1); }} /* Onboarding explainer */ .value-grid {{ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: -8px 0 28px; }} .value-card {{ padding: 12px 14px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px; }} .value-kicker {{ font-family: 'JetBrains Mono', monospace; font-size: 10px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; color: var(--brand); margin-bottom: 4px; }} .value-copy {{ font-size: 13px; line-height: 1.45; color: var(--text-secondary); }} /* Agent trace */ .trace-wrap {{ margin-top: 8px; padding: 12px 14px; background: var(--bg-surface); border: 1px solid var(--border-soft); border-radius: 12px; opacity: 0.82; }} .trace-step {{ display: flex; align-items: center; gap: 10px; padding: 6px 10px; border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; }} .trace-step.is-active {{ color: var(--brand); background: rgba(234, 88, 12, 0.06); }} .trace-step.is-done {{ color: var(--recovery-green); }} .trace-dot {{ width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; }} .trace-step.is-active .trace-dot {{ animation: pulse 1.2s ease-in-out infinite; }} @keyframes pulse {{ 0%,100% {{ opacity: 1; }} 50% {{ opacity: 0.3; }} }} /* AI coach block */ .coach-block {{ padding: 20px 22px; background: linear-gradient(180deg, var(--bg-surface), var(--bg-base)); border: 1px solid var(--border); border-radius: 14px; margin-top: 8px; }} .coach-header {{ display: flex; align-items: center; gap: 10px; margin-bottom: 14px; font-family: 'JetBrains Mono', monospace; font-size: 9px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--brand); }} .coach-pill {{ background: var(--bg-elevated); color: var(--text-secondary); padding: 3px 8px; border-radius: 4px; font-size: 9px; }} .coach-body {{ font-size: 14px; color: var(--text-primary); line-height: 1.7; white-space: pre-wrap; min-height: 60px; }} .coach-cursor {{ display: inline-block; width: 7px; height: 14px; background: var(--brand); margin-left: 2px; vertical-align: text-bottom; animation: blink 1s steps(2, start) infinite; }} @keyframes blink {{ to {{ visibility: hidden; }} }} /* Buttons */ button.primary, .gr-button-primary {{ background: var(--brand) !important; color: white !important; border: none !important; border-radius: 10px !important; font-weight: 700 !important; letter-spacing: 0.02em !important; transition: transform 0.15s, box-shadow 0.15s !important; }} button.primary:hover, .gr-button-primary:hover {{ transform: translateY(-1px) !important; box-shadow: 0 6px 20px rgba(234, 88, 12, 0.35) !important; }} /* Inputs */ input, textarea, .gr-input, .gr-text-input, .gr-dropdown {{ background: var(--bg-surface) !important; border: 1px solid var(--border) !important; color: var(--text-primary) !important; border-radius: 8px !important; }} input:focus, textarea:focus {{ border-color: var(--brand) !important; outline: none !important; }} /* Checkbox */ .gr-checkbox {{ background: transparent !important; }} .gr-checkbox input[type="checkbox"] {{ appearance: none; width: 18px; height: 18px; border: 1.5px solid var(--text-muted); border-radius: 4px; background: var(--bg-surface); cursor: pointer; position: relative; transition: all 0.15s; }} .gr-checkbox input[type="checkbox"]:checked {{ background: var(--brand); border-color: var(--brand); }} .gr-checkbox input[type="checkbox"]:checked::after {{ content: '✓'; color: white; position: absolute; top: -2px; left: 3px; font-size: 14px; font-weight: 800; }} .gr-checkbox label {{ color: var(--text-primary) !important; font-weight: 500 !important; }} /* Care checkbox — green when checked (positive stressor) */ #care-checkbox input[type="checkbox"]:checked {{ background: var(--recovery-green) !important; border-color: var(--recovery-green) !important; }} #care-checkbox label {{ color: var(--recovery-green) !important; }} /* Image upload area */ .gr-image, .gr-image-upload {{ background: var(--bg-surface) !important; border: 1px dashed var(--border) !important; border-radius: 10px !important; }} /* Empty state */ .empty-state {{ text-align: center; padding: 80px 20px; color: var(--text-faint); }} .empty-state .icon {{ font-size: 48px; opacity: 0.3; margin-bottom: 12px; }} .empty-state .label {{ font-size: 14px; font-weight: 500; color: var(--text-muted); }} .empty-state .sub {{ font-size: 12px; color: var(--text-faint); margin-top: 4px; }} /* Hide group labels for cleaner look */ .gr-group {{ background: transparent !important; border: none !important; }} .gr-form {{ background: transparent !important; }} /* App header */ .app-header {{ text-align: center; padding: 36px 0 28px; border-bottom: 1px solid var(--border-soft); margin-bottom: 28px; }} .app-title {{ font-family: 'Inter', sans-serif; font-weight: 800; letter-spacing: 0.22em; text-transform: uppercase; font-size: 12px; color: var(--text-primary); margin: 0 0 6px; }} .app-subtitle {{ font-family: 'Inter', sans-serif; font-size: 14px; color: var(--text-secondary); line-height: 1.55; margin: 0; max-width: 480px; margin: 0 auto; }} /* Footer */ .app-footer {{ text-align: center; padding: 28px 16px 16px; margin-top: 36px; border-top: 1px solid var(--border-soft); font-size: 10px; color: var(--text-faint); font-family: 'JetBrains Mono', monospace; }} .app-footer a {{ color: var(--brand); text-decoration: none; }} /* Header attribution pills */ .attr-row {{ display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 14px; flex-wrap: wrap; }} .attr-pill {{ display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 999px; font-family: 'JetBrains Mono', monospace; font-size: 10px; font-weight: 600; color: var(--text-secondary); background: var(--bg-surface); text-decoration: none; transition: border-color 0.15s, color 0.15s; }} .attr-pill:hover {{ border-color: var(--brand); color: var(--text-primary); }} .attr-pill .dot {{ width: 6px; height: 6px; border-radius: 50%; background: var(--recovery-green); box-shadow: 0 0 8px var(--recovery-green); }} .start-hint {{ margin: -2px 0 14px; color: var(--text-secondary); font-size: 13px; line-height: 1.45; }} .scenario-panel {{ max-width: 760px; margin: -8px auto 26px; text-align: center; }} .scenario-panel .section-label {{ justify-content: center; }} .scenario-panel .start-hint {{ max-width: 560px; margin: -2px auto 16px; }} .scenario-actions {{ max-width: 680px; margin: 0 auto; gap: 8px; }} .input-panel {{ max-width: 380px; }} .results-panel {{ max-width: 760px; margin: 0 auto; }} /* Ready pulse for empty coach/trace */ .ready-pulse {{ display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--brand); margin-right: 8px; animation: pulse 1.6s ease-in-out infinite; }} /* Triage plan */ .plan-block {{ margin-bottom: 20px; padding: 16px 18px; background: linear-gradient(180deg, var(--bg-surface), var(--bg-elevated)); border: 1px solid var(--border); border-left: 3px solid var(--brand); border-radius: 14px; animation: fadeUp 0.6s cubic-bezier(0.22, 1, 0.36, 1) 0.15s backwards; }} .plan-source {{ font-family: 'JetBrains Mono', monospace; font-size: 9px; color: var(--text-faint); font-weight: 600; margin-left: auto; text-transform: none; letter-spacing: 0.1em; }} .plan-line {{ display: flex; align-items: center; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--border-soft); animation: fadeUp 0.4s cubic-bezier(0.22, 1, 0.36, 1) backwards; }} .plan-line:last-child {{ border-bottom: none; }} .plan-tag {{ font-family: 'JetBrains Mono', monospace; font-size: 9px; font-weight: 800; letter-spacing: 0.14em; padding: 3px 8px; border: 1px solid; border-radius: 4px; flex-shrink: 0; min-width: 78px; text-align: center; }} .plan-text {{ font-family: 'Inter', sans-serif; font-size: 14px; font-weight: 500; color: var(--text-primary); line-height: 1.4; }} /* Counterfactual hint */ .cf-block {{ display: flex; align-items: flex-start; gap: 12px; padding: 12px 16px; background: var(--bg-surface); border: 1px solid var(--border); border-left: 2px solid; border-radius: 0 10px 10px 0; margin: 16px 0; animation: fadeUp 0.6s cubic-bezier(0.22, 1, 0.36, 1) 0.2s backwards; }} .cf-label {{ font-family: 'JetBrains Mono', monospace; font-size: 9px; font-weight: 800; letter-spacing: 0.16em; flex-shrink: 0; padding-top: 2px; min-width: 140px; }} .cf-body {{ font-size: 13px; color: var(--text-secondary); line-height: 1.55; }} /* Running debt pill */ .debt-pill {{ display: inline-flex; align-items: center; gap: 8px; padding: 5px 14px; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 999px; font-family: 'JetBrains Mono', monospace; font-size: 11px; transition: all 0.15s; margin-bottom: 14px; }} /* Preset scenario chips */ .preset-row {{ display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 18px; }} .preset-chip {{ font-family: 'JetBrains Mono', monospace; font-size: 11px !important; font-weight: 700 !important; padding: 7px 14px !important; border-radius: 999px !important; border: 1px solid var(--border) !important; background: var(--bg-surface) !important; color: var(--text-secondary) !important; letter-spacing: 0.04em !important; transition: all 0.15s !important; box-shadow: none !important; }} .preset-chip:hover {{ border-color: var(--brand) !important; color: var(--text-primary) !important; background: var(--surface-wash) !important; }} /* Clear all button */ button.clear-all {{ font-family: 'JetBrains Mono', monospace !important; font-size: 10px !important; font-weight: 600 !important; padding: 4px 12px !important; border: 1px solid var(--border) !important; border-radius: 8px !important; background: transparent !important; color: var(--text-secondary) !important; transition: all 0.15s !important; margin-bottom: 8px !important; }} button.clear-all:hover {{ border-color: var(--text-faint) !important; color: var(--text-secondary) !important; }} /* Sample badge */ .sample-badge {{ font-family: 'JetBrains Mono', monospace; font-size: 9px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }} .sample-badge .dot {{ width: 5px; height: 5px; border-radius: 50%; background: var(--brand); animation: pulse 1.6s ease-in-out infinite; }} /* Smooth theme transitions — 0.2s matches existing hover rhythms */ .gradio-container, .sys-meter, .coach-block, .coach-pill, .tl-wrap, .face-pill, .face-pill-placeholder, .cf-block, .debt-pill, .attr-pill, input, textarea, .gr-input, .gr-text-input, .gr-dropdown, .gr-checkbox input[type="checkbox"]:not(:checked), .gr-checkbox label, .sci-card, .trace-step, .plan-line, .plan-tag, .sample-badge, .app-header, .app-footer, .section-label, .section-label::after, .debt-hero-label, .debt-verdict, .sys-label, .sys-time, .sys-cause, .sys-bar-fill, .proto-conn, .proto-action, .face-label, .face-meta, .plan-source, .plan-text, .cf-label, .cf-body, .empty-state .label, .empty-state .sub, .orb-wrap::before, .orb-wrap::after, #care-checkbox input[type="checkbox"]:not(:checked) {{ transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease; }} /* These have !important on their existing transitions, so !important needed here too */ .preset-chip, button.clear-all, button.primary, .gr-button-primary {{ transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease !important; }} /* Comparison card carousel */ .cmp-section {{ margin-top: 28px; }} .cmp-row {{ display: flex; gap: 10px; overflow-x: auto; padding: 4px 0 12px; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; }} .cmp-card {{ min-width: 170px; max-width: 200px; flex-shrink: 0; scroll-snap-align: start; padding: 12px 14px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px; position: relative; }} .cmp-hero {{ font-family: 'DM Serif Display', Georgia, serif; font-size: 30px; line-height: 1; margin-bottom: 2px; }} .cmp-label {{ font-family: 'JetBrains Mono', monospace; font-size: 9px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-secondary); }} .cmp-time {{ font-family: 'JetBrains Mono', monospace; font-size: 8px; color: var(--text-faint); margin-top: 2px; margin-bottom: 8px; }} .cmp-sys {{ display: flex; align-items: center; gap: 5px; margin-top: 5px; font-size: 10px; }} .cmp-sys-glyph {{ width: 16px; font-family: 'JetBrains Mono', monospace; font-weight: 700; flex-shrink: 0; }} .cmp-sys-bar {{ flex: 1; height: 3px; background: var(--bar-track); border-radius: 2px; overflow: hidden; }} .cmp-sys-fill {{ height: 100%; border-radius: 2px; transition: width 0.4s ease; }} .cmp-del {{ position: absolute; top: 4px; right: 6px; width: 20px; height: 20px; border-radius: 50%; border: none; background: transparent; color: var(--text-faint); font-size: 13px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s; font-family: system-ui, sans-serif; }} .cmp-del:hover {{ background: rgba(244, 63, 94, 0.15); color: #F43F5E; }} .cmp-empty {{ text-align: center; padding: 24px; color: var(--text-faint); font-size: 12px; }} /* Timeline chart */ .tl-wrap {{ margin: 16px 0; padding: 14px 16px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px; }} .tl-label {{ font-family: 'JetBrains Mono', monospace; font-size: 7px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-faint); min-width: 32px; flex-shrink: 0; }} .tl-row {{ display: flex; align-items: center; gap: 6px; padding: 3px 0; }} .tl-bar-wrap {{ flex: 1; height: 10px; background: rgba(168, 162, 158, 0.06); border-radius: 5px; overflow: hidden; position: relative; }} .tl-bar {{ height: 100%; border-radius: 5px; transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1); }} .tl-bar-stack {{ height: 100%; display: flex; border-radius: 5px; overflow: hidden; }} .tl-seg {{ height: 100%; transition: flex 0.6s cubic-bezier(0.22, 1, 0.36, 1); }} .tl-seg:first-child {{ border-radius: 5px 0 0 5px; }} .tl-seg:last-child {{ border-radius: 0 5px 5px 0; }} .tl-seg:only-child {{ border-radius: 5px; }} .tl-score {{ font-family: 'JetBrains Mono', monospace; font-size: 9px; font-weight: 700; min-width: 22px; text-align: right; flex-shrink: 0; }} .tl-now {{ display: inline-block; font-size: 7px; font-weight: 800; letter-spacing: 0.1em; color: var(--brand); margin-left: 4px; }} .tl-dom {{ font-family: 'JetBrains Mono', monospace; font-size: 8px; font-weight: 800; min-width: 16px; text-align: center; flex-shrink: 0; }} /* Timeline legend */ .tl-legend {{ display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--border-soft); }} .tl-leg {{ font-family: 'JetBrains Mono', monospace; font-size: 8px; font-weight: 600; display: inline-flex; align-items: center; gap: 4px; }} .tl-leg-swatch {{ width: 8px; height: 8px; border-radius: 2px; display: inline-block; flex-shrink: 0; }} /* Save compare button */ .save-compare {{ font-family: 'JetBrains Mono', monospace !important; font-size: 10px !important; font-weight: 700 !important; padding: 5px 12px !important; border-radius: 8px !important; border: 1px solid var(--border) !important; background: transparent !important; color: var(--text-secondary) !important; transition: all 0.15s !important; }} .save-compare:hover {{ border-color: var(--brand) !important; color: var(--brand) !important; }} /* Mobile / narrow viewport */ @media (max-width: 900px) {{ .gradio-container {{ padding: 0 16px 40px !important; }} .app-header {{ padding: 24px 0 20px; margin-bottom: 18px; }} .value-grid {{ grid-template-columns: 1fr; margin: -4px 0 22px; }} .input-panel, .results-panel {{ max-width: 100%; }} .debt-hero {{ font-size: clamp(5.5rem, 28vw, 8rem); }} .orb-wrap {{ padding: 28px 20px 22px; }} .gradio-row > div {{ flex-wrap: wrap !important; }} }} @media (max-width: 600px) {{ .gradio-container {{ padding: 0 10px 28px !important; }} .app-header {{ padding: 18px 0 16px; margin-bottom: 14px; }} .app-title {{ font-size: 10px; }} .app-subtitle {{ font-size: 11px; }} .attr-pill {{ font-size: 8px; padding: 2px 8px; }} .debt-hero {{ font-size: clamp(3.8rem, 24vw, 5.5rem) !important; }} .debt-hero-label {{ font-size: 8px; }} .debt-verdict {{ font-size: 12px; }} .orb-wrap {{ padding: 20px 12px 16px; }} .orb-wrap::before {{ inset: -20px; }} .orb-wrap::after {{ inset: -35px; }} .section-label {{ font-size: 10px; margin: 0 0 8px; }} .value-card {{ padding: 10px 12px; }} .value-kicker {{ font-size: 9px; }} .value-copy {{ font-size: 12px; }} .sys-meter {{ padding: 8px 10px; gap: 8px; }} .sys-glyph {{ width: 22px; height: 22px; font-size: 10px; }} .sys-label {{ font-size: 11px; }} .sys-time {{ font-size: 8px; }} .sys-cause {{ font-size: 10px; }} .coach-block {{ padding: 14px 16px; }} .coach-body {{ font-size: 12px; min-height: 40px; }} .proto-action {{ font-size: 11px; }} .proto-step {{ gap: 8px; }} .preset-chip {{ font-size: 9px !important; padding: 4px 10px !important; }} .debt-pill {{ font-size: 9px; padding: 4px 10px; }} .face-pill {{ padding: 10px 12px; gap: 10px; }} .face-num {{ font-size: 28px; }} .cf-block {{ padding: 10px 12px; flex-direction: column; gap: 6px; }} .cf-label {{ min-width: auto; font-size: 8px; }} .cf-body {{ font-size: 11px; }} .plan-line {{ gap: 8px; padding: 7px 0; }} .plan-tag {{ font-size: 8px; min-width: 64px; padding: 2px 6px; }} .plan-text {{ font-size: 11px; }} .trace-step {{ font-size: 9px; padding: 4px 8px; }} .tl-legend {{ gap: 6px; }} .tl-leg {{ font-size: 7px; }} .tl-leg-swatch {{ width: 6px; height: 6px; }} .tl-dom {{ font-size: 7px; min-width: 12px; }} .cmp-card {{ min-width: 140px; max-width: 160px; padding: 10px 10px; }} .cmp-hero {{ font-size: 24px; }} .cmp-label {{ font-size: 8px; }} .empty-state {{ padding: 40px 16px; }} .empty-state .icon {{ font-size: 36px; }} .empty-state .label {{ font-size: 12px; }} .empty-state .sub {{ font-size: 10px; }} .sci-card {{ padding: 10px 12px; }} .sci-fact {{ font-size: 11px; }} .sci-cite {{ font-size: 9px; }} .face-meta {{ font-size: 8px; }} .face-status {{ font-size: 9px; }} input, textarea {{ font-size: 14px !important; }} .gr-checkbox input[type="checkbox"] {{ width: 22px; height: 22px; }} .gr-checkbox input[type="checkbox"]:checked::after {{ font-size: 16px; top: -1px; left: 5px; }} .gr-checkbox label {{ font-size: 14px !important; }} .gr-dropdown {{ font-size: 13px !important; }} }} @media (max-width: 420px) {{ .gradio-container {{ padding: 0 8px 24px !important; }} .app-header {{ padding: 14px 0 12px; margin-bottom: 12px; }} .app-title {{ font-size: 9px; letter-spacing: 0.16em; }} .debt-hero {{ font-size: clamp(2.8rem, 22vw, 3.8rem) !important; }} .orb-wrap {{ padding: 14px 8px 12px; }} .orb-wrap::before {{ inset: -14px; }} .orb-wrap::after {{ inset: -24px; }} .preset-row {{ gap: 4px; }} .preset-chip {{ font-size: 8px !important; padding: 3px 8px !important; }} .attr-pill {{ font-size: 7px; padding: 2px 6px; gap: 4px; }} .cmp-card {{ min-width: 120px; max-width: 140px; padding: 8px; }} .cmp-hero {{ font-size: 20px; }} .plan-tag {{ font-size: 7px; min-width: 54px; }} .coach-block {{ padding: 12px 12px; }} .coach-header {{ font-size: 8px; }} .coach-pill {{ font-size: 8px; padding: 2px 6px; }} button.clear-all {{ font-size: 9px !important; padding: 3px 8px !important; }} .save-compare {{ font-size: 9px !important; padding: 3px 10px !important; }} }} """ # ─── HTML renderers ────────────────────────────────────────────────────────── def render_hero(score: int, verdict: str, is_sample: bool = False) -> str: color, _, _ = debt_tier(score) badge = "" if is_sample: badge = ( '
' '' "SAMPLE RESULT · choose a scenario or click Calculate Orbura for your own" "
" ) return f""" {badge}
Orbura Score
{score}
{html.escape(verdict)}
""" def render_system_meters(system_scores) -> str: max_score = max((s.score for s in system_scores), default=0) primary_system = None if max_score > 0: primary_system = max(system_scores, key=lambda s: s.score).system rows = [] for s in system_scores: accent_active, accent_soft, accent_muted = SYSTEM_ACCENTS.get( s.system, ("var(--text-secondary)", "var(--border-soft)", "var(--text-muted)") ) is_primary = s.system == primary_system glyph = SYSTEM_GLYPHS.get(s.system, "•") bar_color = accent_active if is_primary else accent_muted label_color = "var(--text-primary)" if is_primary else "var(--text-secondary)" pct = max(0, min(100, s.score)) glyph_bg = accent_soft if is_primary else "rgba(168, 162, 158, 0.06)" rows.append(f"""
{glyph}
{s.icon} {html.escape(s.label)} clears {s.cleared_at}
{html.escape(s.cause_text)}
""") return f"""
Five-system breakdown
{''.join(rows)}
""" def render_prescription(system_scores, debt_score: int) -> str: if debt_score < 20: return f"""
Recovery Protocol · Cleared
All five systems below threshold. Maintain the streak.
""" # Map system actions into the four temporal windows based on severity windows = ["RIGHT NOW", "THIS MORNING", "TODAY", "AVOID"] active = [s for s in system_scores if s.score > 15] if not active: active = system_scores[:3] # Sort: highest-debt system goes to RIGHT NOW active.sort(key=lambda s: -s.score) steps = [] for i, s in enumerate(active[:4]): window = windows[min(i, 3)] color = WINDOW_COLORS[window] steps.append((i + 1, window, s.action_text, color)) is_last = lambda i: i == len(steps) - 1 steps_html = "" for i, (num, window, action, color) in enumerate(steps): connector = "" if is_last(i) else '
' steps_html += f"""
{str(num).zfill(2)}
{connector}
{window} · {html.escape(active[i].label)}

{html.escape(action)}

""" return f"""
Recovery Protocol
{steps_html}
""" def render_science(system_scores) -> str: items = [] for s in system_scores: if s.score > 20 and s.science_fact and s.science_cite: accent = SYSTEM_ACCENTS.get(s.system, ("var(--text-secondary)",))[0] items.append((accent, s.science_fact, s.science_cite)) if not items: return "" cards = "".join( f"""

{html.escape(fact)}

— {html.escape(cite)}

""" for accent, fact, cite in items ) return f"""
The science
{cards}
""" def render_face_scan(face_stress, is_healthy, _features) -> str: status_color = "var(--recovery-green)" if is_healthy else "var(--brand)" status_text = "Healthy" if is_healthy else "Stressed" status_bg = "rgba(74, 222, 128, 0.10)" if is_healthy else "rgba(234, 88, 12, 0.10)" pct = max(0, min(100, face_stress)) return f"""
Face Scan
{face_stress:.0f}/100
{status_text}
Processed on-device. No biometric data transmitted.
""" def render_face_scan_placeholder() -> str: """Call-to-action placeholder for face scan before the user runs an analysis.""" return f"""
📷
Capture a photo or use your webcam
MediaPipe FaceMesh → 7 features → stress MLP · all on-device
""" # ─── Debt timeline chart ───────────────────────────────────────────────── def compute_debt_timeline(system_scores, now=None): """Compute how total body debt changes over the recovery window. Each system decays linearly from its current score to 0 over its recovery window. Returns list of dicts, each with total score, per-system breakdown, and the dominant system at that point. """ if now is None: now = datetime.now() max_window = max((s for s in system_scores), default=None, key=lambda s: s.recovery_hrs) max_hrs = max_window.recovery_hrs if max_window else 10 max_hrs = max(max_hrs, 8) max_hrs = min(max_hrs, 48) system_order = ["cardiovascular", "brain", "liver", "muscular", "gut"] points = [] for h in range(0, int(max_hrs) + 2, 2): total = 0 systems_at_h = [] for sys_name in system_order: s = next((x for x in system_scores if x.system == sys_name), None) if s is None: continue if s.recovery_hrs > 0 and h <= s.recovery_hrs: remaining_frac = (s.recovery_hrs - h) / s.recovery_hrs sys_score = round(s.score * remaining_frac, 1) else: sys_score = 0.0 total += sys_score accent = SYSTEM_ACCENTS.get(sys_name, ("var(--text-muted)",))[0] systems_at_h.append({ "name": sys_name, "glyph": SYSTEM_GLYPHS.get(sys_name, "•"), "score": round(sys_score), "color": accent, }) total = round(total) color, _, _ = debt_tier(total) label = "Now" if h == 0 else f"+{h}h" # Find dominant system (highest contributing) dominant = max(systems_at_h, key=lambda x: x["score"]) if systems_at_h else None points.append({ "hour": h, "score": total, "color": color, "label": label, "systems": systems_at_h, "dominant": dominant["name"] if dominant and dominant["score"] > 0 else None, "dominant_glyph": dominant["glyph"] if dominant and dominant["score"] > 0 else "", }) return points def render_timeline(points: list[dict]) -> str: """Render the debt timeline as a horizontal bar chart with system breakdown. Each bar is a stacked segment showing each system's contribution in its accent color. The dominant system is labeled to the right of the bar. """ if not points: return "" max_score = max(p["score"] for p in points) or 1 rows = [] for p in points: now_tag = '· now' if p["hour"] == 0 else "" # Build stacked bar segments — each system gets a proportional slice bar_pct = max(2.0, (p["score"] / max_score) * 100) segments = [] if p["systems"] and p["score"] > 0: total_sys = sum(x["score"] for x in p["systems"]) for sys in p["systems"]: if sys["score"] <= 0: continue share = (sys["score"] / total_sys) * 100 sys_label = {"cardiovascular":"Cardiovascular","brain":"Brain","liver":"Liver","muscular":"Muscular","gut":"Gut"}.get(sys["name"], sys["name"]) segments.append(f'') else: segments = [] seg_html = "".join(segments) if segments else f'' # Dominant system indicator dom_html = "" if p["dominant"] and p["dominant_glyph"]: dom_color = SYSTEM_ACCENTS.get(p["dominant"], ("var(--text-secondary)",))[0] dom_html = f'{p["dominant_glyph"]}' rows.append(f"""
{p['label']}{now_tag}
{seg_html}
{p['score']} {dom_html}
""") # Build a legend showing system glyphs legend_items = [] for sys_name in ["cardiovascular", "brain", "liver", "muscular", "gut"]: accent = SYSTEM_ACCENTS.get(sys_name, ("var(--text-muted)",))[0] glyph = SYSTEM_GLYPHS.get(sys_name, "•") label = { "cardiovascular": "Cardio", "brain": "Brain", "liver": "Liver", "muscular": "Muscle", "gut": "Gut", }.get(sys_name, sys_name) legend_items.append(f'{glyph} {label}') legend_html = f'
{" ".join(legend_items)}
' if len(legend_items) > 0 else "" return f"""
Recovery forecast
{legend_html} {"".join(rows)}
""" def render_plan(plan: dict | None, lines_so_far: list[str] | None = None) -> str: """Triage plan: 3 lines from SmolLM2's structured plan step. `lines_so_far` lets the UI show the plan being formed (one line at a time) as the LLM streams. Falls back to `plan` dict for the final render. """ if lines_so_far is None: lines_so_far = [] if plan: if plan.get("priority"): lines_so_far.append(f"PRIORITY: {plan['priority']}") if plan.get("secondary"): lines_so_far.append(f"SECONDARY: {plan['secondary']}") if plan.get("avoid"): lines_so_far.append(f"AVOID: {plan['avoid']}") if not lines_so_far and not plan: return "" rendered_lines = [] for line in lines_so_far: up = line.upper().strip() if up.startswith("PRIORITY:"): color = "#DC2626" label = "DO FIRST" elif up.startswith("SECONDARY:"): color = "#EA580C" label = "SECONDARY" elif up.startswith("AVOID:"): color = "#A78BFA" label = "AVOID" else: color = "var(--text-muted)" label = "" rest = line.split(":", 1)[1].strip() if ":" in line else line rendered_lines.append(f"""
{label} {html.escape(rest)}
""") return f"""
Recovery plan SmolLM2-360M
{''.join(rendered_lines)}
""" def render_counterfactual(cf: dict | None) -> str: if not cf: return "" accent = SYSTEM_ACCENTS.get(cf["system"], ("var(--text-secondary)",))[0] return f"""
WHAT WOULD CHANGE THIS If you had {html.escape(cf['lever_label'])}, {html.escape(cf['system_label'])} debt would drop from {cf['from_score']} to {cf['to_score']}.
""" def render_agent_trace(steps: list[tuple[str, str, str]]) -> str: """steps: list of (label, status, message) where status is pending|active|done|error.""" if steps: items = [] for label, status, message in steps: items.append(f"""
{html.escape(label)} {html.escape(message)}
""") body = "".join(items) else: body = f"""
parse_stressors
compute_live_score
face_scan
llm_coach
Awaiting analysis
""" return f"""
How this was generated
{body}
""" def render_empty_state() -> str: return f"""
🫀
Your debt will appear here
Log your stressors on the left. Tap calculate.
""" # ─── Running estimate (live debt pill) ─────────────────────────────────────── def compute_running_estimate( alcohol, alcohol_type, alcohol_count, training, training_area, training_intensity, sleep, sleep_hours, stress, stress_carried, ill, ill_severity, care, bed_time, wake_time, ) -> str: """Return a small HTML pill showing the current running debt score.""" stressors = build_stressors( alcohol, alcohol_type, alcohol_count, training, training_area, training_intensity, sleep, sleep_hours, stress, stress_carried, ill, ill_severity, care, ) score = compute_live_score(stressors) color, _, _ = debt_tier(score) return f'Running debt · {score}/100' # ─── Preset scenario fillers ───────────────────────────────────────────────── def _fill_preset( a, a_t, a_c, a_v, t, t_a, t_i, t_v, s, s_h, s_v, st, st_c, st_v, i, i_s, i_v, c, bt, wt, ): """Return (20-element) tuple matching the preset outputs list below.""" return (a, a_t, a_c, gr.Group(visible=a_v), t, t_a, t_i, gr.Group(visible=t_v), s, s_h, gr.Group(visible=s_v), st, st_c, gr.Group(visible=st_v), i, i_s, gr.Group(visible=i_v), c, bt, wt) def fill_bad_night(): """Drank red wine 3-4, trained legs hard, slept 4-6.""" return _fill_preset( True, "red_wine", "3-4", True, True, "legs", "hard", True, True, "4-6", True, False, "yes", False, False, "moderate", False, False, "2:00 AM", "8:00 AM", ) def fill_recovery_day(): """Beer 1-2, slept 6-7, took care of myself.""" return _fill_preset( True, "beer", "1-2", True, False, "full_body", "hard", False, True, "6-7", True, False, "yes", False, False, "moderate", False, True, "10:00 PM", "6:30 AM", ) def fill_hit_it_hard(): """No alcohol, trained destroyed, slept okay, took care.""" return _fill_preset( False, "red_wine", "3-4", False, True, "legs", "destroyed", True, True, "6-7", True, False, "yes", False, False, "moderate", False, True, "10:30 PM", "6:00 AM", ) def fill_sick(): """Slept terribly, high stress, floored by illness.""" return _fill_preset( False, "red_wine", "3-4", False, False, "full_body", "hard", False, True, "under_4", True, True, "yes", True, True, "floored", True, False, "11:00 PM", "7:00 AM", ) def clear_all_form(): """Reset all form inputs to defaults.""" return _fill_preset( False, "red_wine", "3-4", False, False, "full_body", "hard", False, False, "4-6", False, False, "yes", False, False, "moderate", False, False, "", "", ) # ─── Sample preview builder ────────────────────────────────────────────────── def render_sample_preview(): """Pre-render a sample analysis so the right column has content on first load.""" sample_stressors = build_stressors( True, "red_wine", "3-4", True, "legs", "hard", True, "4-6", False, "yes", False, "moderate", False, ) sample_now = datetime.now() sample_score = compute_live_score(sample_stressors) sample_system_scores = compute_system_scores( sample_stressors, now=sample_now, bed_time="2:00 AM", wake_time="8:00 AM", ) _, sample_verdict, _ = debt_tier(sample_score) hero_html = render_hero(sample_score, sample_verdict, is_sample=True) meters_html = render_system_meters(sample_system_scores) rx_html = render_prescription(sample_system_scores, sample_score) science_html = render_science(sample_system_scores) system_dicts = [ {"label": s.label, "score": s.score, "cleared_at": s.cleared_at} for s in sample_system_scores ] fallback_plan = _fallback_plan(system_dicts) plan_html = render_plan(fallback_plan) cf = compute_counterfactual(sample_stressors, sample_system_scores, "2:00 AM", "8:00 AM") cf_html = render_counterfactual(cf) sample_trace = [ ("parse_stressors", "done", "3 stressors"), ("compute_live_score", "done", f"score={sample_score}/100"), ("triage_plan", "done", "PRIORITY · SECONDARY · AVOID"), ("llm_coach", "done", "sample"), ] trace_html = render_agent_trace(sample_trace) face_html = render_face_scan_placeholder() timeline_html = render_timeline(compute_debt_timeline(sample_system_scores, now=sample_now)) return hero_html, plan_html, meters_html, face_html, timeline_html, rx_html + science_html, trace_html, cf_html, _sample_coach() def _sample_coach() -> str: return f"""
AI Recovery Coach SmolLM2-360M · local
Sample advice shown. Tap Calculate Orbura for your own.
""" # ─── Main analysis pipeline (yields streaming trace updates) ───────────────── def build_stressors( alcohol, alcohol_type, alcohol_count, training, training_area, training_intensity, sleep, sleep_hours, stress, stress_carried, ill, ill_severity, care, ): stressors = [] if alcohol: stressors.append(Stressor(type="alcohol", alcohol_type=alcohol_type, alcohol_count=alcohol_count)) if training: stressors.append(Stressor(type="training", training_area=training_area, training_intensity=training_intensity)) if sleep: stressors.append(Stressor(type="sleep", sleep_hours=sleep_hours)) if stress: stressors.append(Stressor(type="stress", stress_carried=stress_carried)) if ill: stressors.append(Stressor(type="ill", ill_severity=ill_severity)) if care: stressors.append(Stressor(type="care")) return stressors def run_analysis_stream( alcohol, alcohol_type, alcohol_count, training, training_area, training_intensity, sleep, sleep_hours, stress, stress_carried, ill, ill_severity, care, bed_time, wake_time, face_image, progress=gr.Progress(), ): """Streaming generator. Yield tuple: (hero, meters, rx, face, timeline, plan, trace, counterfactual, coach) """ stressors = build_stressors( alcohol, alcohol_type, alcohol_count, training, training_area, training_intensity, sleep, sleep_hours, stress, stress_carried, ill, ill_severity, care, ) EMPTY = render_empty_state() NUL = "" E_C = _empty_coach() E_P = "" E_T = render_agent_trace([]) E_CF = "" # Step 1: parse stressors trace = [("parse_stressors", "active", f"{len(stressors)} selected")] yield (EMPTY, EMPTY, NUL, NUL, "", E_P, E_T, E_CF, E_C) time.sleep(0.05) if not stressors: trace[-1] = ("parse_stressors", "error", "none selected") msg = ( f'
🫀
' f'
Log at least one stressor
' f'
Tap a checkbox on the left to begin.
' ) yield (msg, NUL, NUL, NUL, "", E_P, render_agent_trace(trace), E_CF, E_C) return trace[-1] = ("parse_stressors", "done", f"{len(stressors)} stressors") progress(0.1, desc="Computing debt score...") # Step 2: compute scores trace.append(("compute_live_score", "active", "deterministic engine")) yield (NUL, NUL, NUL, NUL, "", E_P, render_agent_trace(trace), E_CF, E_C) time.sleep(0.05) live_score = compute_live_score(stressors) system_scores = compute_system_scores( stressors, now=datetime.now(), bed_time=bed_time or None, wake_time=wake_time or None, ) trace[-1] = ("compute_live_score", "done", f"score={live_score}/100") progress(0.3, desc="Mapping 5 systems...") # Compute timeline once system scores are available timeline_html = render_timeline(compute_debt_timeline(system_scores)) # Step 3: face scan face_html = "" face_stress = None if face_image is not None: trace.append(("face_scan", "active", "MediaPipe FaceMesh")) yield (NUL, NUL, NUL, NUL, timeline_html, E_P, render_agent_trace(trace), E_CF, E_C) time.sleep(0.05) features = run_face_scan(face_image) if features: arr = features_to_array(features) face_stress, is_healthy = predict_stress_score(arr) face_html = render_face_scan(face_stress, is_healthy, features) trace[-1] = ("face_scan", "done", f"stress={face_stress:.0f}/100") else: trace[-1] = ("face_scan", "error", "no face detected") else: trace.append(("face_scan", "done", "skipped")) # Step 3.5: triage plan (the real "agent" step) system_dicts = [ {"label": s.label, "score": s.score, "cleared_at": s.cleared_at} for s in system_scores ] trace.append(("triage_plan", "active", "SmolLM2-360M")) plan_html = render_plan(None, []) yield (NUL, NUL, NUL, NUL, timeline_html, plan_html, render_agent_trace(trace), E_CF, E_C) plan_dict: dict = {"priority": None, "secondary": None, "avoid": None} plan_lines: list[str] = [] try: for plan_dict, line in stream_plan(system_dicts): if line: plan_lines.append(line) plan_html = render_plan(None, list(plan_lines)) yield (NUL, NUL, NUL, NUL, timeline_html, plan_html, render_agent_trace(trace), E_CF, E_C) except Exception as e: print(f"Plan stream failed: {e}") plan_html = render_plan(plan_dict, plan_lines) trace[-1] = ("triage_plan", "done", "PRIORITY · SECONDARY · AVOID") progress(0.5, desc="Building system breakdown...") # Step 4: render hero + systems + prescription _, verdict, _ = debt_tier(live_score) hero_html = render_hero(live_score, verdict) meters_html = render_system_meters(system_scores) rx_html = render_prescription(system_scores, live_score) science_html = render_science(system_scores) cf = compute_counterfactual(stressors, system_scores, bed_time, wake_time) cf_html = render_counterfactual(cf) # Step 5: streaming LLM advice trace.append(("llm_coach", "active", "SmolLM2-360M local")) accumulated = "" stressor_summary = ", ".join( f"{STRESSOR_DEFS[s.type]['icon']} {STRESSOR_DEFS[s.type]['label']}" for s in stressors ) yield ( hero_html, meters_html, rx_html + science_html, face_html, timeline_html, plan_html, render_agent_trace(trace), cf_html, _coach_with_cursor(""), ) try: for piece in stream_advice(live_score, system_dicts, stressor_summary, face_stress): accumulated += piece yield ( hero_html, meters_html, rx_html + science_html, face_html, timeline_html, plan_html, render_agent_trace(trace), cf_html, _coach_with_cursor(accumulated), ) except Exception as e: print(f"Stream fallback: {e}") accumulated = _fallback_advice(live_score, system_dicts, stressor_summary) yield ( hero_html, meters_html, rx_html + science_html, face_html, timeline_html, plan_html, render_agent_trace(trace), cf_html, _coach_with_cursor(accumulated), ) trace[-1] = ("llm_coach", "done", f"{len(accumulated)} chars") progress(1.0, desc="Done") yield ( hero_html, meters_html, rx_html + science_html, face_html, timeline_html, plan_html, render_agent_trace(trace), cf_html, _coach_with_cursor(accumulated), ) def _empty_coach() -> str: return f"""
AI Recovery Coach SmolLM2-360M · local
Ready. Tap Calculate Orbura to stream advice.
""" def _coach_with_cursor(text: str) -> str: safe = html.escape(text) return f"""
AI Recovery CoachSmolLM2-360M · local
{safe}
""" # ─── Compare scenarios ─────────────────────────────────────────────────── def _build_compare_label(stressors: list) -> str: """Build a short label from stressor icons.""" if not stressors: return "Clear day" icons = [STRESSOR_DEFS[s.type]["icon"] for s in stressors] return " ".join(icons) def render_comparison_html(comparisons: list) -> str: """Render the horizontal comparison carousel as HTML.""" if not comparisons: return '' cards = [] for i, c in enumerate(comparisons): color, verdict, _ = debt_tier(c["score"]) systems_html = "" for sys in c["system_scores"]: accent = SYSTEM_ACCENTS.get(sys["system"], ("var(--text-secondary)",))[0] pct = max(0, min(100, sys["score"])) systems_html += f"""
{sys['glyph']}
""" cards.append(f"""
{c['score']}
{html.escape(c['label'])}
{html.escape(c['timestamp'])}
{systems_html}
""") return f'
{"".join(cards)}
' def save_compare( comparisons, # list from gr.State alcohol, alcohol_type, alcohol_count, training, training_area, training_intensity, sleep, sleep_hours, stress, stress_carried, ill, ill_severity, care, bed_time, wake_time, ): """Recompute from current form inputs, append to comparisons, return updated state + HTML.""" stressors = build_stressors( alcohol, alcohol_type, alcohol_count, training, training_area, training_intensity, sleep, sleep_hours, stress, stress_carried, ill, ill_severity, care, ) score = compute_live_score(stressors) system_scores = compute_system_scores( stressors, now=datetime.now(), bed_time=bed_time or None, wake_time=wake_time or None, ) label = _build_compare_label(stressors) now_str = datetime.now().strftime("%I:%M %p").lstrip("0") entry = { "score": score, "label": label, "timestamp": now_str, "system_scores": [ { "system": s.system, "glyph": SYSTEM_GLYPHS.get(s.system, "•"), "score": s.score, } for s in system_scores ], } new_list = list(comparisons or []) new_list.append(entry) return new_list, render_comparison_html(new_list) def remove_compare(comparisons, idx: int): """Remove a comparison by index. Returns -1 sentinel to reset the trigger.""" new_list = list(comparisons or []) if 0 <= idx < len(new_list): del new_list[idx] return new_list, render_comparison_html(new_list), -1 def clear_comparisons(): """Clear all comparisons.""" return [], "" # ─── Pre-compute sample preview ───────────────────────────────────────────── SAMPLE_HERO, SAMPLE_PLAN, SAMPLE_METERS, SAMPLE_FACE, SAMPLE_TIMELINE, SAMPLE_RX, SAMPLE_TRACE, SAMPLE_CF, SAMPLE_COACH = render_sample_preview() # ─── Layout ────────────────────────────────────────────────────────────────── with gr.Blocks(title="Orbura", theme=APP_THEME, css=CUSTOM_CSS) as demo: gr.HTML(f"""

🫀 Orbura

Pick a scenario or log your day to calculate physiological debt, see the body systems affected, and get a focused recovery plan from local AI.

SmolLM2-360M · local 360M params · 250MB RAM Built with OpenAI Codex
Score
A 0–100 read on the recovery load your body is carrying right now.
Systems
See which body systems are driving the debt and when they clear.
Plan
Get the next action, secondary support, and what to avoid today.
""") with gr.Group(elem_classes="scenario-panel"): gr.HTML('
Start with a scenario
') gr.HTML('

Fastest path: choose a preset to see Orbura calculate a realistic recovery plan, or customize your own day below.

') with gr.Row(elem_classes="scenario-actions"): preset_bad_night = gr.Button("🌙 Bad night", elem_classes="preset-chip", size="sm") preset_recovery = gr.Button("♻️ Recovery day", elem_classes="preset-chip", size="sm") preset_hit_hard = gr.Button("🔥 Hit it hard", elem_classes="preset-chip", size="sm") preset_sick = gr.Button("🤒 Sick", elem_classes="preset-chip", size="sm") with gr.Row(): with gr.Column(scale=1, elem_classes="input-panel"): with gr.Accordion("Customize your day", open=False): gr.HTML('
Log today’s stressors
') # Running debt pill debt_pill = gr.HTML(value='Running debt · 0/100') alcohol = gr.Checkbox(label="🍺 Drank", value=False) with gr.Group(visible=False) as alcohol_details: alcohol_type = gr.Dropdown( choices=["beer", "red_wine", "white_wine", "spirits", "cocktails", "champagne"], value="red_wine", label="What?", ) alcohol_count = gr.Dropdown( choices=["1-2", "3-4", "5+", "lost_count"], value="3-4", label="How many?", ) training = gr.Checkbox(label="💪 Trained", value=False) with gr.Group(visible=False) as training_details: training_area = gr.Dropdown( choices=["legs", "upper", "cardio", "hiit", "full_body", "mobility"], value="full_body", label="What?", ) training_intensity = gr.Dropdown( choices=["easy", "hard", "destroyed"], value="hard", label="Intensity?", ) sleep = gr.Checkbox(label="😴 Slept badly", value=False) with gr.Group(visible=False) as sleep_details: sleep_hours = gr.Dropdown( choices=["under_4", "4-6", "6-7"], value="4-6", label="How many hours?", ) stress = gr.Checkbox(label="😤 High stress", value=False) with gr.Group(visible=False) as stress_details: stress_carried = gr.Dropdown( choices=["yes", "mostly_gone"], value="yes", label="Still carrying it?", ) ill = gr.Checkbox(label="🤒 Feeling ill", value=False) with gr.Group(visible=False) as ill_details: ill_severity = gr.Dropdown( choices=["mild", "moderate", "floored"], value="moderate", label="How bad?", ) care = gr.Checkbox(label="✦ Took care of myself", value=False, elem_id="care-checkbox") gr.HTML('
Sleep timing
') bed_time = gr.Dropdown( choices=TIME_OPTIONS, value="", label="Bedtime", allow_custom_value=True, info="Select or type (e.g. 2:00 AM)", ) wake_time = gr.Dropdown( choices=TIME_OPTIONS, value="", label="Wake time", allow_custom_value=True, info="Select or type (e.g. 8:30 AM)", ) gr.HTML('
Face scan (optional)
') face_image = gr.Image( label="Capture or upload", sources=["webcam", "upload"], type="numpy", ) with gr.Row(): clear_btn = gr.Button("Clear all", elem_classes="clear-all", size="sm") analyze_btn = gr.Button("Calculate Orbura →", variant="primary", size="lg") with gr.Row(): save_compare_btn = gr.Button("📋 Save to compare", elem_classes="save-compare", size="sm") clear_compare_btn = gr.Button("✕ Clear saved", elem_classes="save-compare", size="sm") with gr.Column(scale=2, elem_classes="results-panel"): hero_output = gr.HTML(value=SAMPLE_HERO) plan_output = gr.HTML(value=SAMPLE_PLAN) meters_output = gr.HTML(value=SAMPLE_METERS) face_output = gr.HTML(value=SAMPLE_FACE) timeline_output = gr.HTML(value=SAMPLE_TIMELINE) with gr.Row(): with gr.Column(scale=3): rx_output = gr.HTML(value=SAMPLE_RX) with gr.Column(scale=2): coach_output = gr.HTML(value=SAMPLE_COACH) counterfactual_output = gr.HTML(value=SAMPLE_CF) with gr.Accordion("How this was generated", open=False): trace_output = gr.HTML(value=SAMPLE_TRACE) gr.HTML('
Saved comparisons
') compare_output = gr.HTML(value="", visible=True) comparisons_state = gr.State([]) ANALYSIS_INPUTS = [ alcohol, alcohol_type, alcohol_count, training, training_area, training_intensity, sleep, sleep_hours, stress, stress_carried, ill, ill_severity, care, bed_time, wake_time, face_image, ] ANALYSIS_OUTPUTS = [ hero_output, meters_output, rx_output, face_output, timeline_output, plan_output, trace_output, counterfactual_output, coach_output, ] # ─── Event wiring ──────────────────────────────────────────────────────── # Toggle detail sections alcohol.change(lambda v: gr.Group(visible=v), alcohol, alcohol_details) training.change(lambda v: gr.Group(visible=v), training, training_details) sleep.change(lambda v: gr.Group(visible=v), sleep, sleep_details) stress.change(lambda v: gr.Group(visible=v), stress, stress_details) ill.change(lambda v: gr.Group(visible=v), ill, ill_details) # Live running debt pill — update on any form input change debt_inputs = [ alcohol, alcohol_type, alcohol_count, training, training_area, training_intensity, sleep, sleep_hours, stress, stress_carried, ill, ill_severity, care, bed_time, wake_time, ] for inp in debt_inputs: inp.change( fn=compute_running_estimate, inputs=debt_inputs, outputs=debt_pill, ) # Preset scenario buttons: fill form then auto-run analysis _PRESET_FILL_OUTPUTS = [ alcohol, alcohol_type, alcohol_count, alcohol_details, training, training_area, training_intensity, training_details, sleep, sleep_hours, sleep_details, stress, stress_carried, stress_details, ill, ill_severity, ill_details, care, bed_time, wake_time, ] for preset_btn, fill_fn in [ (preset_bad_night, fill_bad_night), (preset_recovery, fill_recovery_day), (preset_hit_hard, fill_hit_it_hard), (preset_sick, fill_sick), ]: preset_btn.click( fn=fill_fn, outputs=_PRESET_FILL_OUTPUTS, ).then( fn=run_analysis_stream, inputs=ANALYSIS_INPUTS, outputs=ANALYSIS_OUTPUTS, ) # Clear all button clear_btn.click( fn=clear_all_form, outputs=_PRESET_FILL_OUTPUTS, ) # Main Calculate button analyze_btn.click( fn=run_analysis_stream, inputs=ANALYSIS_INPUTS, outputs=ANALYSIS_OUTPUTS, ) # Save to compare COMPARE_INPUTS = [ alcohol, alcohol_type, alcohol_count, training, training_area, training_intensity, sleep, sleep_hours, stress, stress_carried, ill, ill_severity, care, bed_time, wake_time, ] save_compare_btn.click( fn=save_compare, inputs=[comparisons_state] + COMPARE_INPUTS, outputs=[comparisons_state, compare_output], ) # Clear all comparisons clear_compare_btn.click( fn=clear_comparisons, outputs=[comparisons_state, compare_output], ) gr.HTML(f""" """) if __name__ == "__main__": demo.launch()