1(function($){ 2 "use strict"; 3 4 // ---------- Helpers ---------- 5 function escapeHtml(str){ 6 return (str || "").toString() 7 .replace(/&/g,"&") 8 .replace(/</g,"<") 9 .replace(/>/g,">") 10 .replace(/"/g,""") 11 .replace(/'/g,"'"); 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.