PageSourceSearch

https://sridaladamaligawa.lk/wp-content/plugins/maligawa-ai-agent/assets/js/mag-widget.js?ver=2.10.0

js sridaladamaligawa.lk collected 2026-10-02 09:54:03 UTC 12,219 bytes, 351 lines download raw bytes

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.