UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

294 lines (259 loc) 8.02 kB
/** * Chat Messages Component * Manages the messages container and message rendering in the chat widget */ import { Message } from './Message.js'; export class ChatMessages { constructor(widget, state, options = {}) { this.widget = widget; this.state = state; this.options = options; this.messageHistory = []; } /** * Generate the HTML for the messages container * @returns {string} Messages container HTML string */ getHTML() { return ` <div class="besper-chat-messages" id="besper-messages"> <!-- Messages and typing indicator will be dynamically added here --> </div> `; } /** * Add a message to the chat * @param {string} content - Message content * @param {string} sender - Message sender ('user' or 'bot') * @param {string} timestampOrType - Optional timestamp or message type * @returns {Message} The created message instance */ addMessage(content, sender, timestampOrType = null) { const messagesContainer = this.widget.querySelector('#besper-messages'); if (!messagesContainer) return null; // Determine if third parameter is timestamp or message type let timestamp = null; let messageType = 'normal'; if (timestampOrType) { // If it's 'offline' or other known types, treat as message type if (timestampOrType === 'offline' || timestampOrType === 'error') { messageType = timestampOrType; } else { // Otherwise treat as timestamp timestamp = timestampOrType; } } const message = new Message(content, sender, { timestamp, messageType, config: this.state.botConfig, }); // Render the message const messageHTML = message.getHTML(); messagesContainer.insertAdjacentHTML('beforeend', messageHTML); // Get the rendered element const messageElement = messagesContainer.querySelector(`#${message.id}`); // Store message in history this.messageHistory.push(message.toObject()); // Setup message event listeners message.setupEventListeners(messageElement, { onOverflowClick: () => this.handleOverflowClick(), }); // Check for table overflow after a short delay to ensure rendering (skip on mobile) if (!message.isMobileDevice()) { setTimeout(() => { message.checkTableOverflow(messageElement, warningElement => { this.setupOverflowWarning(warningElement); }); }, 100); } // Auto-scroll to the new message this.scrollToBottom(); return message; } /** * Setup overflow warning behavior * @param {HTMLElement} warningElement - The overflow warning element */ setupOverflowWarning(warningElement) { // Add auto-expanding animation warningElement.classList.add('auto-expanding'); // Auto-expand after 1.5 seconds setTimeout(() => { if (warningElement.classList.contains('auto-expanding')) { warningElement.classList.remove('auto-expanding'); this.handleOverflowClick(); } }, 1500); } /** * Handle overflow warning click */ handleOverflowClick() { if (this.options.onExpandView) { this.options.onExpandView(); } } /** * Add welcome message if configured */ addWelcomeMessage() { const welcomeMessage = this.state.botConfig?.welcomeMessage; if (welcomeMessage) { this.addMessage(welcomeMessage, 'bot'); } } /** * Add an offline message when session creation fails */ addOfflineMessage() { const offlineMessage = "I'm currently experiencing connection issues. Please try again in a moment."; this.addMessage(offlineMessage, 'bot', 'offline'); } /** * Clear all messages */ clearMessages() { const messagesContainer = this.widget.querySelector('#besper-messages'); if (messagesContainer) { messagesContainer.innerHTML = ''; } this.messageHistory = []; } /** * Show typing indicator */ showTypingIndicator() { this.hideTypingIndicator(); // Remove any existing indicator const messagesContainer = this.widget.querySelector('#besper-messages'); if (!messagesContainer) return; const typingIndicator = ` <div class="besper-typing-indicator" id="besper-typing"> <div class="besper-message besper-bot"> <div class="besper-message-avatar besper-bot" ${ !this.state.botConfig?.logoUrl ? 'style="display: none;"' : '' }> ${ this.state.botConfig?.logoUrl ? `<img src="${this.state.botConfig.logoUrl}" alt="Bot Logo" class="besper-avatar-image">` : '' } </div> <div class="besper-message-content"> <div class="besper-typing-dots"> <span></span> <span></span> <span></span> </div> </div> </div> </div> `; messagesContainer.insertAdjacentHTML('beforeend', typingIndicator); this.scrollToBottom(); } /** * Hide typing indicator */ hideTypingIndicator() { const typingIndicator = this.widget.querySelector('#besper-typing'); if (typingIndicator) { typingIndicator.remove(); } } /** * Scroll to the bottom of the messages container */ scrollToBottom() { const messagesContainer = this.widget.querySelector('#besper-messages'); if (messagesContainer) { messagesContainer.scrollTop = messagesContainer.scrollHeight; } } /** * Get message history * @returns {Array} Array of message objects */ getMessageHistory() { return [...this.messageHistory]; } /** * Set message history (useful for restoring conversation) * @param {Array} history - Array of message objects */ setMessageHistory(history) { this.clearMessages(); this.messageHistory = []; if (Array.isArray(history)) { history.forEach(messageData => { this.addMessage( messageData.content, messageData.sender, messageData.timestamp ); }); } } /** * Export conversation as text * @returns {string} Conversation text */ exportConversation() { return this.messageHistory .map(msg => { const time = new Date(msg.timestamp).toLocaleString(); const sender = msg.sender === 'user' ? 'You' : 'Bot'; // Strip HTML tags for plain text export const content = msg.content.replace(/<[^>]*>/g, ''); return `[${time}] ${sender}: ${content}`; }) .join('\n'); } /** * Check if there are any messages * @returns {boolean} Whether there are messages */ hasMessages() { return this.messageHistory.length > 0; } /** * Get the last message * @returns {Object|null} Last message object or null */ getLastMessage() { return this.messageHistory.length > 0 ? this.messageHistory[this.messageHistory.length - 1] : null; } /** * Update view state for expanded/normal mode * @param {string} viewState - 'normal' or 'expanded' */ updateViewState(viewState) { const chatContainer = this.widget.querySelector('#besper-chat-container'); if (!chatContainer) return; if (viewState === 'expanded') { chatContainer.classList.add('expanded'); } else { chatContainer.classList.remove('expanded'); } // Update overflow warnings const warnings = this.widget.querySelectorAll( '.besper-table-overflow-warning' ); warnings.forEach(warning => { const textElement = warning.querySelector('.besper-overflow-text'); if (textElement) { if (viewState === 'expanded') { textElement.textContent = 'Return to normal view'; warning.classList.add('expanded'); } else { textElement.textContent = 'Optimize view for better readability (Click to expand)'; warning.classList.remove('expanded'); } } }); } }