/* My Vital Cell β Floating "Get in Touch" CTA
Self-contained overlay: animated dot-grid-headset agent icon, popup with
Contact Us + Custom Plan Builder (persona-aware), inline captcha in chat,
markdown rendering, plan-context handoff to AI. */
(function () {
const { useState, useEffect, useRef, useMemo } = React;
/* ---------- Mobile popup fix (viewport-safe, keyboard-safe) ---------- */
if (typeof document !== 'undefined' && !document.getElementById('mvc-cta-mobile-css')) {
const s = document.createElement('style');
s.id = 'mvc-cta-mobile-css';
s.textContent = `
@media (max-width: 720px) {
.mvc-cta-popup {
width: 96vw !important;
height: min(88vh, 88dvh) !important;
min-width: 0 !important;
min-height: 0 !important;
max-width: 96vw !important;
border-radius: 20px !important;
}
.mvc-cta-popup > div { border-radius: 18px !important; }
}
@supports (height: 100dvh) {
@media (max-width: 720px) {
.mvc-cta-popup { height: 88dvh !important; }
}
}
`;
document.head.appendChild(s);
}
/* ---------- Client-side mirror of src/lib/mvc-agent (personas + catalog) ---------- */
const PERSONAS = [
{ key: 'biohacker', label: 'The Biohacker', desc: 'Quantified-self, stack optimizer, reads the papers.',
color: '#7A5CC6', emoji: 'π§¬', goals: ['longevity', 'cognitive', 'metabolic'] },
{ key: 'athlete', label: 'The Athlete', desc: 'Trains hard, needs recovery and edge.',
color: '#0F5B60', emoji: 'ποΈ', goals: ['recovery', 'metabolic', 'longevity'] },
{ key: 'aesthetic', label: 'Skin & Hair', desc: 'Focused on visible results β glow, thickness, post-procedure.',
color: '#E24A63', emoji: 'β¨', goals: ['regeneration'] },
{ key: 'founder', label: 'The Executive', desc: 'High-output, cognitive edge, resilient sleep.',
color: '#3A6EA5', emoji: 'β‘', goals: ['cognitive', 'longevity', 'metabolic'] },
{ key: 'longevity', label: 'Healthy Aging', desc: 'Playing the long game β mitochondria, resilience, prevention.',
color: '#C9A84C', emoji: 'πΏ', goals: ['longevity', 'regeneration', 'metabolic'] },
{ key: 'recovery-injury', label: 'Injury Recovery', desc: 'Rehabbing something specific and want it healed right.',
color: '#2F855A', emoji: 'π©Ή', goals: ['recovery'] },
];
const CATALOG = [
{ slug: 'ghk-cu', name: 'GHK-Cu Copper Peptide', cat: 'regeneration', catLabel: 'Regeneration', route: 'topical', price: 79, beginner: true,
blurb: 'Copper tripeptide β signals repair and remodeling.',
protocol: '2mg/mL serum, AM/PM to clean skin', timing: 'Daily; visible change by week 6.',
rationale: 'Studied for collagen synthesis and follicle stimulation.' },
{ slug: 'bpc-blend', name: 'BPC-157 + TB-500 Blend', cat: 'regeneration', catLabel: 'Regeneration', route: 'inj', price: 189,
blurb: 'Synergy blend for skin/hair regeneration.',
protocol: '250mcg SC daily, 4 weeks on / 2 off', timing: 'Morning, empty stomach.',
rationale: 'Repair vascularization + tissue remodeling.' },
{ slug: 'bpc-157', name: 'BPC-157', cat: 'recovery', catLabel: 'Recovery', route: 'inj', price: 99, beginner: true,
blurb: 'The most-studied tissue-repair peptide.',
protocol: '250β500mcg SC daily near site, 4β6 weeks', timing: 'Post-training or bedtime.',
rationale: 'Angiogenic; tendon/ligament/gut healing pathways.' },
{ slug: 'tb-500', name: 'TB-500 (Thymosin Ξ²4)', cat: 'recovery', catLabel: 'Recovery', route: 'inj', price: 149,
blurb: 'Slower-acting recovery peptide.',
protocol: '2mg 2Γ/wk Γ 4 weeks, then 2mg weekly', timing: 'Split doses across the week.',
rationale: 'Actin modulation β cell migration and repair.' },
{ slug: 'kpv', name: 'KPV Anti-Inflammatory', cat: 'recovery', catLabel: 'Recovery', route: 'oral', price: 89, beginner: true,
blurb: 'Oral tripeptide for systemic + gut inflammation.',
protocol: '500mcg oral daily', timing: 'Morning with water.',
rationale: 'Ξ±-MSH fragment β NF-ΞΊB pathway.' },
{ slug: 'aod-9604', name: 'AOD-9604', cat: 'metabolic', catLabel: 'Metabolic', route: 'inj', price: 109, beginner: true,
blurb: 'GH fragment β fat metabolism without GH side effects.',
protocol: '300mcg SC daily, fasted AM', timing: '30 min before food.',
rationale: 'Targets lipolysis without affecting IGF-1.' },
{ slug: 'tesofensine', name: 'Tesofensine Research Kit', cat: 'metabolic', catLabel: 'Metabolic', route: 'oral', price: 179,
blurb: 'Advanced-only appetite research compound.',
protocol: 'Micro-doses, 4 weeks on / 4 off, HR monitoring', timing: 'Morning only.',
rationale: 'Triple monoamine reuptake β cardio caution.' },
{ slug: 'epithalon', name: 'Epithalon', cat: 'longevity', catLabel: 'Longevity', route: 'inj', price: 129, beginner: true,
blurb: 'Pineal peptide β telomerase + circadian regulation.',
protocol: '10mg SC daily Γ 10 days, twice yearly', timing: 'Evening before sleep.',
rationale: 'Studied for telomerase and melatonin rhythm.' },
{ slug: 'mots-c', name: 'MOTS-c', cat: 'longevity', catLabel: 'Longevity', route: 'inj', price: 149,
blurb: 'Mitochondrial-derived β metabolic + longevity.',
protocol: '10mg SC 2β3Γ/wk Γ 6 weeks', timing: 'Pre-training days.',
rationale: 'AMPK activator; improves insulin sensitivity.' },
{ slug: 'semax', name: 'Semax Nasal', cat: 'cognitive', catLabel: 'Cognitive', route: 'topical', price: 89, beginner: true,
blurb: 'Intranasal nootropic β focus + neuroprotection.',
protocol: '300β600mcg intranasal, AM only', timing: 'Up to 5 days/week.',
rationale: 'Increases BDNF; no HPA activation.' },
{ slug: 'selank', name: 'Selank Nasal', cat: 'cognitive', catLabel: 'Cognitive', route: 'topical', price: 89, beginner: true,
blurb: 'Anxiolytic nootropic β calm focus.',
protocol: '300mcg intranasal 1β2Γ/day', timing: 'AM + midday if needed.',
rationale: 'GABA + enkephalin modulation.' },
];
const BUDGET_CAP = { low: 175, mid: 425, high: 900 };
/* ---------- Minimal Markdown β JSX (bold, bullets, line breaks, headers) ---------- */
function Markdown({ text }) {
const lines = String(text || '').split('\n');
const blocks = [];
let list = null;
const flush = () => { if (list) { blocks.push(list); list = null; } };
lines.forEach((raw, i) => {
const line = raw.replace(/\s+$/, '');
if (/^\s*[-β’]\s+/.test(line)) {
if (!list) list = { type: 'ul', items: [] };
list.items.push(line.replace(/^\s*[-β’]\s+/, ''));
} else if (/^#{1,3}\s+/.test(line)) {
flush();
const level = line.match(/^#+/)[0].length;
blocks.push({ type: 'h', level, text: line.replace(/^#+\s+/, '') });
} else if (line.trim() === '') {
flush();
blocks.push({ type: 'br' });
} else {
flush();
blocks.push({ type: 'p', text: line });
}
});
flush();
const inline = (s) => {
// **bold** then `code` β safe minimal parser
const parts = [];
const re = /\*\*(.+?)\*\*/g;
let last = 0, m;
while ((m = re.exec(s))) {
if (m.index > last) parts.push(s.slice(last, m.index));
parts.push({m[1]});
last = m.index + m[0].length;
}
if (last < s.length) parts.push(s.slice(last));
return parts;
};
return (
{blocks.map((b, i) => {
if (b.type === 'h') return React.createElement('div', { key: i, style: { fontWeight: 700, fontSize: 14, margin: '4px 0' } }, inline(b.text));
if (b.type === 'ul') return
{b.items.map((it, j) => - {inline(it)}
)}
;
if (b.type === 'br') return
;
return
{inline(b.text)}
;
})}
);
}
/* ---------- Animated Dot-Grid + Headset Agent Icon ---------- */
function AgentIcon({ size = 22, tone = 'auto' }) {
const ref = useRef(null);
useEffect(() => {
let raf = 0;
const dots = ref.current?.querySelectorAll('circle.dot') || [];
const start = performance.now();
const tick = (t) => {
const dt = (t - start) / 1000;
dots.forEach((d, i) => {
const r = parseFloat(d.dataset.br);
const col = i % 5, row = (i / 5) | 0;
const wave = 0.5 + 0.5 * Math.sin(dt * 1.6 + (col + row) * 0.55);
d.setAttribute('r', (r * (0.75 + wave * 0.5)).toFixed(2));
d.setAttribute('opacity', (0.45 + wave * 0.55).toFixed(2));
});
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, []);
const stroke = tone === 'light' ? '#fff' : 'currentColor';
const s = size;
return (
);
}
/* ---------- Plan builder (mirrors src/lib/mvc-agent/planner.ts) ---------- */
function buildPlan({ persona, goals, experience, route, budget, safetyFlags }) {
if (safetyFlags.length) return { blocked: true, reasons: safetyFlags };
const p = PERSONAS.find(x => x.key === persona) || null;
const gs = goals.length ? goals : (p?.goals ?? []);
let pool = CATALOG.filter(c => gs.includes(c.cat));
if (route === 'inj') pool = pool.filter(c => c.route !== 'oral');
if (route === 'oral') pool = pool.filter(c => c.route !== 'inj');
if (experience === 'new') pool = pool.filter(c => c.beginner);
const goalOrder = p?.goals ?? gs;
pool.sort((a, b) => {
const ai = goalOrder.indexOf(a.cat), bi = goalOrder.indexOf(b.cat);
if (ai !== bi) return (ai < 0 ? 99 : ai) - (bi < 0 ? 99 : bi);
if (experience === 'new') return (b.beginner ? 1 : 0) - (a.beginner ? 1 : 0);
return a.price - b.price;
});
const cap = budget ? BUDGET_CAP[budget] : 425;
const items = []; let spent = 0;
for (const c of pool) {
if (spent + c.price > cap) continue;
items.push(c); spent += c.price;
if (items.length >= 3) break;
}
return { blocked: false, persona: p?.label ?? null, items, monthly: spent };
}
/* ---------- Builder wizard ---------- */
function Builder({ onAddToCart, onOpenAI, onPlanChange }) {
const [step, setStep] = useState(0);
const [state, setState] = useState({
persona: null, personas: [], goals: [], experience: null, route: null, budget: null,
age: null, pregnant: null, condition: null, onMed: null,
});
const set = (k, v) => setState(s => ({ ...s, [k]: v }));
const toggle = (k, v) => setState(s => {
const arr = s[k] || [];
return { ...s, [k]: arr.includes(v) ? arr.filter(x => x !== v) : [...arr, v] };
});
const safetyFlags = [];
if (state.pregnant === 'yes') safetyFlags.push('Pregnancy / breastfeeding β peptide research protocols are not appropriate. Please consult a physician.');
if (state.condition === 'yes') safetyFlags.push('Active serious condition β plan requires physician oversight before proceeding.');
if (state.onMed === 'yes' && state.goals.includes('metabolic')) safetyFlags.push('Already on a GLP-1 or similar β stacking is unsafe without medical supervision.');
if (state.age === 'under') safetyFlags.push('Under 21 β our research protocols require adult (21+) participation.');
const steps = [
{ title: 'Which of these fit your research goals?', subtitle: 'Pick as many as apply β we\'ll tune the whole plan around them.', kind: 'persona' },
{ title: 'Which other fields are you interested to incorporate?', subtitle: 'Pick one or more β we\'ll balance the plan.', kind: 'goals' },
{ title: 'What is your experience with peptides?', field: 'experience', kind: 'chips',
opts: [['new', 'Totally New'], ['some', 'Done Some Research'], ['adv', 'Experienced Researcher']] },
{ title: 'Monthly budget?', field: 'budget', kind: 'chips',
opts: [['low', '$100β250'], ['mid', '$250β500'], ['high', '$500+']] },
{ title: 'Disclaimer', kind: 'disclaimer' },
{ title: 'Your custom plan', kind: 'result' },
];
const cur = steps[step];
const canAdvance = (() => {
if (cur.kind === 'persona') return (state.personas || []).length > 0;
if (cur.kind === 'goals') return state.goals.length > 0;
if (cur.kind === 'chips') return !!state[cur.field];
if (cur.kind === 'disclaimer') return !!state.ack;
return true;
})();
const plan = step === steps.length - 1 ? buildPlan({ ...state, safetyFlags }) : null;
// Bubble the current plan context up so the AI has it even before "add to cart"
useEffect(() => {
if (!onPlanChange) return;
onPlanChange({
persona: state.persona, personas: state.personas, goals: state.goals, experience: state.experience,
route: state.route, budget: state.budget,
health: { age: state.age, pregnant: state.pregnant, condition: state.condition, onMed: state.onMed },
});
}, [state.persona, state.personas, state.goals, state.experience, state.route, state.budget, state.age, state.pregnant, state.condition, state.onMed]);
return (
{steps.map((_, i) => (
))}
{cur.title}
{cur.subtitle &&
{cur.subtitle}
}
{cur.kind === 'persona' && (
{PERSONAS.map(p => {
const sel = state.personas || [];
const on = sel.includes(p.key);
const pick = () => setState(s => {
const cursel = s.personas || [];
const next = cursel.includes(p.key) ? cursel.filter(x => x !== p.key) : [...cursel, p.key];
const goals = [];
next.forEach(k => {
const pp = PERSONAS.find(x => x.key === k);
(pp?.goals || []).slice(0, 2).forEach(g => { if (!goals.includes(g)) goals.push(g); });
});
return { ...s, personas: next, persona: next[0] || null, goals };
});
return (
);
})}
)}
{cur.kind === 'goals' && (
{[
{ k: 'regeneration', t: 'Skin & Aesthetics', d: 'Glow, hair, post-procedure', color: '#E24A63' },
{ k: 'recovery', t: 'Recovery & Performance', d: 'Joints, tendons, training', color: '#0F5B60' },
{ k: 'metabolic', t: 'Metabolic & Weight', d: 'Appetite, glucose, body comp', color: '#C9A84C' },
{ k: 'longevity', t: 'Longevity & Energy', d: 'Mitochondria, healthy aging', color: '#3A6EA5' },
{ k: 'cognitive', t: 'Focus & Mood', d: 'Clarity, resilience, sleep', color: '#7A5CC6' },
].map(g => {
const on = state.goals.includes(g.k);
return (
);
})}
)}
{cur.kind === 'chips' && (
{cur.opts.map(([k, l]) => (
))}
)}
{cur.kind === 'disclaimer' && (
)}
{cur.kind === 'result' && plan && (
plan.blocked ? (
Let's talk to a specialist first.
{plan.reasons.map((r, i) => - {r}
)}
) : (
{plan.persona ? <>Tuned for {plan.persona} Β· > : null}
Estimated monthly: ${plan.monthly}
{plan.items.map(p => (
{p.name}
{p.catLabel} Β· ${p.price}/mo
{p.blurb}
Protocol{p.protocol}
Timing{p.timing}
Why: {p.rationale}
))}
{plan.items.length === 0 &&
No compounds match your filters β try loosening budget or route.
}
)
)}
{step < steps.length - 1 && (
)}
);
}
function YesNoRow({ label, val, onSet }) {
return (
{label}
{['no', 'yes'].map(v => (
))}
);
}
/* ---------- Chat pane with inline captcha + markdown + final synthesis + plan β cart ---------- */
function ChatPane({ context, onAddToCart }) {
const [messages, setMessages] = useState([
{ role: 'assistant', text: "Hey β before I open the floodgates, one **carbon-based** check:", captcha: true },
]);
const [passed, setPassed] = useState(false);
const [input, setInput] = useState('');
const [busy, setBusy] = useState(false);
const [remaining, setRemaining] = useState(10);
const [finalPlan, setFinalPlan] = useState(null);
const scrollRef = useRef(null);
useEffect(() => { scrollRef.current?.scrollTo({ top: 1e9, behavior: 'smooth' }); }, [messages, busy]);
const onCaptchaPass = () => {
setPassed(true);
setMessages(m => [
...m,
{ role: 'user', text: "π not a robot" },
{ role: 'assistant', text: context?.goals?.length
? `Nice β human confirmed. I've got your plan context (${context.persona || 'no persona set'}, goals: ${context.goals.join(', ') || 'β'}). What do you want to dig into?`
: "Nice β human confirmed. Tell me what you're optimizing for and I'll get specific." },
]);
};
const send = async () => {
const q = input.trim(); if (!q || busy || remaining <= 0 || !passed) return;
setInput(''); setBusy(true);
const next = [...messages, { role: 'user', text: q }];
setMessages(next);
try {
const agent = window.MVC_AGENT;
const reply = agent ? await agent.ask(q, next, context) : { text: "AI specialist is warming up β try Contact Us for a human.", counted: false };
setMessages(m => [...m, { role: 'assistant', text: reply.text, final: reply.final }]);
if (reply.counted !== false) setRemaining(r => Math.max(0, r - 1));
if (reply.final) { setRemaining(0); setFinalPlan(reply.text); }
} catch (e) {
setMessages(m => [...m, { role: 'assistant', text: "I hit a snag reaching my server. Please try Contact Us for a human specialist." }]);
} finally { setBusy(false); }
};
return (
{context?.items?.length > 0 && (
)}
{remaining} msgs left
{messages.map((m, i) => (
{m.role === 'user' ? m.text :
}
{m.captcha && !passed &&
}
{m.final && (
)}
))}
{busy && (
)}
{remaining === 0 && (
Session complete β for deeper personalization, book a call with a human specialist.
)}
setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send()}
placeholder={!passed ? 'Prove you\'re human first β' : remaining === 0 ? 'Session complete' : 'Ask about a compound, dose, stackβ¦'}
disabled={remaining === 0 || !passed}
style={{ flex: 1, padding: '11px 14px', borderRadius: 10, border: '1px solid var(--color-border)', fontSize: 13, outline: 'none', background: '#fff' }} />
);
}
/* ---------- Inline captcha bubble (charming, low-friction) ---------- */
function InlineCaptcha({ onPass }) {
const [pick, setPick] = useState(null);
const opts = ['π', 'π€', 'π'];
return (
Tap the one that's not a robot:
{opts.map((e, i) => (
))}
{pick !== null && opts[pick] !== 'π' && (
Close β the bees are the humans here.
)}
);
}
/* ---------- Styles ---------- */
const primaryBtn = {
padding: '11px 20px', borderRadius: 10, border: 'none', cursor: 'pointer',
background: 'var(--color-ink-strong)', color: '#fff', fontSize: 13, fontWeight: 600,
letterSpacing: '0.06em', transition: 'background 160ms',
};
const ghostBtn = {
padding: '11px 18px', borderRadius: 10, cursor: 'pointer',
background: 'transparent', color: 'var(--color-ink-strong)', border: '1px solid var(--color-border)',
fontSize: 13, fontWeight: 500,
};
const inputStyle = {
padding: '12px 14px', borderRadius: 10, border: '1px solid var(--color-border)',
fontSize: 14, outline: 'none', background: '#fff', fontFamily: 'inherit', color: 'var(--color-ink-strong)',
};
/* ---------- Main CTA ---------- */
function CTA() {
const [open, setOpen] = useState(false);
const [view, setView] = useState('onboard');
const [iconAtBottom, setIconAtBottom] = useState(false);
const [planContext, setPlanContext] = useState(null);
const [toast, setToast] = useState(null);
const [cartOpen, setCartOpen] = useState(false);
const goChat = () => {
if (!iconAtBottom) { setIconAtBottom(true); setTimeout(() => setView('chat'), 550); }
else setView('chat');
};
const startBuilder = () => { setView('builder'); setIconAtBottom(true); };
const addToCart = (items) => {
try {
if (window.VC_CART && Array.isArray(items)) {
items.forEach((it) => {
const name = typeof it === 'string' ? it : (it?.name || it?.title || 'Custom item');
window.VC_CART.add({ name, catLabel: it?.catLabel || 'Custom plan', formula: it?.formula || '' });
});
}
} catch {}
setPlanContext(pc => ({ ...(pc || {}), items }));
setToast(`Added ${items.length} item${items.length > 1 ? 's' : ''} to cart`);
setTimeout(() => setToast(null), 2400);
};
const reset = () => { setView('onboard'); setIconAtBottom(false); };
const onPlanChange = (ctx) => setPlanContext(pc => ({ ...ctx, items: pc?.items }));
useEffect(() => {
const openFromEvent = (event) => {
const nextView = event?.detail?.view || 'builder';
setOpen(true);
if (nextView === 'builder' || nextView === 'start') {
setView('builder');
setIconAtBottom(true);
} else if (nextView === 'contact') {
setView('contact');
setIconAtBottom(false);
} else if (nextView === 'chat') {
setView('chat');
setIconAtBottom(true);
} else {
setView('onboard');
setIconAtBottom(false);
}
};
window.addEventListener('mvc-cta-open', openFromEvent);
return () => window.removeEventListener('mvc-cta-open', openFromEvent);
}, []);
useEffect(() => {
const updateCartVisibility = (event) => setCartOpen(Boolean(event?.detail?.open));
window.addEventListener('vc-cart-visibility', updateCartVisibility);
return () => window.removeEventListener('vc-cart-visibility', updateCartVisibility);
}, []);
return (
<>
{toast && (
β {toast}
)}
{open && (
{ if (e.target === e.currentTarget) { setOpen(false); reset(); } }}
style={{
position: 'fixed', inset: 0, zIndex: 65,
background: 'rgba(20,20,28,0.28)', backdropFilter: 'blur(8px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
animation: 'mvcFadeIn 200ms ease-out',
}}>
{view === 'onboard' && (
How can we help?
Choose the path that fits β we'll take it from there.
setView('contact')} full />
)}
{view === 'contact' && (
Talk to a specialist
setView('schedule')} />
setView('message')} />
)}
{view === 'schedule' && (
Pick a time
{['Mon 10:00', 'Mon 14:30', 'Tue 09:00', 'Tue 15:00', 'Wed 11:00', 'Thu 13:30'].map(s => (
))}
)}
{view === 'message' && (
)}
{view === 'builder' && (
)}
{view === 'chat' && (
Your session
The specialist to the right knows your plan and safety profile. Ask about compounds, stacks, dosing frameworks, or timing.
Safety first: we won't recommend anything past your stated conditions.
Sessions are capped at 10 messages β we'll wrap with a final recommendation.
)}
{view === 'chat' && (
)}
)}
>
);
}
function OptionCard({ title, desc, accent, onClick, badge, full }) {
return (
);
}
window.MVC_CTA = CTA;
})();