(() => { const getConfig = function () { const fourteenLoadedEventName = "FOURTEEN_LOADED"; const fourteenOrigin = "https://widget.14.ai"; const iframe = document.createElement("iframe"); iframe.allow = "clipboard-write"; let didLoad = false; let isEmbedReady = false; let onReadyCallbacks = []; let onOpenChangeCallback = null; let onNotificationsChangeCallback = null; let bubbleState = { isExpanded: false, bubbleWidth: 64, bubbleHeight: 64 }; let anchorPosition = 'bottom-right'; // Track current anchor position const sendMessage = (message) => { if (didLoad) { iframe.contentWindow.postMessage(message, fourteenOrigin); } else { window.addEventListener( fourteenLoadedEventName, () => { iframe.contentWindow.postMessage(message, fourteenOrigin); }, { once: true } ); } }; let dimensionsTimeout = null; const isMobileDevice = () => { if (typeof window === 'undefined') return false; const isTouchDevice = ( 'ontouchstart' in window || navigator.maxTouchPoints > 1 || window.matchMedia("(pointer: coarse)").matches ); const isPhoneUA = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); // Exclude tablets by checking the smaller dimension // Phones: smaller dimension <= 600px (iPhone Pro Max is 430px) // Tablets: smaller dimension >= 744px (iPad Mini is 744px) const smallerDimension = Math.min(window.innerWidth, window.innerHeight); const isPhoneSize = smallerDimension <= 600; return (isTouchDevice || isPhoneUA) && isPhoneSize; }; const sendWindowDimensions = () => { const isMobile = isMobileDevice(); // On mobile, use visual viewport to account for keyboard const visibleHeight = isMobile && window.visualViewport ? window.visualViewport.height : window.innerHeight; const visibleWidth = isMobile && window.visualViewport ? window.visualViewport.width : window.innerWidth; const dimensions = { type: 'FOURTEEN_WINDOW_DIMENSIONS_COIuY89DKU6nK6Jb', width: visibleWidth, height: visibleHeight, isMobile: isMobile }; sendMessage(dimensions); }; const debouncedSendWindowDimensions = () => { if (dimensionsTimeout) { clearTimeout(dimensionsTimeout); } dimensionsTimeout = setTimeout(() => { sendWindowDimensions(); }, 100); }; let resizeTimeout = null; const calculateAndSetIframeDimensions = (immediate = false) => { // Clear any pending resize if (resizeTimeout) { clearTimeout(resizeTimeout); resizeTimeout = null; } const performResize = () => { // Detect mobile device using feature detection const isMobile = isMobileDevice(); // Get the current bottom position of the iframe const computedStyle = window.getComputedStyle(iframe); const currentBottom = parseInt(computedStyle.bottom) || 0; // Calculate available height from the bottom position to the top of the window const availableHeight = window.innerHeight - currentBottom; if (bubbleState.isExpanded) { if (isMobile) { // Mobile: fit within visible viewport (accounts for keyboard) const visibleHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight; const visibleWidth = window.visualViewport ? window.visualViewport.width : window.innerWidth; // On iOS, visualViewport.offsetTop accounts for keyboard shifting viewport const offsetTop = window.visualViewport ? window.visualViewport.offsetTop : 0; const offsetLeft = window.visualViewport ? window.visualViewport.offsetLeft : 0; iframe.style.width = visibleWidth + "px"; iframe.style.height = visibleHeight + "px"; // Position at top of visual viewport (accounts for iOS keyboard offset) iframe.style.top = offsetTop + "px"; iframe.style.left = offsetLeft + "px"; iframe.style.bottom = "auto"; iframe.style.right = "auto"; } else { // Desktop: panel dimensions - ensure panel + internal padding fits within viewport const maxWidthFromScreen = window.innerWidth - 60; // 40px padding + 20px margin const panelWidth = Math.min(maxWidthFromScreen, 440); // Check if full panel height (700px + 60px offset) exceeds available height const idealPanelHeight = 700; const requiredHeightWithOffset = idealPanelHeight + 60; let panelHeight, iframeHeight; if (requiredHeightWithOffset > availableHeight) { // Panel is too tall - use available height, no top offset panelHeight = availableHeight - 40; // Only account for internal padding iframeHeight = availableHeight; } else { // Panel fits with offset - use normal calculation const maxHeightFromScreen = availableHeight - 60; // 40px padding + 20px margin panelHeight = Math.min(maxHeightFromScreen, idealPanelHeight); iframeHeight = panelHeight + 40; } // Set iframe size iframe.style.width = (panelWidth + 40) + "px"; iframe.style.height = iframeHeight + "px"; // Reset positioning for desktop panel based on anchor position iframe.style.bottom = "0px"; iframe.style.top = "unset"; if (anchorPosition === 'bottom-left') { iframe.style.left = "0px"; iframe.style.right = "unset"; } else { iframe.style.right = "0px"; iframe.style.left = "unset"; } } } else { // Bubble dimensions iframe.style.width = (bubbleState.bubbleWidth + 40) + "px"; iframe.style.height = (bubbleState.bubbleHeight + 40) + "px"; // Reset positioning for bubble state based on anchor position iframe.style.bottom = "0px"; iframe.style.top = "unset"; if (anchorPosition === 'bottom-left') { iframe.style.left = "0px"; iframe.style.right = "unset"; } else { iframe.style.right = "0px"; iframe.style.left = "unset"; } } }; if (immediate) { performResize(); } else { // Delay resize to match bubble animation duration // Spring with stiffness 350, damping 28 takes roughly 800-1000ms to settle resizeTimeout = setTimeout(() => { performResize(); }, 300); } }; const setPopupOpen = (open) => { console.debug('setPopupOpen', open); }; // Setup viewport meta tag to prevent zoom const setupViewportMeta = () => { let viewportMeta = document.querySelector('meta[name="viewport"]'); const originalContent = viewportMeta?.getAttribute('content') || ''; if (!viewportMeta) { viewportMeta = document.createElement('meta'); viewportMeta.name = 'viewport'; document.head.appendChild(viewportMeta); } // Set viewport to prevent zoom viewportMeta.setAttribute('content', 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'); return originalContent; }; // Lock/unlock scroll on parent page const lockScroll = () => { const scrollY = window.scrollY; document.body.style.position = 'fixed'; document.body.style.top = `-${scrollY}px`; document.body.style.width = '100%'; document.body.style.overflowY = 'scroll'; }; const unlockScroll = () => { const scrollY = document.body.style.top; document.body.style.position = ''; document.body.style.top = ''; document.body.style.width = ''; document.body.style.overflowY = ''; window.scrollTo(0, parseInt(scrollY || '0') * -1); }; return { init: function () { const load = () => { // Setup viewport to prevent zoom if (isMobileDevice()) { setupViewportMeta(); } iframe.src = "https://widget.14.ai/COIuY89DKU6nK6Jb"; iframe.id = "COIuY89DKU6nK6Jb"; iframe.style.border = 0; iframe.style.padding = 0; iframe.style.margin = 0; iframe.style.position = "fixed"; iframe.style.zIndex = 9999; iframe.style.right = 0; iframe.style.bottom = 0; iframe.style.outline = "none"; iframe.style.display = "none"; iframe.style.background = "transparent"; iframe.allowTransparency = "true"; iframe.style.boxSizing = "borderBox"; iframe.style.userSelect = "none"; iframe.style.opacity = "0"; iframe.style.transition = "opacity 200ms ease-in-out"; iframe.addEventListener("load", () => { iframe.style.display = "block"; didLoad = true; calculateAndSetIframeDimensions(true); // Immediate on load sendWindowDimensions(); // Send initial window dimensions window.dispatchEvent(new CustomEvent(fourteenLoadedEventName)); setTimeout(() => { iframe.style.opacity = "1"; }, 500); }); // Listen for window resize to update iframe dimensions // immediately window.addEventListener("resize", () => { calculateAndSetIframeDimensions(true); debouncedSendWindowDimensions(); // Send updated window dimensions (debounced) }); // Listen for visual viewport changes (mobile keyboard) if (window.visualViewport) { const handleViewportChange = () => { if (isMobileDevice()) { calculateAndSetIframeDimensions(true); debouncedSendWindowDimensions(); } }; window.visualViewport.addEventListener("resize", handleViewportChange); window.visualViewport.addEventListener("scroll", handleViewportChange); } const container = document.getElementById("fourteen-container-COIuY89DKU6nK6Jb") ?? document.body; container.appendChild(iframe); window.addEventListener("message", (event) => { const eventType = event.data?.type; switch (eventType) { case "FOURTEEN_EMBED_OPEN_POPUP_COIuY89DKU6nK6Jb": { if (isMobileDevice()) { lockScroll(); } onOpenChangeCallback?.(true); break; } case "FOURTEEN_EMBED_CLOSE_POPUP_COIuY89DKU6nK6Jb": { if (isMobileDevice()) { unlockScroll(); } onOpenChangeCallback?.(false); break; } case "FOURTEEN_EMBED_NOTIFICATIONS_CHANGE_COIuY89DKU6nK6Jb": { onNotificationsChangeCallback?.(event.data.numNotifications); break; } case "FOURTEEN_EMBED_READY_COIuY89DKU6nK6Jb": { isEmbedReady = true; onReadyCallbacks.forEach((callback) => callback()); onReadyCallbacks = []; sendMessage({ type: 'FOURTEEN_SET_SOURCE', url: window.location.href }); sendWindowDimensions(); // Send window dimensions when React component is ready break; } case "FOURTEEN_EMBED_UNREADY_COIuY89DKU6nK6Jb": { isEmbedReady = false; break; } case "FOURTEEN_EMBED_RESIZE_COIuY89DKU6nK6Jb": { const { isExpanded, bubbleWidth, bubbleHeight } = event.data; const previousExpanded = bubbleState.isExpanded; bubbleState = { isExpanded, bubbleWidth, bubbleHeight }; if (isExpanded && !previousExpanded) { // Expanding: resize immediately so iframe is big enough for animation calculateAndSetIframeDimensions(true); } else if (!isExpanded && previousExpanded) { // Collapsing: delay resize until animation completes calculateAndSetIframeDimensions(false); } else { // Size change while in same state (e.g., bubble width change) calculateAndSetIframeDimensions(true); } break; } case "FOURTEEN_IFRAME_SHOW_COIuY89DKU6nK6Jb": { iframe.style.display = "block"; break; } case "FOURTEEN_IFRAME_SET_POSITION_COIuY89DKU6nK6Jb_bottom-left": { anchorPosition = 'bottom-left'; iframe.style.right = "unset"; iframe.style.left = "0px"; break; } case "FOURTEEN_IFRAME_SET_POSITION_COIuY89DKU6nK6Jb_bottom-right": { anchorPosition = 'bottom-right'; iframe.style.right = "0px"; iframe.style.left = "unset"; break; } case "FOURTEEN_IFRAME_HIDE_COIuY89DKU6nK6Jb": { iframe.style.display = "none"; break; } default: { break; } } }); }; if (document.readyState === "complete") { load(); } else { document.addEventListener("readystatechange", () => { if (document.readyState === "complete") { load(); } }); } }, setMetadata: (metadata) => { sendMessage({ metadata }); }, setUser: (emailOrObject, name) => { if (typeof emailOrObject === 'string') { sendMessage({ type: 'FOURTEEN_SET_USER', user: { email: emailOrObject, name } }); } else { sendMessage({ type: 'FOURTEEN_SET_USER', user: emailOrObject }); } }, setUserSecure: (encrypted) => { sendMessage({ type: 'FOURTEEN_SET_USER_SECURE', user: { encrypted } }); }, logout: () => { sendMessage({ type: 'FOURTEEN_LOGOUT_USER' }); }, setStyle: (style) => { sendMessage({ type: 'FOURTEEN_SET_STYLE_COIuY89DKU6nK6Jb', style }); }, openPopup: () => { sendMessage({ type: 'FOURTEEN_EMBED_OPEN_POPUP_COIuY89DKU6nK6Jb' }); }, closePopup: () => { sendMessage({ type: 'FOURTEEN_EMBED_CLOSE_POPUP_COIuY89DKU6nK6Jb' }); }, onOpenChange: (callback) => { onOpenChangeCallback = callback; }, openOpenChange: (callback) => { onOpenChangeCallback = callback; }, onNotificationsChange: (callback) => { onNotificationsChangeCallback = callback; }, onReady: (callback) => { if (isEmbedReady) { callback(); } else { onReadyCallbacks.push(callback); } } }; }; // Unique instance window.fourteenEmbed_COIuY89DKU6nK6Jb = window.fourteenEmbed_COIuY89DKU6nK6Jb || getConfig(); // If this is the first instance, also assign it to the global window.fourteen if (!window.fourteen) { window.fourteen = window.fourteenEmbed_COIuY89DKU6nK6Jb; } // Initialize the widget window.fourteenEmbed_COIuY89DKU6nK6Jb.init(); })();