https://sridaladamaligawa.lk/wp-content/plugins/maligawa-ai-agent/assets/js/mag-widget.js?ver=2.10.0
1(function () { 2 'use strict'; 3 4 var trigger = document.getElementById('mag-trigger'); 5 var panel = document.getElementById('mag-panel'); 6 var closeBtn = document.getElementById('mag-close'); 7 var messages = document.getElementById('mag-messages'); 8 var input = document.getElementById('mag-input'); 9 var sendBtn = document.getElementById('mag-send'); 10 var chips = document.querySelectorAll('.mag-chip'); 11 12 var conversationHistory = []; // [{role:'user'|'assistant', text: '...'}], kept for this chat session only 13 var introEl = document.getElementById('mag-intro'); 14 15 // The greeting + quick chips only make sense before the conversation has 16 // started â clear them out as soon as the visitor sends anything. 17 function hideIntro() { 18 if (introEl) { 19 introEl.remove(); 20 introEl = null; 21 } 22 } 23 24 // ---------- Panel open/close ---------- 25 26 // The AI often writes markdown (**bold**, `code`, # headers, bullet lists), 27// but our chat bubbles only show plain text â so strip the markdown symbols 28// instead of showing them literally. 29function cleanMarkdown(text) { 30 return text 31 .replace(/\*\*(.*?)\*\*/g, '$1') // **bold** 32 .replace(/\*(.*?)\*/g, '$1') // *italic* 33 .replace(/__(.*?)__/g, '$1') // __bold__ 34 .replace(/_(.*?)_/g, '$1') // _italic_ 35 .replace(/`(.*?)`/g, '$1') // `code` 36 .replace(/^#{1,6}\s+/gm, '') // # headers 37 .replace(/^[-*+]\s+/gm, '⢠'); // - bullet lists -> plain bullet 38} 39 40 41 function openPanel() { 42 panel.classList.remove('mag-hidden'); 43 requestAnimationFrame(function () { panel.classList.add('mag-open'); }); 44 document.documentElement.classList.add('mag-lock'); 45 trigger.classList.add('mag-trigger-hide'); 46 if (window.innerWidth > 900) input.focus(); 47 } 48 function closePanel() { 49 panel.classList.remove('mag-open'); 50 document.documentElement.classList.remove('mag-lock'); 51 trigger.classList.remove('mag-trigger-hide'); 52 setTimeout(function () { panel.classList.add('mag-hidden'); }, 300); 53 } 54 55 var teaser = document.getElementById('mag-teaser'); 56 57 function hideTeaser() { 58 if (teaser) teaser.classList.remove('mag-teaser-show'); 59 } 60 61 trigger.addEventListener('click', function () { 62 hideTeaser(); 63 openPanel(); 64 }); 65 66 if (teaser) { 67 teaser.addEventListener('click', function () { 68 hideTeaser(); 69 openPanel(); 70 }); 71 // Shows 1.5s after page load, disappears on its own after 3s. 72 setTimeout(function () { 73 teaser.classList.add('mag-teaser-show'); 74 setTimeout(hideTeaser, 3000); 75 }, 1500); 76 } 77 closeBtn.addEventListener('click', closePanel); 78 79 // ---------- Chat ---------- 80 function addUserMessage(text) { 81 var el = document.createElement('div'); 82 el.className = 'mag-msg mag-msg-user'; 83 el.textContent = text; 84 messages.appendChild(el); 85 scrollToBottom(); 86 } 87 88 // Renders the photo the visitor picked directly in their chat bubble. The 89 // data URI is already sitting in memory client-side (from downscaleImage), 90 // so this just displays it â nothing extra is uploaded or stored anywhere 91 // beyond the one /identify request already being made for recognition. 92 function addUserImageMessage(dataUri) { 93 var el = document.createElement('div'); 94 el.className = 'mag-msg mag-msg-user mag-msg-image'; 95 var img = document.createElement('img'); 96 img.src = dataUri; 97 img.alt = 'Photo you sent'; 98 el.appendChild(img); 99 messages.appendChild(el); 100 scrollToBottom(); 101 } 102 103 function addBotMessage(text) { 104 var el = document.createElement('div'); 105 el.className = 'mag-msg mag-msg-bot'; 106 107 var textSpan = document.createElement('span'); 108 textSpan.className = 'mag-msg-text'; 109 textSpan.textContent = cleanMarkdown(text); 110 el.appendChild(textSpan); 111 112 if ('speechSynthesis' in window) { 113 var speakBtn = document.createElement('button'); 114 speakBtn.className = 'mag-speak-btn'; 115 speakBtn.setAttribute('aria-label', 'Play audio'); 116 speakBtn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">' + 117 '<path d="M5 9v6h4l5 4V5L9 9H5Z" fill="currentColor"/>' + 118 '<path d="M17 9a4 4 0 0 1 0 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' + 119 '</svg>'; 120 speakBtn.addEventListener('click', function () { 121 toggleSpeak(cleanMarkdown(text), speakBtn); 122}); 123 el.appendChild(speakBtn); 124 } 125 126 messages.appendChild(el); 127 scrollToBottom(); 128 } 129 130 function addBotTyping() { 131 var el = document.createElement('div'); 132 el.className = 'mag-msg mag-msg-bot mag-typing'; 133 el.textContent = '...'; 134 messages.appendChild(el); 135 scrollToBottom(); 136 return el; 137 } 138 139 function scrollToBottom() { 140 messages.scrollTop = messages.scrollHeight; 141 } 142
143 // ---------- Play audio (text-to-speech) ---------- 144 // Uses the browser's own built-in SpeechSynthesis API â free, no server 145 // call, works offline in most browsers. Only one message plays at a time. 146 var activeSpeakBtn = null; 147 148 function toggleSpeak(text, btn) { 149 if (!('speechSynthesis' in window)) return; 150 151 var wasActive = btn === activeSpeakBtn; 152 window.speechSynthesis.cancel(); // stop whatever might currently be playing 153 if (activeSpeakBtn) activeSpeakBtn.classList.remove('mag-speak-active'); 154 activeSpeakBtn = null; 155 156 if (wasActive) return; // this button's own audio was playing â just stop it 157 158 var utterance = new SpeechSynthesisUtterance(text); 159 utterance.onend = function () { 160 btn.classList.remove('mag-speak-active'); 161 if (activeSpeakBtn === btn) activeSpeakBtn = null; 162 }; 163 utterance.onerror = utterance.onend; 164 165 activeSpeakBtn = btn; 166 btn.classList.add('mag-speak-active'); 167 window.speechSynthesis.speak(utterance); 168 } 169 170 // Conversation history is kept as user/assistant pairs so it always stays 171 // easy to trim without breaking alternation (see class-mag-rest.php for 172 // the matching server-side safeguard). 173 function askAgent(query) { 174 hideIntro(); 175 addUserMessage(query); 176 var typingEl = addBotTyping(); 177 conversationHistory.push({ role: 'user', text: query }); 178 179 fetch(MAG_DATA.restUrl, { 180 method: 'POST', 181 headers: { 182 'Content-Type': 'application/json', 183 'X-WP-Nonce': MAG_DATA.nonce 184 }, 185 body: JSON.stringify({ query: query, history: conversationHistory.slice(0, -1), agentName: MAG_DATA.agentName }) 186 }) 187 .then(function (res) { return res.json(); }) 188 .then(function (data) { 189 typingEl.remove(); 190 addBotMessage(data.answer); 191 conversationHistory.push({ role: 'assistant', text: data.answer }); 192 if (conversationHistory.length > 12) { 193 conversationHistory = conversationHistory.slice(-12); 194 } 195 }) 196 .catch(function () { 197 typingEl.remove(); 198 addBotMessage("Sorry, I couldn't reach the server just now. Please try again."); 199 }); 200 } 201 202 function handleSend() { 203 var val = input.value.trim(); 204 if (!val) return; 205 input.value = ''; 206 askAgent(val); 207 } 208 209 sendBtn.addEventListener('click', handleSend); 210 input.addEventListener('keydown', function (e) { 211 if (e.key === 'Enter') handleSend(); 212 }); 213 214 chips.forEach(function (chip) { 215 chip.addEventListener('click', function () { 216 askAgent(chip.getAttribute('data-q')); 217 }); 218 }); 219 220 // ---------- Photo Identify ("what am I looking at?") ---------- 221 // Uses a plain file input (with the "capture" hint) instead of a live 222 // getUserMedia video preview: the browser's own camera/file picker handles 223 // permissions and device access, so there's no custom video-stream code 224 // that can get stuck on an insecure origin or an unsupported browser. 225 var photoBtn = document.getElementById('mag-photo-btn'); 226 var photoInput = document.getElementById('mag-photo-input'); 227 228 photoBtn.addEventListener('click', function () { 229 photoInput.value = ''; // allow re-selecting the same file twice in a row 230 photoInput.click(); 231 }); 232 233 photoInput.addEventListener('change', function () { 234 var file = photoInput.files && photoInput.files[0]; 235 if (!file) return; 236 downscaleImage(file, 900, 0.82, function (dataUri) { 237 sendPhotoForIdentification(dataUri); 238 }); 239 }); 240 241 // Downscales/re-encodes the chosen image client-side so the upload stays 242 // fast â maxDim px on the long edge is plenty for identification. 243 function downscaleImage(file, maxDim, quality, callback) { 244 var img = new Image(); 245 var reader = new FileReader(); 246 reader.onload = function (e) { 247 img.onload = function () { 248 var w = img.naturalWidth, h = img.naturalHeight; 249 var scale = Math.min(1, maxDim / Math.max(w, h)); 250 var canvas = document.createElement('canvas'); 251 canvas.width = w * scale; 252 canvas.height = h * scale; 253 canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height); 254 callback(canvas.toDataURL('image/jpeg', quality)); 255 }; 256 img.src = e.target.result; 257 };
258 reader.readAsDataURL(file); 259 } 260 261 function sendPhotoForIdentification(dataUri) { 262 hideIntro(); 263 addUserImageMessage(dataUri); 264 var typingEl = addBotTyping(); 265 266 fetch(MAG_DATA.identifyUrl, { 267 method: 'POST', 268 headers: { 269 'Content-Type': 'application/json', 270 'X-WP-Nonce': MAG_DATA.nonce 271 }, 272 body: JSON.stringify({ image: dataUri, agentName: MAG_DATA.agentName }) 273 }) 274 .then(function (res) { return res.json(); }) 275 .then(function (data) { 276 typingEl.remove(); 277 addBotMessage(data.description || "I couldn't identify that photo."); 278 279 // Fold this into the conversation so a text follow-up like "tell me more 280 // about it" can still resolve "it" to what was just identified. 281 conversationHistory.push({ role: 'user', text: 'I sent a photo.' }); 282 conversationHistory.push({ role: 'assistant', text: data.description || '' }); 283 if (conversationHistory.length > 12) { 284 conversationHistory = conversationHistory.slice(-12); 285 } 286 }) 287 .catch(function () { 288 typingEl.remove(); 289 addBotMessage("Sorry, I couldn't process that photo just now. Please try again."); 290 }); 291 } 292 293 // ---------- Voice input ---------- 294 // Uses the browser's built-in SpeechRecognition API â free, no server call. 295 // Well supported in Chrome/Edge/Android; not available in Firefox and only 296 // partially in some iOS Safari versions, so this fails gracefully with a 297 // chat message rather than a dead button. 298 var micBtn = document.getElementById('mag-mic-btn'); 299 var originalPlaceholder = input.placeholder; 300 var SpeechRecognitionCtor = window.SpeechRecognition || window.webkitSpeechRecognition || null; 301 var recognition = null; 302 var isListening = false; 303 304 micBtn.addEventListener('click', function () { 305 if (!SpeechRecognitionCtor) { 306 addBotMessage("Voice input isn't supported in this browser yet â try Chrome or Edge, or just type your question."); 307 return; 308 } 309 310 if (isListening) { 311 recognition.stop(); 312 return; 313 } 314 315 hideIntro(); 316 recognition = new SpeechRecognitionCtor(); 317 recognition.lang = navigator.language || 'en-US'; 318 recognition.interimResults = true; 319 recognition.maxAlternatives = 1; 320 321 recognition.onstart = function () { 322 isListening = true; 323 micBtn.classList.add('mag-mic-active'); 324 input.placeholder = 'Listening...'; 325 }; 326 327 recognition.onresult = function (e) { 328 var transcript = ''; 329 for (var i = 0; i < e.results.length; i++) { 330 transcript += e.results[i][0].transcript; 331 } 332 input.value = transcript; 333 }; 334 335 recognition.onerror = function () { 336 isListening = false; 337 micBtn.classList.remove('mag-mic-active'); 338 input.placeholder = originalPlaceholder; 339 }; 340 341 recognition.onend = function () { 342 isListening = false; 343 micBtn.classList.remove('mag-mic-active'); 344 input.placeholder = originalPlaceholder; 345 if (input.value.trim()) handleSend(); 346 }; 347 348 recognition.start(); 349 }); 350 351})();
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.