Scott Harshman commited on
Commit
3df4ef6
·
verified ·
1 Parent(s): c97544f

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -821
app.py DELETED
@@ -1,821 +0,0 @@
1
- import streamlit as st
2
- import streamlit.components.v1 as components
3
- import os, json, re
4
- from reportlab.lib.pagesizes import letter
5
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
6
- from reportlab.lib.units import inch
7
- from reportlab.platypus import (
8
- SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
9
- HRFlowable, KeepTogether, PageBreak,
10
- )
11
- from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
12
- from reportlab.lib import colors
13
- from io import BytesIO
14
- from datetime import datetime
15
- from config import *
16
-
17
- # ── Shorthand color references
18
- NAVY = BRAND["colors"]["navy"]
19
- TEAL = BRAND["colors"]["teal"]
20
- SKY = BRAND["colors"]["sky"]
21
- ORANGE = BRAND["colors"]["orange"]
22
- AMBER = BRAND["colors"]["amber"]
23
- WHITE = BRAND["colors"]["white"]
24
-
25
-
26
- # =============================================================================
27
- # RECOMMENDATION ENGINE
28
- # =============================================================================
29
- def score_peptides(selected_goals):
30
- """Score every peptide against the patient's selected goals.
31
-
32
- Returns a list of (peptide_key, score, matched_categories) sorted by score
33
- descending, then by suggested-vial cost ascending (value-first tiebreaker).
34
- """
35
- # Build category weights from selected goals
36
- cat_scores = {}
37
- for goal in selected_goals:
38
- for cat_name, weight in GOAL_MAPPINGS.get(goal, []):
39
- cat_scores[cat_name] = cat_scores.get(cat_name, 0) + weight
40
-
41
- if not cat_scores:
42
- return []
43
-
44
- results = []
45
- for pep_key, pep in PEPTIDES.items():
46
- score = 0
47
- matched = []
48
- for cat in pep["categories"]:
49
- if cat in cat_scores:
50
- score += cat_scores[cat]
51
- matched.append(cat)
52
- if score > 0:
53
- results.append((pep_key, score, matched))
54
-
55
- # Sort: highest score first, then cheapest suggested-vial price as tiebreaker
56
- results.sort(
57
- key=lambda x: (
58
- -x[1],
59
- PEPTIDES[x[0]]["pricing"].get(PEPTIDES[x[0]]["suggested_vials"], 9999),
60
- )
61
- )
62
- return results
63
-
64
-
65
- def get_top_categories(selected_goals, limit=3):
66
- """Return the top N matched categories from selected goals."""
67
- cat_scores = {}
68
- for goal in selected_goals:
69
- for cat_name, weight in GOAL_MAPPINGS.get(goal, []):
70
- cat_scores[cat_name] = cat_scores.get(cat_name, 0) + weight
71
- sorted_cats = sorted(cat_scores.items(), key=lambda x: -x[1])
72
- return [c[0] for c in sorted_cats[:limit]]
73
-
74
-
75
- def group_by_tier(scored_peptides):
76
- """Group scored peptides into branded blends, other blends, and singles."""
77
- branded = []
78
- blends = []
79
- singles = []
80
- for pep_key, score, matched in scored_peptides:
81
- pep = PEPTIDES[pep_key]
82
- if pep.get("brand_name"):
83
- branded.append((pep_key, score, matched))
84
- elif pep.get("is_blend"):
85
- blends.append((pep_key, score, matched))
86
- else:
87
- singles.append((pep_key, score, matched))
88
- return branded, blends, singles
89
-
90
-
91
- # =============================================================================
92
- # PDF GENERATOR
93
- # =============================================================================
94
- def generate_pdf(patient_info, selected_goals, top_categories, recommendations):
95
- """Generate a branded PDF of the peptide recommendation."""
96
- buffer = BytesIO()
97
- doc = SimpleDocTemplate(
98
- buffer, pagesize=letter,
99
- topMargin=0.55 * inch, bottomMargin=0.6 * inch,
100
- leftMargin=0.65 * inch, rightMargin=0.65 * inch,
101
- )
102
- story = []
103
- pw = letter[0] - 1.3 * inch
104
-
105
- def ps(name, **kwargs):
106
- return ParagraphStyle(name, **kwargs)
107
-
108
- C_NAVY = colors.HexColor(NAVY)
109
- C_TEAL = colors.HexColor(TEAL)
110
- C_ORANGE = colors.HexColor(ORANGE)
111
- C_AMBER = colors.HexColor(AMBER)
112
- C_SKY = colors.HexColor(SKY)
113
- C_LIGHT = colors.HexColor("#eef4f8")
114
- C_CARD = colors.HexColor("#f0f6fa")
115
- C_BORDER = colors.HexColor("#b8d0de")
116
-
117
- brand_s = ps("Br", fontName="Helvetica-Bold", fontSize=9, textColor=C_ORANGE, spaceAfter=0, alignment=TA_CENTER)
118
- title_s = ps("T", fontName="Helvetica-Bold", fontSize=20, leading=26, textColor=colors.white, spaceAfter=0, alignment=TA_CENTER)
119
- sub_s = ps("Su", fontName="Helvetica", fontSize=10, textColor=C_SKY, spaceAfter=0, alignment=TA_CENTER)
120
- meta_s = ps("Me", fontName="Helvetica", fontSize=9, leading=14, textColor=C_NAVY, spaceAfter=3)
121
- h2_s = ps("H2", fontName="Helvetica-Bold", fontSize=11, textColor=colors.white, spaceBefore=0, spaceAfter=0)
122
- h3_s = ps("H3", fontName="Helvetica-Bold", fontSize=10, textColor=C_NAVY, spaceBefore=4, spaceAfter=3)
123
- body_s = ps("B", fontName="Helvetica", fontSize=9.5, leading=15, textColor=C_NAVY, spaceAfter=5)
124
- bullet_s = ps("Bu", fontName="Helvetica", fontSize=9.5, leading=15, textColor=C_NAVY, leftIndent=14, spaceAfter=3)
125
- disc_s = ps("Di", fontName="Helvetica-Oblique", fontSize=8, leading=12, textColor=colors.HexColor("#4a6070"), spaceAfter=4)
126
- price_s = ps("Pr", fontName="Helvetica-Bold", fontSize=10, textColor=C_ORANGE, spaceAfter=2)
127
- pep_hdr_s = ps("Ph", fontName="Helvetica-Bold", fontSize=10, textColor=colors.white, spaceBefore=0, spaceAfter=0)
128
- ft_s = ps("Ft", fontName="Helvetica", fontSize=8, leading=12, textColor=C_NAVY, spaceAfter=0, alignment=TA_CENTER)
129
-
130
- def banner(para, bg, line_below=None, pad_v=7, pad_h=12):
131
- t = Table([[para]], colWidths=[pw])
132
- cmds = [
133
- ("BACKGROUND", (0, 0), (-1, -1), bg),
134
- ("LEFTPADDING", (0, 0), (-1, -1), pad_h),
135
- ("RIGHTPADDING", (0, 0), (-1, -1), pad_h),
136
- ("TOPPADDING", (0, 0), (-1, -1), pad_v),
137
- ("BOTTOMPADDING", (0, 0), (-1, -1), pad_v),
138
- ]
139
- if line_below:
140
- cmds.append(("LINEBELOW", (0, 0), (-1, -1), 3, line_below))
141
- t.setStyle(TableStyle(cmds))
142
- return t
143
-
144
- def section_hdr(text):
145
- return KeepTogether([
146
- Spacer(1, 8),
147
- banner(Paragraph(text.upper(), h2_s), C_NAVY, line_below=C_ORANGE, pad_v=8),
148
- Spacer(1, 6),
149
- ])
150
-
151
- # ── COVER ────────────────────────────────────────────────────────────────
152
- hdr_tbl = Table([
153
- [Paragraph("IGNITE PERFORMANCE & HEALTH", brand_s)],
154
- [Paragraph("Peptide Protocol Recommendation", title_s)],
155
- [Paragraph("Physician-Supervised Peptide Therapy", sub_s)],
156
- ], colWidths=[pw])
157
- hdr_tbl.setStyle(TableStyle([
158
- ("BACKGROUND", (0, 0), (-1, -1), C_NAVY),
159
- ("LEFTPADDING", (0, 0), (-1, -1), 20),
160
- ("RIGHTPADDING", (0, 0), (-1, -1), 20),
161
- ("TOPPADDING", (0, 0), (0, 0), 14),
162
- ("BOTTOMPADDING", (0, 0), (0, 0), 4),
163
- ("TOPPADDING", (0, 1), (0, 1), 4),
164
- ("BOTTOMPADDING", (0, 1), (0, 1), 4),
165
- ("TOPPADDING", (0, 2), (0, 2), 2),
166
- ("BOTTOMPADDING", (0, 2), (0, 2), 14),
167
- ("ALIGN", (0, 0), (-1, -1), "CENTER"),
168
- ]))
169
- story.append(hdr_tbl)
170
- story.append(HRFlowable(width="100%", thickness=5, color=C_ORANGE, spaceAfter=12))
171
-
172
- # ── Patient info ──
173
- name = patient_info.get("name", "")
174
- date_str = datetime.now().strftime("%B %d, %Y")
175
- pi_tbl = Table([[
176
- Paragraph(f"<b>Prepared for:</b> {name}", meta_s),
177
- Paragraph(f"<b>Date:</b> {date_str}", meta_s),
178
- ]], colWidths=[pw * 0.6, pw * 0.4])
179
- pi_tbl.setStyle(TableStyle([
180
- ("BACKGROUND", (0, 0), (-1, -1), C_LIGHT),
181
- ("BOX", (0, 0), (-1, -1), 0.75, C_BORDER),
182
- ("LINEBELOW", (0, 0), (-1, -1), 2, C_ORANGE),
183
- ("LEFTPADDING", (0, 0), (-1, -1), 10),
184
- ("RIGHTPADDING", (0, 0), (-1, -1), 10),
185
- ("TOPPADDING", (0, 0), (-1, -1), 9),
186
- ("BOTTOMPADDING", (0, 0), (-1, -1), 9),
187
- ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
188
- ]))
189
- story.append(pi_tbl)
190
- story.append(Spacer(1, 14))
191
-
192
- # ── Goals & Categories ──
193
- story.append(section_hdr("Your Goals"))
194
- goals_text = " · ".join(selected_goals)
195
- story.append(Paragraph(f"<b>Selected goals:</b> {goals_text}", body_s))
196
- cats_text = " · ".join(top_categories)
197
- story.append(Paragraph(f"<b>Top therapeutic areas:</b> {cats_text}", body_s))
198
- story.append(Spacer(1, 8))
199
-
200
- # ── Recommendations ──
201
- story.append(section_hdr("Recommended Peptides"))
202
-
203
- branded, blends, singles = group_by_tier(recommendations)
204
-
205
- def render_pep_block(pep_key, score, matched):
206
- pep = PEPTIDES[pep_key]
207
- display_name = pep.get("brand_name") or pep_key
208
- sv = pep["suggested_vials"]
209
- sv_price = pep["pricing"].get(sv, 0)
210
- per_week = sv_price / max((sv * 4), 1) # rough per-week estimate
211
-
212
- items = [
213
- Spacer(1, 4),
214
- banner(Paragraph(f" {display_name}", pep_hdr_s), C_TEAL, line_below=C_NAVY, pad_v=5),
215
- ]
216
- if pep.get("brand_name") and pep_key != pep["brand_name"]:
217
- items.append(Paragraph(f"<i>{pep_key}</i>", disc_s))
218
- items.append(Paragraph(pep["description"], body_s))
219
- items.append(Paragraph(
220
- f"<b>Dosage:</b> {pep['dosage']} | <b>Cycle:</b> {pep['cycle']} | "
221
- f"<b>Suggested vials:</b> {sv}", bullet_s))
222
- items.append(Paragraph(
223
- f"<b>Suggested cycle cost:</b> ${sv_price:,.2f} ({sv} vials) | "
224
- f"<b>~${per_week:,.2f}/week</b>", price_s))
225
- items.append(Paragraph(
226
- f"Categories: {', '.join(matched)}", disc_s))
227
- items.append(Spacer(1, 6))
228
- return KeepTogether(items)
229
-
230
- if branded:
231
- story.append(Paragraph("<b>Ignite Branded Blends</b> — curated multi-peptide protocols", h3_s))
232
- for pep_key, score, matched in branded[:6]:
233
- story.append(render_pep_block(pep_key, score, matched))
234
-
235
- if blends:
236
- story.append(Paragraph("<b>Combination Blends</b>", h3_s))
237
- for pep_key, score, matched in blends[:4]:
238
- story.append(render_pep_block(pep_key, score, matched))
239
-
240
- if singles:
241
- story.append(Paragraph("<b>Individual Peptides</b>", h3_s))
242
- for pep_key, score, matched in singles[:4]:
243
- story.append(render_pep_block(pep_key, score, matched))
244
-
245
- # ── Disclaimer ──
246
- story.append(Spacer(1, 12))
247
- story.append(HRFlowable(width="100%", thickness=1, color=C_BORDER, spaceAfter=8))
248
- story.append(Paragraph(BRAND["disclaimer"], disc_s))
249
- story.append(Spacer(1, 6))
250
-
251
- # ── Footer ──
252
- story.append(HRFlowable(width="100%", thickness=3, color=C_ORANGE, spaceAfter=0))
253
- ft_brand_s = ps("Fb", fontName="Helvetica-Bold", fontSize=10, textColor=C_ORANGE, spaceAfter=1, alignment=TA_CENTER)
254
- ft_info_s = ps("Fi", fontName="Helvetica", fontSize=8, leading=12, textColor=colors.white, spaceAfter=1, alignment=TA_CENTER)
255
- ft_web_s = ps("Fw", fontName="Helvetica-Bold", fontSize=8, textColor=C_SKY, spaceAfter=0, alignment=TA_CENTER)
256
-
257
- ft_tbl = Table([
258
- [Paragraph("IGNITE PERFORMANCE &amp; HEALTH", ft_brand_s)],
259
- [Paragraph("14830 Clayton Rd, Chesterfield, MO 63017 | (314) 887-0858 | info@ignitepah.com", ft_info_s)],
260
- [Paragraph("www.ignitepah.com", ft_web_s)],
261
- ], colWidths=[pw])
262
- ft_tbl.setStyle(TableStyle([
263
- ("BACKGROUND", (0, 0), (-1, -1), C_NAVY),
264
- ("TOPPADDING", (0, 0), (0, 0), 8),
265
- ("BOTTOMPADDING", (0, -1), (0, -1), 8),
266
- ("TOPPADDING", (0, 1), (-1, -1), 1),
267
- ("BOTTOMPADDING", (0, 1), (-1, -1), 1),
268
- ("ALIGN", (0, 0), (-1, -1), "CENTER"),
269
- ]))
270
- story.append(ft_tbl)
271
-
272
- doc.build(story)
273
- buffer.seek(0)
274
- return buffer
275
-
276
-
277
- # =============================================================================
278
- # FOOTER RENDERER
279
- # =============================================================================
280
- def render_footer():
281
- st.markdown(
282
- '<div class="site-footer">'
283
- f'{"<img class=footer-logo src=" + chr(34) + LOGO_B64 + chr(34) + " alt=Ignite />" if LOGO_B64 else ""}'
284
- '<p class="brand">Ignite Performance &amp; Health</p>'
285
- '<p class="tagline">Physician-Supervised Peptide Therapy</p>'
286
- '<div style="width:60px;height:2px;background:#f58300;margin:14px auto;border-radius:2px;"></div>'
287
- '<p class="contact-label">Visit Us</p>'
288
- '<p class="contact-row">14830 Clayton Rd, Chesterfield, MO 63017</p>'
289
- '<p class="contact-label">Get In Touch</p>'
290
- '<p class="contact-row">'
291
- '<a href="tel:3148870858">(314) 887-0858</a>'
292
- '<span class="sep">|</span>'
293
- '<a href="mailto:info@ignitepah.com">info@ignitepah.com</a>'
294
- '</p>'
295
- '<p class="contact-row"><a href="https://ignitepah.com" target="_blank">www.ignitepah.com</a></p>'
296
- '<p class="disclaimer">'
297
- f'{BRAND["disclaimer"]}'
298
- '</p>'
299
- '</div>',
300
- unsafe_allow_html=True,
301
- )
302
-
303
-
304
- # =============================================================================
305
- # PAGE CONFIG & CSS
306
- # =============================================================================
307
- st.set_page_config(
308
- page_title="Ignite Performance & Health — Peptide Protocol Selector",
309
- layout="wide",
310
- initial_sidebar_state="collapsed",
311
- )
312
-
313
- st.markdown(
314
- '<link rel="preconnect" href="https://fonts.googleapis.com">'
315
- '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>'
316
- '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,400;0,600;0,700;0,800;1,700&family=Barlow+Condensed:wght@700;800&display=swap">',
317
- unsafe_allow_html=True,
318
- )
319
-
320
- st.markdown("""
321
- <style>
322
- .stApp { background-color: #f0f6fa; }
323
- html, body, [class*="css"] { font-family: 'Barlow', 'Segoe UI', sans-serif; }
324
- .stMarkdown p, .stMarkdown span,
325
- div[data-testid="stMarkdownContainer"] p,
326
- div[data-testid="stMarkdownContainer"] span,
327
- div[data-testid="stMarkdownContainer"] li,
328
- .stRadio label, .stRadio span,
329
- .stCheckbox label, .stCheckbox span,
330
- .stNumberInput label, .stTextInput label, .stTextArea label,
331
- .stCaption, .stCaption p, .stForm p, .stForm span, .stForm label { color: #023047 !important; }
332
- div[data-testid="stMarkdownContainer"] .site-footer p,
333
- div[data-testid="stMarkdownContainer"] .site-footer span,
334
- div[data-testid="stMarkdownContainer"] .site-footer a,
335
- .stMarkdown .site-footer p, .stMarkdown .site-footer a { color: #ffffff !important; }
336
- div[data-testid="stMarkdownContainer"] .site-footer .brand,
337
- .stMarkdown .site-footer .brand { color: #f58300 !important; }
338
- div[data-testid="stMarkdownContainer"] .site-footer .tagline,
339
- .stMarkdown .site-footer .tagline { color: #8ecae6 !important; }
340
- div[data-testid="stMarkdownContainer"] .site-footer .disclaimer,
341
- .stMarkdown .site-footer .disclaimer { color: rgba(255,255,255,0.7) !important; }
342
- .stTextInput input, .stNumberInput input, .stTextArea textarea {
343
- color: #023047 !important; background-color: #ffffff !important;
344
- border: 1px solid #ccdde8 !important; border-radius: 6px !important;
345
- }
346
- .stTextInput input:focus, .stNumberInput input:focus, .stTextArea textarea:focus {
347
- border-color: #219ebc !important; box-shadow: 0 0 0 2px rgba(33,158,188,0.2) !important;
348
- }
349
-
350
- /* ── Hero ── */
351
- .hero {
352
- background: linear-gradient(135deg, #023047 60%, #219ebc 100%);
353
- border-radius: 12px; padding: 2.2rem 2.5rem 1.8rem; margin-bottom: 1.5rem;
354
- text-align: center;
355
- }
356
- .hero-logo { display: block; margin: 0 auto 18px; height: 130px; width: auto; }
357
- .hero-title { color: #f58300 !important; font-family: 'Barlow Condensed', sans-serif; font-size: 2.2rem; font-weight: 800; margin: 0 0 4px; letter-spacing: 1px; text-transform: uppercase; }
358
- .hero-sub { color: #8ecae6 !important; font-size: 1.1rem; font-weight: 600; margin: 8px 0 0; }
359
- .hero-tagline { color: rgba(255,255,255,0.85) !important; font-size: 0.85rem; margin: 8px 0 0; letter-spacing: 0.5px; }
360
-
361
- /* ── Section bands ── */
362
- .section-band {
363
- background: linear-gradient(90deg, #023047 0%, #012233 100%);
364
- color: #ffc533 !important; font-family: 'Barlow Condensed', sans-serif;
365
- font-size: 1.1rem; font-weight: 800; letter-spacing: 1.2px;
366
- padding: 12px 16px; margin: 20px 0 14px;
367
- border-radius: 4px; text-transform: uppercase;
368
- }
369
-
370
- /* ── Form & buttons ── */
371
- .stForm { background: white; padding: 20px; border-radius: 8px; }
372
- .stFormSubmitButton > button,
373
- .stFormSubmitButton > button:active,
374
- .stFormSubmitButton > button:focus {
375
- background: linear-gradient(135deg, #f58300 0%, #ffc533 100%) !important;
376
- color: #023047 !important; font-size: 1rem; font-weight: 700;
377
- letter-spacing: 0.5px; padding: 12px 24px; border-radius: 6px !important;
378
- border: none !important; box-shadow: 0 4px 12px rgba(245,131,0,0.25) !important;
379
- }
380
- .stFormSubmitButton > button p,
381
- .stFormSubmitButton > button span { color: #023047 !important; font-weight: 700 !important; }
382
- .stFormSubmitButton > button:hover {
383
- background: linear-gradient(135deg, #ffc533 0%, #f58300 100%) !important;
384
- }
385
- .stDownloadButton > button,
386
- .stDownloadButton > button:active,
387
- .stDownloadButton > button:focus {
388
- background: linear-gradient(135deg, #f58300 0%, #ffc533 100%) !important;
389
- color: #023047 !important; font-size: 0.9rem; font-weight: 700;
390
- padding: 10px 20px; border-radius: 6px !important;
391
- border: none !important; box-shadow: 0 2px 8px rgba(245,131,0,0.25) !important;
392
- width: 100%;
393
- }
394
- .stDownloadButton > button p,
395
- .stDownloadButton > button span { color: #023047 !important; font-weight: 700 !important; }
396
- .stDownloadButton > button:hover {
397
- background: linear-gradient(135deg, #ffc533 0%, #f58300 100%) !important;
398
- }
399
- .stButton > button,
400
- .stButton > button:active,
401
- .stButton > button:focus {
402
- background: linear-gradient(135deg, #f58300 0%, #ffc533 100%) !important;
403
- color: #023047 !important; font-size: 1rem; font-weight: 700;
404
- padding: 12px 24px; border-radius: 6px !important;
405
- border: none !important; box-shadow: 0 4px 12px rgba(245,131,0,0.25) !important;
406
- }
407
- .stButton > button p,
408
- .stButton > button span { color: #023047 !important; font-weight: 700 !important; }
409
- .stButton > button:hover {
410
- background: linear-gradient(135deg, #ffc533 0%, #f58300 100%) !important;
411
- }
412
-
413
- /* ── Peptide cards ── */
414
- .pep-card {
415
- background: white; border: 1px solid #b8d0de; border-radius: 8px;
416
- padding: 16px; margin-bottom: 16px;
417
- }
418
- .pep-card-branded {
419
- background: white; border: 2px solid #219ebc; border-radius: 8px;
420
- padding: 16px; margin-bottom: 16px;
421
- }
422
- .pep-badge {
423
- display: inline-block; background: #219ebc; color: white;
424
- font-size: 0.7rem; font-weight: 700; padding: 4px 8px;
425
- border-radius: 3px; margin-bottom: 8px; letter-spacing: 0.5px;
426
- }
427
- .pep-badge-branded {
428
- display: inline-block; background: #f58300; color: white;
429
- font-size: 0.7rem; font-weight: 700; padding: 4px 8px;
430
- border-radius: 3px; margin-bottom: 8px; letter-spacing: 0.5px;
431
- }
432
- .pep-name {
433
- color: #023047 !important; font-family: 'Barlow Condensed', sans-serif;
434
- font-size: 1.3rem; font-weight: 800; margin-bottom: 2px;
435
- }
436
- .pep-components {
437
- color: #219ebc !important; font-size: 0.85rem; font-style: italic; margin-bottom: 8px;
438
- }
439
- .pep-desc {
440
- color: #4a6070 !important; font-size: 0.9rem; margin-bottom: 14px; line-height: 1.5;
441
- }
442
- .pep-label {
443
- color: #f58300 !important; font-weight: 700; font-size: 0.75rem;
444
- text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;
445
- }
446
- .pep-value {
447
- color: #023047 !important; font-size: 0.9rem; margin-bottom: 10px;
448
- }
449
- .pep-cats {
450
- display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px;
451
- }
452
- .cat-pill {
453
- background: #219ebc; color: #ffffff !important;
454
- font-size: 0.72rem; font-weight: 700; padding: 4px 10px;
455
- border-radius: 12px; white-space: nowrap;
456
- }
457
-
458
- /* ── Pricing grid ── */
459
- .pricing-grid {
460
- display: grid; gap: 8px; margin-top: 10px;
461
- }
462
- .price-cell {
463
- background: #f0f6fa; border: 1px solid #b8d0de; border-radius: 6px;
464
- padding: 10px 8px; text-align: center;
465
- }
466
- .price-cell-suggested {
467
- background: linear-gradient(135deg, #fff8ec 0%, #fff0d6 100%); border: 2.5px solid #f58300; border-radius: 6px;
468
- padding: 12px 8px; text-align: center; box-shadow: 0 2px 8px rgba(245,131,0,0.2);
469
- position: relative;
470
- }
471
- .price-vials { color: #023047 !important; font-weight: 700; font-size: 0.8rem; margin-bottom: 2px; }
472
- .price-amount { color: #f58300 !important; font-family: 'Barlow Condensed', sans-serif; font-size: 1.55rem; font-weight: 800; }
473
- .price-per-week { color: #023047 !important; font-size: 0.72rem; font-weight: 600; margin-top: 2px; }
474
- .price-suggested-label { background: #f58300; color: #ffffff !important; font-size: 0.65rem; font-weight: 700; text-transform: uppercase; padding: 2px 6px; border-radius: 3px; display: inline-block; margin-top: 4px; }
475
-
476
- /* ── Styled HR ── */
477
- .styled-hr { margin: 2rem 0; border: 0; height: 2px; background: linear-gradient(90deg, transparent, #f58300, transparent); }
478
-
479
- /* ── CTA box ── */
480
- .cta-box {
481
- background: linear-gradient(135deg, #023047 0%, #034a6e 100%);
482
- border: 2px solid #f58300; border-radius: 8px; padding: 24px;
483
- margin: 24px 0; color: #ffffff !important;
484
- }
485
- .cta-box, .cta-box * { color: #ffffff !important; }
486
- .cta-box h3 { color: #ffc533 !important; font-family: 'Barlow Condensed', sans-serif; font-size: 1.4rem; font-weight: 800; margin-bottom: 12px; }
487
- .cta-box p { color: #ffffff !important; line-height: 1.6; margin-bottom: 10px; }
488
- .cta-box strong { color: #ffc533 !important; }
489
- .cta-link {
490
- display: inline-block; background: #f58300; color: #ffffff !important;
491
- font-weight: 700; padding: 12px 20px; border-radius: 6px;
492
- text-decoration: none; margin-top: 14px; letter-spacing: 0.5px;
493
- }
494
- .cta-link:hover { background: #ffc533; color: #023047 !important; }
495
-
496
- /* ── Site footer ── */
497
- .site-footer {
498
- background: #023047; color: #ffffff !important; padding: 32px 24px; text-align: center;
499
- margin-top: 40px; border-top: 4px solid #f58300;
500
- }
501
- .site-footer, .site-footer * { color: #ffffff !important; }
502
- .site-footer .brand { color: #f58300 !important; font-family: 'Barlow Condensed', sans-serif; font-size: 1.3rem; font-weight: 800; margin: 8px 0 4px; letter-spacing: 1px; }
503
- .site-footer .tagline { color: #8ecae6 !important; font-size: 0.85rem; margin-bottom: 14px; }
504
- .site-footer .contact-label { color: #ffc533 !important; font-weight: 700; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 12px; margin-bottom: 4px; }
505
- .site-footer .contact-row { color: white !important; font-size: 0.95rem; margin-bottom: 4px; line-height: 1.6; }
506
- .site-footer .contact-row a { color: white !important; text-decoration: none; }
507
- .site-footer .contact-row a:hover { color: #ffc533 !important; }
508
- .site-footer .sep { color: #8ecae6 !important; margin: 0 10px; }
509
- .site-footer .disclaimer { color: rgba(255,255,255,0.7) !important; font-size: 0.8rem; margin-top: 16px; line-height: 1.6; }
510
-
511
- /* ── Mobile ── */
512
- @media (max-width: 768px) {
513
- .hero { padding: 1.5rem 1.2rem 1.2rem; }
514
- .hero-logo { height: 90px; }
515
- .hero-title { font-size: 1.6rem !important; }
516
- .pep-name { font-size: 1.1rem; }
517
- .pricing-grid { grid-template-columns: repeat(3, 1fr) !important; }
518
- .price-amount { font-size: 1.2rem; }
519
- }
520
- @media (max-width: 480px) {
521
- .hero-title { font-size: 1.3rem !important; }
522
- .pricing-grid { grid-template-columns: repeat(2, 1fr) !important; }
523
- }
524
- </style>
525
- """, unsafe_allow_html=True)
526
-
527
-
528
- # =============================================================================
529
- # SESSION STATE
530
- # =============================================================================
531
- if "pep_results" not in st.session_state:
532
- st.session_state["pep_results"] = None
533
- st.session_state["pep_goals"] = []
534
- st.session_state["pep_top_cats"] = []
535
- st.session_state["pep_patient_name"] = ""
536
- st.session_state["pep_patient_email"] = ""
537
- st.session_state["pep_patient_phone"] = ""
538
-
539
-
540
- # =============================================================================
541
- # RESULTS PAGE
542
- # =============================================================================
543
- if st.session_state["pep_results"]:
544
- pname = st.session_state.get("pep_patient_name", "")
545
- pemail = st.session_state.get("pep_patient_email", "")
546
- pphone = st.session_state.get("pep_patient_phone", "")
547
- selected_goals = st.session_state.get("pep_goals", [])
548
- top_cats = st.session_state.get("pep_top_cats", [])
549
- results = st.session_state["pep_results"]
550
-
551
- # ── Hero ──
552
- st.markdown(
553
- '<div class="hero">'
554
- f'{"<img class=hero-logo src=" + chr(34) + LOGO_B64 + chr(34) + " alt=Ignite />" if LOGO_B64 else ""}'
555
- '<div class="hero-title">Peptide Protocol Selector</div>'
556
- '<div class="hero-sub">Your Personalized Peptide Recommendations</div>'
557
- f'<div class="hero-tagline">Prepared for {pname}</div>'
558
- '</div>',
559
- unsafe_allow_html=True,
560
- )
561
-
562
- # ── Goals summary ──
563
- st.markdown('<div class="section-band">Your Health Goals</div>', unsafe_allow_html=True)
564
- goals_html = " · ".join(selected_goals)
565
- st.markdown(f'<p style="color:#023047;font-size:0.95rem;margin-bottom:8px;">{goals_html}</p>', unsafe_allow_html=True)
566
- cats_html = " ".join(f'<span class="cat-pill">{c}</span>' for c in top_cats)
567
- st.markdown(f'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:20px;">{cats_html}</div>', unsafe_allow_html=True)
568
-
569
- # ── Group and display ──
570
- branded, blends, singles = group_by_tier(results)
571
-
572
- def render_card(pep_key, score, matched, is_branded=False):
573
- pep = PEPTIDES[pep_key]
574
- display_name = pep.get("brand_name") or pep_key
575
- card_class = "pep-card-branded" if is_branded else "pep-card"
576
- badge_class = "pep-badge-branded" if is_branded else "pep-badge"
577
- # Only show badge on singles and non-branded blends
578
- show_badge = not is_branded
579
- badge_text = "BLEND" if pep["is_blend"] else "SINGLE"
580
-
581
- components_str = " + ".join(pep["components"]) if pep["is_blend"] else ""
582
- cat_pills = " ".join(f'<span class="cat-pill">{c}</span>' for c in matched)
583
-
584
- sv = pep["suggested_vials"]
585
- price_cells = ""
586
- for vials in range(1, 7):
587
- price = pep["pricing"][vials]
588
- is_suggested = vials == sv
589
- cell_class = "price-cell-suggested" if is_suggested else "price-cell"
590
- suggested_label = '<div class="price-suggested-label">✓ Recommended</div>' if is_suggested else ""
591
- per_week = price / max(vials * 4, 1)
592
- price_cells += (
593
- f'<div class="{cell_class}">'
594
- f'<div class="price-vials">{vials} Vial{"s" if vials > 1 else ""}</div>'
595
- f'<div class="price-amount">${price:,.2f}</div>'
596
- f'<div class="price-per-week">${per_week:,.2f}/wk</div>'
597
- f'{suggested_label}'
598
- f'</div>'
599
- )
600
-
601
- badge_html = f'<div class="{badge_class}">{badge_text}</div>' if show_badge else ""
602
- st.markdown(
603
- f'<div class="{card_class}">'
604
- f'{badge_html}'
605
- f'<div class="pep-name">{display_name}</div>'
606
- f'{"<div class=pep-components>" + components_str + "</div>" if components_str else ""}'
607
- f'<div class="pep-desc">{pep["description"]}</div>'
608
- f'<div class="pep-cats">{cat_pills}</div>'
609
- f'<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px;">'
610
- f'<div>'
611
- f'<div class="pep-label">Dosage</div>'
612
- f'<div class="pep-value">{pep["dosage"]}</div>'
613
- f'</div>'
614
- f'<div>'
615
- f'<div class="pep-label">Cycle</div>'
616
- f'<div class="pep-value">{pep["cycle"]}</div>'
617
- f'</div>'
618
- f'</div>'
619
- f'<div class="pep-label">Vial Amount: {pep["vial_amount"]} | Source: {pep["source"]}</div>'
620
- f'<div class="pep-label" style="margin-top:10px;">Pricing</div>'
621
- f'<div class="pricing-grid" style="grid-template-columns:repeat(6,1fr)">{price_cells}</div>'
622
- f'</div>',
623
- unsafe_allow_html=True,
624
- )
625
-
626
- if branded:
627
- st.markdown('<div class="section-band">Ignite Branded Blends — Recommended</div>', unsafe_allow_html=True)
628
- st.markdown(
629
- '<p style="color:#4a6070;font-size:0.9rem;margin-bottom:16px;">'
630
- 'Curated multi-peptide protocols from our proprietary blend line. These combine complementary peptides '
631
- 'into a single vial for convenience and synergistic benefit.</p>',
632
- unsafe_allow_html=True,
633
- )
634
- for pep_key, score, matched in branded[:6]:
635
- render_card(pep_key, score, matched, is_branded=True)
636
-
637
- if blends:
638
- st.markdown('<div class="section-band">Combination Blends</div>', unsafe_allow_html=True)
639
- for pep_key, score, matched in blends[:4]:
640
- render_card(pep_key, score, matched, is_branded=False)
641
-
642
- if singles:
643
- st.markdown('<div class="section-band">Individual Peptides</div>', unsafe_allow_html=True)
644
- st.markdown(
645
- '<p style="color:#4a6070;font-size:0.9rem;margin-bottom:16px;">'
646
- 'Single-peptide options for targeted protocols or for patients who prefer to build their stack one component at a time.</p>',
647
- unsafe_allow_html=True,
648
- )
649
- for pep_key, score, matched in singles[:4]:
650
- render_card(pep_key, score, matched, is_branded=False)
651
-
652
- # ── CTA ──
653
- st.markdown(
654
- f'<div class="cta-box">'
655
- f'<h3>Ready to Get Started, {pname}?</h3>'
656
- f'<p>These recommendations are based on your stated goals. All peptide protocols require '
657
- f'physician evaluation before we begin. Our team will walk through each option during your consultation, '
658
- f'answer your questions, and build a protocol that fits your situation.</p>'
659
- f'<p><strong>What happens next:</strong> our team reaches out, we schedule your consultation, '
660
- f'and we go from there.</p>'
661
- f'<p><strong>Contact:</strong> {pemail} &nbsp;|&nbsp; {pphone}</p>'
662
- f'<a class="cta-link" href="{BRAND["scheduling_url"]}" target="_blank">Schedule Your Consultation</a>'
663
- f'</div>',
664
- unsafe_allow_html=True,
665
- )
666
-
667
- # ── PDF Download ──
668
- st.markdown('<hr class="styled-hr">', unsafe_allow_html=True)
669
- st.markdown("#### Download Your Recommendation")
670
-
671
- if "cached_pep_pdf" not in st.session_state:
672
- st.session_state["cached_pep_pdf"] = generate_pdf(
673
- {"name": pname, "email": pemail, "phone": pphone},
674
- selected_goals,
675
- top_cats,
676
- results,
677
- )
678
-
679
- st.download_button(
680
- "Download PDF",
681
- data=st.session_state["cached_pep_pdf"],
682
- file_name="ignite_peptide_protocol.pdf",
683
- mime="application/pdf",
684
- use_container_width=True,
685
- )
686
-
687
- # ── Start over ──
688
- st.markdown('<hr class="styled-hr">', unsafe_allow_html=True)
689
- if st.button("Start New Selection", use_container_width=True):
690
- for key in ["pep_results", "pep_goals", "pep_top_cats",
691
- "pep_patient_name", "pep_patient_email", "pep_patient_phone",
692
- "cached_pep_pdf"]:
693
- if key in st.session_state:
694
- del st.session_state[key]
695
- st.session_state["pep_results"] = None
696
- st.session_state["pep_goals"] = []
697
- st.session_state["pep_top_cats"] = []
698
- st.session_state["pep_patient_name"] = ""
699
- st.session_state["pep_patient_email"] = ""
700
- st.session_state["pep_patient_phone"] = ""
701
- st.rerun()
702
-
703
- render_footer()
704
- st.stop()
705
-
706
-
707
- # =============================================================================
708
- # INTAKE FORM
709
- # =============================================================================
710
- st.markdown(
711
- '<div class="hero">'
712
- f'{"<img class=hero-logo src=" + chr(34) + LOGO_B64 + chr(34) + " alt=Ignite />" if LOGO_B64 else ""}'
713
- '<div class="hero-title">Peptide Protocol Selector</div>'
714
- '<div class="hero-sub">Find the Right Peptides for Your Goals</div>'
715
- '<div class="hero-tagline">Physician-Supervised · Evidence-Informed · Personalized</div>'
716
- '</div>',
717
- unsafe_allow_html=True,
718
- )
719
-
720
- st.markdown(
721
- "Select your health goals below and we will match you to the peptide protocols that fit best. "
722
- "Every recommendation requires physician review before we proceed. "
723
- "This is not a prescription tool — it is a starting point for your consultation.",
724
- )
725
-
726
- with st.form("peptide_intake"):
727
-
728
- st.markdown('<div class="section-band">Your Information</div>', unsafe_allow_html=True)
729
- c1, c2 = st.columns(2)
730
- with c1:
731
- first_name = st.text_input("First Name *")
732
- email = st.text_input("Email Address *")
733
- with c2:
734
- last_name = st.text_input("Last Name *")
735
- phone = st.text_input("Cell Phone *")
736
-
737
- st.markdown('<div class="section-band">What Are You Looking to Address?</div>', unsafe_allow_html=True)
738
- st.caption("Select all goals that apply. We will match peptides to your priorities.")
739
-
740
- # Build flat goal list grouped by category
741
- goal_checks = {}
742
- cat_keys = list(CATEGORIES.keys())
743
- mid = (len(cat_keys) + 1) // 2
744
-
745
- c1, c2 = st.columns(2)
746
- with c1:
747
- for cat_name in cat_keys[:mid]:
748
- cat = CATEGORIES[cat_name]
749
- st.markdown(
750
- f'<p style="color:#219ebc;font-weight:700;font-size:0.85rem;margin:12px 0 4px;text-transform:uppercase;">'
751
- f'{cat_name}</p>',
752
- unsafe_allow_html=True,
753
- )
754
- for goal in cat["goals"]:
755
- goal_checks[goal] = st.checkbox(goal, key=f"g_{goal}")
756
- with c2:
757
- for cat_name in cat_keys[mid:]:
758
- cat = CATEGORIES[cat_name]
759
- st.markdown(
760
- f'<p style="color:#219ebc;font-weight:700;font-size:0.85rem;margin:12px 0 4px;text-transform:uppercase;">'
761
- f'{cat_name}</p>',
762
- unsafe_allow_html=True,
763
- )
764
- for goal in cat["goals"]:
765
- goal_checks[goal] = st.checkbox(goal, key=f"g_{goal}")
766
-
767
- st.markdown('<div class="section-band">Additional Context</div>', unsafe_allow_html=True)
768
- additional = st.text_area(
769
- "Anything else we should know? (injuries, current medications, previous peptide experience, etc.)",
770
- placeholder="Optional — helps our physician team customize your protocol.",
771
- )
772
-
773
- # ── Consent ──
774
- st.markdown('<div class="section-band">Consent &amp; Privacy</div>', unsafe_allow_html=True)
775
- st.markdown(
776
- '<p style="color:#023047;font-size:0.88rem;line-height:1.6;margin-bottom:12px;">'
777
- '<strong style="color:#f58300;">Important:</strong> This platform is '
778
- '<strong>not HIPAA-compliant</strong>. Do not share sensitive medical records, '
779
- 'Social Security numbers, or information you would not disclose on a non-secure platform.</p>',
780
- unsafe_allow_html=True,
781
- )
782
- consent = st.checkbox(
783
- "I understand this is an informational tool, not a prescription. "
784
- "All peptide protocols require physician evaluation and approval.",
785
- key="consent",
786
- )
787
-
788
- submitted = st.form_submit_button("Find My Peptides", use_container_width=True)
789
-
790
- # ── Processing ──
791
- if submitted:
792
- selected_goals = [g for g, checked in goal_checks.items() if checked]
793
-
794
- errors = []
795
- if not first_name: errors.append("First Name")
796
- if not last_name: errors.append("Last Name")
797
- if not email: errors.append("Email Address")
798
- if not phone: errors.append("Cell Phone")
799
- if not selected_goals: errors.append("At least one health goal")
800
- if not consent: errors.append("Consent acknowledgment")
801
-
802
- if errors:
803
- st.error(f"Please complete: **{', '.join(errors)}**")
804
- st.stop()
805
-
806
- results = score_peptides(selected_goals)
807
- top_cats = get_top_categories(selected_goals)
808
-
809
- if not results:
810
- st.warning("No peptides matched your selected goals. Please try different selections.")
811
- st.stop()
812
-
813
- st.session_state["pep_results"] = results
814
- st.session_state["pep_goals"] = selected_goals
815
- st.session_state["pep_top_cats"] = top_cats
816
- st.session_state["pep_patient_name"] = f"{first_name} {last_name}"
817
- st.session_state["pep_patient_email"] = email
818
- st.session_state["pep_patient_phone"] = phone
819
- st.rerun()
820
-
821
- render_footer()