PageSourceSearch

https://3d-labs.com/wp-admin/admin-ajax.php?action=dlabs_chat_js&ver=1.1.0

js 3d-labs.com collected 2026-10-08 22:47:42 UTC 12,075 bytes, 340 lines download raw bytes

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.