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.