1(function (global) { 2 "use strict"; 3 4 var S = global.EvomenConciergeState; 5 var R = global.EvomenConciergeRender; 6 var U = global.EvomenConciergeUI; 7 8 var MAX_TURNS = 10; 9 10 // ---- Turnstile helpers ------------------------------------------------ 11 // 12 // The widget is rendered invisibly (execution: "execute"), and we call 13 // turnstile.execute() before the very first request of a session. We cache 14 // the resolved token per page load because the worker only checks on turn 15 // 1. If the plugin has no site key configured, getTurnstileToken() resolves 16 // to an empty string and the request goes through unchallenged (the worker 17 // also short-circuits when its TURNSTILE_SECRET is unset). 18 19 var turnstileWidgetId = null; 20 var turnstilePromise = null; 21 22 function ensureTurnstileWidget() { 23 var opts = global.evomenConcierge || {}; 24 if (!opts.turnstileSiteKey) return null; 25 if (!global.turnstile || typeof global.turnstile.render !== "function") return null; 26 if (turnstileWidgetId !== null) return turnstileWidgetId; 27 28 var container = document.createElement("div"); 29 container.id = "evomen-turnstile-container"; 30 container.style.position = "fixed"; 31 container.style.left = "-9999px"; 32 container.style.bottom = "-9999px"; 33 container.style.width = "0"; 34 container.style.height = "0"; 35 container.setAttribute("aria-hidden", "true"); 36 document.body.appendChild(container); 37 38 try { 39 turnstileWidgetId = global.turnstile.render(container, { 40 sitekey: opts.turnstileSiteKey, 41 size: "invisible", 42 execution: "execute", 43 "refresh-expired": "auto", 44 }); 45 } catch (err) { 46 console.warn("[concierge] turnstile render failed", err); 47 turnstileWidgetId = null; 48 } 49 return turnstileWidgetId; 50 } 51 52 function getTurnstileToken() { 53 var opts = global.evomenConcierge || {}; 54 if (!opts.turnstileSiteKey) return Promise.resolve(""); 55 if (!global.turnstile) { 56 // Script still loading. Wait briefly, then give up gracefully: the 57 // worker will reject, the user sees the offline fallback, and the 58 // next reload will succeed. 59 return new Promise(function (resolve) { 60 var tries = 0; 61 var iv = setInterval(function () { 62 tries++; 63 if (global.turnstile || tries > 20) { 64 clearInterval(iv); 65 resolve(global.turnstile ? getTurnstileToken() : ""); 66 } 67 }, 250); 68 }); 69 } 70 if (turnstilePromise) return turnstilePromise; 71 72 var widgetId = ensureTurnstileWidget(); 73 if (widgetId === null) return Promise.resolve(""); 74 75 turnstilePromise = new Promise(function (resolve) { 76 try { 77 global.turnstile.execute(widgetId, { 78 callback: function (token) { resolve(token || ""); }, 79 "error-callback": function () { resolve(""); }, 80 "timeout-callback": function () { resolve(""); }, 81 }); 82 } catch (err) { 83 console.warn("[concierge] turnstile execute failed", err); 84 resolve(""); 85 } 86 }); 87 return turnstilePromise; 88 } 89 90 // Authoritative page attribution comes from WordPress via 91 // wp_localize_script (evomenConcierge.pageContext). The URL-regex path 92 // below is only a fallback for edge cases where that server context is 93 // missing â e.g. the widget being dropped into a static HTML page 94 // outside WP. Every branch stamps a `source` field so D1 can later show 95 // which path produced each event's attribution. 96 function detectPageContext() { 97 var path = window.location.pathname.replace(/\/$/, "") || "/"; 98 var title = document.title || ""; 99 var opts = global.evomenConcierge || {}; 100 var server = opts.pageContext; 101 102 if (server && server.pageType) { 103 return { 104 pageType: server.pageType, 105 pageId: server.pageId || undefined, 106 pageUrl: server.pageUrl || path, 107 pageTitle: server.pageTitle || title, 108 episodeNumber: typeof server.episodeNumber === "number" ? server.episodeNumber : undefined, 109 show: server.show || undefined, 110 mode: server.mode || undefined, 111 archetype: server.archetype || undefined, 112 support: server.support || undefined, 113 source: "wp", 114 }; 115 } 116 117 var lower = path.toLowerCase(); 118 var programs = [ 119 { re: /^\/pillars-of-presence/, id: "pillars" }, 120 { re: /^\/mens-group-experience/, id: "mge" }, 121 { re: /^\/mens-group-mastery/, id: "mgm" }, 122 { re: /^\/heart-of-shadow|^\/heartofshadow|^\/the-heart-of-shadow/, id: "hos" }, 123 { re: /^\/retreats?(\/|$)/, id: "retreat" }, 124 { re: /^\/private-coaching|^\/coaching(\/|$)/, id: "private-coaching" }, 125 { re: /^\/full-contact/, id: "full-contact" }, 126 ]; 127 for (var i = 0; i < programs.length; i++) { 128 if (programs[i].re.test(lower)) { 129 return { pageType: "program", pageId: programs[i].id, pageUrl: path, pageTitle: title, source: "url" }; 130 } 131 } 132 133 if (/^\/dear-men/.test(lower)) { 134 return { pageType: "podcast", pageId: "dear-men", pageUrl: path, pageTitle: title, source: "url" }; 135 } 136 if (/^\/(podcast|podcasts|episode|episodes|evolutionary-men-podcast)\//.test(lower)) {
137 var parts = lower.split("/").filter(Boolean); 138 var epSlug = parts.length > 1 ? parts[parts.length - 1] : undefined; 139 return { pageType: "podcast", pageId: epSlug, pageUrl: path, pageTitle: title, source: "url" }; 140 } 141 if (/^\/(blog|post|posts|article|articles)\//.test(lower)) { 142 var blogParts = lower.split("/").filter(Boolean); 143 var blogSlug = blogParts.length > 1 ? blogParts[blogParts.length - 1] : undefined; 144 return { pageType: "blog", pageId: blogSlug, pageUrl: path, pageTitle: title, source: "url" }; 145 } 146 if (/^\/category\//.test(lower) || /^\/tag\//.test(lower)) { 147 return { pageType: "archive", pageUrl: path, pageTitle: title, source: "url" }; 148 } 149 if (path === "/" || path === "" || (opts.isHome === true)) { 150 return { pageType: "home", pageUrl: "/", pageTitle: title, source: "url" }; 151 } 152 return { pageType: "generic", pageUrl: path, pageTitle: title, source: "fallback" }; 153 } 154 155 function ConciergeInstance(root) { 156 this.root = root; 157 this.mode = root.dataset.concierge; // "hero" | "bubble" 158 this.state = S.load(); 159 160 // The hero form is the trigger; the sheet form is for follow-ups. 161 // The bubble only has one form (inside the sheet). 162 this.heroForm = root.querySelector("[data-concierge-form]"); 163 this.heroInput = root.querySelector("[data-concierge-input]"); 164 165 // Sheet elements (present in both hero and bubble) 166 this.sheet = root.querySelector("[data-concierge-sheet]"); 167 this.sheetForm = root.querySelector("[data-concierge-sheet-form]") || this.sheet?.querySelector("[data-concierge-form]"); 168 this.sheetInput = root.querySelector("[data-concierge-sheet-input]") || this.sheet?.querySelector("[data-concierge-input]"); 169 this.convo = root.querySelector("[data-concierge-conversation]"); 170 171 // For the bubble, heroForm and sheetForm may be the same element 172 this.activeInput = this.heroInput; 173 174 this._scrollBtn = null; 175 176 this.init(); 177 } 178 179 ConciergeInstance.prototype.init = function () { 180 // Support-fit mode: the bubble sheet template has no chip container 181 // (chips historically render only in the homepage hero), so inject one 182 // for this mode's auto-opened sheet. Scoped to support-fit to leave the 183 // rest of the site's sheet UX unchanged; removed after the first 184 // message (see sendMessage). 185 var opts = global.evomenConcierge || {}; 186 if ( 187 opts.pageContext && opts.pageContext.mode === "support-fit" && 188 this.mode === "bubble" && this.sheet && 189 this.state.messages.length === 0 && 190 !this.sheet.querySelector("[data-concierge-chips]") 191 ) { 192 var chipList = document.createElement("ul"); 193 chipList.className = "concierge-hero__chips concierge-bubble__chips"; 194 chipList.setAttribute("data-concierge-chips", ""); 195 this.sheet.insertBefore(chipList, this.sheet.querySelector("[data-concierge-form]")); 196 } 197 198 U.mountChips(this.root); 199 // The rotating mobile placeholder cues are generic pain statements; in 200 // support-fit mode the chips carry the intent instead, so keep the 201 // static placeholder. 202 var supportFit = opts.pageContext && opts.pageContext.mode === "support-fit"; 203 if (this.mode === "hero" && !supportFit) U.startRotatingPlaceholder(this.heroInput); 204 205 // Hero form submit: opens the sheet and sends 206 if (this.heroForm) { 207 this.heroForm.addEventListener("submit", function (e) { 208 e.preventDefault(); 209 var text = this.heroInput.value.trim(); 210 if (!text) return; 211 this.heroInput.value = ""; 212 this.openSheet(); 213 this.sendMessage(text); 214 }.bind(this)); 215 } 216 217 // Sheet form submit: sends within the sheet 218 if (this.sheetForm && this.sheetForm !== this.heroForm) { 219 this.sheetForm.addEventListener("submit", function (e) { 220 e.preventDefault(); 221 var text = this.sheetInput.value.trim(); 222 if (!text) return; 223 this.sendMessage(text); 224 }.bind(this)); 225 } 226 227 // Delegated click handler â shared between root and sheet. The sheet gets 228 // reparented to <body> when opened (to escape Divi stacking contexts), so 229 // clicks inside it won't bubble to this.root. Binding on both covers all cases. 230 var clickHandler = function (e) { 231 var chipBtn = e.target.closest("[data-chip-id]"); 232 if (chipBtn) { 233 if (this.heroInput) this.heroInput.value = ""; 234 this.openSheet(); 235 this.sendMessage(chipBtn.textContent.trim(), chipBtn.dataset.chipId); 236 return; 237 } 238 var freshBtn = e.target.closest("[data-concierge-fresh]"); 239 if (freshBtn) { this.startFresh(); return; } 240 var privacyClose = e.target.closest("[data-concierge-privacy-close]"); 241 if (privacyClose) { this.togglePrivacy(false); return; } 242 var privacyBtn = e.target.closest("[data-concierge-privacy]"); 243 if (privacyBtn) { this.togglePrivacy(true); return; } 244 var closeBtn = e.target.closest("[data-concierge-close]"); 245 if (closeBtn) { this.closeSheet(); return; } 246 var toggleBtn = e.target.closest("[data-concierge-toggle]"); 247 if (toggleBtn) { 248 if (this.sheet && !this.sheet.hidden) this.closeSheet(); 249 else this.openSheet(); 250 return; 251 } 252 var sugBtn = e.target.closest("[data-suggestion]"); 253 if (sugBtn) { 254 var sugStrip = sugBtn.closest(".concierge-suggestions"); 255 if (sugStrip) sugStrip.remove(); 256 this.sendMessage(sugBtn.textContent.trim()); 257 return; 258 } 259 var fbBtn = e.target.closest("[data-feedback]"); 260 if (fbBtn) { 261 var fbRow = fbBtn.closest(".concierge-feedback"); 262 var btns = fbRow.querySelectorAll(".concierge-feedback__btn"); 263 Array.prototype.forEach.call(btns, function (b) { b.disabled = true; }); 264 fbBtn.classList.add("concierge-feedback__btn--active"); 265 this.trackFeedback(fbBtn.getAttribute("data-feedback")); 266 return; 267 } 268 var ctaBtn = e.target.closest(".concierge-cta-btn"); 269 if (ctaBtn) { this.trackCtaButton(ctaBtn); return; } 270 var card = e.target.closest(".concierge-card"); 271 if (card) { this.trackCta(card); } 272 }.bind(this); 273 this.root.addEventListener("click", clickHandler); 274 if (this.sheet) this.sheet.addEventListener("click", clickHandler); 275 276 // Cross-tab sync 277 S.onChange(function (next) { 278 this.state = next; 279 this.rerenderHistory(); 280 }.bind(this)); 281 282 // Restore history if any; warm returning visitors 283 if (this.state.messages.length > 0) { 284 this.rerenderHistory(); 285 if (this.mode === "hero" && this.heroInput) { 286 this.heroInput.placeholder = "Welcome back. What else is on your mind?"; 287 } 288 } 289 290 // Scroll-to-bottom indicator 291 if (this.convo && this.sheet) { 292 this._scrollBtn = document.createElement("button"); 293 this._scrollBtn.type = "button"; 294 this._scrollBtn.className = "concierge-scroll-bottom"; 295 this._scrollBtn.setAttribute("aria-label", "Scroll to bottom"); 296 this._scrollBtn.innerHTML = "▾"; // â¾ small down triangle 297 this.sheet.appendChild(this._scrollBtn); 298 this._scrollBtn.addEventListener("click", function () { 299 this.scrollToBottom(); 300 }.bind(this)); 301 this.convo.addEventListener("scroll", function () { 302 this.updateScrollBtn(); 303 }.bind(this)); 304 } 305 306 // Bind sheet gestures for both hero and bubble 307 if (this.sheet) { 308 U.bindSheetGestures(this.sheet, this.closeSheet.bind(this)); 309 } 310 }; 311 312 ConciergeInstance.prototype.rerenderHistory = function () { 313 if (!this.convo) return; 314 this.convo.innerHTML = ""; 315 for (var i = 0; i < this.state.messages.length; i++) { 316 var msg = this.state.messages[i]; 317 this.appendMessageDom(msg.role, msg.content); 318 } 319 }; 320 321 ConciergeInstance.prototype.appendMessageDom = function (role, content) { 322 var el = document.createElement("div"); 323 el.className = "concierge-msg concierge-msg--" + role; 324 el.innerHTML = R.renderMarkdown(content); 325 this.convo.appendChild(el); 326 return el; 327 }; 328 329 ConciergeInstance.prototype.openSheet = function () { 330 if (this.sheet) { 331 // Reparent to <body> so the sheet escapes Divi section stacking 332 // contexts. position:fixed + z-index only works when no ancestor 333 // creates a competing context (Divi sections use transform/z-index). 334 if (this.sheet.parentNode !== document.body) { 335 document.body.appendChild(this.sheet); 336 } 337 this.sheet.hidden = false;
338 this.root.classList.add("concierge--engaged"); 339 if (this.mode === "bubble") this.root.classList.add("concierge-bubble--open"); 340 // Focus the sheet input for follow-ups 341 if (this.sheetInput) { 342 setTimeout(function () { this.sheetInput.focus(); }.bind(this), 100); 343 } 344 } 345 }; 346 347 ConciergeInstance.prototype.closeSheet = function () { 348 if (this.sheet) { 349 this.sheet.hidden = true; 350 if (this.mode === "bubble") this.root.classList.remove("concierge-bubble--open"); 351 } 352 }; 353 354 ConciergeInstance.prototype.sendMessage = function (text, chipId) { 355 if (this.state.turn >= MAX_TURNS) { 356 this.appendMessageDom( 357 "assistant", 358 "I want to make sure you don't lose this thread. The best next step is probably a conversation with a real human." 359 ); 360 return; 361 } 362 363 // Clear the active input 364 if (this.sheetInput) this.sheetInput.value = ""; 365 // Remove any existing suggestion chips 366 var oldSuggestions = this.convo.querySelectorAll(".concierge-suggestions"); 367 Array.prototype.forEach.call(oldSuggestions, function (el) { el.remove(); }); 368 // Support-fit sheet chips are one-shot: remove once any message is sent. 369 var sheetChips = this.sheet ? this.sheet.querySelector("[data-concierge-chips]") : null; 370 if (sheetChips) sheetChips.remove(); 371 S.appendMessage(this.state, "user", text); 372 this.appendMessageDom("user", text); 373 this.scrollToBottom(); 374 375 var typing = U.showTypingIndicator(this.convo); 376 this.scrollToBottom(); 377 378 this.streamResponse(text, chipId, typing).catch(function (err) { 379 if (typing.parentNode) typing.remove(); 380 this.appendMessageDom( 381 "assistant", 382 "The guide is offline right now. You can [book a call with Jason directly](https://evolutionarymen.as.me/exploratory) or [browse programs](https://evolutionary.men/#programs) while I get back on my feet." 383 ); 384 console.error("[concierge]", err); 385 }.bind(this)); 386 }; 387 388 ConciergeInstance.prototype.streamResponse = function (text, chipId, typingEl) { 389 var self = this; 390 var opts = global.evomenConcierge || {}; 391 var url = opts.ajaxUrl + "?action=evomen_concierge&nonce=" + encodeURIComponent(opts.nonce); 392 393 // History excludes the current user message (which was appended just 394 // before this call). First request of a session has an empty history, 395 // which is also the only request where the worker checks the Turnstile 396 // token. Fetch one up front; no-op when Turnstile is not configured. 397 var history = self.state.messages.slice(0, -1).slice(-6); 398 var tokenPromise = history.length === 0 399 ? getTurnstileToken() 400 : Promise.resolve(""); 401 402 return tokenPromise.then(function (turnstileToken) { 403 var body = { 404 message: text, 405 history: history, 406 sessionHash: self.state.sessionHash, 407 chipClicked: chipId, 408 pageContext: detectPageContext(), 409 }; 410 if (turnstileToken) body.turnstileToken = turnstileToken; 411 412 return fetch(url, { 413 method: "POST", 414 headers: { "Content-Type": "application/json", Accept: "text/event-stream" }, 415 body: JSON.stringify(body), 416 }); 417 }).then(function (res) { 418 if (!res.ok || !res.body) throw new Error("bad response: " + res.status); 419 420 var reader = res.body.getReader(); 421 var decoder = new TextDecoder(); 422 var buf = ""; 423 var assistantEl = null; 424 var assistantText = ""; 425 426 function read() { 427 return reader.read().then(function (result) { 428 if (result.done) return finish(); 429 buf += decoder.decode(result.value, { stream: true }); 430 var parts = buf.split("\n\n"); 431 buf = parts.pop() || ""; 432 433 for (var i = 0; i < parts.length; i++) { 434 var lines = parts[i].split("\n"); 435 var eventLine = null, dataLine = null; 436 for (var j = 0; j < lines.length; j++) { 437 if (lines[j].indexOf("event:") === 0) eventLine = lines[j]; 438 if (lines[j].indexOf("data:") === 0) dataLine = lines[j]; 439 } 440 if (!eventLine || !dataLine) continue; 441 442 var event = eventLine.slice(6).trim(); 443 var data; 444 try { data = JSON.parse(dataLine.slice(5).trim()); } catch (e) { continue; } 445 446 if (event === "token") { 447 if (!assistantEl) { 448 if (typingEl.parentNode) typingEl.remove(); 449 assistantEl = self.appendMessageDom("assistant", ""); 450 self.maybeScrollToBottom(); 451 } 452 assistantText += data.text || ""; 453 assistantEl.innerHTML = R.renderMarkdown(assistantText); 454 self.maybeScrollToBottom(); 455 } else if (event === "replace") { 456 if (assistantEl) { 457 assistantText = data.text || ""; 458 assistantEl.innerHTML = R.renderMarkdown(assistantText); 459 } 460 } else if (event === "cards") { 461 if (Array.isArray(data.cards) && data.cards.length > 0) { 462 var cardsEl = document.createElement("div"); 463 cardsEl.className = "concierge-cards"; 464 cardsEl.innerHTML = data.cards.map(R.renderCard).join(""); 465 self.convo.appendChild(cardsEl); 466 self.maybeScrollToBottom(); 467 } 468 } else if (event === "cta") { 469 if (Array.isArray(data.ctas) && data.ctas.length > 0) { 470 var ctaEl = document.createElement("div"); 471 ctaEl.className = "concierge-ctas"; 472 ctaEl.innerHTML = data.ctas.map(function(c) { 473 return '<a class="concierge-cta-btn" href="' + R.escape(c.url) + '" target="_blank" rel="noopener noreferrer" data-cta-id="' + R.escape(c.id) + '">' + R.escape(c.label) + ' →</a>'; 474 }).join(""); 475 self.convo.appendChild(ctaEl); 476 self.maybeScrollToBottom(); 477 } 478 } else if (event === "email_capture") { 479 var emailEl = document.createElement("div"); 480 emailEl.className = "concierge-email-capture"; 481 emailEl.innerHTML = '<p class="concierge-email-capture__prompt">' + R.escape(data.prompt || "Get Jason\'s best insights in your inbox:") + '</p>' + 482 '<form class="concierge-email-capture__form" data-concierge-email-form>' + 483 '<input type="email" class="concierge-email-capture__input" placeholder="[email protected]" required>' + 484 '<button type="submit">Subscribe</button>' + 485 '</form>'; 486 self.convo.appendChild(emailEl); 487 self.maybeScrollToBottom(); 488 // Handle email submit 489 var emailForm = emailEl.querySelector("[data-concierge-email-form]"); 490 emailForm.addEventListener("submit", function(ev) { 491 ev.preventDefault(); 492 var emailInput = emailEl.querySelector(".concierge-email-capture__input"); 493 var email = emailInput.value.trim(); 494 if (!email) return; 495 var opts = global.evomenConcierge || {}; 496 fetch(opts.ajaxUrl + "?action=evomen_concierge_email&nonce=" + encodeURIComponent(opts.nonce), { 497 method: "POST", 498 headers: {"Content-Type": "application/json"}, 499 body: JSON.stringify({email: email, sessionHash: self.state.sessionHash}), 500 }).then(function(r) { 501 if (r.ok) { 502 // Replace form with success + optional relationship follow-up. 503 // Same rel-* tag scheme as the quiz funnel so concierge and 504 // quiz subscribers share one segmentation. 505 emailEl.innerHTML = 506 '<p class="concierge-email-capture__success">You\'re in. Check your inbox.</p>' + 507 '<div class="concierge-followup" data-concierge-followup>' + 508 '<p class="concierge-followup__prompt">Optional: where are you in relationships right now?</p>' + 509 '<div class="concierge-followup__options">' + 510 '<button type="button" data-rel="single">Single</button>' + 511 '<button type="button" data-rel="dating">Dating</button>' + 512 '<button type="button" data-rel="committed">Committed</button>' + 513 '<button type="button" data-rel="married">Married</button>' + 514 '<button type="button" data-rel="divorced">Divorced</button>' + 515 '</div>' + 516 '<button type="button" class="concierge-followup__skip" data-rel-skip>Skip</button>' + 517 '</div>'; 518 var followup = emailEl.querySelector("[data-concierge-followup]"); 519 var skipBtn = followup.querySelector("[data-rel-skip]"); 520 skipBtn.addEventListener("click", function() { 521 followup.parentNode.removeChild(followup); 522 }); 523 var relBtns = followup.querySelectorAll("button[data-rel]"); 524 Array.prototype.forEach.call(relBtns, function(btn
524) { 525 btn.addEventListener("click", function() { 526 var rel = btn.getAttribute("data-rel"); 527 Array.prototype.forEach.call(relBtns, function(b) { b.disabled = true; }); 528 skipBtn.disabled = true; 529 btn.textContent = "Saving..."; 530 fetch(opts.ajaxUrl + "?action=evomen_concierge_email&nonce=" + encodeURIComponent(opts.nonce), { 531 method: "POST", 532 headers: {"Content-Type": "application/json"}, 533 body: JSON.stringify({email: email, relationship: rel, follow_up: true, sessionHash: self.state.sessionHash}), 534 }).then(function(rr) { 535 if (rr.ok) { 536 followup.innerHTML = '<p class="concierge-followup__thanks">Got it. Thanks.</p>'; 537 } else { 538 btn.textContent = rel.charAt(0).toUpperCase() + rel.slice(1); 539 Array.prototype.forEach.call(relBtns, function(b) { b.disabled = false; }); 540 skipBtn.disabled = false; 541 } 542 }).catch(function() { 543 btn.textContent = rel.charAt(0).toUpperCase() + rel.slice(1); 544 Array.prototype.forEach.call(relBtns, function(b) { b.disabled = false; }); 545 skipBtn.disabled = false; 546 }); 547 }); 548 }); 549 self.maybeScrollToBottom(); 550 } else { 551 emailEl.querySelector("button").textContent = "Try again"; 552 } 553 }).catch(function() { 554 emailEl.querySelector("button").textContent = "Try again"; 555 }); 556 emailEl.querySelector("button").textContent = "Sending..."; 557 emailEl.querySelector("button").disabled = true; 558 }); 559 } else if (event === "suggestions") { 560 if (Array.isArray(data.suggestions) && data.suggestions.length > 0) { 561 var sugEl = document.createElement("div"); 562 sugEl.className = "concierge-suggestions"; 563 sugEl.innerHTML = data.suggestions.map(function (s) { 564 return '<button type="button" class="concierge-suggestion" data-suggestion>' + R.escape(s) + '</button>'; 565 }).join(""); 566 self.convo.appendChild(sugEl); 567 self.maybeScrollToBottom(); 568 } 569 } else if (event === "done") { 570 S.appendMessage(self.state, "assistant", assistantText); 571 // Add feedback thumbs to the last assistant message 572 if (assistantEl) { 573 var fb = document.createElement("div"); 574 fb.className = "concierge-feedback"; 575 fb.innerHTML = 576 '<button type="button" class="concierge-feedback__btn" data-feedback="positive" aria-label="Helpful">' + 577 '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z"/></svg>
577' + 578 '</button>' + 579 '<button type="button" class="concierge-feedback__btn" data-feedback="negative" aria-label="Not helpful">' + 580 '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22h0a3.13 3.13 0 0 1-3-3.88Z"/></svg>' + 581 '</button>'; 582 assistantEl.appendChild(fb); 583 } 584 } else if (event === "error") { 585 throw new Error(data.error || "upstream error"); 586 } 587 } 588 return read(); 589 }); 590 } 591 592 function finish() { 593 if (typingEl.parentNode) typingEl.remove(); 594 } 595 596 return read(); 597 }); 598 }; 599 600 ConciergeInstance.prototype.trackCta = function (cardEl) { 601 var opts = global.evomenConcierge || {}; 602 var url = cardEl.getAttribute("href") || ""; 603 var typeMatch = cardEl.className.match(/concierge-card--(\w+)/); 604 var ctaId = typeMatch ? typeMatch[1] : "unknown"; 605 var body = JSON.stringify({ 606 event: "cta_click", 607 ctaId: ctaId, 608 ctaUrl: url, 609 sessionHash: this.state.sessionHash, 610 turn: this.state.turn, 611 pageContext: detectPageContext(), 612 }); 613 if (navigator.sendBeacon) { 614 var blob = new Blob([body], { type: "application/json" }); 615 navigator.sendBeacon(opts.ajaxUrl + "?action=evomen_concierge_track", blob); 616 } 617 }; 618 619 ConciergeInstance.prototype.trackCtaButton = function (btnEl) { 620 var opts = global.evomenConcierge || {}; 621 var body = JSON.stringify({ 622 event: "cta_click", 623 ctaId: btnEl.getAttribute("data-cta-id") || "unknown", 624 ctaUrl: btnEl.getAttribute("href") || "", 625 sessionHash: this.state.sessionHash, 626 turn: this.state.turn, 627 pageContext: detectPageContext(), 628 }); 629 if (navigator.sendBeacon) { 630 var blob = new Blob([body], { type: "application/json" }); 631 navigator.sendBeacon(opts.ajaxUrl + "?action=evomen_concierge_track", blob); 632 } 633 }; 634 635 ConciergeInstance.prototype.trackFeedback = function (type) { 636 var opts = global.evomenConcierge || {}; 637 var body = JSON.stringify({ 638 event: "feedback_" + type, 639 sessionHash: this.state.sessionHash, 640 turn: this.state.turn, 641 pageContext: detectPageContext(), 642 }); 643 if (navigator.sendBeacon) { 644 var blob = new Blob([body], { type: "application/json" }); 645 navigator.sendBeacon(opts.ajaxUrl + "?action=evomen_concierge_track", blob); 646 } 647 }; 648 649 ConciergeInstance.prototype.isNearBottom = function () { 650 if (!this.convo) return true; 651 return this.convo.scrollHeight - this.convo.scrollTop - this.convo.clientHeight < 60; 652 }; 653 654 ConciergeInstance.prototype.scrollToBottom = function () { 655 if (!this.convo) return; 656 this.convo.scrollTop = this.convo.scrollHeight; 657 this.updateScrollBtn(); 658 }; 659 660 ConciergeInstance.prototype.maybeScrollToBottom = function () { 661 if (this.isNearBottom()) { 662 this.scrollToBottom(); 663 } else { 664 this.updateScrollBtn(); 665 } 666 }; 667 668 ConciergeInstance.prototype.updateScrollBtn = function () { 669 if (!this._scrollBtn) return; 670 if (this.isNearBottom()) { 671 this._scrollBtn.classList.remove("concierge-scroll-bottom--visible"); 672 } else { 673 this._scrollBtn.classList.add("concierge-scroll-bottom--visible"); 674 } 675 }; 676 677 ConciergeInstance.prototype.togglePrivacy = function (open) { 678 var modal = this.root.querySelector("[data-concierge-privacy-modal]"); 679 if (!modal) modal = document.querySelector("[data-concierge-privacy-modal]"); 680 if (!modal) return; 681 modal.hidden = !open; 682 }; 683 684 ConciergeInstance.prototype.startFresh = function () { 685 S.clear(); 686 this.state = S.load(); // fresh state with new sessionHash 687 if (this.convo) this.convo.innerHTML = ""; 688 this.closeSheet(); 689 this.root.classList.remove("concierge--engaged"); 690 // Reset hero input placeholder 691 if (this.heroInput) this.heroInput.placeholder = "What's on your mind?"; 692 }; 693
694 function init() { 695 var roots = document.querySelectorAll("[data-concierge]"); 696 var instances = []; 697 for (var i = 0; i < roots.length; i++) { 698 instances.push(new ConciergeInstance(roots[i])); 699 } 700 701 // Support-fit mode: the visitor clicked "let's figure out what support 702 // fits you" in an email, so opening on load is explicit intent, not a 703 // proactive nudge (the never-auto-open-on-mobile rule doesn't apply). 704 // Fires once per page load, no persistence, normal close behavior. 705 var opts = global.evomenConcierge || {}; 706 if (opts.pageContext && opts.pageContext.mode === "support-fit") { 707 // On the dedicated /support-fit page the hero embed IS the experience, 708 // so auto-opening the corner sheet over it would double the surface. 709 // Auto-open only when no hero instance exists on the page. 710 var hasHero = false; 711 for (var h = 0; h < instances.length; h++) { 712 if (instances[h].mode === "hero") { hasHero = true; break; } 713 } 714 if (!hasHero) { 715 var target = null; 716 for (var k = 0; k < instances.length; k++) { 717 if (instances[k].mode === "bubble") { target = instances[k]; break; } 718 } 719 if (!target && instances.length > 0) target = instances[0]; 720 if (target && target.sheet) target.openSheet(); 721 } 722 } 723 } 724 725 if (document.readyState === "loading") { 726 document.addEventListener("DOMContentLoaded", init); 727 } else { 728 init(); 729 } 730 731 global.EvomenConcierge = { ConciergeInstance: ConciergeInstance, detectPageContext: detectPageContext }; 732})(window);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.