| """ |
| 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 |
|
|
| |
|
|
| BRAND_PRIMARY = "#EA580C" |
| BRAND_SECONDARY = "#F59E0B" |
| RECOVERY_GREEN = "#4ADE80" |
|
|
| |
| 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 = 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; }} |
| }} |
| """ |
|
|
| |
|
|
|
|
| def render_hero(score: int, verdict: str, is_sample: bool = False) -> str: |
| color, _, _ = debt_tier(score) |
| badge = "" |
| if is_sample: |
| badge = ( |
| '<div class="sample-badge">' |
| '<span class="dot"></span>' |
| "SAMPLE RESULT Β· choose a scenario or click <strong>Calculate Orbura</strong> for your own" |
| "</div>" |
| ) |
| return f""" |
| {badge} |
| <div class="orb-wrap" style="color: {color};"> |
| <div class="debt-hero-label">Orbura Score</div> |
| <div class="debt-hero" style="color: {color};">{score}</div> |
| <div class="debt-verdict" style="color: var(--text-primary);">{html.escape(verdict)}</div> |
| </div> |
| """ |
|
|
|
|
| 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""" |
| <div class="sys-meter {'is-primary' if is_primary else ''}" style="border-color: {accent_soft if is_primary else 'var(--border)'};"> |
| <div class="sys-glyph" style="background: {glyph_bg}; color: {bar_color};">{glyph}</div> |
| <div class="sys-body"> |
| <div class="sys-row"> |
| <span class="sys-label">{s.icon} {html.escape(s.label)}</span> |
| <span class="sys-time" style="color: {bar_color};">clears {s.cleared_at}</span> |
| </div> |
| <div class="sys-bar"> |
| <div class="sys-bar-fill" style="width: {pct}%; background: {bar_color};"></div> |
| </div> |
| <div class="sys-cause">{html.escape(s.cause_text)}</div> |
| </div> |
| </div> |
| """) |
|
|
| return f""" |
| <div> |
| <div class="section-label">Five-system breakdown</div> |
| {''.join(rows)} |
| </div> |
| """ |
|
|
|
|
| def render_prescription(system_scores, debt_score: int) -> str: |
| if debt_score < 20: |
| return f""" |
| <div class="coach-block"> |
| <div class="coach-header">Recovery Protocol Β· Cleared</div> |
| <div class="coach-body" style="color: var(--recovery-green);">All five systems below threshold. Maintain the streak.</div> |
| </div> |
| """ |
|
|
| |
| 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] |
|
|
| |
| 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 '<div class="proto-conn"></div>' |
| steps_html += f""" |
| <div class="proto-step"> |
| <div class="proto-rail"> |
| <div class="proto-num" style="color: {color};">{str(num).zfill(2)}</div> |
| {connector} |
| </div> |
| <div style="flex: 1; padding-bottom: {'0' if is_last(i) else '16px'};"> |
| <div class="proto-window" style="color: {color};">{window} Β· {html.escape(active[i].label)}</div> |
| <p class="proto-action">{html.escape(action)}</p> |
| </div> |
| </div> |
| """ |
|
|
| return f""" |
| <div class="coach-block"> |
| <div class="coach-header">Recovery Protocol</div> |
| {steps_html} |
| </div> |
| """ |
|
|
|
|
| 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""" |
| <div class="sci-card" style="color: {accent};"> |
| <p class="sci-fact">{html.escape(fact)}</p> |
| <p class="sci-cite">β {html.escape(cite)}</p> |
| </div> |
| """ for accent, fact, cite in items |
| ) |
|
|
| return f""" |
| <div style="margin-top: 24px;"> |
| <div class="section-label">The science</div> |
| {cards} |
| </div> |
| """ |
|
|
|
|
| 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""" |
| <div class="face-pill"> |
| <div> |
| <div class="face-label">Face Scan</div> |
| <div class="face-num" style="color: {status_color};">{face_stress:.0f}<span style="font-size: 14px; color: var(--text-faint);">/100</span></div> |
| </div> |
| <div style="flex: 1;"> |
| <span class="face-status" style="color: {status_color}; background: {status_bg};">{status_text}</span> |
| <div class="face-bar"> |
| <div class="face-bar-fill" style="width: {pct}%; background: {status_color};"></div> |
| </div> |
| <div class="face-meta" style="margin-top: 6px; font-style: italic;">Processed on-device. No biometric data transmitted.</div> |
| </div> |
| </div> |
| """ |
|
|
|
|
| def render_face_scan_placeholder() -> str: |
| """Call-to-action placeholder for face scan before the user runs an analysis.""" |
| return f""" |
| <div class="face-pill-placeholder"> |
| <div class="icon">π·</div> |
| <div class="label">Capture a photo or use your webcam</div> |
| <div class="sub">MediaPipe FaceMesh β 7 features β stress MLP Β· all on-device</div> |
| </div> |
| """ |
|
|
|
|
| |
|
|
|
|
| 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" |
|
|
| |
| 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 = '<span class="tl-now">Β· now</span>' if p["hour"] == 0 else "" |
|
|
| |
| 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'<span class="tl-seg" style="flex:{share:.1f};background:{sys["color"]}" title="{sys_label}: {sys["score"]} pts"></span>') |
| else: |
| segments = [] |
|
|
| seg_html = "".join(segments) if segments else f'<span class="tl-bar" style="width:100%;background:{p["color"]}"></span>' |
|
|
| |
| dom_html = "" |
| if p["dominant"] and p["dominant_glyph"]: |
| dom_color = SYSTEM_ACCENTS.get(p["dominant"], ("var(--text-secondary)",))[0] |
| dom_html = f'<span class="tl-dom" style="color:{dom_color};">{p["dominant_glyph"]}</span>' |
|
|
| rows.append(f""" |
| <div class="tl-row"> |
| <span class="tl-label">{p['label']}{now_tag}</span> |
| <div class="tl-bar-wrap"> |
| <div class="tl-bar-stack" style="width:{bar_pct:.0f}%;"> |
| {seg_html} |
| </div> |
| </div> |
| <span class="tl-score" style="color: {p['color']};">{p['score']}</span> |
| {dom_html} |
| </div> |
| """) |
|
|
| |
| 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'<span class="tl-leg" style="color:{accent};"><span class="tl-leg-swatch" style="background:{accent};"></span>{glyph} {label}</span>') |
|
|
| legend_html = f'<div class="tl-legend">{" ".join(legend_items)}</div>' if len(legend_items) > 0 else "" |
|
|
| return f""" |
| <div class="tl-wrap"> |
| <div class="section-label" style="margin-bottom: 10px;">Recovery forecast</div> |
| {legend_html} |
| {"".join(rows)} |
| </div> |
| """ |
|
|
|
|
| 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""" |
| <div class="plan-line"> |
| <span class="plan-tag" style="color: {color}; border-color: {color}40; background: {color}10;">{label}</span> |
| <span class="plan-text">{html.escape(rest)}</span> |
| </div> |
| """) |
|
|
| return f""" |
| <div class="plan-block"> |
| <div class="section-label">Recovery plan <span class="plan-source">SmolLM2-360M</span></div> |
| {''.join(rendered_lines)} |
| </div> |
| """ |
|
|
|
|
| def render_counterfactual(cf: dict | None) -> str: |
| if not cf: |
| return "" |
| accent = SYSTEM_ACCENTS.get(cf["system"], ("var(--text-secondary)",))[0] |
| return f""" |
| <div class="cf-block" style="border-left-color: {accent};"> |
| <span class="cf-label" style="color: {accent};">WHAT WOULD CHANGE THIS</span> |
| <span class="cf-body"> |
| If you had <strong style="color: var(--text-primary);">{html.escape(cf['lever_label'])}</strong>, |
| <strong style="color: {accent};">{html.escape(cf['system_label'])}</strong> debt would drop |
| from <strong style="color: var(--text-primary);">{cf['from_score']}</strong> to |
| <strong style="color: var(--recovery-green);">{cf['to_score']}</strong>. |
| </span> |
| </div> |
| """ |
|
|
|
|
| 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""" |
| <div class="trace-step is-{status}"> |
| <span class="trace-dot"></span> |
| <span style="flex: 1;">{html.escape(label)}</span> |
| <span style="color: var(--text-faint); font-size: 10px;">{html.escape(message)}</span> |
| </div> |
| """) |
| body = "".join(items) |
| else: |
| body = f""" |
| <div class="trace-step"> |
| <span class="trace-dot" style="background: var(--text-faint);"></span> |
| <span style="flex: 1; color: var(--text-faint);">parse_stressors</span> |
| </div> |
| <div class="trace-step"> |
| <span class="trace-dot" style="background: var(--text-faint);"></span> |
| <span style="flex: 1; color: var(--text-faint);">compute_live_score</span> |
| </div> |
| <div class="trace-step"> |
| <span class="trace-dot" style="background: var(--text-faint);"></span> |
| <span style="flex: 1; color: var(--text-faint);">face_scan</span> |
| </div> |
| <div class="trace-step"> |
| <span class="trace-dot" style="background: var(--text-faint);"></span> |
| <span style="flex: 1; color: var(--text-faint);">llm_coach</span> |
| </div> |
| <div class="trace-step" style="margin-top: 8px;"> |
| <span class="ready-pulse"></span> |
| <span style="color: var(--text-muted); font-size: 10px;">Awaiting analysis</span> |
| </div> |
| """ |
|
|
| return f""" |
| <div class="trace-wrap"> |
| <div class="section-label">How this was generated</div> |
| {body} |
| </div> |
| """ |
|
|
|
|
| def render_empty_state() -> str: |
| return f""" |
| <div class="empty-state"> |
| <div class="icon">π«</div> |
| <div class="label">Your debt will appear here</div> |
| <div class="sub">Log your stressors on the left. Tap calculate.</div> |
| </div> |
| """ |
|
|
|
|
| |
|
|
|
|
| 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'<span class="debt-pill" style="color: {color};">Running debt Β· <strong>{score}</strong>/100</span>' |
|
|
|
|
| |
|
|
|
|
| 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, |
| "", "", |
| ) |
|
|
|
|
| |
|
|
|
|
| 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""" |
| <div class="coach-block"> |
| <div class="coach-header"> |
| <span>AI Recovery Coach</span> |
| <span class="coach-pill">SmolLM2-360M Β· local</span> |
| </div> |
| <div class="coach-body" style="color: var(--text-faint);"> |
| <span class="ready-pulse"></span> |
| Sample advice shown. Tap <strong style="color: var(--text-secondary);">Calculate Orbura</strong> for your own. |
| </div> |
| </div> |
| """ |
|
|
|
|
| |
|
|
|
|
| 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 = "" |
|
|
| |
| 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'<div class="empty-state"><div class="icon">π«</div>' |
| f'<div class="label">Log at least one stressor</div>' |
| f'<div class="sub">Tap a checkbox on the left to begin.</div></div>' |
| ) |
| 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...") |
|
|
| |
| 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...") |
|
|
| |
| timeline_html = render_timeline(compute_debt_timeline(system_scores)) |
|
|
| |
| 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")) |
|
|
| |
| 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...") |
|
|
| |
| _, 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) |
|
|
| |
| 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""" |
| <div class="coach-block"> |
| <div class="coach-header"> |
| <span>AI Recovery Coach</span> |
| <span class="coach-pill">SmolLM2-360M Β· local</span> |
| </div> |
| <div class="coach-body" style="color: var(--text-faint);"> |
| <span class="ready-pulse"></span>Ready. Tap <strong style="color: var(--text-secondary);">Calculate Orbura</strong> to stream advice. |
| </div> |
| </div> |
| """ |
|
|
|
|
| def _coach_with_cursor(text: str) -> str: |
| safe = html.escape(text) |
| return f""" |
| <div class="coach-block"> |
| <div class="coach-header"><span>AI Recovery Coach</span><span class="coach-pill">SmolLM2-360M Β· local</span></div> |
| <div class="coach-body">{safe}<span class="coach-cursor"></span></div> |
| </div> |
| """ |
|
|
|
|
| |
|
|
|
|
| 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""" |
| <div class="cmp-sys"> |
| <span class="cmp-sys-glyph" style="color: {accent};">{sys['glyph']}</span> |
| <div class="cmp-sys-bar"><div class="cmp-sys-fill" style="width:{pct}%;background:{accent}"></div></div> |
| </div>""" |
| cards.append(f""" |
| <div class="cmp-card"> |
| <div class="cmp-hero" style="color: {color};">{c['score']}</div> |
| <div class="cmp-label">{html.escape(c['label'])}</div> |
| <div class="cmp-time">{html.escape(c['timestamp'])}</div> |
| {systems_html} |
| </div> |
| """) |
| return f'<div class="cmp-row">{"".join(cards)}</div>' |
|
|
|
|
| def save_compare( |
| comparisons, |
| 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 [], "" |
|
|
|
|
| |
|
|
| SAMPLE_HERO, SAMPLE_PLAN, SAMPLE_METERS, SAMPLE_FACE, SAMPLE_TIMELINE, SAMPLE_RX, SAMPLE_TRACE, SAMPLE_CF, SAMPLE_COACH = render_sample_preview() |
|
|
| |
|
|
| with gr.Blocks(title="Orbura", theme=APP_THEME, css=CUSTOM_CSS) as demo: |
| gr.HTML(f""" |
| <div class="app-header"> |
| <h1 class="app-title">π« Orbura</h1> |
| <p class="app-subtitle">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.</p> |
| <div class="attr-row"> |
| <span class="attr-pill"><span class="dot"></span>SmolLM2-360M Β· local</span> |
| <a class="attr-pill" href="https://huggingface.co/HuggingFaceTB/SmolLM2-360M-Instruct">360M params Β· 250MB RAM</a> |
| <a class="attr-pill" href="https://github.com/udirobert/orbura">Built with OpenAI Codex</a> |
| </div> |
| </div> |
| <div class="value-grid"> |
| <div class="value-card"> |
| <div class="value-kicker">Score</div> |
| <div class="value-copy">A 0β100 read on the recovery load your body is carrying right now.</div> |
| </div> |
| <div class="value-card"> |
| <div class="value-kicker">Systems</div> |
| <div class="value-copy">See which body systems are driving the debt and when they clear.</div> |
| </div> |
| <div class="value-card"> |
| <div class="value-kicker">Plan</div> |
| <div class="value-copy">Get the next action, secondary support, and what to avoid today.</div> |
| </div> |
| </div> |
| """) |
|
|
| with gr.Group(elem_classes="scenario-panel"): |
| gr.HTML('<div class="section-label">Start with a scenario</div>') |
| gr.HTML('<p class="start-hint">Fastest path: choose a preset to see Orbura calculate a realistic recovery plan, or customize your own day below.</p>') |
| 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('<div class="section-label" style="margin-top: 4px;">Log todayβs stressors</div>') |
|
|
| |
| debt_pill = gr.HTML(value='<span class="debt-pill" style="color: var(--text-muted);">Running debt Β· <strong>0</strong>/100</span>') |
|
|
| 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('<div class="section-label" style="margin-top: 20px;">Sleep timing</div>') |
| 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('<div class="section-label" style="margin-top: 20px;">Face scan <span style="font-weight: 400; text-transform: none; letter-spacing: normal; color: var(--text-muted);">(optional)</span></div>') |
| 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('<div class="section-label cmp-section">Saved comparisons</div>') |
| 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, |
| ] |
|
|
| |
|
|
| |
| 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) |
|
|
| |
| 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_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_btn.click( |
| fn=clear_all_form, |
| outputs=_PRESET_FILL_OUTPUTS, |
| ) |
|
|
| |
| analyze_btn.click( |
| fn=run_analysis_stream, |
| inputs=ANALYSIS_INPUTS, |
| outputs=ANALYSIS_OUTPUTS, |
| ) |
|
|
| |
| 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_compare_btn.click( |
| fn=clear_comparisons, |
| outputs=[comparisons_state, compare_output], |
| ) |
|
|
| gr.HTML(f""" |
| <div class="app-footer"> |
| Orbura uses <a href="https://huggingface.co/HuggingFaceTB/SmolLM2-360M-Instruct">SmolLM2-360M-Instruct</a> (360M parameters) running locally via HuggingFace Transformers.<br> |
| Face analysis uses <a href="https://google.github.io/mediapipe/solutions/face_mesh">MediaPipe FaceMesh</a>. No biometric data leaves your device.<br> |
| Built with <a href="https://openai.com/index/openai-codex/">OpenAI Codex</a>. Source: <a href="https://github.com/udirobert/orbura">github.com/udirobert/orbura</a>.<br> |
| Submitted to the <a href="https://huggingface.co/spaces/build-small-hackathon/orbura">Build Small Hackathon</a>. |
| </div> |
| """) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|