PageSourceSearch

https://evolutionary.men/wp-content/plugins/claude-podcast-recommender/assets/js/concierge.js?ver=6.14.6

js evolutionary.men collected 2026-10-02 03:42:56 UTC 32,278 bytes, 732 lines download raw bytes

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 = "&#x25BE;"; // ▾ 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) + ' &rarr;</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.