const CHATTERY_STYLES = ` .chatbot-widget { position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px; background-color: #0092ca; border-radius: 50%; display: flex; justify-content: center; align-items: center; cursor: pointer; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); transition: all 0.3s ease; z-index: 999998; } .chatbot-widget:hover { background-color: #1976D2; } .chatbot-widget i { color: #fff; font-size: 24px; } .chatbot-header { background-color: #0092ca; color: #fff; padding: 24px 16px; font-size: 16px; line-height: 16px; font-weight: bold; position: relative; } .chatbot-messages { height: calc(100% - 128px); box-sizing: border-box; overflow-y: auto; padding: 16px; } .chatbot-message { margin-bottom: 10px; white-space: pre-wrap; font-size: 15px; line-height: 1.4; } .loading-indicator .loading-text { color: #fff; display: inline-block; animation: loadingDots 1.4s infinite; } .sender-assistant, .sender-user { padding: 15px; border-radius: 10px; font-size: 14px; line-height: 1.4; margin-bottom: 20px; font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; } .sender-user { background: #eee; margin-right: 60px; } .sender-assistant { margin-left: 60px; background: #0092ca; color: white; } @keyframes loadingDots { 0%, 20% { color: #fff; text-shadow: .2em 0 0 rgba(255,255,255,0), .4em 0 0 rgba(255,255,255,0); } 40% { color: #fff; text-shadow: .2em 0 0 rgba(255,255,255,0), .4em 0 0 rgba(255,255,255,0); } 60% { text-shadow: .2em 0 0 #fff, .4em 0 0 rgba(255,255,255,0); } 80%, 100% { text-shadow: .2em 0 0 #fff, .4em 0 0 #fff; } } .chatbot-input-container { display: flex; align-items: center; padding: 10px; background-color: #fff; } .send-button { cursor: pointer; color: #0092ca; font-size: 24px; } .chatbot-container { position: fixed; bottom: 100px; right: 20px; width: 350px; background-color: #fff; border-radius: 0; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); display: none; height: 540px; max-height: 80%; z-index: 999999; /* Ensure it's on top of everything */ } .chatbot-input { width: 100%; padding: 10px; border: none; font-size: 15px; border-top: 1px solid #eee; outline: none; margin-bottom: 0 !important; border-radius: 0 !important; border-left: none; border-right: none; border-bottom: none; } @media only screen and (max-width: 600px) { .chatbot-container { bottom: 0; right: 0; width: 100%; height: 100%; max-height: none; } } ` class Chattery { subscribe(event, callback) { this.ensureSource().addEventListener(event, e => { const data = JSON.parse(e.data) callback(data.payload, data.event, data.room_key); }) } ensureSubscription() { if (this.subscription) return this.subscription this.subscription = true this.subscribe('heart', data => { console.log("[Chattery] Heartbeat received:", data && data.c) }) this.subscribe('chunk', data => { this.currMsg += data.body; this.displayMessage({ role: 'assistant', content: this.currMsg, isLast: data.last }); this.extendTimer() if (data.last) { this.killTimer() this.currMsg = ''; } }) } ensureSource() { if (this.source) return this.source this.source = new EventSource('https://chattery.io/stream/subscribe/' + this.session.id); // this.source.onmessage = function (e) { console.warn("[Chattery] onmessage:", e.data) } this.source.onerror = function (e) { console.log("[Chattery] onerror: reconnecting...", e) } this.source.onopen = function (e) { console.log("[Chattery] connected!") } return this.source } loadSession() { this.session = localStorage.getItem('sessionData'); if (this.session) this.session = JSON.parse(this.session); // if nothing saved, or older than 1 hour, create new session if (!this.session || (new Date() - new Date(this.session.updatedAt)) > 3600 * 1000) { this.session = { id: crypto.randomUUID(), messages: [], updatedAt: new Date().toISOString(), } } // if last message.role is 'user', remove it while (this.session.messages.length > 0 && this.session.messages[this.session.messages.length - 1].role == 'user') this.session.messages.pop(); } saveSession() { this.session.updatedAt = new Date().toISOString(); localStorage.setItem('sessionData', JSON.stringify(this.session)); } constructor({ key, api_key, name, greeting, starters }) { this.loadSession(); this.apiKey = api_key; this.starters = starters || []; this.name = name || 'Help Bot'; this.currMsg = ''; this.greeting = greeting || 'Hello! How can I assist you today?'; console.warn("[Chattery] Created bot:", api_key); this.start() this.loadMessages(this.session.messages) this.timerId = null; this.isWaitingForResponse = false; this.timerCount = 0 this.eventTags = { bot_key: key, bot_name: this.name, } } logEvent(event, eventData = {}) { if (dataLayer) { let data = { event, ...eventData, ...this.eventTags, } dataLayer.push(data); console.log('[Chattery] logEvent:', event, data) } else { console.warn('[Chattery] GoogleTagManager not found, so will not log:', event, eventData) } } killTimer() { this.isWaitingForResponse = false; clearTimeout(this.timerId) this.timerId = null; this.timerCount = 0 } extendTimer() { this.timerCount += 1 if (this.timerCount >= 10) { this.killTimer() return } clearTimeout(this.timerId) this.timerId = setTimeout(() => { fetch(`https://chattery.io/api/messages/${this.session.id}`) .then(response => response.json()) .then(data => { let { messages } = data; console.log("[Chattery] Streaming timed out! Polled messages:", messages) if (messages.length > this.session.messages.length) { this.killTimer(); this.loadMessages(messages); } else { this.extendTimer() } }) .catch(error => console.error("Error polling messages:", error)); }, 5000); } start() { const style = document.createElement('style'); style.innerHTML = CHATTERY_STYLES; document.head.appendChild(style); const chatWidget = document.createElement('div'); chatWidget.classList.add('chatbot-widget'); chatWidget.innerHTML = 'Chat'; const chatContainer = document.createElement('div'); chatContainer.classList.add('chatbot-container'); const chatHeader = document.createElement('div'); chatHeader.classList.add('chatbot-header'); chatHeader.textContent = `${this.name}`; const chatCloseButton = document.createElement('button'); chatCloseButton.innerHTML = ''; chatCloseButton.style.cssText = 'background: none; border: none; color: white; position: absolute; right: 0; top: 0; bottom: 0;'; chatHeader.appendChild(chatCloseButton); const chatMessages = document.createElement('div'); chatMessages.classList.add('chatbot-messages'); const chatInput = document.createElement('input'); chatInput.classList.add('chatbot-input'); chatInput.type = 'text'; chatInput.placeholder = 'Type your message...'; chatInput.style.flexGrow = '1'; chatInput.style.marginRight = '10px'; chatInput.autofocus = true; const sendButton = document.createElement('i'); sendButton.classList.add('fas', 'fa-paper-plane', 'send-button'); const chatInputContainer = document.createElement('div'); chatInputContainer.classList.add('chatbot-input-container'); chatInputContainer.appendChild(chatInput); chatInputContainer.appendChild(sendButton); chatContainer.appendChild(chatHeader); chatContainer.appendChild(chatMessages); chatContainer.appendChild(chatInputContainer); document.body.appendChild(chatWidget); document.body.appendChild(chatContainer); chatWidget.addEventListener('click', () => { if (chatContainer.style.display == 'block') { chatContainer.style.display = 'none'; this.logEvent('chattery.chat_close'); return; } chatContainer.style.display = 'block'; this.ensureSubscription(); chatInput.focus(); this.logEvent('chattery.chat_open'); }); chatCloseButton.addEventListener('click', () => { chatContainer.style.display = 'none'; this.logEvent('chattery.chat_close'); }); this.send = () => { if (this.isWaitingForResponse) { return; } const content = chatInput.value; if (content.trim() == '') return this.isWaitingForResponse = true; chatInput.value = ''; this.displayMessage({ role: 'user', content, isLast: true }); this.post() chatMessages.scrollTop = chatMessages.scrollHeight; this.logEvent('chattery.user_msg', { msg: content }); } chatInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') { this.send() } }); sendButton.addEventListener('click', () => { this.send(); }); } post() { const pageText = this.getPageText(); this.showLoadingIndicator(); let body = { page_text: pageText, messages: this.session.messages, session_id: this.session.id, } console.log("[Chattery] User message:", body) fetch('https://chattery.io/api/chat/' + this.apiKey, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(body), }) .catch(error => { console.error("[Chattery] Error sending User message:", error); this.isWaitingForResponse = false; }); this.extendTimer() } displayStarters() { console.log("STARTERS:", this.starters) const chatMessages = document.querySelector('.chatbot-messages'); this.starters.forEach(starter => { const starterElement = document.createElement('div'); starterElement.classList.add('chatbot-message', 'starter-question'); starterElement.textContent = starter; starterElement.style.cursor = 'pointer'; starterElement.style.color = '#0092ca'; starterElement.style.textDecoration = 'underline'; starterElement.addEventListener('click', () => { this.displayMessage({ role: 'user', content: starter, isLast: true }); this.post(); // hide starters let starterQuestions = document.querySelectorAll('.starter-question'); starterQuestions.forEach(starterQuestion => starterQuestion.remove()); }); chatMessages.appendChild(starterElement); }); } loadMessages(messages) { this.session.messages = []; let oldMessages = document.querySelectorAll('.chatbot-message'); oldMessages.forEach(message => message.remove()); if (messages.length > 1) { const clearChatMessage = document.createElement('div'); clearChatMessage.classList.add('chatbot-message', 'clear-chat-message'); clearChatMessage.textContent = 'Clear Chat'; clearChatMessage.style.color = '#888'; clearChatMessage.style.cursor = 'pointer'; clearChatMessage.style.textAlign = 'center'; clearChatMessage.style.marginBottom = '10px'; clearChatMessage.onclick = () => { this.loadMessages([]); }; document.querySelector('.chatbot-messages').appendChild(clearChatMessage); } console.log("[Chattery] Loading messages:", messages) if (messages.length == 0) { this.displayMessage({ role: 'assistant', content: this.greeting, isLast: true }); } else { for (const message of messages) { let { role, content } = message; this.displayMessage({ role, content, isLast: true }); } } if (this.session.messages.length == 1) { this.displayStarters(); return } } showLoadingIndicator() { this.displayMessage({ role: 'assistant', isLast: true, isLoading: true }); } displayMessage({ role, content = '', isLast = false, isLoading = false }) { let sender = (role == 'user' ? 'You' : this.name) const chatMessages = document.querySelector('.chatbot-messages'); if (isLoading) { let messageElement = document.createElement('div'); messageElement.classList.add('chatbot-message', 'loading-indicator', 'sender-assistant'); messageElement.innerHTML = `${sender}: .`; chatMessages.appendChild(messageElement); return; } let loadingIndicator = document.querySelector('.loading-indicator'); if (loadingIndicator) loadingIndicator.remove(); let senderClass = `sender-${role}` let messageElement = chatMessages.lastElementChild; if (!messageElement || !messageElement.classList.contains(senderClass)) { messageElement = document.createElement('div'); messageElement.classList.add('chatbot-message', senderClass); messageElement.innerHTML = `${sender}: `; chatMessages.appendChild(messageElement); } messageElement.lastChild.textContent = content; chatMessages.scrollTop = chatMessages.scrollHeight; if (isLast && !isLoading) { let lastRole = this.session.messages.length > 0 && this.session.messages[this.session.messages.length - 1].role if (lastRole != role) { this.session.messages.push({ role, content }); this.saveSession(); } else { console.error("[Chattery] Ignoring same-turn message:", this.session.messages, { role, content }) } } } getPageText() { return document.body.innerText; } } function ensureFontAwesomeLoaded(callback) { if (document.querySelector('.fa, .fas')) { callback(); return; } const link = document.createElement('link'); link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'; link.rel = 'stylesheet'; link.onload = () => callback(); document.head.appendChild(link); } ensureFontAwesomeLoaded( () => new Chattery({"api_key": "bee14f1c-981b-48ce-9da0-74481adcf1c9", "key": 2, "name": "FOSH Assistant", "greeting": "Welcome to FOSH, how can I help you?", "starters": ["Do the wallets offer RFID protection?", "How many cards do the wallets hold?", "What is the warranty?", "How thin are the wallets?"]}) )