UNPKG

@ingridsandev/chatbot-widget

Version:

A framework-agnostic chatbot widget using Web Components

383 lines (322 loc) 10.4 kB
import ChatInterface from './chat-interface.js'; import AgentSelector from './agent-selector.js'; import EventEmitter from '../utils/event-emitter.js'; import Storage from '../utils/storage.js'; import '../styles/chatbot.css'; import '../styles/themes.css'; class ChatbotWidget extends HTMLElement { constructor() { super(); this.eventEmitter = new EventEmitter(); this.storage = new Storage(); this.isOpen = false; // Default configuration this.config = { agents: [{ id: 'default', name: 'AI Assistant', avatar: '🤖' }], theme: 'light', position: 'bottom-right', title: 'AI Assistant', placeholder: 'Type your message...', open: false }; // Bind methods this.handleMessageSend = this.handleMessageSend.bind(this); this.handleAgentChange = this.handleAgentChange.bind(this); // Store bound event handlers for cleanup this.boundClickOutside = this.handleClickOutside.bind(this); this.boundKeydown = this.handleKeydown.bind(this); } static get observedAttributes() { return ['agents', 'theme', 'position', 'open', 'title', 'placeholder']; } connectedCallback() { this.loadConfig(); this.render(); this.setupEventListeners(); // Restore open state if (this.config.open || this.storage.get('isOpen', false)) { setTimeout(() => this.open(), 100); } } disconnectedCallback() { this.cleanup(); } attributeChangedCallback(name, oldValue, newValue) { if (oldValue === newValue) return; try { switch (name) { case 'agents': try { this.config.agents = JSON.parse(newValue); this.updateAgents(); } catch { console.warn('Invalid agents JSON:', newValue); } break; case 'theme': this.config.theme = newValue; this.updateTheme(); break; case 'position': this.config.position = newValue; this.updatePosition(); break; case 'open': this.config.open = newValue === 'true' || newValue === ''; if (this.config.open) { this.open(); } else { this.close(); } break; case 'title': this.config.title = newValue; this.updateTitle(); break; case 'placeholder': this.config.placeholder = newValue; this.updatePlaceholder(); break; } } catch (error) { console.warn(`Error handling attribute change for ${name}:`, error); } } loadConfig() { // Load from attributes const agentsAttr = this.getAttribute('agents'); if (agentsAttr) { try { this.config.agents = JSON.parse(agentsAttr); } catch { console.warn('Invalid agents attribute:', agentsAttr); } } this.config.theme = this.getAttribute('theme') || this.config.theme; this.config.position = this.getAttribute('position') || this.config.position; this.config.title = this.getAttribute('title') || this.config.title; this.config.placeholder = this.getAttribute('placeholder') || this.config.placeholder; this.config.open = this.hasAttribute('open'); } render() { this.className = `chatbot-widget position-${this.config.position} theme-${this.config.theme}`; // Create trigger button this.triggerButton = document.createElement('button'); this.triggerButton.className = 'chatbot-trigger'; this.triggerButton.innerHTML = '💬'; this.triggerButton.setAttribute('aria-label', 'Open chat'); // Create chat container this.chatContainer = document.createElement('div'); this.chatContainer.className = 'chatbot-container'; // Header const header = document.createElement('div'); header.className = 'chatbot-header'; this.titleElement = document.createElement('h3'); this.titleElement.className = 'chatbot-title'; this.titleElement.textContent = this.config.title; const closeButton = document.createElement('button'); closeButton.className = 'chatbot-close'; closeButton.innerHTML = '✕'; closeButton.setAttribute('aria-label', 'Close chat'); header.appendChild(this.titleElement); header.appendChild(closeButton); // Agent selector this.agentSelector = new AgentSelector(this.config.agents); const agentSelectorEl = this.agentSelector.render(); // Chat interface this.chatInterface = new ChatInterface({ placeholder: this.config.placeholder }); const chatInterfaceEl = this.chatInterface.render(); // Assembly this.chatContainer.appendChild(header); this.chatContainer.appendChild(agentSelectorEl); this.chatContainer.appendChild(chatInterfaceEl); this.appendChild(this.triggerButton); this.appendChild(this.chatContainer); // Event listeners this.triggerButton.addEventListener('click', () => this.toggle()); closeButton.addEventListener('click', () => this.close()); } setupEventListeners() { // Chat interface events this.chatInterface.on('messageSend', this.handleMessageSend); // Agent selector events this.agentSelector.on('agentChange', this.handleAgentChange); // Click outside to close - store reference for cleanup document.addEventListener('click', this.boundClickOutside); // Escape key to close - store reference for cleanup document.addEventListener('keydown', this.boundKeydown); } handleClickOutside(e) { if (this.isOpen && !this.contains(e.target)) { this.close(); } } handleKeydown(e) { if (e.key === 'Escape' && this.isOpen) { this.close(); } } handleMessageSend(data) { const currentAgent = this.agentSelector.getCurrentAgent(); // Emit custom event const event = new CustomEvent('messageSend', { detail: { message: data.message, agent: currentAgent }, bubbles: true }); this.dispatchEvent(event); } handleAgentChange(data) { // Emit custom event const event = new CustomEvent('agentSwitch', { detail: data, bubbles: true }); this.dispatchEvent(event); } // Public API methods open() { if (this.isOpen) return; this.isOpen = true; this.chatContainer.classList.add('open'); this.triggerButton.classList.add('open'); this.triggerButton.innerHTML = '✕'; this.storage.set('isOpen', true); // Focus input setTimeout(() => { const input = this.chatInterface.input; if (input) input.focus(); }, 300); // Emit event const event = new CustomEvent('chatToggle', { detail: { isOpen: true }, bubbles: true }); this.dispatchEvent(event); } close() { if (!this.isOpen) return; this.isOpen = false; this.chatContainer.classList.remove('open'); this.triggerButton.classList.remove('open'); this.triggerButton.innerHTML = '💬'; this.storage.set('isOpen', false); // Emit event const event = new CustomEvent('chatToggle', { detail: { isOpen: false }, bubbles: true }); this.dispatchEvent(event); } toggle() { if (this.isOpen) { this.close(); } else { this.open(); } } addMessage(text, isUser = false, agent = null) { if (!this.chatInterface) return; const messageAgent = agent || this.agentSelector.getCurrentAgent(); this.chatInterface.addMessage(text, isUser, messageAgent); // Emit event const event = new CustomEvent('messageReceived', { detail: { message: text, isUser, agent: messageAgent }, bubbles: true }); this.dispatchEvent(event); } clearMessages() { if (this.chatInterface) { this.chatInterface.clearMessages(); } } setAgent(agentId) { const agent = this.config.agents.find(a => a.id === agentId); if (agent && this.agentSelector) { this.agentSelector.setAgent(agent); } } showTyping() { if (this.chatInterface) { this.chatInterface.showTyping(); } } hideTyping() { if (this.chatInterface) { this.chatInterface.hideTyping(); } } // Update methods updateAgents() { if (this.agentSelector) { this.agentSelector.updateAgents(this.config.agents); // Re-render agent selector const newAgentSelector = new AgentSelector(this.config.agents); const oldElement = this.querySelector('.chatbot-agent-selector'); const newElement = newAgentSelector.render(); if (oldElement) { oldElement.replaceWith(newElement); } this.agentSelector = newAgentSelector; this.agentSelector.on('agentChange', this.handleAgentChange); } } updateTheme() { this.className = this.className.replace(/theme-\w+/, `theme-${this.config.theme}`); } updatePosition() { this.className = this.className.replace(/position-\w+-\w+/, `position-${this.config.position}`); } updateTitle() { if (this.titleElement) { this.titleElement.textContent = this.config.title; } } updatePlaceholder() { if (this.chatInterface) { this.chatInterface.setPlaceholder(this.config.placeholder); } } cleanup() { try { // Clean up components if (this.chatInterface) { this.chatInterface.off('messageSend', this.handleMessageSend); this.chatInterface.destroy?.(); } if (this.agentSelector) { this.agentSelector.off('agentChange', this.handleAgentChange); this.agentSelector.destroy?.(); } // Remove global event listeners document.removeEventListener('click', this.boundClickOutside); document.removeEventListener('keydown', this.boundKeydown); } catch (error) { console.warn('Error during cleanup:', error); } } // Getters get agents() { return this.config.agents; } get currentAgent() { return this.agentSelector ? this.agentSelector.getCurrentAgent() : null; } get messages() { return this.chatInterface ? this.chatInterface.messages : []; } } // Register the custom element if (!customElements.get('chatbot-widget')) { customElements.define('chatbot-widget', ChatbotWidget); } export default ChatbotWidget;