/* BioReveal — Main funnel app. (v2.1 — 4-screen funnel w/ contact step) * * Flow: Goals → Contact (name/email/phone) → BioScan → Safety → AI Reveal * * Contact lives at step 2 so even bail-outs are captured leads. */ const { StepIntro, StepGoals, StepContact, StepBioscan, StepSafety, ShenStep } = window.BR_STEPS; // Feature flag: ?shen=1 in URL → use Shen.AI SDK for the bioscan step, // gated by window.SHEN_API_KEY also being set. Falls back to the legacy // homemade bioscan in all other cases. const SHEN_ENABLED = (() => { if (typeof window === "undefined") return false; const params = new URLSearchParams(window.location.search); return params.get("shen") === "1" && !!window.SHEN_API_KEY; })(); const BioscanStep = SHEN_ENABLED ? ShenStep : StepBioscan; // CTA buttons across the site link to bioreveal.html?start=1 so the user // lands straight in the quiz. Plain bioreveal.html still shows the marketing // intro (hero, FAQ, social proof) for organic / nav visits. const AUTOSTART = (() => { if (typeof window === "undefined") return false; const params = new URLSearchParams(window.location.search); return params.get("start") === "1"; })(); function App() { const [stage, setStage] = React.useState(AUTOSTART ? "quiz" : "intro"); const [step, setStep] = React.useState(0); const [state, setState] = React.useState({ bioScan: null, goals: { ids: [], note: "" }, contact: { name: "", email: "", phone: "" }, safety: { age: 32, sex: null, pregnant: false, cancer_history: false, current_meds: "", allergies: "" }, }); const [profile, setProfile] = React.useState(null); const [synthError, setSynthError] = React.useState(null); const update = (key, val) => setState(s => ({ ...s, [key]: val })); const contactReady = () => { const c = state.contact || {}; return (c.name || "").trim().length >= 2 && /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(c.email || ""); }; const stepDef = [ { key: "goals", render: () => update("goals", v)} />, ready: () => (state.goals?.ids || []).length > 0 }, { key: "contact", render: () => update("contact", v)} />, ready: contactReady }, { key: "bioscan", render: () => update("bioScan", v)} />, ready: () => true }, { key: "safety", render: () => update("safety", v)} />, ready: () => !!state.safety?.sex }, ]; const cur = stepDef[step]; const totalSteps = stepDef.length; const progress = Math.round(((step + 1) / totalSteps) * 100); const next = () => { if (step >= totalSteps - 1) { setStage("analysis"); } else setStep(step + 1); }; const back = () => { if (step === 0) setStage("intro"); else setStep(step - 1); }; const startSynthesis = async () => { setSynthError(null); const s = state.safety || {}; const g = state.goals || { ids: [], note: "" }; const c = state.contact || {}; const note = (g.note || "").trim(); const intake = { goals: g.ids || [], basics: { age: s.age, sex: s.sex }, medical: { pregnant: s.pregnant, conditions: s.cancer_history ? ["cancer_history"] : [], family_history: s.cancer_history ? ["fh_cancer"] : [], current_meds: (s.current_meds || "").split(",").map(x => x.trim()).filter(Boolean), allergies: (s.allergies || "").split(",").map(x => x.trim()).filter(Boolean), }, vitals: state.bioScan?.vitals || {}, skin: state.bioScan?.skin || {}, // Voice transcript field carries the written note now — same downstream // contract as before, so synthesize.py + chat.py don't need to change. voice: note ? { transcript: note } : {}, shen: state.bioScan?.shen || {}, wearable: {}, name: (c.name || "").trim() || null, email: (c.email || "").trim() || null, phone: (c.phone || "").trim() || null, }; try { const res = await fetch("/api/synthesize", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(intake), }); const data = await res.json(); if (data.error) { setSynthError(data.message || data.error); return; } setProfile({ protocol: data, intake }); setStage("reveal"); window.scrollTo(0, 0); } catch (e) { setSynthError(e.message); } }; if (stage === "reveal" && profile) { return { setStage("intro"); setStep(0); setProfile(null); window.scrollTo(0, 0); }} />; } return (
{stage === "intro" && { setStage("quiz"); setStep(0); window.scrollTo(0, 0); }}/>} {stage === "quiz" && (
BIOREVEAL · LIVE STEP {step + 1} / {totalSteps}
{cur.render()}
)} {stage === "analysis" && (
)}
); } ReactDOM.createRoot(document.getElementById("root")).render();