@ingridsandev/chatbot-widget
Version:
A framework-agnostic chatbot widget using Web Components
270 lines (223 loc) • 7.1 kB
JavaScript
import EventEmitter from '../utils/event-emitter.js';
import Storage from '../utils/storage.js';
class ChatInterface extends EventEmitter {
constructor(config = {}) {
super();
this.config = {
placeholder: 'Type your message...',
showTimestamp: true,
maxMessages: 100,
...config
};
this.messages = [];
this.storage = new Storage();
this.isTyping = false;
// Load saved messages
this.loadMessages();
}
render() {
const container = document.createElement('div');
container.className = 'chatbot-chat-interface';
// Messages area
this.messagesContainer = document.createElement('div');
this.messagesContainer.className = 'chatbot-messages';
// Typing indicator
this.typingIndicator = document.createElement('div');
this.typingIndicator.className = 'chatbot-typing';
this.typingIndicator.textContent = 'AI is typing';
// Input area
const inputArea = document.createElement('div');
inputArea.className = 'chatbot-input-area';
const inputContainer = document.createElement('div');
inputContainer.className = 'chatbot-input-container';
this.input = document.createElement('textarea');
this.input.className = 'chatbot-input';
this.input.placeholder = this.config.placeholder;
this.input.rows = 1;
this.sendButton = document.createElement('button');
this.sendButton.className = 'chatbot-send-button';
this.sendButton.innerHTML = '➤';
this.sendButton.disabled = true;
// Event listeners
this.setupEventListeners();
// Assembly
inputContainer.appendChild(this.input);
inputContainer.appendChild(this.sendButton);
inputArea.appendChild(inputContainer);
container.appendChild(this.messagesContainer);
container.appendChild(this.typingIndicator);
container.appendChild(inputArea);
// Render existing messages
this.renderMessages();
return container;
}
setupEventListeners() {
// Input handling
this.input.addEventListener('input', () => {
this.adjustTextareaHeight();
this.updateSendButton();
});
this.input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
// Send button
this.sendButton.addEventListener('click', () => {
this.sendMessage();
});
}
adjustTextareaHeight() {
this.input.style.height = 'auto';
this.input.style.height = Math.min(this.input.scrollHeight, 100) + 'px';
}
updateSendButton() {
const hasText = this.input.value.trim().length > 0;
this.sendButton.disabled = !hasText || this.isTyping;
}
sendMessage() {
const text = this.input.value.trim();
if (!text || this.isTyping) return;
this.addMessage(text, true);
this.input.value = '';
this.adjustTextareaHeight();
this.updateSendButton();
// Emit message send event
this.emit('messageSend', { message: text });
}
addMessage(text, isUser = false, agent = null, timestamp = null) {
try {
const message = {
id: Date.now() + Math.random(),
text,
isUser,
agent,
timestamp: timestamp || new Date()
};
this.messages.push(message);
// Limit messages
if (this.messages.length > this.config.maxMessages) {
this.messages = this.messages.slice(-this.config.maxMessages);
}
this.renderMessage(message);
this.scrollToBottom();
this.saveMessages();
this.emit('messageAdded', { message });
} catch (error) {
console.warn('Error adding message:', error);
}
}
renderMessages() {
this.messagesContainer.innerHTML = '';
this.messages.forEach(message => {
this.renderMessage(message, false);
});
this.scrollToBottom();
}
renderMessage(message, animate = true) {
try {
const messageEl = document.createElement('div');
messageEl.className = `chatbot-message ${message.isUser ? 'user' : 'bot'}`;
// Avatar
const avatar = document.createElement('div');
avatar.className = 'chatbot-message-avatar';
if (message.isUser) {
avatar.textContent = '👤';
} else if (message.agent && message.agent.avatar) {
avatar.textContent = message.agent.avatar;
} else {
avatar.textContent = '🤖';
}
// Content
const content = document.createElement('div');
content.className = 'chatbot-message-content';
content.textContent = message.text;
// Timestamp
if (this.config.showTimestamp) {
const time = document.createElement('div');
time.className = 'chatbot-message-time';
time.textContent = this.formatTime(message.timestamp);
content.appendChild(time);
}
messageEl.appendChild(avatar);
messageEl.appendChild(content);
if (!animate) {
messageEl.style.animation = 'none';
}
this.messagesContainer.appendChild(messageEl);
} catch (error) {
console.warn('Error rendering message:', error);
}
}
formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
scrollToBottom() {
setTimeout(() => {
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
}, 50);
}
showTyping() {
this.isTyping = true;
this.typingIndicator.classList.add('show');
this.updateSendButton();
this.scrollToBottom();
}
hideTyping() {
this.isTyping = false;
this.typingIndicator.classList.remove('show');
this.updateSendButton();
}
clearMessages() {
this.messages = [];
this.messagesContainer.innerHTML = '';
this.saveMessages();
this.emit('messagesCleared');
}
saveMessages() {
try {
const messagesToSave = this.messages.slice(-20);
this.storage.set('messages', messagesToSave);
} catch (error) {
console.warn('Error saving messages:', error);
}
}
loadMessages() {
try {
const savedMessages = this.storage.get('messages', []);
this.messages = savedMessages.map(msg => ({
...msg,
timestamp: new Date(msg.timestamp)
}));
} catch (error) {
console.warn('Error loading messages:', error);
this.messages = [];
}
}
/**
* Clean up event listeners and references
*/
destroy() {
try {
if (this.input) {
this.input.removeEventListener('input', this.adjustTextareaHeight);
this.input.removeEventListener('keydown', this.sendMessage);
}
if (this.sendButton) {
this.sendButton.removeEventListener('click', this.sendMessage);
}
this.removeAllListeners();
} catch (error) {
console.warn('Error during chat interface cleanup:', error);
}
}
setPlaceholder(placeholder) {
this.config.placeholder = placeholder;
if (this.input) {
this.input.placeholder = placeholder;
}
}
}
export default ChatInterface;