ultra-chat-bot
Version:
made with ❤️ by Mohamed Majri
1,247 lines (1,239 loc) • 94.3 kB
JavaScript
'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(() => {