UNPKG

ultra-chat-bot

Version:
1,267 lines (1,250 loc) 114 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('lucide-react'), require('socket.io-client')) : typeof define === 'function' && define.amd ? define(['exports', 'react', 'lucide-react', 'socket.io-client'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.UltraChatBot = {}, global.React, global.LucideReact, global.io)); })(this, (function (exports, React, lucideReact, socket_ioClient) { 'use strict'; var jsxRuntime = {exports: {}}; var reactJsxRuntime_production = {}; /** * @license React * react-jsx-runtime.production.js * * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var hasRequiredReactJsxRuntime_production; function requireReactJsxRuntime_production () { if (hasRequiredReactJsxRuntime_production) return reactJsxRuntime_production; hasRequiredReactJsxRuntime_production = 1; var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"); function jsxProd(type, config, maybeKey) { var key = null; void 0 !== maybeKey && (key = "" + maybeKey); void 0 !== config.key && (key = "" + config.key); if ("key" in config) { maybeKey = {}; for (var propName in config) "key" !== propName && (maybeKey[propName] = config[propName]); } else maybeKey = config; config = maybeKey.ref; return { $$typeof: REACT_ELEMENT_TYPE, type: type, key: key, ref: void 0 !== config ? config : null, props: maybeKey }; } reactJsxRuntime_production.Fragment = REACT_FRAGMENT_TYPE; reactJsxRuntime_production.jsx = jsxProd; reactJsxRuntime_production.jsxs = jsxProd; return reactJsxRuntime_production; } var reactJsxRuntime_development = {}; /** * @license React * react-jsx-runtime.development.js * * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var hasRequiredReactJsxRuntime_development; function requireReactJsxRuntime_development () { if (hasRequiredReactJsxRuntime_development) return reactJsxRuntime_development; hasRequiredReactJsxRuntime_development = 1; "production" !== process.env.NODE_ENV && (function () { function getComponentNameFromType(type) { if (null == type) return null; if ("function" === typeof type) return type.$$typeof === REACT_CLIENT_REFERENCE ? null : type.displayName || type.name || null; if ("string" === typeof type) return type; switch (type) { case REACT_FRAGMENT_TYPE: return "Fragment"; case REACT_PROFILER_TYPE: return "Profiler"; case REACT_STRICT_MODE_TYPE: return "StrictMode"; case REACT_SUSPENSE_TYPE: return "Suspense"; case REACT_SUSPENSE_LIST_TYPE: return "SuspenseList"; case REACT_ACTIVITY_TYPE: return "Activity"; } if ("object" === typeof type) switch ( ("number" === typeof type.tag && console.error( "Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue." ), type.$$typeof) ) { case REACT_PORTAL_TYPE: return "Portal"; case REACT_CONTEXT_TYPE: return (type.displayName || "Context") + ".Provider"; case REACT_CONSUMER_TYPE: return (type._context.displayName || "Context") + ".Consumer"; case REACT_FORWARD_REF_TYPE: var innerType = type.render; type = type.displayName; type || ((type = innerType.displayName || innerType.name || ""), (type = "" !== type ? "ForwardRef(" + type + ")" : "ForwardRef")); return type; case REACT_MEMO_TYPE: return ( (innerType = type.displayName || null), null !== innerType ? innerType : getComponentNameFromType(type.type) || "Memo" ); case REACT_LAZY_TYPE: innerType = type._payload; type = type._init; try { return getComponentNameFromType(type(innerType)); } catch (x) {} } return null; } function testStringCoercion(value) { return "" + value; } function checkKeyStringCoercion(value) { try { testStringCoercion(value); var JSCompiler_inline_result = !1; } catch (e) { JSCompiler_inline_result = true; } if (JSCompiler_inline_result) { JSCompiler_inline_result = console; var JSCompiler_temp_const = JSCompiler_inline_result.error; var JSCompiler_inline_result$jscomp$0 = ("function" === typeof Symbol && Symbol.toStringTag && value[Symbol.toStringTag]) || value.constructor.name || "Object"; JSCompiler_temp_const.call( JSCompiler_inline_result, "The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", JSCompiler_inline_result$jscomp$0 ); return testStringCoercion(value); } } function getTaskName(type) { if (type === REACT_FRAGMENT_TYPE) return "<>"; if ( "object" === typeof type && null !== type && type.$$typeof === REACT_LAZY_TYPE ) return "<...>"; try { var name = getComponentNameFromType(type); return name ? "<" + name + ">" : "<...>"; } catch (x) { return "<...>"; } } function getOwner() { var dispatcher = ReactSharedInternals.A; return null === dispatcher ? null : dispatcher.getOwner(); } function UnknownOwner() { return Error("react-stack-top-frame"); } function hasValidKey(config) { if (hasOwnProperty.call(config, "key")) { var getter = Object.getOwnPropertyDescriptor(config, "key").get; if (getter && getter.isReactWarning) return false; } return void 0 !== config.key; } function defineKeyPropWarningGetter(props, displayName) { function warnAboutAccessingKey() { specialPropKeyWarningShown || ((specialPropKeyWarningShown = true), console.error( "%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)", displayName )); } warnAboutAccessingKey.isReactWarning = true; Object.defineProperty(props, "key", { get: warnAboutAccessingKey, configurable: true }); } function elementRefGetterWithDeprecationWarning() { var componentName = getComponentNameFromType(this.type); didWarnAboutElementRef[componentName] || ((didWarnAboutElementRef[componentName] = true), console.error( "Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release." )); componentName = this.props.ref; return void 0 !== componentName ? componentName : null; } function ReactElement( type, key, self, source, owner, props, debugStack, debugTask ) { self = props.ref; type = { $$typeof: REACT_ELEMENT_TYPE, type: type, key: key, props: props, _owner: owner }; null !== (void 0 !== self ? self : null) ? Object.defineProperty(type, "ref", { enumerable: false, get: elementRefGetterWithDeprecationWarning }) : Object.defineProperty(type, "ref", { enumerable: false, value: null }); type._store = {}; Object.defineProperty(type._store, "validated", { configurable: false, enumerable: false, writable: true, value: 0 }); Object.defineProperty(type, "_debugInfo", { configurable: false, enumerable: false, writable: true, value: null }); Object.defineProperty(type, "_debugStack", { configurable: false, enumerable: false, writable: true, value: debugStack }); Object.defineProperty(type, "_debugTask", { configurable: false, enumerable: false, writable: true, value: debugTask }); Object.freeze && (Object.freeze(type.props), Object.freeze(type)); return type; } function jsxDEVImpl( type, config, maybeKey, isStaticChildren, source, self, debugStack, debugTask ) { var children = config.children; if (void 0 !== children) if (isStaticChildren) if (isArrayImpl(children)) { for ( isStaticChildren = 0; isStaticChildren < children.length; isStaticChildren++ ) validateChildKeys(children[isStaticChildren]); Object.freeze && Object.freeze(children); } else console.error( "React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead." ); else validateChildKeys(children); if (hasOwnProperty.call(config, "key")) { children = getComponentNameFromType(type); var keys = Object.keys(config).filter(function (k) { return "key" !== k; }); isStaticChildren = 0 < keys.length ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}"; didWarnAboutKeySpread[children + isStaticChildren] || ((keys = 0 < keys.length ? "{" + keys.join(": ..., ") + ": ...}" : "{}"), console.error( 'A props object containing a "key" prop is being spread into JSX:\n let props = %s;\n <%s {...props} />\nReact keys must be passed directly to JSX without using spread:\n let props = %s;\n <%s key={someKey} {...props} />', isStaticChildren, children, keys, children ), (didWarnAboutKeySpread[children + isStaticChildren] = true)); } children = null; void 0 !== maybeKey && (checkKeyStringCoercion(maybeKey), (children = "" + maybeKey)); hasValidKey(config) && (checkKeyStringCoercion(config.key), (children = "" + config.key)); if ("key" in config) { maybeKey = {}; for (var propName in config) "key" !== propName && (maybeKey[propName] = config[propName]); } else maybeKey = config; children && defineKeyPropWarningGetter( maybeKey, "function" === typeof type ? type.displayName || type.name || "Unknown" : type ); return ReactElement( type, children, self, source, getOwner(), maybeKey, debugStack, debugTask ); } function validateChildKeys(node) { "object" === typeof node && null !== node && node.$$typeof === REACT_ELEMENT_TYPE && node._store && (node._store.validated = 1); } var React$1 = React, REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"); var REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = React$1.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, hasOwnProperty = Object.prototype.hasOwnProperty, isArrayImpl = Array.isArray, createTask = console.createTask ? console.createTask : function () { return null; }; React$1 = { "react-stack-bottom-frame": function (callStackForError) { return callStackForError(); } }; var specialPropKeyWarningShown; var didWarnAboutElementRef = {}; var unknownOwnerDebugStack = React$1["react-stack-bottom-frame"].bind( React$1, UnknownOwner )(); var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner)); var didWarnAboutKeySpread = {}; reactJsxRuntime_development.Fragment = REACT_FRAGMENT_TYPE; reactJsxRuntime_development.jsx = function (type, config, maybeKey, source, self) { var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++; return jsxDEVImpl( type, config, maybeKey, false, source, self, trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask ); }; reactJsxRuntime_development.jsxs = function (type, config, maybeKey, source, self) { var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++; return jsxDEVImpl( type, config, maybeKey, true, source, self, trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask ); }; })(); return reactJsxRuntime_development; } if (process.env.NODE_ENV === 'production') { jsxRuntime.exports = requireReactJsxRuntime_production(); } else { jsxRuntime.exports = requireReactJsxRuntime_development(); } var jsxRuntimeExports = jsxRuntime.exports; // Geist font family with fallbacks const GEIST_FONT_FAMILY$2 = '"Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; const Avatar = ({ style, children }) => { const baseStyles = { position: 'relative', display: 'flex', height: '2.5rem', width: '2.5rem', flexShrink: 0, overflow: 'hidden', borderRadius: '50%', ...style, }; return (jsxRuntimeExports.jsx("span", { style: baseStyles, children: children })); }; const AvatarImage = ({ src, alt }) => { const [imageLoaded, setImageLoaded] = React.useState(false); const [imageError, setImageError] = React.useState(false); const handleLoad = () => { setImageLoaded(true); setImageError(false); }; const handleError = () => { setImageError(true); setImageLoaded(false); }; if (!src || imageError) { return null; } return (jsxRuntimeExports.jsx("img", { src: src, alt: alt, style: { height: '100%', width: '100%', objectFit: 'cover', display: imageLoaded ? 'block' : 'none', }, onLoad: handleLoad, onError: handleError })); }; const AvatarFallback = ({ style, children }) => { const baseStyles = { display: 'flex', height: '100%', width: '100%', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f3f4f6', color: '#6b7280', fontSize: '0.875rem', fontWeight: '500', fontFamily: GEIST_FONT_FAMILY$2, ...style, }; return (jsxRuntimeExports.jsx("span", { style: baseStyles, children: children })); }; // Geist font family with fallbacks const GEIST_FONT_FAMILY$1 = '"Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; const Button = ({ variant = 'default', size = 'default', className, children, style, ...props }) => { const baseStyles = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', borderRadius: '0.375rem', fontSize: '0.875rem', fontWeight: '500', fontFamily: GEIST_FONT_FAMILY$1, transition: 'all 0.2s ease-in-out', cursor: 'pointer', border: 'none', textDecoration: 'none', outline: 'none', ...getVariantStyles(variant), ...getSizeStyles(size), }; return (jsxRuntimeExports.jsx("button", { style: { ...baseStyles, ...style, }, ...props, children: children })); }; function getVariantStyles(variant) { switch (variant) { case 'outline': return { backgroundColor: 'transparent', border: '1px solid #d1d5db', color: '#374151', }; case 'ghost': return { backgroundColor: 'transparent', color: '#374151', }; default: return { backgroundColor: '#3b82f6', color: 'white', }; } } function getSizeStyles(size) { switch (size) { case 'sm': return { height: '2rem', paddingLeft: '0.75rem', paddingRight: '0.75rem', fontSize: '0.75rem', }; case 'lg': return { height: '2.75rem', paddingLeft: '2rem', paddingRight: '2rem', fontSize: '1rem', }; case 'icon': return { height: '2.5rem', width: '2.5rem', padding: 0, }; default: return { height: '2.5rem', paddingLeft: '1rem', paddingRight: '1rem', }; } } const Card = ({ style, children }) => { const baseStyles = { backgroundColor: '#ffffff', border: '1px solid #e5e7eb', borderRadius: '0.5rem', boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', ...style, }; return (jsxRuntimeExports.jsx("div", { style: baseStyles, children: children })); }; const CardContent = ({ style, children }) => { const baseStyles = { padding: '1.5rem', ...style, }; return (jsxRuntimeExports.jsx("div", { style: baseStyles, children: children })); }; const CardFooter = ({ style, children }) => { const baseStyles = { display: 'flex', alignItems: 'center', padding: '1.5rem', paddingTop: '0', ...style, }; return (jsxRuntimeExports.jsx("div", { style: baseStyles, children: children })); }; const CardAction = ({ style, children }) => { const baseStyles = { paddingTop: '1rem', ...style, }; return (jsxRuntimeExports.jsx("div", { style: baseStyles, children: children })); }; // ChatSessionManager class for handling session management class ChatSessionManager { constructor(socketUrl) { this.socket = null; this.sessionId = null; this.chatId = null; this.isInitialized = false; this.eventHandlers = new Map(); this.sessionKey = 'chatbot-session'; this.sessionExpiry = 60 * 60 * 1000; // 1 hour for guests this.socketUrl = socketUrl; } // Event handling on(event, handler) { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, []); } this.eventHandlers.get(event).push(handler); } off(event, handler) { if (!this.eventHandlers.has(event)) return; if (!handler) { this.eventHandlers.delete(event); } else { const handlers = this.eventHandlers.get(event); const index = handlers.indexOf(handler); if (index > -1) { handlers.splice(index, 1); } } } emit(event, ...args) { const handlers = this.eventHandlers.get(event) || []; handlers.forEach(handler => handler(...args)); } // Session management getStoredSession() { try { const stored = localStorage.getItem(this.sessionKey); if (!stored) return null; const session = JSON.parse(stored); // Check if session has expired (only for guests) if (!session.email && Date.now() - session.timestamp > this.sessionExpiry) { this.clearStoredSession(); return null; } return session; } catch (error) { this.clearStoredSession(); return null; } } storeSession(sessionId, chatId, email) { try { const sessionData = { sessionId, chatId, timestamp: Date.now(), ...(email && { email }) }; localStorage.setItem(this.sessionKey, JSON.stringify(sessionData)); } catch (error) { // Handle localStorage errors silently } } clearStoredSession() { try { localStorage.removeItem(this.sessionKey); } catch (error) { // Handle localStorage errors silently } } // Session validation async validateSession(sessionId) { try { const response = await fetch(`${this.socketUrl}/session/${sessionId}`); const data = await response.json(); return data.valid === true; } catch (error) { return false; } } // Initialize session and connection async initialize(userInfo, options) { const storedSession = this.getStoredSession(); let sessionId = null; let chatId = null; if (storedSession) { const isValid = await this.validateSession(storedSession.sessionId); if (isValid) { sessionId = storedSession.sessionId; chatId = storedSession.chatId; } else { this.clearStoredSession(); } } // Connect to socket this.socket = socket_ioClient.io(this.socketUrl); return new Promise((resolve, reject) => { if (!this.socket) { reject(new Error('Failed to create socket connection')); return; } this.socket.on('connect', () => { var _a; this.emit('connect'); // Join with session info const joinData = { name: (userInfo === null || userInfo === void 0 ? void 0 : userInfo.firstname) && (userInfo === null || userInfo === void 0 ? void 0 : userInfo.lastname) ? `${userInfo.firstname} ${userInfo.lastname}` : "Anonymous Guest", email: (userInfo === null || userInfo === void 0 ? void 0 : userInfo.email) || null, firstname: (userInfo === null || userInfo === void 0 ? void 0 : userInfo.firstname) || null, lastname: (userInfo === null || userInfo === void 0 ? void 0 : userInfo.lastname) || null, origin: options === null || options === void 0 ? void 0 : options.companyName, visitedPaths: (options === null || options === void 0 ? void 0 : options.visitedPaths) || [], currentPath: (options === null || options === void 0 ? void 0 : options.currentPath) || window.location.pathname, ...(sessionId && { sessionId }), ...(chatId && { chatId }) }; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.emit("guest:join", joinData); }); this.socket.on('disconnect', () => { this.emit('disconnect'); }); this.socket.on("guest:joined", (data) => { this.sessionId = data.sessionId; this.chatId = data.chatId; this.isInitialized = true; // Store session this.storeSession(data.sessionId, data.chatId, userInfo === null || userInfo === void 0 ? void 0 : userInfo.email); // Set up message handlers this.setupMessageHandlers(options); // Handle chat history if provided if (data.isReconnection && data.chatHistory && data.chatHistory.length > 0) { this.emit('chatHistoryRestored', data.chatHistory); } else if (data.isReconnection) { // Reconnection but no chat history - emit empty history event // console.log('🔄 Reconnection detected but no chat history found'); this.emit('chatHistoryRestored', []); } resolve({ success: true, sessionId: data.sessionId, chatId: data.chatId, reconnected: data.reconnected || !!sessionId, isReconnection: data.isReconnection, chatHistory: data.chatHistory }); }); this.socket.on('error', (error) => { this.emit('error', error); reject(error); }); // Timeout after 10 seconds setTimeout(() => { if (!this.isInitialized) { reject(new Error('Connection timeout')); } }, 10000); }); } setupMessageHandlers(options) { if (!this.socket) return; // Message handling this.socket.on("message", (data) => { this.emit('message', data); }); // Message sent confirmation this.socket.on("message:sent", (data) => { this.emit('messageSent', data); }); // Typing indicators if (options === null || options === void 0 ? void 0 : options.showTypingIndicator) { this.socket.on("admin:startTyping", (data) => { this.emit('adminStartTyping', data); }); this.socket.on("admin:stopTyping", (data) => { this.emit('adminStopTyping', data); }); this.socket.on("typing", (data) => { this.emit('typing', data); }); } // Message seen events this.socket.on("messages:seenByAdmin", (data) => { this.emit('messagesSeenByAdmin', data); }); this.socket.on("messages:marked", (data) => { this.emit('messagesMarked', data); }); // Chat history events this.socket.on("guest:chatHistory", (data) => { this.emit('chatHistory', data.messages); }); } // Send message sendMessage(text, userInfo, options) { if (!this.socket || !this.isInitialized) { throw new Error('Session not initialized'); } const messageData = { text, sender: "user", origin: options === null || options === void 0 ? void 0 : options.companyName, timestamp: new Date().toISOString(), paths: (options === null || options === void 0 ? void 0 : options.visitedPaths) || [], currentPath: (options === null || options === void 0 ? void 0 : options.currentPath) || window.location.pathname, }; // Add user info if available if ((userInfo === null || userInfo === void 0 ? void 0 : userInfo.firstname) && (userInfo === null || userInfo === void 0 ? void 0 : userInfo.lastname) && (userInfo === null || userInfo === void 0 ? void 0 : userInfo.email)) { messageData.firstname = userInfo.firstname; messageData.lastname = userInfo.lastname; messageData.email = userInfo.email; } this.socket.emit("guest:message", messageData); } // Typing indicators startTyping() { if (this.socket && this.isInitialized) { this.socket.emit('guest:startTyping'); } } stopTyping() { if (this.socket && this.isInitialized) { this.socket.emit('guest:stopTyping'); } } // Mark messages as seen markMessagesSeen(messageIds) { if (this.socket && this.isInitialized) { this.socket.emit("guest:markSeen", { messageIds }); } } // Request chat history getChatHistory() { if (this.socket && this.isInitialized) { this.socket.emit("guest:getChatHistory"); } } // Send path navigation update updateNavigationPath(currentPath, visitedPaths, userInfo) { if (this.socket && this.isInitialized) { const pathData = { currentPath, visitedPaths, timestamp: new Date().toISOString(), }; // Add user info if available if ((userInfo === null || userInfo === void 0 ? void 0 : userInfo.firstname) && (userInfo === null || userInfo === void 0 ? void 0 : userInfo.lastname) && (userInfo === null || userInfo === void 0 ? void 0 : userInfo.email)) { pathData.firstname = userInfo.firstname; pathData.lastname = userInfo.lastname; pathData.email = userInfo.email; } this.socket.emit("guest:pathUpdate", pathData); } } // Connection status get isConnected() { var _a; return ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) || false; } get currentSessionId() { return this.sessionId; } get currentChatId() { return this.chatId; } // Cleanup disconnect() { if (this.socket) { this.socket.disconnect(); this.socket = null; } this.isInitialized = false; this.sessionId = null; this.chatId = null; this.eventHandlers.clear(); } } // const socketUrl = "http://localhost:5001"; const socketUrl = "https://chat-bot.gentauronline.com"; const fallBackPrimaryColor = "#003299"; const fallBackSecondaryColor = "#efb100"; const audioUrl = "https://cdn.gentaur.com/chat-bots/Voicy_Telegram%20SFX%205.mp3"; // Geist font family with fallbacks const GEIST_FONT_FAMILY = '"Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; // Function to load Geist font from Google Fonts const loadGeistFont = () => { // Check if font is already loaded if (document.querySelector('link[href*="fonts.googleapis.com"][href*="Geist"]')) { return; } // Create preconnect links for better performance const preconnect1 = document.createElement('link'); preconnect1.rel = 'preconnect'; preconnect1.href = 'https://fonts.googleapis.com'; const preconnect2 = document.createElement('link'); preconnect2.rel = 'preconnect'; preconnect2.href = 'https://fonts.gstatic.com'; preconnect2.crossOrigin = 'anonymous'; // Create link element for Geist font const fontLink = document.createElement('link'); fontLink.rel = 'stylesheet'; fontLink.href = 'https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap'; // Add to document head document.head.appendChild(preconnect1); document.head.appendChild(preconnect2); document.head.appendChild(fontLink); }; // Function to load custom animations const loadCustomAnimations = () => { // Check if animations are already loaded if (document.querySelector('#chatbot-animations')) { return; } const style = document.createElement('style'); style.id = 'chatbot-animations'; style.textContent = ` @keyframes slideIn { 0% { width: 0; opacity: 0; } 100% { width: 100%; opacity: 1; } } @keyframes bounce { 0%, 20%, 50%, 80%, 100% { transform: translateY(0); } 40% { transform: translateY(-6px); } 60% { transform: translateY(-3px); } } @keyframes pulse { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.05); opacity: 0.9; } 100% { transform: scale(1); opacity: 1; } } `; document.head.appendChild(style); }; // Function to darken a hex color function darkenColor(hex, percent = 20) { // Remove # if present hex = hex.replace('#', ''); // Parse RGB values const r = parseInt(hex.substr(0, 2), 16); const g = parseInt(hex.substr(2, 2), 16); const b = parseInt(hex.substr(4, 2), 16); // Darken by reducing RGB values const darkenedR = Math.max(0, Math.floor(r * (100 - percent) / 100)); const darkenedG = Math.max(0, Math.floor(g * (100 - percent) / 100)); const darkenedB = Math.max(0, Math.floor(b * (100 - percent) / 100)); // Convert back to hex return `#${darkenedR.toString(16).padStart(2, '0')}${darkenedG.toString(16).padStart(2, '0')}${darkenedB.toString(16).padStart(2, '0')}`; } function formatTimeToHHMM() { const date = new Date(); const hours = date.getUTCHours().toString().padStart(2, "0"); const minutes = date.getUTCMinutes().toString().padStart(2, "0"); return `${hours}:${minutes} UTC`; } function formatChatHistoryMessages(chatHistory, botAvatar, userAvatar) { return chatHistory.map((msg) => ({ id: msg._id || "msg-" + Date.now() + Math.random(), text: msg.text, sender: (msg.sender === "admin" || msg.sender === "system" || msg.sender === "bot") ? "bot" : "user", timestamp: new Date(msg.timestamp), avatar: (msg.sender === "admin" || msg.sender === "system" || msg.sender === "bot") ? botAvatar : userAvatar, seenByAdmin: msg.seenByAdmin || false, seenByGuest: msg.seenByGuest || false, seenAt: msg.seenAt ? new Date(msg.seenAt) : undefined, originalSender: msg.sender, senderName: msg.senderName, })); } const ChatBot = ({ UID, config = {}, onMessageSent, onMessageReceived, onConnect, onDisconnect, className, }) => { const [serverConfig, setServerConfig] = React.useState({}); const [isConfigLoading, setIsConfigLoading] = React.useState(true); const mergedConfig = React.useMemo(() => { return { ...serverConfig, ...config }; }, [serverConfig, config]); const [isOpen, setIsOpen] = React.useState(false); const [activeTab, setActiveTab] = React.useState("home"); const [messages, setMessages] = React.useState([]); const [inputText, setInputText] = React.useState(""); const [isConnected, setIsConnected] = React.useState(false); const [isAdminTyping, setIsAdminTyping] = React.useState(false); const [chatSessionManager, setChatSessionManager] = React.useState(null); const [unseenBotMessageCount, setUnseenBotMessageCount] = React.useState(0); const [sessionInfo, setSessionInfo] = React.useState({}); const messagesEndRef = React.useRef(null); React.useRef(null); const [isHoveringChatButton, setIsHoveringChatButton] = React.useState(false); const [isAnimating, setIsAnimating] = React.useState(false); // Typing system refs and state const typingTimeoutRef = React.useRef(null); const isCurrentlyTyping = React.useRef(false); const [hasUserSentMessage, setHasUserSentMessage] = React.useState(false); // Seen system refs and state const unseenAdminMessages = React.useRef(new Set()); React.useRef(null); const pendingPathUpdates = React.useRef([]); const lastSentPath = React.useRef(""); const pathUpdateTimeoutRef = React.useRef(null); // Track all visited paths with localStorage persistence and session timeout const [visitedPaths, setVisitedPaths] = React.useState([]); const [pathsInitialized, setPathsInitialized] = React.useState(false); const updateVisitedPaths = (newPath) => { setVisitedPaths(prev => { let updated = prev; if (!prev.includes(newPath)) { updated = [...prev, newPath]; try { localStorage.setItem('chatbot-visited-paths', JSON.stringify(updated)); localStorage.setItem('chatbot-paths-timestamp', Date.now().toString()); } catch (error) { // Handle localStorage errors silently } } else { try { localStorage.setItem('chatbot-paths-timestamp', Date.now().toString()); } catch (error) { // Handle localStorage errors silently } } debouncedPathUpdate(newPath, updated); return updated; }); }; // Add current path on mount and track path changes React.useEffect(() => { // Wait for paths to be initialized from localStorage if (!pathsInitialized) { return; } const currentPath = window.location.pathname; // Add current path if not already in the list updateVisitedPaths(currentPath); const handlePopState = () => { const newPath = window.location.pathname; updateVisitedPaths(newPath); }; const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function (...args) { originalPushState.apply(history, args); const newPath = window.location.pathname; updateVisitedPaths(newPath); }; history.replaceState = function (...args) { originalReplaceState.apply(history, args); const newPath = window.location.pathname; updateVisitedPaths(newPath); }; window.addEventListener('popstate', handlePopState); return () => { window.removeEventListener('popstate', handlePopState); history.pushState = originalPushState; history.replaceState = originalReplaceState; }; }, [pathsInitialized]); const isOpenRef = React.useRef(isOpen); const activeTabRef = React.useRef(activeTab); const chatSessionManagerRef = React.useRef(chatSessionManager); React.useEffect(() => { isOpenRef.current = isOpen; }, [isOpen]); React.useEffect(() => { activeTabRef.current = activeTab; }, [activeTab]); React.useEffect(() => { chatSessionManagerRef.current = chatSessionManager; if (chatSessionManager && chatSessionManager.isConnected) { processPendingPathUpdates(); } }, [chatSessionManager]); React.useEffect(() => { if (isConnected && chatSessionManager) { processPendingPathUpdates(); } }, [isConnected, chatSessionManager]); React.useEffect(() => { return () => { if (pathUpdateTimeoutRef.current) { clearTimeout(pathUpdateTimeoutRef.current); } }; }, []); // Function to fetch bot config from server const fetchBotConfig = async () => { if (!UID) { setIsConfigLoading(false); return; } try { const response = await fetch(`${socketUrl}/bot-config/${UID}`); if (response.ok) { const botConfig = await response.json(); const mappedConfig = { companyName: botConfig.companyName, botName: botConfig.botName, homeScreenMessage: botConfig.homeScreenMessage, presentationMessage: botConfig.presentationMessage, botAvatar: botConfig.botAvatar, userAvatar: botConfig.userAvatar, theme: botConfig.theme, primaryColor: botConfig.primaryColor, secondaryColor: botConfig.secondaryColor, placeholder: botConfig.placeholder, welcomeMessage: botConfig.welcomeMessage, socketUrl: socketUrl, autoConnect: botConfig.autoConnect, showTypingIndicator: botConfig.showTypingIndicator, height: botConfig.height, width: botConfig.width, position: botConfig.position, firstname: botConfig.firstname, lastname: botConfig.lastname, email: botConfig.email, audioUrl: botConfig.audioUrl, enableNotificationSound: botConfig.enableNotificationSound, }; setServerConfig(mappedConfig); } } catch (error) { // Error fetching bot config } finally { setIsConfigLoading(false); } }; React.useEffect(() => { fetchBotConfig(); }, [UID]); // Load Geist font and custom animations when component mounts React.useEffect(() => { loadGeistFont(); loadCustomAnimations(); }, []); // Initialize visited paths from localStorage after config is loaded React.useEffect(() => { if (isConfigLoading) return; // Check localStorage for existing paths and validate session timeout if (typeof window !== 'undefined') { try { const saved = localStorage.getItem('chatbot-visited-paths'); const lastUpdate = localStorage.getItem('chatbot-paths-timestamp'); if (saved && lastUpdate) { const sessionTimeout = mergedConfig.sessionTimeoutHours ? mergedConfig.sessionTimeoutHours * 60 * 60 * 1000 : 2 * 60 * 60 * 1000; // Default: 2 hours in milliseconds const timeSinceLastUpdate = Date.now() - parseInt(lastUpdate); // If session is still valid, restore saved paths if (timeSinceLastUpdate < sessionTimeout) { const parsedPaths = JSON.parse(saved); setVisitedPaths(parsedPaths); } else { // Session expired, clear old data localStorage.removeItem('chatbot-visited-paths'); localStorage.removeItem('chatbot-paths-timestamp'); } } } catch (error) { // Clear corrupted data localStorage.removeItem('chatbot-visited-paths'); localStorage.removeItem('chatbot-paths-timestamp'); } } // Mark paths as initialized setPathsInitialized(true); }, [isConfigLoading, mergedConfig.sessionTimeoutHours]); React.useEffect(() => { if (pathsInitialized && typeof window !== 'undefined') { const currentPath = window.location.pathname; immediatePathUpdate(currentPath, visitedPaths); } }, [pathsInitialized, visitedPaths]); // Aggressive fallback check for welcome message React.useEffect(() => { if (isConfigLoading || !mergedConfig.welcomeMessage) return; const fallbackTimer = setTimeout(() => { setMessages(currentMessages => { const hasRealMessages = currentMessages.some(msg => !msg.id.startsWith("welcome-")); const hasWelcomeMessage = currentMessages.some(msg => msg.id.startsWith("welcome-")); if (!hasRealMessages && !hasWelcomeMessage) { const welcomeMsg = { id: "welcome-" + Date.now(), text: mergedConfig.welcomeMessage, sender: "bot", timestamp: new Date(), avatar: mergedConfig.botAvatar, }; return [welcomeMsg]; } return currentMessages; }); }, 2000); return () => clearTimeout(fallbackTimer); }, [isConfigLoading, mergedConfig.welcomeMessage, mergedConfig.botAvatar, chatSessionManager, isConnected]); // Specific check for session establishment without chat history React.useEffect(() => { if (!chatSessionManager || !isConnected || isConfigLoading || !mergedConfig.welcomeMessage) return; const sessionEstablishedTimer = setTimeout(() => { setMessages(currentMessages => { const hasRealMessages = currentMessages.some(msg => !msg.id.startsWith("welcome-")); const hasWelcomeMessage = currentMessages.some(msg => msg.id.startsWith("welcome-")); if (!hasRealMessages && !hasWelcomeMessage) { const welcomeMsg = { id: "welcome-" + Date.now(), text: mergedConfig.welcomeMessage, sender: "bot", timestamp: new Date(), avatar: mergedConfig.botAvatar, }; return [welcomeMsg]; } return currentMessages; }); }, 500); return () => clearTimeout(sessionEstablishedTimer); }, [chatSessionManager, isConnected, isConfigLoading, mergedConfig.welcomeMessage, sessionInfo]); React.useEffect(() => { if (isConfigLoading) return; if (mergedConfig.autoConnect && !chatSessionManager) { initializeChatSession(); } return () => { if (chatSessionManager) { chatSessionManager.disconnect(); } }; }, [isConfigLoading, mergedConfig.autoConnect, chatSessionManager]); React.useEffect(() => { if (isConfigLoading || !mergedConfig.welcomeMessage) { return; } const timers = [ setTimeout(() => { setMessages(currentMessages => { const welcomeExis