1(function () { 2 "use strict"; 3 4 const API_URL = "https://admin.casefox.com/api/ai-chatbot.php"; 5 const AUTO_OPEN_DELAY = 6000; 6 const MOBILE_BREAKPOINT = 768; 7 8 const widget = document.getElementById("cfxChatWidget"); 9 const toggleBtn = document.getElementById("cfxChatToggle"); 10 const closeBtn = document.getElementById("cfxCloseBtn"); 11 const resetBtn = document.getElementById("cfxResetBtn"); 12 const chatBody = document.getElementById("cfxChatBody"); 13 const inputEl = document.getElementById("cfxInput"); 14 const sendBtn = document.getElementById("cfxSendBtn"); 15 16 if (!widget || !toggleBtn || !closeBtn || !resetBtn || !chatBody || !inputEl || !sendBtn) { 17 console.warn("CaseFox chatbot: required HTML elements missing."); 18 return; 19 } 20 21 let chatHistory = []; 22 let isWaiting = false; 23 let currentQuickContainer = null; 24 let autoMessageRunning = false; 25 let autoOpenTimer = null; 26 let introStarted = false; 27 28 function isMobileView() { 29 return window.innerWidth <= MOBILE_BREAKPOINT; 30 } 31 32 function setCookie(name, value, days) { 33 const expires = new Date(Date.now() + days * 864e5).toUTCString(); 34 document.cookie = 35 name + 36 "=" + 37 encodeURIComponent(value) + 38 "; expires=" + 39 expires + 40 "; path=/; SameSite=Lax"; 41 } 42 43 function getCookie(name) { 44 const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)")); 45 return match ? decodeURIComponent(match[2]) : ""; 46 } 47 48 function createSessionId() { 49 if (window.crypto && window.crypto.getRandomValues) { 50 const bytes = new Uint8Array(13); 51 window.crypto.getRandomValues(bytes); 52 53 return Array.from(bytes, function (byte) { 54 return byte.toString(16).padStart(2, "0"); 55 }).join("").slice(0, 25); 56 } 57 58 return Array.from({ length: 25 }, function () { 59 return Math.floor(Math.random() * 16).toString(16); 60 }).join(""); 61 } 62 63 function getSessionId() { 64 let sid = getCookie("casefox_chat_session_id"); 65 66 if (!sid || sid.length < 8) { 67 sid = createSessionId(); 68 setCookie("casefox_chat_session_id", sid, 30); 69 } 70 71 return sid; 72 } 73 74 const sessionId = getSessionId(); 75 76 function getTime() { 77 return new Date().toLocaleTimeString([], { 78 hour: "2-digit", 79 minute: "2-digit" 80 }); 81 } 82 83 function escapeHtml(str) { 84 if (!str) return ""; 85 86 const div = document.createElement("div"); 87 div.textContent = String(str); 88 89 return div.innerHTML; 90 } 91 92 function formatAssistantMessage(rawText) { 93 if (!rawText) return ""; 94 95 let safeText = String(rawText).replace(/[&<>]/g, function (m) { 96 if (m === "&") return "&"; 97 if (m === "<") return "<"; 98 if (m === ">") return ">"; 99 return m; 100 }); 101 102 safeText = safeText.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, function (match, linkText, url) { 103 const cleanText = escapeHtml(linkText); 104 const cleanUrl = escapeHtml(url).replace(/"/g, """).replace(/'/g, "'"); 105 106 return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer">${cleanText}</a>`; 107 }); 108 109 safeText = safeText.replace(/(https?:\/\/[^\s<]+)(?![^<]*>|[^<]*<\/a>)/g, function (url) { 110 const cleanUrl = escapeHtml(url).replace(/"/g, """).replace(/'/g, "'"); 111 112 return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer">${cleanUrl}</a>`; 113 }); 114 115 return safeText.replace(/\n/g, "<br>"); 116 } 117 118 function formatUserMessage(text) { 119 return escapeHtml(text).replace(/\n/g, "<br>"); 120 } 121 122 function scrollToBottom() { 123 chatBody.scrollTop = chatBody.scrollHeight; 124 } 125 126 function removeTyping() { 127 const el = document.getElementById("cfxTypingIndicator"); 128 if (el) el.remove(); 129 } 130 131 function showTyping() { 132 removeTyping(); 133 134 const typingDiv = document.createElement("div"); 135 typingDiv.id = "cfxTypingIndicator"; 136 typingDiv.className = "cfx-typing"; 137 typingDiv.innerHTML = ` 138 CaseFox AI 139 <span class="dot"></span>
140 <span class="dot"></span> 141 <span class="dot"></span> 142 `; 143 144 chatBody.appendChild(typingDiv); 145 scrollToBottom(); 146 } 147 148 function addMessage(role, text, addToHistory) { 149 const saveToHistory = addToHistory !== false; 150 151 const msgDiv = document.createElement("div"); 152 msgDiv.className = `cfx-message ${role === "user" ? "cfx-user" : "cfx-bot"}`; 153 154 msgDiv.innerHTML = 155 role === "assistant" 156 ? formatAssistantMessage(text) 157 : formatUserMessage(text); 158 159 const timeSpan = document.createElement("span"); 160 timeSpan.className = "cfx-time"; 161 timeSpan.textContent = getTime(); 162 163 msgDiv.appendChild(timeSpan); 164 chatBody.appendChild(msgDiv); 165 scrollToBottom(); 166 167 if (saveToHistory) { 168 chatHistory.push({ 169 role: role === "user" ? "user" : "assistant", 170 content: text 171 }); 172 } 173 } 174 175 function isValidEmail(email) { 176 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); 177 } 178 179 function showLeadForm(lead) { 180 document.querySelectorAll(".cfx-lead-form").forEach(function (form) { 181 form.remove(); 182 }); 183 184 const safeLead = lead || {}; 185 const nameMissing = !safeLead.name; 186 const emailMissing = !safeLead.email; 187 188 if (!nameMissing && !emailMissing) return; 189 190 const form = document.createElement("div"); 191 form.className = "cfx-lead-form"; 192 193 let title = "Please share your name and email so our team can contact you."; 194 195 if (!nameMissing && emailMissing) { 196 title = "Please share your email so our team can contact you."; 197 } 198 199 if (nameMissing && !emailMissing) { 200 title = "Please share your name so our team can contact you."; 201 } 202 203 form.innerHTML = ` 204 <div class="cfx-lead-form-title">${escapeHtml(title)}</div> 205 206 <div class="cfx-lead-error" id="cfxLeadError"> 207 Please enter valid details. 208 </div> 209 210 ${ 211 nameMissing 212 ? `<input type="text" id="cfxLeadName" placeholder="Your name" autocomplete="name">` 213 : "" 214 } 215 216 ${ 217 emailMissing 218 ? `<input type="email" id="cfxLeadEmail" placeholder="Your email" autocomplete="email">` 219 : "" 220 } 221 222 <button type="button" id="cfxLeadSubmit"> 223 Submit 224 </button> 225 `; 226 227 chatBody.appendChild(form); 228 scrollToBottom(); 229 230 const submitBtn = document.getElementById("cfxLeadSubmit"); 231 const errorBox = document.getElementById("cfxLeadError"); 232 233 if (!submitBtn || !errorBox) return; 234 235 submitBtn.addEventListener("click", function () { 236 const nameInput = document.getElementById("cfxLeadName"); 237 const emailInput = document.getElementById("cfxLeadEmail"); 238 239 const name = nameInput ? nameInput.value.trim() : safeLead.name; 240 const email = emailInput ? emailInput.value.trim() : safeLead.email; 241 242 errorBox.style.display = "none"; 243 244 if (nameMissing && !name) { 245 errorBox.textContent = "Please enter your name."; 246 errorBox.style.display = "block"; 247 248 if (nameInput) nameInput.focus(); 249 return; 250 } 251 252 if (emailMissing && !isValidEmail(email)) { 253 errorBox.textContent = "Please enter a valid email address."; 254 errorBox.style.display = "block"; 255 256 if (emailInput) emailInput.focus(); 257 return; 258 } 259 260 let message = ""; 261 262 if (nameMissing && emailMissing) { 263 message = `My name is ${name} and my email is ${email}`; 264 } else if (emailMissing) { 265 message = `My email is ${email}`; 266 } else if (nameMissing) { 267 message = `My name is ${name}`; 268 } 269 270 form.remove(); 271 sendMessage(message); 272 }); 273 } 274 275 async function sendMessage(userMsg) { 276 const message = String(userMsg || "").trim(); 277 278 if (isWaiting || !message) return; 279 280 if (currentQuickContainer) { 281 currentQuickContainer.style.display = "none"; 282 } 283 284 addMessage("user", message, true); 285 286 inputEl.value = ""; 287 isWaiting = true; 288 inputEl.disabled = true; 289 sendBtn.disabled = true; 290 sendBtn.style.opacity = "0.6"; 291 292 showTyping(); 293 294 try { 295 const payload = { 296 session_id: sessionId, 297 message: message, 298 history: chatHistory.slice(0, -1) 299 }; 300 301 const resp = await fetch(API_URL, { 302 method: "POST", 303 headers: { 304 "Content-Type": "application/json" 305 }, 306 body: JSON.stringify(payload) 307 }); 308 309 removeTyping(); 310 311 if (!resp.ok) { 312 throw new Error("HTTP " + resp.status); 313 } 314 315 const data = await resp.json(); 316 317 let botReply = "Thank you for reaching out. Could you rephrase?"; 318 319 if (data && typeof data.message === "string" && data.message.trim()) { 320 botReply = data.message.trim(); 321 } else if (data && typeof data.reply === "string" && data.reply.trim()) { 322 botReply = data.reply.trim(); 323 } 324 325 addMessage("assistant", botReply, true); 326 327 if ( 328 data && 329 data.success === true && 330 data.lead_status === "incomplete" && 331 data.lead 332 ) { 333 showLeadForm(data.lead); 334 } 335 } catch (error) { 336 removeTyping(); 337 addMessage("assistant", "â ï¸ Connection issue. Please try again later.", true); 338 } finally { 339 isWaiting = false; 340 inputEl.disabled = false; 341 sendBtn.disabled = false; 342 sendBtn.style.opacity = "1"; 343 inputEl.focus(); 344 scrollToBottom(); 345 } 346 } 347 348 function isReturningVisitor() { 349 return getCookie("cfx_visited") === "true"; 350 } 351 352 function markVisitorAsReturning() {
353 setCookie("cfx_visited", "true", 365); 354 } 355 356 function buildInitialUI() { 357 chatBody.innerHTML = ""; 358 currentQuickContainer = null; 359 360 const badge = document.createElement("div"); 361 badge.className = "cfx-ai-badge"; 362 badge.innerHTML = '<i class="bi bi-robot"></i> CaseFox Assistant'; 363 364 chatBody.appendChild(badge); 365 scrollToBottom(); 366 } 367 368 function showQuickReplies() { 369 if (currentQuickContainer) { 370 currentQuickContainer.remove(); 371 currentQuickContainer = null; 372 } 373 374 const quickDiv = document.createElement("div"); 375 quickDiv.className = "cfx-quick-replies"; 376 quickDiv.id = "cfxQuickContainer"; 377 378 const options = ["Considering CaseFox for my firm", "Just exploring options", "Need support", "Ready to switch to CaseFox", "Book Demo"]; 379 380 options.forEach(function (opt) { 381 const btn = document.createElement("button"); 382 btn.type = "button"; 383 btn.textContent = opt; 384 385 btn.addEventListener("click", function () { 386 if (!isWaiting) { 387 quickDiv.style.display = "none"; 388 sendMessage(opt); 389 } 390 }); 391 392 quickDiv.appendChild(btn); 393 }); 394 395 chatBody.appendChild(quickDiv); 396 currentQuickContainer = quickDiv; 397 scrollToBottom(); 398 } 399 400 function addAutoOpenMessages() { 401 if (autoMessageRunning || introStarted) return; 402 403 autoMessageRunning = true; 404 introStarted = true; 405 406 const returning = isReturningVisitor(); 407 408 if (returning) { 409 setTimeout(function () { 410 if (!widget.classList.contains("active")) return; 411 412 addMessage( 413 "assistant", 414 "Welcome back! I'm CaseFox AI. ð ", 415 false 416 ); 417 }, 800); 418 419 setTimeout(function () { 420 if (!widget.classList.contains("active")) return; 421 422 addMessage( 423 "assistant", 424 "CaseFox is Trusted by 25,000+ legal professionals since 2011 â I'm here to help you figure out if CaseFox is right for your firm.", 425 false 426 ); 427 }, 1500); 428 429 setTimeout(function () { 430 if (!widget.classList.contains("active")) return; 431 432 addMessage( 433 "assistant", 434 "What brings you here today?", 435 false 436 ); 437 }, 2100); 438 439 setTimeout(function () { 440 if (!widget.classList.contains("active")) return; 441 442 showQuickReplies(); 443 autoMessageRunning = false; 444 }, 2300); 445 } else { 446 setTimeout(function () { 447 if (!widget.classList.contains("active")) return; 448 449 addMessage( 450 "assistant", 451 "Hey there! Welcome to CaseFox. ð", 452 false 453 ); 454 }, 700); 455 456 setTimeout(function () { 457 if (!widget.classList.contains("active")) return; 458 459 addMessage( 460 "assistant", 461 "Trusted by 25,000+ legal professionals since 2011 â I'm here to help you figure out if CaseFox is right for your firm.", 462 false 463 ); 464 }, 1600); 465 466 setTimeout(function () { 467 if (!widget.classList.contains("active")) return; 468 469 addMessage( 470 "assistant", 471 "What brings you here today?", 472 false 473 ); 474 }, 2500); 475 476 setTimeout(function () { 477 if (!widget.classList.contains("active")) return; 478 479 showQuickReplies(); 480 markVisitorAsReturning(); 481 autoMessageRunning = false; 482 }, 3300); 483 } 484 } 485 486 function openWidget() { 487 widget.classList.add("active"); 488 toggleBtn.innerHTML = '<i class="bi bi-x-lg"></i>'; 489 490 setTimeout(function () { 491 inputEl.focus(); 492 }, 120); 493 } 494 495 function closeWidget() { 496 widget.classList.remove("active"); 497 toggleBtn.innerHTML = '<i class="bi bi-chat-dots-fill"></i>'; 498 } 499 500 function toggleWidget() { 501 if (widget.classList.contains("active")) { 502 closeWidget(); 503 return; 504 } 505 506 openWidget(); 507 508 if (autoOpenTimer) { 509 clearTimeout(autoOpenTimer); 510 autoOpenTimer = null; 511 } 512 513 if (!introStarted && chatBody.querySelectorAll(".cfx-message").length === 0) { 514 addAutoOpenMessages();
515 } 516 } 517 518 function openWidgetByAuto() { 519 if (isMobileView()) return; 520 if (widget.classList.contains("active")) return; 521 if (introStarted) return; 522 523 openWidget(); 524 addAutoOpenMessages(); 525 } 526 527 function resetConversation() { 528 if (isWaiting) return; 529 530 chatHistory = []; 531 autoMessageRunning = false; 532 introStarted = false; 533 534 if (autoOpenTimer) { 535 clearTimeout(autoOpenTimer); 536 autoOpenTimer = null; 537 } 538 539 document.querySelectorAll(".cfx-lead-form").forEach(function (form) { 540 form.remove(); 541 }); 542 543 buildInitialUI(); 544 545 if (widget.classList.contains("active")) { 546 addAutoOpenMessages(); 547 } 548 } 549 550 function initWidget() { 551 buildInitialUI(); 552 553 toggleBtn.addEventListener("click", toggleWidget); 554 closeBtn.addEventListener("click", closeWidget); 555 556 sendBtn.addEventListener("click", function () { 557 sendMessage(inputEl.value); 558 }); 559 560 inputEl.addEventListener("keydown", function (e) { 561 if (e.key === "Enter" && !e.shiftKey) { 562 e.preventDefault(); 563 sendMessage(inputEl.value); 564 } 565 }); 566 567 resetBtn.addEventListener("click", resetConversation); 568 569 autoOpenTimer = setTimeout(openWidgetByAuto, AUTO_OPEN_DELAY); 570 } 571 572 if (document.readyState === "loading") { 573 document.addEventListener("DOMContentLoaded", initWidget); 574 } else { 575 initWidget(); 576 } 577})();
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.