PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.0
MxChat – AI Chatbot & Content Generation for WordPress v3.2.0
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.0, at js/admin-testing-tab.js

609 lines 25.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.mxchatAdminTestPanel = 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 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 if (contentType && contentType.includes('application/json')) {
121 response.clone().json().then(function(data) {
122 if (self && data && data.testing_data) {
123 self.handleTestingData(data.testing_data);
124 } else if (self && data && data.data && data.data.testing_data) {
125 self.handleTestingData(data.data.testing_data);
126 }
127 }).catch(function() {});
128 }
129 }
130 return response;
131 });
132 };
133
134 this.log('Chat interception active');
135 },
136
137 // =====================================================
138 // Handle incoming testing data
139 // =====================================================
140
141 handleTestingData: function(testingData) {
142 this.log('Chat data captured');
143
144 // Update query
145 this.updateLastQuery(testingData.query || 'No query');
146
147 // Update approved URLs
148 this.updateApprovedUrls(testingData.approved_urls || []);
149
150 // Update document matches
151 this.updateTopMatches(
152 testingData.top_matches || [],
153 testingData.similarity_threshold || 0.75,
154 testingData.sources_used || 0,
155 testingData.total_chunks_used || 0
156 );
157
158 // Update action matches
159 this.updateActionMatches(testingData.action_matches || []);
160
161 // Log summary
162 if (testingData.knowledge_base_type) {
163 this.log('Knowledge Base: ' + testingData.knowledge_base_type);
164 }
165 if (testingData.similarity_threshold) {
166 this.log('Similarity Threshold: ' + (testingData.similarity_threshold * 100) + '%');
167 }
168 if (testingData.top_matches && testingData.top_matches.length > 0) {
169 var aboveThreshold = testingData.top_matches.filter(function(m) { return m.above_threshold; }).length;
170 this.log(aboveThreshold + ' above threshold, ' + (testingData.top_matches.length - aboveThreshold) + ' below');
171 this.log('Highest similarity: ' + testingData.top_matches[0].similarity_percentage + '%');
172 } else {
173 this.log('No document matches found');
174 }
175 if (testingData.action_matches && testingData.action_matches.length > 0) {
176 var triggered = testingData.action_matches.find(function(a) { return a.triggered; });
177 if (triggered) {
178 this.log('Action Triggered: ' + triggered.intent_label + ' (' + triggered.similarity_percentage + '%)');
179 } else {
180 this.log('No actions triggered - Highest: ' + testingData.action_matches[0].intent_label + ' (' + testingData.action_matches[0].similarity_percentage + '%)');
181 }
182 }
183 },
184
185 // =====================================================
186 // Update UI sections
187 // =====================================================
188
189 updateLastQuery: function(query) {
190 $('#mxch-testing-last-query').text(query);
191 this.lastQueryData = { query: query, timestamp: new Date() };
192 },
193
194 updateApprovedUrls: function(approvedUrls) {
195 var el = $('#mxch-testing-approved-urls');
196
197 if (!approvedUrls || !Array.isArray(approvedUrls) || approvedUrls.length === 0) {
198 el.html('<div class="mxch-testing-no-data">No approved URLs (AI cannot cite links)</div>');
199 return;
200 }
201
202 var html = '<div class="urls-header"><strong>' + approvedUrls.length + ' URL' + (approvedUrls.length !== 1 ? 's' : '') + ' approved for AI citations</strong></div>';
203
204 approvedUrls.forEach(function(url) {
205 var displayUrl = url;
206 try {
207 var urlObj = new URL(url);
208 displayUrl = urlObj.hostname + urlObj.pathname;
209 } catch (e) {}
210
211 html += '<div class="url-card"><div class="url-line">' +
212 '<span class="url-icon">&#128279;</span>' +
213 '<a href="' + url + '" target="_blank" rel="noopener noreferrer" class="url-link" title="' + url + '">' + displayUrl + '</a>' +
214 '</div></div>';
215 });
216
217 html += '<div class="urls-note">The AI can only cite these URLs. Others are automatically removed from responses.</div>';
218 el.html(html);
219 },
220
221 updateActionMatches: function(actionMatches) {
222 var el = $('#mxch-testing-action-scores');
223
224 if (!actionMatches || actionMatches.length === 0) {
225 el.html('<div class="mxch-testing-no-data">No actions checked</div>');
226 return;
227 }
228
229 var html = '<div class="actions-header"><strong>Top ' + actionMatches.length + ' actions checked</strong></div>';
230
231 actionMatches.forEach(function(action) {
232 var isTriggered = action.triggered;
233 var isAboveThreshold = action.above_threshold;
234 var statusIcon = isTriggered ? '&#127919;' : (isAboveThreshold ? '&#9888;&#65039;' : '&#10060;');
235
236 var statusLabel;
237 if (isTriggered) {
238 statusLabel = 'TRIGGERED';
239 } else if (isAboveThreshold) {
240 statusLabel = 'Above threshold';
241 } else {
242 statusLabel = 'Below threshold';
243 }
244
245 var cardClass = isTriggered ? 'action-triggered' : (isAboveThreshold ? 'action-above-threshold' : 'action-below-threshold');
246
247 html += '<div class="action-card ' + cardClass + '">' +
248 '<div class="action-line">' +
249 '<span class="action-icon">' + statusIcon + '</span>' +
250 '<span class="action-name">' + action.intent_label + '</span>' +
251 '<span class="action-score">' + action.similarity_percentage + '%</span>' +
252 '</div>' +
253 '<div class="action-details">' +
254 '<span class="action-status">' + statusLabel + '</span>' +
255 '<span class="action-threshold">Threshold: ' + action.threshold_percentage + '%</span>' +
256 '</div>' +
257 '</div>';
258 });
259
260 el.html(html);
261 },
262
263 updateTopMatches: function(topMatches, threshold, sourcesUsed, totalChunksUsed) {
264 var el = $('#mxch-testing-similarity-scores');
265
266 if (!topMatches || topMatches.length === 0) {
267 el.html('<div class="mxch-testing-no-data">No similarity data available</div>');
268 return;
269 }
270
271 // Group matches by source URL
272 var groupedByUrl = {};
273 topMatches.forEach(function(match) {
274 var url = match.source_display || 'Unknown';
275 if (!groupedByUrl[url]) {
276 groupedByUrl[url] = {
277 url: url,
278 isUrl: url.indexOf('http') === 0,
279 bestScore: 0,
280 usedForContext: false,
281 totalChunks: match.total_chunks || 1,
282 matchedChunks: [],
283 isChunked: match.is_chunk || false
284 };
285 }
286
287 if (match.similarity_percentage > groupedByUrl[url].bestScore) {
288 groupedByUrl[url].bestScore = match.similarity_percentage;
289 }
290
291 if (match.used_for_context) {
292 groupedByUrl[url].usedForContext = true;
293 }
294
295 groupedByUrl[url].matchedChunks.push({
296 chunkIndex: match.chunk_index,
297 score: match.similarity_percentage,
298 usedForContext: match.used_for_context,
299 aboveThreshold: match.above_threshold
300 });
301 });
302
303 var urlGroups = Object.values(groupedByUrl).sort(function(a, b) { return b.bestScore - a.bestScore; });
304 var usedUrlCount = sourcesUsed > 0 ? sourcesUsed : urlGroups.filter(function(g) { return g.usedForContext; }).length;
305 var chunksInfo = totalChunksUsed > 0 ? totalChunksUsed + ' chunks sent to AI' : topMatches.length + ' chunk matches';
306
307 var html = '<div class="matches-header">' +
308 '<strong>' + usedUrlCount + ' source' + (usedUrlCount === 1 ? '' : 's') + ' used for AI context</strong>' +
309 '<span class="matches-subheader">(' + chunksInfo + ')</span>' +
310 '</div>';
311
312 var self = this;
313
314 urlGroups.forEach(function(group, groupIndex) {
315 var cardClass = group.usedForContext ? 'above-threshold' : 'below-threshold';
316 var statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
317 var contextLabel = group.usedForContext ? 'Used for AI context' : 'Not used';
318
319 var chunkSummary = '';
320 if (group.isChunked && group.totalChunks > 1) {
321 var usedChunkCount = group.matchedChunks.filter(function(c) { return c.usedForContext; }).length;
322 chunkSummary = '<span class="chunk-summary">' + usedChunkCount + '/' + group.totalChunks + ' chunks matched</span>';
323 }
324
325 var hasMultipleChunks = group.matchedChunks.length > 1;
326 var expandToggle = hasMultipleChunks
327 ? '<span class="chunk-expand-toggle" data-group="' + groupIndex + '">&#9654; Show chunks</span>'
328 : '';
329
330 html += '<div class="match-card ' + cardClass + '">' +
331 '<div class="match-header">' +
332 '<div class="match-title">' +
333 '<span class="status-icon">' + statusIcon + '</span>' +
334 '<span class="similarity-score">' + group.bestScore + '%</span>' +
335 chunkSummary +
336 '</div>' +
337 '<span class="context-label">' + contextLabel + '</span>' +
338 '</div>' +
339 '<div class="match-source">' +
340 (group.isUrl ?
341 '<span class="source-icon link-icon">&#128279;</span> ' + group.url :
342 '<span class="source-icon doc-icon">&#128196;</span> ' + group.url
343 ) +
344 '</div>' +
345 expandToggle +
346 (hasMultipleChunks ? self.renderChunkDetails(group.matchedChunks, groupIndex) : '') +
347 '</div>';
348 });
349
350 el.html(html);
351
352 // Bind expand toggles
353 el.find('.chunk-expand-toggle').on('click', function() {
354 var groupId = $(this).data('group');
355 var details = el.find('.chunk-details[data-group="' + groupId + '"]');
356 var isExpanded = details.toggleClass('expanded').hasClass('expanded');
357 $(this).html(isExpanded ? '&#9660; Hide chunks' : '&#9654; Show chunks');
358 });
359 },
360
361 renderChunkDetails: function(chunks, groupIndex) {
362 var sortedChunks = chunks.slice().sort(function(a, b) { return (a.chunkIndex || 0) - (b.chunkIndex || 0); });
363
364 var html = '<div class="chunk-details" data-group="' + groupIndex + '">';
365
366 sortedChunks.forEach(function(chunk) {
367 var chunkNum = (chunk.chunkIndex !== null && chunk.chunkIndex !== undefined) ? chunk.chunkIndex + 1 : '?';
368 var statusClass = chunk.usedForContext ? 'chunk-used' : 'chunk-not-used';
369 var statusIcon = chunk.usedForContext ? '&#10003;' : '&#9675;';
370
371 html += '<div class="chunk-detail-row ' + statusClass + '">' +
372 '<span class="chunk-detail-icon">' + statusIcon + '</span>' +
373 '<span class="chunk-detail-num">Chunk ' + chunkNum + '</span>' +
374 '<span class="chunk-detail-score">' + chunk.score + '%</span>' +
375 '</div>';
376 });
377
378 html += '</div>';
379 return html;
380 },
381
382 // =====================================================
383 // System info loading
384 // =====================================================
385
386 loadSystemInfo: function() {
387 this.updateSimilarityThreshold();
388 this.updateSystemPrompt();
389 this.updateKnowledgeBaseStatus();
390 },
391
392 updateSimilarityThreshold: function() {
393 $.post(mxchatAdminTestData.ajaxUrl, {
394 action: 'mxchat_get_similarity_threshold',
395 nonce: mxchatAdminTestData.nonce
396 }).done(function(data) {
397 if (data.success) {
398 $('#mxch-testing-threshold').html('<code>' + data.data.threshold_percentage + '</code>');
399 } else {
400 $('#mxch-testing-threshold').html('<span class="error-text">Error loading threshold</span>');
401 }
402 }).fail(function() {
403 $('#mxch-testing-threshold').html('<span class="error-text">Connection error</span>');
404 });
405 },
406
407 updateSystemPrompt: function() {
408 var self = this;
409 var el = $('#mxch-testing-system-prompt');
410 el.text('Loading system prompt...');
411
412 $.post(mxchatAdminTestData.ajaxUrl, {
413 action: 'mxchat_get_system_info',
414 nonce: mxchatAdminTestData.nonce
415 }).done(function(data) {
416 if (data.success) {
417 el.text(data.data.system_prompt || 'No system prompt configured');
418
419 if (data.data.is_openrouter) {
420 self.log('Model: OpenRouter - ' + data.data.openrouter_model);
421 } else {
422 self.log('Model: ' + data.data.selected_model);
423 }
424
425 var apiStatus = data.data.api_status;
426 var configuredApis = Object.keys(apiStatus).filter(function(key) { return apiStatus[key]; });
427 if (configuredApis.length > 0) {
428 self.log('Configured APIs: ' + configuredApis.join(', '));
429 }
430 } else {
431 el.text('Error loading system prompt');
432 }
433 }).fail(function() {
434 el.text('Connection error');
435 });
436 },
437
438 updateKnowledgeBaseStatus: function() {
439 var el = $('#mxch-testing-kb-status');
440 el.html('Checking...');
441
442 $.post(mxchatAdminTestData.ajaxUrl, {
443 action: 'mxchat_get_kb_status',
444 nonce: mxchatAdminTestData.nonce
445 }).done(function(data) {
446 if (data.success) {
447 var kbData = data.data;
448 el.html('<span class="success-text">&#10003; ' + kbData.status + '</span> (' + kbData.type + ' - ' + kbData.documents + ')');
449 } else {
450 el.html('<span class="error-text">Error loading KB status</span>');
451 }
452 }).fail(function() {
453 el.html('<span class="error-text">Connection error</span>');
454 });
455 },
456
457 // =====================================================
458 // Clear session
459 // =====================================================
460
461 clearChatSession: function() {
462 var self = this;
463
464 // Determine the bot ID for the testing chatbot
465 var botId = 'testing';
466
467 // Get current session ID
468 var cookieName = 'mxchat_session_id_' + botId;
469 var sessionId = this.getCookie(cookieName) || this.getCurrentSessionId(botId);
470
471 if (!sessionId) {
472 this.log('No active session found');
473 return;
474 }
475
476 this.log('Clearing session: ' + sessionId);
477
478 // Use MxChatInstances to properly reset
479 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.resetChatSession === 'function') {
480 MxChatInstances.resetChatSession(botId);
481 }
482
483 // Get new session ID
484 var newSessionId = '';
485 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
486 newSessionId = MxChatInstances.getChatSession(botId);
487 }
488
489 if (!newSessionId) {
490 newSessionId = 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
491 document.cookie = cookieName + '=' + newSessionId + '; path=/; max-age=86400; SameSite=Lax';
492 }
493
494 // Call backend to clear old session
495 $.post(mxchatAdminTestData.ajaxUrl, {
496 action: 'mxchat_start_fresh_session',
497 nonce: mxchatAdminTestData.nonce,
498 old_session_id: sessionId,
499 new_session_id: newSessionId
500 }).done(function(data) {
501 if (data.success) {
502 self.log('Session cleared successfully');
503
504 // Clear the chat UI for the testing bot
505 var chatBox = document.getElementById('chat-box-testing');
506 if (chatBox) {
507 var messages = chatBox.querySelectorAll('.bot-message, .user-message');
508 messages.forEach(function(msg, index) {
509 // Keep first welcome message
510 if (index === 0 && msg.classList.contains('bot-message')) return;
511 msg.remove();
512 });
513 }
514
515 // Clear input
516 var chatInput = document.getElementById('chat-input-testing');
517 if (chatInput) {
518 chatInput.value = '';
519 }
520
521 // Show popular questions again
522 var pq = document.getElementById('mxchat-popular-questions-testing');
523 if (pq) {
524 pq.style.display = 'block';
525 }
526
527 // Clear debug displays
528 self.updateLastQuery('New session started');
529 self.updateTopMatches([], 0, 0, 0);
530 self.updateApprovedUrls([]);
531 self.updateActionMatches([]);
532
533 self.log('Fresh session started: ' + newSessionId);
534 } else {
535 self.log('Error: ' + (data.data && data.data.message ? data.data.message : 'Unknown error'));
536 }
537 }).fail(function() {
538 self.log('Connection error when clearing session');
539 });
540 },
541
542 // =====================================================
543 // Debug console
544 // =====================================================
545
546 log: function(message) {
547 var consoleEl = document.getElementById('mxch-testing-debug-console');
548 if (!consoleEl) return;
549
550 var timestamp = new Date().toLocaleTimeString();
551 var entry = document.createElement('div');
552 entry.className = 'mxch-testing-debug-entry';
553 entry.innerHTML = '<span class="mxch-testing-debug-timestamp">[' + timestamp + ']</span> ' + message;
554 consoleEl.appendChild(entry);
555 consoleEl.scrollTop = consoleEl.scrollHeight;
556
557 // Keep only last 50 entries
558 var entries = consoleEl.querySelectorAll('.mxch-testing-debug-entry');
559 if (entries.length > 50) {
560 entries[0].remove();
561 }
562 },
563
564 clearDebugConsole: function() {
565 var consoleEl = document.getElementById('mxch-testing-debug-console');
566 if (consoleEl) {
567 consoleEl.innerHTML = '<div class="mxch-testing-debug-entry">Debug console cleared...</div>';
568 }
569 },
570
571 // =====================================================
572 // Helpers
573 // =====================================================
574
575 getCookie: function(name) {
576 var value = '; ' + document.cookie;
577 var parts = value.split('; ' + name + '=');
578 if (parts.length === 2) return parts.pop().split(';').shift();
579 return '';
580 },
581
582 getCurrentSessionId: function(botId) {
583 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
584 var session = MxChatInstances.getChatSession(botId);
585 if (session) return session;
586 }
587
588 var cookieId = this.getCookie('mxchat_session_id_' + botId);
589 if (cookieId) return cookieId;
590
591 var chatInput = document.getElementById('chat-input-' + botId);
592 if (chatInput && chatInput.dataset.sessionId) {
593 return chatInput.dataset.sessionId;
594 }
595
596 return '';
597 }
598 };
599
600 // Initialize when DOM is ready
601 $(document).ready(function() {
602 // Only init if we're on the settings page and the testing section exists
603 if (document.getElementById('testing')) {
604 AdminTestingPanel.init();
605 }
606 });
607
608 })(jQuery);
609