/* * widget.js - Shilte AI Multi-Tenant Chat Widget
* Deployed to: https://cdn.shilte.ai/widget.js
*/
class ShilteChatWidget {
constructor(businessId, apiUrl) {
this.businessId = businessId;
this.apiUrl = apiUrl;
this.isGreetingSent = false;
// The chat history to be maintained across the session
this.chatHistory = this.loadHistory();
if (!this.businessId || !this.apiUrl) {
console.error("Shilte AI Widget requires both data-business-id and data-api-url attributes.");
return;
}
this.init();
}
// --- Core UI Setup ---
init() {
// 1. Create a container element and attach a Shadow DOM for isolation
const container = document.createElement('div');
container.id = 'shilte-ai-chat-container';
document.body.appendChild(container);
const shadowRoot = container.attachShadow({ mode: 'open' });
// 2. Add Styles (Inline for simplicity, would be a separate CSS file in production)
shadowRoot.innerHTML = `
💬
`;
this.ui = {
window: shadowRoot.getElementById('chat-window'),
body: shadowRoot.getElementById('chat-body'),
input: shadowRoot.getElementById('user-input'),
sendButton: shadowRoot.getElementById('send-btn'),
bubble: shadowRoot.getElementById('chat-bubble'),
closeButton: shadowRoot.getElementById('close-btn'),
};
this.addEventListeners();
this.loadChatHistory();
this.loadAutoGreeting();
}
addEventListeners() {
this.ui.bubble.addEventListener('click', () => this.toggleChat(true));
this.ui.closeButton.addEventListener('click', () => this.toggleChat(false));
this.ui.sendButton.addEventListener('click', () => this.handleUserInput());
this.ui.input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.handleUserInput();
});
}
toggleChat(open) {
if (open) {
this.ui.window.classList.add('open');
this.ui.input.focus();
this.scrollToBottom();
} else {
this.ui.window.classList.remove('open');
}
}
// --- History & Display Utilities ---
loadHistory() {
try {
const key = `shilte_chat_history_${this.businessId}`;
const history = localStorage.getItem(key);
return history ? JSON.parse(history) : [];
} catch (e) {
console.warn("Failed to load chat history from localStorage.", e);
return [];
}
}
saveHistory() {
try {
const key = `shilte_chat_history_${this.businessId}`;
localStorage.setItem(key, JSON.stringify(this.chatHistory));
} catch (e) {
console.warn("Failed to save chat history to localStorage.", e);
}
}
loadChatHistory() {
this.chatHistory.forEach(msg => {
if (msg.role !== 'system') { // Don't display system roles
this.displayMessage(msg.text, msg.role === 'user' ? 'user' : 'bot');
}
});
this.scrollToBottom();
}
displayMessage(text, role) {
const msgDiv = document.createElement('div');
msgDiv.className = role === 'user' ? 'message-user' : 'message-bot';
msgDiv.textContent = text;
this.ui.body.appendChild(msgDiv);
this.scrollToBottom();
return msgDiv;
}
scrollToBottom() {
this.ui.body.scrollTop = this.ui.body.scrollHeight;
}
// --- API & Core Logic ---
async loadAutoGreeting() {
if (this.chatHistory.length > 0 || this.isGreetingSent) return;
try {
// New cached endpoint for fast greeting response
const response = await fetch(`${this.apiUrl}/crm/shilte/greeting`, {
method: 'POST', // Use POST if the greeting relies on the current prompt/settings
headers: {
'X-Business-ID': this.businessId,
'Content-Type': 'application/json'
},
body: JSON.stringify({ businessID: this.businessId }) // May need a dummy body
});
if (response.ok) {
const data = await response.json();
// Display the greeting after a short, friendly delay
setTimeout(() => {
this.displayMessage(data.message, 'bot');
this.chatHistory.push({ role: 'assistant', text: data.message });
this.saveHistory();
this.isGreetingSent = true;
}, 1000); // 1.0 second delay
}
} catch (error) {
console.error("Error fetching auto-greeting:", error);
// Fallback: If network fails, display a generic message
this.displayMessage("Hello! How can I assist you today?", 'bot');
}
}
async handleUserInput() {
const userQuery = this.ui.input.value.trim();
if (!userQuery) return;
// 1. Display and save user message
this.displayMessage(userQuery, 'user');
this.chatHistory.push({ role: 'user', text: userQuery });
this.saveHistory();
this.ui.input.value = '';
// 2. Show Typing Indicator
const typingIndicator = this.displayMessage("Bot is typing...", 'bot');
try {
// 3. Call the main chat generation endpoint
const response = await fetch(`${this.apiUrl}/crm/shilte/generate`, {
method: 'POST',
headers: {
'X-Business-ID': this.businessId, // CRITICAL: Tenant Isolation
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_query: userQuery,
// Send entire history for context
chat_history: this.chatHistory.map(msg => ({ role: msg.role, parts: [{ text: msg.text }] }))
})
});
if (!response.ok) {
throw new Error(`API returned status ${response.status}`);
}
const data = await response.json();
// 4. Remove typing indicator and display bot response
this.ui.body.removeChild(typingIndicator);
const botResponseText = data.response;
this.displayMessage(botResponseText, 'bot');
this.chatHistory.push({ role: 'assistant', text: botResponseText });
this.saveHistory();
} catch (error) {
this.ui.body.removeChild(typingIndicator);
this.displayMessage("Sorry, I'm having trouble connecting right now. Please try again later.", 'bot');
console.error("Chat API Error:", error);
}
}
}
// --- Initialization Logic ---
document.addEventListener('DOMContentLoaded', () => {
// Find the script tag that loaded this file
const scriptTag = document.querySelector('script[data-business-id][data-api-url]');
if (scriptTag) {
const businessId = scriptTag.getAttribute('data-business-id');
const apiUrl = scriptTag.getAttribute('data-api-url');
// Initialize the widget
new ShilteChatWidget(businessId, apiUrl);
} else {
console.error("Shilte AI Widget: Cannot find the script tag with required attributes.");
}
});