var RM_CHATBOT_CSS = ".rm-chatbot { --rm-primary: #0F5C6E; --rm-accent: #D4853E; --rm-bg: #F5F0E8; --rm-card: #FFFFFF; --rm-text: #1A1A1A; --rm-muted: #666666; --rm-bot-bg: #EDE7DC; --rm-user-bg: var(--rm-primary); --rm-user-text: #FFFFFF; --rm-border: #DDD3C2; --rm-shadow: 0 8px 32px rgba(15, 92, 110, 0.18); position: fixed; bottom: 20px; right: 20px; z-index: 2147483000; font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; font-size: 15px; color: var(--rm-text); }\n.rm-chatbot * { box-sizing: border-box; }\n.rm-chatbot-bubble { width: 60px; height: 60px; border-radius: 50%; background: var(--rm-primary); color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: var(--rm-shadow); border: none; font-size: 26px; transition: transform 150ms ease; }\n.rm-chatbot-bubble:hover { transform: scale(1.06); }\n.rm-chatbot-bubble[hidden] { display: none; }\n.rm-chatbot-bubble { position: relative; }\n.rm-bubble-icon { line-height: 1; }\n.rm-bubble-badge { position: absolute; top: -2px; right: -2px; min-width: 22px; height: 22px; background: #E04A47; color: #fff; border: 2px solid #fff; border-radius: 11px; font-size: 12px; font-weight: 700; display: flex; align-items: center; justify-content: center; padding: 0 5px; box-shadow: 0 2px 6px rgba(224, 74, 71, 0.4); animation: rm-bubble-pulse-@UID 2.4s infinite ease-in-out; }\n.rm-chatbot-bubble.rm-bubble-seen .rm-bubble-badge { display: none; }\n@keyframes rm-bubble-pulse-@UID {\n 0%, 100% { transform: scale(1); opacity: 1; }\n 50% { transform: scale(1.15); opacity: 0.85;}\n}\n.rm-chatbot-panel { width: 380px; max-width: calc(100vw - 40px); height: 580px; max-height: calc(100vh - 40px); min-height: 420px; min-width: 300px; background: var(--rm-bg); border-radius: 16px; display: flex; flex-direction: column; box-shadow: var(--rm-shadow); overflow: hidden; position: relative; }\n.rm-chatbot-panel[hidden] { display: none; }\n.rm-chatbot-header { background: var(--rm-primary); color: #fff; padding: 12px 16px; display: flex; align-items: center; gap: 8px; }\n.rm-chatbot-header img { height: 28px; width: auto; }\n.rm-chatbot-header .rm-title { flex: 1; font-weight: 600; font-size: 15px; }\n.rm-chatbot-header .rm-close { background: transparent; border: 0; color: #fff; cursor: pointer; font-size: 22px; padding: 0 4px; line-height: 1; opacity: 0.85; }\n.rm-chatbot-header .rm-close:hover { opacity: 1; }\n.rm-chatbot-header .rm-voice-toggle { background: transparent; border: 0; color: #fff; cursor: pointer; font-size: 18px; padding: 4px 6px; opacity: 0.85; line-height: 1; border-radius: 6px; transition: background-color 120ms ease, opacity 120ms ease; }\n.rm-chatbot-header .rm-voice-toggle:hover { opacity: 1; background: rgba(255,255,255,0.1); }\n.rm-chatbot-header .rm-voice-toggle.on { background: var(--rm-accent); opacity: 1; }\n.rm-chatbot-header .rm-header-btn { background: transparent; border: 0; color: #fff; cursor: pointer; padding: 4px 7px; font-size: 14px; line-height: 1; opacity: 0.85; border-radius: 6px; min-width: 24px; transition: background-color 120ms ease, opacity 120ms ease; }\n.rm-chatbot-header .rm-header-btn:hover { opacity: 1; background: rgba(255,255,255,0.12); }\n.rm-chatbot-header .rm-header-btn:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }\n.rm-chatbot-feedback-host { margin-top: 8px; }\n.rm-chatbot .rm-terms-card { background: #FFF8E7; border: 1px solid #E8D7A4; border-left: 4px solid var(--rm-accent); border-radius: 8px; padding: 12px 14px; margin: 4px 0 4px; font-size: 13px; color: #5C3F1A; line-height: 1.5; }\n.rm-chatbot .rm-terms-label { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; margin-bottom: 10px; }\n.rm-chatbot .rm-terms-cb { margin-top: 2px; width: 16px; height: 16px; flex: 0 0 auto; cursor: pointer; accent-color: var(--rm-primary); }\n.rm-chatbot .rm-terms-link { color: var(--rm-primary); font-weight: 600; text-decoration: underline; }\n.rm-chatbot .rm-terms-continue { background: var(--rm-primary); color: #fff; border: 0; border-radius: 6px; padding: 7px 16px; font-weight: 700; font-size: 13px; cursor: pointer; }\n.rm-chatbot .rm-terms-continue[disabled] { background: #C9C2B5; cursor: not-allowed; }\n.rm-chatbot .rm-terms-card-done { background: #EAF7F2; border-color: #B8DFD0; border-left-color: var(--rm-primary); color: #0A4756; font-weight: 600; }\n.rm-chatbot .rm-chatbot-composer textarea[disabled], .rm-chatbot .rm-chatbot-composer button[disabled] { opacity: 0.55; cursor: not-allowed; }\n.rm-chatbot-messages { flex: 1 1 auto; overflow-y: auto; padding: 14px 14px 4px; min-height: 240px; display: flex; flex-direction: column; gap: 10px; }\n.rm-msg { max-width: 86%; padding: 10px 14px; border-radius: 14px; line-height: 1.4; word-wrap: break-word; white-space: pre-wrap; }\n.rm-msg.bot { background: var(--rm-bot-bg); color: var(--rm-text); border-top-left-radius: 4px; align-self: flex-start; }\n.rm-msg.user { background: var(--rm-user-bg); color: var(--rm-user-text); border-top-right-radius: 4px; align-self: flex-end; }\n.rm-quick-replies { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 14px 8px; }\n.rm-quick-replies button { background: var(--rm-card); color: var(--rm-primary); border: 1.5px solid var(--rm-primary); border-radius: 18px; padding: 6px 12px; cursor: pointer; font: inherit; transition: all 120ms ease; }\n.rm-quick-replies button:hover { background: var(--rm-primary); color: #fff; }\n.rm-quick-replies button:disabled { opacity: 0.5; cursor: default; }\n.rm-lead-card { margin: 8px 14px; padding: 12px 14px; background: var(--rm-card); border: 1px solid var(--rm-border); border-radius: 10px; }\n.rm-lead-card h4 { margin: 0 0 8px; font-size: 14px; color: var(--rm-primary); }\n.rm-lead-card input { width: 100%; padding: 7px 10px; margin-bottom: 6px; border: 1px solid var(--rm-border); border-radius: 6px; font: inherit; }\n.rm-lead-card .rm-actions { display: flex; gap: 8px; margin-top: 6px; }\n.rm-lead-card button { flex: 1; padding: 8px; border-radius: 6px; cursor: pointer; font: inherit; border: 0; }\n.rm-lead-card .rm-submit { background: var(--rm-primary); color: #fff; }\n.rm-lead-card .rm-skip { background: transparent; color: var(--rm-muted); border: 1px solid var(--rm-border); }\n.rm-upload { margin: 8px 14px; padding: 14px; border: 2px dashed var(--rm-accent); border-radius: 10px; text-align: center; cursor: pointer; background: var(--rm-card); }\n.rm-upload:hover { background: #fff; }\n.rm-upload input[type=file] { display: none; }\n.rm-chatbot .rm-booking-panel { margin: 8px 14px; padding: 12px 14px; background: var(--rm-card); border: 1px solid var(--rm-border); border-radius: 10px; }\n.rm-booking-panel h4 { margin: 0 0 10px; color: var(--rm-primary); font-size: 14px; }\n.rm-booking-panel p.small { font-size: 12px; color: var(--rm-muted); margin: 0 0 10px; }\n.rm-booking-chip { display: block; width: 100%; text-align: left; background: #fff; color: var(--rm-primary); border: 1.5px solid var(--rm-primary); border-radius: 8px; padding: 8px 12px; margin-bottom: 6px; cursor: pointer; font: inherit; }\n.rm-booking-chip:hover { background: var(--rm-primary); color: #fff; }\n.rm-booking-day { font-weight: 600; color: var(--rm-primary); margin: 12px 0 4px; font-size: 13px; }\n.rm-booking-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }\n.rm-booking-slot { background: var(--rm-bot-bg); color: var(--rm-text); border: 1px solid var(--rm-border); border-radius: 6px; padding: 6px 4px; font: inherit; font-size: 12px; cursor: pointer; }\n.rm-booking-slot:hover { background: var(--rm-accent); color: #fff; border-color: var(--rm-accent); }\n.rm-booking-input { width: 100%; padding: 7px 10px; margin-bottom: 6px; border: 1px solid var(--rm-border); border-radius: 6px; font: inherit; }\n.rm-booking-confirm { width: 100%; background: var(--rm-primary); color: #fff; border: 0; padding: 10px; border-radius: 8px; cursor: pointer; font: inherit; font-weight: 600; margin-top: 6px; }\n.rm-booking-confirm:hover { background: var(--rm-accent); }\n.rm-booking-confirm:disabled { opacity: 0.6; cursor: default; }\n.rm-booking-success { padding: 10px; background: #E7F4EC; color: #2e7d3a; border: 1px solid #B6DCC1; border-radius: 6px; font-weight: 600; text-align: center; }\n.rm-chatbot .rm-intake-progress { padding: 6px 14px 4px; background: var(--rm-card); border-top: 1px solid var(--rm-border); }\n.rm-intake-progress-track { background: var(--rm-bot-bg); height: 6px; border-radius: 3px; overflow: hidden; }\n.rm-intake-progress-fill { height: 100%; background: var(--rm-accent); transition: width 300ms ease; }\n.rm-intake-progress.done .rm-intake-progress-fill { background: #3A7D5C; }\n.rm-intake-progress-label { font-size: 11px; color: var(--rm-muted); margin-top: 2px; text-align: right; }\n.rm-chatbot-composer { display: flex; gap: 6px; padding: 10px 12px; background: var(--rm-card); border-top: 1px solid var(--rm-border); align-items: flex-end; flex: 0 0 auto; min-height: 56px; }\n.rm-chatbot-composer textarea { flex: 1; resize: none; max-height: 100px; padding: 8px 10px; border: 1px solid var(--rm-border); border-radius: 8px; font: inherit; font-size: 14px; }\n.rm-chatbot-composer button { background: var(--rm-primary); color: #fff; border: 0; border-radius: 8px; padding: 0 14px; cursor: pointer; font-size: 16px; height: 36px; }\n.rm-chatbot-composer button:disabled { opacity: 0.5; cursor: default; }\n.rm-chatbot-composer .rm-mic { background: var(--rm-card); color: var(--rm-primary); border: 1.5px solid var(--rm-primary); padding: 0 10px; font-size: 18px; }\n.rm-chatbot-composer .rm-mic:hover { background: var(--rm-bot-bg); }\n.rm-chatbot-composer .rm-mic.listening { background: var(--rm-accent); color: #fff; border-color: var(--rm-accent); animation: rm-pulse-@UID 1.2s infinite ease-in-out; }\n@keyframes rm-pulse-@UID {\n 0%,100% { box-shadow: 0 0 0 0 rgba(212, 133, 62, 0.5); }\n 50% { box-shadow: 0 0 0 8px rgba(212, 133, 62, 0); }\n}\n.rm-typing { align-self: flex-start; padding: 10px 14px; background: var(--rm-bot-bg); border-radius: 14px; border-top-left-radius: 4px; }\n.rm-typing .dot { display: inline-block; width: 6px; height: 6px; margin: 0 1px; background: var(--rm-muted); border-radius: 50%; animation: rm-bounce-@UID 1.2s infinite ease-in-out; }\n.rm-typing .dot:nth-child(2) { animation-delay: 0.15s; }\n.rm-typing .dot:nth-child(3) { animation-delay: 0.30s; }\n@keyframes rm-bounce-@UID {\n 0%,80%,100% { transform: translateY(0); opacity: 0.4; }\n 40% { transform: translateY(-4px); opacity: 1; }\n}\n.rm-chatbot.rm-bottom-left { left: 20px; right: auto; }\n.rm-chatbot.rm-bottom-right { right: 20px; left: auto; }\n.rm-chatbot .rm-cta-link { display: inline-block; margin-top: 6px; color: var(--rm-primary); text-decoration: underline; font-weight: 600; }\n.rm-chatbot .rm-cta-button { display: inline-block; margin: 2px 0 4px; padding: 8px 14px; background: var(--rm-primary); color: #fff; border-radius: 18px; text-decoration: none; font-weight: 600; align-self: flex-start; }\n.rm-chatbot .rm-cta-button:hover { background: var(--rm-accent); color: #fff; }\n.rm-chatbot [hidden] { display: none !important; }\n@media (max-width: 480px) { .rm-chatbot.rm-bottom-left, .rm-chatbot.rm-bottom-right { bottom: 12px; } .rm-chatbot-panel { max-width: calc(100vw - 24px); } }"; (function () { var CSS_ID = 'rm-chatbot-embed-css'; var currentWebsiteUrl = 'https://www.northernbeachesacupuncture.com.au'; var DEFAULT_ENDPOINTS = { start: currentWebsiteUrl+'/chat-bot-multisite-start', chat: currentWebsiteUrl+'/chat-bot-multisite-chat', terms: currentWebsiteUrl+'/chat-bot-multisite-terms', lead: currentWebsiteUrl+'/chat-bot-multisite-lead', transcript: currentWebsiteUrl+'/chat-bot-multisite-transcript', voice: currentWebsiteUrl+'/chat-bot-multisite-voice', intent: currentWebsiteUrl+'/chat-bot-multisite-intent', products: currentWebsiteUrl+'/chat-bot-multisite-products', booking: currentWebsiteUrl+'/chat-bot-multisite-booking', intake: currentWebsiteUrl+'/chat-bot-multisite-intake', upload: currentWebsiteUrl+'/chat-bot-multisite-upload', rental: currentWebsiteUrl+'/chat-bot-multisite-rental' }; function injectCss() { if (document.getElementById(CSS_ID)) return; var style = document.createElement('style'); style.id = CSS_ID; style.type = 'text/css'; style.appendChild(document.createTextNode(RM_CHATBOT_CSS)); (document.head || document.documentElement).appendChild(style); } function boot(root) { if (!root || root.getAttribute('data-cbm-ready') === '1') return; root.setAttribute('data-cbm-ready', '1'); if (!root.getAttribute('data-endpoints')) root.setAttribute('data-endpoints', JSON.stringify(DEFAULT_ENDPOINTS)); /* ── configuration from the section (data-*) ── */ var ENDPOINTS = {}; try { ENDPOINTS = JSON.parse(root.getAttribute('data-endpoints') || '{}'); } catch (e) { ENDPOINTS = {}; } var SECTION_ID = parseInt(root.getAttribute('data-section-id') || '0', 10) || 0; var LANG = root.getAttribute('data-language') || 'en'; var OWN_PRIMARY = root.getAttribute('data-primary') || ''; var OWN_ACCENT = root.getAttribute('data-accent') || ''; var STORAGE_KEY = 'rm_chatbot_token'; var TERMS_KEY = 'rm_chatbot_terms_v1'; function store(key, value) { try { if (value === null) localStorage.removeItem(key); else localStorage.setItem(key, value); } catch (e) { } } function stored(key) { try { return localStorage.getItem(key); } catch (e) { return null; } } function urlParam(name) { var m = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.search || ''); return m ? decodeURIComponent(m[1].replace(/\+/g, ' ')) : ''; } var URL_OPEN_CHAT = urlParam('open_chat') === '1'; var URL_HELP_TOPIC = urlParam('topic').slice(0, 60); var state = { token: stored(STORAGE_KEY), siteId: null, brand: null, ui: {}, user: null, open: false, sending: false, helpTopic: URL_HELP_TOPIC || null, termsAccepted: stored(TERMS_KEY) === '1', termsVersion: 'v3.0-2026-05-10', voiceOn: false, voiceGender: null, langTag: 'en-AU', voiceObj: null, listening: false, speechRecog: null, stopping: false }; var HAS_TTS = typeof window.speechSynthesis !== 'undefined' && typeof window.SpeechSynthesisUtterance !== 'undefined'; var SR_CTOR = window.SpeechRecognition || window.webkitSpeechRecognition || null; var HAS_STT = !!SR_CTOR; /* ── helpers ── */ function el(tag, attrs, children) { var node = document.createElement(tag); var k; attrs = attrs || {}; for (k in attrs) { if (!attrs.hasOwnProperty(k)) continue; if (k === 'style') setStyle(node, attrs[k]); else if (k.indexOf('on') === 0) node.addEventListener(k.slice(2).toLowerCase(), attrs[k]); else if (k === 'html') node.innerHTML = attrs[k]; else node.setAttribute(k, attrs[k]); } if (children !== undefined && children !== null) { if (!(children instanceof Array)) children = [children]; for (var i = 0; i < children.length; i++) { if (children[i] === null || children[i] === undefined) continue; node.appendChild(typeof children[i] === 'string' ? document.createTextNode(children[i]) : children[i]); } } return node; } function setStyle(node, styles) { for (var s in styles) { if (styles.hasOwnProperty(s)) node.style[s] = styles[s]; } } function removeNode(node) { if (node && node.parentNode) node.parentNode.removeChild(node); } function txt(key, fallback) { return state.ui && state.ui[key] ? state.ui[key] : fallback; } /** POST JSON to one of the module's routes; resolves with the response or rejects with its error */ function api(key, payload) { var d = $.Deferred(); var body = $.extend({ section_id: SECTION_ID, language: LANG }, payload || {}); $.ajax({ url: ENDPOINTS[key], type: 'POST', data: JSON.stringify(body), contentType: 'application/json', dataType: 'json', headers: { 'X-Lang': LANG } }) .done(function (data) { if (!data || data.ok === false) d.reject(new Error((data && data.error) || 'Request failed')); else d.resolve(data); }) .fail(function (xhr) { var msg = xhr && xhr.responseJSON && xhr.responseJSON.error ? xhr.responseJSON.error : ('HTTP ' + (xhr ? xhr.status : 0)); d.reject(new Error(msg)); }); return d.promise(); } /* ── DOM: bubble + panel inside the section ── */ var bubble, panel, header, headerTitle, msgList, qrBar, composer, composerInput, composerSend, composerMic, headerVoiceBtn, helpBtn, mailBtn, feedbackBtn; function buildDom() { bubble = el('button', { 'class': 'rm-chatbot-bubble', type: 'button', 'aria-label': 'Open chat — new message' }, [ el('span', { 'class': 'rm-bubble-icon', html: '💬' }), el('span', { 'class': 'rm-bubble-badge', 'aria-hidden': 'true' }, '1') ]); bubble.addEventListener('click', openPanel); headerTitle = el('div', { 'class': 'rm-title' }, 'Chat'); headerVoiceBtn = el('button', { 'class': 'rm-voice-toggle', type: 'button', 'aria-label': 'Toggle voice mode', title: 'Talk instead of typing', html: '🔊' }); headerVoiceBtn.addEventListener('click', requestVoiceMode); headerVoiceBtn.hidden = true; var closeBtn = el('button', { 'class': 'rm-close', type: 'button', 'aria-label': 'Close', html: '×' }); closeBtn.addEventListener('click', closePanel); helpBtn = el('button', { 'class': 'rm-header-btn', type: 'button', 'aria-label': 'Help — open Frequently Asked Questions', title: 'Need help? Open FAQ', html: '?' }); helpBtn.addEventListener('click', openFaqDrawer); helpBtn.hidden = true; mailBtn = el('button', { 'class': 'rm-header-btn', type: 'button', 'aria-label': 'Email me a copy of this conversation', title: 'Email me a copy of this chat', html: '✉' }); mailBtn.addEventListener('click', promptEmailTranscript); feedbackBtn = el('button', { 'class': 'rm-header-btn', type: 'button', 'aria-label': 'Leave optional feedback about this chat', title: 'Leave feedback (optional)', html: '★' }); feedbackBtn.addEventListener('click', renderFeedbackSurveyInline); feedbackBtn.hidden = true; header = el('div', { 'class': 'rm-chatbot-header' }, [headerTitle, headerVoiceBtn, helpBtn, mailBtn, feedbackBtn, closeBtn]); msgList = el('div', { 'class': 'rm-chatbot-messages', role: 'log', 'aria-live': 'polite' }); qrBar = el('div', { 'class': 'rm-quick-replies' }); composerInput = el('textarea', { rows: 1, placeholder: 'Type your message…', 'aria-label': 'Message' }); composerInput.addEventListener('input', autoSize); composerInput.addEventListener('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendUserText(); } }); composerMic = el('button', { 'class': 'rm-mic', type: 'button', 'aria-label': 'Tap to talk', title: 'Tap to talk', html: '🎤' }); composerMic.addEventListener('click', toggleListening); composerMic.hidden = true; composerSend = el('button', { 'class': 'rm-send', type: 'button', 'aria-label': 'Send', html: '↑' }); composerSend.addEventListener('click', sendUserText); composer = el('div', { 'class': 'rm-chatbot-composer' }, [composerInput, composerMic, composerSend]); panel = el('div', { 'class': 'rm-chatbot-panel', role: 'dialog', 'aria-label': 'Chat' }, [header, msgList, qrBar, composer]); panel.hidden = true; setStyle(panel, { minHeight: '420px', minWidth: '300px', display: 'flex', flexDirection: 'column' }); setStyle(msgList, { flex: '1 1 auto', minHeight: '240px' }); setStyle(composer, { flex: '0 0 auto', minHeight: '56px' }); root.appendChild(bubble); root.appendChild(panel); applyColours(null); } function autoSize() { composerInput.style.height = 'auto'; composerInput.style.height = Math.min(composerInput.scrollHeight, 100) + 'px'; } function applyColours(palette) { /* the section's own colours win; otherwise the site configuration's palette */ var primary = OWN_PRIMARY || (palette && palette.primary) || ''; var accent = OWN_ACCENT || (palette && palette.accent) || ''; if (primary) { root.style.setProperty('--rm-primary', primary); root.style.setProperty('--rm-user-bg', primary); } if (accent) root.style.setProperty('--rm-accent', accent); if (!OWN_PRIMARY && palette && palette.background) root.style.setProperty('--rm-bg', palette.background); } function applyStart(data) { state.brand = data.brand || null; state.ui = data.ui || {}; state.user = data.user || null; applyColours(state.brand ? state.brand.palette : null); headerTitle.textContent = txt('header_title', 'Chat'); helpBtn.hidden = !state.ui.help_drawer; feedbackBtn.hidden = !state.ui.feedback_survey; headerVoiceBtn.hidden = !(HAS_TTS && state.ui.voice_mode); composerMic.hidden = !(HAS_STT && state.ui.voice_mode); } /* ── voice (browser text-to-speech + speech recognition) ── */ function resolveVoice(gender, langTag) { if (!HAS_TTS) return null; var voices = window.speechSynthesis.getVoices() || []; if (!voices.length) return null; var EN_FEMALE = /female|woman|karen|moira|samantha|tessa|fiona|veena|catherine|allison|susan|zira|hazel|sophie|amelie|amy|joanna|salli|kimberly|ivy|kendra|nicole|olivia|catriona/i; var EN_MALE = /male|man|alex|daniel|fred|tom|jorge|diego|paulo|david|mark|guy|matthew|justin|kevin|brian|russell|thomas|aaron|ryan|lee|james|gordon/i; var ZH_FEMALE = /tingting|ting-ting|mei|sin-ji|huihui|hui-hui|yaoyao|yao-yao|tian-tian|tian|tracy|jenny|xiaoxiao|xiaoyi|mei-jia|li-jia|kana/i; var ZH_MALE = /li-mu|limu|kangkang|kang-kang|ya-ling|yating|yunjian|yunxi|yunyang|yun|gao/i; var prefix = (langTag || 'en').split('-')[0].toLowerCase(); var chinese = prefix === 'zh'; function score(v) { var s = 0; var lang = (v.lang || '').toLowerCase(); var n = v.name || ''; var f = chinese ? ZH_FEMALE : EN_FEMALE; var m = chinese ? ZH_MALE : EN_MALE; if (v.lang === langTag) s += 100; else if (lang.indexOf(prefix) === 0) s += 50; else if (chinese && /zh-/i.test(v.lang || '')) s += 40; if (gender === 'female' && f.test(n)) s += 40; if (gender === 'male' && m.test(n)) s += 40; if (gender === 'female' && m.test(n)) s -= 60; if (gender === 'male' && f.test(n)) s -= 60; if (/novelty|whisper|cellos|good news|bad news|bahh|bells|boing|bubbles|deranged|hysterical|pipe|trinoids|zarvox/i.test(n)) s -= 100; if (v.localService === true) s += 5; return s; } var sorted = voices.slice().sort(function (a, b) { return score(b) - score(a); }); return sorted[0] || null; } function cleanForTTS(text) { return String(text || '').replace(/\[QUICK_REPLIES\][^\n]*$/m, '').replace(/[*_`#>]+/g, '').replace(/\s+/g, ' ').trim(); } function speakIfVoice(text, replies) { if (!state.voiceOn || !HAS_TTS) return; try { window.speechSynthesis.cancel(); } catch (e) { } var say = cleanForTTS(text); if (!say) return; if (replies && replies.length) { var opts = []; for (var i = 0; i < replies.length; i++) opts.push((i + 1) + '. ' + replies[i]); say += '. Options: ' + opts.join('. '); } var u = new window.SpeechSynthesisUtterance(say); if (state.voiceObj) u.voice = state.voiceObj; u.lang = state.langTag || 'en-AU'; window.speechSynthesis.speak(u); } if (HAS_TTS && window.speechSynthesis.getVoices().length === 0 && window.speechSynthesis.addEventListener) { window.speechSynthesis.addEventListener('voiceschanged', function () { if (state.voiceOn && state.voiceGender) state.voiceObj = resolveVoice(state.voiceGender, state.langTag); }); } var stableTranscript = ''; function ensureRecognizer() { if (state.speechRecog || !HAS_STT) return state.speechRecog; var r = new SR_CTOR(); r.lang = state.langTag || 'en-AU'; r.continuous = true; r.interimResults = true; r.maxAlternatives = 1; r.addEventListener('result', function (e) { var interim = ''; for (var i = e.resultIndex; i < e.results.length; i++) { var seg = e.results[i]; var t = (seg[0] && seg[0].transcript) || ''; if (seg.isFinal) stableTranscript += t + ' '; else interim += t; } composerInput.value = (stableTranscript + interim).replace(/^\s+/, ''); autoSize(); }); r.addEventListener('end', function () { composerMic.classList.remove('listening'); if (state.listening && !state.stopping) { try { r.start(); composerMic.classList.add('listening'); } catch (e) { state.listening = false; } } else { state.listening = false; var final = stableTranscript.trim(); if (final) { stableTranscript = ''; composerInput.value = final; autoSize(); sendUserText(); } } }); r.addEventListener('error', function (ev) { state.listening = false; composerMic.classList.remove('listening'); handleMicError(ev && ev.error ? ev.error : 'unknown'); }); state.speechRecog = r; return r; } var micPermission = null; function withMicPermission(done) { if (micPermission === 'granted') { done(true); return; } if (micPermission === 'denied') { done(false); return; } if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { var tracks = stream.getTracks(); for (var i = 0; i < tracks.length; i++) tracks[i].stop(); micPermission = 'granted'; done(true); }, function (err) { micPermission = 'denied'; handleMicError(err && err.name === 'NotAllowedError' ? 'not-allowed' : 'audio-capture'); done(false); }); return; } done(true); } function handleMicError(code) { var msg; if (code === 'not-allowed' || code === 'service-not-allowed') msg = 'Microphone blocked. Click the 🔒 icon in your browser address bar and allow microphone access for this site.'; else if (code === 'audio-capture') msg = 'No microphone detected. Plug in a mic or check your device settings.'; else if (code === 'no-speech') msg = "I didn't hear anything. Try again and speak clearly."; else if (code === 'network') msg = 'Network error — voice recognition needs internet. Check your connection.'; else if (code === 'language-not-supported') msg = 'Your browser does not have voice recognition for this language. Type your message instead.'; else if (code === 'aborted') return; else msg = 'Microphone trouble. Try tapping the mic again, or type your message.'; showToast(msg, 5000); } function showToast(text, ms) { var t = el('div', { 'class': 'rm-toast' }, text); setStyle(t, { position: 'absolute', left: '12px', right: '12px', bottom: '70px', background: 'rgba(20,20,20,0.92)', color: '#fff', padding: '10px 14px', borderRadius: '10px', fontSize: '13px', zIndex: '10', lineHeight: '1.4', boxShadow: '0 4px 14px rgba(0,0,0,0.3)', opacity: '0', transition: 'opacity 200ms' }); panel.appendChild(t); setTimeout(function () { t.style.opacity = '1'; }, 16); setTimeout(function () { t.style.opacity = '0'; setTimeout(function () { removeNode(t); }, 250); }, ms || 3500); } function toggleListening() { var r = ensureRecognizer(); if (!r) { showToast('Voice input not supported in this browser. Try Chrome or Edge.', 5000); return; } if (state.listening) { state.stopping = true; try { r.stop(); } catch (e) { } setTimeout(function () { state.stopping = false; }, 500); return; } withMicPermission(function (ok) { if (!ok) return; try { r.lang = state.langTag || 'en-AU'; stableTranscript = ''; r.start(); state.listening = true; composerMic.classList.add('listening'); composerMic.setAttribute('aria-pressed', 'true'); } catch (e) { setTimeout(function () { try { r.start(); state.listening = true; composerMic.classList.add('listening'); } catch (e2) { handleMicError('aborted'); } }, 300); } }); } function requestVoiceMode() { if (state.voiceOn) { disableVoiceMode(); return; } renderMessage('I’d prefer talking instead of typing', 'user'); renderQuickReplies([]); sendToServer({ message: 'I’d prefer talking instead of typing' }); } function enableVoiceMode(gender, langTag) { state.voiceOn = true; state.voiceGender = gender; state.langTag = langTag || state.langTag || 'en-AU'; state.voiceObj = resolveVoice(gender, state.langTag); headerVoiceBtn.classList.add('on'); headerVoiceBtn.title = 'Voice mode ON — tap to switch back to text'; api('voice', { token: state.token, voice_mode: true, voice_gender: gender }); } function disableVoiceMode() { state.voiceOn = false; state.voiceGender = null; state.voiceObj = null; try { window.speechSynthesis.cancel(); } catch (e) { } headerVoiceBtn.classList.remove('on'); headerVoiceBtn.title = 'Talk instead of typing'; api('voice', { token: state.token, voice_mode: false }); } /* ── rendering ── */ function scrollDown() { msgList.scrollTop = msgList.scrollHeight; } function renderMessage(text, who) { var node = el('div', { 'class': 'rm-msg ' + who }, text); msgList.appendChild(node); scrollDown(); return node; } function renderQuickReplies(replies) { qrBar.innerHTML = ''; if (!replies || !replies.length) return; for (var i = 0; i < replies.length; i++) { (function (label) { var b = el('button', { type: 'button' }, label); b.addEventListener('click', function () { sendQuickReply(label); }); qrBar.appendChild(b); })(replies[i]); } } function renderLink(href, label, asButton) { var a = el('a', { href: href, target: '_blank', rel: 'noopener', 'class': asButton ? 'rm-cta-button' : 'rm-cta-link' }, label); msgList.appendChild(a); scrollDown(); } function renderProductCards(products) { if (!products || !products.length) return; var wrap = el('div', { 'class': 'rm-product-cards', style: { margin: '4px 0 10px' } }); wrap.appendChild(el('div', { style: { fontSize: '12px', color: 'var(--rm-muted,#667085)', margin: '0 0 6px' } }, txt('products_title', 'Products:'))); var strip = el('div', { style: { display: 'flex', gap: '8px', overflowX: 'auto', paddingBottom: '4px' } }); for (var i = 0; i < products.length; i++) { var p = products[i]; var card = el('div', { style: { minWidth: '150px', maxWidth: '160px', flex: '0 0 auto', background: '#fff', border: '1px solid #e2e8f0', borderRadius: '12px', padding: '8px' } }); if (p.image) { card.appendChild(el('div', { style: { height: '84px', borderRadius: '8px', background: '#f7fafc', overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '6px' } }, el('img', { src: p.image, alt: p.name || '', loading: 'lazy', style: { maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' } }))); } card.appendChild(el('div', { style: { fontSize: '13px', fontWeight: '600', lineHeight: '1.25', marginBottom: '2px' } }, p.name || '')); if (p.price) card.appendChild(el('div', { style: { fontSize: '12px', fontWeight: '600', color: 'var(--rm-primary,#0F5C6E)', marginBottom: '6px' } }, '$' + p.price)); card.appendChild(el('a', { href: p.url, target: '_blank', rel: 'noopener', style: { display: 'inline-block', fontSize: '12px', textDecoration: 'none', background: 'var(--rm-primary,#0F5C6E)', color: '#fff', padding: '5px 10px', borderRadius: '16px' } }, 'View & buy →')); strip.appendChild(card); } wrap.appendChild(strip); if (txt('products_note', '')) wrap.appendChild(el('div', { style: { fontSize: '11px', color: 'var(--rm-muted,#8a8a8a)', margin: '6px 0 0' } }, txt('products_note', ''))); msgList.appendChild(wrap); scrollDown(); } function renderTyping() { var node = el('div', { 'class': 'rm-typing' }, [el('span', { 'class': 'dot' }), el('span', { 'class': 'dot' }), el('span', { 'class': 'dot' })]); msgList.appendChild(node); scrollDown(); return node; } function renderIntakeProgress(progress) { var pct = Math.max(0, Math.min(100, progress.pct || 0)); var done = !!progress.done; var bar = root.querySelector('.rm-intake-progress'); if (!bar) { bar = el('div', { 'class': 'rm-intake-progress' }, [el('div', { 'class': 'rm-intake-progress-track' }, [el('div', { 'class': 'rm-intake-progress-fill' })]), el('div', { 'class': 'rm-intake-progress-label' })]); panel.insertBefore(bar, composer); } bar.querySelector('.rm-intake-progress-fill').style.width = pct + '%'; bar.querySelector('.rm-intake-progress-label').textContent = done ? '✅ Intake complete' : '📋 Intake ' + pct + '% complete'; if (done) bar.classList.add('done'); else bar.classList.remove('done'); } /* ── inline booking (clinic sites): service → session → location → practitioner → time → confirm ── */ var booking = null; function bookingPanel() { var p = root.querySelector('.rm-booking-panel'); if (!p) { p = el('div', { 'class': 'rm-booking-panel' }); msgList.appendChild(p); } return p; } function isoDay(offsetDays) { return new Date(Date.now() + offsetDays * 86400000).toISOString().slice(0, 10); } function renderBookingPanel(flow) { booking = booking || { entity_member_id: flow.entity_member_id, sub_service_id: flow.sub_service_id || null, service_id: null, location_id: null, practitioner_user_id: null, from_date: isoDay(0), to_date: isoDay(14) }; removeNode(root.querySelector('.rm-booking-panel')); var p = bookingPanel(); scrollDown(); if (flow.services && !booking.service_id) { p.appendChild(el('h4', {}, 'Choose a service')); for (var i = 0; i < flow.services.length; i++) { (function (s) { var b = el('button', { type: 'button', 'class': 'rm-booking-chip' }, s.name); b.addEventListener('click', function () { booking.service_id = s.id; booking.service_name = s.name; loadSubServices(); }); p.appendChild(b); })(flow.services[i]); } return; } if (booking.sub_service_id && !booking.location_id) loadLocations(); else if (booking.location_id && !booking.practitioner_user_id) loadPractitioners(); else if (booking.practitioner_user_id) loadSlots(); } function chips(p, items, label, pick) { for (var i = 0; i < items.length; i++) { (function (item) { var b = el('button', { type: 'button', 'class': 'rm-booking-chip' }, label(item)); b.addEventListener('click', function () { pick(item); }); p.appendChild(b); })(items[i]); } } function loadSubServices() { api('booking', { action: 'list_sub_services', service_id: booking.service_id, location_id: booking.location_id }).done(function (data) { var p = bookingPanel(); p.innerHTML = ''; p.appendChild(el('h4', {}, booking.service_name + ' — pick a session length')); chips(p, data.sub_services || [], function (s) { return s.name + ' — ' + (s.shown_duration_min || s.internal_duration_min) + ' min · $' + parseFloat(s.price || 0).toFixed(0); }, function (s) { booking.sub_service_id = s.id; booking.sub_service_name = s.name; booking.price = s.price; loadLocations(); }); }); } function loadLocations() { api('booking', { action: 'list_locations' }).done(function (data) { var p = bookingPanel(); p.innerHTML = ''; p.appendChild(el('h4', {}, 'Choose a location')); chips(p, data.locations || [], function (l) { return l.full_location || ('Location #' + l.id); }, function (l) { booking.location_id = l.id; booking.location_name = l.full_location || ('Location #' + l.id); loadPractitioners(); }); }); } function loadPractitioners() { api('booking', { action: 'list_practitioners', sub_service_id: booking.sub_service_id }).done(function (data) { var p = bookingPanel(); p.innerHTML = ''; p.appendChild(el('h4', {}, 'Choose a practitioner')); chips(p, data.practitioners || [], function (x) { var n = ((x.first_name || '') + ' ' + (x.first_surname || '')).trim(); return n || x.alias || ('Practitioner #' + x.assigned_user_id); }, function (x) { booking.practitioner_user_id = x.assigned_user_id; booking.practitioner_name = ((x.first_name || '') + ' ' + (x.first_surname || '')).trim(); loadSlots(); }); }); } function formatDate(iso) { var d = new Date(iso + 'T00:00:00'); try { return d.toLocaleDateString(undefined, { weekday: 'short', day: 'numeric', month: 'short' }); } catch (e) { return iso; } } function loadSlots() { api('booking', { action: 'list_slots', sub_service_id: booking.sub_service_id, practitioner_user_id: booking.practitioner_user_id, location_id: booking.location_id, from_date: booking.from_date, to_date: booking.to_date }).done(function (data) { var p = bookingPanel(); p.innerHTML = ''; p.appendChild(el('h4', {}, 'Pick a time')); var slots = data.slots || []; if (!slots.length) { p.appendChild(el('p', { 'class': 'small' }, 'No slots in the next 2 weeks. Try a later date or call us.')); var next = el('button', { type: 'button', 'class': 'rm-booking-chip' }, 'Show next 2 weeks'); next.addEventListener('click', function () { booking.from_date = booking.to_date; booking.to_date = new Date(Date.parse(booking.from_date) + 14 * 86400000).toISOString().slice(0, 10); loadSlots(); }); p.appendChild(next); return; } var byDate = {}; var days = []; for (var i = 0; i < slots.length; i++) { var day = slots[i].date_time_from.slice(0, 10); if (!byDate[day]) { byDate[day] = []; days.push(day); } byDate[day].push(slots[i]); } days.sort(); for (var d = 0; d < days.length; d++) { p.appendChild(el('div', { 'class': 'rm-booking-day' }, formatDate(days[d]))); var grid = el('div', { 'class': 'rm-booking-grid' }); for (var s = 0; s < byDate[days[d]].length; s++) { (function (slot) { var b = el('button', { type: 'button', 'class': 'rm-booking-slot' }, slot.date_time_from.slice(11, 16)); b.addEventListener('click', function () { confirmSlot(slot); }); grid.appendChild(b); })(byDate[days[d]][s]); } p.appendChild(grid); } }); } function confirmSlot(slot) { booking.slot_from = slot.date_time_from; booking.slot_to = slot.date_time_to; booking.availability_days_id = slot.availability_days_id; booking.room_id = slot.room_id; var p = bookingPanel(); p.innerHTML = ''; p.appendChild(el('h4', {}, 'Confirm your booking')); p.appendChild(el('p', { 'class': 'small' }, (booking.sub_service_name || 'Session') + ' with ' + (booking.practitioner_name || 'practitioner') + ', ' + formatDate(slot.date_time_from.slice(0, 10)) + ' at ' + slot.date_time_from.slice(11, 16))); var nameI = el('input', { type: 'text', placeholder: 'First name', 'class': 'rm-booking-input' }); var surI = el('input', { type: 'text', placeholder: 'Surname', 'class': 'rm-booking-input' }); var emailI = el('input', { type: 'email', placeholder: 'Email', 'class': 'rm-booking-input' }); var phoneI = el('input', { type: 'tel', placeholder: 'Mobile', 'class': 'rm-booking-input' }); if (state.user) { if (state.user.email) emailI.value = state.user.email; if (state.user.name) nameI.value = state.user.name.split(' ')[0]; } p.appendChild(nameI); p.appendChild(surI); p.appendChild(emailI); p.appendChild(phoneI); var confirm = el('button', { type: 'button', 'class': 'rm-booking-confirm' }, 'Confirm — $' + parseFloat(booking.price || 0).toFixed(0)); confirm.addEventListener('click', function () { if (!emailI.value || !nameI.value) { showToast('Name and email are required.', 4000); return; } confirm.disabled = true; api('booking', { action: 'book', chatbot_token: state.token, first_name: nameI.value, surname: surI.value, email: emailI.value, phone: phoneI.value, sub_service_id: booking.sub_service_id, practitioner_user_id: booking.practitioner_user_id, location_id: booking.location_id, slot_from: booking.slot_from, slot_to: booking.slot_to, availability_days_id: booking.availability_days_id, room_id: booking.room_id }) .done(function (r) { p.innerHTML = ''; p.appendChild(el('div', { 'class': 'rm-booking-success' }, '✅ Booking #' + r.order_id + ' confirmed. ' + (r.message || "You'll get a confirmation email shortly."))); booking = null; }) .fail(function (e) { confirm.disabled = false; showToast(e.message || 'Booking failed — please try again', 5000); }); }); p.appendChild(confirm); } /* ── blood-test upload (clinic sites) ── */ function renderBloodUploadPicker(endpoint, accept, maxBytes) { var card = el('div', { 'class': 'rm-upload-card', style: { marginTop: '8px', padding: '10px 12px', borderRadius: '8px', background: '#F4F1EA', border: '1px solid #E0DAD0' } }); var heading = el('div', { style: { fontWeight: '600', marginBottom: '6px' } }, '📎 Choose blood-test file(s) — up to ' + Math.round(maxBytes / 1024 / 1024) + ' MB each'); var input = el('input', { type: 'file', multiple: 'multiple', accept: accept, style: { display: 'block', marginBottom: '6px' } }); var status = el('div', { 'class': 'rm-upload-status', style: { fontSize: '13px', color: '#555', minHeight: '18px' } }); var submit = el('button', { 'class': 'rm-submit', type: 'button', style: { marginTop: '4px' } }, 'Send to my practitioner'); submit.addEventListener('click', function () { var files = input.files; if (!files || !files.length) { status.textContent = 'Pick at least one file first.'; return; } submit.disabled = true; status.textContent = 'Uploading ' + files.length + ' file(s)…'; var fd = new FormData(); fd.append('token', state.token); fd.append('section_id', SECTION_ID); for (var i = 0; i < files.length; i++) fd.append('files[]', files[i]); $.ajax({ url: endpoint, type: 'POST', data: fd, processData: false, contentType: false, dataType: 'json' }) .done(function (data) { if (data && data.ok) { status.textContent = data.message || 'Files received.'; input.disabled = true; renderMessage(data.message || 'Files attached to your intake form.', 'bot'); } else { status.textContent = ((data && data.error) || 'Upload failed') + (data && data.needs_intake_bridge ? ' — please start the intake first.' : ''); submit.disabled = false; } }) .fail(function () { status.textContent = 'Upload failed — please try again or call us.'; submit.disabled = false; }); }); card.appendChild(heading); card.appendChild(input); card.appendChild(submit); card.appendChild(status); msgList.appendChild(card); scrollDown(); } /* ── lead card ── */ function renderLeadCard() { if (root.querySelector('.rm-lead-card')) return; var card = el('div', { 'class': 'rm-lead-card' }); card.appendChild(el('h4', {}, txt('lead_title', 'Want us to follow up?'))); var nameI = el('input', { type: 'text', placeholder: 'Your name (optional)' }); var emailI = el('input', { type: 'email', placeholder: 'Email address' }); var phoneI = el('input', { type: 'tel', placeholder: 'Mobile (optional)' }); if (state.user) { if (state.user.name) nameI.value = state.user.name; if (state.user.email) emailI.value = state.user.email; } var actions = el('div', { 'class': 'rm-actions' }); var submit = el('button', { 'class': 'rm-submit', type: 'button' }, 'Send'); var skip = el('button', { 'class': 'rm-skip', type: 'button' }, 'Not now'); submit.addEventListener('click', function () { submit.disabled = true; api('lead', { token: state.token, name: nameI.value, email: emailI.value, phone: phoneI.value, topic: state.helpTopic || '' }) .done(function (r) { removeNode(card); renderMessage(r.message || 'Thanks! We have your details — someone will be in touch shortly.', 'bot'); }) .fail(function (e) { submit.disabled = false; showToast(e.message || 'Could not save your details. Please try again.', 5000); }); }); skip.addEventListener('click', function () { removeNode(card); }); actions.appendChild(submit); actions.appendChild(skip); card.appendChild(nameI); card.appendChild(emailI); card.appendChild(phoneI); card.appendChild(actions); msgList.appendChild(card); scrollDown(); } /* ── conversation ── */ function startSession() { api('start', { token: state.token, page_url: location.href, referrer: document.referrer, language: LANG }) .done(function (data) { state.token = data.token; state.siteId = data.site_id; store(STORAGE_KEY, state.token); applyStart(data); renderMessage(data.message, 'bot'); renderQuickReplies(data.quick_replies); if (state.helpTopic === 'submit_trouble' && state.ui.submit_trouble_text) { renderQuickReplies([]); renderMessage(state.ui.submit_trouble_text, 'bot'); } if (!state.termsAccepted) renderTermsConsent(); }) .fail(function () { renderMessage('Sorry — I could not start the chat. Please refresh and try again.', 'bot'); }); } function sendUserText() { var text = composerInput.value.trim(); if (!text || state.sending) return; if (!state.termsAccepted) { showToast('Please accept the Terms & Conditions to start chatting.', 4000); lockComposer(); return; } composerInput.value = ''; autoSize(); renderMessage(text, 'user'); renderQuickReplies([]); sendToServer({ message: text }); } function sendQuickReply(label) { if (state.sending) return; if (!state.termsAccepted) { showToast('Please accept the Terms & Conditions to start chatting.', 4000); lockComposer(); return; } renderMessage(label, 'user'); renderQuickReplies([]); sendToServer({ message: label, selected_reply: label }); } function sendToServer(payload) { state.sending = true; composerSend.disabled = true; var typing = renderTyping(); api('chat', $.extend({ token: state.token }, payload)) .done(function (data) { removeNode(typing); renderMessage(data.message, 'bot'); if (data.link_url) renderLink(data.link_url, (data.link_label || 'Open') + ' →', true); renderQuickReplies(data.quick_replies); if (data.lead_nudge) renderLeadCard(); if (data.booking_url) renderLink(data.booking_url, '👉 Open booking calendar', false); if (data.intake_progress && typeof data.intake_progress.pct === 'number') renderIntakeProgress(data.intake_progress); if (data.booking_flow && data.booking_flow.mode === 'inline') renderBookingPanel(data.booking_flow); if (state.ui.product_cards && !(data.booking_flow && data.booking_flow.mode === 'inline') && !data.upload_blood_test && !data.intake_url) { api('products', { message: payload.message || '', reply: data.message || '' }).done(function (pm) { if (pm && pm.products) renderProductCards(pm.products); }); } if (data.intake_url) renderLink(data.intake_url, '📋 Open my intake form (pre-filled)', false); if (data.download_url) renderLink(data.download_url, '⬇️ Download blood-test request form', true); if (data.upload_blood_test) renderBloodUploadPicker(data.upload_endpoint || ENDPOINTS.upload, data.upload_accept || '.pdf,.jpg,.jpeg,.png,.docx', data.upload_max_bytes || 10485760); if (data.lang_tag) state.langTag = data.lang_tag; if (data.voice_confirmed && data.voice_gender) enableVoiceMode(data.voice_gender, data.lang_tag || state.langTag); speakIfVoice(data.message, data.quick_replies); }) .fail(function (e) { removeNode(typing); renderMessage(e && e.message && /maximum length/.test(e.message) ? e.message : txt('error_text', 'Sorry — that did not go through. Please try again, or call us.'), 'bot'); }) .always(function () { state.sending = false; composerSend.disabled = false; if (state.termsAccepted) composerInput.focus(); }); } function openPanel() { state.open = true; bubble.hidden = true; panel.hidden = false; bubble.classList.add('rm-bubble-seen'); if (msgList.children.length === 0) startSession(); setTimeout(function () { composerInput.focus(); }, 50); } function closePanel() { state.open = false; panel.hidden = true; bubble.hidden = false; } /* ── quick Terms & Conditions consent ── */ function lockComposer() { composerInput.disabled = true; composerInput.placeholder = 'Please accept the Terms & Conditions to start chatting'; composerSend.disabled = true; composerMic.disabled = true; } function unlockComposer() { composerInput.disabled = false; composerInput.placeholder = 'Type your message…'; composerSend.disabled = false; composerMic.disabled = false; setTimeout(function () { composerInput.focus(); }, 50); } function renderTermsConsent() { if (msgList.querySelector('.rm-terms-card')) return; var card = el('div', { 'class': 'rm-terms-card', role: 'group', 'aria-label': 'Terms & Conditions consent' }); var label = el('label', { 'class': 'rm-terms-label' }); var cb = el('input', { type: 'checkbox', 'class': 'rm-terms-cb', 'aria-label': 'I accept the Terms & Conditions and Privacy Policy' }); var link = el('a', { href: txt('terms_url', '/terms-of-use'), target: '_blank', rel: 'noopener', 'class': 'rm-terms-link' }, 'Terms & Conditions and Privacy Policy'); label.appendChild(cb); label.appendChild(document.createTextNode(' I have read and accept the ')); label.appendChild(link); var go = el('button', { 'class': 'rm-terms-continue', type: 'button', disabled: 'disabled' }, 'Continue'); cb.addEventListener('change', function () { go.disabled = !cb.checked; }); go.addEventListener('click', function () { if (!cb.checked) return; go.disabled = true; store(TERMS_KEY, '1'); state.termsAccepted = true; api('terms', { token: state.token, version: state.termsVersion }); card.innerHTML = ''; card.classList.add('rm-terms-card-done'); card.appendChild(document.createTextNode('✓ Terms accepted — you can now chat below.')); unlockComposer(); }); card.appendChild(label); card.appendChild(go); msgList.appendChild(card); scrollDown(); lockComposer(); } /* ── FAQ drawer + feedback survey of the intake form app (clinic sites) and the transcript by email ── */ var faqLoading = false; function withFaqWidget(done) { if (window.RMFaq) { done(); return; } var base = String(txt('faq_base', '/questionnaire')).replace(/\/$/, ''); if (!faqLoading) { faqLoading = true; window.RM_API_BASE = base + '/api'; window.RM_FAQ_NO_AUTOMOUNT = true; var link = document.createElement('link'); link.rel = 'stylesheet'; link.href = base + '/assets/css/faq-survey.css'; document.head.appendChild(link); var s = document.createElement('script'); s.src = base + '/assets/js/faq-survey.js'; s.defer = true; document.head.appendChild(s); } var tries = 0; var t = setInterval(function () { if (window.RMFaq || ++tries > 100) { clearInterval(t); if (window.RMFaq) done(); } }, 50); } function openFaqDrawer() { withFaqWidget(function () { if (window.RMFaq && typeof window.RMFaq.openDrawer === 'function') window.RMFaq.openDrawer(); }); } function renderFeedbackSurveyInline() { withFaqWidget(function () { if (!window.RMFaq || typeof window.RMFaq.renderSurveyInto !== 'function') return; removeNode(msgList.querySelector('.rm-chatbot-feedback-host')); var host = el('div', { 'class': 'rm-chatbot-feedback-host' }); msgList.appendChild(host); scrollDown(); window.RMFaq.renderSurveyInto(host, { source: 'chatbot', chatbotSessionToken: state.token || null, siteId: state.siteId || null, language: state.langTag ? state.langTag.split('-')[0] : (LANG || 'en'), onDone: function (result) { if (result && result.skipped) return; renderMessage('Thanks for your feedback — that really helps us improve.', 'bot'); } }); }); } function promptEmailTranscript() { if (!state.token) { showToast('No conversation yet — start chatting first.', 4000); return; } removeNode(msgList.querySelector('.rm-chatbot-email-host')); var host = el('div', { 'class': 'rm-chatbot-email-host', style: { marginTop: '8px', padding: '10px 12px', borderRadius: '8px', background: '#F4F1EA', border: '1px solid #E0DAD0' } }); var heading = el('div', { style: { fontWeight: '600', marginBottom: '6px', color: '#3C2470' } }, 'Email me a copy of this chat'); var input = el('input', { type: 'email', placeholder: 'you@example.com', style: { width: '100%', padding: '8px 10px', borderRadius: '6px', border: '1px solid #C9C2B5', boxSizing: 'border-box', marginBottom: '6px', font: 'inherit' } }); if (state.brand && state.brand.lead_email) input.value = state.brand.lead_email; var send = el('button', { 'class': 'rm-submit', type: 'button', style: { marginRight: '6px' } }, 'Send'); var cancel = el('button', { 'class': 'rm-skip', type: 'button' }, 'Cancel'); var status = el('div', { style: { fontSize: '13px', color: '#555', marginTop: '6px' } }); send.addEventListener('click', function () { var v = (input.value || '').trim(); if (!v) { status.textContent = 'Please enter an email.'; return; } send.disabled = true; status.textContent = 'Sending…'; api('transcript', { token: state.token, email: v, topic: state.helpTopic || '' }) .done(function (data) { removeNode(host); renderMessage((data && data.message) || ('We have sent a copy of this conversation to ' + v + '.'), 'bot'); }) .fail(function (e) { send.disabled = false; status.textContent = (e && e.message) || 'Could not send the transcript right now.'; }); }); cancel.addEventListener('click', function () { removeNode(host); }); host.appendChild(heading); host.appendChild(input); host.appendChild(send); host.appendChild(cancel); host.appendChild(status); msgList.appendChild(host); scrollDown(); setTimeout(function () { input.focus(); }, 50); } /* ── boot: the chat always starts closed (bubble with the unread badge), except on ?open_chat=1 ── */ buildDom(); if (URL_OPEN_CHAT) setTimeout(openPanel, 200); } function scan() { injectCss(); var divs = document.querySelectorAll('[data-cbm]'); for (var i = 0; i < divs.length; i++) boot(divs[i]); } function ready() { if (typeof window.jQuery === 'undefined') { setTimeout(ready, 200); return; } /* the widget uses jQuery, as the whole CMS */ window.jQuery(scan); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ready); else ready(); window.rmChatbotScan = scan; /* for a div added later by another script */ })();