1/** 2 * Sardar Patel AI Counselling Chatbot Frontend JS Script 3 */ 4 5jQuery(document).ready(function($) { 6 // Check if container exists 7 if (!$('#spai-chatbot-container').length) { 8 return; 9 } 10 11 // State Variables 12 const restUrl = spaiSettings.restUrl; 13 const nonce = spaiSettings.nonce; 14 const sessionKey = spaiSettings.sessionKey; 15 let sessionId = localStorage.getItem(sessionKey); 16 17 if (!sessionId) { 18 sessionId = 'spai_' + Math.random().toString(36).substring(2, 15) + '_' + Date.now(); 19 localStorage.setItem(sessionKey, sessionId); 20 } 21 22 // Capture UTM Parameters 23 const getUtmParams = () => { 24 const urlParams = new URLSearchParams(window.location.search); 25 return { 26 page_url: window.location.href, 27 referrer: document.referrer || '', 28 utm_source: urlParams.get('utm_source') || '', 29 utm_medium: urlParams.get('utm_medium') || '', 30 utm_campaign: urlParams.get('utm_campaign') || '', 31 utm_term: urlParams.get('utm_term') || '', 32 utm_content: urlParams.get('utm_content') || '' 33 }; 34 }; 35 36 const utmParams = getUtmParams(); 37 38 // Elements 39 const $launcher = $('#spai-chatbot-launcher'); 40 const $panel = $('#spai-chatbot-panel'); 41 const $closeBtn = $('#spai-chatbot-close'); 42 const $input = $('#spai-chat-input'); 43 const $sendBtn = $('#spai-chat-send'); 44 const $body = $('#spai-chat-body'); 45 const $messagesList = $('.spai-messages-list'); 46 const $typingIndicator = $('.spai-typing-indicator'); 47 const $consentCheckbox = $('#spai-consent-checkbox'); 48 49 // Toggle Open/Close 50 if ($launcher.length) { 51 $launcher.on('click', function() { 52 const isOpen = $panel.hasClass('open'); 53 if (isOpen) { 54 closeChat(); 55 } else { 56 openChat(); 57 } 58 }); 59 } 60 61 $closeBtn.on('click', closeChat); 62 63 function openChat() { 64 $panel.addClass('open').show().attr('aria-hidden', 'false'); 65 $launcher.addClass('active').attr('aria-expanded', 'true'); 66 $input.focus(); 67 scrollToBottom(); 68 69 // If empty, send a silent init trigger to load greeting or resume 70 if ($messagesList.children().length <= 1) { 71 loadChatHistory(); 72 } 73 } 74 75 function closeChat() { 76 $panel.removeClass('open').hide().attr('aria-hidden', 'true'); 77 $launcher.removeClass('active').attr('aria-expanded', 'false'); 78 } 79 80 // Auto-expand input textarea 81 $input.on('input', function() { 82 this.style.height = 'auto'; 83 this.style.height = (this.scrollHeight - 10) + 'px'; 84 if ($(this).val().trim() !== '') { 85 $sendBtn.addClass('active'); 86 } else { 87 $sendBtn.removeClass('active'); 88 } 89 }); 90 91 // Enter Key triggers send 92 $input.on('keydown', function(e) { 93 if (e.key === 'Enter' && !e.shiftKey) { 94 e.preventDefault(); 95 sendMessage(); 96 } 97 }); 98 99 $sendBtn.on('click', sendMessage); 100 101 // Quick Reply buttons click 102 $(document).on('click', '.spai-quick-reply-btn', function() { 103 const text = $(this).text().trim(); 104 const intent = $(this).data('intent'); 105 sendMessage(text, { intent: intent }); 106 }); 107 108 // Choices buttons click (inside bubbles) 109 $(document).on('click', '.spai-choice-btn', function() { 110 if ($(this).is('a')) { 111 return; // Let standard link redirect fire 112 } 113 const text = $(this).text().trim(); 114 const value = $(this).data('value'); 115 const type = $(this).data('type'); // 'counselling_mode', 'slot', etc. 116 117 // Disable all buttons in this bubble to prevent double clicks 118 $(this).parent().find('.spai-choice-btn').attr('disabled', 'disabled'); 119 $(this).addClass('selected'); 120 121 sendMessage(text, { 122 choice_value: value, 123 choice_type: type 124 }); 125 }); 126 127 // Main Send Message Function 128 function sendMessage(customText = '', meta = {}) { 129 const text = customText !== '' ? customText : $input.val().trim(); 130 if (text === '') return; 131 132 // Verify GDPR consent if lead collection is in progress 133 if (meta.choice_type === 'consent' && !$consentCheckbox.is(':checked')) { 134 alert('Please check the consent box to proceed.'); 135 return; 136 } 137 138 // Clear input 139 if (customText === '') { 140 $input.val(''); 141 $input.css('height', 'auto'); 142 $sendBtn.removeClass('active'); 143 } 144 145 // Append User Message to UI 146 appendMessage(text, 'user'); 147 scrollToBottom(); 148 149 // Show Typing Indicator 150 showTyping(); 151 152 // AJAX Request to Chatbot API 153 const payload = $.extend({ 154 session_id: sessionId, 155 message: text, 156 nonce: nonce 157 }
157, utmParams, meta); 158 159 $.ajax({ 160 url: restUrl + 'chat', 161 method: 'POST', 162 beforeSend: function(xhr) { 163 xhr.setRequestHeader('X-WP-Nonce', nonce); 164 }, 165 contentType: 'application/json', 166 data: JSON.stringify(payload), 167 success: function(response) { 168 hideTyping(); 169 if (response.success && response.data) { 170 processBotResponse(response.data); 171 } else { 172 appendFallback(); 173 } 174 }, 175 error: function(err) { 176 hideTyping(); 177 appendFallback(); 178 } 179 }); 180 } 181 182 // Process response data from Bot API 183 function processBotResponse(data) { 184 const replyText = data.reply; 185 appendMessage(replyText, 'bot', data.choices || []); 186 187 // If lead synchronization or phone collected was successful, save info 188 if (data.session_state) { 189 localStorage.setItem('spai_state', data.session_state); 190 } 191 192 // Custom Redirect or WhatsApp CTA handling 193 if (data.whatsapp_url) { 194 appendWhatsAppButton(data.whatsapp_url); 195 } 196 197 scrollToBottom(); 198 } 199 200 // Append Message UI Helper 201 function appendMessage(text, role, choices = []) { 202 const timeStr = getCurrentTime(); 203 const roleClass = role === 'user' ? 'spai-message-user' : 'spai-message-bot'; 204 205 // Convert text formatting (newlines and simple bold/bullets) 206 let formattedText = escapeHtml(text) 207 .replace(/\n/g, '<br>') 208 .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') 209 .replace(/^- (.*?)(?=<br>|$)/gm, '<li>$1</li>'); 210 211 // Handle unordered lists wrapping 212 if (formattedText.includes('<li>')) { 213 formattedText = formattedText.replace(/(<li>.*?<\/li>)+/g, '<ul>$&</ul>'); 214 } 215 216 let choiceHtml = ''; 217 if (choices && choices.length > 0) { 218 choiceHtml = '<div class="spai-choice-buttons">'; 219 choices.forEach(choice => { 220 if (choice.type === 'url') { 221 choiceHtml += `<a href="${escapeHtml(choice.url)}" target="_blank" class="spai-choice-btn" style="text-decoration:none; text-align:center; display:inline-block; line-height:1.4;">${escapeHtml(choice.label)}</a>`; 222 } else { 223 choiceHtml += `<button class="spai-choice-btn" data-value="${escapeHtml(choice.value)}" data-type="${escapeHtml(choice.type || '')}">${escapeHtml(choice.label)}</button>`; 224 } 225 }); 226 choiceHtml += '</div>'; 227 } 228 229 const html = ` 230 <div class="spai-message ${roleClass}"> 231 <div class="spai-message-bubble"> 232 <p>${formattedText}</p> 233 ${choiceHtml} 234 </div> 235 <div class="spai-message-time">${timeStr}</div> 236 </div> 237 `; 238 239 $messagesList.append(html); 240 } 241 242 // Append WhatsApp Click-to-chat CTA button 243 function appendWhatsAppButton(url) { 244 const html = ` 245 <div class="spai-message spai-message-bot"> 246 <div class="spai-message-bubble" style="background-color: #25d366; color: #fff; text-align: center;"> 247 <p><strong>Request Recorded!</strong></p> 248 <p>Click below to start chat immediately:</p> 249 <div class="spai-choice-buttons" style="margin-top: 5px;"> 250 <a href="${url}" target="_blank" class="spai-choice-btn" style="background: #fff; color: #25d366; text-decoration: none; text-align: center; display: block; border-color: #fff; padding: 10px 15px; border-radius: 8px;">ð¬ Start Chat</a> 251 </div> 252 </div> 253 <div class="spai-message-time">${getCurrentTime()}</div> 254 </div> 255 `; 256 $messagesList.append(html); 257 } 258 259 // Fallback UI when API error occurs 260 function appendFallback() { 261 const whatsappNumber = spaiSettings.businessWhatsApp || ''; 262 const phone = spaiSettings.businessPhone || ''; 263 const company = spaiSettings.companyName || 'our team'; 264 265 let buttonsHtml = ''; 266 if (phone) { 267 buttonsHtml += `<a href="tel:${phone}" class="spai-choice-btn" style="text-decoration: none; display: block; text-align: center;">ð Call Us</a>`; 268 } 269 if (whatsappNumber) { 270 buttonsHtml += `<a href="https://wa.me/${whatsappNumber}" target="_blank" class="spai-choice-btn" style="text-decoration: none; display: block; text-align: center;">ð¬ WhatsApp Chat</a>`; 271 } 272 273 const html = ` 274 <div class="spai-message spai-message-bot"> 275 <div class="spai-message-bubble"> 276 <p>Sorry, I'm unable to answer that right now. You can contact ${company} directly for assistance.</p> 277 ${buttonsHtml ? `<div class="spai-choice-buttons">${buttonsHtml}</div>` : ''}
278 </div> 279 <div class="spai-message-time">${getCurrentTime()}</div> 280 </div> 281 `; 282 $messagesList.append(html); 283 scrollToBottom(); 284 } 285 286 // Load chat transcripts to resume conversations 287 function loadChatHistory() { 288 $.ajax({ 289 url: restUrl + 'chat?session_id=' + sessionId, 290 method: 'GET', 291 beforeSend: function(xhr) { 292 xhr.setRequestHeader('X-WP-Nonce', nonce); 293 }, 294 success: function(response) { 295 if (response.success && response.data && response.data.length > 0) { 296 // Clear default greeting since history is loaded 297 $messagesList.empty(); 298 response.data.forEach(msg => { 299 appendMessage(msg.message, msg.role); 300 }); 301 scrollToBottom(); 302 } 303 } 304 }); 305 } 306 307 // Helper utilities 308 function scrollToBottom() { 309 $body.scrollTop($body[0].scrollHeight); 310 } 311 312 function showTyping() { 313 $typingIndicator.show(); 314 scrollToBottom(); 315 } 316 317 function hideTyping() { 318 $typingIndicator.hide(); 319 } 320 321 function getCurrentTime() { 322 const now = new Date(); 323 return now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0'); 324 } 325 326 function escapeHtml(text) { 327 return text 328 .replace(/&/g, "&") 329 .replace(/</g, "<") 330 .replace(/>/g, ">") 331 .replace(/"/g, """) 332 .replace(/'/g, "'"); 333 } 334 335 // Close on ESC key press 336 $(document).on('keyup', function(e) { 337 if (e.key === 'Escape') { 338 closeChat(); 339 } 340 }); 341});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.