PageSourceSearch

https://www.eclipseroofinglouisville.com/wp-content/plugins/301-in…/assets/chatbot.js?ver=1.4.3

js eclipseroofinglouisville.com collected 2026-10-07 03:27:31 UTC 37,959 bytes, 979 lines download raw bytes

1(function($){
2  "use strict";
3
4  // ---------- Helpers ----------
5  function escapeHtml(str){
6    return (str || "").toString()
7      .replace(/&/g,"&")
8      .replace(/</g,"&lt;")
9      .replace(/>/g,"&gt;")
10      .replace(/"/g,"&quot;")
11      .replace(/'/g,"&#39;");
12  }
13
14  function linkifyLine(escapedLine){
15    return escapedLine.replace(/(^|[^"'>])(https?:\/\/[^\s<]+)/g, function(_, pre, url){
16      const safeUrl = url.replace(/["'<>]/g, "");
17      return pre + '<a href="'+safeUrl+'" target="_blank" rel="noopener noreferrer">'+url+'</a>';
18    });
19  }
20
21  // Supports "Title: https://url" lines; otherwise linkifies bare URLs.
22  function renderRichText(text){
23    const lines = (text||"").toString().split(/\n/);
24    const out = lines.map(line=>{
25      const m = line.match(/^\s*([^:]{2,}):\s*(https?:\/\/\S+)\s*$/);
26      if(m){
27        const title = escapeHtml(m[1].trim());
28        const url = m[2].trim().replace(/["'<>]/g,"");
29        const urlEsc = escapeHtml(url);
30        return '<a href="'+urlEsc+'" target="_blank" rel="noopener noreferrer">'+title+'</a>';
31      }
32      const esc = escapeHtml(line);
33      return linkifyLine(esc);
34    });
35    return out.join("<br/>");
36  }
37
38  function fireGA(eventName, params){
39    try{
40      if(window.gtag){
41        window.gtag("event", eventName, params || {});
42      } else if(window.dataLayer){
43        window.dataLayer.push(Object.assign({event: eventName}, params || {}));
44      }
45    }catch(e){}
46  }
47
48  function rest(endpoint, method, data){
49    return fetch(_301InteractiveBot.restBase + endpoint, {
50      method: method || "POST",
51      credentials: "same-origin",
52      headers: {
53        "Content-Type": "application/json",
54        "X-WP-Nonce": _301InteractiveBot.nonce
55      },
56      body: data ? JSON.stringify(data) : null
57    }).then(async (res)=>{
58      if(!res.ok){
59        const t = await res.text();
60        throw new Error(t || ("HTTP " + res.status));
61      }
62      return res.json();
63    });
64  }
65
66  // ---------- UI helpers ----------
67  function setStatus($w, msg){
68    const $status = $w.find("._301interactivebot-status");
69    const text = msg || "";
70    $status.text(text);
71    $status.toggleClass("is-visible", !!text);
72  }
73
74  function addMsg($box, sender, msg){
75    const cls = sender === "admin" ? "bot" : sender; // display admin as bot-style bubble
76    const html = '<div class="_301interactivebot-msg '+cls+'">'+ renderRichText(msg) +'</div>';
77    $box.append(html);
78    $box.scrollTop($box[0].scrollHeight);
79  }
80
81  function renderRecommended($box, links, chat_id){
82    if(!window._301InteractiveBot || !_301InteractiveBot.showRecommendedLinks) return;
83    if(!links || !links.length) return;
84    const $wrap = $("<div/>").addClass("_301interactivebot-reco-wrap");
85    const $title = $("<div/>").addClass("_301interactivebot-reco-title").text("Recommended pages");
86    const $cards = $("<div/>").addClass("_301interactivebot-reco-cards");
87
88    links.slice(0,6).forEach(l=>{
89      const url = (l.url||"").toString();
90      if(!url) return;
91      const text = (l.title||url).toString();
92      const $a = $("<a/>")
93        .addClass("_301interactivebot-reco-card")
94        .attr("href", url)
95        .attr("target","_blank")
96        .attr("rel","noopener noreferrer")
97        .append($("<div/>").addClass("_301interactivebot-reco-card-title").text(text))
98        .append($("<div/>").addClass("_301interactivebot-reco-card-url").text(url));
99
100      $a.on("click", ()=>{
101        try{
102          if(chat_id){
103            rest("/event","POST",{chat_id:chat_id, event_type:"link_click", event_value: JSON.stringify({title:text,url:url}), url: window.location.href});
104          }
105        }catch(e){}
106      });
107
108      $cards.append($a);
109    });
110
111    $wrap.append($title).append($cards);
112    $box.append($wrap);
113    $box.scrollTop($box[0].scrollHeight);
114  }
115
116  // ---------- Polling ----------
117  function pollLoop($w, chat_id, seen, onIncoming, state){
118    const pollState = state || { sinceId: 0 };
119    const $box = $w.find("._301interactivebot-messages");
120    const tick = ()=>{
121      fetch(_301InteractiveBot.restBase + "/poll?chat_id=" + encodeURIComponent(chat_id) + "&since_id=" + pollState.sinceId, {
122        headers: {"X-WP-Nonce": _301InteractiveBot.nonce},
123        credentials: "same-origin"
124      }).then(r=>r.json()).then(resp=>{
125        if(resp && Array.isArray(resp.messages)){
126          resp.messages.forEach(m=>{
127            const mid = parseInt(m.id, 10) || 0;
128            pollState.sinceId = Math.max(pollState.sinceId, mid);
129            if(seen && seen.has(m.sender, m.message)) return;
130            addMsg($box, m.sender, m.message);
131            if(seen) seen.add(m.sender, m.message);
132            if(typeof onIncoming === "function"){ try{ onIncoming(m); }catch(e){} }
133          });
134
135          if(resp.admin_takeover === 1){
136            setStatus($w, "A live team member has joined the chat.");
137          } else {
138            setStatus($w, "");
139          }
140        }
141      }).catch(()=>{});
142    };
143    tick();
144    return setInterval(tick, (parseInt(_301InteractiveBot.pollIntervalMs||1000,10) || 1000));
145  }
146
147  // ---------- Init ----------
148  function initWidget($w){
149    // Theme/branding
150    try{
151      $w.css("--_301interactivebot-primary", _301InteractiveBot.primaryColor || "#0b1f3a");
152      $w.css("--_301interactivebot-accent", _301InteractiveBot.accentColor || "#2563eb");
153      $w.css("--_301interactivebot-bubble", _301InteractiveBot.bubbleColor || "#2563eb");
154      $w.css("--_301interactivebot-text", _301InteractiveBot.textColor || "#0b1f3a");
155
156      if(_301InteractiveBot.logoUrl){
157        $w.find("._301interactivebot-logo").attr("src", _301InteractiveBot.logoUrl).show();
158      }
159      if((_301InteractiveBot.closedIcon || "chat") === "none"){
160        $w.find("._301interactivebot-bubble").hide();
161      } else {
162        $w.find("._301interactivebot-bubble").show();
163      }
164    }catch(e){}
165
166    function applyWidgetPosition(){
167      const mode = (_301InteractiveBot.widgetMode || 'floating');
168      const pos = (_301InteractiveBot.widgetPosition || 'bottom-right');
169      const bp = parseInt(_301InteractiveBot.widgetMobileBreakpoint||768,10);
170      const isMobile = window.innerWidth <= bp;
171      const ox = isMobile ? (parseInt(_301InteractiveBot.widgetOffsetXMobile||12,10)) : (parseInt(_301InteractiveBot.widgetOffsetXDesktop||20,10));
172      const oy = isMobile ? (parseInt(_301InteractiveBot.widgetOffsetYMobile||12,10)) : (parseInt(_301InteractiveBot.widgetOffsetYDesktop||20,10));
173      const z = parseInt(_301InteractiveBot.widgetZIndex||999999,10);
174
175      if(mode === 'floating'){
176        $w.css({position:'fixed', zIndex: z});
177        const clear = {top:'',bottom:'',left:'',right:''};
178        $w.css(clear);
179        if(pos.indexOf('bottom')===0) $w.css('bottom', oy+'px'); else $w.css('top', oy+'px');
180        if(pos.indexOf('right')>0) $w.css('right', ox+'px'); else $w.css('left', ox+'px');
181      } else {
182        // embedded
183        $w.css({position:'relative', top:'',bottom:'',left:'',right:'', zIndex:''});
184      }
185    }
186    applyWidgetPosition();
187    window.addEventListener('resize', function(){ applyWidgetPosition(); }, {passive:true});
188
189    const $box   = $w.find("._301interactivebot-messages");
190    const $input = $w.find("._301interactivebot-input");
191    const $send  = $w.find("._301interactivebot-send");
192    const $thinking = $w.find("._301interactivebot-thinking");
193    const $bubble = $w.find("._301interactivebot-bubble");
194    const $toggle = $w.find("._301interactivebot-toggle");
195    const $window = $w.find("._301interactivebot-window");
196    const $inputRow = $w.find("._301interactivebot-input-row");
197    const $lead = $w.find("._301interactivebot-lead");
198    const $leadFirst = $w.find("._301interactivebot-lead-first");
199    const $leadLast = $w.find("._301interactivebot-lead-last");
200    const $leadPhone = $w.find("._301interactivebot-lead-phone");
201    const $leadEmail = $w.find("._301interactivebot-lead-email");
202    const $leadAddress = $w.find("._301interactivebot-lead-address");
203    const $leadCity = $w.find("._301interactivebot-lead-city");
204    const $leadState = $w.find("._301interactivebot-lead-state");
205    const $leadZip = $w.find("._301interactivebot-lead-zip");
206    const $leadSubmit = $w.find("._301interactivebot-lead-submit");
207
208    let chat_id = null;
209    let session_key = null;
210    let poller = null;
211    let pageWatcher = null;
212    let lastPageUrl = window.location.href;
213    const storageKey = "301interactivebot_session";
214    const uiStateKey = "301interactivebot_widget_state";
215    let leadSavedPayload = null;
216    let leadSaveTimer = null;
217    let leadFlow = null;
218    let pendingLeadLinks = null;
219    let leadCollected = false;
220    const leadCaptureMode = (_301InteractiveBot.leadCaptureMode || "form").toString();
221    const requireEmail = !!_301InteractiveBot.requireEmail;
222    const requirePhone = !!_301InteractiveBot.requirePhone;
223    const requireAddress = !!_301InteractiveBot.requireAddress;
224    const requireCity = !!_301InteractiveBot.requireCity;
225    const requireState = !!_301InteractiveBot.requireState;
226    const requireZip = !!_301InteractiveBot.requireZip;
227    const escalationEnabled = !!_301InteractiveBot.escalationEnabled;
228    const escalationKeywords = Array.isArray(_301InteractiveBot.escalationKeywords) ? _301InteractiveBot.escalationKeywords.map(v=>(v||"").toString().trim().toLowerCase()).filter(Boolean) : [];
229    const leadPromptIntro = (_301InteractiveBot.leadPromptIntro || "To help with your request, please share your contact details.").toString();
230
231    function applyLeadFieldRequirements(){
232      const textFor = (name, required) => required ? `${name} (required)` : `${name} (optional)`;
233
234      $leadPhone.attr("placeholder", textFor("Phone", requirePhone));
235      $leadEmail.attr("placeholder", textFor("Email", requireEmail));
236      $leadAddress.attr("placeholder", textFor("Street Address", requireAddress));
237      $leadCity.attr("placeholder", textFor("City", requireCity));
238      $leadZip.attr("placeholder", textFor("Zip Code", requireZip));
239
240      $leadPhone.prop("required", requirePhone);
241      $leadEmail.prop("required", requireEmail);
242      $leadAddress.prop("required", requireAddress);
243      $leadCity.prop("required", requireCity);
244      $leadState.prop("required", requireState);
245      $leadZip.prop("required", requireZip);
246      $leadFirst.prop("required", true);
247      $leadLast.prop("required", true);
248
249      const $stateFirst = $leadState.find("option").first();
250      if($stateFirst.length){
251        $stateFirst.text(requireState ? "State (required)" : "State");
252      }
253    }
254    applyLeadFieldRequirements();
255
256    // pending lock (only defined once)
257    let pending = false;
258    function setPending(on, mode){
259      pending = !!on;
260      $thinking.toggleClass("is-visible", pending);
261      $thinking.find("._301interactivebot-thinking-text")
262        .text(mode === "admin" ? "Admin is replying…" : "Thinking…");
263      $input.prop("disabled", pending);
264      $send.prop("disabled", pending);
265    }
266
267    // Seen set for de-dup
268    const seen = {
269      map: {},
270      order: [],
271      key(sender, msg){ return (sender + "|" + (msg||"")).slice(0, 9000); },
272      has(sender, msg){ return !!this.map[this.key(sender,msg)]; },
273      add(sender, msg){
274        const k = this.key(sender,msg);
275        if(this.map[k]) return;
276        this.map[k] = 1;
277        this.order.push(k);
278        if(this.order.length > 300){
279          const old = this.order.shift();
280          delete this.map[old];
281        }
282      }
283    };
284
285    // Service-area and county/state mapping logic intentionally disabled for generic deployments.
286
287    function detectPricingIntent(text){
288      const msg = (text || "").toString().toLowerCase();
289      if(/\b(price|pricing|cost|how much|quote|estimate)\b/i.test(msg)) return true;
290      if(!escalationEnabled || !escalationKeywords.length) return false;
291      return escalationKeywords.some(keyword => keyword && msg.includes(keyword));
292    }
293
294    function shouldOpenLeadFromReply(replyText){
295      const txt = (replyText || "").toString().toLowerCase();
296      if(!txt) return false;
297      const asksLead = (
298        /first\s*name/.test(txt) &&
299        /last\s*name/.test(txt) &&
300        /address|street/.test(txt)
301      );
302      const explicitLeadPrompt = /please provide|share your|to get your info|get your info|lead info/.test(txt);
303      return asksLead || explicitLeadPrompt;
304    }
305
306    // Service-area price list sharing intentionally disabled for generic deployments.
307
308    function getLeadPayload(){
309      return {
310        first_name: ($leadFirst.val() || "").trim(),
311        last_name: ($leadLast.val() || "").trim(),
312        phone: ($leadPhone.val() || "").trim(),
313        email: ($leadEmail.val() || "").trim(),
314        address: ($leadAddress.val() || "").trim(),
315        city: ($leadCity.val() || "").trim(),
316        state: (($leadState.val() || "").trim().toUpperCase()),
317        zip: (($leadZip.val() || "").trim().replace(/\D/g, '').slice(0, 5))
318      };
319    }
320
321    function isLeadComplete(payload){
322      const hasAddress = !requireAddress || !!payload.address;
323      const hasCity = !requireCity || !!payload.city;
324      const hasState = !requireState || !!payload.state;
325      const hasZip = !requireZip || !!payload.zip;
326      const hasEmail = !requireEmail || !!payload.email;
327      const hasPhone = !requirePhone || !!payload.phone;
328      return payload.first_name && payload.last_name && hasAddress && hasCity && hasState && hasZip && hasEmail && hasPhone;
329    }
330
331    function shouldCollectLeadNow(){
332      if(leadCollected) return false;
333      if(leadCaptureMode === "chat" && leadFlow && leadFlow.active) return false;
334      return true;
335    }
336
337    function scheduleLeadSave(){
338      if(leadSaveTimer) clearTimeout(leadSaveTimer);
339      leadSaveTimer = setTimeout(saveLeadIfReady, 300);
340    }
341
342    function saveLeadIfReady(force, payloadOverride, announce){
343      if(!chat_id) return;
344      const payload = payloadOverride || getLeadPayload();
345      if(!force && !isLeadComplete(payload)) return;
346      const payloadKey = JSON.stringify(payload);
347      if(!force && payloadKey === leadSavedPayload) return;
348      leadSavedPayload = payloadKey;
349      rest("/lead","POST", Object.assign({chat_id, current_page: window.location.href, announce: !!announce}, payload)).then(()=>{
350        leadCollected = true;
351        if(force) setStatus($w, "Thanks! We'll be in touch.");
352        if(pendingLeadLinks && pendingLeadLinks.length){
353          renderRecommended($box, pendingLeadLinks, chat_id);
354          pendingLeadLinks = null;
355        }
356      }).catch(()=>{});
357    }
358
359    function handleMessageResponse(resp){
360      if(!resp) return;
361      if(resp.user_message_id){
362        pollState.sinceId = Math.max(pollState.sinceId, parseInt(resp.user_message_id, 10) || 0);
363      }
364      if(resp.mode === "bot" && resp.reply){
365        if(resp.message_id){
366          pollState.sinceId = Math.max(pollState.sinceId, parseInt(resp.message_id, 10) || 0);
367        }
368        addMsg($box, "bot", resp.reply);
369        seen.add("bot", resp.reply);
370        if(leadCaptureMode === "form" && !leadCollected && shouldOpenLeadFromReply(resp.reply)){
371          toggleLeadForm(true);
372        }
373        setPending(false);
374      } else if(resp.mode === "admin"){
375        setStatus($w, "A live team member is handling the chat.");
376        setPending(true, "admin");
377      }
378    }
379
380    function requestPostLeadResponse(summary){
381      if(!chat_id) return;
382      setPending(true, "ai");
383      rest("/message","POST",{
384        chat_id: chat_id || undefined,
385        session_key: session_key || undefined,
386        message: summary,
387        current_page: window.location.href,
388        referrer: document.referrer || ""
389      }).then(resp=>{
390        handleMessageResponse(resp);
391        if(resp && resp.suggested_links && resp.suggested_links.length){
392          if(resp.should_collect_lead || (leadFlow && leadFlow.active) || (leadCaptureMode === "form" && $lead.is(":visible"))){
393            pendingLeadLinks = resp.suggested_links;
394          } else {
395            renderRecommended($box, resp.suggested_links, chat_id);
396          }
397        }
398      }).catch(()=>{
399        setPending(false);
400      });
401    }
402
403    function persistWidgetOpenState(isOpen){
404      try {
405        localStorage.setItem(uiStateKey, JSON.stringify({isOpen: !!isOpen}));
406      } catch(e){}
407    }
408
409    function getInitialWidgetOpenState(){
410      try {
411        const raw = localStorage.getItem(uiStateKey);
412        if(!raw) return true; // default open
413        const parsed = JSON.parse(raw);
414        if(parsed && typeof parsed.isOpen === "boolean") return parsed.isOpen;
415      } catch(e){}
416      return true;
417    }
418
419    function setOpen(isOpen, persist){
420      $w.toggleClass("minimized", !isOpen);
421      $window.attr("aria-hidden", isOpen ? "false" : "true");
422      if(persist !== false) persistWidgetOpenState(isOpen);
423      if(isOpen){
424        setTimeout(()=>{ $input.trigger("focus"); }, 0);
425      }
426    }
427
428    $bubble.on("click", ()=>{ setOpen(true, true); activity(); });
429    $toggle.on("click", ()=>{ setOpen(false, true); });
430
431    function logPageView(url){
432      if(!chat_id) return;
433      rest("/event","POST",{chat_id: chat_id, event_type:"page_view", event_value:"", url: url || window.location.href}).catch(()=>{});
434    }
435
436    function startPageWatcher(){
437      if(pageWatcher) return;
438      lastPageUrl = window.location.href;
439      logPageView(lastPageUrl);
440      pageWatcher = setInterval(()=>{
441        if(!chat_id) return;
442        const current = window.location.href;
443        if(current !== lastPageUrl){
444          lastPageUrl = current;
445          logPageView(current);
446        }
447      }, 2000);
448    }
449
450    function stopPageWatcher(){
451      if(pageWatcher){
452        clearInterval(pageWatcher);
453        pageWatcher = null;
454      }
455    }
456
457    // Idle timeout
458    const idleSeconds = parseInt(_301InteractiveBot.idleTimeoutSeconds || 300, 10);
459    let idleTimer = null;
460    let warnTimer = null;
461
462    function clearIdle(){
463      if(idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
464      if(warnTimer) { clearTimeout(warnTimer); warnTimer = null; }
465    }
466
467    function showIdleWarning(){
468      if(!chat_id) return;
469      if($("._301interactivebot-idle-modal").length) return;
470      let remaining = 30;
471      const $modal = $(
472        '<div class="_301interactivebot-idle-modal">'+
473          '<div class="_301interactivebot-idle-card">'+
474            '<div class="_301interactivebot-idle-title">Still there?</div>'+
475            '<div class="_301interactivebot-idle-text">Your chat will end in 30 seconds.</div>'+
476            '<div class="_301interactivebot-idle-actions">'+
477              '<button type="button" class="_301interactivebot-idle-keep">Keep Open</button>'+
478              '<button type="button" class="_301interactivebot-idle-end">End Now</button>'+
479            '</div>'+
480          '</div>'+
481        '</div>'
482      );
483      $("body").append($modal);
484      const $text = $modal.find("._301interactivebot-idle-text");
485      $text.text(`Your chat will end in ${remaining} seconds.`);
486      const tick = setInterval(()=>{
487        remaining -= 1;
488        if(remaining <= 0){
489          clearInterval(tick);
490          $modal.remove();
491          endIdle("idle_timeout");
492          return;
493        }
494        $text.text(`Your chat will end in ${remaining} seconds.`);
495      }, 1000);
496      const clearTick = ()=>{ clearInterval(tick); };
497      $modal.find("._301interactivebot-idle-keep").on("click", ()=>{
498        clearTick();
499        $modal.remove();
500        resetIdle();
501      });
502      $modal.find("._301interactivebot-idle-end").on("click", ()=>{
503        clearTick();
504        $modal.remove();
505        endIdle("idle_end_now");
506      });
507    }
508
509    function clearChatUI(){
510      $box.empty();
511      seen.map = {};
512      seen.order = [];
513    }
514
515    function resetLeadForm(){
516      $leadFirst.val("");
517      $leadLast.val("");
518      $leadPhone.val("");
519      $leadEmail.val("");
520      $leadAddress.val("");
521      $leadCity.val("");
522      $leadState.val("");
523      $leadZip.val("");
524      leadSavedPayload = null;
525      leadFlow = null;
526      pendingLeadLinks = null;
527      if(leadSaveTimer){
528        clearTimeout(leadSaveTimer);
529        leadSaveTimer = null;
530      }
531      toggleLeadForm(false);
532    }
533
534    function resetWidgetAfterChatEnd(endMessage){
535      if(endMessage){
536        addMsg($box, "bot", endMessage);
537        seen.add("bot", endMessage);
538      }
539      setPending(false);
540      stopPageWatcher();
541      try{ localStorage.removeItem(storageKey); }catch(e){}
542      chat_id = null;
543      session_key = null;
544      pollState.sinceId = 0;
545      resetLeadForm();
546      clearChatUI();
547      setStatus($w, "");
548      $input.prop("disabled", false);
549      $send.prop("disabled", false);
550      setOpen(false, true);
551      addMsg($box, "bot", _301InteractiveBot.welcome || "Hi! How can I help?");
552      seen.add("bot", _301InteractiveBot.welcome || "Hi! How can I help?");
553    }
554
555    function endIdle(reason){
556      if(!chat_id) return;
557      clearIdle();
558      $("._301interactivebot-idle-modal").remove();
559      // end server-side
560      rest("/end","POST",{chat_id}).catch(()=>{});
561      fireGA("301interactivebot_chat_end", {chat_id: chat_id, reason: "idle"});
562      rest("/event","POST",{chat_id:chat_id, event_type:"chat_end", event_value:"idle", url: window.location.href}).catch(()=>{});
563      if(poller){
564        clearInterval(poller);
565        poller = null;
566      }
567      resetWidgetAfterChatEnd("Chat ended due to inactivity.");
568    }
569
570    function resetIdle(){
571      clearIdle();
572      if(!chat_id) return;
573      if(!idleSeconds || idleSeconds < 60) return;
574      warnTimer = setTimeout(showIdleWarning, Math.max(0, (idleSeconds - 30)) * 1000);
575      idleTimer = setTimeout(()=>endIdle("idle_timeout"), idleSeconds * 1000);
576    }
577
578    function activity(){
579      resetIdle();
580    }
581
582    function endChatOnUnexpectedClose(){
583      if(!chat_id) return;
584      const url = _301InteractiveBot.restBase + "/end";
585      const payload = JSON.stringify({ chat_id: chat_id });
586      let sent = false;
587
588      try{
589        if(navigator && typeof navigator.sendBeacon === "function"){
590          const blob = new Blob([payload], {type:"application/json"});
591          sent = navigator.sendBeacon(url, blob);
592        }
593      }catch(e){}
594
595      if(!sent){
596        try{
597          fetch(url, {
598            method: "POST",
599            headers: {"Content-Type":"application/json"},
600            credentials: "same-origin",
601            body: payload,
602            keepalive: true
603          }).catch(()=>{});
604        }catch(e){}
605      }
606    }
607
608    // Idle reset on activity (clean block, no duplicates)
609    const activityEvents = ["click","scroll","keydown","mousemove","touchstart"];
610    activityEvents.forEach(ev=>{
611      window.addEventListener(ev, activity, {passive:true});
612    });
613    document.addEventListener("visibilitychange", activity);
614    // Disabled automatic end-on-unload so chats persist across page navigation.
615    // This preserves chat/session continuity when users click to another page.
616    // window.addEventListener("pagehide", (e)=>{
617    //   if(e && e.persisted) return;
618    //   endChatOnUnexpectedClose();
619    // });
620    // window.addEventListener("beforeunload", ()=>{
621    //   endChatOnUnexpectedClose();
622    // });
623
624    function toggleLeadForm(show){
625      if(leadCaptureMode !== "form"){
626        $lead.hide();
627        return;
628      }
629      $lead.toggle(!!show);
630      $inputRow.toggle(!show);
631    }
632
633    [$leadFirst, $leadLast, $leadPhone, $leadEmail, $leadAddress, $leadCity, $leadState, $leadZip].forEach($field=>{
634      $field.on("input blur", scheduleLeadSave);
635    });
636    $leadSubmit.on("click", ()=>{
637      const payload = getLeadPayload();
638      const missing = [];
639      if(!payload.first_name) missing.push("First Name");
640      if(!payload.last_name) missing.push("Last Name");
641      if(requireAddress && !payload.address) missing.push("Street Address");
642      if(requireCity && !payload.city) missing.push("City");
643      if(requireState && !payload.state) missing.push("State");
644      if(requireZip && !payload.zip) missing.push("Zip Code");
645      if(requireState && payload.state && !/^(KY|IN)$/.test(payload.state.toUpperCase())) missing.push("State (KY/IN)");
646      if(requireZip && payload.zip && !/^\d{5}$/.test(payload.zip)) missing.push("Valid Zip Code");
647      if(requireEmail && !payload.email) missing.push("Email");
648      if(requirePhone && !payload.phone) missing.push("Phone");
649      if(missing.length){
650        setStatus($w, `${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} required.`);
651        return;
652      }
653      setStatus($w, "");
654      const emailText = payload.email ? `Email: ${payload.email}` : "Email: (not provided)";
655      const summary = `Customer info submitted: ${payload.first_name} ${payload.last_name}, Phone: ${payload.phone || "(not provided)"}, ${emailText}, Address: ${payload.address}, ${payload.city}, ${payload.state} ${payload.zip}.`;
656      addMsg($box, "user", summary);
657      seen.add("user", summary);
658      fireGA("301interactivebot_chat_lead_form", {chat_id: chat_id || undefined});
659      saveLeadIfReady(true, payload, true);
660      leadCollected = true;
661      //maybeSharePriceList(payload);
662      toggleLeadForm(false);
663      requestPostLeadResponse(summary);
664    });
665
666    function startLeadChatFlow(){
667      if(leadCaptureMode !== "chat") return;
668      if(leadFlow && leadFlow.active) return;
669      leadFlow = {active: true, step: "first_name", data: {}};
670      addMsg($box, "bot", leadPromptIntro);
671      seen.add("bot", leadPromptIntro);
672      addMsg($box, "bot", "What’s your first name?");
673      seen.add("bot", "What’s your first name?");
674    }
675
676    function handleLeadChatInput(message){
677      if(!leadFlow || !leadFlow.active) return false;
678      const msg = (message || "").trim();
679      if(!msg) return true;
680      switch(leadFlow.step){
681        case "first_name":
682          leadFlow.data.first_name = msg;
683          leadFlow.step = "last_name";
684          addMsg($box, "bot", "Thanks! What’s your last name?");
685          seen.add("bot", "Thanks! What’s your last name?");
686          return true;
687        case "last_name":
688          leadFlow.data.last_name = msg;
689          leadFlow.step = "phone";
690          addMsg($box, "bot", "What’s your phone number? (optional, type skip to continue)");
691          seen.add("bot", "What’s your phone number? (optional, type skip to continue)");
692          return true;
693        case "phone":
694          if(requirePhone && msg.toLowerCase() === "skip"){
695            addMsg($box, "bot", "Phone is required for this request. What’s your phone number?");
696            seen.add("bot", "Phone is required for this request. What’s your phone number?");
697            return true;
698          }
699          leadFlow.data.phone = (msg.toLowerCase() === "skip") ? "" : msg;
700          leadFlow.step = "email";
701          if(requireEmail){
702            addMsg($box, "bot", "What’s your email?");
703            seen.add("bot", "What’s your email?");
704          } else {
705            addMsg($box, "bot", "What’s your email? (optional, type skip to continue)");
706            seen.add("bot", "What’s your email? (optional, type skip to continue)");
707          }
708          return true;
709        case "email":
710          if(requireEmail && msg.toLowerCase() === "skip"){
711            addMsg($box, "bot", "Email is required for this request. What’s your email?");
712            seen.add("bot", "Email is required for this request. What’s your email?");
713            return true;
714          }
715          leadFlow.data.email = (msg.toLowerCase() === "skip") ? "" : msg;
716          if(requireAddress){
717            leadFlow.step = "address";
718            addMsg($box, "bot", "What is your street address?");
719            seen.add("bot", "What is your street address?");
720          } else if(requireCity) {
721            leadFlow.step = "city";
722            addMsg($box, "bot", "What city is that in?");
723            seen.add("bot", "What city is that in?");
724          } else if(requireState) {
725            leadFlow.step = "state";
726            addMsg($box, "bot", "Select state: KY or IN.");
727            seen.add("bot", "Select state: KY or IN.");
728          } else if(requireZip) {
729            leadFlow.step = "zip";
730            addMsg($box, "bot", "What is the 5-digit zip code?");
731            seen.add("bot", "What is the 5-digit zip code?");
732          } else {
733            leadFlow.step = "done";
734          }
735          return true;
736        case "address":
737          leadFlow.data.address = msg;
738          if(requireAddress && !leadFlow.data.address){
739            addMsg($box, "bot", "Please provide your street address so we can follow up.");
740            seen.add("bot", "Please provide your street address so we can follow up.");
741            return true;
742          }
743          if(requireCity){
744            leadFlow.step = "city";
745            addMsg($box, "bot", "What city is that in?");
746            seen.add("bot", "What city is that in?");
747          } else if(requireState){
748            leadFlow.step = "state";
749            addMsg($box, "bot", "Select state: KY or IN.");
750            seen.add("bot", "Select state: KY or IN.");
751          } else if(requireZip){
752            leadFlow.step = "zip";
753            addMsg($box, "bot", "What is the 5-digit zip code?");
754            seen.add("bot", "What is the 5-digit zip code?");
755          } else {
756            leadFlow.step = "done";
757          }
758          return true;
759        case "city":
760          leadFlow.data.city = msg;
761          if(requireCity && !leadFlow.data.city){
762            addMsg($box, "bot", "Please provide a city.");
763            seen.add("bot", "Please provide a city.");
764            return true;
765          }
766          if(requireState){
767            leadFlow.step = "state";
768            addMsg($box, "bot", "Select state: KY or IN.");
769            seen.add("bot", "Select state: KY or IN.");
770          } else if(requireZip){
771            leadFlow.step = "zip";
772            addMsg($box, "bot", "What is the 5-digit zip code?");
773            seen.add("bot", "What is the 5-digit zip code?");
774          } else {
775            leadFlow.step = "done";
776          }
777          return true;
778        case "state":
779          leadFlow.data.state = msg.toUpperCase();
780          if(requireState && !/^(KY|IN)$/.test(leadFlow.data.state)){
781            addMsg($box, "bot", "Please enter KY or IN.");
782            seen.add("bot", "Please enter KY or IN.");
783            return true;
784          }
785          if(requireZip){
786            leadFlow.step = "zip";
787            addMsg($box, "bot", "What is the 5-digit zip code?");
788            seen.add("bot", "What is the 5-digit zip code?");
789          } else {
790            leadFlow.step = "done";
791          }
792          return true;
793        case "zip":
794          leadFlow.data.zip = msg.replace(/\D/g, '').slice(0, 5);
795          if(requireZip && leadFlow.data.zip.length !== 5){
796            addMsg($box, "bot", "Please enter a valid 5-digit zip code.");
797            seen.add("bot", "Please enter a valid 5-digit zip code.");
798            return true;
799          }
800          leadFlow.step = "done";
801          // fall through
802        case "done":
803          leadFlow.active = false;
804          const payload = {
805            first_name: leadFlow.data.first_name || "",
806            last_name: leadFlow.data.last_name || "",
807            phone: leadFlow.data.phone || "",
808            email: leadFlow.data.email || "",
809            address: leadFlow.data.address || "",
810            city: leadFlow.data.city || "",
811            state: leadFlow.data.state || "",
812            zip: leadFlow.data.zip || ""
813          };
814          saveLeadIfReady(true, payload);
815          leadCollected = true;
816          addMsg($box, "bot", "Thanks! We’ve passed your info to a New Home Consultant.");
817          seen.add("bot", "Thanks! We’ve passed your info to a New Home Consultant.");
818          requestPostLeadResponse(`Customer info submitted: ${payload.first_name} ${payload.last_name}, Phone: ${payload.phone || "(not provided)"}, Email: ${payload.email || "(not provided)"}, Address: ${payload.address || "(not provided)"}, ${payload.city || "(city not provided)"}, ${payload.state || "(state not provided)"} ${payload.zip || "(zip not provided)"}.`);
819          leadFlow = null;
820          return true;
821        default:
822          return false;
823      }
824    }
825
826    // Start chat
827    setOpen(getInitialWidgetOpenState(), false);
828    addMsg($box, "bot", _301InteractiveBot.welcome || "Hi! How can I help?");
829    seen.add("bot", _301InteractiveBot.welcome || "Hi! How can I help?");
830    toggleLeadForm(false);
831
832    let stored = null;
833    try{
834      stored = JSON.parse(localStorage.getItem(storageKey) || "null");
835    }catch(e){}
836    const startPayload = {};
837    if(stored && stored.chat_id && stored.session_key){
838      startPayload.chat_id = stored.chat_id;
839      startPayload.session_key = stored.session_key;
840    } else if(stored && stored.session_key){
841      startPayload.session_key = stored.session_key;
842    }
843
844    const pollState = { sinceId: 0 };
845
846    rest("/start","POST",startPayload).then(resp=>{
847      chat_id = resp.chat_id || null;
848      session_key = resp.session_key || (stored ? stored.session_key : null);
849      if(session_key){
850        try{
851          const toStore = {session_key: session_key};
852          if(chat_id){
853            toStore.chat_id = chat_id;
854          }
855          localStorage.setItem(storageKey, JSON.stringify(toStore));
856        }catch(e){}
857      }
858      if(chat_id){
859        fireGA("301interactivebot_chat_start", {chat_id: chat_id});
860        rest("/event","POST",{chat_id:chat_id, event_type:"chat_start", event_value:"", url: window.location.href}).catch(()=>{});
861        resetIdle();
862        startPageWatcher();
863        // begin polling
864        poller = pollLoop($w, chat_id, seen, (m)=>{
865          // unlock when AI/admin actually responds
866          if(pending && (m.sender === "bot" || m.sender === "admin")){
867            setPending(false);
868          }
869          activity();
870        }, pollState);
871      }
872    }).catch(()=>{
873      setStatus($w, "Unable to start chat right now.");
874    });
875
876    function send(){
877      if(pending) return;
878      const msg = ($input.val() || "").trim();
879      if(!msg) return;
880
881      $input.val("");
882      addMsg($box, "user", msg);
883      seen.add("user", msg);
884      setPending(true, "ai");
885      activity();
886      const shouldEscalateNow = detectPricingIntent(msg);
887
888      if(handleLeadChatInput(msg)){
889        setPending(false);
890        return;
891      }
892
893      // Immediate ACK version expects queued response
894      rest("/message","POST",{
895        chat_id: chat_id || undefined,
896        session_key: session_key || undefined,
897        message: msg,
898        current_page: window.location.href,
899        referrer: document.referrer || ""
900      }).then(resp=>{
901        if(resp && resp.session_key && !session_key){
902          session_key = resp.session_key;
903        }
904        if(resp && resp.chat_id && !chat_id){
905          chat_id = resp.chat_id;
906          try{
907            localStorage.setItem(storageKey, JSON.stringify({chat_id, session_key}));
908          }catch(e){}
909          fireGA("301interactivebot_chat_start", {chat_id: chat_id});
910          rest("/event","POST",{chat_id:chat_id, event_type:"chat_start", event_value:"", url: window.location.href}).catch(()=>{});
911          resetIdle();
912          if(!poller){
913            poller = pollLoop($w, chat_id, seen, (m)=>{
914              if(pending && (m.sender === "bot" || m.sender === "admin")){
915                setPending(false);
916              }
917              activity();
918            }, pollState);
919          }
920          startPageWatcher();
921        }
922        if(resp && resp.user_message_id){
923          pollState.sinceId = Math.max(pollState.sinceId, parseInt(resp.user_message_id, 10) || 0);
924        }
925        if(resp && (resp.should_collect_lead || shouldEscalateNow) && shouldCollectLeadNow()){
926          if(leadCaptureMode === "chat"){
927            startLeadChatFlow();
928          } else {
929            addMsg($box, "bot", leadPromptIntro);
930            seen.add("bot", leadPromptIntro);
931            toggleLeadForm(true);
932          }
933          if(resp && resp.suggested_links && resp.suggested_links.length){
934            pendingLeadLinks = resp.suggested_links;
935          }
936        }
937        handleMessageResponse(resp);
938        if(resp && resp.suggested_links && resp.suggested_links.length){
939          if(resp.should_collect_lead || (leadFlow && leadFlow.active) || (leadCaptureMode === "form" && $lead.is(":visible"))){
940            pendingLeadLinks = resp.suggested_links;
941          } else {
942            renderRecommended($box, resp.suggested_links, chat_id);
943          }
944        }
945        scheduleLeadSave();
946      }).catch(()=>{
947        setPending(false);
948        addMsg($box, "bot", "Sorry—something went wrong. Please try again.");
949        seen.add("bot", "Sorry—something went wrong. Please try again.");
950      });
951    }
952
953    $send.on("click", send);
954    $input.on("keydown", (e)=>{ if(e.key === "Enter"){ e.preventDefault(); send(); } });
955
956    // End chat button (if present)
957    $w.find("._301interactivebot-endchat").on("click", ()=>{
958      if(!chat_id) return;
959      clearIdle();
960      $("._301interactivebot-idle-modal").remove();
961      rest("/end","POST",{chat_id}).catch(()=>{});
962      rest("/event","POST",{chat_id:chat_id, event_type:"chat_end", event_value:"manual", url: window.location.href}).catch(()=>{});
963      fireGA("301interactivebot_chat_end", {chat_id: chat_id, reason: "manual"});
964      if(poller){
965        clearInterval(poller);
966        poller = null;
967      }
968      resetWidgetAfterChatEnd("Chat ended. Thanks for visiting!");
969    });
970  }
971
972  $(function(){
973    // Initialize all widgets rendered on page
974    $("._301interactivebot-widget").each(function(){
975      initWidget($(this));
976    });
977  });
978
979})(jQuery);

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.