PageSourceSearch

https://admin.casefox.com/api/storage/chatbot.js

js casefox.com collected 2026-10-01 11:49:50 UTC 15,477 bytes, 577 lines download raw bytes

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 "&amp;";
97      if (m === "<") return "&lt;";
98      if (m === ">") return "&gt;";
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, "&quot;").replace(/'/g, "&#39;");
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, "&quot;").replace(/'/g, "&#39;");
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.