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

908 lines 35.8 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);
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) {
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 // Count unique URLs used for context
443 const usedUrlCount = urlGroups.filter(g => g.usedForContext).length;
444
445 let html = `<div class="matches-header">
446 <strong>${usedUrlCount} entr${usedUrlCount === 1 ? 'y' : 'ies'} used for AI context</strong>
447 <span class="matches-subheader">(from ${topMatches.length} chunk matches)</span>
448 </div>`;
449
450 urlGroups.forEach((group, groupIndex) => {
451 const cardClass = group.usedForContext ? 'above-threshold' : 'below-threshold';
452 const statusIcon = group.usedForContext ? '✓' : '✗';
453 const contextLabel = group.usedForContext ? 'Used for AI context' : 'Not used';
454
455 // Build chunk summary
456 let chunkSummary = '';
457 if (group.isChunked && group.totalChunks > 1) {
458 const usedChunkCount = group.matchedChunks.filter(c => c.usedForContext).length;
459 chunkSummary = `<span class="chunk-summary">${usedChunkCount}/${group.totalChunks} chunks matched</span>`;
460 }
461
462 // Check if this entry has multiple matched chunks to show expand toggle
463 const hasMultipleChunks = group.matchedChunks.length > 1;
464 const expandToggle = hasMultipleChunks
465 ? `<span class="chunk-expand-toggle" data-group="${groupIndex}">Show chunks</span>`
466 : '';
467
468 html += `
469 <div class="match-card ${cardClass}">
470 <div class="match-header">
471 <div class="match-title">
472 <span class="status-icon">${statusIcon}</span>
473 <span class="similarity-score">${group.bestScore}%</span>
474 ${chunkSummary}
475 </div>
476 <span class="context-label">${contextLabel}</span>
477 </div>
478 <div class="match-source">
479 ${group.isUrl ?
480 `<span class="source-icon link-icon">🔗</span> ${group.url}` :
481 `<span class="source-icon doc-icon">📄</span> ${group.url}`
482 }
483 </div>
484 ${expandToggle}
485 ${hasMultipleChunks ? this.renderChunkDetails(group.matchedChunks, groupIndex) : ''}
486 </div>
487 `;
488 });
489
490 scoresEl.innerHTML = html;
491
492 // Add click handlers for expand toggles
493 scoresEl.querySelectorAll('.chunk-expand-toggle').forEach(toggle => {
494 toggle.addEventListener('click', (e) => {
495 const groupId = e.target.dataset.group;
496 const details = scoresEl.querySelector(`.chunk-details[data-group="${groupId}"]`);
497 if (details) {
498 const isExpanded = details.classList.toggle('expanded');
499 e.target.textContent = isExpanded ? '▼ Hide chunks' : '▶ Show chunks';
500 }
501 });
502 });
503 }
504
505 renderChunkDetails(chunks, groupIndex) {
506 // Sort chunks by chunk index
507 const sortedChunks = [...chunks].sort((a, b) => (a.chunkIndex || 0) - (b.chunkIndex || 0));
508
509 let html = `<div class="chunk-details" data-group="${groupIndex}">`;
510
511 sortedChunks.forEach(chunk => {
512 const chunkNum = (chunk.chunkIndex !== null && chunk.chunkIndex !== undefined)
513 ? chunk.chunkIndex + 1
514 : '?';
515 const statusClass = chunk.usedForContext ? 'chunk-used' : 'chunk-not-used';
516 const statusIcon = chunk.usedForContext ? '✓' : '○';
517
518 html += `
519 <div class="chunk-detail-row ${statusClass}">
520 <span class="chunk-detail-icon">${statusIcon}</span>
521 <span class="chunk-detail-num">Chunk ${chunkNum}</span>
522 <span class="chunk-detail-score">${chunk.score}%</span>
523 </div>
524 `;
525 });
526
527 html += '</div>';
528 return html;
529 }
530
531 updateLastQuery(query, topMatches) {
532 const queryEl = this.panel.querySelector('#last-query');
533 queryEl.textContent = query;
534
535 this.lastQueryData = {
536 query,
537 topMatches,
538 timestamp: new Date()
539 };
540 }
541
542 clearChatSession() {
543 // Determine the active bot ID from MxChatInstances
544 let botId = 'default';
545 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getAllBotIds === 'function') {
546 const botIds = MxChatInstances.getAllBotIds();
547 if (botIds.length > 0) {
548 botId = botIds[0];
549 }
550 }
551
552 // Get current session ID using the correct bot-suffixed cookie name
553 const cookieName = 'mxchat_session_id_' + botId;
554 const sessionId = this.getCookie(cookieName) || this.getCookie('mxchat_session_id') || this.getCurrentSessionId();
555
556 if (!sessionId) {
557 this.log('No active session found');
558 return;
559 }
560
561 this.log('Current session ID: ' + sessionId);
562 this.log('Starting fresh session...');
563
564 // Use MxChatInstances.resetChatSession to properly reset in-memory state + cookie
565 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.resetChatSession === 'function') {
566 MxChatInstances.resetChatSession(botId);
567 }
568
569 // Get the new session ID that was just set by resetChatSession
570 let newSessionId = '';
571 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
572 newSessionId = MxChatInstances.getChatSession(botId);
573 }
574
575 // Fallback if MxChatInstances wasn't available
576 if (!newSessionId) {
577 newSessionId = 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
578 this.clearMxChatCookie(botId);
579 this.setChatSession(newSessionId, botId);
580 }
581
582 this.log('New session ID: ' + newSessionId);
583
584 // Call backend to clear old session data
585 fetch(mxchatTestData.ajaxUrl, {
586 method: 'POST',
587 headers: {
588 'Content-Type': 'application/x-www-form-urlencoded',
589 },
590 body: new URLSearchParams({
591 action: 'mxchat_start_fresh_session',
592 nonce: mxchatTestData.nonce,
593 old_session_id: sessionId,
594 new_session_id: newSessionId
595 })
596 })
597 .then(response => response.json())
598 .then(data => {
599 if (data.success) {
600 this.log('Backend session cleared: ' + data.data.message);
601
602 // Update the session ID everywhere in the DOM
603 this.updateSessionIdEverywhere(newSessionId);
604
605 // Clear the chat UI
606 this.clearChatUI();
607
608 // Show popular questions again
609 const popularQuestions = document.querySelector('#mxchat-popular-questions');
610 if (popularQuestions) {
611 popularQuestions.style.display = 'block';
612 }
613
614 // Clear testing data displays
615 this.updateLastQuery('New session started', []);
616 this.updateTopMatches([], 0);
617 this.updateApprovedUrls([]);
618
619 this.log('Fresh session started successfully');
620
621 } else {
622 this.log('Error clearing session: ' + (data.data?.message || 'Unknown error'));
623 }
624 })
625 .catch(error => {
626 console.error('Error clearing chat session:', error);
627 this.log('Connection error when clearing session');
628 });
629 }
630
631 // Helper function to get cookie (same as your existing one)
632 getCookie(name) {
633 let value = "; " + document.cookie;
634 let parts = value.split("; " + name + "=");
635 if (parts.length == 2) return parts.pop().split(";").shift();
636 }
637
638 // Helper function to set session cookie (matches chat-script.js format)
639 setChatSession(sessionId, botId) {
640 botId = botId || 'default';
641 document.cookie = 'mxchat_session_id_' + botId + '=' + sessionId + '; path=/; max-age=86400; SameSite=Lax';
642 }
643
644 // Helper function to clear the MxChat session cookie
645 clearMxChatCookie(botId) {
646 botId = botId || 'default';
647 // Clear both bot-suffixed and legacy cookie formats
648 document.cookie = 'mxchat_session_id_' + botId + '=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
649 document.cookie = 'mxchat_session_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
650 this.log('Session cookie cleared');
651 }
652
653 updateSessionIdEverywhere(newSessionId) {
654 // Update global session ID variable if it exists
655 if (window.mxchatSessionId) {
656 window.mxchatSessionId = newSessionId;
657 }
658
659 // Update session ID in chat input data attribute
660 const chatInput = document.querySelector('#chat-input');
661 if (chatInput) {
662 chatInput.dataset.sessionId = newSessionId;
663 }
664
665 // Update any hidden session ID fields
666 const sessionInputs = document.querySelectorAll('input[name="session_id"]');
667 sessionInputs.forEach(input => {
668 input.value = newSessionId;
669 });
670
671 // Update any data attributes that store session ID
672 const elementsWithSessionId = document.querySelectorAll('[data-session-id]');
673 elementsWithSessionId.forEach(element => {
674 element.dataset.sessionId = newSessionId;
675 });
676
677 // Update URL parameter if it exists
678 if (window.location.search.includes('session_id=')) {
679 const url = new URL(window.location);
680 url.searchParams.set('session_id', newSessionId);
681 window.history.replaceState({}, '', url);
682 }
683
684 this.log('🔄 Session ID updated everywhere in DOM');
685 }
686
687 clearChatUI() {
688 const chatBox = document.querySelector('#chat-box');
689 if (chatBox) {
690 // Remove all messages (both user and bot)
691 const allMessages = chatBox.querySelectorAll('.bot-message, .user-message');
692 allMessages.forEach(msg => {
693 // Keep the first bot message if it's a welcome message
694 if (msg === chatBox.querySelector('.bot-message') &&
695 msg.textContent.toLowerCase().includes('welcome')) {
696 return; // Keep welcome message
697 }
698 msg.remove();
699 });
700 this.log('🧹 Chat UI cleared');
701 }
702
703 // Clear chat input
704 const chatInput = document.querySelector('#chat-input');
705 if (chatInput) {
706 chatInput.value = '';
707 }
708 }
709
710 getCurrentSessionId() {
711 // Try MxChatInstances first (most reliable — matches chat-script.js)
712 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
713 const botIds = typeof MxChatInstances.getAllBotIds === 'function' ? MxChatInstances.getAllBotIds() : ['default'];
714 const botId = botIds.length > 0 ? botIds[0] : 'default';
715 const instanceSession = MxChatInstances.getChatSession(botId);
716 if (instanceSession) {
717 return instanceSession;
718 }
719 }
720
721 // Try bot-suffixed cookie, then legacy cookie
722 const botCookieId = this.getCookie('mxchat_session_id_default');
723 if (botCookieId) {
724 return botCookieId;
725 }
726 const cookieSessionId = this.getCookie('mxchat_session_id');
727 if (cookieSessionId) {
728 return cookieSessionId;
729 }
730
731 // Try to get session ID from various DOM sources
732 const chatInput = document.querySelector('#chat-input');
733 if (chatInput && chatInput.dataset.sessionId) {
734 return chatInput.dataset.sessionId;
735 }
736
737 // Try to get from URL parameters
738 const urlParams = new URLSearchParams(window.location.search);
739 const sessionFromUrl = urlParams.get('session_id');
740 if (sessionFromUrl) {
741 return sessionFromUrl;
742 }
743
744 // Try to get from global variables
745 if (window.chatSessionId) {
746 return window.chatSessionId;
747 }
748
749 // Generate a temporary session ID if none found
750 return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
751 }
752
753 loadSystemInfo() {
754 // Load system information from backend
755 this.updateSimilarityThreshold();
756 this.updateSystemPrompt();
757 this.updateKnowledgeBaseStatus();
758 }
759
760 updateSimilarityThreshold() {
761 fetch(mxchatTestData.ajaxUrl, {
762 method: 'POST',
763 headers: {
764 'Content-Type': 'application/x-www-form-urlencoded',
765 },
766 body: new URLSearchParams({
767 action: 'mxchat_get_similarity_threshold',
768 nonce: mxchatTestData.nonce
769 })
770 })
771 .then(response => response.json())
772 .then(data => {
773 const thresholdEl = this.panel.querySelector('#similarity-threshold');
774 if (data.success) {
775 thresholdEl.innerHTML = `<code>${data.data.threshold_percentage}</code>`;
776 } else {
777 thresholdEl.innerHTML = '<span class="error-text">Error loading threshold</span>';
778 }
779 })
780 .catch(error => {
781 console.error('Error fetching similarity threshold:', error);
782 const thresholdEl = this.panel.querySelector('#similarity-threshold');
783 thresholdEl.innerHTML = '<span class="error-text">Connection error</span>';
784 });
785 }
786
787 updateSystemPrompt() {
788 const promptEl = this.panel.querySelector('#system-prompt');
789 promptEl.textContent = 'Loading system prompt...';
790
791 fetch(mxchatTestData.ajaxUrl, {
792 method: 'POST',
793 headers: {
794 'Content-Type': 'application/x-www-form-urlencoded',
795 },
796 body: new URLSearchParams({
797 action: 'mxchat_get_system_info',
798 nonce: mxchatTestData.nonce
799 })
800 })
801 .then(response => response.json())
802 .then(data => {
803 if (data.success) {
804 promptEl.textContent = data.data.system_prompt || 'No system prompt configured';
805
806 // Enhanced model display with OpenRouter support
807 if (data.data.is_openrouter) {
808 this.log(`🤖 Model: OpenRouter`);
809 this.log(` └─ Using: ${data.data.openrouter_model}`);
810 } else {
811 this.log(`🤖 Model: ${data.data.selected_model}`);
812 }
813
814 // Enhanced API status with OpenRouter
815 const apiStatus = data.data.api_status;
816 const configuredApis = Object.keys(apiStatus).filter(key => apiStatus[key]);
817
818 if (configuredApis.length > 0) {
819 this.log(`🔑 Configured APIs: ${configuredApis.map(api => {
820 // Capitalize and format API names
821 if (api === 'openai') return 'OpenAI';
822 if (api === 'xai') return 'X.AI';
823 if (api === 'openrouter') return 'OpenRouter';
824 return api.charAt(0).toUpperCase() + api.slice(1);
825 }).join(', ')}`);
826 } else {
827 this.log(`⚠️ No API keys configured`);
828 }
829
830 // Specific warning for OpenRouter if selected but no key
831 if (data.data.is_openrouter && !apiStatus.openrouter) {
832 this.log(`WARNING: OpenRouter selected but no API key configured!`);
833 }
834 } else {
835 promptEl.textContent = 'Error loading system prompt';
836 }
837 })
838 .catch(error => {
839 console.error('Error fetching system info:', error);
840 promptEl.textContent = 'Connection error';
841 });
842 }
843
844 updateKnowledgeBaseStatus() {
845 const statusEl = this.panel.querySelector('#kb-status');
846 statusEl.innerHTML = 'Checking...';
847
848 fetch(mxchatTestData.ajaxUrl, {
849 method: 'POST',
850 headers: {
851 'Content-Type': 'application/x-www-form-urlencoded',
852 },
853 body: new URLSearchParams({
854 action: 'mxchat_get_kb_status',
855 nonce: mxchatTestData.nonce
856 })
857 })
858 .then(response => response.json())
859 .then(data => {
860 if (data.success) {
861 const kbData = data.data;
862 statusEl.innerHTML = `<span class="success-text">${kbData.status}</span> (${kbData.type} - ${kbData.documents})`;
863 } else {
864 statusEl.innerHTML = '<span class="error-text">Error loading KB status</span>';
865 }
866 })
867 .catch(error => {
868 console.error('Error fetching KB status:', error);
869 statusEl.innerHTML = '<span class="error-text">Connection error</span>';
870 });
871 }
872
873 clearDebugConsole() {
874 const console = this.panel.querySelector('#debug-console');
875 console.innerHTML = '<div class="debug-entry">Debug console cleared...</div>';
876 }
877
878 log(message) {
879 const console = this.panel.querySelector('#debug-console');
880 const timestamp = new Date().toLocaleTimeString();
881 const logEntry = document.createElement('div');
882 logEntry.className = 'debug-entry';
883 logEntry.innerHTML = `<span class="debug-timestamp">[${timestamp}]</span> ${message}`;
884 console.appendChild(logEntry);
885 console.scrollTop = console.scrollHeight;
886
887 // Keep only last 50 entries to prevent memory issues
888 const entries = console.querySelectorAll('.debug-entry');
889 if (entries.length > 50) {
890 entries[0].remove();
891 }
892 }
893 }
894
895 // Initialize the test panel when the script loads
896 document.addEventListener('DOMContentLoaded', function() {
897 // Only initialize if user is admin and testing is enabled
898 if (window.mxchatTestingEnabled) {
899 window.mxchatTestPanel = new MxChatTestPanel();
900 }
901 });
902
903 // Global function to enable testing mode programmatically
904 window.enableMxChatTesting = function() {
905 if (!window.mxchatTestPanel) {
906 window.mxchatTestPanel = new MxChatTestPanel();
907 }
908 };