PageSourceSearch

https://zippykind.com/assets/front/v3/assets/js/zk-chat.js?ver=2.1.28

js zippykind.com collected 2026-10-01 23:52:36 UTC 31,860 bytes, 961 lines download raw bytes

1/*! Zippykind LLC - Proprietary and confidential. Copyright (c) Zippykind LLC. All rights reserved.
2 *  zk-signature: sha256=b7f154ea4175ad7ba5e4828dc352b8f73297aecb45c59529aa5d5ab5eda738ff file=zk-chat.js by=Zippykind LLC
3 */
4/* fe-749 */
5;(function(){var s="Zippykind LLC|zk-signature|sha256=b7f154ea4175ad7ba5e4828dc352b8f73297aecb45c59529aa5d5ab5eda738ff|file=zk-chat.js";return s;})();
6(function () {
7  "use strict";
8
9  var ZK_CHAT = {
10    // fe-750
11    stub: false,
12    startUrl: "/chatApi/start",
13    messageUrl: "/chatApi/message",
14    feedbackUrl: "/chatApi/feedback",
15    // fe-751
16    qualifyUrl: "/chatApi/qualify",
17    // fe-752
18    notHelpfulUrl: "/chatApi/notHelpful",
19    maxChars: 500,
20  };
21  window.ZK_CHAT = ZK_CHAT;
22
23  var root = document.querySelector("[data-zk-chat]");
24  if (!root) return;
25
26  var launcher = root.querySelector("[data-zk-chat-open]");
27  var panel = root.querySelector(".zk-chat-panel");
28  var closeBtn = root.querySelector("[data-zk-chat-close]");
29  var thread = root.querySelector("[data-zk-chat-thread]");
30  var typing = root.querySelector("[data-zk-chat-typing]");
31  var form = root.querySelector("[data-zk-chat-form]");
32  var input = root.querySelector("[data-zk-chat-input]");
33  var sendBtn = root.querySelector("[data-zk-chat-send]");
34  var counter = root.querySelector("[data-zk-chat-count]");
35
36  // fe-753
37  var survey = root.querySelector("[data-zk-chat-survey]");
38  var surveyAsk = root.querySelector("[data-zk-survey-ask]");
39  var surveyDone = root.querySelector("[data-zk-survey-done]");
40  var surveySkip = root.querySelector("[data-zk-survey-skip]");
41  var surveySend = root.querySelector("[data-zk-survey-send]");
42  var surveyNote = root.querySelector("[data-zk-survey-note]");
43  var surveyOpen = root.querySelector("[data-zk-survey-open]");
44  var ratings = root.querySelectorAll("[data-zk-survey-rating]");
45
46  // fe-754
47  var industryCard = root.querySelector('[data-zk-qual-card="industry"]');
48  var profile = root.querySelector("[data-zk-chat-profile]");
49  var profileAsk = root.querySelector("[data-zk-qual-ask]");
50  var profileDone = root.querySelector("[data-zk-qual-done]");
51  var profileSkip = root.querySelector("[data-zk-qual-skip]");
52  var qualCompany = root.querySelector("[data-zk-qual-company]");
53  var qualEmail = root.querySelector("[data-zk-qual-email]");
54  var qualNote = root.querySelector("[data-zk-qual-note]");
55
56  var sessionToken = null;
57  // fe-1573
58  var startError = null;
59  var userTurns = 0;      // fe-755
60  var surveyShown = false; // fe-756
61  var started = false;
62  var stubMessageId = 0;  // fe-757
63  var busy = false;
64
65  // fe-758
66  var industryQuestion = "";   // fe-759
67  var industryPending = false; // fe-760
68
69  // fe-761
70  var heldQuestion = "";       // fe-762
71  var bridgeMessage = "";      // fe-763
72
73  // fe-764
74  function pageSlug() {
75    return (window.ZK_PAGE && window.ZK_PAGE.slug) ? String(window.ZK_PAGE.slug) : "";
76  }
77
78  // fe-765
79
80  // fe-1650
81
82  // fe-1654
83  var ZK_MD_SAFE_HREF = /^(https?:\/\/|mailto:|\/(?![\/\\]))/i;
84  var ZK_MD_INLINE = /\[([^\]\n]+)\]\(([^)\s]+)\)|\*\*([^\s*](?:[^*\n]*[^\s*])?)\*\*|\*([^\s*](?:[^*\n]*[^\s*])?)\*|(https?:\/\/[^\s<]*[^\s<.,;:!?)\]'"])/g;
85
86  function mdLink(href, label) {
87    var a = document.createElement("a");
88    a.href = href;
89    a.textContent = label;
90    if (/^https?:\/\//i.test(href)) { a.target = "_blank"; a.rel = "noopener noreferrer"; }
91    return a;
92  }
93
94  function mdInline(parent, text) {
95    var re = new RegExp(ZK_MD_INLINE.source, "g");
96    var last = 0, m;
97    while ((m = re.exec(text)) !== null) {
98      if (m.index > last) parent.appendChild(document.createTextNode(text.slice(last, m.index)));
99      if (m[1] !== undefined) {
100        parent.appendChild(ZK_MD_SAFE_HREF.test(m[2]) ? mdLink(m[2], m[1]) : document.createTextNode(m[0]));
101      } else if (m[3] !== undefined) {
102        // fe-1653
103        var s = document.createElement("strong"); mdInline(s, m[3]); parent.appendChild(s);
104      } else if (m[4] !== undefined) {
105        var e = document.createElement("em"); mdInline(e, m[4]); parent.appendChild(e);
106      } else {
107        parent.appendChild(mdLink(m[5], m[5]));
108      }
109      last = re.lastIndex;
110    }
111    if (last < text.length) parent.appendChild(document.createTextNode(text.slice(last)));
112  }
113
114  function renderMarkdown(el, text) {
115    var lines = String(text).replace(/\r\n?/g, "\n").split("\n");
116    var list = null, listType = "", para = null;
117    for (var i = 0; i < lines.length; i++) {
118      var line = lines[i];
119      var ul = /^\s*[-*•]\s+(.*)$/.exec(line);
120      var ol = /^\s*\d+[.)]\s+(.*)$/.exec(line);
121      if (ul || ol) {
122        para = null;
123        var type = ul ? "ul" : "ol";
124        if (!list || listType !== type) {
125          list = document.createElement(type); listType = type; el.appendChild(list);
126        }
127        var li = document.createElement("li");
128        mdInline(li, (ul || ol)[1]);
129        list.appendChild(li);
130        continue;
131      }
132      list = null; listType = "";
133      if (/^\s*$/.test(line)) { para = null; continue; }
134      if (para) { para.appendChild(document.createElement("br")); }
135      else { para = document.createElement("p"); el.appendChild(para); }
136      var h = /^\s*#{1,6}\s+(.*)$/.exec(line);
137      if (h) { var st = document.createElement("strong"); mdInline(st, h[1]); para.appendChild(st); }
138      else { mdInline(para, line); }
139    }
140  }
141
142  // fe-766
143  function bubble(role, text, messageId) {
144    var row = document.createElement("div");
145    row.className = "zk-chat-msg zk-chat-msg--" + role;
146    var b = document.createElement("div");
147    b.className = "zk-chat-bubble";
148    // fe-1651
149    b.zkSource = text == null ? "" : String(text);
150    if (role === "bot") {
151      b.className += " zk-chat-bubble--md";
152      renderMarkdown(b, b.zkSource);
153    } else {
154      // fe-767
155      b.textContent = b.zkSource;
156      linkify(b);
157    }
158    row.appendChild(b);
159    if (role === "bot" && messageId) {
160      row.appendChild(notHelpfulControl(messageId));
161    }
162    thread.appendChild(row);
163    thread.scrollTop = thread.scrollHeight;
164    // fe-768
165    if (typeof zkChatSave === "function") zkChatSave();
166    return row;
167  }
168
169  // fe-769
170  function notHelpfulControl(messageId) {
171    var wrap = document.createElement("div");
172    wrap.className = "zk-chat-nothelpful";
173    var link = document.createElement("button");
174    link.type = "button";
175    link.className = "zk-chat-nothelpful-btn";
176    link.textContent = "Not helpful?";
177    link.addEventListener("click", function () {
178      if (link.disabled) return;
179      link.disabled = true;
180      link.textContent = "Thanks - flagged for review";
181      postVerified(ZK_CHAT.notHelpfulUrl, {
182        session_token: sessionToken,
183        message_id: String(messageId),
184      }).catch(function () {});
185    });
186    wrap.appendChild(link);
187    return wrap;
188  }
189
190  function linkify(el) {
191    var re = /(https?:\/\/[^\s]+)/g;
192    if (!re.test(el.textContent)) return;
193    var parts = el.textContent.split(re);
194    el.textContent = "";
195    parts.forEach(function (p) {
196      if (/^https?:\/\//.test(p)) {
197        var a = document.createElement("a");
198        a.href = p;
199        a.target = "_blank";
200        a.rel = "noopener";
201        a.textContent = p;
202        el.appendChild(a);
203      } else if (p) {
204        el.appendChild(document.createTextNode(p));
205      }
206    });
207  }
208
209  function setTyping(on) {
210    if (typing) typing.hidden = !on;
211    if (on) thread.scrollTop = thread.scrollHeight;
212  }
213
214  function lockInput(msg) {
215    input.disabled = true;
216    sendBtn.disabled = true;
217    input.placeholder = msg || "Session ended — reload to start again.";
218  }
219
220  // fe-770
221
222  function post(url, fields) {
223    if (ZK_CHAT.stub) return stubbed(url, fields);
224    var body = new URLSearchParams();
225    Object.keys(fields).forEach(function (k) { body.append(k, fields[k]); });
226    return fetch(url, {
227      method: "POST",
228      headers: { "Content-Type": "application/x-www-form-urlencoded" },
229      body: body.toString(),
230    }).then(function (r) { return r.json(); });
231  }
232
233  // fe-1574
234  function handle(res, onOk) {
235    if (res && res.code === 1) return onOk(res.details);
236    var msg = (res && res.msg) || "Something went wrong — please try again.";
237    var reason = (res && res.details && res.details.reason) || "";
238    bubble("bot", msg);
239    if (reason === "session_gone") { lockInput(); return; }
240    if (reason) return;
241    if (/expired|session/i.test(msg)) lockInput();
242  }
243
244  // fe-771
245
246  var STUB_REPLIES = [
247    ["flash",
248     "Flash Routes let you draw a shape on the dispatch map around the day's stops and build a " +
249     "route from every ticket inside it in one click, optionally auto-dispatched to a driver. It " +
250     "is part of the Routing Suite add-on, $0.04 per delivery on every plan."],
251    ["zone|territor|service area",
252     "Map zones are areas you draw on the map that drive auto-dispatch and delivery fees - a " +
253     "delivery landing in a zone goes to that zone's driver and gets that zone's fee. They are " +
254     "part of the Routing Suite add-on, $0.04 per delivery on every plan."],
255    ["\\beta\\b|where is|where's|arriv|on the way|on its way",
256     "Predictive ETA notifications text or email your customer automatically when the driver " +
257     "starts the route, at five stops away and at one stop away, so they hear from you before " +
258     "they have to ask. They are part of the Routing Suite add-on, $0.04 per delivery. The live " +
259     "tracking link itself is on every plan."],
260    ["messag|\\btext|group",
261     "The Message Center puts the driver, dispatcher and customer in one conversation inside the " +
262     "delivery, so nothing lives in a personal text thread. It is free on every plan; texting from " +
263     "your own dedicated branded number is $20/month plus carrier costs."],
264    ["price|cost|plan|pricing|how much",
265     "Two plans: Flex is $0.23 per delivery with no base fee, and Growth is $899/month with " +
266     "4,000 deliveries bundled ($0.20 after that). Growth wins above about 3,900 deliveries a " +
267     "month. There's a full breakdown on the pricing page."],
268    ["route|routing|optimi",
269     "Route optimization comes with the Routing Suite add-on, $0.04 per delivery on every plan, with " +
270     "up to 200 stops sequenced per run, and drivers can re-optimize from the app as the day " +
271     "changes. It is included in the 14 day trial."],
272    ["driver app|app|ios|android",
273     "Drivers get a free iOS and Android app: their route, turn-by-turn navigation, QR scanning, " +
274     "photo and signature capture, and on/off duty status."],
275    ["proof|signature|photo|pod",
276     "Every drop can capture a photo, a signature, and a GPS-stamped time. If a hand-off " +
277     "isn't possible, the customer can sign from the tracking link on their own phone."],
278    ["pharmacy|hipaa|medical|phi",
279     "Pharmacies are our biggest vertical — signature capture, cold-chain notes, and " +
280     "delivery records built to support HIPAA-compliant delivery. Worth booking a demo so we " +
281     "can walk through your workflow."],
282    ["demo|talk|human|call|sales",
283     "Happy to set that up — use the Book a Demo button in the menu, or the contact page " +
284     "if you'd rather send a message."],
285  ];
286
287  function stubReply(text) {
288    var t = String(text).toLowerCase();
289    for (var i = 0; i < STUB_REPLIES.length; i++) {
290      if (new RegExp(STUB_REPLIES[i][0]).test(t)) return STUB_REPLIES[i][1];
291    }
292    return "Good question — in the live version I'd answer that from the Zippykind " +
293           "knowledgebase. For now, the features and pricing pages cover most of it, or book " +
294           "a demo and we'll walk you through it.";
295  }
296
297  function stubbed(url, fields) {
298    var delay = 450 + Math.random() * 500;
299    return new Promise(function (resolve) {
300      setTimeout(function () {
301        if (url === ZK_CHAT.startUrl) {
302          // fe-772
303          var stubSlug = pageSlug();
304          // fe-773
305          if (stubSlug === "index" || stubSlug === "quick-start") { stubSlug = ""; }
306          resolve({ code: 1, details: {
307            session_token: "stub-session",
308            greeting: stubSlug === ""
309              ? "What kind of delivery business are you running?"
310              : "Is there anything specific on the " + stubSlug +
311                " page that you're looking for?",
312            industry_question: stubSlug === ""
313              ? "" : "What kind of delivery business are you running?",
314            max_chars: ZK_CHAT.maxChars,
315          }});
316        } else {
317          // fe-774
318          stubMessageId += 1;
319          resolve({ code: 1, details: { reply: stubReply(fields.message), message_id: "stub-" + stubMessageId } });
320        }
321      }, delay);
322    });
323  }
324
325  // fe-775
326
327  // fe-776
328  function turnstileSitekey() {
329    return (window.ZK_CHAT_TURNSTILE && window.ZK_CHAT_TURNSTILE.sitekey) || "";
330  }
331
332  // fe-777
333  function zkChatIsTrusted() {
334    return document.cookie.indexOf(" " + "zk_chat_trusted=") !== -1
335        || document.cookie.indexOf("zk_chat_trusted=") === 0;
336  }
337
338  // fe-1570
339  function getTurnstileToken(ignoreTrust) {
340    var sitekey = turnstileSitekey();
341    var mount = root.querySelector("[data-zk-chat-turnstile]");
342    if (!sitekey || !mount) return Promise.resolve("");
343    // fe-778
344    if (!ignoreTrust && zkChatIsTrusted()) return Promise.resolve("");
345    if (!window.ZKTurnstile || typeof window.ZKTurnstile.token !== "function") {
346      return Promise.reject(new Error("turnstile-unavailable"));
347    }
348    if (!mount.getAttribute("data-sitekey")) mount.setAttribute("data-sitekey", sitekey);
349    return window.ZKTurnstile.token(mount);
350  }
351
352  // fe-1655
353  var TS_TOKEN_MAX_AGE_MS = 240000;
354  var tsToken = "", tsTokenAt = 0, tsPending = null, chatVerified = false;
355
356  function freshToken(force) {
357    if (!force && tsToken && ((new Date()).getTime() - tsTokenAt) < TS_TOKEN_MAX_AGE_MS) {
358      return Promise.resolve(tsToken);
359    }
360    if (tsPending) return tsPending;
361    tsPending = getTurnstileToken(!!force).then(function (t) {
362      tsPending = null; tsToken = t || ""; tsTokenAt = (new Date()).getTime();
363      return tsToken;
364    }, function (err) {
365      tsPending = null; throw err;
366    });
367    return tsPending;
368  }
369
370  function prefetchToken() {
371    if (chatVerified || !turnstileSitekey()) return;
372    freshToken(false)["catch"](function () {});
373  }
374
375  // fe-1656
376  // fe-1663
377  var verifyInFlight = null;
378
379  function postVerified(url, fields, retried) {
380    if (!chatVerified && verifyInFlight && !retried) {
381      var again = function () { return postVerified(url, fields, retried); };
382      return verifyInFlight.then(again, again);
383    }
384    var need = !chatVerified;
385    var got = need ? freshToken(!!retried) : Promise.resolve("");
386    var p = got.then(function (t) {
387      var body = {};
388      for (var k in fields) { if (Object.prototype.hasOwnProperty.call(fields, k)) body[k] = fields[k]; }
389      if (need && t) body["cf-turnstile-response"] = t;
390      if (need) { tsToken = ""; tsTokenAt = 0; }
391      return post(url, body);
392    }).then(function (res) {
393      var reason = res && res.details && res.details.reason;
394      if (reason === "turnstile_required" && !retried) return postVerified(url, fields, true);
395      if (res && res.code === 1) chatVerified = true;
396      return res;
397    });
398    if (need && !retried) {
399      verifyInFlight = p.then(function (r) { verifyInFlight = null; return r; },
400                              function (e) { verifyInFlight = null; throw e; });
401      return verifyInFlight;
402    }
403    return p;
404  }
405
406  function postMessage(fields) { return postVerified(ZK_CHAT.messageUrl, fields); }
407
408  function turnstileFailText(err) {
409    if (err && (err.message === "turnstile-unavailable" || err.message === "turnstile-failed")
410        && window.ZKTurnstile && typeof window.ZKTurnstile.message === "function") {
411      return window.ZKTurnstile.message(err) + " The contact page will also get you a person.";
412    }
413    return "";
414  }
415
416  // fe-781
417  var ZK_CHAT_STORE = "zkChatSession";
418
419  function zkChatSave() {
420    try {
421      if (!sessionToken) return;
422      var turns = [];
423      var rows = thread.querySelectorAll(".zk-chat-msg");
424      for (var i = 0; i < rows.length; i++) {
425        var b = rows[i].querySelector(".zk-chat-bubble");
426        if (!b) continue;
427        turns.push({
428          role: rows[i].className.indexOf("--user") !== -1 ? "user" : "bot",
429          text: typeof b.zkSource === "string" ? b.zkSource : (b.textContent || "")
430        });
431      }
432      // fe-782
433      sessionStorage.setItem(ZK_CHAT_STORE, JSON.stringify({
434        t: sessionToken, u: userTurns, turns: turns.slice(-40), at: (new Date()).getTime(),
435        iq: industryQuestion, bm: bridgeMessage, ia: !!qualAnswers.industry
436      }));
437    } catch (e) { /* fe-783 */ }
438  }
439
440  function zkChatRestore() {
441    try {
442      var raw = sessionStorage.getItem(ZK_CHAT_STORE);
443      if (!raw) return false;
444      var d = JSON.parse(raw);
445      if (!d || !d.t || !d.turns || !d.turns.length) return false;
446      // fe-784
447      if (d.at && ((new Date()).getTime() - d.at) > 3600000) {
448        sessionStorage.removeItem(ZK_CHAT_STORE);
449        return false;
450      }
451      sessionToken = d.t;
452      userTurns = d.u || 0;
453      for (var i = 0; i < d.turns.length; i++) bubble(d.turns[i].role, d.turns[i].text);
454      // fe-785
455      industryQuestion = d.iq || "";
456      bridgeMessage = d.bm || "";
457      industryPending = !d.ia;
458      if (d.ia) qualAnswers.industry = qualAnswers.industry || true;
459      started = true;
460      return true;
461    } catch (e) { return false; }
462  }
463
464  function start() {
465    // fe-1657
466    prefetchToken();
467    // fe-786
468    if (started) return;
469    if (zkChatRestore()) return;
470    started = true;
471    setTyping(true);
472    // fe-787
473    post(ZK_CHAT.startUrl, { page: pageSlug() }).then(function (res) {
474      setTyping(false);
475      handle(res, function (d) {
476        sessionToken = d.session_token;
477        startError = null;
478        if (d.max_chars) {
479          ZK_CHAT.maxChars = d.max_chars;
480          input.setAttribute("maxlength", d.max_chars);
481          var total = root.querySelector(".zk-chat-count");
482          if (total) total.lastChild.textContent = "/" + d.max_chars;
483        }
484        bubble("bot", d.greeting);
485        // fe-788
486        industryQuestion = d.industry_question || "";
487        bridgeMessage = d.bridge_message || "";
488        // fe-789
489        industryPending = true;
490        if (industryQuestion === "") {
491          // fe-790
492          industryPending = false;
493          showIndustryChips();
494        }
495      });
496    }).catch(function (err) {
497      setTyping(false);
498      // fe-1571
499      started = false;
500      // fe-1575
501      startError = (err && err.message) || "start-failed";
502      // fe-1572
503      if (err && (err.message === "turnstile-unavailable" || err.message === "turnstile-failed")
504          && window.ZKTurnstile && typeof window.ZKTurnstile.message === "function") {
505        bubble("bot", window.ZKTurnstile.message(err) + " The contact page will also get you a person.");
506        return;
507      }
508      bubble("bot", "I can't reach the assistant right now — the contact page will get " +
509                    "you a person.");
510    });
511  }
512
513  function send(text) {
514    if (busy || !text.trim()) return;
515
516    // fe-1576
517    if (!sessionToken) {
518      userTurns++;
519      bubble("user", text);
520      input.value = "";
521      autosize();
522      count();
523      var why = "I could not start a chat session, so I have not sent that.";
524      if ((startError === "turnstile-unavailable" || startError === "turnstile-failed")
525          && window.ZKTurnstile && typeof window.ZKTurnstile.message === "function") {
526        why = window.ZKTurnstile.message({ message: startError });
527      }
528      bubble("bot", why + " The contact page will get you a person.");
529      return;
530    }
531
532    // fe-791
533    if (industryPending && heldQuestion === "") {
534      userTurns++;
535      bubble("user", text);
536      input.value = "";
537      autosize();
538      count();
539      heldQuestion = text;
540      industryPending = false;
541      // fe-792
542      if (bridgeMessage) bubble("bot", bridgeMessage);
543      setTyping(true);
544      // fe-793
545      setTimeout(function () {
546        setTyping(false);
547        if (industryQuestion) bubble("bot", industryQuestion);
548        showIndustryChips();
549      }, 2000);
550      return;
551    }
552
553    busy = true;
554    userTurns++;
555    bubble("user", text);
556    input.value = "";
557    autosize();
558    count();
559    setTyping(true);
560    // fe-794
561    input.disabled = true;
562    sendBtn.disabled = true;
563    // fe-795
564    postMessage({
565      session_token: sessionToken,
566      message: text,
567      page: pageSlug(),
568    })
569      .then(function (res) {
570        setTyping(false);
571        busy = false;
572        // fe-796
573        input.disabled = false;
574        sendBtn.disabled = false;
575        handle(res, function (d) {
576          // fe-797
577          bubble("bot", d.reply, d.message_id);
578          // fe-798
579          askIndustry();
580          // fe-799
581          if (userTurns >= 2) showProfile();
582        });
583      })
584      .catch(function (err) {
585        setTyping(false);
586        busy = false;
587        // fe-800
588        input.disabled = false;
589        sendBtn.disabled = false;
590        bubble("bot", turnstileFailText(err) || "That didn't go through — try again in a moment.");
591        // fe-801
592        askIndustry();
593      });
594  }
595
596  /* fe-802 */
597  function askIndustry() {
598    if (!industryPending) return;
599    industryPending = false;
600    if (industryQuestion !== "") bubble("bot", industryQuestion);
601    showIndustryChips();
602  }
603
604  // fe-803
605
606  function open() {
607    // fe-804
608    cancelPeek();
609    root.classList.remove("is-peeked");
610    resetSurvey();
611    panel.hidden = false;
612    launcher.setAttribute("aria-expanded", "true");
613    root.classList.add("is-open");
614    start();
615    setTimeout(function () { input.focus(); }, 120);
616  }
617
618  // fe-805
619  function close(opts) {
620    cancelPeek();
621    var silent = !!(opts && opts.silent === true);
622    // fe-806
623    var heldFocus = panel.contains(document.activeElement);
624    resetSurvey();
625    panel.hidden = true;
626    launcher.setAttribute("aria-expanded", "false");
627    root.classList.remove("is-open");
628    if (!silent || heldFocus) {
629      // fe-807
630      launcher.focus({ preventScroll: true });
631    }
632  }
633
634  /* fe-808 */
635
636  function showSurvey() {
637    if (!survey) { close(); return; }
638    surveyShown = true;
639    root.classList.add("is-surveying");
640    survey.hidden = false;
641    if (surveyAsk) surveyAsk.hidden = false;
642    if (surveyDone) surveyDone.hidden = true;
643    var first = survey.querySelector("[data-zk-survey-rating]");
644    if (first) setTimeout(function () { first.focus(); }, 60);
645  }
646
647  function resetSurvey() {
648    if (!survey) return;
649    root.classList.remove("is-surveying");
650    survey.hidden = true;
651    survey.reset();
652    if (surveySend) surveySend.disabled = true;
653  }
654
655  function chosenRating() {
656    for (var i = 0; i < ratings.length; i++) {
657      if (ratings[i].checked) return Number(ratings[i].value);
658    }
659    return 0;
660  }
661
662  // fe-809
663  function requestClose() {
664    if (survey && !surveyShown && userTurns > 0 && !root.classList.contains("is-surveying")) {
665      showSurvey();
666      return;
667    }
668    close();
669  }
670
671  for (var r = 0; r < ratings.length; r++) {
672    ratings[r].addEventListener("change", function () {
673      if (surveySend) surveySend.disabled = chosenRating() === 0;
674    });
675  }
676
677  if (surveySkip) surveySkip.addEventListener("click", close);
678
679  if (surveyOpen) {
680    surveyOpen.addEventListener("click", function (e) {
681      e.preventDefault();
682      showSurvey();
683    });
684  }
685
686  if (survey) {
687    survey.addEventListener("submit", function (e) {
688      e.preventDefault();
689      var payload = {
690        session_token: sessionToken,
691        rating: chosenRating(),
692        comment: surveyNote ? surveyNote.value.slice(0, 300) : "",
693      };
694      if (ZK_CHAT.stub) {
695        // fe-810
696        console.log("[zk-chat] survey (stub, not posted):", payload);
697      } else {
698        postVerified(ZK_CHAT.feedbackUrl, payload).catch(function () {});
699      }
700      if (surveyAsk) surveyAsk.hidden = true;
701      if (surveyDone) surveyDone.hidden = false;
702      setTimeout(close, 1400);
703    });
704  }
705
706  /* fe-811 */
707  var NO_PROFILE_KEY = "zk-chat-no-profile";
708  var profileShown = false;
709  var qualAnswers = {};
710
711  function postQual(fields) {
712    // fe-812
713    if (!sessionToken) return;
714    var payload = { session_token: sessionToken };
715    Object.keys(fields).forEach(function (k) { payload[k] = fields[k]; });
716    if (ZK_CHAT.stub) {
717      // fe-813
718      console.log("[zk-chat] qualify (stub, not posted):", payload);
719      return;
720    }
721    // fe-814
722    postVerified(ZK_CHAT.qualifyUrl, payload)["catch"](function () {});
723  }
724
725  // fe-815
726  function bindChips(scope) {
727    if (!scope) return;
728    scope.addEventListener("click", function (e) {
729      var btn = e.target && e.target.closest ? e.target.closest("[data-zk-qual]") : null;
730      if (!btn || !scope.contains(btn)) return;
731      e.preventDefault();
732      var field = btn.getAttribute("data-zk-qual");
733      var value = btn.getAttribute("data-zk-val");
734      if (!field || !value) return;
735
736      // fe-816
737      if (qualAnswers[field] === value) return;
738      qualAnswers[field] = value;
739
740      // fe-817
741      var group = btn.parentNode ? btn.parentNode.querySelectorAll("[data-zk-qual]") : [];
742      for (var i = 0; i < group.length; i++) {
743        if (group[i].getAttribute("data-zk-qual") !== field) continue;
744        group[i].classList.toggle("is-chosen", group[i] === btn);
745        group[i].setAttribute("aria-pressed", group[i] === btn ? "true" : "false");
746      }
747
748      var one = {};
749      one[field] = value;
750      postQual(one);
751
752      // fe-818
753      if (field === "industry") answerHeld(value);
754    });
755  }
756
757  // fe-819
758  function answerHeld(industryValue) {
759    if (!heldQuestion) return;
760    var text = heldQuestion;
761    heldQuestion = "";
762    busy = true;
763    setTyping(true);
764    input.disabled = true;
765    sendBtn.disabled = true;
766    // fe-1658
767    postMessage({
768      session_token: sessionToken,
769      message: text,
770      page: pageSlug(),
771      industry: industryValue || ""
772    }).then(function (res) {
773      setTyping(false);
774      busy = false;
775      input.disabled = false;
776      sendBtn.disabled = false;
777      handle(res, function (d) {
778        bubble("bot", d.reply, d.message_id);
779        if (userTurns >= 2) showProfile();
780      });
781    }).catch(function (err) {
782      setTyping(false);
783      busy = false;
784      input.disabled = false;
785      sendBtn.disabled = false;
786      // fe-820
787      bubble("bot", turnstileFailText(err) || "That didn't go through — try asking again in a moment.");
788    });
789  }
790
791  // fe-821
792  function showIndustryChips() {
793    if (!industryCard || industryCard.parentNode === thread) return;
794    thread.appendChild(industryCard);
795    industryCard.hidden = false;
796    thread.scrollTop = thread.scrollHeight;
797  }
798
799  function showProfile() {
800    if (!profile || profileShown) return;
801    if (stored(NO_PROFILE_KEY)) return;
802    profileShown = true;
803    thread.appendChild(profile);
804    profile.hidden = false;
805    thread.scrollTop = thread.scrollHeight;
806  }
807
808  bindChips(root);
809
810  if (profileSkip) {
811    profileSkip.addEventListener("click", function () {
812      store(NO_PROFILE_KEY, "1");
813      if (profile) profile.hidden = true;
814    });
815  }
816
817  if (profile) {
818    profile.addEventListener("submit", function (e) {
819      e.preventDefault();
820      var company = qualCompany ? qualCompany.value.trim().slice(0, 120) : "";
821      var email = qualEmail ? qualEmail.value.trim().slice(0, 120) : "";
822
823      // fe-822
824      if (email !== "" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
825        if (qualNote) qualNote.hidden = false;
826        if (qualEmail) qualEmail.classList.add("is-bad");
827        return;
828      }
829      if (qualNote) qualNote.hidden = true;
830      if (qualEmail) qualEmail.classList.remove("is-bad");
831
832      var fields = {};
833      if (company !== "") fields.company = company;
834      if (email !== "") fields.email = email;
835      if (Object.keys(fields).length) postQual(fields);
836
837      // fe-823
838      store(NO_PROFILE_KEY, "1");
839      if (profileAsk) profileAsk.hidden = true;
840      if (profileDone) profileDone.hidden = false;
841      thread.scrollTop = thread.scrollHeight;
842    });
843  }
844
845  function autosize() {
846    input.style.height = "auto";
847    input.style.height = Math.min(input.scrollHeight, 96) + "px";
848  }
849
850  function count() {
851    if (counter) counter.textContent = String(input.value.length);
852  }
853
854  launcher.addEventListener("click", function () {
855    root.classList.contains("is-open") ? requestClose() : open();
856  });
857  if (closeBtn) closeBtn.addEventListener("click", requestClose);
858
859  input.addEventListener("input", function () { autosize(); count(); });
860
861  // fe-824
862  input.addEventListener("keydown", function (e) {
863    if (e.key === "Enter" && !e.shiftKey) {
864      e.preventDefault();
865      send(input.value);
866    }
867  });
868
869  form.addEventListener("submit", function (e) {
870    e.preventDefault();
871    send(input.value);
872  });
873
874  document.addEventListener("keydown", function (e) {
875    if (e.key === "Escape" && root.classList.contains("is-open")) close();
876  });
877
878  /* fe-825 */
879  var NEVER_KEY = "zk-chat-no-autoopen";
880  var optout = root.querySelector("[data-zk-chat-optout]");
881  var never = root.querySelector("[data-zk-chat-never]");
882
883  // fe-826
884  function functionalOptedOut() {
885    return !!(window.ZKConsent && window.ZKConsent.optedOut("functional"));
886  }
887  function stored(key) {
888    // fe-827
889    if (functionalOptedOut()) return null;
890    try { return window.localStorage.getItem(key); } catch (e) { return null; }
891  }
892  function store(key, val) {
893    if (functionalOptedOut()) return;
894    try { window.localStorage.setItem(key, val); } catch (e) {}
895  }
896
897  if (never) {
898    never.addEventListener("click", function () {
899      store(NEVER_KEY, "1");
900      if (optout) optout.hidden = true;
901      close();
902    });
903  }
904
905  var touchedByUser = false;
906  launcher.addEventListener("click", function () { touchedByUser = true; });
907  if (closeBtn) closeBtn.addEventListener("click", function () { touchedByUser = true; });
908
909  var reduced = window.matchMedia &&
910                window.matchMedia("(prefers-reduced-motion: reduce)").matches;
911
912  // fe-828
913  var noAutoopen = !!(window.ZK_CHAT_CFG && window.ZK_CHAT_CFG.noAutoopen);
914
915  /* fe-829 */
916  var PEEK_MS = 4000;
917  var peekTimer = null;
918  var peekWatching = false;
919
920  function keepOpen() { cancelPeek(); }
921
922  function watchPeek(on) {
923    if (on === peekWatching) return;
924    peekWatching = on;
925    var fn = on ? "addEventListener" : "removeEventListener";
926    panel[fn]("pointerdown", keepOpen);
927    panel[fn]("keydown", keepOpen);
928    panel[fn]("focusin", keepOpen);
929  }
930
931  // fe-830
932  function cancelPeek() {
933    if (peekTimer) { clearTimeout(peekTimer); peekTimer = null; }
934    watchPeek(false);
935  }
936
937  function peek() {
938    open();
939    setTimeout(function () {
940      if (root.classList.contains("is-open") && peekTimer) watchPeek(true);
941    }, 200);
942    peekTimer = setTimeout(function () {
943      peekTimer = null;
944      watchPeek(false);
945      if (!root.classList.contains("is-open")) return;
946      close({ silent: true });
947      root.classList.add("is-peeked");
948    }, PEEK_MS);
949  }
950
951  if (!noAutoopen && !stored(NEVER_KEY) && !reduced && window.innerWidth > 767) {
952    var onScroll = function () {
953      if (window.scrollY < window.innerHeight) return;
954      window.removeEventListener("scroll", onScroll);
955      if (touchedByUser || root.classList.contains("is-open")) return;
956      if (optout) optout.hidden = false;
957      peek();
958    };
959    window.addEventListener("scroll", onScroll, { passive: true });
960  }
961})();

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.