PageSourceSearch

https://smart-scale.ai/script.js

js smart-scale.ai collected 2026-10-05 15:05:23 UTC 20,417 bytes, 526 lines download raw bytes

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.