UNPKG

ultra-chat-bot

Version:
1,247 lines (1,239 loc) 94.3 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var lucideReact = require('lucide-react'); var React = require('react'); var socket_ioClient = require('socket.io-client'); // 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 (jsxRuntime.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 (jsxRuntime.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 (jsxRuntime.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 (jsxRuntime.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 (jsxRuntime.jsx("div", { style: baseStyles, children: children })); }; const CardContent = ({ style, children }) => { const baseStyles = { padding: '1.5rem', ...style, }; return (jsxRuntime.jsx("div", { style: baseStyles, children: children })); }; const CardFooter = ({ style, children }) => { const baseStyles = { display: 'flex', alignItems: 'center', padding: '1.5rem', paddingTop: '0', ...style, }; return (jsxRuntime.jsx("div", { style: baseStyles, children: children })); }; const CardAction = ({ style, children }) => { const baseStyles = { paddingTop: '1rem', ...style, }; return (jsxRuntime.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 welcomeExists = currentMessages.some(msg => msg.id.startsWith("welcome-")); const hasRealMessages = currentMessages.some(msg => !msg.id.startsWith("welcome-")); if (!welcomeExists && !hasRealMessages) { const welcomeMsg = { id: "welcome-" + Date.now(), text: mergedConfig.welcomeMessage, sender: "bot", timestamp: new Date(), avatar: mergedConfig.botAvatar, }; return [welcomeMsg]; } return currentMessages; }); }, 200), setTimeout(() => { setMessages(currentMessages => { const welcomeExists = currentMessages.some(msg => msg.id.startsWith("welcome-")); const hasRealMessages = currentMessages.some(msg => !msg.id.startsWith("welcome-")); if (!welcomeExists && !hasRealMessages) { const welcomeMsg = { id: "welcome-" + Date.now(), text: mergedConfig.welcomeMessage, sender: "bot", timestamp: new Date(), avatar: mergedConfig.botAvatar, }; return [welcomeMsg]; } return currentMessages; }); }, 800) ]; return () => { timers.forEach(timer => clearTimeout(timer)); }; }, [isConfigLoading, mergedConfig.welcomeMessage, mergedConfig.botAvatar, chatSessionManager, isConnected]); React.useEffect(() => { scrollToBottom(); }, [messages]); React.useEffect(() => { if (isOpen && activeTab === "chat") { setUnseenBotMessageCount(0); setTimeout(() => { markUnseenBotMessagesAsSeen(); }, 100); } }, [isOpen, activeTab, messages, chatSessionManager]); const initializeChatSession = async () => { if (!mergedConfig.socketUrl) { return; } // Create new session manager const sessionManager = new ChatSessionManager(mergedConfig.socketUrl); // Set up event handlers sessionManager.on('connect', () => { setIsConnected(true); onConnect === null || onConnect === void 0 ? void 0 : onConnect(); }); sessionManager.on('disconnect', () => { setIsConnected(false); setIsAdminTyping(false); onDisconnect === null || onDisconnect === void 0 ? void 0 : onDisconnect(); }); // Handle incoming messages sessionManager.on('message', (data) => { const newMessage = { id: data._id || "msg-" + Date.now() + Math.random(), text: data.text, sender: (data.sender === "bot" || data.sender === "system" || data.sender === "admin") ? "bot" : "user", timestamp: new Date(data.timestamp), avatar: (data.sender === "bot" || data.sender === "system" || data.sender === "admin") ? mergedConfig.botAvatar : mergedConfig.userAvatar, seenByAdmin: data.seenByAdmin || false, seenByGuest: data.seenByGuest || false, seenAt: data.seenAt ? new Date(data.seenAt) : undefined, // Preserve original sender info to distinguish admin from bot messages originalSender: data.sender, senderName: data.senderName, }; setMessages((prev) => [...prev, newMessage]); // Play sound for bot/system/admin messages if (data.sender === "bot" || data.sender === "system" || data.sender === "admin") { // Play sound if enabled in config (default is true) if (mergedConfig.enableNotificationSound !== false) { try { const soundUrl = mergedConfig.audioUrl || audioUrl; const audio = new Audio(soundUrl); audio.play().catch(e => { // Silently handle audio play failures in production if (process.env.NODE_ENV === 'development') { console.log("Audio play failed:", e); } }); } catch (error) { // Silently handle audio creation failures in production if (process.env.NODE_ENV === 'development') { console.log("Audio creation failed:", error); } } } // Increment unseen count if chat is not open or not on chat tab if (!isOpenRef.current || activeTabRef.current !== "chat") { setUnseenBotMessageCount(prev => prev + 1); } } // console.log("new message", newMessage); onMessageReceived === null || onMessageReceived === void 0 ? void 0 : onMessageReceived(newMessage); if (newMessage.sender === "bot" && data._id) { unseenAdminMessages.current.add(data._id); if (isOpenRef.current && activeTabRef.current === "chat") { setTimeout(() => { if (chatSessionManagerRef.current && chatSessionManagerRef.current.isConnected && data._id) { chatSessionManagerRef.current.markMessagesSeen([data._id]); } }, 100); } } // Clear admin typing when message is received setIsAdminTyping(false); }); sessionManager.on('messageSent', (data) => { if (data._id && data.text) { setMessages(prev => prev.map(msg => { const isRecentMessage = msg.sender === "user" && msg.text === data.text && (Date.now() - msg.timestamp.getTime()) < 10000; if (isRecentMessage && data._id) { return { ...msg, id: data._id }; } return msg; })); } }); // Handle typing indicators sessionManager.on('adminStartTyping', (data) => { if (mergedConfig.showTypingIndicator) { setIsAdminTyping(true); } }); sessionManager.on('adminStopTyping', (data) => { if (mergedConfig.showTypingIndicator) { setIsAdminTyping(false); } }); sessionManager.on('typing', (data) => { if (mergedConfig.showTypingIndicator) { setIsAdminTyping(data.isTyping); } }); sessionManager.on('messagesSeenByAdmin', (data) => { setMessages(prev => { const updatedMessages = prev.map(msg => { const isMessageSeen = data.messageIds.includes(msg.id); return isMessageSeen ? { ...msg, seenByAdmin: true, seenAt: new Date() } : msg; }); return updatedMessages; }); }); sessionManager.on('chatHistoryRestored', (chatHistory) => { const restoredMessages = formatChatHistoryMessages(chatHistory, mergedConfig.botAvatar, mergedConfig.userAvatar); const welcomeMessage = { "id": "welcome-" + Date.now(), "text": mergedConfig.welcomeMessage, "sender": "bot", "timestamp": new Date(), "avatar": mergedConfig.botAvatar, "seenByAdmin": false, "seenByGuest": false, "originalSender": "system", "senderName": "System" }; const unseenCount = chatHistory.filter(msg => (msg.sender === "bot" || msg.sender === "system" || msg.sender === "admin") && !msg.seenByGuest && msg.text !== "New user connected to chat" && msg.text !== "Oops ! Agents are busy. Leave your email, and we’ll be in touch 😊!").length; if (!isOpenRef.current || activeTabRef.current !== "chat") { setUnseenBotMessageCount(unseenCount); } setMessages([welcomeMessage, ...restoredMessages]); if (restoredMessages.length === 0 && mergedConfig.welcomeMessage) { setTimeout(() => { setMessages(currentMessages => { const hasRealMessages = currentMessages.some(msg => !msg.id.startsWith("welcome-")); const hasWelcome = currentMessages.some(msg => msg.id.startsWith("welcome-")); if (!hasRealMessages && !hasWelcome) { const welcomeMsg = { id: "welcome-" + Date.now(), text: mergedConfig.welcomeMessage, sender: "bot", timestamp: new Date(), avatar: mergedConfig.botAvatar, }; return [welcomeMsg]; } return currentMessages; }); }, 200); } }); sessionManager.on('chatHistory', (chatHistory) => { const historyMessages = formatChatHistoryMessages(chatHistory, mergedConfig.botAvatar, mergedConfig.userAvatar); setMessages(historyMessages); if (historyMessages.length === 0 && mergedConfig.welcomeMessage) { setTimeout(() => { setMessages(currentMessages => { const hasWelcome = currentMessages.some(msg => msg.id.startsWith("welcome-")); if (!hasWelcome) { const welcomeMsg = { id: "welcome-" + Date.now(), text: mergedConfig.welcomeMessage, sender: "bot", timestamp: new Date(), avatar: mergedConfig.botAvatar, }; return [welcomeMsg]; } return currentMessages; }); }, 100); } }); try { // Initialize session with user info and options const userInfo = { firstname: mergedConfig.firstname, lastname: mergedConfig.lastname, email: mergedConfig.email, }; const options = { companyName: mergedConfig.companyName, visitedPaths: visitedPaths, currentPath: window.location.pathname, showTypingIndicator: mergedConfig.showTypingIndicator, }; const result = await sessionManager.initialize(userInfo, options); if (result.success) { setChatSessionManager(sessionManager); setSessionInfo(result); if (result.isReconnection && result.chatHistory && result.chatHistory.length > 0) { // Chat history will be restored via the event handler } } } catch (error) { console.error('Failed to initialize chat session:', error); } }; const scrollToBottom = () => { var _a; (_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: "smooth" }); }; const sendMessage = () => { if (!inputText.trim() || !chatSessionManager) return; const userMessage = { id: "user-" + Date.now(), text: inputText, sender: "user", timestamp: new Date(), avatar: mergedConfig.userAvatar, }; setMessages((prev) => [...prev, userMessage]); setHasUserSentMessage(true); stopTyping(); const isLoggedIn = mergedConfig.firstname && mergedConfig.lastname && mergedConfig.email && mergedConfig.firstname !== "" && mergedConfig.lastname !== "" && mergedConfig.email !== ""; const userInfo = isLoggedIn ? { firstname: mergedConfig.firstname, lastname: mergedConfig.lastname, email: mergedConfig.email, } : undefined; const options = { companyName: mergedConfig.companyName, visitedPaths: visitedPaths, currentPath: window.location.pathname, }; chatSessionManager.sendMessage(inputText, userInfo, options); onMessageSent === null || onMessageSent === void 0 ? void 0 : onMessageSent(inputText); setInputText(""); }; // Path tracking utility functions const sendPathUpdate = (currentPath, visitedPaths) => { if (!chatSessionManager || !chatSessionManager.isConnected) { pendingPathUpdates.current.push({ path: currentPath, paths: [...visitedPaths] }); return; } const isLoggedIn = mergedConfig.firstname && mergedConfig.lastname && mergedConfig.email && mergedConfig.firstname !== "" && mergedConfig.lastname !== "" && mergedConfig.email !== ""; const userInfo = isLoggedIn ? { firstname: mergedConfig.firstname, lastname: mergedConfig.lastname, email: mergedConfig.email, } : undefined; chatSessionManager.updateNavigationPath(currentPath, visitedPaths, userInfo); lastSentPath.current = currentPath; }; const processPendingPathUpdates = () => { if (pendingPathUpdates.current.length === 0 || !chatSessionManager || !chatSessionManager.isConnected) { return; } const updates = pendingPathUpdates.current; const latestUpdate = updates[updates.length - 1]; if (latestUpdate) { sendPathUpdate(latestUpdate.path, latestUpdate.paths); } pendingPathUpdates.current = []; }; const debouncedPathUpdate = (currentPath, visitedPaths) => { if (pathUpdateTimeoutRef.current) { clearTimeout(pathUpdateTimeoutRef.current); } pathUpdateTimeoutRef.current = setTimeout(() => { sendPathUpdate(currentPath, visitedPaths); }, 100); }; const immediatePathUpdate = (currentPath, visitedPaths) => { sendPathUpdate(currentPath, visitedPaths); }; const startTyping = () => { if (!chatSessionManager || !isConnected || !hasUserSentMessage) return; if (!isCurrentlyTyping.current) { isCurrentlyTyping.current = true; chatSessionManager.startTyping(); } }; const stopTyping = () => { if (!chatSessionManager || !isConnected) return; if (isCurrentlyTyping.current) { isCurrentlyTyping.current = false; chatSessionManager.stopTyping(); } if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); typingTimeoutRef.current = null; } }; const handleInputChange = (e) => { const value = e.target.value; setInputText(value); if (!chatSessionManager || !isConnected) return; if (!hasUserSentMessage) return; if (value.trim()) { startTyping(); if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } typingTimeoutRef.current = setTimeout(() => { stopTyping(); }, 2000); } else { stopTyping(); } }; const handleKeyPress = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; // Mark all unseen bot messages as seen const markUnseenBotMessagesAsSeen = () => { const unseenBotMessageIds = messages .filter(msg => (msg.sender === "bot" || msg.originalSender === "admin") && !msg.seenByGuest && msg.id && !msg.id.startsWith("welcome-")) .map(msg => msg.id); if (unseenBotMessageIds.length > 0 && chatSessionManager) { chatSessionManager.markMessagesSeen(unseenBotMessageIds); } }; const handleChatNowClick = () => { setActiveTab("chat"); setUnseenBotMessageCount(0); markUnseenBotMessagesAsSeen(); }; const toggleChat = () => { if (isAnimating) return; if (isOpen) { setIsAnimating(true); setTimeout(() => {