@ingridsandev/chatbot-widget
Version:
A framework-agnostic chatbot widget using Web Components
1 lines • 16.1 kB
JavaScript
(()=>{"use strict";const e=new class{constructor(e="ChatbotWidget"){this.prefix=e,this.logLevel=this.getLogLevel()}getLogLevel(){if("undefined"!=typeof window&&window.localStorage){const e=localStorage.getItem("chatbot-log-level");if(e)return e}return"warn"}formatMessage(e,t,...s){return[`[${(new Date).toISOString()}] ${this.prefix} [${e.toUpperCase()}]:`,t,...s]}debug(e,...t){this.shouldLog("debug")&&console.debug(...this.formatMessage("debug",e,...t))}info(e,...t){this.shouldLog("info")&&console.info(...this.formatMessage("info",e,...t))}warn(e,...t){this.shouldLog("warn")&&console.warn(...this.formatMessage("warn",e,...t))}error(e,...t){this.shouldLog("error")&&console.error(...this.formatMessage("error",e,...t))}shouldLog(e){const t=["debug","info","warn","error"],s=t.indexOf(this.logLevel);return t.indexOf(e)>=s}},t=class{constructor(){this.events={},this.maxListeners=10}on(t,s){if("function"!=typeof s)throw new TypeError("Listener must be a function");return this.events[t]||(this.events[t]=[]),this.events[t].length>=this.maxListeners&&e.warn(`MaxListenersExceededWarning: Possible EventEmitter memory leak detected. ${this.events[t].length+1} ${t} listeners added.`),this.events[t].push(s),this}off(e,t){return this.events[e]?t?(this.events[e]=this.events[e].filter((e=>e!==t)),0===this.events[e].length&&delete this.events[e],this):(delete this.events[e],this):this}emit(t,s){if(!this.events[t])return!1;const n=[...this.events[t]];for(const i of n)try{i(s)}catch(s){e.error(`Error in event listener for "${t}":`,s)}return!0}once(e,t){const s=n=>{try{t(n)}finally{this.off(e,s)}};return this.on(e,s),this}removeAllListeners(e){return e?delete this.events[e]:this.events={},this}listenerCount(e){return this.events[e]?this.events[e].length:0}setMaxListeners(e){return this.maxListeners=e,this}},s=class{constructor(e="chatbot-widget"){this.prefix=e}set(e,t){try{const s=`${this.prefix}-${e}`;return localStorage.setItem(s,JSON.stringify(t)),!0}catch(t){return console.warn(`Storage set failed for key "${e}":`,t),!1}}get(e,t=null){try{const s=`${this.prefix}-${e}`,n=localStorage.getItem(s);return n?JSON.parse(n):t}catch(s){return console.warn(`Storage get failed for key "${e}":`,s),t}}remove(e){try{const t=`${this.prefix}-${e}`;return localStorage.removeItem(t),!0}catch(t){return console.warn(`Storage remove failed for key "${e}":`,t),!1}}clear(){try{return Object.keys(localStorage).forEach((e=>{e.startsWith(`${this.prefix}-`)&&localStorage.removeItem(e)})),!0}catch(e){return console.warn("Storage clear failed:",e),!1}}setSession(e,t){try{const s=`${this.prefix}-${e}`;return sessionStorage.setItem(s,JSON.stringify(t)),!0}catch(t){return console.warn(`Session storage set failed for key "${e}":`,t),!1}}getSession(e,t=null){try{const s=`${this.prefix}-${e}`,n=sessionStorage.getItem(s);return n?JSON.parse(n):t}catch(s){return console.warn(`Session storage get failed for key "${e}":`,s),t}}},n=class extends t{constructor(e={}){super(),this.config={placeholder:"Type your message...",showTimestamp:!0,maxMessages:100,...e},this.messages=[],this.storage=new s,this.isTyping=!1,this.loadMessages()}render(){const e=document.createElement("div");e.className="chatbot-chat-interface",this.messagesContainer=document.createElement("div"),this.messagesContainer.className="chatbot-messages",this.typingIndicator=document.createElement("div"),this.typingIndicator.className="chatbot-typing",this.typingIndicator.textContent="AI is typing";const t=document.createElement("div");t.className="chatbot-input-area";const s=document.createElement("div");return s.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=!0,this.setupEventListeners(),s.appendChild(this.input),s.appendChild(this.sendButton),t.appendChild(s),e.appendChild(this.messagesContainer),e.appendChild(this.typingIndicator),e.appendChild(t),this.renderMessages(),e}setupEventListeners(){this.input.addEventListener("input",(()=>{this.adjustTextareaHeight(),this.updateSendButton()})),this.input.addEventListener("keydown",(e=>{"Enter"!==e.key||e.shiftKey||(e.preventDefault(),this.sendMessage())})),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 e=this.input.value.trim().length>0;this.sendButton.disabled=!e||this.isTyping}sendMessage(){const e=this.input.value.trim();e&&!this.isTyping&&(this.addMessage(e,!0),this.input.value="",this.adjustTextareaHeight(),this.updateSendButton(),this.emit("messageSend",{message:e}))}addMessage(e,t=!1,s=null,n=null){try{const i={id:Date.now()+Math.random(),text:e,isUser:t,agent:s,timestamp:n||new Date};this.messages.push(i),this.messages.length>this.config.maxMessages&&(this.messages=this.messages.slice(-this.config.maxMessages)),this.renderMessage(i),this.scrollToBottom(),this.saveMessages(),this.emit("messageAdded",{message:i})}catch(e){console.warn("Error adding message:",e)}}renderMessages(){this.messagesContainer.innerHTML="",this.messages.forEach((e=>{this.renderMessage(e,!1)})),this.scrollToBottom()}renderMessage(e,t=!0){try{const s=document.createElement("div");s.className="chatbot-message "+(e.isUser?"user":"bot");const n=document.createElement("div");n.className="chatbot-message-avatar",e.isUser?n.textContent="👤":e.agent&&e.agent.avatar?n.textContent=e.agent.avatar:n.textContent="🤖";const i=document.createElement("div");if(i.className="chatbot-message-content",i.textContent=e.text,this.config.showTimestamp){const t=document.createElement("div");t.className="chatbot-message-time",t.textContent=this.formatTime(e.timestamp),i.appendChild(t)}s.appendChild(n),s.appendChild(i),t||(s.style.animation="none"),this.messagesContainer.appendChild(s)}catch(e){console.warn("Error rendering message:",e)}}formatTime(e){return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}scrollToBottom(){setTimeout((()=>{this.messagesContainer.scrollTop=this.messagesContainer.scrollHeight}),50)}showTyping(){this.isTyping=!0,this.typingIndicator.classList.add("show"),this.updateSendButton(),this.scrollToBottom()}hideTyping(){this.isTyping=!1,this.typingIndicator.classList.remove("show"),this.updateSendButton()}clearMessages(){this.messages=[],this.messagesContainer.innerHTML="",this.saveMessages(),this.emit("messagesCleared")}saveMessages(){try{const e=this.messages.slice(-20);this.storage.set("messages",e)}catch(e){console.warn("Error saving messages:",e)}}loadMessages(){try{const e=this.storage.get("messages",[]);this.messages=e.map((e=>({...e,timestamp:new Date(e.timestamp)})))}catch(e){console.warn("Error loading messages:",e),this.messages=[]}}destroy(){try{this.input&&(this.input.removeEventListener("input",this.adjustTextareaHeight),this.input.removeEventListener("keydown",this.sendMessage)),this.sendButton&&this.sendButton.removeEventListener("click",this.sendMessage),this.removeAllListeners()}catch(e){console.warn("Error during chat interface cleanup:",e)}}setPlaceholder(e){this.config.placeholder=e,this.input&&(this.input.placeholder=e)}},i="selectedAgent",a=class extends t{constructor(e=[]){super(),this.agents=Array.isArray(e)?e:[],this.currentAgent=this.agents.length>0?this.agents[0]:null,this.storage=new s,this._boundHandleAgentChange=this._handleAgentChange.bind(this),this._loadSavedAgent()}_loadSavedAgent(){try{const e=this.storage.get(i);if(e){const t=this.agents.find((t=>t.id===e));t&&(this.currentAgent=t)}}catch(e){console.warn("Failed to load saved agent preference:",e)}}render(){const e=document.createElement("div");if(e.className="chatbot-agent-selector",this.agents.length<=1)return e.style.display="none",e;const t=document.createElement("select");return t.className="chatbot-agent-select",t.setAttribute("aria-label","Select chatbot agent"),this.agents.forEach((e=>{if(!e||!e.id)return;const s=document.createElement("option");s.value=e.id,s.textContent=`${e.avatar||"🤖"} ${e.name||"Unnamed Agent"}`,s.selected=this.currentAgent&&this.currentAgent.id===e.id,t.appendChild(s)})),t.addEventListener("change",this._boundHandleAgentChange),this._selectElement=t,e.appendChild(t),e}_handleAgentChange(e){const t=this.agents.find((t=>t.id===e.target.value));t&&this.setAgent(t)}setAgent(e){if(!e||!e.id)return void console.warn("Invalid agent provided to setAgent");const t=this.currentAgent;this.currentAgent=e;try{this.storage.set(i,e.id)}catch(e){console.warn("Failed to save agent preference:",e)}this.emit("agentChange",{agent:e,previousAgent:t})}getCurrentAgent(){return this.currentAgent}updateAgents(e){if(Array.isArray(e)){if(this.agents=e,this.currentAgent&&!this.agents.find((e=>e?.id===this.currentAgent.id))){const e=this.currentAgent,t=this.agents.length>0?this.agents[0]:null;if(t&&t!==this.currentAgent)this.setAgent(t);else if(!t){this.currentAgent=null;try{this.storage.remove?.(i)}catch(e){console.warn("Failed to remove saved agent preference:",e)}this.emit("agentChange",{agent:null,previousAgent:e})}}}else console.warn("updateAgents expects an array, received:",typeof e)}destroy(){this._selectElement&&this._boundHandleAgentChange&&(this._selectElement.removeEventListener("change",this._boundHandleAgentChange),this._selectElement=null),this.removeAllListeners()}};class o extends HTMLElement{constructor(){super(),this.eventEmitter=new t,this.storage=new s,this.isOpen=!1,this.config={agents:[{id:"default",name:"AI Assistant",avatar:"🤖"}],theme:"light",position:"bottom-right",title:"AI Assistant",placeholder:"Type your message...",open:!1},this.handleMessageSend=this.handleMessageSend.bind(this),this.handleAgentChange=this.handleAgentChange.bind(this),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(),(this.config.open||this.storage.get("isOpen",!1))&&setTimeout((()=>this.open()),100)}disconnectedCallback(){this.cleanup()}attributeChangedCallback(e,t,s){if(t!==s)try{switch(e){case"agents":try{this.config.agents=JSON.parse(s),this.updateAgents()}catch{console.warn("Invalid agents JSON:",s)}break;case"theme":this.config.theme=s,this.updateTheme();break;case"position":this.config.position=s,this.updatePosition();break;case"open":this.config.open="true"===s||""===s,this.config.open?this.open():this.close();break;case"title":this.config.title=s,this.updateTitle();break;case"placeholder":this.config.placeholder=s,this.updatePlaceholder()}}catch(t){console.warn(`Error handling attribute change for ${e}:`,t)}}loadConfig(){const e=this.getAttribute("agents");if(e)try{this.config.agents=JSON.parse(e)}catch{console.warn("Invalid agents attribute:",e)}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}`,this.triggerButton=document.createElement("button"),this.triggerButton.className="chatbot-trigger",this.triggerButton.innerHTML="💬",this.triggerButton.setAttribute("aria-label","Open chat"),this.chatContainer=document.createElement("div"),this.chatContainer.className="chatbot-container";const e=document.createElement("div");e.className="chatbot-header",this.titleElement=document.createElement("h3"),this.titleElement.className="chatbot-title",this.titleElement.textContent=this.config.title;const t=document.createElement("button");t.className="chatbot-close",t.innerHTML="✕",t.setAttribute("aria-label","Close chat"),e.appendChild(this.titleElement),e.appendChild(t),this.agentSelector=new a(this.config.agents);const s=this.agentSelector.render();this.chatInterface=new n({placeholder:this.config.placeholder});const i=this.chatInterface.render();this.chatContainer.appendChild(e),this.chatContainer.appendChild(s),this.chatContainer.appendChild(i),this.appendChild(this.triggerButton),this.appendChild(this.chatContainer),this.triggerButton.addEventListener("click",(()=>this.toggle())),t.addEventListener("click",(()=>this.close()))}setupEventListeners(){this.chatInterface.on("messageSend",this.handleMessageSend),this.agentSelector.on("agentChange",this.handleAgentChange),document.addEventListener("click",this.boundClickOutside),document.addEventListener("keydown",this.boundKeydown)}handleClickOutside(e){this.isOpen&&!this.contains(e.target)&&this.close()}handleKeydown(e){"Escape"===e.key&&this.isOpen&&this.close()}handleMessageSend(e){const t=this.agentSelector.getCurrentAgent(),s=new CustomEvent("messageSend",{detail:{message:e.message,agent:t},bubbles:!0});this.dispatchEvent(s)}handleAgentChange(e){const t=new CustomEvent("agentSwitch",{detail:e,bubbles:!0});this.dispatchEvent(t)}open(){if(this.isOpen)return;this.isOpen=!0,this.chatContainer.classList.add("open"),this.triggerButton.classList.add("open"),this.triggerButton.innerHTML="✕",this.storage.set("isOpen",!0),setTimeout((()=>{const e=this.chatInterface.input;e&&e.focus()}),300);const e=new CustomEvent("chatToggle",{detail:{isOpen:!0},bubbles:!0});this.dispatchEvent(e)}close(){if(!this.isOpen)return;this.isOpen=!1,this.chatContainer.classList.remove("open"),this.triggerButton.classList.remove("open"),this.triggerButton.innerHTML="💬",this.storage.set("isOpen",!1);const e=new CustomEvent("chatToggle",{detail:{isOpen:!1},bubbles:!0});this.dispatchEvent(e)}toggle(){this.isOpen?this.close():this.open()}addMessage(e,t=!1,s=null){if(!this.chatInterface)return;const n=s||this.agentSelector.getCurrentAgent();this.chatInterface.addMessage(e,t,n);const i=new CustomEvent("messageReceived",{detail:{message:e,isUser:t,agent:n},bubbles:!0});this.dispatchEvent(i)}clearMessages(){this.chatInterface&&this.chatInterface.clearMessages()}setAgent(e){const t=this.config.agents.find((t=>t.id===e));t&&this.agentSelector&&this.agentSelector.setAgent(t)}showTyping(){this.chatInterface&&this.chatInterface.showTyping()}hideTyping(){this.chatInterface&&this.chatInterface.hideTyping()}updateAgents(){if(this.agentSelector){this.agentSelector.updateAgents(this.config.agents);const e=new a(this.config.agents),t=this.querySelector(".chatbot-agent-selector"),s=e.render();t&&t.replaceWith(s),this.agentSelector=e,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(){this.titleElement&&(this.titleElement.textContent=this.config.title)}updatePlaceholder(){this.chatInterface&&this.chatInterface.setPlaceholder(this.config.placeholder)}cleanup(){try{this.chatInterface&&(this.chatInterface.off("messageSend",this.handleMessageSend),this.chatInterface.destroy?.()),this.agentSelector&&(this.agentSelector.off("agentChange",this.handleAgentChange),this.agentSelector.destroy?.()),document.removeEventListener("click",this.boundClickOutside),document.removeEventListener("keydown",this.boundKeydown)}catch(e){console.warn("Error during cleanup:",e)}}get agents(){return this.config.agents}get currentAgent(){return this.agentSelector?this.agentSelector.getCurrentAgent():null}get messages(){return this.chatInterface?this.chatInterface.messages:[]}}customElements.get("chatbot-widget")||customElements.define("chatbot-widget",o);const r=o;"undefined"==typeof window||customElements.get("chatbot-widget")||customElements.define("chatbot-widget",r)})();