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 = '';
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?"]})
)