anasraza526's picture
Clean deploy to Hugging Face
ac90985
(function () {
// Auto-detect the API base URL from the script source
const scriptTag = document.currentScript;
const scriptSrc = scriptTag ? scriptTag.src : '';
const detectedBase = scriptSrc ? new URL(scriptSrc).origin : 'http://localhost:8000';
const WIDGET_API_BASE = detectedBase;
console.log('CustomerAgentWidget: Script loaded from', WIDGET_API_BASE);
function createChatWidget(arg1, arg2 = {}) {
let websiteId, config;
if (typeof arg1 === 'object' && arg1 !== null && arg1.websiteId) {
// Handle init({ websiteId: 123, ...config })
config = { ...arg2, ...arg1 };
websiteId = config.websiteId;
} else {
// Handle init(123, { ...config })
websiteId = arg1;
config = arg2;
}
console.log('CustomerAgentWidget: Initializing with', { websiteId, config });
const apiBase = config.apiUrl || 'http://localhost:8000';
const widgetContainer = document.createElement('div');
widgetContainer.id = 'customer-agent-widget';
const position = config.position || 'bottom-right';
const size = config.size || 'medium';
const sizeMap = { small: '50px', medium: '60px', large: '70px' };
widgetContainer.style.cssText = `
position: fixed;
${position.includes('bottom') ? 'bottom: 20px;' : 'top: 20px;'}
${position.includes('right') ? 'right: 20px;' : 'left: 20px;'}
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
transition: all 0.3s ease;
pointer-events: none;
`;
const chatBox = document.createElement('div');
chatBox.className = 'ca-widget-chat-box';
const header = document.createElement('div');
header.style.cssText = `
background: linear-gradient(135deg, ${config.primaryColor || '#4F46E5'}, ${adjustColor(config.primaryColor || '#4F46E5', -30)}); /* Defaults to Indigo-600 */
color: ${config.textColor || 'white'};
padding: 20px;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
overflow: hidden;
`;
const statusDot = document.createElement('div');
statusDot.style.cssText = `
width: 8px;
height: 8px;
background: #10B981;
border-radius: 50%;
margin-right: 8px;
animation: pulse 2s infinite;
`;
const headerContent = document.createElement('div');
headerContent.style.cssText = 'display: flex; align-items: center; flex-direction: column; align-items: flex-start;';
headerContent.innerHTML = `
<div style="display: flex; align-items: center;">
${statusDot.outerHTML}
<span style="font-size: 16px;">Customer Supporte</span>
</div>
<span style="font-size: 12px; opacity: 0.8; margin-top: 2px;">We're here to help!</span>
`;
// SVG Icons
const icons = {
mic: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>`,
call: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>`,
maximize: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" x2="14" y1="3" y2="10"/><line x1="3" x2="10" y1="21" y2="14"/></svg>`,
minimize: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" x2="21" y1="10" y2="3"/><line x1="3" x2="10" y1="21" y2="14"/></svg>`,
minimize: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" x2="21" y1="10" y2="3"/><line x1="3" x2="10" y1="21" y2="14"/></svg>`,
close: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>`,
send: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>`,
chat: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
volume: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5L6 9H2v6h4l5 4V5z"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>`,
stop: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/></svg>`,
speed: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>`
};
const headerControls = document.createElement('div');
headerControls.style.cssText = 'display: flex; align-items: center; gap: 8px;';
const createHeaderBtn = (html, id) => {
const btn = document.createElement('button');
btn.id = id;
btn.innerHTML = html;
btn.style.cssText = `
background: rgba(255,255,255,0.15);
border: none;
color: white;
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
backdrop-filter: blur(10px);
`;
btn.addEventListener('mouseenter', () => {
btn.style.background = 'rgba(255,255,255,0.25)';
btn.style.transform = 'scale(1.05)';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = 'rgba(255,255,255,0.15)';
btn.style.transform = 'scale(1)';
});
return btn;
};
const micBtn = createHeaderBtn(icons.mic, 'mic-btn');
micBtn.title = 'Voice Input (Coming Soon)';
micBtn.style.opacity = '0.5';
micBtn.style.cursor = 'not-allowed';
const callBtn = createHeaderBtn(icons.call, 'call-btn');
callBtn.title = 'Call Support (Coming Soon)';
callBtn.style.opacity = '0.5';
callBtn.style.cursor = 'not-allowed';
const fullScreenBtn = createHeaderBtn(icons.maximize, 'fullscreen-btn');
fullScreenBtn.title = 'Toggle Full Screen';
const closeBtn = createHeaderBtn(icons.close, 'close-chat');
// Enhanced Close Button Styling
closeBtn.style.background = 'rgba(255, 255, 255, 0.2)';
closeBtn.style.backdropFilter = 'blur(12px)';
closeBtn.addEventListener('mouseenter', () => {
closeBtn.style.background = '#EF4444'; // Red on hover
closeBtn.style.transform = 'rotate(90deg) scale(1.1)';
closeBtn.style.boxShadow = '0 0 15px rgba(239, 68, 68, 0.4)';
});
closeBtn.addEventListener('mouseleave', () => {
closeBtn.style.background = 'rgba(255, 255, 255, 0.2)';
closeBtn.style.transform = 'rotate(0deg) scale(1)';
closeBtn.style.boxShadow = 'none';
});
// headerControls.appendChild(micBtn); // Removed as requested
headerControls.appendChild(callBtn);
headerControls.appendChild(fullScreenBtn);
headerControls.appendChild(closeBtn);
headerControls.appendChild(callBtn);
headerControls.appendChild(fullScreenBtn);
headerControls.appendChild(closeBtn);
header.appendChild(headerContent);
header.appendChild(headerControls);
const messages = document.createElement('div');
messages.className = 'messages-container';
messages.style.cssText = `
flex: 1;
overflow-y: auto;
padding: 20px;
background: linear-gradient(to bottom, #fafafa, #ffffff);
scroll-behavior: smooth;
`;
// Custom scrollbar
const style = document.createElement('style');
style.textContent = `
#customer-agent-widget > * {
pointer-events: auto;
}
#customer-agent-widget * {
box-sizing: border-box;
}
#customer-agent-widget .messages-container {
overscroll-behavior: contain;
}
#customer-agent-widget *::-webkit-scrollbar {
width: 6px;
}
#customer-agent-widget *::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
#customer-agent-widget *::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
#customer-agent-widget *::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes bounce {
0%, 20%, 53%, 80%, 100% { transform: translate3d(0,0,0); }
40%, 43% { transform: translate3d(0,-8px,0); }
70% { transform: translate3d(0,-4px,0); }
90% { transform: translate3d(0,-2px,0); }
}
/* Responsive Styles */
.ca-widget-chat-box {
display: none;
background: white;
border-radius: 24px;
box-shadow: 0 25px 80px -15px rgba(0, 0, 0, 0.25);
overflow: hidden;
flex-direction: column;
transform: scale(0.95);
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.2);
position: absolute;
bottom: 80px;
right: 0;
width: 380px;
height: 600px;
max-height: calc(100vh - 120px);
max-width: calc(100vw - 40px);
}
.ca-widget-chat-box.active {
display: flex;
transform: scale(1);
opacity: 1;
}
.ca-widget-chat-box.fullscreen {
width: 100vw !important;
height: 100vh !important;
bottom: 0 !important;
right: 0 !important;
border-radius: 0 !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
max-height: 100vh !important;
}
@media (max-width: 580px) {
.ca-widget-chat-box {
width: calc(100vw - 30px);
max-width: 450px;
height: calc(100vh - 100px);
max-height: 700px;
bottom: 75px;
right: -10px; /* Slight offset to align better with button */
border-radius: 20px;
}
#customer-agent-widget {
right: 15px !important;
bottom: 15px !important;
}
.ca-widget-chat-box.fullscreen {
width: 100vw !important;
height: 100vh !important;
max-width: 100vw !important;
max-height: 100vh !important;
right: -15px !important; /* Counteract container right:15px */
bottom: -15px !important; /* Counteract container bottom:15px */
}
}
.speech-controls {
display: flex;
gap: 8px;
margin-top: 8px;
opacity: 0;
transition: opacity 0.2s ease;
align-items: center;
}
.ca-widget-chat-bubble:hover .speech-controls {
opacity: 1;
}
.speech-btn {
background: rgba(0,0,0,0.05);
border: none;
padding: 4px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #6B7280;
transition: all 0.2s ease;
}
.speech-btn:hover {
background: rgba(0,0,0,0.1);
color: #374151;
}
.speed-indicator {
font-size: 10px;
font-weight: bold;
color: #6B7280;
background: rgba(0,0,0,0.05);
padding: 2px 4px;
border-radius: 4px;
cursor: pointer;
user-select: none;
}
.speed-indicator:hover {
background: rgba(0,0,0,0.1);
}
`;
document.head.appendChild(style);
const inputContainer = document.createElement('div');
inputContainer.style.cssText = `
padding: 15px;
border-top: 1px solid rgba(0,0,0,0.05);
background: white;
backdrop-filter: blur(10px);
flex-shrink: 0;
`;
const messageForm = document.createElement('div');
messageForm.style.cssText = 'display: flex; gap: 10px; margin-bottom: 10px;';
const micButton = document.createElement('button');
micButton.innerHTML = icons.mic;
micButton.title = "Voice Input";
micButton.style.cssText = `
background: none;
border: none;
color: #9CA3AF;
cursor: pointer;
padding: 8px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
`;
micButton.addEventListener('mouseenter', () => micButton.style.color = config.primaryColor || '#3B82F6');
micButton.addEventListener('mouseleave', () => micButton.style.color = '#9CA3AF');
const inputWrapper = document.createElement('div');
inputWrapper.style.cssText = `
flex: 1;
display: flex;
align-items: center;
position: relative;
border: 2px solid #e5e7eb;
border-radius: 25px;
background: #f9fafb;
transition: all 0.2s ease;
`;
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Type your message...';
input.style.cssText = `
flex: 1;
padding: 12px 16px;
padding-right: 44px; /* Room for mic button */
border: none;
background: transparent;
outline: none;
font-size: 14px;
color: #374151;
`;
// micButton state: Disabled until voice functionality is added
micButton.disabled = true;
micButton.style.opacity = '0.4';
micButton.style.cursor = 'not-allowed';
micButton.style.display = 'flex'; // Restore visibility
inputWrapper.appendChild(input);
inputWrapper.appendChild(micButton);
input.addEventListener('focus', () => {
inputWrapper.style.borderColor = config.primaryColor || '#3B82F6';
inputWrapper.style.background = 'white';
inputWrapper.style.boxShadow = `0 0 0 3px ${config.primaryColor || '#3B82F6'}20`;
});
input.addEventListener('blur', () => {
inputWrapper.style.borderColor = '#e5e7eb';
inputWrapper.style.background = '#f9fafb';
inputWrapper.style.boxShadow = 'none';
});
const sendButton = document.createElement('button');
sendButton.innerHTML = icons.send;
sendButton.style.cssText = `
background: ${config.primaryColor || '#3B82F6'};
color: ${config.textColor || 'white'};
border: none;
padding: 0;
border-radius: 50%;
cursor: pointer;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.2s ease;
box-shadow: 0 2px 8px ${config.primaryColor || '#3B82F6'}40;
flex-shrink: 0;
`;
// sendButton hover effects ...
const contactForm = document.createElement('div');
contactForm.style.cssText = 'display: none; gap: 5px; flex-direction: column;';
contactForm.innerHTML = `
<input type="text" placeholder="Your name (optional)" id="visitor-name" style="padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;">
<input type="email" placeholder="Your email (optional)" id="visitor-email" style="padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;">
<button id="contact-owner" style="background: #10B981; color: white; border: none; padding: 8px; border-radius: 4px; cursor: pointer;">Contact Owner</button>
`;
const toggleButton = document.createElement('button');
toggleButton.innerHTML = icons.chat;
toggleButton.style.cssText = `
width: ${sizeMap[size]};
height: ${sizeMap[size]};
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: linear-gradient(135deg, ${config.primaryColor || '#3B82F6'}, ${adjustColor(config.primaryColor || '#3B82F6', -20)});
color: ${config.textColor || 'white'};
border: none;
font-size: ${size === 'small' ? '18px' : size === 'large' ? '28px' : '24px'};
cursor: pointer;
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
`;
// Add ripple effect
toggleButton.addEventListener('click', (e) => {
const ripple = document.createElement('div');
ripple.style.cssText = `
position: absolute;
border-radius: 50%;
background: rgba(255,255,255,0.6);
transform: scale(0);
animation: ripple 0.6s linear;
pointer-events: none;
`;
const rect = toggleButton.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = (e.clientX - rect.left - size / 2) + 'px';
ripple.style.top = (e.clientY - rect.top - size / 2) + 'px';
toggleButton.appendChild(ripple);
setTimeout(() => ripple.remove(), 600);
});
// Add hover effects
toggleButton.addEventListener('mouseenter', () => {
toggleButton.style.transform = 'scale(1.1)';
toggleButton.style.boxShadow = '0 12px 35px rgba(0,0,0,0.2)';
});
toggleButton.addEventListener('mouseleave', () => {
toggleButton.style.transform = 'scale(1)';
toggleButton.style.boxShadow = '0 8px 25px rgba(0,0,0,0.15)';
});
// Add CSS for ripple animation
if (!document.querySelector('#ripple-style')) {
const rippleStyle = document.createElement('style');
rippleStyle.id = 'ripple-style';
rippleStyle.textContent = `
@keyframes ripple {
to { transform: scale(4); opacity: 0; }
}
`;
document.head.appendChild(rippleStyle);
}
let currentSpeechSpeed = 1.0;
const speeds = [0.5, 1.0, 1.5, 2.0];
function cycleSpeed() {
const currentIndex = speeds.indexOf(currentSpeechSpeed);
currentSpeechSpeed = speeds[(currentIndex + 1) % speeds.length];
// Update all speed indicators
document.querySelectorAll('.speed-indicator').forEach(el => {
el.textContent = currentSpeechSpeed + 'x';
});
}
let chatHistory = [];
let showingContactForm = false;
let sessionId = localStorage.getItem(`chat-session-${websiteId}`) || generateSessionId();
let visitorName = '';
let visitorEmail = '';
let websocket = null;
function generateSessionId() {
const id = 'session-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
localStorage.setItem(`chat-session-${websiteId}`, id);
return id;
}
function connectWebSocket() {
try {
const wsUrl = apiBase.replace(/^http/, 'ws') + `/ws/${websiteId}`;
websocket = new WebSocket(wsUrl);
websocket.onopen = () => {
console.log('WebSocket connected for session:', sessionId);
console.log('Website ID:', websiteId);
// Register session with server
websocket.send(JSON.stringify({
type: 'register_session',
session_id: sessionId,
website_id: websiteId
}));
};
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received WebSocket message:', data); console.log('Needs Contact:', data.needs_owner_contact);
if (data.type === 'admin_response' || data.type === 'ai_response') {
// Remove typing indicator if any
hideTypingIndicator();
// Show response
const msgText = data.message || data.response;
addMessage(msgText, false, data.needs_owner_contact, data.sender || 'Support Team');
} else if (data.type === 'session_registered') {
console.log('Session registered successfully:', data.session_id);
}
};
websocket.onclose = () => {
console.log('WebSocket disconnected');
// Reconnect after 3 seconds
setTimeout(connectWebSocket, 3000);
};
websocket.onerror = (error) => {
console.error('WebSocket error:', error);
};
} catch (error) {
console.error('Failed to connect WebSocket:', error);
}
}
function addMessage(text, isUser = false, showContact = false, senderName = null) {
const messageDiv = document.createElement('div');
messageDiv.style.cssText = `
margin-bottom: 16px;
display: flex;
${isUser ? 'justify-content: flex-end;' : 'justify-content: flex-start;'}
animation: slideUp 0.3s ease;
`;
if (!isUser) {
const avatar = document.createElement('div');
avatar.style.cssText = `
width: 32px;
height: 32px;
border-radius: 50%;
background: ${config.primaryColor || '#3B82F6'};
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
margin-right: 8px;
flex-shrink: 0;
align-self: flex-end;
`;
avatar.textContent = '🤖';
messageDiv.appendChild(avatar);
}
const bubble = document.createElement('div');
bubble.style.cssText = `
max-width: 75%;
padding: 12px 16px;
border-radius: ${isUser ? '20px 20px 4px 20px' : '20px 20px 20px 4px'};
font-size: 14px;
line-height: 1.4;
word-wrap: break-word;
position: relative;
${isUser
? `background: linear-gradient(135deg, ${config.primaryColor || '#3B82F6'}, ${adjustColor(config.primaryColor || '#3B82F6', -10)}); color: ${config.textColor || 'white'}; box-shadow: 0 2px 8px ${config.primaryColor || '#3B82F6'}30;`
: `background: white; color: #374151; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border: 1px solid #f0f0f0;`
}
transition: all 0.2s ease;
`;
// Add sender name for admin messages
if (senderName && !isUser) {
const senderLabel = document.createElement('div');
senderLabel.style.cssText = 'font-size: 11px; color: #6B7280; margin-bottom: 4px; font-weight: 500;';
senderLabel.textContent = senderName;
bubble.appendChild(senderLabel);
}
const messageText = document.createElement('div');
// Enhanced Markdown Parser
const parseMarkdown = (text) => {
let html = text;
// 1. Parse Links: [text](url) -> <a href="url">text</a>
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
html = html.replace(linkRegex, (match, linkText, url) => {
return `<a href="${url}" target="_blank" style="color: ${config.primaryColor || '#3B82F6'}; text-decoration: underline;">${linkText}</a>`;
});
// 2. Parse Italics: _text_ -> <em>text</em>
const italicRegex = /_([^_]+)_/g;
html = html.replace(italicRegex, '<em>$1</em>');
return html;
};
messageText.innerHTML = parseMarkdown(text);
bubble.appendChild(messageText);
// Add Speech Controls for Bot Messages
if (!isUser) {
bubble.className = 'ca-widget-chat-bubble';
const speechControls = document.createElement('div');
speechControls.className = 'speech-controls';
const playBtn = document.createElement('button');
playBtn.className = 'speech-btn';
playBtn.innerHTML = icons.volume;
playBtn.title = 'Speak Response';
const speedBtn = document.createElement('div');
speedBtn.className = 'speed-indicator';
speedBtn.textContent = currentSpeechSpeed + 'x';
speedBtn.title = 'Change Speed';
let isSpeaking = false;
let utterance = null;
playBtn.onclick = () => {
if (isSpeaking) {
window.speechSynthesis.cancel();
playBtn.innerHTML = icons.volume;
isSpeaking = false;
} else {
window.speechSynthesis.cancel(); // Stop any current speech
utterance = new SpeechSynthesisUtterance(text);
utterance.rate = currentSpeechSpeed;
utterance.onend = () => {
playBtn.innerHTML = icons.volume;
isSpeaking = false;
};
utterance.onerror = () => {
playBtn.innerHTML = icons.volume;
isSpeaking = false;
};
playBtn.innerHTML = icons.stop;
window.speechSynthesis.speak(utterance);
isSpeaking = true;
}
};
speedBtn.onclick = (e) => {
e.stopPropagation();
cycleSpeed();
if (isSpeaking && utterance) {
// If speaking, restart with new speed
playBtn.click(); // Stop
setTimeout(() => playBtn.click(), 50); // Restart
}
};
speechControls.appendChild(playBtn);
speechControls.appendChild(speedBtn);
bubble.appendChild(speechControls);
}
// Add hover effect for bot messages
if (!isUser) {
bubble.addEventListener('mouseenter', () => {
bubble.style.transform = 'translateY(-1px)';
bubble.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
});
bubble.addEventListener('mouseleave', () => {
bubble.style.transform = 'translateY(0)';
bubble.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
});
}
messageDiv.appendChild(bubble);
messages.appendChild(messageDiv);
// Smooth scroll to bottom
setTimeout(() => {
messages.scrollTo({
top: messages.scrollHeight,
behavior: 'smooth'
});
}, 100);
chatHistory.push({ text, isUser });
if (showContact && !showingContactForm) {
showContactForm();
}
}
function showContactForm() {
showingContactForm = true;
contactForm.style.display = 'flex';
messageForm.style.display = 'none';
// Check if contact message already exists to prevent duplicates
const existingContactMsg = inputContainer.querySelector('.contact-prompt-message');
if (!existingContactMsg) {
const contactMsg = document.createElement('div');
contactMsg.className = 'contact-prompt-message';
contactMsg.style.cssText = 'text-align: center; color: #6B7280; font-size: 12px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;';
contactMsg.innerHTML = '<span>Would you like the owner to contact you directly?</span><button id="close-contact" style="background: none; border: none; color: #6B7280; cursor: pointer; font-size: 14px;">✕</button>';
inputContainer.insertBefore(contactMsg, contactForm);
}
}
// Client-side content filter
function isInappropriate(text) {
const inappropriate = [
/\b(porn|sex|nude|xxx|adult|erotic)\b/i,
/\b(fuck|shit|damn|bitch|asshole)\b/i,
/\b(kill|die|murder|bomb|weapon)\b/i
];
return inappropriate.some(pattern => pattern.test(text));
}
async function sendMessage() {
const message = input.value.trim();
if (!message) return;
// Filter inappropriate content on client side
if (isInappropriate(message)) {
addMessage(message, true);
input.value = '';
addMessage('I\'m designed to provide helpful business information. Please keep our conversation professional and appropriate.');
return;
}
addMessage(message, true);
input.value = '';
showTypingIndicator(); // Show typing animation
// OPTIMIZED: Use WebSocket if available
if (websocket && websocket.readyState === WebSocket.OPEN) {
try {
websocket.send(JSON.stringify({
type: 'chat_message',
message: message,
visitor_name: visitorName,
visitor_email: visitorEmail, // Captured if available
website_id: websiteId,
session_id: sessionId
}));
// Typing indicator assumes socket will reply eventually
// (We don't get a promise here, relying on onmessage)
return;
} catch (e) {
console.warn("Socket send failed, falling back to HTTP");
fallbackToHttp(message);
}
} else {
fallbackToHttp(message);
}
}
async function fallbackToHttp(message) {
try {
const responsePromise = fetch(`${apiBase}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'ngrok-skip-browser-warning': 'true'
},
body: JSON.stringify({
message: message,
website_id: websiteId,
session_id: sessionId,
visitor_name: visitorName,
visitor_email: visitorEmail
})
});
// Dynamic Status Updates (Engagement Milestones)
const messages = [
"Anas is deep searching...",
"Contextualizing your request...",
"Summarizing information...",
"Almost there! Formulating answer..."
];
let msgIndex = 0;
const statusInterval = setInterval(() => {
if (msgIndex < messages.length) {
showTypingIndicator(messages[msgIndex]);
msgIndex++;
}
}, 2000);
const response = await responsePromise;
clearInterval(statusInterval);
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
hideTypingIndicator(); // Hide typing animation
// Add AI response to chat
addMessage(data.response, false, data.needs_owner_contact);
} catch (error) {
console.error('Error sending message:', error);
hideTypingIndicator();
addMessage("Only Anas knows the answer to that... Let me connect you.", false, true);
showContactForm();
}
}
async function contactOwner() {
const name = document.getElementById('visitor-name').value;
const email = document.getElementById('visitor-email').value;
// Find the last USER message (skip AI responses)
let lastMessage = '';
for (let i = chatHistory.length - 1; i >= 0; i--) {
if (chatHistory[i].isUser) {
lastMessage = chatHistory[i].text;
break;
}
}
// Store visitor info for future messages
visitorName = name;
visitorEmail = email;
console.log('[Widget v1.2] Contacting owner. Last User Message:', lastMessage);
try {
const response = await fetch(`${apiBase}/api/actions/contact-owner`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'ngrok-skip-browser-warning': 'true'
},
body: JSON.stringify({
website_id: websiteId,
session_id: sessionId, // Pass session ID to update anonymous user
visitor_name: name,
visitor_email: email,
message: lastMessage,
chat_context: JSON.stringify(chatHistory)
})
});
const data = await response.json();
addMessage(data.message);
contactForm.style.display = 'none';
} catch (error) {
addMessage('Failed to contact owner. Please try again.');
}
}
// Enhanced toggle functionality
toggleButton.onclick = () => {
const isOpening = !chatBox.classList.contains('active');
if (isOpening) {
chatBox.classList.add('active');
toggleButton.innerHTML = icons.close;
toggleButton.style.animation = 'bounce 0.6s ease';
if (window.innerWidth <= 580) {
document.body.style.overflow = 'hidden';
}
} else {
chatBox.classList.remove('active');
chatBox.classList.remove('fullscreen'); // Reset fullscreen when closing
isFullScreen = false;
fullScreenBtn.innerHTML = icons.maximize;
toggleButton.innerHTML = icons.chat;
widgetContainer.style.zIndex = '10000';
document.body.style.overflow = '';
}
};
// Full Screen Toggle Logic
let isFullScreen = false;
fullScreenBtn.onclick = () => {
isFullScreen = !isFullScreen;
if (isFullScreen) {
chatBox.classList.add('fullscreen');
fullScreenBtn.innerHTML = icons.minimize;
widgetContainer.style.zIndex = '100000';
} else {
chatBox.classList.remove('fullscreen');
fullScreenBtn.innerHTML = icons.maximize;
widgetContainer.style.zIndex = '10000';
}
};
closeBtn.onclick = () => {
chatBox.classList.remove('active');
chatBox.classList.remove('fullscreen');
isFullScreen = false;
fullScreenBtn.innerHTML = icons.maximize;
toggleButton.innerHTML = icons.chat;
widgetContainer.style.zIndex = '10000';
};
// Close button hover effects handled by createHeaderBtn logic
sendButton.onclick = sendMessage;
input.onkeypress = (e) => {
if (e.key === 'Enter') sendMessage();
};
document.addEventListener('click', (e) => {
if (e.target.id === 'contact-owner') {
contactOwner();
} else if (e.target.id === 'close-contact') {
contactForm.style.display = 'none';
messageForm.style.display = 'flex';
showingContactForm = false;
const contactMsg = inputContainer.querySelector('.contact-prompt-message');
if (contactMsg) {
contactMsg.remove();
}
}
});
// Assembly
messageForm.appendChild(inputWrapper);
messageForm.appendChild(sendButton);
inputContainer.appendChild(messageForm);
inputContainer.appendChild(contactForm);
chatBox.appendChild(header);
chatBox.appendChild(messages);
chatBox.appendChild(inputContainer);
widgetContainer.appendChild(chatBox);
widgetContainer.appendChild(toggleButton);
if (document.body) {
document.body.appendChild(widgetContainer);
console.log('CustomerAgentWidget: Widget appended to DOM');
} else {
console.log('CustomerAgentWidget: document.body not ready, waiting for DOMContentLoaded');
document.addEventListener('DOMContentLoaded', () => {
document.body.appendChild(widgetContainer);
console.log('CustomerAgentWidget: Widget appended to DOM (delayed)');
});
}
// Connect WebSocket for real-time messages
connectWebSocket();
// Initial greeting with typing indicator
setTimeout(() => {
showTypingIndicator();
setTimeout(() => {
hideTypingIndicator();
// Get dynamic greeting based on website tone
fetch(`${apiBase}/api/websites/${websiteId}/greeting`, {
headers: { 'ngrok-skip-browser-warning': 'true' }
})
.then(response => response.json())
.then(data => addMessage(data.greeting))
.catch(() => addMessage('Hello! I\'m Anas, your virtual assistant. How can I help you today? 👋'));
}, 1500);
}, 1000);
function showTypingIndicator(statusText = "") {
let typingDiv = document.getElementById('typing-indicator');
// If already exists, just update text
if (typingDiv) {
const statusEl = typingDiv.querySelector('.typing-status');
if (statusEl) statusEl.textContent = statusText;
return;
}
typingDiv = document.createElement('div');
typingDiv.id = 'typing-indicator';
typingDiv.style.cssText = `
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 16px;
animation: slideUp 0.3s ease;
`;
const avatarAndBubble = document.createElement('div');
avatarAndBubble.style.cssText = 'display: flex; align-items: flex-end;';
const avatar = document.createElement('div');
avatar.style.cssText = `
width: 32px;
height: 32px;
border-radius: 50%;
background: ${config.primaryColor || '#3B82F6'};
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
margin-right: 8px;
`;
avatar.textContent = '🤖';
const bubble = document.createElement('div');
bubble.style.cssText = `
background: white;
padding: 12px 16px;
border-radius: 20px 20px 20px 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border: 1px solid #f0f0f0;
`;
const dots = document.createElement('div');
dots.innerHTML = '<span>●</span><span>●</span><span>●</span>';
dots.style.cssText = `
display: flex;
gap: 4px;
color: #9CA3AF;
`;
// Animate dots
const spans = dots.querySelectorAll('span');
spans.forEach((span, i) => {
span.style.animation = `pulse 1.4s ease-in-out ${i * 0.2}s infinite`;
});
bubble.appendChild(dots);
avatarAndBubble.appendChild(avatar);
avatarAndBubble.appendChild(bubble);
typingDiv.appendChild(avatarAndBubble);
if (statusText) {
const statusEl = document.createElement('div');
statusEl.className = 'typing-status';
statusEl.style.cssText = 'font-size: 11px; color: #9CA3AF; margin-left: 40px; margin-top: 4px; font-style: italic;';
statusEl.textContent = statusText;
typingDiv.appendChild(statusEl);
}
messages.appendChild(typingDiv);
messages.scrollTo({
top: messages.scrollHeight,
behavior: 'smooth'
});
}
function hideTypingIndicator() {
const indicator = document.getElementById('typing-indicator');
if (indicator) {
indicator.remove();
}
}
}
// Auto-initialize
const script = document.currentScript;
const websiteId = script ? script.getAttribute('data-website-id') : null;
const configData = script ? script.getAttribute('data-config') : null;
let config = {
theme: 'blue',
position: 'bottom-right',
size: 'medium',
primaryColor: '#3B82F6',
textColor: '#FFFFFF',
backgroundColor: '#FFFFFF'
};
if (configData) {
try {
config = { ...config, ...JSON.parse(configData) };
} catch (e) {
console.warn('Invalid widget config, using defaults');
}
}
if (websiteId) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => createChatWidget(websiteId, config));
} else {
createChatWidget(websiteId, config);
}
}
// Helper function to adjust color brightness
function adjustColor(color, amount) {
const usePound = color[0] === '#';
const col = usePound ? color.slice(1) : color;
const num = parseInt(col, 16);
let r = (num >> 16) + amount;
let g = (num >> 8 & 0x00FF) + amount;
let b = (num & 0x0000FF) + amount;
r = r > 255 ? 255 : r < 0 ? 0 : r;
g = g > 255 ? 255 : g < 0 ? 0 : g;
b = b > 255 ? 255 : b < 0 ? 0 : b;
return (usePound ? '#' : '') + (r << 16 | g << 8 | b).toString(16).padStart(6, '0');
}
window.CustomerAgentWidget = { init: createChatWidget };
})();