1/* 3D-LABS Chat Agent â front-end widget. 2 Loaded as a real .js file so nothing on the page can collide with inline PHP. 3 Configuration is read from window.DLABS_CHAT (set via wp_localize_script). */ 4(function () { 5 if (!window.DLABS_CHAT) return; 6 var CFG = window.DLABS_CHAT; 7 8 // v1.11.4: client-side safety net. Never build the widget on transactional pages 9 // even if cached JS somehow loads there. The visitor is paying, do not interrupt. 10 var path = (window.location && window.location.pathname) || ''; 11 if (/\/(cart|checkout|shop-cart|shop-checkout|order-received|order-pay|my-account|thank-you)(\/|$)/i.test(path)) { 12 return; 13 } 14 15 function el(tag, attrs, children) { 16 var n = document.createElement(tag); 17 if (attrs) { 18 for (var k in attrs) { 19 if (k === 'style') n.style.cssText = attrs[k]; 20 else if (k === 'text') n.textContent = attrs[k]; 21 else n.setAttribute(k, attrs[k]); 22 } 23 } 24 (children || []).forEach(function (c) { if (c) n.appendChild(c); }); 25 return n; 26 } 27 28 // Build the launcher 29 var launcher = el('div', { 30 id: 'dlabs-chat-launcher', 31 role: 'button', 32 'aria-label': 'Chat with us', 33 tabindex: '0', 34 text: 'ð¬' 35 }); 36 launcher.style.background = CFG.accent; 37 38 // Build the panel 39 var panel = el('div', { id: 'dlabs-chat-panel' }); 40 41 var header = el('div', { id: 'dlabs-chat-header' }); 42 header.style.background = CFG.accent; 43 var headText = el('div', {}, [ 44 el('div', { id: 'dlabs-chat-title', text: CFG.business_name || 'Chat' }), 45 el('div', { id: 'dlabs-chat-sub', text: 'We usually reply in a few seconds' }) 46 ]); 47 var closeBtn = el('button', { id: 'dlabs-chat-close', 'aria-label': 'Close', text: 'Ã' }); 48 header.appendChild(headText); 49 header.appendChild(closeBtn); 50 51 var log = el('div', { id: 'dlabs-chat-log' }); 52 53 var inputRow = el('div', { id: 'dlabs-chat-inputrow' }); 54 var input = el('input', { id: 'dlabs-chat-input', type: 'text', placeholder: 'Ask about our servicesâ¦' }); 55 var send = el('button', { id: 'dlabs-chat-send', text: 'Send' }); 56 send.style.background = CFG.accent; 57 inputRow.appendChild(input); 58 inputRow.appendChild(send); 59 60 panel.appendChild(header); 61 panel.appendChild(log); 62 panel.appendChild(inputRow); 63 64 document.body.appendChild(launcher); 65 document.body.appendChild(panel); 66 67 var sessionId = null; 68 var nonce = null; 69 var started = false; 70 71 function bubble(role, text) { 72 var wrap = el('div', { class: 'dlabs-bubble-wrap dlabs-' + role }); 73 var b = el('div', { class: 'dlabs-bubble dlabs-' + role + '-bubble' }); 74 if (role === 'user') b.style.background = CFG.accent; 75 // v1.11.2: safely turn URLs in the text into clickable links (no innerHTML, no XSS). 76 // wa.me links get rendered as a distinct green "Chat on WhatsApp" pill so the 77 // visitor never has to read out digits, they just tap. 78 linkify(b, text || ''); 79 wrap.appendChild(b); 80 log.appendChild(wrap); 81 log.scrollTop = log.scrollHeight; 82 } 83 84 function linkify(container, text) { 85 // v1.11.3: parse BOTH markdown-style [label](url) AND bare https://... URLs. 86 // The LLM often emits markdown links from habit. If we do not parse them, 87 // visitors see raw "[here](https://...)" text and cannot click through. 88 // Also: all links open in the SAME window per user preference (no target=_blank). 89 // Only http/https allowed (no javascript: or data:) for safety. 90 91 var mdRe = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g; 92 var urlRe = /https?:\/\/[^\s)]+/g; 93 var matches = []; 94 var m; 95 96 while ((m = mdRe.exec(text)) !== null) { 97 matches.push({ index: m.index, length: m[0].length, label: m[1], url: m[2], trailing: '' }); 98 } 99 while ((m = urlRe.exec(text)) !== null) { 100 // Skip URLs already inside a markdown match we captured above 101 var covered = matches.some(function (mm) { 102 return m.index >= mm.index && m.index < mm.index + mm.length; 103 }); 104 if (covered) continue; 105 var url = m[0]; 106 var trailing = ''; 107 while (url.length && /[.,;:!?]$/.test(url)) { 108 trailing = url.slice(-1) + trailing; 109 url = url.slice(0, -1); 110 } 111 matches.push({ index: m.index, length: m[0].length, label: url, url: url, trailing: trailing }); 112 } 113 matches.sort(function (a, b) { return a.index - b.index; }); 114 115 var pos = 0;
116 matches.forEach(function (mm) { 117 if (mm.index > pos) { 118 container.appendChild(document.createTextNode(text.slice(pos, mm.index))); 119 } 120 var isWhatsApp = mm.url.indexOf('https://wa.me/') === 0 || 121 mm.url.indexOf('https://api.whatsapp.com/') === 0; 122 var a = el('a', { 123 class: isWhatsApp ? 'dlabs-wa-link' : 'dlabs-inline-link', 124 href: mm.url, 125 text: isWhatsApp ? 'Chat on WhatsApp' : mm.label 126 }); 127 container.appendChild(a); 128 if (mm.trailing) container.appendChild(document.createTextNode(mm.trailing)); 129 pos = mm.index + mm.length; 130 }); 131 if (pos < text.length) { 132 container.appendChild(document.createTextNode(text.slice(pos))); 133 } 134 } 135 136 function typing(on) { 137 var t = document.getElementById('dlabs-typing'); 138 if (on && !t) { 139 var wrap = el('div', { class: 'dlabs-bubble-wrap dlabs-assistant', id: 'dlabs-typing' }); 140 wrap.appendChild(el('div', { class: 'dlabs-bubble dlabs-typing-dots', text: 'â¢â¢â¢' })); 141 log.appendChild(wrap); 142 log.scrollTop = log.scrollHeight; 143 } else if (!on && t) { 144 t.remove(); 145 } 146 } 147 148 function renderTool(t) { 149 if (!t || !t.result) return; 150 if (t.name === 'recommend_product' && t.result.url) { 151 var a = el('a', { 152 class: 'dlabs-link', 153 href: t.result.url, 154 text: (t.result.title || t.result.url) 155 }); 156 log.appendChild(a); 157 } 158 if (t.name === 'create_checkout' && t.result.url) { 159 var label = t.result.provider === 'woocommerce' ? 'Add to cart' 160 : t.result.provider === 'cashfree' ? 'Pay securely via Cashfree' 161 : t.result.provider === 'whatsapp_fallback' ? 'Chat with our team on WhatsApp' 162 : 'Pay via PayPal'; 163 var pay = el('a', { 164 class: 'dlabs-cta', 165 href: t.result.url, 166 text: label 167 }); 168 log.appendChild(pay); 169 } 170 log.scrollTop = log.scrollHeight; 171 } 172 173 // v1.10.0: build a time-of-day greeting from the VISITOR's local browser time, 174 // so a visitor in London at 9am sees "Good morning to you" even when the WP server 175 // in India thinks it is 2:30pm. 176 function localGreetingPrefix() { 177 var h = new Date().getHours(); 178 if (h < 12) return 'Good morning to you'; 179 if (h < 17) return 'Good afternoon to you'; 180 return 'Good evening to you'; 181 } 182 183 // v1.10.0: silence nudge state. Warm, hopeful, no-emoji nudges that fire if the visitor 184 // goes quiet for 45 seconds after a bot reply. hasContact flips true once the server 185 // captures an email or phone, so we do not nudge for contact after that. 186 var nudgeTimer = null; 187 var nudgeCount = 0; 188 var hasContact = false; 189 190 function clearNudge() { 191 if (nudgeTimer) { clearTimeout(nudgeTimer); nudgeTimer = null; } 192 } 193 194 function scheduleNudge() { 195 clearNudge(); 196 if (nudgeCount >= 2) return; // never nudge more than twice in a session 197 nudgeTimer = setTimeout(function () { 198 var messages; 199 if (hasContact) { 200 messages = [ 201 'Still here whenever you are ready. No hurry at all.', 202 'Take your time. If it is easier, message us here: https://wa.me/919843511204' 203 ]; 204 } else { 205 messages = [ 206 'Still there? No worries, take your time. If you prefer, message us here: https://wa.me/919843511204 and a real engineer will reply in minutes.', 207 'No pressure at all. Whenever you are ready, just share your email and we will take it from there.', 208 'Take your time. You can also reach us here: https://wa.me/919843511204' 209 ]; 210 } 211 bubble('assistant', messages[Math.floor(Math.random() * messages.length)]); 212 nudgeCount++; 213 }, 45000); 214 } 215 216 function openPanel() { 217 panel.classList.add('open'); 218 if (started) return; 219 started = true; 220 fetch(CFG.rest + '/start', { 221 method: 'POST', 222 headers: { 'Content-Type': 'application/json' }, 223 body: '{}' 224 }) 225 .then(function (r) { return r.json(); }) 226 .then(function (d) { 227 if (!d || !d.sessionId) throw new Error('Bad response from /start'); 228 sessionId = d.sessionId; 229 nonce = d.nonce; 230 // Replace the {GREETING_PREFIX} token with the visitor's local-time greeting. 231 var greeting = d.greeting || 'Good day to you. How can I help?'; 232 greeting = greeting.replace('{GREETING_PREFIX}', localGreetingPrefix()); 233 bubble('assistant', greeting); 234 scheduleNudge(); 235 }) 236 .catch(function (err) { 237 console.error('[dlabs-chat] start failed', err); 238 bubble('assistant', 'Sorry, we had a small hiccup connecting. Please try again in a moment.'); 239 }); 240 } 241 242 function submit() { 243 var text = (input.value || '').trim(); 244 if (!text) return; 245 if (!sessionId) { 246 bubble('assistant', 'Just a moment, still connecting. Please try again in a second.'); 247 return; 248 } 249 clearNudge();
249 // visitor spoke, so cancel any pending nudge 250 bubble('user', text); 251 input.value = ''; 252 input.focus(); 253 typing(true); 254 fetch(CFG.rest + '/message', { 255 method: 'POST', 256 headers: { 'Content-Type': 'application/json' }, 257 body: JSON.stringify({ 258 sessionId: sessionId, 259 text: text, 260 pageUrl: window.location.href, 261 nonce: nonce 262 }) 263 }) 264 .then(function (r) { 265 // Try JSON first; if the server returned HTML (a fatal error page), fall back to the raw text 266 return r.text().then(function (raw) { 267 var parsed; 268 try { parsed = JSON.parse(raw); } 269 catch (e) { parsed = { message: 'Sorry, we had a small hiccup on our side (' + r.status + '). Please try again.', raw: raw.slice(0, 200) }; } 270 return { ok: r.ok, status: r.status, d: parsed }; 271 }); 272 }) 273 .then(function (res) { 274 typing(false); 275 if (!res.ok) { 276 var msg = (res.d && res.d.message) ? res.d.message : ('Sorry, small hiccup on our side, please try again. Error ' + res.status); 277 console.error('[dlabs-chat] message failed', res); 278 bubble('assistant', msg); 279 return; 280 } 281 bubble('assistant', res.d.message || 'One moment please.'); 282 // If the server captured a lead this turn, remember it so we do not 283 // nudge the visitor to share contact info again later. 284 (res.d.tools || []).forEach(function (t) { 285 if (t && t.name === 'capture_lead' && t.result && t.result.ok) { 286 hasContact = true; 287 } 288 renderTool(t); 289 }); 290 scheduleNudge(); // re-arm silence nudge after every bot reply 291 }) 292 .catch(function (err) { 293 typing(false); 294 console.error('[dlabs-chat] fetch failed', err); 295 bubble('assistant', 'Sorry, we could not reach the server just now. Please try once more.'); 296 }); 297 } 298 299 launcher.addEventListener('click', openPanel); 300 launcher.addEventListener('keydown', function (e) { 301 if (e.key === 'Enter' || e.key === ' ') openPanel(); 302 }); 303 closeBtn.addEventListener('click', function () { 304 panel.classList.remove('open'); 305 try { 306 sessionStorage.setItem('dlabs_chat_closed', '1'); 307 // Remember close time for cooldown mode 308 localStorage.setItem('dlabs_chat_closed_at', String(Date.now())); 309 } catch (e) {} 310 }); 311 send.addEventListener('click', submit); 312 input.addEventListener('keydown', function (e) { if (e.key === 'Enter') submit(); }); 313 314 // Auto-open logic â three modes: 315 // session = fires once per browser tab session (respectful default) 316 // always = fires on every pageview (highest engagement, some find it pushy) 317 // cooldown = fires again after N milliseconds since last close 318 var delaySec = Number(CFG.auto_open || 0); 319 var mode = CFG.open_mode || 'session'; 320 var cooldownMs = Number(CFG.open_cooldown || 0); 321 322 function shouldAutoOpen() { 323 if (delaySec <= 0) return false; 324 try { 325 if (mode === 'always') return true; 326 if (mode === 'session') return sessionStorage.getItem('dlabs_chat_closed') !== '1'; 327 if (mode === 'cooldown') { 328 var closedAt = Number(localStorage.getItem('dlabs_chat_closed_at') || 0); 329 return !closedAt || (Date.now() - closedAt) > cooldownMs; 330 } 331 } catch (e) {} 332 return true; // if storage fails, default to opening 333 } 334 335 if (shouldAutoOpen()) { 336 setTimeout(function () {
337 if (!panel.classList.contains('open')) openPanel(); 338 }, delaySec * 1000); 339 } 340})();
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.