PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.8
MxChat – AI Chatbot & Content Generation for WordPress v3.2.8
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / js / admin-testing-tab.js

admin-testing-tab.js in MxChat – AI Chatbot & Content Generation for WordPress 3.2.8, at js/admin-testing-tab.js

660 lines 28.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * MxChat Admin Testing Tab
3 * Handles the in-admin testing interface with debug data display.
4 * Ported from test-panel.js but adapted for inline admin use.
5 */
6
7 (function($) {
8 'use strict';
9
10 var AdminTestingPanel = {
11 initialized: false,
12 lastQueryData: null,
13
14 init: function() {
15 if (this.initialized) return;
16
17 // Only initialize when the testing section becomes visible
18 this.bindTabActivation();
19 this.bindActionButtons();
20 this.interceptChatResponses();
21
22 this.initialized = true;
23 this.log('Admin testing panel initialized');
24 },
25
26 // =====================================================
27 // Tab activation - load system info when testing tab is shown
28 // =====================================================
29
30 bindTabActivation: function() {
31 var self = this;
32
33 // Watch for the testing section becoming active
34 var observer = new MutationObserver(function(mutations) {
35 mutations.forEach(function(mutation) {
36 if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
37 var testingSection = document.getElementById('testing');
38 if (testingSection && testingSection.classList.contains('active')) {
39 self.loadSystemInfo();
40 }
41 }
42 });
43 });
44
45 var testingSection = document.getElementById('testing');
46 if (testingSection) {
47 observer.observe(testingSection, { attributes: true });
48
49 // If already active (e.g. direct URL hash)
50 if (testingSection.classList.contains('active')) {
51 self.loadSystemInfo();
52 }
53 }
54 },
55
56 // =====================================================
57 // Action buttons
58 // =====================================================
59
60 bindActionButtons: function() {
61 var self = this;
62
63 $('#mxch-testing-clear-session').on('click', function() {
64 self.clearChatSession();
65 });
66
67 $('#mxch-testing-clear-debug').on('click', function() {
68 self.clearDebugConsole();
69 });
70 },
71
72 // =====================================================
73 // AJAX Interception - capture testing_data from responses
74 // =====================================================
75
76 interceptChatResponses: function() {
77 var self = this;
78
79 // Store reference globally so interception can find us
80 window.mxchatTestPanelInstance = self;
81
82 // Intercept jQuery AJAX calls
83 if (window.jQuery) {
84 var originalAjax = jQuery.ajax;
85
86 jQuery.ajax = function(options) {
87 var originalSuccess = options.success;
88
89 options.success = function(data, textStatus, jqXHR) {
90 // Check if this is a chat request
91 if (options.data &&
92 (options.data.action === 'mxchat_handle_chat_request' ||
93 options.data.action === 'mxchat_stream_chat')) {
94
95 if (self && data && data.testing_data) {
96 self.handleTestingData(data.testing_data);
97 } else if (self && data && data.data && data.data.testing_data) {
98 self.handleTestingData(data.data.testing_data);
99 }
100 }
101
102 if (originalSuccess) {
103 originalSuccess.call(this, data, textStatus, jqXHR);
104 }
105 };
106
107 return originalAjax.call(this, options);
108 };
109 }
110
111 // Intercept fetch API calls (for both JSON and SSE streaming)
112 var originalFetch = window.fetch;
113
114 window.fetch = function() {
115 var args = arguments;
116 return originalFetch.apply(this, args).then(function(response) {
117 if (args[0] && typeof args[0] === 'string' &&
118 (args[0].includes('admin-ajax.php') || args[0].includes('mxchat'))) {
119 var contentType = response.headers.get('content-type') || '';
120
121 if (contentType.includes('application/json')) {
122 // Non-streaming: parse the full JSON response
123 response.clone().json().then(function(data) {
124 if (self && data && data.testing_data) {
125 self.handleTestingData(data.testing_data);
126 } else if (self && data && data.data && data.data.testing_data) {
127 self.handleTestingData(data.data.testing_data);
128 }
129 }).catch(function() {});
130 } else if (contentType.includes('text/event-stream') || contentType.includes('text/plain')) {
131 // Streaming (SSE): read just enough to find the testing_data event
132 // Using clone() so the original stream is unaffected for chat-script.js
133 try {
134 var clonedResponse = response.clone();
135 var reader = clonedResponse.body.getReader();
136 var decoder = new TextDecoder();
137 var sseBuffer = '';
138 var found = false;
139
140 function readChunk() {
141 reader.read().then(function(result) {
142 if (result.done || found) {
143 reader.cancel().catch(function() {});
144 return;
145 }
146
147 sseBuffer += decoder.decode(result.value, { stream: true });
148 var sseLines = sseBuffer.split('\n');
149
150 for (var i = 0; i < sseLines.length; i++) {
151 var sseLine = sseLines[i];
152 if (sseLine.indexOf('data: ') === 0) {
153 var payload = sseLine.substring(6);
154 try {
155 var parsed = JSON.parse(payload);
156 if (parsed && parsed.testing_data) {
157 self.handleTestingData(parsed.testing_data);
158 found = true;
159 reader.cancel().catch(function() {});
160 return;
161 }
162 } catch (e) {}
163 }
164 }
165
166 // Keep reading until we find testing_data or hit a limit
167 if (sseBuffer.length < 50000) {
168 readChunk();
169 } else {
170 reader.cancel().catch(function() {});
171 }
172 }).catch(function() {
173 // Stream read error — safe to ignore
174 });
175 }
176
177 readChunk();
178 } catch (e) {}
179 }
180 }
181 return response;
182 });
183 };
184
185 this.log('Chat interception active');
186 },
187
188 // =====================================================
189 // Handle incoming testing data
190 // =====================================================
191
192 handleTestingData: function(testingData) {
193 this.log('Chat data captured');
194
195 // Update query
196 this.updateLastQuery(testingData.query || 'No query');
197
198 // Update approved URLs
199 this.updateApprovedUrls(testingData.approved_urls || []);
200
201 // Update document matches
202 this.updateTopMatches(
203 testingData.top_matches || [],
204 testingData.similarity_threshold || 0.75,
205 testingData.sources_used || 0,
206 testingData.total_chunks_used || 0
207 );
208
209 // Update action matches
210 this.updateActionMatches(testingData.action_matches || []);
211
212 // Log summary
213 if (testingData.knowledge_base_type) {
214 this.log('Knowledge Base: ' + testingData.knowledge_base_type);
215 }
216 if (testingData.similarity_threshold) {
217 this.log('Similarity Threshold: ' + (testingData.similarity_threshold * 100) + '%');
218 }
219 if (testingData.top_matches && testingData.top_matches.length > 0) {
220 var aboveThreshold = testingData.top_matches.filter(function(m) { return m.above_threshold; }).length;
221 this.log(aboveThreshold + ' above threshold, ' + (testingData.top_matches.length - aboveThreshold) + ' below');
222 this.log('Highest similarity: ' + testingData.top_matches[0].similarity_percentage + '%');
223 } else {
224 this.log('No document matches found');
225 }
226 if (testingData.action_matches && testingData.action_matches.length > 0) {
227 var triggered = testingData.action_matches.find(function(a) { return a.triggered; });
228 if (triggered) {
229 this.log('Action Triggered: ' + triggered.intent_label + ' (' + triggered.similarity_percentage + '%)');
230 } else {
231 this.log('No actions triggered - Highest: ' + testingData.action_matches[0].intent_label + ' (' + testingData.action_matches[0].similarity_percentage + '%)');
232 }
233 }
234 },
235
236 // =====================================================
237 // Update UI sections
238 // =====================================================
239
240 updateLastQuery: function(query) {
241 $('#mxch-testing-last-query').text(query);
242 this.lastQueryData = { query: query, timestamp: new Date() };
243 },
244
245 updateApprovedUrls: function(approvedUrls) {
246 var el = $('#mxch-testing-approved-urls');
247
248 if (!approvedUrls || !Array.isArray(approvedUrls) || approvedUrls.length === 0) {
249 el.html('<div class="mxch-testing-no-data">No approved URLs (AI cannot cite links)</div>');
250 return;
251 }
252
253 var html = '<div class="urls-header"><strong>' + approvedUrls.length + ' URL' + (approvedUrls.length !== 1 ? 's' : '') + ' approved for AI citations</strong></div>';
254
255 approvedUrls.forEach(function(url) {
256 var displayUrl = url;
257 try {
258 var urlObj = new URL(url);
259 displayUrl = urlObj.hostname + urlObj.pathname;
260 } catch (e) {}
261
262 html += '<div class="url-card"><div class="url-line">' +
263 '<span class="url-icon">&#128279;</span>' +
264 '<a href="' + url + '" target="_blank" rel="noopener noreferrer" class="url-link" title="' + url + '">' + displayUrl + '</a>' +
265 '</div></div>';
266 });
267
268 html += '<div class="urls-note">The AI can only cite these URLs. Others are automatically removed from responses.</div>';
269 el.html(html);
270 },
271
272 updateActionMatches: function(actionMatches) {
273 var el = $('#mxch-testing-action-scores');
274
275 if (!actionMatches || actionMatches.length === 0) {
276 el.html('<div class="mxch-testing-no-data">No actions checked</div>');
277 return;
278 }
279
280 var html = '<div class="actions-header"><strong>Top ' + actionMatches.length + ' actions checked</strong></div>';
281
282 actionMatches.forEach(function(action) {
283 var isTriggered = action.triggered;
284 var isAboveThreshold = action.above_threshold;
285 var statusIcon = isTriggered ? '&#127919;' : (isAboveThreshold ? '&#9888;&#65039;' : '&#10060;');
286
287 var statusLabel;
288 if (isTriggered) {
289 statusLabel = 'TRIGGERED';
290 } else if (isAboveThreshold) {
291 statusLabel = 'Above threshold';
292 } else {
293 statusLabel = 'Below threshold';
294 }
295
296 var cardClass = isTriggered ? 'action-triggered' : (isAboveThreshold ? 'action-above-threshold' : 'action-below-threshold');
297
298 html += '<div class="action-card ' + cardClass + '">' +
299 '<div class="action-line">' +
300 '<span class="action-icon">' + statusIcon + '</span>' +
301 '<span class="action-name">' + action.intent_label + '</span>' +
302 '<span class="action-score">' + action.similarity_percentage + '%</span>' +
303 '</div>' +
304 '<div class="action-details">' +
305 '<span class="action-status">' + statusLabel + '</span>' +
306 '<span class="action-threshold">Threshold: ' + action.threshold_percentage + '%</span>' +
307 '</div>' +
308 '</div>';
309 });
310
311 el.html(html);
312 },
313
314 updateTopMatches: function(topMatches, threshold, sourcesUsed, totalChunksUsed) {
315 var el = $('#mxch-testing-similarity-scores');
316
317 if (!topMatches || topMatches.length === 0) {
318 el.html('<div class="mxch-testing-no-data">No similarity data available</div>');
319 return;
320 }
321
322 // Group matches by source URL
323 var groupedByUrl = {};
324 topMatches.forEach(function(match) {
325 var url = match.source_display || 'Unknown';
326 if (!groupedByUrl[url]) {
327 groupedByUrl[url] = {
328 url: url,
329 isUrl: url.indexOf('http') === 0,
330 bestScore: 0,
331 usedForContext: false,
332 totalChunks: match.total_chunks || 1,
333 matchedChunks: [],
334 isChunked: match.is_chunk || false
335 };
336 }
337
338 if (match.similarity_percentage > groupedByUrl[url].bestScore) {
339 groupedByUrl[url].bestScore = match.similarity_percentage;
340 }
341
342 if (match.used_for_context) {
343 groupedByUrl[url].usedForContext = true;
344 }
345
346 groupedByUrl[url].matchedChunks.push({
347 chunkIndex: match.chunk_index,
348 score: match.similarity_percentage,
349 usedForContext: match.used_for_context,
350 aboveThreshold: match.above_threshold
351 });
352 });
353
354 var urlGroups = Object.values(groupedByUrl).sort(function(a, b) { return b.bestScore - a.bestScore; });
355 var usedUrlCount = sourcesUsed > 0 ? sourcesUsed : urlGroups.filter(function(g) { return g.usedForContext; }).length;
356 var chunksInfo = totalChunksUsed > 0 ? totalChunksUsed + ' chunks sent to AI' : topMatches.length + ' chunk matches';
357
358 var html = '<div class="matches-header">' +
359 '<strong>' + usedUrlCount + ' source' + (usedUrlCount === 1 ? '' : 's') + ' used for AI context</strong>' +
360 '<span class="matches-subheader">(' + chunksInfo + ')</span>' +
361 '</div>';
362
363 var self = this;
364
365 urlGroups.forEach(function(group, groupIndex) {
366 var cardClass = group.usedForContext ? 'above-threshold' : 'below-threshold';
367 var statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
368 var contextLabel = group.usedForContext ? 'Used for AI context' : 'Not used';
369
370 var chunkSummary = '';
371 if (group.isChunked && group.totalChunks > 1) {
372 var usedChunkCount = group.matchedChunks.filter(function(c) { return c.usedForContext; }).length;
373 chunkSummary = '<span class="chunk-summary">' + usedChunkCount + '/' + group.totalChunks + ' chunks matched</span>';
374 }
375
376 var hasMultipleChunks = group.matchedChunks.length > 1;
377 var expandToggle = hasMultipleChunks
378 ? '<span class="chunk-expand-toggle" data-group="' + groupIndex + '">&#9654; Show chunks</span>'
379 : '';
380
381 html += '<div class="match-card ' + cardClass + '">' +
382 '<div class="match-header">' +
383 '<div class="match-title">' +
384 '<span class="status-icon">' + statusIcon + '</span>' +
385 '<span class="similarity-score">' + group.bestScore + '%</span>' +
386 chunkSummary +
387 '</div>' +
388 '<span class="context-label">' + contextLabel + '</span>' +
389 '</div>' +
390 '<div class="match-source">' +
391 (group.isUrl ?
392 '<span class="source-icon link-icon">&#128279;</span> ' + group.url :
393 '<span class="source-icon doc-icon">&#128196;</span> ' + group.url
394 ) +
395 '</div>' +
396 expandToggle +
397 (hasMultipleChunks ? self.renderChunkDetails(group.matchedChunks, groupIndex) : '') +
398 '</div>';
399 });
400
401 el.html(html);
402
403 // Bind expand toggles
404 el.find('.chunk-expand-toggle').on('click', function() {
405 var groupId = $(this).data('group');
406 var details = el.find('.chunk-details[data-group="' + groupId + '"]');
407 var isExpanded = details.toggleClass('expanded').hasClass('expanded');
408 $(this).html(isExpanded ? '&#9660; Hide chunks' : '&#9654; Show chunks');
409 });
410 },
411
412 renderChunkDetails: function(chunks, groupIndex) {
413 var sortedChunks = chunks.slice().sort(function(a, b) { return (a.chunkIndex || 0) - (b.chunkIndex || 0); });
414
415 var html = '<div class="chunk-details" data-group="' + groupIndex + '">';
416
417 sortedChunks.forEach(function(chunk) {
418 var chunkNum = (chunk.chunkIndex !== null && chunk.chunkIndex !== undefined) ? chunk.chunkIndex + 1 : '?';
419 var statusClass = chunk.usedForContext ? 'chunk-used' : 'chunk-not-used';
420 var statusIcon = chunk.usedForContext ? '&#10003;' : '&#9675;';
421
422 html += '<div class="chunk-detail-row ' + statusClass + '">' +
423 '<span class="chunk-detail-icon">' + statusIcon + '</span>' +
424 '<span class="chunk-detail-num">Chunk ' + chunkNum + '</span>' +
425 '<span class="chunk-detail-score">' + chunk.score + '%</span>' +
426 '</div>';
427 });
428
429 html += '</div>';
430 return html;
431 },
432
433 // =====================================================
434 // System info loading
435 // =====================================================
436
437 loadSystemInfo: function() {
438 this.updateSimilarityThreshold();
439 this.updateSystemPrompt();
440 this.updateKnowledgeBaseStatus();
441 },
442
443 updateSimilarityThreshold: function() {
444 $.post(mxchatAdminTestData.ajaxUrl, {
445 action: 'mxchat_get_similarity_threshold',
446 nonce: mxchatAdminTestData.nonce
447 }).done(function(data) {
448 if (data.success) {
449 $('#mxch-testing-threshold').html('<code>' + data.data.threshold_percentage + '</code>');
450 } else {
451 $('#mxch-testing-threshold').html('<span class="error-text">Error loading threshold</span>');
452 }
453 }).fail(function() {
454 $('#mxch-testing-threshold').html('<span class="error-text">Connection error</span>');
455 });
456 },
457
458 updateSystemPrompt: function() {
459 var self = this;
460 var el = $('#mxch-testing-system-prompt');
461 el.text('Loading system prompt...');
462
463 $.post(mxchatAdminTestData.ajaxUrl, {
464 action: 'mxchat_get_system_info',
465 nonce: mxchatAdminTestData.nonce
466 }).done(function(data) {
467 if (data.success) {
468 el.text(data.data.system_prompt || 'No system prompt configured');
469
470 if (data.data.is_openrouter) {
471 self.log('Model: OpenRouter - ' + data.data.openrouter_model);
472 } else {
473 self.log('Model: ' + data.data.selected_model);
474 }
475
476 var apiStatus = data.data.api_status;
477 var configuredApis = Object.keys(apiStatus).filter(function(key) { return apiStatus[key]; });
478 if (configuredApis.length > 0) {
479 self.log('Configured APIs: ' + configuredApis.join(', '));
480 }
481 } else {
482 el.text('Error loading system prompt');
483 }
484 }).fail(function() {
485 el.text('Connection error');
486 });
487 },
488
489 updateKnowledgeBaseStatus: function() {
490 var el = $('#mxch-testing-kb-status');
491 el.html('Checking...');
492
493 $.post(mxchatAdminTestData.ajaxUrl, {
494 action: 'mxchat_get_kb_status',
495 nonce: mxchatAdminTestData.nonce
496 }).done(function(data) {
497 if (data.success) {
498 var kbData = data.data;
499 el.html('<span class="success-text">&#10003; ' + kbData.status + '</span> (' + kbData.type + ' - ' + kbData.documents + ')');
500 } else {
501 el.html('<span class="error-text">Error loading KB status</span>');
502 }
503 }).fail(function() {
504 el.html('<span class="error-text">Connection error</span>');
505 });
506 },
507
508 // =====================================================
509 // Clear session
510 // =====================================================
511
512 clearChatSession: function() {
513 var self = this;
514
515 // Determine the bot ID for the testing chatbot
516 var botId = 'testing';
517
518 // Get current session ID
519 var cookieName = 'mxchat_session_id_' + botId;
520 var sessionId = this.getCookie(cookieName) || this.getCurrentSessionId(botId);
521
522 if (!sessionId) {
523 this.log('No active session found');
524 return;
525 }
526
527 this.log('Clearing session: ' + sessionId);
528
529 // Use MxChatInstances to properly reset
530 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.resetChatSession === 'function') {
531 MxChatInstances.resetChatSession(botId);
532 }
533
534 // Get new session ID
535 var newSessionId = '';
536 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
537 newSessionId = MxChatInstances.getChatSession(botId);
538 }
539
540 if (!newSessionId) {
541 newSessionId = 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
542 document.cookie = cookieName + '=' + newSessionId + '; path=/; max-age=86400; SameSite=Lax';
543 }
544
545 // Call backend to clear old session
546 $.post(mxchatAdminTestData.ajaxUrl, {
547 action: 'mxchat_start_fresh_session',
548 nonce: mxchatAdminTestData.nonce,
549 old_session_id: sessionId,
550 new_session_id: newSessionId
551 }).done(function(data) {
552 if (data.success) {
553 self.log('Session cleared successfully');
554
555 // Clear the chat UI for the testing bot
556 var chatBox = document.getElementById('chat-box-testing');
557 if (chatBox) {
558 var messages = chatBox.querySelectorAll('.bot-message, .user-message');
559 messages.forEach(function(msg, index) {
560 // Keep first welcome message
561 if (index === 0 && msg.classList.contains('bot-message')) return;
562 msg.remove();
563 });
564 }
565
566 // Clear input
567 var chatInput = document.getElementById('chat-input-testing');
568 if (chatInput) {
569 chatInput.value = '';
570 }
571
572 // Show popular questions again
573 var pq = document.getElementById('mxchat-popular-questions-testing');
574 if (pq) {
575 pq.style.display = 'block';
576 }
577
578 // Clear debug displays
579 self.updateLastQuery('New session started');
580 self.updateTopMatches([], 0, 0, 0);
581 self.updateApprovedUrls([]);
582 self.updateActionMatches([]);
583
584 self.log('Fresh session started: ' + newSessionId);
585 } else {
586 self.log('Error: ' + (data.data && data.data.message ? data.data.message : 'Unknown error'));
587 }
588 }).fail(function() {
589 self.log('Connection error when clearing session');
590 });
591 },
592
593 // =====================================================
594 // Debug console
595 // =====================================================
596
597 log: function(message) {
598 var consoleEl = document.getElementById('mxch-testing-debug-console');
599 if (!consoleEl) return;
600
601 var timestamp = new Date().toLocaleTimeString();
602 var entry = document.createElement('div');
603 entry.className = 'mxch-testing-debug-entry';
604 entry.innerHTML = '<span class="mxch-testing-debug-timestamp">[' + timestamp + ']</span> ' + message;
605 consoleEl.appendChild(entry);
606 consoleEl.scrollTop = consoleEl.scrollHeight;
607
608 // Keep only last 50 entries
609 var entries = consoleEl.querySelectorAll('.mxch-testing-debug-entry');
610 if (entries.length > 50) {
611 entries[0].remove();
612 }
613 },
614
615 clearDebugConsole: function() {
616 var consoleEl = document.getElementById('mxch-testing-debug-console');
617 if (consoleEl) {
618 consoleEl.innerHTML = '<div class="mxch-testing-debug-entry">Debug console cleared...</div>';
619 }
620 },
621
622 // =====================================================
623 // Helpers
624 // =====================================================
625
626 getCookie: function(name) {
627 var value = '; ' + document.cookie;
628 var parts = value.split('; ' + name + '=');
629 if (parts.length === 2) return parts.pop().split(';').shift();
630 return '';
631 },
632
633 getCurrentSessionId: function(botId) {
634 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
635 var session = MxChatInstances.getChatSession(botId);
636 if (session) return session;
637 }
638
639 var cookieId = this.getCookie('mxchat_session_id_' + botId);
640 if (cookieId) return cookieId;
641
642 var chatInput = document.getElementById('chat-input-' + botId);
643 if (chatInput && chatInput.dataset.sessionId) {
644 return chatInput.dataset.sessionId;
645 }
646
647 return '';
648 }
649 };
650
651 // Initialize when DOM is ready
652 $(document).ready(function() {
653 // Only init if we're on the settings page and the testing section exists
654 if (document.getElementById('testing')) {
655 AdminTestingPanel.init();
656 }
657 });
658
659 })(jQuery);
660