PageSourceSearch

https://app.chattitude.ai/widget.js

js chattitude.ai collected 2026-10-05 11:55:00 UTC 11,781 bytes, 309 lines download raw bytes

1(function () {
2  // Session storage key - use same key as ChatbotFrame.js
3  var SESSION_STORAGE_KEY = 'widgetSessionId';
4
5  // Helper functions for session storage
6  function getStoredSession() {
7    try {
8      var stored = localStorage.getItem(SESSION_STORAGE_KEY);
9      return stored; // Return the sessionId directly, not wrapped in object
10    } catch (e) {
11      return null;
12    }
13  }
14
15  function setStoredSession(sessionId) {
16    try {
17      if (sessionId) {
18        localStorage.setItem(SESSION_STORAGE_KEY, sessionId);
19      } else {
20        localStorage.removeItem(SESSION_STORAGE_KEY);
21      }
22    } catch (e) {
23      // Ignore storage errors
24    }
25  }
26
27  // Function to initialize the widget
28  function initWidget() {
29    // Get config from global or query params
30    var config = window.slickbotsConfig || {};
31    if (!config.agent) {
32      // Try to get from query string
33      var params = new URLSearchParams(window.location.search);
34      config.agent = params.get('agent');
35    }
36    if (!config.tenant) {
37      var params = new URLSearchParams(window.location.search);
38      config.tenant = params.get('tenant');
39    }
40    if (!config.widgetUrl) {
41      var params = new URLSearchParams(window.location.search);
42      config.widgetUrl = params.get('widgetUrl');
43    }
44    if (!config.ctread) {
45      var params = new URLSearchParams(window.location.search);
46      config.ctread = params.get('ctread');
47    }
48
49    var embedContainerSelector = config.embedContainer || null;
50    var embedLayout = config.embedLayout;
51    if (embedContainerSelector) {
52      if (embedLayout === 'floating') {
53        console.warn(
54          'Chattitude widget: embedContainer with embedLayout "floating" is not supported; falling back to default floating widget on body.'
55        );
56        embedContainerSelector = null;
57        embedLayout = null;
58      } else if (!embedLayout || embedLayout === 'full') {
59        embedLayout = 'full';
60      }
61    }
62    var isEmbedFull = Boolean(embedContainerSelector && embedLayout === 'full');
63
64    // Build iframe src with agent as query param, even if widgetUrl already has query params/hash
65    var baseUrl = config.widgetUrl + '/index.html?mode=chat';
66    var hasHash = baseUrl.indexOf('#') !== -1;
67    var query = (baseUrl.indexOf('?') === -1 ? '?' : '&') + 'agent=' + encodeURIComponent(config.agent);
68
69    if (config.tenant) query += '&tenant=' + encodeURIComponent(config.tenant);
70    if (config.ctread) query += '&ctread=' + encodeURIComponent(config.ctread);
71    if (isEmbedFull) query += '&embed=full';
72    var transparentBg =
73      isEmbedFull &&
74      (config.transparentBackground === true || config.transparentBackground === 'true');
75    if (transparentBg) query += '&transparent=true';
76    // Pass parent window's location.href as pageUrl
77    query += '&pageUrl=' + encodeURIComponent(window.location.href);
78    var src;
79    if (hasHash) {
80      // If widgetUrl uses hash routing, append query after the hash
81      var parts = baseUrl.split('#');
82      src = parts[0] + '#' + parts[1].replace(/\/?$/, '') + (parts[1].indexOf('?') === -1 ? '?' : '&') + query;
83    } else {
84      src = baseUrl + query;
85    }
86
87    var mountEl = document.body;
88    if (embedContainerSelector) {
89      mountEl = document.querySelector(embedContainerSelector);
90      if (!mountEl) {
91        console.error('Chattitude widget: embed container not found:', embedContainerSelector);
92        return;
93      }
94      var pos = window.getComputedStyle(mountEl).position;
95      if (pos === 'static' || !pos) {
96        mountEl.style.position = 'relative';
97      }
98    }
99
100    // Create the iframe
101    var iframe = document.createElement('iframe');
102    iframe.src = src;
103    // Detect mobile device (full embed fills host container — avoid launcher-sized iframe)
104    var uaMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
105    var isMobile = uaMobile && !isEmbedFull;
106    var originalOverflow = document.body.style.overflow;
107
108    // Base iframe styles
109    iframe.style.border = 'none';
110    iframe.style.overflow = 'hidden';
111    iframe.style.boxSizing = 'border-box';
112
113    if (isEmbedFull) {
114      iframe.style.position = 'absolute';
115      iframe.style.top = '0';
116      iframe.style.left = '0';
117      iframe.style.right = '0';
118      iframe.style.bottom = '0';
119      iframe.style.width = '100%';
120      iframe.style.height = '100%';
121      iframe.style.maxWidth = '100%';
122      iframe.style.maxHeight = '100%';
123      iframe.style.margin = '0';
124      iframe.style.borderRadius =
125        typeof config.embedBorderRadius === 'string' ? config.embedBorderRadius : '12px';
126      iframe.style.zIndex = typeof config.embedZIndex === 'number' ? String(config.embedZIndex) : '1';
127    } else {
128      iframe.style.position = 'fixed';
129      iframe.style.zIndex = '999999';
130      iframe.style.borderRadius = '16px';
131      iframe.style.marginBottom = '10px';
132      iframe.style.marginRight = '10px';
133
134      if (isMobile) {
135        // Mobile: Start minimized (100x100 with 20px padding around 60px icon) to avoid blocking clicks
136        iframe.style.bottom = '5px';
137        iframe.style.right = '5px';
138        iframe.style.top = 'auto';
139        iframe.style.left = 'auto';
140        iframe.style.width = '100px';
141        iframe.style.height = '100px';
142        iframe.style.maxWidth = '100px';
143        iframe.style.maxHeight = '100px';
144        iframe.style.borderRadius = '50%';
145      } else {
146        // Desktop: Fixed bottom-right position
147        iframe.style.bottom = '0';
148        iframe.style.right = '0';
149        iframe.style.top = 'auto';
150        iframe.style.left = 'auto';
151        iframe.style.width = '370px';
152        iframe.style.height = '520px';
153        iframe.style.maxWidth = '370px';
154        iframe.style.maxHeight = '520px';
155        iframe.style.borderRadius = '16px';
156      }
157    }
158
159    iframe.allow = 'clipboard-write; microphone;';
160    iframe.setAttribute(
161      'sandbox',
162      'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox'
163    );
164    iframe.title = 'Chattitude Agent';
165
166    // Extract expected origin from iframe src for security verification
167    var expectedOrigin = null;
168    try {
169      var iframeUrl = new URL(iframe.src);
170      expectedOrigin = iframeUrl.origin;
171    } catch (e) {
172      // If URL parsing fails, we'll verify against the current origin
173      expectedOrigin = window.location.origin;
174    }
175
176    // Listen for messages from the widget - register BEFORE appending iframe
177    window.addEventListener('message', function (event) {
178      // Security: Verify message origin matches the iframe origin
179      // This prevents malicious sites from sending fake resize messages
180      if (expectedOrigin && event.origin !== expectedOrigin) {
181        console.warn('⚠️ Ignoring postMessage from unexpected origin:', event.origin, 'Expected:', expectedOrigin);
182        return;
183      }
184
185      // Handle session request from iframe
186      if (event.data && event.data.type === 'slickbots-request-session') {
187        var session = getStoredSession();
188        iframe.contentWindow.postMessage(
189          {
190            type: 'slickbots-session-response',
191            sessionId: session
192          },
193          '*'
194        );
195        return;
196      }
197
198      // Handle session change from iframe
199      if (event.data && event.data.type === 'slickbots-session-changed') {
200        setStoredSession(event.data.sessionId);
201        return;
202      }
203
204      if (event.data && event.data.type === 'slickbots-resize') {
205        if (isEmbedFull) {
206          // Host container + CSS control size; iframe stays fill
207          return;
208        }
209        // Re-detect mobile in message handler for reliability
210        // Use both user agent and viewport width as fallback
211        var isMobileDevice = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
212        var isSmallViewport = window.innerWidth <= 768; // Common mobile breakpoint
213        var isMobileHandler = isMobileDevice || isSmallViewport;
214
215        if (isMobileHandler) {
216          // Mobile: Update size based on widget state
217          // Widget sends 60x60 when minimized, fullscreen size when maximized
218          var width = event.data.chatWidth || 60;
219          var height = event.data.chatHeight || 60;
220
221          // Determine if minimized (small icon with padding, possibly with bubble) or maximized (fullscreen)
222          // Height can be up to ~220px when bubble is visible, width up to ~280px
223          if (width <= 300 && height <= 250) {
224            // Minimized: small icon with padding at bottom-right
225            // Clamp to reasonable values for minimized state
226            width = Math.max(40, Math.min(width, 300));
227            height = Math.max(40, Math.min(height, 250));
228
229            iframe.style.width = width + 'px';
230            iframe.style.maxWidth = width + 'px';
231            iframe.style.height = height + 'px';
232            iframe.style.maxHeight = height + 'px';
233            iframe.style.bottom = '5px';
234            iframe.style.right = '5px';
235            iframe.style.top = 'auto';
236            iframe.style.left = 'auto';
237            // Use rounded corners instead of full circle when bubble is visible (larger size)
238            if (width > 150 || height > 150) {
239              iframe.style.borderRadius = '20px'; // Rounded rectangle for bubble
240            } else {
241              iframe.style.borderRadius = '50%'; // Full circle for icon only
242            }
243            document.body.style.overflow = originalOverflow;
244          } else {
245            // Maximized: fullscreen - use 100% width and height
246            iframe.style.width = '100%';
247            iframe.style.maxWidth = '100%';
248            iframe.style.height = '100%';
249            iframe.style.maxHeight = '100%';
250            iframe.style.bottom = '0';
251            iframe.style.right = '0';
252            iframe.style.left = '0';
253            iframe.style.top = '0';
254            iframe.style.borderRadius = '0';
255            document.body.style.overflow = 'hidden';
256          }
257        } else {
258          // Desktop: Update size but maintain bottom-right positioning
259          // Always update both width and height, even if only one is provided
260          var width = event.data.chatWidth
261            ? Math.min(event.data.chatWidth, window.innerWidth || 1200)
262            : parseInt(iframe.style.width, 10) || 350;
263          var height = event.data.chatHeight
264            ? Math.min(event.data.chatHeight, window.innerHeight || 1200)
265            : parseInt(iframe.style.height, 10) || 520;
266
267          iframe.style.width = width + 'px';
268          iframe.style.maxWidth = width + 'px';
269          iframe.style.height = height + 'px';
270          iframe.style.maxHeight = height + 'px';
271
272          // Ensure positioning stays bottom-right
273          iframe.style.bottom = '0';
274          iframe.style.right = '0';
275          iframe.style.top = 'auto';
276          iframe.style.left = 'auto';
277          iframe.style.transform = 'none';
278
279          if (width > 300 || height > 500) {
280            iframe.style.boxShadow = '0 12px 48px rgba(0, 0, 0, 0.35), 0 4px 16px rgba(0, 0, 0, 0.25)';
281          } else {
282            iframe.style.boxShadow = 'none';
283          }
284        }
285      } else if (event.data && event.data.type === 'closeSlickbot') {
286        // Handle close message from widget
287        iframe.style.display = 'none';
288        // Optionally remove the iframe entirely
289        // iframe.parentNode.removeChild(iframe);
290      }
291    });
292
293    // Insert iframe into page AFTER message listener is registered
294    mountEl.appendChild(iframe);
295  }
296
297  function run() {
298    if (!document.body) {
299      return;
300    }
301    initWidget();
302  }
303
304  if (document.readyState === 'loading') {
305    document.addEventListener('DOMContentLoaded', run);
306  } else {
307    run();
308  }
309})();

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.