diff --git "a/app.py" "b/app.py" new file mode 100644--- /dev/null +++ "b/app.py" @@ -0,0 +1,2032 @@ +import streamlit as st +import streamlit.components.v1 as components +from openai import OpenAI +import os, json, re, csv, smtplib, logging +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.base import MIMEBase +from email import encoders +from reportlab.lib.pagesizes import letter +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.units import inch +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable, KeepTogether, PageBreak +from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT +from reportlab.lib import colors +from io import BytesIO, StringIO +from datetime import datetime +from config import * + +# ── OpenAI client (lazy — only created when actually needed) +_openai_client = None +def get_openai_client(): + global _openai_client + if _openai_client is None: + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + st.error("**OPENAI_API_KEY not set.** Add it as an environment variable or a Hugging Face Space secret.") + st.stop() + _openai_client = OpenAI(api_key=api_key) + return _openai_client + +# ── Shorthand color references from config +NAVY = BRAND["colors"]["navy"] +TEAL = BRAND["colors"]["teal"] +SKY = BRAND["colors"]["sky"] +ORANGE = BRAND["colors"]["orange"] +AMBER = BRAND["colors"]["amber"] +WHITE = BRAND["colors"]["white"] + +# ============================================================================= +# SEX-AWARE SERVICE DISPLAY NAME +# ============================================================================= +def sex_label(service_name, patient_sex=""): + """Swap HRT ↔ TRT based on patient sex for display purposes.""" + if service_name == "HRT" and patient_sex == "Male": + return "TRT" + return service_name + +def sex_moa(service_name, patient_sex=""): + """Return the correct MOA tooltip text based on patient sex.""" + display = sex_label(service_name, patient_sex) + return SERVICE_MOA.get(display, SERVICE_MOA.get(service_name, "")) + +# ============================================================================= +# CONTRAINDICATION CHECKING +# ============================================================================= +def check_contraindications(service_list, patient_flags): + """Check which services trigger contraindication flags for the patient.""" + if "None of the above" in patient_flags or not patient_flags: + return {} + hits = {} + for service in service_list: + triggered = [f for f in SERVICE_CONTRAINDICATIONS.get(service, []) if f in patient_flags] + if triggered: + hits[service] = triggered + return hits + + +def get_stack_contraindication_summary(stack_name, patient_flags): + """Get contraindication summary for a specific wellness stack.""" + return check_contraindications(WELLNESS_PROGRAMS[stack_name]["services"], patient_flags) + + +# ============================================================================= +# PRICING AND GOAL MAPPING +# ============================================================================= +def get_pricing_summary(stack_name): + """Get a readable pricing summary for a wellness program.""" + stack = WELLNESS_PROGRAMS[stack_name] + tiers = stack["tiers"] + sorted_tiers = sorted(tiers.items(), key=lambda x: x[1]["sort_order"]) + # Return the mid-tier price as representative + mid = sorted_tiers[len(sorted_tiers) // 2] + return { + "tier_name": mid[0], + "monthly_26wk": mid[1]["26_week"], + "monthly_52wk": mid[1]["52_week"], + "all_tiers": tiers, + } + + +def map_goals_to_targets(goals_data): + """Map patient goals and symptoms to target health categories.""" + ts = {cat: 0 for cat in TARGET_CATEGORIES} + + # Primary goals mapping + for goal in goals_data.get("primary_goals", []): + for target, weight in PRIMARY_GOALS.get(goal, []): + ts[target] += weight + + # Current symptoms mapping + for symptom in goals_data.get("current_issues", []): + for target, weight in CURRENT_SYMPTOMS.get(symptom, []): + ts[target] += weight + + # Energy/recovery mapping + energy = goals_data.get("energy_recovery", "") + for target, weight in ENERGY_RECOVERY_OPTIONS.get(energy, []): + ts[target] += weight + + return ts + + +def recommend_programs(target_scores, num_primary_goals, patient_sex="any", patient_flags=None): + """ + Recommend wellness programs filtered by patient sex and with contraindication awareness. + + Args: + target_scores: Dict of target category scores + num_primary_goals: Number of primary goals selected + patient_sex: "Male" or "Female" for sex-based filtering + patient_flags: List of contraindication flags + + Returns: + Tuple of (recommended_stack_names, top_target_categories) + """ + if patient_flags is None: + patient_flags = [] + + # Get top 3 targets by score + sorted_targets = sorted(target_scores.items(), key=lambda x: x[1], reverse=True) + top_targets = [t[0] for t in sorted_targets if t[1] > 0][:3] + + # Filter programs by sex using the sex_filter field in config + sex_key = patient_sex.lower() if patient_sex else "any" + eligible = { + sn: si for sn, si in WELLNESS_PROGRAMS.items() + if si.get("sex_filter", "any") in (sex_key, "any") + } + + # Score eligible programs by how well they target the top goals + stack_scores = { + sn: sum(target_scores.get(t, 0) * 2 for t in si["targets"] if t in top_targets) + for sn, si in eligible.items() + } + + # Sort by robustness first (lowest display_order = most comprehensive), + # then by goal-match score as tiebreaker within same robustness tier. + # This ensures the most robust package always leads (high-to-low strategy). + sorted_stacks = sorted( + stack_scores.items(), + key=lambda x: (eligible[x[0]].get("display_order", 50), -x[1]), + ) + + # Determine how many stacks to recommend + n = min(num_primary_goals, 2) if num_primary_goals <= 2 else 3 + recommended = [s[0] for s in sorted_stacks[:n]] + + # Contraindication-aware fallback: if ALL recommended stacks have contraindication hits, + # find highest-scoring stack with no contraindication hits and suggest it as an alternative + active_flags = [f for f in patient_flags if f != "None of the above"] + if active_flags and recommended: + all_have_contras = all( + check_contraindications(WELLNESS_PROGRAMS[sn]["services"], active_flags) + for sn in recommended + ) + if all_have_contras: + # Look for a fallback with no contraindication hits + for sn, score in sorted_stacks: + if sn not in recommended: + hits = check_contraindications(WELLNESS_PROGRAMS[sn]["services"], active_flags) + if not hits: + recommended.append(sn) + break + + # All sex-eligible programs (for "also available" display) — already in robustness order + all_eligible = [s[0] for s in sorted_stacks] + + return recommended, top_targets, all_eligible + + +# ============================================================================= +# GPT WELLNESS PATHWAY GENERATOR +# ============================================================================= +def generate_wellness_pathway(intake_data, recommended_stacks, contraindication_flags): + """Generate a personalized wellness pathway using GPT-4.""" + def price_info(sn): + stack = WELLNESS_PROGRAMS[sn] + tiers = stack["tiers"] + sorted_t = sorted(tiers.items(), key=lambda x: x[1]["sort_order"]) + low = sorted_t[-1][1]["26_week"] # Cheapest tier + high = sorted_t[0][1]["26_week"] # Most expensive tier + return f"${low:,.2f} – ${high:,.2f}/month (26-week commitment, 15% off at 52 weeks)" + + stacks_summary = "\n\n".join([ + f"**{sn}**: {WELLNESS_PROGRAMS[sn]['description']}\n" + f"Services: {', '.join(WELLNESS_PROGRAMS[sn]['services'])}\n" + f"Targets: {', '.join(WELLNESS_PROGRAMS[sn]['targets'])}\n" + f"Pricing Range: {price_info(sn)}\n" + f"Cycle: {WELLNESS_PROGRAMS[sn]['cycle']}" + for sn in recommended_stacks + ]) + + contra_context = "" + active = [f for f in contraindication_flags if f != "None of the above"] + if active: + contra_context = ( + f"\n\nCONTRAINDICATION FLAGS REPORTED BY PATIENT: {', '.join(active)}\n" + "Please address these flags explicitly in the Safety & Monitoring section. " + "Note any services requiring extra caution or modification. Do NOT recommend discontinuing — " + "advise physician-supervised evaluation of each flagged item." + ) + + intake_summary = ( + f"Patient Demographics:\n" + f"- Name: {intake_data['name']} | Sex: {intake_data['sex']} | Age: {intake_data['age']}\n\n" + f"Primary Goals: {', '.join(intake_data['primary_goals'])}\n" + f"Energy & Recovery: {intake_data['energy_recovery']}\n" + f"Current Issues: {', '.join(intake_data['current_issues']) if intake_data['current_issues'] else 'None reported'}\n" + f"Physical Activity: {intake_data['activity_level']}\n" + f"Medical Considerations: {intake_data['medical_considerations']}" + f"{contra_context}\n\n" + f"RECOMMENDED PROGRAM(S):\n{stacks_summary}" + ) + + try: + response = get_openai_client().chat.completions.create( + model=GPT_CONFIG["model"], + messages=[ + {"role": "system", "content": PATHWAY_SYSTEM_PROMPT}, + {"role": "user", "content": f"Create a program recommendation:\n\n{intake_summary}"} + ], + temperature=GPT_CONFIG["temperature"], + ) + return response.choices[0].message.content + except Exception as e: + return f"Error generating pathway: {str(e)}\n\nPlease ensure your OpenAI API key is configured." + + +# ============================================================================= +# WELLNESS PATHWAY HTML RENDERER +# ============================================================================= +def render_pathway_html(raw_text, stack_services, patient_sex=""): + """Convert GPT markdown to rich HTML with service subsection cards.""" + lines = raw_text.split("\n") + output = [] + service_set = set(stack_services) + in_ul = False + in_ol = False + in_pep = False + pep_bullets = [] + + def close_lists(): + nonlocal in_ul, in_ol + if in_ul: output.append(""); in_ul = False + if in_ol: output.append(""); in_ol = False + + def close_pep_card(): + nonlocal in_pep + if in_pep: + if pep_bullets: + output.append('") + pep_bullets.clear() + output.append("") # close pw-service-card + in_pep = False + + def fmt_inline(text): + text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) + text = re.sub(r'\*([^*]+)\*', r'\1', text) + for svc in service_set: + display = sex_label(svc, patient_sex) + moa = sex_moa(svc, patient_sex) + if moa and svc in text: + safe_moa = moa.replace('"', '"').replace("'", "'") + text = text.replace( + svc, + f'{display}', + 1, + ) + return text + + output.append('
') + + for raw_line in lines: + stripped = raw_line.strip() + if not stripped: + close_lists() + if not in_pep: + output.append('
') + continue + + # ── Major section **Bold** ───────────────────────────────── + if stripped.startswith("**") and stripped.endswith("**") and stripped.count("**") == 2: + close_lists() + close_pep_card() + clean = stripped.replace("**", "").strip() + output.append(f'
{clean}
') + continue + + # ── Service subsection ### Name ──────────────────────────── + if stripped.startswith("###"): + close_lists() + close_pep_card() # close any previously open card first + title = stripped.lstrip("#").strip() + title = re.split(r'\s*[\(\[]', title)[0].strip() + display = sex_label(title, patient_sex) + moa = sex_moa(title, patient_sex) + safe_moa = moa.replace('"', '"').replace("'", "'") if moa else "" + tip_attr = f' data-tip="{safe_moa}"' if safe_moa else "" + output.append('
') + output.append( + f'
' + f'{display}
' + ) + in_pep = True + continue + + # ── H2 ## ────────────────────────────────────────────────── + if stripped.startswith("##"): + close_lists() + close_pep_card() + clean = stripped.lstrip("#").replace("**", "").strip() + output.append(f'

{fmt_inline(clean)}

') + continue + + # ── Bullet ───────────────────────────────────────────────── + if stripped and stripped[0] in ("-", "*", "•"): + close_lists() + if not in_ul and not in_pep: + output.append('"); in_ul = False + if not in_ol: + output.append('
    '); in_ol = True + output.append(f"
  1. {fmt_inline(nm.group(2))}
  2. ") + continue + + # ── Paragraph ─────────────────────────────────────────────── + close_lists() + if in_pep: + close_pep_card() + output.append(f'

    {fmt_inline(stripped)}

    ') + + close_lists() + close_pep_card() + output.append("
") # close pathway-wrapper + return "\n".join(output) + + +# ============================================================================= +# PDF GENERATOR — with robust markdown parsing +# ============================================================================= +def generate_wellness_pdf(pathway_content, patient_info, recommended_stacks, contraindication_flags=None): + """Generate a professional wellness PDF with robust markdown handling.""" + from reportlab.platypus import HRFlowable, KeepTogether, PageBreak + from reportlab.lib.enums import TA_RIGHT + + buffer = BytesIO() + doc = SimpleDocTemplate(buffer, pagesize=letter, + topMargin=0.55*inch, bottomMargin=0.6*inch, + leftMargin=0.65*inch, rightMargin=0.65*inch) + story = [] + pw = letter[0] - 1.3*inch # usable page width + + def ps(name, **kwargs): + return ParagraphStyle(name, **kwargs) + + # ── Shared colours ──────────────────────────────────────────────────────── + C_NAVY = colors.HexColor(NAVY) + C_TEAL = colors.HexColor(TEAL) + C_ORANGE = colors.HexColor(ORANGE) + C_AMBER = colors.HexColor(AMBER) + C_SKY = colors.HexColor(SKY) + C_LIGHT = colors.HexColor("#eef4f8") + C_CARD = colors.HexColor("#f0f6fa") + C_WARMWH = colors.HexColor("#fff8ec") + C_BORDER = colors.HexColor("#b8d0de") + + # ── Type styles ─────────────────────────────────────────────────────────── + brand_s = ps("Br", fontName="Helvetica-Bold", fontSize=9, textColor=C_ORANGE, spaceAfter=0, alignment=TA_CENTER) + title_s = ps("T", fontName="Helvetica-Bold", fontSize=22, leading=28, textColor=colors.white, spaceAfter=0, alignment=TA_CENTER) + sub_s = ps("Su", fontName="Helvetica", fontSize=10, textColor=C_SKY, spaceAfter=0, alignment=TA_CENTER) + meta_s = ps("Me", fontName="Helvetica", fontSize=9, leading=14, textColor=C_NAVY, spaceAfter=3) + h2_s = ps("H2", fontName="Helvetica-Bold", fontSize=11, textColor=colors.white, spaceBefore=0, spaceAfter=0) + h3_s = ps("H3", fontName="Helvetica-Bold", fontSize=10, textColor=C_NAVY, spaceBefore=4, spaceAfter=3) + body_s = ps("B", fontName="Helvetica", fontSize=9.5, leading=15, textColor=C_NAVY, spaceAfter=5) + bullet_s = ps("Bu", fontName="Helvetica", fontSize=9.5, leading=15, textColor=C_NAVY, leftIndent=14, spaceAfter=3) + num_s = ps("Nu", fontName="Helvetica", fontSize=9.5, leading=15, textColor=C_NAVY, leftIndent=14, spaceAfter=3) + warn_s = ps("W", fontName="Helvetica-Bold", fontSize=9, leading=13, textColor=C_ORANGE, spaceAfter=3) + disc_s = ps("Di", fontName="Helvetica-Oblique", fontSize=8, leading=12, textColor=colors.HexColor("#4a6070"), spaceAfter=4) + wk_lbl_s = ps("Wl", fontName="Helvetica-Bold", fontSize=7, leading=9, textColor=C_ORANGE, spaceAfter=2, alignment=TA_CENTER) + wk_s = ps("Wk", fontName="Helvetica-Bold", fontSize=16, leading=18, textColor=C_ORANGE, spaceAfter=3, alignment=TA_CENTER) + pr_s = ps("Pr", fontName="Helvetica", fontSize=8, leading=11, textColor=colors.HexColor("#555"), spaceAfter=1, alignment=TA_CENTER) + pep_hdr_s = ps("Ph", fontName="Helvetica-Bold", fontSize=10, textColor=colors.white, spaceBefore=0, spaceAfter=0) + sh_s = ps("Sh", fontName="Helvetica-Bold", fontSize=10, textColor=colors.white, spaceBefore=0, spaceAfter=0) + ft_s = ps("Ft", fontName="Helvetica", fontSize=8, leading=12, textColor=C_NAVY, spaceAfter=0, alignment=TA_CENTER) + sn_s = ps("Sn", fontName="Helvetica-Bold", fontSize=13, textColor=C_ORANGE, spaceAfter=0) + + # ── Helper: full-width coloured banner ──────────────────────────────────── + def banner(para, bg, line_below=None, pad_v=7, pad_h=12): + t = Table([[para]], colWidths=[pw]) + cmds = [ + ("BACKGROUND", (0,0),(-1,-1), bg), + ("LEFTPADDING", (0,0),(-1,-1), pad_h), + ("RIGHTPADDING",(0,0),(-1,-1), pad_h), + ("TOPPADDING", (0,0),(-1,-1), pad_v), + ("BOTTOMPADDING",(0,0),(-1,-1), pad_v), + ] + if line_below: + cmds.append(("LINEBELOW", (0,0),(-1,-1), 3, line_below)) + t.setStyle(TableStyle(cmds)) + return t + + def section_hdr(text): + """Navy banner with orange underline.""" + return KeepTogether([ + Spacer(1, 8), + banner(Paragraph(text.upper(), h2_s), C_NAVY, line_below=C_ORANGE, pad_v=8), + Spacer(1, 6), + ]) + + # ========================================================================= + # PAGE 1 — COVER / SUMMARY + # ========================================================================= + + hdr_tbl = Table([ + [Paragraph("IGNITE PERFORMANCE & HEALTH", brand_s)], + [Paragraph("Your Ignition Sequence™", title_s)], + [Paragraph("Your Program Recommendation", sub_s)], + ], colWidths=[pw]) + hdr_tbl.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(-1,-1), C_NAVY), + ("LEFTPADDING", (0,0),(-1,-1), 20), + ("RIGHTPADDING", (0,0),(-1,-1), 20), + ("TOPPADDING", (0,0),(0,0), 14), + ("BOTTOMPADDING", (0,0),(0,0), 4), + ("TOPPADDING", (0,1),(0,1), 4), + ("BOTTOMPADDING", (0,1),(0,1), 4), + ("TOPPADDING", (0,2),(0,2), 2), + ("BOTTOMPADDING", (0,2),(0,2), 14), + ("ALIGN", (0,0),(-1,-1), "CENTER"), + ("VALIGN", (0,0),(-1,-1), "MIDDLE"), + ])) + story.append(hdr_tbl) + story.append(HRFlowable(width="100%", thickness=5, color=C_ORANGE, spaceAfter=12)) + + # ── Patient info ───────────────────────────────────────────────────────── + name = patient_info.get("name", "") + age = patient_info.get("age", "") + sex = patient_info.get("sex", "") + date = patient_info.get("date", "") + + pi_tbl = Table([[ + Paragraph(f"Patient: {name}", meta_s), + Paragraph(f"Age: {age} Sex: {sex}", meta_s), + Paragraph(f"Date: {date}", meta_s), + ]], colWidths=[pw*0.42, pw*0.22, pw*0.36]) + pi_tbl.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(-1,-1), C_LIGHT), + ("BOX", (0,0),(-1,-1), 0.75, C_BORDER), + ("LINEBELOW", (0,0),(-1,-1), 2, C_ORANGE), + ("LEFTPADDING", (0,0),(-1,-1), 10), ("RIGHTPADDING", (0,0),(-1,-1), 10), + ("TOPPADDING", (0,0),(-1,-1), 9), ("BOTTOMPADDING",(0,0),(-1,-1), 9), + ("VALIGN", (0,0),(-1,-1), "MIDDLE"), + ])) + story.append(pi_tbl) + story.append(Spacer(1, 14)) + + # ── Contraindications (if any) ──────────────────────────────────────────── + active_flags = [f for f in (contraindication_flags or []) if f != "None of the above"] + if active_flags: + contra_items = [ + banner(Paragraph("⚠ PHYSICIAN REVIEW REQUIRED", h2_s), + colors.HexColor("#8B0000"), pad_v=9), + Spacer(1, 5), + Paragraph( + "The following patient-reported flags require physician evaluation before initiating therapy. " + "These flags do not automatically disqualify a patient from therapy.", body_s), + ] + # Show what the patient reported + flags_str = " · ".join(active_flags) + contra_items.append(Paragraph(f"Reported: {flags_str}", warn_s)) + contra_items.append(Spacer(1, 4)) + + # Show stack-specific impacts + has_hits = False + for sn in recommended_stacks: + hits = get_stack_contraindication_summary(sn, active_flags) + if hits: + has_hits = True + contra_items.append(Paragraph(f"{sn} — affected services:", h3_s)) + for service, flags in hits.items(): + contra_items.append(Paragraph(f" • {service}: {', '.join(flags)}", warn_s)) + if not has_hits: + contra_items.append(Paragraph( + "No recommended services are directly contraindicated, but physician review is still required.", body_s)) + contra_items.append(Spacer(1, 10)) + story.append(KeepTogether(contra_items)) + + # ── Recommended stacks ──────────────────────────────────────────────────── + story.append(section_hdr("Recommended Program(s)")) + + # Service pill style + pep_pill_s = ps("Pp", fontName="Helvetica-Bold", fontSize=8, leading=11, + textColor=C_AMBER, spaceAfter=0, alignment=TA_CENTER) + + for sn in recommended_stacks: + ps_data = get_pricing_summary(sn) + s = WELLNESS_PROGRAMS[sn] + targets_str = " · ".join(s["targets"]) + + # Build service pills + pep_cells = [Paragraph(f" {p} ", pep_pill_s) for p in s["services"]] + n_peps = len(s["services"]) + pill_col_w = min(1.15*inch, (pw - 2.6*inch) / max(n_peps, 1)) + pep_pills_tbl = Table([pep_cells], colWidths=[pill_col_w]*n_peps) + pep_pills_tbl.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(-1,-1), C_NAVY), + ("BOX", (0,0),(-1,-1), 0, colors.white), + ("INNERGRID", (0,0),(-1,-1), 4, colors.white), + ("ALIGN", (0,0),(-1,-1), "CENTER"), + ("VALIGN", (0,0),(-1,-1), "MIDDLE"), + ("TOPPADDING", (0,0),(-1,-1), 4), + ("BOTTOMPADDING", (0,0),(-1,-1), 4), + ("LEFTPADDING", (0,0),(-1,-1), 6), + ("RIGHTPADDING", (0,0),(-1,-1), 6), + ])) + + # Left: description + targets + cycle + service pills + desc_para = Paragraph( + f"{s['description']}

" + f"Targets: {targets_str}
" + f"Cycle: {s['cycle']}", meta_s) + + left_content = Table([ + [desc_para], + [Spacer(1, 4)], + [pep_pills_tbl], + ], colWidths=[pw - 2.0*inch]) + left_content.setStyle(TableStyle([ + ("LEFTPADDING", (0,0),(-1,-1), 0), + ("RIGHTPADDING", (0,0),(-1,-1), 0), + ("TOPPADDING", (0,0),(-1,-1), 0), + ("BOTTOMPADDING", (0,0),(-1,-1), 0), + ])) + + # Right: pricing box - mid-tier as representative (convert monthly to weekly) + wk_price_26 = ps_data['monthly_26wk'] * 12.0 / 52.0 + wk_price_52 = ps_data['monthly_52wk'] * 12.0 / 52.0 + price_inner = Table([ + [Paragraph("52-WEEK · PER WEEK", wk_lbl_s)], + [Paragraph(f"${wk_price_52:,.2f}", wk_s)], + [HRFlowable(width="80%", thickness=0.5, color=C_BORDER, spaceAfter=2)], + [Paragraph("26-WEEK · PER WEEK", wk_lbl_s)], + [Paragraph(f"${wk_price_26:,.2f}", pr_s)], + ], colWidths=[1.55*inch]) + price_inner.setStyle(TableStyle([ + ("ALIGN", (0,0),(-1,-1), "CENTER"), + ("TOPPADDING", (0,0),(-1,-1), 2), + ("BOTTOMPADDING",(0,0),(-1,-1), 2), + ])) + + card_tbl = Table([[left_content, price_inner]], colWidths=[pw - 1.7*inch, 1.7*inch]) + card_tbl.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(0,0), C_CARD), + ("BACKGROUND", (1,0),(1,0), C_WARMWH), + ("BOX", (0,0),(-1,-1),0.75, C_BORDER), + ("LINEBEFORE", (1,0),(1,-1), 2, C_ORANGE), + ("LEFTPADDING", (0,0),(0,-1), 10), ("RIGHTPADDING", (0,0),(0,-1), 10), + ("TOPPADDING", (0,0),(-1,-1),10), ("BOTTOMPADDING",(0,0),(-1,-1),10), + ("VALIGN", (0,0),(0,0), "TOP"), + ("VALIGN", (1,0),(1,0), "MIDDLE"), + ])) + + # Stack name banner + sn_banner = banner(Paragraph(f" {sn}", sn_s), colors.HexColor("#012233"), line_below=C_TEAL) + + stack_block = KeepTogether([ + sn_banner, + card_tbl, + Spacer(1, 12), + ]) + story.append(stack_block) + + story.append(Paragraph( + "* Pricing shown is the mid-tier option. Multiple tier options available with different service levels.", disc_s)) + story.append(Spacer(1, 6)) + + # ========================================================================= + # PAGE BREAK before the protocol narrative + # ========================================================================= + story.append(PageBreak()) + + # ── Protocol narrative with robust markdown parsing ────────────────────── + + hit_treatment_page = False + current_pep_hdr = None + pending_bullets = [] + + skip_headings = {"how your stack works", "your personalized wellness protocol", + "your wellness protocol — how it works", "your wellness protocol - how it works"} + + # Condensed styles for the final summary page + cond_bullet_s = ps("Cb", fontName="Helvetica", fontSize=8.5, leading=12, + textColor=C_NAVY, leftIndent=12, spaceAfter=2) + cond_sub_bullet_s = ps("Csb", fontName="Helvetica", fontSize=8, leading=11, + textColor=C_NAVY, leftIndent=26, spaceAfter=1) + cond_num_s = ps("Cn", fontName="Helvetica", fontSize=8.5, leading=12, + textColor=C_NAVY, leftIndent=12, spaceAfter=3) + cond_body_s = ps("CB", fontName="Helvetica", fontSize=8.5, leading=12, + textColor=C_NAVY, spaceAfter=3) + cond_sh_s = ps("CS", fontName="Helvetica-Bold", fontSize=9, + textColor=colors.white, spaceBefore=0, spaceAfter=0) + + sub_bullet_s = ps("Sb", fontName="Helvetica", fontSize=9, leading=14, + textColor=C_NAVY, leftIndent=28, spaceAfter=2) + + for raw_line in pathway_content.split("\n"): + line = raw_line.strip() + indent = len(raw_line) - len(raw_line.lstrip()) + + if not line: + if current_pep_hdr is None and not hit_treatment_page: + story.append(Spacer(1, 2)) + elif hit_treatment_page: + story.append(Spacer(1, 1)) + continue + + # ── Service subsection ### Name ────────────────────────────────────── + if line.startswith("###"): + # Flush previous service block + if current_pep_hdr is not None: + story.append(KeepTogether(list(current_pep_hdr) + list(pending_bullets))) + pending_bullets.clear() + + pep_name = re.split(r'\s*[\(\[]', line.replace("###","").strip())[0].strip() + pep_bar = banner(Paragraph(f" {pep_name}", pep_hdr_s), + C_TEAL, line_below=C_NAVY, pad_v=5) + moa = SERVICE_MOA.get(pep_name, "") + moa_paras = [] + if moa: + moa_paras = [Paragraph(f"{moa}", disc_s)] + current_pep_hdr = [Spacer(1, 5), pep_bar] + moa_paras + pending_bullets = [] + continue + + # ── Major section **Bold** or **Bold:** – ROBUST HANDLING ──────────── + is_section = False + if line.startswith("**"): + # Handle trailing colon variants: **Header** or **Header:** + test = line.rstrip(":").rstrip() + if test.endswith("**") and test.count("**") == 2: + is_section = True + + if is_section: + # Flush any open service block + if current_pep_hdr is not None: + story.append(KeepTogether(list(current_pep_hdr) + list(pending_bullets))) + pending_bullets.clear() + current_pep_hdr = None + + clean = line.replace("**","").rstrip(":").strip() + + # Skip redundant headings + if clean.lower() in skip_headings: + continue + + # Treatment Protocol triggers a new page + if not hit_treatment_page and "Treatment Protocol" in clean: + hit_treatment_page = True + story.append(PageBreak()) + + sec_bar = banner(Paragraph(f" {clean}", cond_sh_s), + colors.HexColor("#034a6e"), line_below=C_ORANGE, + pad_v=5 if hit_treatment_page else 6) + spacer_before = 6 if hit_treatment_page else 10 + spacer_after = 3 if hit_treatment_page else 5 + story.append(KeepTogether([Spacer(1, spacer_before), sec_bar, Spacer(1, spacer_after)])) + continue + + # ── H2 ## ──────────────────────────────────────────────────────────── + if line.startswith("##"): + clean = line.replace("##","").replace("**","").strip() + if clean.lower() in skip_headings: + continue + sp = 2 if hit_treatment_page else 4 + story.append(KeepTogether([ + Spacer(1, sp), + Paragraph(clean, h3_s), + HRFlowable(width="100%", thickness=0.75, color=C_TEAL, spaceAfter=1 if hit_treatment_page else 2), + ])) + continue + + # ── Bullet ──────────────────────────────────────────────────────────── + if line and line[0] in ("-","*","•"): + clean = re.sub(r"\*\*([^*]+)\*\*", r"\1", line.lstrip("-*• ")) + is_sub = indent >= 2 + if hit_treatment_page: + sty = cond_sub_bullet_s if is_sub else cond_bullet_s + else: + sty = sub_bullet_s if is_sub else bullet_s + marker = "–" if is_sub else "•" + p = Paragraph(f"{marker} {clean}", sty) + if current_pep_hdr is not None: + pending_bullets.append(p) + else: + story.append(p) + continue + + # ── Numbered list ───────────────────────────────────────────────────── + nm = re.match(r'^(\d+)[.)]\s+(.*)', line) + if nm: + clean = re.sub(r"\*\*([^*]+)\*\*", r"\1", nm.group(2)) + sty = cond_num_s if hit_treatment_page else num_s + story.append(Paragraph(f"{nm.group(1)}. {clean}", sty)) + continue + + # ── Paragraph — include as body text ───────────────────────────────── + if current_pep_hdr is not None: + story.append(KeepTogether(list(current_pep_hdr) + list(pending_bullets))) + pending_bullets.clear() + current_pep_hdr = None + + clean = re.sub(r"\*\*([^*]+)\*\*", r"\1", line) + clean = re.sub(r"\*([^*]+)\*", r"\1", clean) + sty = cond_body_s if hit_treatment_page else body_s + story.append(Paragraph(clean, sty)) + + # Flush any trailing service block + if current_pep_hdr is not None: + story.append(KeepTogether(list(current_pep_hdr) + list(pending_bullets))) + + # ── Footer ──────────────────────────────────────────────────────────────── + story.append(Spacer(1, 10)) + story.append(HRFlowable(width="100%", thickness=3, color=C_ORANGE, spaceAfter=0)) + + ft_brand_s = ps("Fb", fontName="Helvetica-Bold", fontSize=10, textColor=C_ORANGE, spaceAfter=1, alignment=TA_CENTER) + ft_tag_s = ps("Fg", fontName="Helvetica-Bold", fontSize=7.5, textColor=C_ORANGE, spaceAfter=4, alignment=TA_CENTER) + ft_info_s = ps("Fi", fontName="Helvetica", fontSize=8, leading=12, textColor=colors.white, spaceAfter=1, alignment=TA_CENTER) + ft_web_s = ps("Fw", fontName="Helvetica-Bold", fontSize=8, textColor=C_SKY, spaceAfter=0, alignment=TA_CENTER) + + ft_tbl = Table([ + [Paragraph("IGNITE PERFORMANCE & HEALTH", ft_brand_s)], + [Paragraph("Ignition Sequence™ — Science · Strength · Nutrition", ft_tag_s)], + [Paragraph("14830 Clayton Rd, Chesterfield, MO 63017 | (314) 887-0858 | info@ignitepah.com", ft_info_s)], + [Paragraph("www.ignitepah.com", ft_web_s)], + ], colWidths=[pw]) + ft_tbl.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(-1,-1), C_NAVY), + ("TOPPADDING", (0,0),(0,0), 8), + ("BOTTOMPADDING", (0,-1),(0,-1), 8), + ("TOPPADDING", (0,1),(-1,-1), 1), + ("BOTTOMPADDING", (0,1),(-1,-1), 1), + ("ALIGN", (0,0),(-1,-1), "CENTER"), + ])) + story.append(ft_tbl) + + doc.build(story) + buffer.seek(0) + return buffer + + +# ============================================================================= +# CSV EXPORT +# ============================================================================= +def generate_csv_export(intake_data, recommended_stacks): + """Generate CSV export of assessment data.""" + sio = StringIO() + fieldnames = [ + "Timestamp","First_Name","Last_Name","Email","Phone","Age","Sex", + "Primary_Goals","Energy_Recovery_Level","Current_Issues","Physical_Activity_Level", + "Medical_Considerations","Medical_Details","Contraindication_Flags","Additional_Information", + "Recommended_Stack_1","Recommended_Stack_2","Recommended_Stack_3", + "Stack_1_26Week_Price","Stack_1_52Week_Price", + "Stack_2_26Week_Price","Stack_2_52Week_Price", + "Stack_3_26Week_Price","Stack_3_52Week_Price", + "All_Recommended_Services","Top_Targets", + ] + writer = csv.DictWriter(sio, fieldnames=fieldnames) + writer.writeheader() + + def ps_str(idx, field): + if idx >= len(recommended_stacks): return "" + ps = get_pricing_summary(recommended_stacks[idx]) + if field == "26_week": + return f"${ps['monthly_26wk']:,.2f}" + elif field == "52_week": + return f"${ps['monthly_52wk']:,.2f}" + return "" + + flags = [f for f in intake_data.get("contraindication_flags", []) if f != "None of the above"] + row = { + "Timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "First_Name": intake_data.get("first_name",""), + "Last_Name": intake_data.get("last_name",""), + "Email": intake_data.get("email",""), + "Phone": intake_data.get("phone",""), + "Age": intake_data.get("age",""), + "Sex": intake_data.get("sex",""), + "Primary_Goals": "; ".join(intake_data.get("primary_goals",[])), + "Energy_Recovery_Level": intake_data.get("energy_recovery",""), + "Current_Issues": "; ".join(intake_data.get("current_issues",[])), + "Physical_Activity_Level": intake_data.get("activity_level",""), + "Medical_Considerations": intake_data.get("medical_considerations",""), + "Medical_Details": intake_data.get("medical_details",""), + "Contraindication_Flags": "; ".join(flags) if flags else "None", + "Additional_Information": intake_data.get("additional_info",""), + "Recommended_Stack_1": recommended_stacks[0] if len(recommended_stacks) > 0 else "", + "Recommended_Stack_2": recommended_stacks[1] if len(recommended_stacks) > 1 else "", + "Recommended_Stack_3": recommended_stacks[2] if len(recommended_stacks) > 2 else "", + "Stack_1_26Week_Price": ps_str(0,"26_week"), "Stack_1_52Week_Price": ps_str(0,"52_week"), + "Stack_2_26Week_Price": ps_str(1,"26_week"), "Stack_2_52Week_Price": ps_str(1,"52_week"), + "Stack_3_26Week_Price": ps_str(2,"26_week"), "Stack_3_52Week_Price": ps_str(2,"52_week"), + "All_Recommended_Services": "; ".join(set(p for sn in recommended_stacks for p in WELLNESS_PROGRAMS[sn]["services"])), + "Top_Targets": "; ".join(set(t for sn in recommended_stacks for t in WELLNESS_PROGRAMS[sn]["targets"])), + } + writer.writerow(row) + buf = BytesIO(sio.getvalue().encode("utf-8")) + buf.seek(0) + return buf + + +# ============================================================================= +# JSON EXPORT +# ============================================================================= +def generate_json_export(intake_data, recommended_stacks): + """Generate JSON export of assessment data.""" + flags = [f for f in intake_data.get("contraindication_flags", []) if f != "None of the above"] + stack_details = [] + for sn in recommended_stacks: + si = WELLNESS_PROGRAMS[sn] + ps = get_pricing_summary(sn) + stack_details.append({ + "name": sn, "targets": si["targets"], "description": si["description"], + "services": si["services"], "type": si["type"], + "cycle": si["cycle"], + "contraindication_flags": get_stack_contraindication_summary(sn, flags), + "pricing": { + "monthly_26week": round(ps["monthly_26wk"], 2), + "monthly_52week": round(ps["monthly_52wk"], 2), + "all_tiers": ps["all_tiers"], + }, + }) + export_data = { + "timestamp": datetime.now().isoformat(), + "patient_info": { + "first_name": intake_data.get("first_name",""), "last_name": intake_data.get("last_name",""), + "email": intake_data.get("email",""), "phone": intake_data.get("phone",""), + "age": intake_data.get("age",""), "sex": intake_data.get("sex",""), + }, + "assessment": { + "primary_goals": intake_data.get("primary_goals",[]), + "energy_recovery_level": intake_data.get("energy_recovery",""), + "current_issues": intake_data.get("current_issues",[]), + "physical_activity": intake_data.get("activity_level",""), + "medical_considerations": intake_data.get("medical_considerations",""), + "medical_details": intake_data.get("medical_details",""), + "contraindication_flags": flags, + "additional_info": intake_data.get("additional_info",""), + }, + "recommendations": { + "stacks": stack_details, + "all_services": list(set(p for sn in recommended_stacks for p in WELLNESS_PROGRAMS[sn]["services"])), + "top_targets": list(set(t for sn in recommended_stacks for t in WELLNESS_PROGRAMS[sn]["targets"])), + }, + } + buf = BytesIO(json.dumps(export_data, indent=2).encode("utf-8")) + buf.seek(0) + return buf + + +# ============================================================================= +# EMAIL DELIVERY (Outlook SMTP) +# ============================================================================= +log = logging.getLogger("ignite_email") + +def _smtp_send(to_addr, subject, body_text, attachments=None): + """Send an email via Outlook SMTP. Returns True on success, error string on failure.""" + user = os.getenv("SMTP_USER", "").strip() + pw = os.getenv("SMTP_PASSWORD", "").strip() + if not user or not pw: + return "SMTP credentials not configured (SMTP_USER / SMTP_PASSWORD)" + + msg = MIMEMultipart() + msg["From"] = user + msg["To"] = to_addr + msg["Subject"] = subject + msg.attach(MIMEText(body_text, "plain")) + + for filename, data, mime_type in (attachments or []): + part = MIMEBase(*mime_type.split("/", 1)) + part.set_payload(data) + encoders.encode_base64(part) + part.add_header("Content-Disposition", f'attachment; filename="{filename}"') + msg.attach(part) + + try: + with smtplib.SMTP(EMAIL["smtp_server"], EMAIL["smtp_port"], timeout=15) as srv: + srv.ehlo() + srv.starttls() + srv.ehlo() + srv.login(user, pw) + srv.sendmail(user, to_addr, msg.as_string()) + return True + except Exception as exc: + log.warning("SMTP send failed to %s: %s", to_addr, exc) + return str(exc) + + +def send_client_email(patient_email, pdf_bytes): + """Send the wellness plan PDF to the patient.""" + body = EMAIL["client_body"].format(scheduling_url=BRAND["scheduling_url"]) + return _smtp_send( + to_addr=patient_email, + subject=EMAIL["client_subject"], + body_text=body, + attachments=[("Ignite_Ignition_Sequence.pdf", pdf_bytes, "application/pdf")], + ) + + +def send_clinic_email(intake_data, pdf_bytes, csv_bytes, json_bytes): + """Send full intake data + exports to the clinic.""" + flags = intake_data.get("contraindication_flags", []) + flag_str = ", ".join(flags) if flags else "None" + body = ( + f"New assessment completed — {intake_data.get('name', 'Unknown')}\n" + f"{'=' * 50}\n\n" + f"Email: {intake_data.get('email', '')}\n" + f"Phone: {intake_data.get('phone', '')}\n" + f"Age: {intake_data.get('age', '')} | Sex: {intake_data.get('sex', '')}\n\n" + f"Primary Goals: {', '.join(intake_data.get('primary_goals', []))}\n" + f"Energy & Recovery: {intake_data.get('energy_recovery', '')}\n" + f"Current Issues: {', '.join(intake_data.get('current_issues', []))}\n" + f"Activity Level: {intake_data.get('activity_level', '')}\n" + f"Contraindication Flags: {flag_str}\n" + f"Medical Considerations: {intake_data.get('medical_considerations', '')}\n" + f"Additional Info: {intake_data.get('additional_info', '')}\n\n" + f"PDF, CSV, and JSON exports attached." + ) + safe_name = (intake_data.get("name", "patient")).replace(" ", "_") + return _smtp_send( + to_addr=EMAIL["clinic_recipient"], + subject=f"{EMAIL['clinic_subject']} — {intake_data.get('name', '')}", + body_text=body, + attachments=[ + (f"Ignite_{safe_name}.pdf", pdf_bytes, "application/pdf"), + (f"Ignite_{safe_name}.csv", csv_bytes, "text/csv"), + (f"Ignite_{safe_name}.json", json_bytes, "application/json"), + ], + ) + + +# ============================================================================= +# FOOTER RENDERER +# ============================================================================= +def render_footer(): + """Render the footer with contact and disclaimer info.""" + st.markdown( + '', + unsafe_allow_html=True, + ) + + +# ============================================================================= +# PAGE CONFIG & CSS +# ============================================================================= +st.set_page_config(page_title="Ignite Performance & Health — Ignition Sequence", layout="wide", initial_sidebar_state="collapsed") + +# ── Preload fonts via instead of @import (non-blocking, parallelized) ── +st.markdown( + '' + '' + '', + unsafe_allow_html=True, +) + +st.markdown(""" + +""", unsafe_allow_html=True) + +# ── Shared tooltip (one DOM node replaces ~30 hidden tooltips) ── +# JS must run via components.html — st.markdown strips +""", height=0) + +# ── Branded spinner overlay (separate block for dynamic logo injection) ── +st.markdown( + f"""""", + unsafe_allow_html=True, +) + + +# ============================================================================= +# SESSION STATE INIT +# ============================================================================= +if "wellness_pathway" not in st.session_state: + st.session_state["wellness_pathway"] = None + st.session_state["recommended_stacks"] = [] + st.session_state["all_eligible_stacks"] = [] + st.session_state["intake_data"] = {} + st.session_state["patient_name"] = "" + st.session_state["patient_email"] = "" + st.session_state["patient_phone"] = "" + st.session_state["patient_age"] = "" + st.session_state["patient_sex"] = "" + st.session_state["contraindication_flags"] = [] + st.session_state["scroll_to_top"] = False + st.session_state["results_scroll_done"] = False + + +# ============================================================================= +# RESULTS PAGE +# ============================================================================= +if st.session_state["wellness_pathway"]: + + import streamlit.components.v1 as components + st.markdown('
', unsafe_allow_html=True) + + pname = st.session_state.get("patient_name", "") + pemail = st.session_state.get("patient_email", "") + pphone = st.session_state.get("patient_phone", "") + page = st.session_state.get("patient_age", "") + psex = st.session_state.get("patient_sex", "") + + # ── Results hero ────────────────────────────────────────────────────────── + st.markdown( + '
' + f'' + '
Your
Ignition Sequence™
' + '
Unlock Your Peak Performance
' + f'
Prepared for {pname}
' + '
Physician-Supervised  •  Personalized to Your Physiology  •  Tailored to Your Goals
' + '
', + unsafe_allow_html=True, + ) + + # ── All eligible programs — sorted: robust/expensive first, med-only last ─ + primary_recs = set(st.session_state["recommended_stacks"]) + all_eligible = st.session_state.get("all_eligible_stacks", list(primary_recs)) + patient_flags = st.session_state.get("contraindication_flags", []) + active_flags = [f for f in patient_flags if f != "None of the above"] + + display_stacks = sorted( + all_eligible, + key=lambda sn: WELLNESS_PROGRAMS[sn].get("display_order", 50), + ) + + all_stack_services = set() + for sn in display_stacks: + all_stack_services.update(WELLNESS_PROGRAMS[sn]["services"]) + + for i, sn in enumerate(display_stacks): + stack = WELLNESS_PROGRAMS[sn] + targets_str = " · ".join(stack["targets"]) + + # Badge label — top 3 always get BEST / BETTER / GOOD + if i < 3: + tier_label = ["BEST", "BETTER", "GOOD"][i] + badge_html = f'
{tier_label}
' + else: + badge_html = '' + + # Contraindication check + contra_html = "" + if active_flags: + hits = check_contraindications(stack["services"], active_flags) + if hits: + svc_list = ", ".join(sex_label(s, psex) for s in hits.keys()) + contra_html = ( + f'
' + f'⚠ Physician Review Required — ' + f'{svc_list} may need adjustment based on your health history. ' + f'Our medical team will evaluate during your consultation.' + f'
' + ) + + # Service pills (MOA shown via shared tooltip on hover) + badges = "" + for p in stack["services"]: + display_name = sex_label(p, psex) + moa = sex_moa(p, psex) + esc_moa = moa.replace('"', '"').replace("'", "'") if moa else "" + badges += f'
{display_name}
' + + # Build tier pricing grid (config values are monthly — convert to weekly) + sorted_tiers = sorted(stack["tiers"].items(), key=lambda x: x[1]["sort_order"]) + tier_cells = "" + for tier_name, tier_data in sorted_tiers: + tier_desc = "" + if stack["type"] == "training": + tier_info = TRAINING_TIERS.get(tier_name, {}) + desc_text = tier_info.get("training_desc", "") + if desc_text: + tier_desc = f'
{desc_text}
' + us_text = tier_info.get("ultrasound", "") + if us_text: + tier_desc += f'
Ultrasound: {us_text}
' + # Config stores monthly rates — convert to per-week: monthly * 12 / 52 + pw_52 = tier_data["52_week"] * 12.0 / 52.0 + pw_26 = tier_data["26_week"] * 12.0 / 52.0 + tier_cells += ( + f'
' + f'
{tier_name}
' + f'{tier_desc}' + f'
${pw_52:,.2f}
' + f'
per week · 52-week
' + f'
${pw_26:,.2f}/wk · 26-week
' + f'
' + ) + + n_tiers = len(sorted_tiers) + pricing_grid = f'
{tier_cells}
' + + st.markdown( + f'
' + f'{badge_html}' + f'{contra_html}' + f'
{sn}
' + f'
{stack.get("subtitle", "")}
' + f'
{stack["description"]}
' + f'
' + f'
' + f'
Targets
' + f'
{targets_str}
' + f'
Cycle Protocol
' + f'
{stack["cycle"]}
' + f'
' + f'
' + f'
Included Services — hover for details
' + f'
{badges}
' + f'
' + f'
' + f'
PRICING OPTIONS
' + f'{pricing_grid}' + f'
', + unsafe_allow_html=True, + ) + + # ── Pathway content ─────────────────────────────────────────────────────── + st.markdown('
', unsafe_allow_html=True) + pathway_html = render_pathway_html( + st.session_state["wellness_pathway"], + all_stack_services, + patient_sex=psex, + ) + st.markdown(pathway_html, unsafe_allow_html=True) + + # ── Email status ────────────────────────────────────────────────────────── + for level, msg in st.session_state.get("email_status", []): + if level == "success": + st.success(msg) + else: + st.warning(msg) + + # ── CTA box ─────────────────────────────────────────────────────────────── + st.markdown( + f'
' + f'

Ready to Begin, {pname}?

' + f'

Your recommendation is ready. Our team will review your assessment ' + f'and reach out to schedule your consultation within 24–48 hours.

' + f'

We\'ll contact you at: {pemail}  |  {pphone}

' + f'

Your next steps:

' + f'
    ' + f'
  1. Our team contacts you within 24–48 hours
  2. ' + f'
  3. Schedule your consultation
  4. ' + f'
  5. Complete baseline bloodwork and health assessment
  6. ' + f'
  7. Receive your program details and get started
  8. ' + f'
  9. Ongoing support as your plan evolves
  10. ' + f'
' + f'

Visit us: 14830 Clayton Rd, Chesterfield, MO 63017  |  ' + f'314-887-0858  |  info@ignitepah.com

' + f'→ Schedule Your Consultation' + f'
', + unsafe_allow_html=True, + ) + + # ── Downloads ───────────────────────────────────────────────────────────── + st.markdown('
', unsafe_allow_html=True) + st.markdown("#### Download Your Results") + st.caption("Save your Ignition Sequence recommendation or export data for clinic use.") + + c1, c2, c3 = st.columns(3) + with c1: + pdf_buf = generate_wellness_pdf( + st.session_state["wellness_pathway"], + {"name": pname, "email": pemail, "phone": pphone, + "age": page, "sex": psex, + "date": datetime.now().strftime("%B %d, %Y")}, + st.session_state["recommended_stacks"], + st.session_state.get("contraindication_flags", []), + ) + st.download_button( + "PDF Report", data=pdf_buf, file_name="ignite_ignition_sequence.pdf", + mime="application/pdf", use_container_width=True, + ) + with c2: + csv_buf = generate_csv_export(st.session_state.get("intake_data", {}), st.session_state["recommended_stacks"]) + safe_name = (pname or "patient").replace(" ", "_") + st.download_button( + "CSV Export", data=csv_buf, file_name=f"ignite_{safe_name}.csv", + mime="text/csv", use_container_width=True, + ) + with c3: + json_buf = generate_json_export(st.session_state.get("intake_data", {}), st.session_state["recommended_stacks"]) + st.download_button( + "JSON Export", data=json_buf, file_name=f"ignite_{safe_name}.json", + mime="application/json", use_container_width=True, + ) + + st.info( + "**Clinic staff:** CSV and JSON exports include full intake data, contraindication flags, " + "and per-week / per-month / total pricing — ready for your EMR or CRM.", + icon="ℹ️", + ) + + # ── Start over button ───────────────────────────────────────────────────── + st.markdown('
', unsafe_allow_html=True) + if st.button("← Start New Assessment", use_container_width=True): + for key in ["wellness_pathway", "recommended_stacks", "all_eligible_stacks", "intake_data", + "patient_name", "patient_email", "patient_phone", "patient_age", "patient_sex", + "contraindication_flags", "email_status"]: + st.session_state[key] = None if key == "wellness_pathway" else ( + [] if key in ["recommended_stacks", "all_eligible_stacks", "contraindication_flags", "email_status"] else "" + ) + st.session_state["scroll_to_top"] = True + st.rerun() + + render_footer() + + # ── Scroll to top (fires ONCE on first results render, then stops) ────── + if not st.session_state.get("results_scroll_done", False): + st.session_state["results_scroll_done"] = True + components.html( + """ + + """, + height=0, + ) + + st.stop() + + +# ============================================================================= +# INTAKE FORM PAGE +# ============================================================================= + +st.markdown('
', unsafe_allow_html=True) + +if st.session_state.pop("scroll_to_top", False): + components.html( + """ + + """, + height=0, + ) + +st.markdown( + '
' + f'' + '
Ignition Sequence™
' + '
Unlock Your Peak Performance
' + '
Physician-Supervised  •  Personalized to Your Physiology  •  Tailored to Your Goals
' + '
', + unsafe_allow_html=True, +) +st.markdown( + "Answer a few questions about your goals and where you are right now. " + "We'll match you to the Ignition Sequence program that fits best. " + "Our team reviews every result and will reach out within 24\u201348 hours to schedule your consultation.", +) + +# ── Intake form ─────────────────────────────────────────────────────────────── +with st.form("wellness_intake_form"): + + st.markdown('
Your Information
', unsafe_allow_html=True) + c1, c2 = st.columns(2) + with c1: + first_name = st.text_input("First Name *") + last_name = st.text_input("Last Name *") + email = st.text_input("Email Address *") + with c2: + phone = st.text_input("Cell Phone *") + age = st.number_input("Age *", min_value=18, max_value=100, value=35) + sex = st.radio("Biological Sex *", ["Male", "Female"], index=None) + + st.markdown('
1 — Current Symptoms
', unsafe_allow_html=True) + st.caption("Check anything you're currently experiencing.") + + # Split symptoms dynamically into two columns + symptom_keys = list(CURRENT_SYMPTOMS.keys()) + mid = (len(symptom_keys) + 1) // 2 + symptom_checks = {} + + c1, c2 = st.columns(2) + with c1: + for symptom in symptom_keys[:mid]: + symptom_checks[symptom] = st.checkbox(symptom, key=f"symptom_{symptom}") + with c2: + for symptom in symptom_keys[mid:]: + symptom_checks[symptom] = st.checkbox(symptom, key=f"symptom_{symptom}") + + st.markdown('
2 — Primary Goals
', unsafe_allow_html=True) + st.caption("Select everything that applies to you.") + + # Split goals dynamically into two columns + goal_keys = list(PRIMARY_GOALS.keys()) + mid = (len(goal_keys) + 1) // 2 + goal_checks = {} + + c1, c2 = st.columns(2) + with c1: + for goal in goal_keys[:mid]: + goal_checks[goal] = st.checkbox(goal, key=f"goal_{goal}") + with c2: + for goal in goal_keys[mid:]: + goal_checks[goal] = st.checkbox(goal, key=f"goal_{goal}") + + st.markdown('
3 — Physical Activity
', unsafe_allow_html=True) + activity_level = st.radio( + "What best describes your current activity level? *", + ACTIVITY_LEVELS, + index=None, + ) + + st.markdown('
4 — Energy & Recovery
', unsafe_allow_html=True) + energy_recovery = st.radio( + "How would you describe your current energy and physical recovery? *", + list(ENERGY_RECOVERY_OPTIONS.keys()), + index=None, + ) + + st.markdown('
5 — Medical History
', unsafe_allow_html=True) + medical_care = st.radio( + "Are you currently receiving care for hormone imbalances, cancer, or other chronic conditions not listed above? *", + ["No", "Yes — please describe below"], index=None, + ) + medical_details = st.text_area( + "If yes, please provide details:", + placeholder="Current conditions, medications, or recent treatments...", + ) + + st.markdown('
6 — Safety Screening
', unsafe_allow_html=True) + st.caption( + "Check any of the following that apply. This allows our physician team to identify services " + "requiring modification or review. Checking an item does not exclude you from therapy." + ) + flag_options = [f for f in ALL_CONTRAINDICATION_FLAGS if f != "None of the above"] + mid = (len(flag_options) + 1) // 2 + contra_checks = {} + cf1, cf2 = st.columns(2) + with cf1: + for flag in flag_options[:mid]: + contra_checks[flag] = st.checkbox(flag, key=f"cf_{flag}") + with cf2: + for flag in flag_options[mid:]: + contra_checks[flag] = st.checkbox(flag, key=f"cf_{flag}") + none_of_above = st.checkbox("None of the above", key="cf_none") + + st.markdown('
Anything Else?
', unsafe_allow_html=True) + additional_info = st.text_area( + "Previous experience with training programs, specific concerns, or timeline goals:", + placeholder="Optional — any context that may help our physician team.", + ) + + # ── Consent & Disclaimers ────────────────────────────────────────────────── + st.markdown('
Consent & Privacy
', unsafe_allow_html=True) + + st.markdown( + '

' + 'Important: This platform is ' + 'not HIPAA-compliant. Any personal or health information you ' + 'enter may be transmitted and stored on servers that do not meet HIPAA security ' + 'standards. Please do not share sensitive medical records, Social Security numbers, ' + 'or information you would not be comfortable disclosing on a non-secure platform.

', + unsafe_allow_html=True, + ) + + consent_privacy = st.checkbox( + "I understand this platform is NOT HIPAA-compliant and that any information " + "I provide may not be protected under HIPAA privacy regulations.", + key="consent_privacy", + ) + consent_contact = st.checkbox( + "I consent to being contacted by Ignite Performance & Health via the email " + "address and phone number I have provided above regarding my assessment " + "and available programs.", + key="consent_contact", + ) + + submitted = st.form_submit_button("Generate My Ignition Sequence", use_container_width=True) + +# ── Form processing ─────────────────────────────────────────────────────────── +if submitted: + primary_goals = [g for g, checked in goal_checks.items() if checked] + current_issues = [i for i, checked in symptom_checks.items() if checked] + + if none_of_above: + selected_flags = ["None of the above"] + else: + selected_flags = [flag for flag, checked in contra_checks.items() if checked] + if not selected_flags: + selected_flags = ["None of the above"] + + errors = [] + if not first_name: errors.append("First Name") + if not last_name: errors.append("Last Name") + if not email: errors.append("Email Address") + if not phone: errors.append("Cell Phone") + if sex is None: errors.append("Biological Sex") + if not primary_goals: errors.append("Primary Goals (select at least one)") + if energy_recovery is None: errors.append("Energy & Recovery level") + if activity_level is None: errors.append("Physical Activity level") + if medical_care is None: errors.append("Medical History") + if not consent_privacy: errors.append("Privacy acknowledgment (required)") + if not consent_contact: errors.append("Consent to contact (required)") + + # ── Male-specific restrictions ──────────────────────────────────────────── + male_conflicts = [] + if sex == "Male": + male_restricted_goals = [g for g in primary_goals if g in MALE_RESTRICTED_GOALS] + male_restricted_syms = [s for s in current_issues if s in MALE_RESTRICTED_SYMPTOMS] + male_restricted_flags = [f for f in selected_flags if f in MALE_RESTRICTED_FLAGS] + + if male_restricted_goals: + male_conflicts.append(f"Primary Goals: {', '.join(male_restricted_goals)}") + if male_restricted_syms: + male_conflicts.append(f"Symptoms: {', '.join(male_restricted_syms)}") + if male_restricted_flags: + male_conflicts.append(f"Flags: {', '.join(male_restricted_flags)}") + + if errors: + st.error(f"Please complete the following required fields: **{', '.join(errors)}**") + st.stop() + + # ── "None of the above" conflict check ──────────────────────────────────── + other_flags_checked = [flag for flag, checked in contra_checks.items() if checked] + if none_of_above and other_flags_checked: + st.error( + "⚠ You selected **\"None of the above\"** in Safety Screening but also checked: " + f"**{', '.join(other_flags_checked)}**. Please select one or the other." + ) + st.stop() + + if male_conflicts: + st.error( + f"⚠ The following selections are not applicable for male patients: " + f"**{', '.join(male_conflicts)}**. Please uncheck them and resubmit." + ) + st.stop() + + # Scroll parent to top AND force-position spinner when it appears + components.html( + """""", + height=0, + ) + with st.spinner("Generating Your Ignition Sequence..."): + goals_data = {"primary_goals": primary_goals, "energy_recovery": energy_recovery, + "current_issues": current_issues, "activity_level": activity_level} + target_scores = map_goals_to_targets(goals_data) + rec_stacks, _, all_eligible = recommend_programs(target_scores, len(primary_goals), patient_sex=sex, patient_flags=selected_flags) + med = medical_details if medical_care == "Yes — please describe below" else "No current conditions reported" + + intake_data = { + "name": f"{first_name} {last_name}", "first_name": first_name, "last_name": last_name, + "email": email, "phone": phone, "age": age, "sex": sex, + "primary_goals": primary_goals, "energy_recovery": energy_recovery, + "current_issues": current_issues, "activity_level": activity_level, + "medical_considerations": med, "medical_details": medical_details or "", + "contraindication_flags": selected_flags, + "additional_info": additional_info or "None provided", + } + + pathway = generate_wellness_pathway(intake_data, rec_stacks, selected_flags) + + st.session_state["wellness_pathway"] = pathway + st.session_state["results_scroll_done"] = False # allow one scroll-to-top on results load + st.session_state["recommended_stacks"] = rec_stacks + st.session_state["all_eligible_stacks"] = all_eligible + st.session_state["intake_data"] = intake_data + st.session_state["patient_name"] = f"{first_name} {last_name}" + st.session_state["patient_email"] = email + st.session_state["patient_phone"] = phone + st.session_state["patient_age"] = age + st.session_state["patient_sex"] = sex + st.session_state["contraindication_flags"] = selected_flags + + # ── Auto-send emails ────────────────────────────────────────────── + patient_info = { + "name": f"{first_name} {last_name}", "email": email, "phone": phone, + "age": age, "sex": sex, "date": datetime.now().strftime("%B %d, %Y"), + } + pdf_buf = generate_wellness_pdf(pathway, patient_info, rec_stacks, selected_flags) + pdf_bytes = pdf_buf.getvalue() + csv_buf = generate_csv_export(intake_data, rec_stacks) + csv_bytes = csv_buf.getvalue() + json_buf = generate_json_export(intake_data, rec_stacks) + json_bytes = json_buf.getvalue() + + client_result = send_client_email(email, pdf_bytes) + clinic_result = send_clinic_email(intake_data, pdf_bytes, csv_bytes, json_bytes) + + email_status = [] + if client_result is True and clinic_result is True: + email_status = [("success", "Your results have been sent to your email.")] + else: + if client_result is not True: + email_status.append(("warning", f"Patient email not sent: {client_result}")) + if clinic_result is not True: + email_status.append(("warning", f"Clinic email not sent: {clinic_result}")) + st.session_state["email_status"] = email_status + + st.rerun() + +render_footer()