UNPKG

@ingridsandev/chatbot-widget

Version:

A framework-agnostic chatbot widget using Web Components

270 lines (223 loc) 7.1 kB
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;