PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.1
MxChat – AI Chatbot & Content Generation for WordPress v3.2.1
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 / test-panel.js

test-panel.js in MxChat – AI Chatbot & Content Generation for WordPress 3.2.1, at js/test-panel.js

909 lines 36.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * MxChat Test Panel JavaScript
3 * Handles the testing interface for admins - always active when panel is open
4 */
5
6 class MxChatTestPanel {
7 constructor() {
8 this.panel = null;
9 this.tab = null;
10 this.isOpen = false;
11 this.lastQueryData = null;
12
13 this.init();
14 }
15
16 init() {
17 // Wait for DOM to be ready
18 if (document.readyState === 'loading') {
19 document.addEventListener('DOMContentLoaded', () => this.setup());
20 } else {
21 this.setup();
22 }
23 }
24
25 setup() {
26 this.createElements();
27 this.bindEvents();
28 this.setupChatInterception();
29 }
30
31 createElements() {
32 // Create the test tab
33 this.tab = document.createElement('div');
34 this.tab.className = 'mxchat-test-tab';
35 this.tab.innerHTML = 'MXCHAT DEBUGGING';
36 this.tab.title = 'Open MxChat Debug Panel';
37 document.body.appendChild(this.tab);
38
39 // Create the test panel
40 this.panel = document.createElement('div');
41 this.panel.className = 'mxchat-test-panel';
42 this.panel.innerHTML = this.getPanelHTML();
43 document.body.appendChild(this.panel);
44 }
45
46 getPanelHTML() {
47 return `
48 <div class="mxchat-test-header">
49 <h3>MxChat Debug Panel</h3>
50 <p>Always-on debugging for administrators</p>
51 <button class="mxchat-test-close" title="Close panel">&times;</button>
52 </div>
53
54 <div class="mxchat-test-content">
55 <!-- Session Management -->
56 <div class="mxchat-test-section">
57 <h4>Quick Actions</h4>
58 <button class="mxchat-test-btn danger" id="clear-chat-session">
59 Clear Chat Session
60 </button>
61 </div>
62
63 <!-- Query Analysis -->
64 <div class="mxchat-test-section">
65 <h4>Last Query Analysis</h4>
66 <div class="mxchat-test-info">
67 <strong>Similarity Threshold:</strong>
68 <span id="similarity-threshold">Loading...</span>
69 </div>
70 <div class="mxchat-test-info">
71 <strong>User Query:</strong>
72 <div id="last-query" class="query-display">Waiting for next query...</div>
73 </div>
74 <div class="mxchat-test-info">
75 <strong>Approved URLs for Citations:</strong>
76 <div class="mxchat-test-results approved-urls-container" id="approved-urls">
77 <div class="no-data-message">No URL data yet</div>
78 </div>
79 </div>
80 <div class="mxchat-test-info">
81 <strong>Document Matches:</strong>
82 <div class="mxchat-test-results similarity-container" id="similarity-scores">
83 <div class="no-data-message">No query data yet</div>
84 </div>
85 </div>
86 <div class="mxchat-test-info">
87 <strong>Actions Triggered:</strong>
88 <div class="mxchat-test-results actions-container" id="action-scores">
89 <div class="no-data-message">No action data yet</div>
90 </div>
91 </div>
92 </div>
93
94 <!-- System Information -->
95 <div class="mxchat-test-section">
96 <h4>System Information</h4>
97 <div class="mxchat-test-info">
98 <strong>System Prompt:</strong>
99 <div class="mxchat-test-results system-prompt-container" id="system-prompt">Loading...</div>
100 </div>
101 <div class="mxchat-test-info">
102 <strong>Knowledge Base:</strong>
103 <span id="kb-status">Checking...</span>
104 </div>
105 </div>
106
107 <!-- Debug Console -->
108 <div class="mxchat-test-section">
109 <h4>Debug Log</h4>
110 <div class="mxchat-test-results debug-console-container" id="debug-console">
111 <div class="debug-entry">Debug panel ready - monitoring chat activity...</div>
112 </div>
113 <button class="mxchat-test-btn secondary" id="clear-debug">
114 Clear Log
115 </button>
116 </div>
117 </div>
118 `;
119 }
120
121
122 bindEvents() {
123 // Tab click to toggle panel
124 this.tab.addEventListener('click', () => this.togglePanel());
125
126 // Close button
127 const closeBtn = this.panel.querySelector('.mxchat-test-close');
128 closeBtn.addEventListener('click', () => this.closePanel());
129
130 // Action buttons
131 this.bindActionButtons();
132
133 // Keep escape key to close (useful shortcut)
134 document.addEventListener('keydown', (e) => {
135 if (e.key === 'Escape' && this.isOpen) {
136 this.closePanel();
137 }
138 });
139 }
140
141 bindActionButtons() {
142 // Clear chat session
143 this.panel.querySelector('#clear-chat-session').addEventListener('click', () => {
144 this.clearChatSession();
145 });
146
147 // Clear debug console
148 this.panel.querySelector('#clear-debug').addEventListener('click', () => {
149 this.clearDebugConsole();
150 });
151 }
152
153 togglePanel() {
154 if (this.isOpen) {
155 this.closePanel();
156 } else {
157 this.openPanel();
158 }
159 }
160
161 openPanel() {
162 this.panel.classList.add('open');
163 this.isOpen = true;
164 this.tab.style.display = 'none';
165 this.loadSystemInfo();
166 this.log('Debug panel opened - capturing chat data automatically');
167 }
168
169 closePanel() {
170 this.panel.classList.remove('open');
171 this.isOpen = false;
172 this.tab.style.display = 'block';
173 }
174
175 setupChatInterception() {
176 // Set up interception for chat responses to capture testing data
177 this.interceptChatResponses();
178 this.log('Chat monitoring initialized');
179 }
180
181 interceptChatResponses() {
182 // Store reference to the test panel instance
183 window.mxchatTestPanelInstance = this;
184
185 // Intercept jQuery AJAX calls (for regular chat)
186 if (window.jQuery) {
187 const originalAjax = jQuery.ajax;
188
189 jQuery.ajax = function(options) {
190 const originalSuccess = options.success;
191
192 options.success = function(data, textStatus, jqXHR) {
193
194 // Check if this is a chat request
195 if (options.data &&
196 (options.data.action === 'mxchat_handle_chat_request' ||
197 options.data.action === 'mxchat_stream_chat')) {
198
199 const testPanel = window.mxchatTestPanelInstance;
200
201 // Always try to handle testing data if it exists (no toggle check)
202 if (testPanel && data && data.testing_data) {
203 testPanel.handleTestingData(data.testing_data);
204 } else if (testPanel && data && data.data && data.data.testing_data) {
205 // Check if data is nested
206 testPanel.handleTestingData(data.data.testing_data);
207 }
208 }
209
210 // Call original success handler
211 if (originalSuccess) {
212 originalSuccess.call(this, data, textStatus, jqXHR);
213 }
214 };
215
216 return originalAjax.call(this, options);
217 };
218 }
219
220 // Also intercept fetch API calls (for streaming)
221 const originalFetch = window.fetch;
222
223 window.fetch = (...args) => {
224 return originalFetch(...args).then(response => {
225 // Check if this is a chat request
226 if (args[0].includes('admin-ajax.php') || args[0].includes('mxchat')) {
227 // For streaming responses that return JSON instead of streams
228 const contentType = response.headers.get('content-type');
229 if (contentType && contentType.includes('application/json')) {
230 response.clone().json().then(data => {
231 const testPanel = window.mxchatTestPanelInstance;
232
233 // Always try to handle testing data if it exists (no toggle check)
234 if (testPanel && data && data.testing_data) {
235 testPanel.handleTestingData(data.testing_data);
236 } else if (testPanel && data && data.data && data.data.testing_data) {
237 // Check nested data
238 testPanel.handleTestingData(data.data.testing_data);
239 }
240 }).catch(() => {
241 // Ignore JSON parsing errors
242 });
243 }
244 }
245 return response;
246 });
247 };
248
249 this.log('Chat interception active for jQuery and fetch requests');
250 }
251
252 handleTestingData(testingData) {
253 this.log('📊 Chat data captured from response');
254
255 // Update query analysis section
256 this.updateLastQuery(testingData.query || 'No query', testingData.top_matches || []);
257
258 // NEW: Update approved URLs
259 this.updateApprovedUrls(testingData.approved_urls || []);
260
261 this.updateTopMatches(testingData.top_matches || [], testingData.similarity_threshold || 0.75, testingData.sources_used || 0, testingData.total_chunks_used || 0);
262
263 // NEW: Update action matches
264 this.updateActionMatches(testingData.action_matches || []);
265
266 // Log additional info
267 if (testingData.knowledge_base_type) {
268 this.log(`📚 Knowledge Base: ${testingData.knowledge_base_type}`);
269 }
270 if (testingData.similarity_threshold) {
271 this.log(`🎯 Similarity Threshold: ${(testingData.similarity_threshold * 100)}%`);
272 }
273
274 // NEW: Log approved URLs count
275 if (testingData.approved_urls && testingData.approved_urls.length > 0) {
276 this.log(`🔗 Approved URLs for citations: ${testingData.approved_urls.length}`);
277 }
278
279 // Show summary in debug console
280 if (testingData.top_matches && testingData.top_matches.length > 0) {
281 const aboveThreshold = testingData.top_matches.filter(match => match.above_threshold).length;
282 const belowThreshold = testingData.top_matches.length - aboveThreshold;
283 const highestScore = testingData.top_matches[0].similarity_percentage;
284
285 this.log(`�
286 Analysis: ${aboveThreshold} above threshold, ${belowThreshold} below threshold`);
287 this.log(`🏆 Highest similarity: ${highestScore}%`);
288 } else {
289 this.log('⚠️ No document matches found');
290 }
291
292 // NEW: Log action summary
293 if (testingData.action_matches && testingData.action_matches.length > 0) {
294 const triggeredAction = testingData.action_matches.find(action => action.triggered);
295 if (triggeredAction) {
296 this.log(`🎯 Action Triggered: ${triggeredAction.intent_label} (${triggeredAction.similarity_percentage}%)`);
297 } else {
298 const highestAction = testingData.action_matches[0];
299 this.log(`🚫 No actions triggered - Highest: ${highestAction.intent_label} (${highestAction.similarity_percentage}%)`);
300 }
301 } else {
302 this.log('📝 No actions checked');
303 }
304 }
305
306 updateApprovedUrls(approvedUrls) {
307 const urlsEl = this.panel.querySelector('#approved-urls');
308
309 // Safety check: ensure approvedUrls is an array
310 if (!approvedUrls || !Array.isArray(approvedUrls) || approvedUrls.length === 0) {
311 urlsEl.innerHTML = '<div class="no-data-message">No approved URLs (AI cannot cite links)</div>';
312 return;
313 }
314
315 let html = `<div class="urls-header">
316 <strong>${approvedUrls.length} URL${approvedUrls.length !== 1 ? 's' : ''} approved for AI citations</strong>
317 </div>`;
318
319 approvedUrls.forEach((url, index) => {
320 // Extract domain for display
321 let displayUrl = url;
322 try {
323 const urlObj = new URL(url);
324 displayUrl = urlObj.hostname + urlObj.pathname;
325 } catch (e) {
326 // Keep original if URL parsing fails
327 }
328
329 html += `
330 <div class="url-card">
331 <div class="url-line">
332 <span class="url-icon">🔗</span>
333 <a href="${url}" target="_blank" rel="noopener noreferrer" class="url-link" title="${url}">
334 ${displayUrl}
335 </a>
336 </div>
337 </div>
338 `;
339 });
340
341 html += `<div class="urls-note">
342 The AI can only cite these URLs. Any other URLs will be automatically removed from responses.
343 </div>`;
344
345 urlsEl.innerHTML = html;
346 }
347
348 updateActionMatches(actionMatches) {
349 const actionsEl = this.panel.querySelector('#action-scores');
350
351 if (!actionMatches || actionMatches.length === 0) {
352 actionsEl.innerHTML = '<div class="no-data-message">No actions checked</div>';
353 return;
354 }
355
356 let html = `<div class="actions-header">
357 <strong>Top ${actionMatches.length} actions checked</strong>
358 </div>`;
359
360 actionMatches.forEach((action, index) => {
361 const isTriggered = action.triggered;
362 const isAboveThreshold = action.above_threshold;
363 const statusIcon = isTriggered ? '🎯' : (isAboveThreshold ? '⚠️' : '❌');
364
365 // Determine the correct label based on status
366 let statusLabel;
367 if (isTriggered) {
368 statusLabel = 'TRIGGERED';
369 } else if (isAboveThreshold) {
370 statusLabel = 'Above threshold';
371 } else {
372 statusLabel = 'Below threshold';
373 }
374
375 // Determine card styling
376 const cardClass = isTriggered ? 'action-triggered' : (isAboveThreshold ? 'action-above-threshold' : 'action-below-threshold');
377
378 html += `
379 <div class="action-card ${cardClass}">
380 <div class="action-line">
381 <span class="action-icon">${statusIcon}</span>
382 <span class="action-name">${action.intent_label}</span>
383 <span class="action-score">${action.similarity_percentage}%</span>
384 </div>
385 <div class="action-details">
386 <span class="action-status">${statusLabel}</span>
387 <span class="action-threshold">Threshold: ${action.threshold_percentage}%</span>
388 </div>
389 </div>
390 `;
391 });
392
393 actionsEl.innerHTML = html;
394 }
395
396 updateTopMatches(topMatches, threshold, sourcesUsed = 0, totalChunksUsed = 0) {
397 const scoresEl = this.panel.querySelector('#similarity-scores');
398
399 if (!topMatches || topMatches.length === 0) {
400 scoresEl.innerHTML = '<div class="no-data-message">No similarity data available</div>';
401 return;
402 }
403
404 // Group matches by source URL
405 const groupedByUrl = {};
406 topMatches.forEach((match) => {
407 const url = match.source_display || 'Unknown';
408 if (!groupedByUrl[url]) {
409 groupedByUrl[url] = {
410 url: url,
411 isUrl: url.startsWith('http'),
412 bestScore: 0,
413 usedForContext: false,
414 totalChunks: match.total_chunks || 1,
415 matchedChunks: [],
416 isChunked: match.is_chunk || false
417 };
418 }
419
420 // Track best score
421 if (match.similarity_percentage > groupedByUrl[url].bestScore) {
422 groupedByUrl[url].bestScore = match.similarity_percentage;
423 }
424
425 // Track if any chunk was used for context
426 if (match.used_for_context) {
427 groupedByUrl[url].usedForContext = true;
428 }
429
430 // Add chunk info
431 groupedByUrl[url].matchedChunks.push({
432 chunkIndex: match.chunk_index,
433 score: match.similarity_percentage,
434 usedForContext: match.used_for_context,
435 aboveThreshold: match.above_threshold
436 });
437 });
438
439 // Convert to array and sort by best score
440 const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore);
441
442 // Use backend counts if available, otherwise fall back to frontend calculation
443 const usedUrlCount = sourcesUsed > 0 ? sourcesUsed : urlGroups.filter(g => g.usedForContext).length;
444 const chunksInfo = totalChunksUsed > 0 ? `${totalChunksUsed} chunks sent to AI` : `${topMatches.length} chunk matches`;
445
446 let html = `<div class="matches-header">
447 <strong>${usedUrlCount} source${usedUrlCount === 1 ? '' : 's'} used for AI context</strong>
448 <span class="matches-subheader">(${chunksInfo})</span>
449 </div>`;
450
451 urlGroups.forEach((group, groupIndex) => {
452 const cardClass = group.usedForContext ? 'above-threshold' : 'below-threshold';
453 const statusIcon = group.usedForContext ? '✓' : '✗';
454 const contextLabel = group.usedForContext ? 'Used for AI context' : 'Not used';
455
456 // Build chunk summary
457 let chunkSummary = '';
458 if (group.isChunked && group.totalChunks > 1) {
459 const usedChunkCount = group.matchedChunks.filter(c => c.usedForContext).length;
460 chunkSummary = `<span class="chunk-summary">${usedChunkCount}/${group.totalChunks} chunks matched</span>`;
461 }
462
463 // Check if this entry has multiple matched chunks to show expand toggle
464 const hasMultipleChunks = group.matchedChunks.length > 1;
465 const expandToggle = hasMultipleChunks
466 ? `<span class="chunk-expand-toggle" data-group="${groupIndex}">Show chunks</span>`
467 : '';
468
469 html += `
470 <div class="match-card ${cardClass}">
471 <div class="match-header">
472 <div class="match-title">
473 <span class="status-icon">${statusIcon}</span>
474 <span class="similarity-score">${group.bestScore}%</span>
475 ${chunkSummary}
476 </div>
477 <span class="context-label">${contextLabel}</span>
478 </div>
479 <div class="match-source">
480 ${group.isUrl ?
481 `<span class="source-icon link-icon">🔗</span> ${group.url}` :
482 `<span class="source-icon doc-icon">📄</span> ${group.url}`
483 }
484 </div>
485 ${expandToggle}
486 ${hasMultipleChunks ? this.renderChunkDetails(group.matchedChunks, groupIndex) : ''}
487 </div>
488 `;
489 });
490
491 scoresEl.innerHTML = html;
492
493 // Add click handlers for expand toggles
494 scoresEl.querySelectorAll('.chunk-expand-toggle').forEach(toggle => {
495 toggle.addEventListener('click', (e) => {
496 const groupId = e.target.dataset.group;
497 const details = scoresEl.querySelector(`.chunk-details[data-group="${groupId}"]`);
498 if (details) {
499 const isExpanded = details.classList.toggle('expanded');
500 e.target.textContent = isExpanded ? '▼ Hide chunks' : '▶ Show chunks';
501 }
502 });
503 });
504 }
505
506 renderChunkDetails(chunks, groupIndex) {
507 // Sort chunks by chunk index
508 const sortedChunks = [...chunks].sort((a, b) => (a.chunkIndex || 0) - (b.chunkIndex || 0));
509
510 let html = `<div class="chunk-details" data-group="${groupIndex}">`;
511
512 sortedChunks.forEach(chunk => {
513 const chunkNum = (chunk.chunkIndex !== null && chunk.chunkIndex !== undefined)
514 ? chunk.chunkIndex + 1
515 : '?';
516 const statusClass = chunk.usedForContext ? 'chunk-used' : 'chunk-not-used';
517 const statusIcon = chunk.usedForContext ? '✓' : '○';
518
519 html += `
520 <div class="chunk-detail-row ${statusClass}">
521 <span class="chunk-detail-icon">${statusIcon}</span>
522 <span class="chunk-detail-num">Chunk ${chunkNum}</span>
523 <span class="chunk-detail-score">${chunk.score}%</span>
524 </div>
525 `;
526 });
527
528 html += '</div>';
529 return html;
530 }
531
532 updateLastQuery(query, topMatches) {
533 const queryEl = this.panel.querySelector('#last-query');
534 queryEl.textContent = query;
535
536 this.lastQueryData = {
537 query,
538 topMatches,
539 timestamp: new Date()
540 };
541 }
542
543 clearChatSession() {
544 // Determine the active bot ID from MxChatInstances
545 let botId = 'default';
546 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getAllBotIds === 'function') {
547 const botIds = MxChatInstances.getAllBotIds();
548 if (botIds.length > 0) {
549 botId = botIds[0];
550 }
551 }
552
553 // Get current session ID using the correct bot-suffixed cookie name
554 const cookieName = 'mxchat_session_id_' + botId;
555 const sessionId = this.getCookie(cookieName) || this.getCookie('mxchat_session_id') || this.getCurrentSessionId();
556
557 if (!sessionId) {
558 this.log('No active session found');
559 return;
560 }
561
562 this.log('Current session ID: ' + sessionId);
563 this.log('Starting fresh session...');
564
565 // Use MxChatInstances.resetChatSession to properly reset in-memory state + cookie
566 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.resetChatSession === 'function') {
567 MxChatInstances.resetChatSession(botId);
568 }
569
570 // Get the new session ID that was just set by resetChatSession
571 let newSessionId = '';
572 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
573 newSessionId = MxChatInstances.getChatSession(botId);
574 }
575
576 // Fallback if MxChatInstances wasn't available
577 if (!newSessionId) {
578 newSessionId = 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
579 this.clearMxChatCookie(botId);
580 this.setChatSession(newSessionId, botId);
581 }
582
583 this.log('New session ID: ' + newSessionId);
584
585 // Call backend to clear old session data
586 fetch(mxchatTestData.ajaxUrl, {
587 method: 'POST',
588 headers: {
589 'Content-Type': 'application/x-www-form-urlencoded',
590 },
591 body: new URLSearchParams({
592 action: 'mxchat_start_fresh_session',
593 nonce: mxchatTestData.nonce,
594 old_session_id: sessionId,
595 new_session_id: newSessionId
596 })
597 })
598 .then(response => response.json())
599 .then(data => {
600 if (data.success) {
601 this.log('Backend session cleared: ' + data.data.message);
602
603 // Update the session ID everywhere in the DOM
604 this.updateSessionIdEverywhere(newSessionId);
605
606 // Clear the chat UI
607 this.clearChatUI();
608
609 // Show popular questions again
610 const popularQuestions = document.querySelector('#mxchat-popular-questions');
611 if (popularQuestions) {
612 popularQuestions.style.display = 'block';
613 }
614
615 // Clear testing data displays
616 this.updateLastQuery('New session started', []);
617 this.updateTopMatches([], 0);
618 this.updateApprovedUrls([]);
619
620 this.log('Fresh session started successfully');
621
622 } else {
623 this.log('Error clearing session: ' + (data.data?.message || 'Unknown error'));
624 }
625 })
626 .catch(error => {
627 console.error('Error clearing chat session:', error);
628 this.log('Connection error when clearing session');
629 });
630 }
631
632 // Helper function to get cookie (same as your existing one)
633 getCookie(name) {
634 let value = "; " + document.cookie;
635 let parts = value.split("; " + name + "=");
636 if (parts.length == 2) return parts.pop().split(";").shift();
637 }
638
639 // Helper function to set session cookie (matches chat-script.js format)
640 setChatSession(sessionId, botId) {
641 botId = botId || 'default';
642 document.cookie = 'mxchat_session_id_' + botId + '=' + sessionId + '; path=/; max-age=86400; SameSite=Lax';
643 }
644
645 // Helper function to clear the MxChat session cookie
646 clearMxChatCookie(botId) {
647 botId = botId || 'default';
648 // Clear both bot-suffixed and legacy cookie formats
649 document.cookie = 'mxchat_session_id_' + botId + '=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
650 document.cookie = 'mxchat_session_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
651 this.log('Session cookie cleared');
652 }
653
654 updateSessionIdEverywhere(newSessionId) {
655 // Update global session ID variable if it exists
656 if (window.mxchatSessionId) {
657 window.mxchatSessionId = newSessionId;
658 }
659
660 // Update session ID in chat input data attribute
661 const chatInput = document.querySelector('#chat-input');
662 if (chatInput) {
663 chatInput.dataset.sessionId = newSessionId;
664 }
665
666 // Update any hidden session ID fields
667 const sessionInputs = document.querySelectorAll('input[name="session_id"]');
668 sessionInputs.forEach(input => {
669 input.value = newSessionId;
670 });
671
672 // Update any data attributes that store session ID
673 const elementsWithSessionId = document.querySelectorAll('[data-session-id]');
674 elementsWithSessionId.forEach(element => {
675 element.dataset.sessionId = newSessionId;
676 });
677
678 // Update URL parameter if it exists
679 if (window.location.search.includes('session_id=')) {
680 const url = new URL(window.location);
681 url.searchParams.set('session_id', newSessionId);
682 window.history.replaceState({}, '', url);
683 }
684
685 this.log('🔄 Session ID updated everywhere in DOM');
686 }
687
688 clearChatUI() {
689 const chatBox = document.querySelector('#chat-box');
690 if (chatBox) {
691 // Remove all messages (both user and bot)
692 const allMessages = chatBox.querySelectorAll('.bot-message, .user-message');
693 allMessages.forEach(msg => {
694 // Keep the first bot message if it's a welcome message
695 if (msg === chatBox.querySelector('.bot-message') &&
696 msg.textContent.toLowerCase().includes('welcome')) {
697 return; // Keep welcome message
698 }
699 msg.remove();
700 });
701 this.log('🧹 Chat UI cleared');
702 }
703
704 // Clear chat input
705 const chatInput = document.querySelector('#chat-input');
706 if (chatInput) {
707 chatInput.value = '';
708 }
709 }
710
711 getCurrentSessionId() {
712 // Try MxChatInstances first (most reliable — matches chat-script.js)
713 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
714 const botIds = typeof MxChatInstances.getAllBotIds === 'function' ? MxChatInstances.getAllBotIds() : ['default'];
715 const botId = botIds.length > 0 ? botIds[0] : 'default';
716 const instanceSession = MxChatInstances.getChatSession(botId);
717 if (instanceSession) {
718 return instanceSession;
719 }
720 }
721
722 // Try bot-suffixed cookie, then legacy cookie
723 const botCookieId = this.getCookie('mxchat_session_id_default');
724 if (botCookieId) {
725 return botCookieId;
726 }
727 const cookieSessionId = this.getCookie('mxchat_session_id');
728 if (cookieSessionId) {
729 return cookieSessionId;
730 }
731
732 // Try to get session ID from various DOM sources
733 const chatInput = document.querySelector('#chat-input');
734 if (chatInput && chatInput.dataset.sessionId) {
735 return chatInput.dataset.sessionId;
736 }
737
738 // Try to get from URL parameters
739 const urlParams = new URLSearchParams(window.location.search);
740 const sessionFromUrl = urlParams.get('session_id');
741 if (sessionFromUrl) {
742 return sessionFromUrl;
743 }
744
745 // Try to get from global variables
746 if (window.chatSessionId) {
747 return window.chatSessionId;
748 }
749
750 // Generate a temporary session ID if none found
751 return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
752 }
753
754 loadSystemInfo() {
755 // Load system information from backend
756 this.updateSimilarityThreshold();
757 this.updateSystemPrompt();
758 this.updateKnowledgeBaseStatus();
759 }
760
761 updateSimilarityThreshold() {
762 fetch(mxchatTestData.ajaxUrl, {
763 method: 'POST',
764 headers: {
765 'Content-Type': 'application/x-www-form-urlencoded',
766 },
767 body: new URLSearchParams({
768 action: 'mxchat_get_similarity_threshold',
769 nonce: mxchatTestData.nonce
770 })
771 })
772 .then(response => response.json())
773 .then(data => {
774 const thresholdEl = this.panel.querySelector('#similarity-threshold');
775 if (data.success) {
776 thresholdEl.innerHTML = `<code>${data.data.threshold_percentage}</code>`;
777 } else {
778 thresholdEl.innerHTML = '<span class="error-text">Error loading threshold</span>';
779 }
780 })
781 .catch(error => {
782 console.error('Error fetching similarity threshold:', error);
783 const thresholdEl = this.panel.querySelector('#similarity-threshold');
784 thresholdEl.innerHTML = '<span class="error-text">Connection error</span>';
785 });
786 }
787
788 updateSystemPrompt() {
789 const promptEl = this.panel.querySelector('#system-prompt');
790 promptEl.textContent = 'Loading system prompt...';
791
792 fetch(mxchatTestData.ajaxUrl, {
793 method: 'POST',
794 headers: {
795 'Content-Type': 'application/x-www-form-urlencoded',
796 },
797 body: new URLSearchParams({
798 action: 'mxchat_get_system_info',
799 nonce: mxchatTestData.nonce
800 })
801 })
802 .then(response => response.json())
803 .then(data => {
804 if (data.success) {
805 promptEl.textContent = data.data.system_prompt || 'No system prompt configured';
806
807 // Enhanced model display with OpenRouter support
808 if (data.data.is_openrouter) {
809 this.log(`🤖 Model: OpenRouter`);
810 this.log(` └─ Using: ${data.data.openrouter_model}`);
811 } else {
812 this.log(`🤖 Model: ${data.data.selected_model}`);
813 }
814
815 // Enhanced API status with OpenRouter
816 const apiStatus = data.data.api_status;
817 const configuredApis = Object.keys(apiStatus).filter(key => apiStatus[key]);
818
819 if (configuredApis.length > 0) {
820 this.log(`🔑 Configured APIs: ${configuredApis.map(api => {
821 // Capitalize and format API names
822 if (api === 'openai') return 'OpenAI';
823 if (api === 'xai') return 'X.AI';
824 if (api === 'openrouter') return 'OpenRouter';
825 return api.charAt(0).toUpperCase() + api.slice(1);
826 }).join(', ')}`);
827 } else {
828 this.log(`⚠️ No API keys configured`);
829 }
830
831 // Specific warning for OpenRouter if selected but no key
832 if (data.data.is_openrouter && !apiStatus.openrouter) {
833 this.log(`WARNING: OpenRouter selected but no API key configured!`);
834 }
835 } else {
836 promptEl.textContent = 'Error loading system prompt';
837 }
838 })
839 .catch(error => {
840 console.error('Error fetching system info:', error);
841 promptEl.textContent = 'Connection error';
842 });
843 }
844
845 updateKnowledgeBaseStatus() {
846 const statusEl = this.panel.querySelector('#kb-status');
847 statusEl.innerHTML = 'Checking...';
848
849 fetch(mxchatTestData.ajaxUrl, {
850 method: 'POST',
851 headers: {
852 'Content-Type': 'application/x-www-form-urlencoded',
853 },
854 body: new URLSearchParams({
855 action: 'mxchat_get_kb_status',
856 nonce: mxchatTestData.nonce
857 })
858 })
859 .then(response => response.json())
860 .then(data => {
861 if (data.success) {
862 const kbData = data.data;
863 statusEl.innerHTML = `<span class="success-text">${kbData.status}</span> (${kbData.type} - ${kbData.documents})`;
864 } else {
865 statusEl.innerHTML = '<span class="error-text">Error loading KB status</span>';
866 }
867 })
868 .catch(error => {
869 console.error('Error fetching KB status:', error);
870 statusEl.innerHTML = '<span class="error-text">Connection error</span>';
871 });
872 }
873
874 clearDebugConsole() {
875 const console = this.panel.querySelector('#debug-console');
876 console.innerHTML = '<div class="debug-entry">Debug console cleared...</div>';
877 }
878
879 log(message) {
880 const console = this.panel.querySelector('#debug-console');
881 const timestamp = new Date().toLocaleTimeString();
882 const logEntry = document.createElement('div');
883 logEntry.className = 'debug-entry';
884 logEntry.innerHTML = `<span class="debug-timestamp">[${timestamp}]</span> ${message}`;
885 console.appendChild(logEntry);
886 console.scrollTop = console.scrollHeight;
887
888 // Keep only last 50 entries to prevent memory issues
889 const entries = console.querySelectorAll('.debug-entry');
890 if (entries.length > 50) {
891 entries[0].remove();
892 }
893 }
894 }
895
896 // Initialize the test panel when the script loads
897 document.addEventListener('DOMContentLoaded', function() {
898 // Only initialize if user is admin and testing is enabled
899 if (window.mxchatTestingEnabled) {
900 window.mxchatTestPanel = new MxChatTestPanel();
901 }
902 });
903
904 // Global function to enable testing mode programmatically
905 window.enableMxChatTesting = function() {
906 if (!window.mxchatTestPanel) {
907 window.mxchatTestPanel = new MxChatTestPanel();
908 }
909 };