1(() => { 2 const qs = (s, ctx = document) => ctx.querySelector(s); 3 const qsa = (s, ctx = document) => Array.from(ctx.querySelectorAll(s)); 4 const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; 5 let chatStarted = false; 6 // Speech-bubble icon used on the chat launcher (pre-load button + n8n toggle), so both look identical. 7 const CHAT_ICON_URI = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%230d0b06'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M21%2011.5a8.38%208.38%200%200%201-.9%203.8%208.5%208.5%200%200%201-7.6%204.7%208.38%208.38%200%200%201-3.8-.9L3%2021l1.9-5.7a8.38%208.38%200%200%201-.9-3.8%208.5%208.5%200%200%201%204.7-7.6%208.38%208.38%200%200%201%203.8-.9h.5a8.48%208.48%200%200%201%208%208v.5z'/%3E%3C/svg%3E"; 8 const setInlineStyles = (el, styles) => { 9 if (!el || !styles) return; 10 Object.entries(styles).forEach(([key, val]) => { 11 el.style[key] = val; 12 }); 13 }; 14 15 const forceLeftAlign = (root = document) => { 16 const headerSelectors = [".jm-header", ".n8n-chat__header", ".n8n-chat__panel header", ".cs-header", ".cs-title"]; 17 headerSelectors.forEach((sel) => { 18 qsa(sel, root).forEach((el) => 19 setInlineStyles(el, { 20 display: "flex", 21 alignItems: "flex-start", 22 justifyContent: "flex-start", 23 textAlign: "left", 24 gap: "8px", 25 }) 26 ); 27 }); 28 qsa(".n8n-chat__messages, .cs-messages", root).forEach((el) => 29 setInlineStyles(el, { alignItems: "flex-start", textAlign: "left" }) 30 ); 31 qsa( 32 ".n8n-chat__message, .n8n-chat__message--bot, .n8n-chat__message--assistant, .n8n-chat__message--user, .cs-message, .cs-message--assistant, .cs-message--user", 33 root 34 ).forEach((el) => 35 setInlineStyles(el, { alignItems: "flex-start", justifyContent: "flex-start", textAlign: "left" }) 36 ); 37 }; 38 39 const forceYellowButtons = (root = document) => { 40 const yellowSelectors = [ 41 ".jm-chat-launcher", 42 ".jm-button", 43 ".n8n-chat__toggle", 44 ".n8n-chat__footer button[type='submit']", 45 ".n8n-chat__quick-replies button", 46 ".n8n-chat__cta button", 47 ".cs-launcher", 48 ".cs-button", 49 ".cs-button-primary", 50 ".cs-chat-button", 51 ".cs-submit-button", 52 ]; 53 yellowSelectors.forEach((sel) => { 54 qsa(sel, root).forEach((el) => 55 setInlineStyles(el, { 56 background: "linear-gradient(135deg, var(--accent), var(--accent-2))", 57 color: "#0d0b06", 58 borderColor: "var(--accent)", 59 boxShadow: "0 10px 30px rgba(0,0,0,0.25)", 60 }) 61 ); 62 }); 63 }; 64 65 const enforceChatUI = () => { 66 forceLeftAlign(document); 67 forceYellowButtons(document); 68 const rispose = qs("cs-widget, .cs-widget"); 69 if (rispose?.shadowRoot) { 70 forceLeftAlign(rispose.shadowRoot); 71 forceYellowButtons(rispose.shadowRoot); 72 } 73 }; 74 75 function initNav() { 76 const toggle = qs(".nav-toggle"); 77 const menu = qs(".nav-links"); 78 const demoItem = qs(".nav-item-demo"); 79 const demoSub = qs(".nav-submenu"); 80 let hideTimer; 81 if (!toggle || !menu) return; 82 toggle.addEventListener("click", () => { 83 const expanded = toggle.getAttribute("aria-expanded") === "true"; 84 toggle.setAttribute("aria-expanded", String(!expanded)); 85 menu.classList.toggle("open"); 86 }); 87 qsa(".nav-links a").forEach((link) => 88 link.addEventListener("click", () => { 89 menu.classList.remove("open"); 90 toggle.setAttribute("aria-expanded", "false"); 91 }) 92 ); 93 94 if (demoItem && demoSub) { 95 const demoLink = demoItem.querySelector("a"); 96 const open = () => { 97 clearTimeout(hideTimer); 98 demoItem.classList.add("open"); 99 }; 100 const close = () => { 101 hideTimer = setTimeout(() => demoItem.classList.remove("open"), 180); 102 }; 103 demoItem.addEventListener("mouseenter", open); 104 demoItem.addEventListener("mouseleave", close); 105 demoSub.addEventListener("mouseenter", open); 106 demoSub.addEventListener("mouseleave", close); 107 // Desktop hover handles open; on mobile submenu is always visible, so no toggle logic needed. 108 } 109 } 110 111 function initSmoothScroll() { 112 qsa('a[href^="#"]').forEach((anchor) => { 113 anchor.addEventListener("click", (e) => { 114 const targetId = anchor.getAttribute("href").slice(1); 115 const target = targetId ? qs(`#${targetId}`) : null; 116 if (target) { 117 e.preventDefault(); 118 target.scrollIntoView({ behavior: prefersReducedMotion ? "auto" : "smooth" }); 119 } 120 }); 121 }); 122 } 123 124 function initReveal() { 125 if (prefersReducedMotion) { 126 qsa("[data-reveal]").forEach((el) => el.classList.add("revealed")); 127 return; 128 } 129 const observer = new IntersectionObserver( 130 (entries) => {
131 entries.forEach((entry) => { 132 if (entry.isIntersecting) { 133 entry.target.classList.add("revealed"); 134 observer.unobserve(entry.target); 135 } 136 }); 137 }, 138 { threshold: 0.18 } 139 ); 140 qsa("[data-reveal]").forEach((el) => observer.observe(el)); 141 } 142 143 function initAccordion() { 144 qsa("[data-accordion]").forEach((item) => { 145 const btn = qs(".faq-question", item); 146 const ans = qs(".faq-answer", item); 147 if (!btn || !ans) return; 148 btn.addEventListener("click", () => { 149 const open = item.classList.contains("active"); 150 qsa("[data-accordion]").forEach((el) => el.classList.remove("active")); 151 if (!open) item.classList.add("active"); 152 }); 153 }); 154 } 155 156 function initParallax() { 157 const el = qs("[data-parallax]"); 158 if (!el || prefersReducedMotion) return; 159 const strength = 14; 160 document.addEventListener("pointermove", (evt) => { 161 const { innerWidth, innerHeight } = window; 162 const x = (evt.clientX / innerWidth - 0.5) * strength; 163 const y = (evt.clientY / innerHeight - 0.5) * strength; 164 el.style.transform = `translate(${x}px, ${y}px)`; 165 }); 166 } 167 168 function initTypewriter() { 169 const target = qs("[data-typewriter]"); 170 if (!target) return; 171 const rawText = target.dataset.typewriterText || target.textContent || ""; 172 const fullText = rawText.replace(/\r?\n/g, "\n").trim(); 173 if (!fullText) return; 174 if (prefersReducedMotion) { 175 target.innerHTML = fullText.replace(/\n/g, "<br>"); 176 return; 177 } 178 target.setAttribute("aria-live", "polite"); 179 target.classList.add("typewriter"); 180 target.textContent = ""; 181 const ghost = document.createElement("span"); 182 ghost.className = "typewriter-ghost"; 183 ghost.textContent = fullText; 184 ghost.setAttribute("aria-hidden", "true"); 185 const live = document.createElement("span"); 186 live.className = "typewriter-live"; 187 target.append(ghost, live); 188 const typeDelay = 61.56; 189 const blankPause = 0; 190 const cycleDelay = 5000; 191 192 const runCycle = () => { 193 live.classList.remove("typewriter-done"); 194 let index = 0; 195 const step = () => { 196 if (index <= fullText.length) { 197 live.textContent = fullText.slice(0, index); 198 index++; 199 setTimeout(step, typeDelay); 200 } else { 201 live.classList.add("typewriter-done"); 202 setTimeout(() => { 203 live.textContent = ""; 204 live.classList.remove("typewriter-done"); 205 setTimeout(runCycle, blankPause); 206 }, cycleDelay); 207 } 208 }; 209 step(); 210 }; 211 runCycle(); 212 } 213 214 async function initRealChat() { 215 const mode = document.body.dataset.chatMode; 216 if (mode === "rispose") return; 217 const path = location.pathname.replace(/\/+$/, "") || "/"; 218 const imagesBase = location.protocol === "file:" ? "images" : "/images"; 219 const configMap = { 220 shopify: { 221 webhookUrl: "https://n8n.srv1160115.hstgr.cloud/webhook/c38073cd-b1c3-401f-ba69-abd8db13d5b1/chat", 222 primary: "#fbbf24", 223 secondary: "#f59e0b", 224 headerName: "Kast-bot", 225 headerSubtitle: "Persoonlijk advies dat bij jou KAST", 226 profileUrl: "https://i.postimg.cc/DztW036D/ffedaa1c-4e42-4c2e-9cb2-f2b835d4d59e-1.png", 227 welcome: "Yo! Welkom bij Kosso Nutrition - hier draait alles om groei, discipline en kwaliteit. Hoe kan ik je helpen?", 228 }, 229 garage: { 230 webhookUrl: "https://n8n.srv1160115.hstgr.cloud/webhook/f68007c2-4b6a-4e91-b1ae-d36ae45158a9/chat", 231 primary: "#E53E3E", 232 secondary: "#C53030", 233 headerName: "Mike", 234 headerSubtitle: "Uw digitale monteur", 235 profileUrl: "https://images.unsplash.com/photo-1568602471122-7832951cc4c5", 236 welcome: "Welkom bij DrivePro! Mijn naam is Mike. Vragen over een APK, onderhoud of reparatie? Ik help je direct!", 237 }, 238 schoonheid: { 239 webhookUrl: "https://n8n.srv1160115.hstgr.cloud/webhook/aeb6b74d-d188-42b1-a024-8bd4a81c7671/chat", 240 primary: "#D4AFB9", 241 secondary: "#b899a2", 242 headerName: "Chloé", 243 headerSubtitle: "Uw beauty adviseur", 244 profileUrl: "https://images.unsplash.com/photo-1494790108377-be9c29b29330", 245 welcome: "Hallo! Ik ben Chloé van LUMINA. Ik help je graag met het vinden van de perfecte behandeling of het boeken van een afspraak.", 246 }, 247 default: { 248 webhookUrl: "https://n8n.srv1160115.hstgr.cloud/webhook/dfbbd7ea-36bc-4d39-a70d-b2e867823f4f/chat", 249 primary: "#fbbf24", 250 secondary: "#f59e0b", 251 headerName: "Joshua", 252 headerSubtitle: "Stel gerust uw vragen", 253 profileUrl: `${imagesBase}/joshua-profiel-48.jpg`, 254 profileUrl2x: `${imagesBase}/joshua-profiel-96.jpg`, 255 welcome: "Hallo, mijn naam is Joshua. Hoe kan ik u helpen?\n\nIk kan u helpen door vragen te beantwoorden over Smart-Scale of zelfs een afspraak voor u in te plannen!", 256 }, 257 }; 258 259 const selected = 260 path.includes("garage") ? configMap.garage :
261 path.includes("schoonheid") ? configMap.schoonheid : 262 path.includes("shopify") ? configMap.shopify : 263 configMap.default; 264 265 if (!selected) return; 266 267 if (!document.getElementById("n8n-chat-style")) { 268 const link = document.createElement("link"); 269 link.id = "n8n-chat-style"; 270 link.rel = "stylesheet"; 271 link.href = "https://cdn.jsdelivr.net/npm/@n8n/chat/dist/style.css"; 272 document.head.appendChild(link); 273 } 274 275 const style = document.createElement("style"); 276 style.id = "n8n-chat-custom-style"; 277 style.textContent = ` 278 :root { --jm-primary: ${selected.primary}; --jm-primary-dark: ${selected.secondary}; } 279 /* Never clip the launcher button so it stays a full circle. */ 280 .chat-window-wrapper, .n8n-chat, .chat-window-toggle, .n8n-chat__toggle { overflow: visible !important; } 281 /* Hide n8n's default icon and render one consistent, fully-round speech-bubble launcher. */ 282 .n8n-chat__toggle, .chat-window-toggle { 283 position: relative !important; 284 width: 62px !important; 285 height: 62px !important; 286 border: 1px solid ${selected.primary} !important; 287 border-radius: 50% !important; 288 background-image: url("${CHAT_ICON_URI}"), linear-gradient(135deg, ${selected.primary}, ${selected.secondary}) !important; 289 background-repeat: no-repeat, no-repeat !important; 290 background-position: center, center !important; 291 background-size: 28px 28px, auto !important; 292 box-shadow: 0 16px 40px rgba(0,0,0,0.35) !important; 293 } 294 .n8n-chat__toggle > *, .chat-window-toggle > * { visibility: hidden !important; } 295 .n8n-chat__panel header { background: #fbbf24 !important; color: #000000 !important; } 296 .n8n-chat__footer button[type="submit"] { background: ${selected.primary} !important; border-color: ${selected.primary} !important; color: #fff !important; } 297 `; 298 document.head.appendChild(style); 299 300 const { createChat } = await import("https://cdn.jsdelivr.net/npm/@n8n/chat/dist/chat.bundle.es.js"); 301 createChat({ webhookUrl: selected.webhookUrl, useShadowDom: false }); 302 303 // Apply custom header/profile/welcome similar to dist build 304 const applyHeader = () => { 305 const headerSelectors = [ 306 ".n8n-chat__header", 307 ".chat-header", 308 ".chat-heading", 309 ".n8n-chat__panel header", 310 ".widget-header", 311 ".chat-widget-header", 312 ]; 313 for (const sel of headerSelectors) { 314 const el = qs(sel); 315 if (!el) continue; 316 const container = el.classList.contains("chat-header") ? el : el.closest(".chat-header"); 317 const isCard = Boolean(container && container.closest(".card")); 318 const textColor = isCard ? "#ffffff" : "#000000"; 319 const objectPosition = selected.headerName === "Mike" ? "right center" : "center"; 320 const srcset = selected.profileUrl2x 321 ? `${selected.profileUrl} 1x, ${selected.profileUrl2x} 2x` 322 : ""; 323 const srcsetAttr = srcset ? `srcset="${srcset}"` : ""; 324 el.innerHTML = ` 325 <div class="jm-header" style="display:flex;align-items:center;gap:8px;justify-content:flex-start;text-align:left;color:${textColor} !important;"> 326 <img class="jm-header__img" src="${selected.profileUrl}" ${srcsetAttr} alt="${selected.headerName}" width="48" height="48" loading="lazy" decoding="async" style="width:48px;height:48px;border-radius:50%;object-fit:cover;object-position:${objectPosition};border:1px solid rgba(255,255,255,0.08);"> 327 <div style="color:${textColor} !important;font-weight:600;font-size:18px;line-height:1.2;"> 328 ${selected.headerName} 329 <div style="color:${textColor} !important;font-size:13px;font-weight:400;margin-top:2px;line-height:1.2;"> 330 ${selected.headerSubtitle} 331 </div> 332 </div> 333 </div> 334 `; 335 setInlineStyles(el, { 336 background: "#fbbf24", 337 color: textColor, 338 }); 339 if (container) { 340 const containerStyles = { 341 background: isCard ? "transparent" : "#fbbf24", 342 color: isCard ? "#fff" : "#000000", 343 }; 344 if (isCard) containerStyles.paddingBottom = "20px"; 345 setInlineStyles(container, containerStyles); 346 } 347 // Remove default subtitle paragraphs like "Start a chat..." 348 qsa("p", el.parentElement || el).forEach((p) => p.remove()); 349 } 350 qsa(".card .chat-header, .card .chat-heading").forEach((cardHeader) => 351 setInlineStyles(cardHeader, { background: "transparent", color: "#fff", paddingBottom: "20px" }) 352 ); 353 }; 354 355 const applyWelcome = () => { 356 const bubbles = qsa(".chat-message, .message, .bot-message, .n8n-chat__message, .bot, .message--bot"); 357 if (!bubbles.length) return; 358 const primary = bubbles.find((b) => { 359 const text = (b.textContent || "").trim(); 360 return !text || /hallo|welkom|hi|hey/i.test(text) || text.length < 200; 361 }) || bubbles[0]; 362 const welcome = selected.welcome || primary.textContent || ""; 363 primary.textContent = welcome;
364 bubbles.forEach((b) => { 365 const text = (b.textContent || "").trim(); 366 if (b !== primary && (/my name is|how can i help/i.test(text) || text.length === 0)) { 367 b.remove(); 368 } 369 }); 370 }; 371 372 const recolor = () => { 373 const toggle = qs(".n8n-chat__toggle") || qs(".chat-window-toggle"); 374 if (toggle) { 375 toggle.style.background = "linear-gradient(135deg, var(--accent), var(--accent-2))"; 376 toggle.style.border = "1px solid var(--accent)"; 377 toggle.style.boxShadow = "0 10px 30px rgba(0,0,0,0.25)"; 378 toggle.style.color = "#0d0b06"; 379 toggle.classList.add("jm-cta-yellow"); 380 } 381 qsa(".n8n-chat__panel header, .chat-header, .chat-heading, .chat-widget-header, .widget-header").forEach((heading) => { 382 heading.style.background = "#fbbf24"; 383 heading.style.color = "#000000"; 384 }); 385 const sendBtn = 386 qs(".n8n-chat__footer button[type='submit']") || 387 qs(".chat-input-send-button") || 388 qs(".chat-inputs-controls button"); 389 if (sendBtn) { 390 sendBtn.classList.add("jm-cta-yellow"); 391 sendBtn.style.background = "var(--accent)"; 392 sendBtn.style.borderColor = "var(--accent)"; 393 sendBtn.style.color = "#0d0b06"; 394 } 395 qsa(".n8n-chat__toggle, .chat-window-toggle").forEach((el) => el.classList.add("jm-cta-yellow")); 396 qsa(".n8n-chat__quick-replies button, .n8n-chat__cta button, .chat-inputs-controls button, .chat-input-send-button").forEach((el) => 397 el.classList.add("jm-cta-yellow") 398 ); 399 }; 400 401 const alignAndColor = () => { 402 enforceChatUI(); 403 }; 404 405 const tryApply = () => { 406 applyHeader(); 407 applyWelcome(); 408 recolor(); 409 alignAndColor(); 410 }; 411 412 setTimeout(tryApply, 400); 413 setTimeout(tryApply, 1000); 414 setTimeout(tryApply, 2000); 415 } 416 417 function hideRisposeFooter() { 418 const widget = qs("cs-widget, .cs-widget"); 419 if (!widget) return false; 420 const tryHideBySelector = (root) => { 421 if (!root?.querySelectorAll) return false; 422 const candidates = root.querySelectorAll(".text-xs.text-stone-500.border-t, .border-t.border-stone-200"); 423 const footer = 424 candidates[0] || 425 Array.from(root.querySelectorAll("*")).find((el) => (el.textContent || "").toLowerCase().includes("powered by rispose")); 426 if (footer) { 427 footer.style.display = "none"; 428 footer.setAttribute("data-hidden", "true"); 429 return true; 430 } 431 return false; 432 }; 433 434 const hiddenInLightDom = tryHideBySelector(widget); 435 const hiddenInShadow = widget.shadowRoot ? tryHideBySelector(widget.shadowRoot) : false; 436 437 if (hiddenInLightDom || hiddenInShadow) { 438 enforceChatUI(); 439 return true; 440 } 441 442 widget.style.overflow = "hidden"; 443 widget.style.clipPath = "inset(0 0 28px 0)"; 444 widget.dataset.risposeFooterCropped = "true"; 445 enforceChatUI(); 446 return true; 447 } 448 449 function createChatLauncher(onClick) { 450 if (qs("#jm-chat-launcher")) return qs("#jm-chat-launcher"); 451 const launcher = document.createElement("button"); 452 launcher.id = "jm-chat-launcher"; 453 launcher.className = "jm-chat-launcher"; 454 launcher.type = "button"; 455 launcher.setAttribute("aria-label", "Open chat"); 456 setInlineStyles(launcher, { 457 position: "fixed", 458 right: "22px", 459 bottom: "22px", 460 width: "62px", 461 height: "62px", 462 borderRadius: "50%", 463 border: "1px solid var(--accent)", 464 backgroundImage: `url("${CHAT_ICON_URI}"), linear-gradient(135deg, var(--accent), var(--accent-2))`, 465 backgroundRepeat: "no-repeat, no-repeat", 466 backgroundPosition: "center, center", 467 backgroundSize: "28px 28px, auto", 468 boxShadow: "0 16px 40px rgba(0, 0, 0, 0.35)", 469 cursor: "pointer", 470 zIndex: "40", 471 }); 472 if (onClick) launcher.addEventListener("click", onClick); 473 document.body.appendChild(launcher); 474 return launcher; 475 } 476 477 function scheduleChatInit() { 478 let launcher = null; 479 const startChat = () => { 480 if (chatStarted) return; 481 chatStarted = true; 482 if (launcher) { 483 launcher.setAttribute("aria-busy", "true"); 484 launcher.style.opacity = "0.7"; 485 } 486 initRealChat(); 487 let tries = 0; 488 const timer = setInterval(() => { 489 const toggle = qs(".n8n-chat__toggle") || qs(".chat-window-toggle"); 490 if (toggle) { 491 if (launcher) launcher.remove(); 492 toggle.click(); 493 clearInterval(timer); 494 return; 495 } 496 if (tries > 20) { 497 if (launcher) { 498 launcher.removeAttribute("aria-busy"); 499 launcher.style.opacity = "1"; 500 } 501 chatStarted = false;
502 clearInterval(timer); 503 } 504 tries++; 505 }, 250); 506 }; 507 launcher = createChatLauncher(startChat); 508 } 509 510 function init() { 511 initNav(); 512 initSmoothScroll(); 513 initReveal(); 514 initTypewriter(); 515 initAccordion(); 516 initParallax(); 517 scheduleChatInit(); 518 let tries = 0; 519 const footerTimer = setInterval(() => { 520 if (hideRisposeFooter() || tries > 25) clearInterval(footerTimer); 521 tries++; 522 }, 300); 523 } 524 525 document.addEventListener("DOMContentLoaded", init); 526})();
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.