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
JavaScript
/**
* 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');
}
}
});
}
}