PageSourceSearch

https://aadsummit.com/wp-content/plugins/generis-chatbot/chatbot.js?ver=12.6.0

js aadsummit.com collected 2026-10-06 22:52:44 UTC 34,870 bytes, 932 lines download raw bytes

1/* aapanel.js — v12.6.0 (strict EU/US contact selection + cancellation override + credentials intent) */
2console.log("Chatbot v12.6.0 — strict EU/US contact selection + cancellation override");
3
4const initialFormData = {
5  "end user/solution provider": null,
6  "full name": null,
7  "job title": null,
8  "company name": null,
9  "work email": null,
10  "phone number": null,
11  "how did you hear about the event": null,
12};
13
14document.addEventListener("DOMContentLoaded", function () {
15  // ===== State =====
16  let messages = [];
17  let formData = initialFormData;
18  const event = eventInfo();
19  let isLoading = false;
20  let formAlreadySubmitted = false;
21
22
23  // 🔹 Force all text in USER bubbles to white (including any auto-linked emails)
24  (function injectUserBubbleColorStyle() {
25    try {
26      const style = document.createElement("style");
27      style.textContent = `
28        .message.user,
29        .message.user * {
30          color: #ffffff !important;
31        }
32      `;
33      document.head.appendChild(style);
34    } catch (e) {
35      console.warn("Could not inject user bubble color style:", e);
36    }
37  })();
38
39  // ===== KB loader (resilient) =====
40  const FALLBACK_US_BLOCK =
41    "For General Inquiries: [email protected],\n\n" +
42    "For Delegates: [email protected],\n\n" +
43    "For Sponsors: [email protected],\n\n" +
44    "For Speaker Info: [email protected],\n\n" +
45    "For Sales: [email protected]";
46
47  const FALLBACK_EU_BLOCK =
48    "For General Inquiries: [email protected],\n\n" +
49    "For Delegates: [email protected],\n\n" +
50    "For Sponsors: [email protected],\n\n" +
51    "For Speaker Info: [email protected],\n\n" +
52    "For Sales: [email protected]";
53
54  const KB_CANDIDATES = [
55    () => window.GENERIS_KB_URL,
56    () => "/wp-content/uploads/kb.json",
57  ];
58
59  let KB_CACHE = null;
60  async function getKB() {
61    if (KB_CACHE) return KB_CACHE;
62    for (const f of KB_CANDIDATES) {
63      const url = f();
64      if (!url) continue;
65      try {
66        const res = await fetch(url, { credentials: "same-origin" });
67        if (!res.ok) throw new Error(`HTTP ${res.status}`);
68        KB_CACHE = await res.json();
69        console.log("[KB] Loaded from:", url);
70        return KB_CACHE;
71      } catch (e) {
72        console.warn("[KB] Failed from candidate:", url, e);
73      }
74    }
75    console.warn("[KB] All sources failed. Using empty general.");
76    KB_CACHE = { general: {} };
77    return KB_CACHE;
78  }
79  // Warm ASAP
80  getKB().catch(() => {});
81
82  // ===== EU detection =====
83  const EU_EVENT_CODES = new Set(["EUBIO", "EMS", "ESC", "EPOS", "EMD", "EFMS", "EAD", "EPS"]);
84  function isEuEvent(code) {
85    return EU_EVENT_CODES.has(String(code || "").toUpperCase());
86  }
87
88  // ===== UTM helpers =====
89  const UTM_PARAMS = {
90    utm_source: "chatbot",
91    utm_medium: "chatbot-app",
92    utm_campaign: "generis_chatbot",
93  };
94  function addOrUpdateUtm(url) {
95    try {
96      if (/^(mailto:|tel:)/i.test(url)) return url;
97      const resolved = new URL(url, window.location.origin);
98      if (!/^https?:$/i.test(resolved.protocol)) return url;
99      Object.entries(UTM_PARAMS).forEach(([k, v]) => resolved.searchParams.set(k, v));
100      return resolved.toString();
101    } catch {
102      return url;
103    }
104  }
105  function processHtmlLinks(html) {
106    if (!html) return html;
107    const temp = document.createElement("div");
108    temp.innerHTML = String(html);
109    temp.querySelectorAll("a[href]").forEach((a) => {
110      const href = a.getAttribute("href") || "";
111      if (/^(mailto:|tel:)/i.test(href)) return;
112      a.setAttribute("href", addOrUpdateUtm(href));
113    });
114    return temp.innerHTML;
115  }
116
117  // ===== Linkify (emails + bare URLs) =====
118  function linkifyEmailsAndUrls(html) {
119    const temp = document.createElement("div");
120    temp.innerHTML = String(html || "");
121    const walker = document.createTreeWalker(temp, NodeFilter.SHOW_TEXT, null);
122
123    const emailRe = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
124    const urlOrDomainRe =
125      /\b(?:https?:\/\/[^\s<]+|www\.[^\s<]+|(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?:\/[^\s<]*)?)/gi;
126    const trailingPunctRe = /[),.;:!?]+$/;
127
128    const mkA = (href, text) => {
129      const a = document.createElement("a");
130      a.href = href;
131
132      if (!/^mailto:/i.test(href)) {
133        a.target = "_blank";
134        a.rel = "noopener";
135      }
136
137      // ✅ If this is the booth order form, use a nice label
138      if (/booth-order-form/i.test(href)) {
139        a.textContent = "Booth Order Form";
140      } else {
141        a.textContent = text;
142      }
143
144      a.style.textDecoration = "underline";
145      // inherit color so in user bubble it becomes white via CSS
146      a.style.color = "inherit";
147      return a;
148    };
149
150    const replacements = [];
151    let node;
152    while ((node = walker.nextNode())) {
153      if (node.parentElement && node.parentElement.closest("a")) continue;
154      const txt = node.nodeValue;
155      const frag = document.createDocumentFragment();
156
157      // Pass 1: emails
158      let i = 0;
159      emailRe.lastIndex = 0;
160      let em;
161      const chunks = [];
162      while ((em = emailRe.exec(txt))) {
163        if (em.index > i) chunks.push({ t: "text", v: txt.slice(i, em.index) });
164        chunks.push({ t: "email", v: em[0] });
165        i = em.index + em[0].length;
166      }
167      if (i < txt.length || chunks.length === 0) chunks.push({ t: "text", v: txt.slice(i) });
168
169      // Pass 2: urls/domains
170      chunks.forEach((c) => {
171        if (c.t === "email") {
172          frag.appendChild(mkA(`mailto:${c.v}`, c.v));
173          return;
174        }
175        const s = c.v;
176        let last = 0;
177        urlOrDomainRe.lastIndex = 0;
178        let um;
179        while ((um = urlOrDomainRe.exec(s))) {
180          const m = um[0];
181          if (m.includes("@")) continue;
182          if (um.index > last) frag.appendChild(document.createTextNode(s.slice(last, um.index)));
183          const clean = m.replace(trailingPunctRe, "");
184          const href = /^https?:\/\//i.test(clean)
185            ? clean
186            : clean.startsWith("www.")
187            ? `http://${clean}`
188            : `http://${clean}`;
189          const text = clean.replace(/^https?:\/\/|^www\./i, "");
190          frag.appendChild(mkA(addOrUpdateUtm(href), text));
191          last = um.index + m.length;
192        }
193        if (last < s.length) frag.appendChild(document.createTextNode(s.slice(last)));
194      });
195
196      if (frag.childNodes.length) replacements.push({ node, frag });
197    }
198    replacements.forEach(({ node, frag }) => node.replaceWith(frag));
199    return temp.innerHTML;
200  }
201
202  // ===== Cancellation intent logic =====
203  const CANCELLATION_TOKENS = [
204    "cancel",
205    "cancellation",
206    "cancelling",
207    "canceling",
208    "can't make it",
209    "cant make it",
210    "cannot make it",
211    "unable to attend",
212    "won't be able to attend",
213    "wont be able to attend",
214    "no longer able to attend",
215    "can no longer attend",
216    "drop out",
217    "back out",
218  ];
219
220  function isCancellationIntent(raw) {
221    const q = String(raw || "").toLowerCase().trim();
222    if (!q) return false;
223
224    if (/\bcancel(l|ling|lation)?\b/.test(q)) return true;
225    if (CANCELLATION_TOKENS.some((t) => q.includes(t))) return true;
226    if (/\b(can(?:not|t)|won'?t|will not)\s+(attend|make it)\b/.test(q)) return true;
227
228    return false;
229  }
230
231  // ===== Credentials / login intent logic =====
232  const CREDENTIALS_TOKENS = [
233    "login credentials",
234    "log in credentials",
235    "credentials",
236    "username and password",
237    "username & password",
238    "catalog login",
239    "catalog credentials",
240    "catalog username",
241    "catalog password",
242    "summit login",
243    "summit credentials",
244    "event login",
245    "event credentials",
246    "meeting login",
247    "meeting credentials",
248    "where is my username",
249    "where is my password",
250    "where can i find my login",
251    "where do i find my login",
252    "lost my login",
253    "lost my credentials",
254    "forgot my login",
255    "forgot my password",
256    "forgot my username",
257    "reset login",
258    "reset credentials",
259  ];
260
261  function isCredentialsIntent(raw) {
262    const q = String(raw || "").toLowerCase().trim();
263    if (!q) return false;
264    return CREDENTIALS_TOKENS.some((t) => q.includes(t));
265  }
266
267  // ===== Contact logic =====
268  function isContactRequest(raw) {
269    const q = String(raw || "").toLowerCase().trim();
270    if (!q) return false;
271
272    // Never treat cancellation or credentials as generic contact requests
273    if (isCancellationIntent(q)) return false;
274    if (isCredentialsIntent(q)) return false;
275
276    // --- Explicit "talk to a rep/human" style patterns ---
277    const regs = [
278      /\b(can|could|may)\s+i\s+(speak|talk)\s+to\s+(a\s+)?(rep|representative|human|agent)\b/i,
279      /\b(connect|route|transfer)\s+me\s+(to|with)\s+(a\s+)?(person|rep|representative|human|agent)\b/i,
280      /\b(i\s+want|i'd like|let me)\s+to\s+(talk|speak)\s+(to|with)\s+(someone|a person|a rep|a representative)\b/i,
281      /\b(human|rep|representative|live\s+(person|agent)|operator)\s+(please|pls|now)?\b/i,
282    ];
283    if (regs.some((rx) => rx.test(q))) return true;
284
285    // --- Verb + human word combo ---
286    const verbs = ["speak", "talk", "contact", "connect", "reach", "get in touch", "call", "phone"];
287    const humanWordRe = /\b(someone|somebody|representative|rep|person|human|agent|operator|team|live agent|live person)\b/;
288
289    if (verbs.some((v) => q.includes(v)) && humanWordRe.test(q)) {
290      return true;
291    }
292
293    // --- Very short messages like "human please", "rep", etc. ---
294    const strongHumanRe = /\b(rep|representative|human|agent|operator)\b/;
295    if (q.split(/\s+/).length <= 6 && strongHumanRe.test(q)) {
296      return true;
297    }
298
299    // --- Flat phrases (multi-word); NONE rely on substring 'rep' anymore ---
300    const flat = [
301      "contact someone",
302      "contact a person",
303      "talk to a person",
304      "speak to a person",
305      "speak to a representative",
306      "talk to a representative",
307      "representative",
308      "human",
309      "agent",
310      "real person",
311      "live person",
312      "live agent",
313      "operator",
314      "customer service",
315      "customer support",
316      "support agent",
317      "help desk",
318      "connect me",
319      "connect me with someone",
320      "get me a human",
321      "human please",
322      "can i contact someone",
323      "can i speak to someone",
324      "can i speak to a rep",
325      "can you connect me",
326      "connect to someone",
327      "connect with a person",
328      "talk to someone",
329      "speak to someone",
330      "talk with someone",
331      "speak with someone",
332      "call someone",
333      "call a rep",
334      "phone number",
335      "talk to a human",
336    ];
337
338    return flat.some((t) => q.includes(t));
339  }
340
341  function formatContactList(blob) {
342    if (!blob) return "";
343    const parts = blob
344      .split(/\n{2,}|,(?!\s*https?:)/)
345      .map((s) => s.trim())
346      .filter(Boolean);
347
348    const rows = parts.map((cur) => {
349      if (cur.includes(":")) {
350        const [label, email] = cur.split(":").map((s) => s.trim());
351        const nice = label ? label[0].toUpperCase() + label.slice(1) : label;
352        return `${nice}:<br>${email}`;
353      }
354      return cur;
355    });
356    return `Here are the best contacts:<br><br>${rows.join("<br><br>")}`;
357  }
358
359  // STRICT: EU summits => eucontact_emails; American => contact_emails
360  function pickContactsFromKBSync(eventCode, lastUserText) {
361    const code = String(eventCode || "").toUpperCase();
362    const kb = KB_CACHE || { general: {} };
363    const general = kb.general || {};
364    const ev = kb[code] || {};
365
366    const q = String(lastUserText || "").toLowerCase();
367    const userEU = /\b(eu|europe|european)\b/.test(q);
368    const userUS = /\b(us|u\.s\.|usa|american|north america)\b/.test(q);
369    const euSite = isEuEvent(code);
370
371    const wantEU = userEU || (!userUS && euSite);
372    const wantUS = userUS || (!userEU && !euSite);
373
374    const evEU = ev.eucontact_emails;
375    const evUS = ev.contact_emails;
376    const genEU = general.eucontact_emails;
377    const genUS = general.contact_emails;
378
379    let chosen;
380    if (wantEU) {
381      chosen = evEU || genEU || FALLBACK_EU_BLOCK;
382      console.log(
383        "[Contacts] strict: EU flow | picked:",
384        evEU ? "event.eucontact_emails" : genEU ? "general.eucontact_emails" : "FALLBACK_EU_BLOCK"
385      );
386    } else if (wantUS) {
387      chosen = evUS || genUS || FALLBACK_US_BLOCK;
388      console.log(
389        "[Contacts] strict: US flow | picked:",
390        evUS ? "event.contact_emails" : genUS ? "general.contact_emails" : "FALLBACK_US_BLOCK"
391      );
392    } else {
393      chosen = genUS || genEU || FALLBACK_US_BLOCK;
394      console.log("[Contacts] strict: unknown region — defaulted.");
395    }
396
397    return chosen;
398  }
399
400  // ===== Post-render hard enforcement =====
401  const chatWindow = document.getElementById("chat-window-messages");
402  const assistantBubbleSelector = ".message.assistant p";
403
404  const CONTACT_LABEL_MARKERS = [
405    "here are the best contacts",
406    "for general inquiries:",
407    "for delegates:",
408    "for sponsors:",
409    "for speaker info:",
410    "for sales:",
411  ];
412
413  const KNOWN_EMAIL_MARKERS = [
414    "@generisgp.com",
415    "[email protected]",
416    "[email protected]",
417    "[email protected]",
418    "[email protected]",
419    "[email protected]",
420    "[email protected]",
421  ];
422
423  function looksLikeContactBlock(htmlLower) {
424    // Never rewrite messages that are about cancelling
425    if (isCancellationIntent(htmlLower)) return false;
426    // Never rewrite credentials / login answers either
427    if (isCredentialsIntent(htmlLower)) return false;
428
429    const hasLabels = CONTACT_LABEL_MARKERS.some((m) => htmlLower.includes(m));
430    const hasEmails = KNOWN_EMAIL_MARKERS.some((m) => htmlLower.includes(m));
431
432    return hasLabels || hasEmails;
433  }
434
435  function rewriteAssistantContactsInNode(pNode) {
436    try {
437      const lastUser = messages.slice().reverse().find((m) => m.role === "user")?.content || "";
438      const chosenBlob = pickContactsFromKBSync((event || {}).code, lastUser);
439      const html = formatContactList(chosenBlob);
440
441      pNode.innerHTML = `<strong>Genie: </strong> ${linkifyEmailsAndUrls(
442        processHtmlLinks(html.replace(/\n/g, "<br>"))
443      )}`;
444      return true;
445    } catch (e) {
446      console.warn("[Contacts] rewrite failed:", e);
447      return false;
448    }
449  }
450
451  if (chatWindow) {
452    const mo = new MutationObserver((mutations) => {
453      mutations.forEach((m) => {
454        m.addedNodes.forEach((node) => {
455          if (!(node instanceof Element)) return;
456
457          const ps =
458            node.matches && node.matches(assistantBubbleSelector)
459              ? [node]
460              : Array.from(node.querySelectorAll(assistantBubbleSelector));
461
462          ps.forEach((p) => {
463            const raw = (p.textContent || p.innerText || "").toLowerCase();
464            if (looksLikeContactBlock(raw)) {
465              const ok = rewriteAssistantContactsInNode(p);
466              if (ok) console.log("[Contacts] Post-render override applied (strict).");
467            }
468          });
469        });
470      });
471    });
472    mo.observe(chatWindow, { childList: true, subtree: true });
473  } else {
474    console.warn("chatWindow not found — contact override observer not attached.");
475  }
476
477  // ===== Rest of UI wiring =====
478  const inputField = document.getElementById("chatbot-input");
479  const sendButton = document.getElementById("chatbot-send");
480  const openChatButton = document.getElementById("chat-button");
481  const closeButton = document.getElementById("chat-close");
482  const chatContainer = document.querySelector(".x-dropdown.chat-modal");
483
484  const isWpAdminBar = !!document.getElementById("wpadminbar");
485  const isWpLoggedIn = /(^|;\s)wordpress_logged_in_/.test(document.cookie);
486  const isEditor = isWpAdminBar || isWpLoggedIn;
487  if (isEditor) console.log("User is wp-admin");
488
489  if (sendButton) sendButton.addEventListener("click", sendMessage);
490  else console.log("Could not find element with id: chatbot-send");
491
492  if (inputField) {
493    inputField.addEventListener("keypress", function (e) {
494      if (e.key === "Enter") sendMessage();
495    });
496  } else {
497    console.log("Could not find element with id: chatbot-input");
498  }
499
500  if (openChatButton) {
501    openChatButton.addEventListener("click", () => {
502      if (messages.length === 0) openingMessage();
503    });
504  } else {
505    console.log("Could not find element with id: chatbot-button");
506  }
507
508  if (closeButton) {
509    closeButton.addEventListener("click", () => {
510      toggleChatVisibility();
511      localStorage.setItem("chatClosedOnce", "true");
512      console.log("chatbot-close saved to localstorage");
513    });
514  } else {
515    console.log("Could not find element with id: chatbot-close");
516  }
517
518  setTimeout(() => {
519    const KEY = "chatbotCooldown";
520    const now = Date.now();
521    const cooldown = localStorage.getItem(KEY);
522    const hasClosedChat = localStorage.getItem("chatClosedOnce");
523    const shouldOpen =
524      chatContainer &&
525      !chatContainer.classList.contains("x-active") &&
526      !isEditor &&
527      !hasClosedChat &&
528      (!cooldown || now >= parseInt(cooldown, 10));
529
530    if (shouldOpen) {
531      toggleChatVisibility();
532      const expiry = now + 80 * 1000;
533      localStorage.setItem(KEY, expiry.toString());
534    }
535  }, 8000);
536
537  async function toggleChatVisibility() {
538    if (!chatContainer) return;
539    const isChatOpen = chatContainer.classList.contains("x-active");
540    if (isChatOpen) {
541      chatContainer.classList.remove("x-active");
542      chatContainer.classList.remove("x-active-animate");
543    } else {
544      chatContainer.classList.add("x-active");
545      chatContainer.classList.add("x-active-animate");
546    }
547    if (messages.length === 0) openingMessage();
548  }
549
550  function openingMessage() {
551    if (isLoading) return;
552    isLoading = true;
553    const { response, messages: newMessages } = grabOpeningMessages(event["name"]);
554    messages = newMessages;
555    updateChatUI(response, "assistant");
556    isLoading = false;
557  }
558
559  // ===== Chat send =====
560  async function sendMessage() {
561    if (isLoading) return;
562
563    const userMessage = (inputField?.value || "").trim();
564    if (!userMessage) return;
565
566    // 🔹 Early intercept: credentials / username & password questions
567    if (isCredentialsIntent(userMessage)) {
568      messages.push({ role: "user", content: userMessage });
569      updateChatUI(userMessage, "user");
570      if (inputField) inputField.value = "";
571
572      try {
573        const kb = await getKB().catch(() => ({ general: {} }));
574        const general = (kb && kb.general) || {};
575        const credentialsAnswer =
576          general.credentials ||
577          'You can locate your username and password in any of the following emails sent from [email protected]: “Welcome Details,” “Welcome & Choose Your Meetings,” “Access the Catalog,” or “Past Due.” If you don’t see these emails in your primary inbox, please check your junk or spam folder.';
578
579        messages.push({ role: "assistant", content: credentialsAnswer });
580        updateChatUI(credentialsAnswer, "assistant");
581      } catch (e) {
582        console.warn("Credentials KB lookup failed:", e);
583      }
584
585      return;
586    }
587
588    // 🔹 Early intercept: cancellation questions (use general.cancellation)
589    if (isCancellationIntent(userMessage)) {
590      messages.push({ role: "user", content: userMessage });
591      updateChatUI(userMessage, "user");
592      if (inputField) inputField.value = "";
593
594      try {
595        const kb = await getKB().catch(() => ({ general: {} }));
596        const general = (kb && kb.general) || {};
597        const cancellationAnswer =
598          general.cancellation ||
599          "If you need to cancel last-minute, please reach out to us as soon as possible so we can assist you. You can contact our team at [email protected] for further instructions.";
600
601        messages.push({ role: "assistant", content: cancellationAnswer });
602        updateChatUI(cancellationAnswer, "assistant");
603      } catch (e) {
604        console.warn("Cancellation KB lookup failed:", e);
605        const fallback =
606          "If you need to cancel last-minute, please reach out to us as soon as possible so we can assist you. You can contact our team at [email protected] for further instructions.";
607        messages.push({ role: "assistant", content: fallback });
608        updateChatUI(fallback, "assistant");
609      }
610
611      return;
612    }
613
614    // Early intercept: user wants a human (strict contacts)
615    if (isContactRequest(userMessage)) {
616      messages.push({ role: "user", content: userMessage });
617      updateChatUI(userMessage, "user");
618      if (inputField) inputField.value = "";
619
620      await getKB().catch(() => ({}));
621      const chosen = pickContactsFromKBSync((event || {}).code, userMessage);
622      updateChatUI(formatContactList(chosen), "assistant");
623      return;
624    }
625
626    console.groupCollapsed(`🟦 USER MESSAGE: "${userMessage}"`);
627    console.time("Chatbot message roundtrip");
628
629    messages.push({ role: "user", content: userMessage });
630    updateChatUI(userMessage, "user");
631    if (inputField) inputField.value = "";
632
633    const loadingBox = onLoading();
634
635    try {
636      isLoading = true;
637      const response = await fetch("https://chatbot.generisgp.com/chatbot", {
638        method: "POST",
639        headers: { "Content-Type": "application/json" },
640        body: JSON.stringify({
641          messages: messages,
642          form_data: formData,
643          event: event,
644        }),
645      });
646
647      console.log("🔄 API status:", response.status, response.statusText);
648      if (!response.ok) throw new Error(`API Error: ${response.statusText}`);
649
650      const botReply = await response.json();
651      console.log("📥 Received from API:", botReply);
652
653      messages = botReply.messages;
654      formData = botReply.form_data;
655
656      if (loadingBox) loadingBox.remove();
657      isLoading = false;
658      
659      console.groupCollapsed("📋 Updated Form Data After Message");
660      console.table(formData);
661      console.groupEnd();
662
663      if (/ready to submit/i.test(botReply.response)) {
664        if (formDataValid() && !formAlreadySubmitted) {
665          formAlreadySubmitted = true;
666          const convertedFormData = convertFormData(formData);
667
668          updateChatUI(
669            "Perfect! I now have all your information. Submitting your access request now...",
670            "assistant"
671          );
672
673          try {
674            updateChatUI(
675              "✅ Your request has been submitted! We’ll reach out to you soon. Is there anything else I can help you with?",
676              "assistant"
677            );
678            await submitAccessRequest(convertedFormData);
679            clearFormData();
680          } catch (error) {
681            console.error("❌ Auto-submission failed:", error);
682            formAlreadySubmitted = false;
683            updateChatUI("Something went wrong submitting your request. Please try again later.", "assistant");
684          }
685        }
686      } else {
687        let displayText = botReply.response;
688
689        // Keep old behavior for access links, BUT
690        // don't treat booth-order-form URLs as access-request links.
691        const structuredLink = botReply.access_link || extractFirstUrl(displayText);
692        const isBoothFormLink = structuredLink && /booth-order-form/i.test(structuredLink);
693
694        // Only strip the inline access-request text when it's NOT a booth link
695        if (structuredLink && !isBoothFormLink) {
696          displayText = sanitizeAccessInText(displayText);
697        }
698
699        updateChatUI(linkifyEmailsAndUrls(displayText), "assistant");
700
701        // Only append the "Access Request" bubble for *real* access links
702        if (structuredLink && !isBoothFormLink) {
703          appendAccessInfoLine(addOrUpdateUtm(structuredLink), "Access Request");
704        }
705      }
706
707      if (botReply.request_access) {
708        if (formDataValid() && !formAlreadySubmitted) {
709          formAlreadySubmitted = true;
710          const convertedFormData = convertFormData(formData);
711          await submitAccessRequest(convertedFormData);
712          clearFormData();
713          updateChatUI(
714            "✅ Your request has been submitted! We’ll reach out to you soon. Is there anything else I can help you with?",
715            "assistant"
716          );
717        }
718      }
719    } catch (error) {
720      console.error("❌ Failed to communicate with chatbot API:", error);
721      if (loadingBox) loadingBox.remove();
722      updateChatUI("Sorry, I’m having trouble connecting right now.", "assistant");
723      isLoading = false;
724    }
725
726    console.timeEnd("Chatbot message roundtrip");
727    console.groupEnd();
728  }
729
730  // ===== UI helpers =====
731  function updateChatUI(message, sender) {
732    if (!chatWindow) {
733      console.error("Error: Chat window element not found.");
734      return;
735    }
736    const messageBox = document.createElement("div");
737    const messageElement = document.createElement("p");
738
739    if (sender === "assistant") {
740      // Assistant: HTML + linkify + UTM processing
741      let html = String(message).replace(/\n/g, "<br>");
742      html = linkifyEmailsAndUrls(html);
743      html = processHtmlLinks(html);
744      messageElement.innerHTML = `<strong>Genie: </strong> ${html}`;
745    } else {
746      // USER: plain text; will inherit white from CSS + inline style
747      const strong = document.createElement("strong");
748      strong.textContent = "You: ";
749      const span = document.createElement("span");
750      span.textContent = String(message);
751      messageElement.appendChild(strong);
752      messageElement.appendChild(span);
753
754      // Extra safety: inline color so entire user bubble text is white
755      messageBox.style.color = "#ffffff";
756    }
757
758    messageBox.className = `message ${sender}`;
759    messageBox.appendChild(messageElement);
760    chatWindow.appendChild(messageBox);
761    chatWindow.scrollTop = chatWindow.scrollHeight;
762  }
763
764  function sanitizeAccessInText(text) {
765    if (!text) return text;
766    const blockRegex = /(?:\r?\n|\r)?\s*For further detail,? you can request access here:\s*[\s\S]*?(\n\s*\n|$)/i;
767    let cleaned = String(text).replace(blockRegex, "$1");
768    const loneUrlLine = /(?:^|\n)\s*(https?:\/\/[^\s<]+)\s*(?=$|\n)/gi;
769    cleaned = cleaned.replace(loneUrlLine, "\n");
770    cleaned = cleaned.replace(/\n{3,}/g, "\n\n").trim();
771    return cleaned;
772  }
773
774  function extractFirstUrl(text) {
775    const urlRegex = /(https?:\/\/[^\s<]+)/i;
776    const m = (text || "").match(urlRegex);
777    return m ? m[1] : null;
778  }
779
780  function appendAccessInfoLine(link, label = "Access Request") {
781    if (!chatWindow) return;
782    const box = document.createElement("div");
783    box.className = "message assistant";
784    const p = document.createElement("p");
785    const utmLink = addOrUpdateUtm(link);
786    p.innerHTML = `<strong>Genie: </strong>For further information, submit an access request through this chatbot or visit: <a href="${utmLink}" target="_blank" rel="noopener" style="text-decoration: underline; color: #41a7fa;">${label}</a>`;
787    box.appendChild(p);
788    chatWindow.appendChild(box);
789    chatWindow.scrollTop = chatWindow.scrollHeight;
790  }
791
792  function convertFormData(formData) {
793    const parts = String(formData["full name"] || "").trim().split(/\s+/);
794    let firstName = "",
795      lastName = "";
796    if (parts.length < 2) firstName = parts[0] || "";
797    else {
798      firstName = parts.slice(0, -1).join(" ");
799      lastName = parts[parts.length - 1];
800    }
801    const { ["full name"]: _omit, ...rest } = formData;
802    return { ...rest, "first name": firstName, "last name": lastName };
803  }
804
805  function onLoading() {
806    if (!chatWindow) return;
807    const loadingBox = document.createElement("div");
808    const loadingMessage = document.createElement("p");
809    loadingMessage.innerHTML = "Genie's thinking...";
810    loadingBox.className = "message assistant loading";
811    loadingBox.appendChild(loadingMessage);
812    chatWindow.appendChild(loadingBox);
813    chatWindow.scrollTop = chatWindow.scrollHeight;
814    return loadingBox;
815  }
816
817  function formDataValid() {
818    return Object.values(formData).reduce((acc, field) => acc && field !== null, true);
819  }
820  function clearFormData() {
821    formData = initialFormData;
822  }
823
824  function eventInfo() {
825    const url = window.location.href;
826    if (url.includes("amdsummit")) return { code: "AMD", name: "American Medical Device Summit" };
827    else if (url.includes("eposummit")) return { code: "EPOS", name: "European Pharma Outsourcing Summit" };
828    else if (url.includes("biomanamerica")) return { code: "BIO", name: "American Biomanufacturing Summit" };
829    else if (url.includes("aadsummit")) return { code: "AAD", name: "American Aerospace & Defense Summit" };
830    else if (url.includes("usauto")) return { code: "AAS", name: "American Automotive Summit" };
831    else if (url.includes("supplychainus")) return { code: "ASC", name: "American Supply Chain Summit" };
832    else if (url.includes("posummit")) return { code: "PMOS", name: "American Pharma Manufacturing & Outsourcing Summit" };
833    else if (url.includes("uspack")) return { code: "APS", name: "American Packaging Summit" };
834    else if (url.includes("cioamerica")) return { code: "CIO", name: "American CIO & Cybersecurity Summit" };
835    else if (url.includes("foodmansummit")) return { code: "FMS", name: "American Food Manufacturing Summit" };
836    else if (url.includes("biomaneurope")) return { code: "EUBIO", name: "European Biomanufacturing Summit" };
837    else if (url.includes("manusummiteu")) return { code: "EMS", name: "European Manufacturing Summit" };
838    else if (url.includes("manusummit")) return { code: "AMS", name: "American Manufacturing Summit" };
839    else if (url.includes("supplychaineu")) return { code: "ESC", name: "European Supply Chain Summit" };
840    else if (url.includes("emdsummit")) return { code: "EMD", name: "European Medical Device Summit" };
841    else if (url.includes("foodmaneurope")) return { code: "EFMS", name: "European Food Manufacturing Summit" };
842    else if (url.includes("chemmansummit")) return { code: "CMS", name: "American Chemical Manufacturing Summit" };
843    else if (url.includes("eupacksummit")) return { code: "EPS", name: "European Packaging Summit" };
844    else if (url.includes("aerosummiteu")) return { code: "EAD", name: "European Aerospace & Defense Summit" };
845    else if (url.includes("electronicssummit")) return { code: "AES", name: "American Electronics & High-Tech Manufacturing Summit" };
846    else if (url.includes("eupacksummit")) return { code: "EPS", name: "European Packaging Summit" };
847    else return { code: "BIO", name: "American Biomanufacturing Summit" };
848  }
849
850  function grabOpeningMessages(eventName) {
851    const systemPrompt = `You are Genie, a helpful and professional AI assistant on the ${eventName} website for Generis Group.
852
853Generis Group is a full-service organizer of world-class business summits 
854for senior decision-makers and top-level solution providers across various industries.
855
856Your primary purpose is to:
8571. Answer general questions about Generis Group and its events.
8582. Help users submit access requests to view the program page for a specific summit.
859
860When assisting with access requests, ask for the required information one field at a time, ensuring a smooth and clear experience for the user.
861
862While maintaining a professional tone, you may occasionally incorporate light, quirky humor—like saying 
863“your wish is my command”—to reflect your name, Genie. However, do this sparingly to keep the conversation a
863ppropriate for a corporate environment.`;
864    const welcomeMessage = getOpener(eventName);
865    const messagesArr = [
866      { role: "system", content: systemPrompt },
867      { role: "assistant", content: welcomeMessage },
868    ];
869    return { response: welcomeMessage, messages: messagesArr };
870  }
871
872  function getOpener(eventName) {
873    const prompts = [
874      `Hello!👋 Welcome to the ${eventName}. I'm Genie, your AI assistant. Whether you have questions about Generis Group events or need help with an access request, I'm here to assist. How can I help you today?`,
875      `Hi there! I'm Genie🧞, here to assist with anything you need—whether it's submitting an access request or answering questions about Generis Group and our events. How can I assist you today?`,
876      `Hello! I'm Genie😊, your friendly assistant for the ${eventName}. If you have questions or need help with an access request, I'm here to assist. How can I help today?`,
877      `Welcome to the ${eventName}! I'm Genie, your virtual assistant, ready to help with access requests or any questions you may have about the summit or Generis Group events. How can I assist you?`,
878      `Hello! I'm Genie, your guide to the ${eventName}. Whether you have questions about Generis Group or need help with an access request for the program page, I'm here to help.`,
879      `Hello! 👋 I'm Genie, your friendly AI assistant, here to help! Are you interested in attending the ${eventName}?`,
880      `Hey there! 🙂 I'm Genie, your friendly AI assistant. Whether you have a question about the summit or would like to request access to exclusive insights, I’m here to help! How can I assist you?`,
881    ];
882
883    
884    return prompts[Math.floor(Math.random() * Math.random() * prompts.length)];
885  }
886
887  async function submitAccessRequest(formData) {
888    console.groupCollapsed("🚀 SUBMIT ACCESS REQUEST INITIATED");
889    console.table(formData);
890
891    try {
892      const payload = {
893        user_type: formData["end user/solution provider"],
894        first_name: formData["first name"],
895        last_name: formData["last name"],
896        job_title: formData["job title"],
897        company_name: formData["company name"],
898        work_email: formData["work email"],
899        phone_number: formData["phone number"],
900        how_hear: formData["how did you hear about the event"],
901      };
902
903      const response = await fetch("/wp-admin/admin-ajax.php?action=chatbot_submit_access", {
904        method: "POST",
905        headers: { "Content-Type": "application/json" },
906        credentials: "same-origin",
907        body: JSON.stringify(payload),
908      });
909
910      let json;
911      try {
912        json = await response.json();
913        console.log("📨 Parsed JSON response:", json);
914      } catch (e) {
915        console.error("⚠️ Could not parse response as JSON. Raw text follows:");
916        const text = await response.text();
917        console.log("🧾 Raw Response Text:", text);
918        return;
919      }
920
921      if (response.ok && json.success) {
922        console.log("✅ SUCCESS: Access request submitted successfully!");
923      } else {
924        console.warn("⚠️ Submission failed. Server message:", json?.data?.message || "Unknown error");
925      }
926    } catch (error) {
927      console.error("❌ Error during form submission:", error);
928    }
929
930    console.groupEnd();
931  }
932});

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.