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

949 lines 38.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 // 58f8b4: URL-guard outcome — stripping used to be completely silent,
280 // which is why fabricated/stripped links were invisible from this panel.
281 if (testingData.url_validation) {
282 const uv = testingData.url_validation;
283 if (uv.removed_count > 0) {
284 this.log(`🚫 URL guard stripped ${uv.removed_count} unapproved link${uv.removed_count !== 1 ? 's' : ''}: ${(uv.removed_urls || []).join(', ')}`);
285 } else if (uv.checked > 0) {
286 this.log(`🔗 URL guard: all ${uv.checked} link${uv.checked !== 1 ? 's' : ''} in the answer approved (${uv.strict ? 'strict' : 'lenient'} mode)`);
287 }
288 }
289
290 // Show summary in debug console
291 if (testingData.top_matches && testingData.top_matches.length > 0) {
292 const aboveThreshold = testingData.top_matches.filter(match => match.above_threshold).length;
293 const belowThreshold = testingData.top_matches.length - aboveThreshold;
294 const highestScore = testingData.top_matches[0].similarity_percentage;
295
296 this.log(`�
297 Analysis: ${aboveThreshold} above threshold, ${belowThreshold} below threshold`);
298 this.log(`🏆 Highest similarity: ${highestScore}%`);
299 } else {
300 this.log('⚠️ No document matches found');
301 }
302
303 // NEW: Log action summary
304 if (testingData.action_matches && testingData.action_matches.length > 0) {
305 const triggeredAction = testingData.action_matches.find(action => action.triggered);
306 if (triggeredAction) {
307 this.log(`🎯 Action Triggered: ${triggeredAction.intent_label} (${triggeredAction.similarity_percentage}%)`);
308 } else {
309 const highestAction = testingData.action_matches[0];
310 this.log(`🚫 No actions triggered - Highest: ${highestAction.intent_label} (${highestAction.similarity_percentage}%)`);
311 }
312 } else {
313 this.log('📝 No actions checked');
314 }
315 }
316
317 updateApprovedUrls(approvedUrls) {
318 const urlsEl = this.panel.querySelector('#approved-urls');
319
320 // Safety check: ensure approvedUrls is an array
321 if (!approvedUrls || !Array.isArray(approvedUrls) || approvedUrls.length === 0) {
322 urlsEl.innerHTML = '<div class="no-data-message">No approved URLs (AI cannot cite links)</div>';
323 return;
324 }
325
326 let html = `<div class="urls-header">
327 <strong>${approvedUrls.length} URL${approvedUrls.length !== 1 ? 's' : ''} approved for AI citations</strong>
328 </div>`;
329
330 approvedUrls.forEach((url, index) => {
331 // Extract domain for display
332 let displayUrl = url;
333 try {
334 const urlObj = new URL(url);
335 displayUrl = urlObj.hostname + urlObj.pathname;
336 } catch (e) {
337 // Keep original if URL parsing fails
338 }
339
340 html += `
341 <div class="url-card">
342 <div class="url-line">
343 <span class="url-icon">🔗</span>
344 <a href="${url}" target="_blank" rel="noopener noreferrer" class="url-link" title="${url}">
345 ${displayUrl}
346 </a>
347 </div>
348 </div>
349 `;
350 });
351
352 html += `<div class="urls-note">
353 The AI can only cite these URLs. Any other URLs will be automatically removed from responses.
354 </div>`;
355
356 urlsEl.innerHTML = html;
357 }
358
359 updateActionMatches(actionMatches) {
360 const actionsEl = this.panel.querySelector('#action-scores');
361
362 if (!actionMatches || actionMatches.length === 0) {
363 actionsEl.innerHTML = '<div class="no-data-message">No actions checked</div>';
364 return;
365 }
366
367 let html = `<div class="actions-header">
368 <strong>Top ${actionMatches.length} actions checked</strong>
369 </div>`;
370
371 actionMatches.forEach((action, index) => {
372 const isTriggered = action.triggered;
373 const isAboveThreshold = action.above_threshold;
374 const statusIcon = isTriggered ? '🎯' : (isAboveThreshold ? '⚠️' : '❌');
375
376 // Determine the correct label based on status
377 let statusLabel;
378 if (isTriggered) {
379 statusLabel = 'TRIGGERED';
380 } else if (isAboveThreshold) {
381 statusLabel = 'Above threshold';
382 } else {
383 statusLabel = 'Below threshold';
384 }
385
386 // Determine card styling
387 const cardClass = isTriggered ? 'action-triggered' : (isAboveThreshold ? 'action-above-threshold' : 'action-below-threshold');
388
389 html += `
390 <div class="action-card ${cardClass}">
391 <div class="action-line">
392 <span class="action-icon">${statusIcon}</span>
393 <span class="action-name">${action.intent_label}</span>
394 <span class="action-score">${action.similarity_percentage}%</span>
395 </div>
396 <div class="action-details">
397 <span class="action-status">${statusLabel}</span>
398 <span class="action-threshold">Threshold: ${action.threshold_percentage}%</span>
399 </div>
400 </div>
401 `;
402 });
403
404 actionsEl.innerHTML = html;
405 }
406
407 updateTopMatches(topMatches, threshold, sourcesUsed = 0, totalChunksUsed = 0) {
408 const scoresEl = this.panel.querySelector('#similarity-scores');
409
410 if (!topMatches || topMatches.length === 0) {
411 scoresEl.innerHTML = '<div class="no-data-message">No similarity data available</div>';
412 return;
413 }
414
415 // Hybrid keyword boost (38ffa1): rows carry matched_via + fused_rank
416 // when hybrid retrieval was on for this message.
417 const hybridOn = topMatches.some(m => m.matched_via);
418
419 // Group matches by source URL
420 const groupedByUrl = {};
421 topMatches.forEach((match) => {
422 const url = match.source_display || 'Unknown';
423 if (!groupedByUrl[url]) {
424 groupedByUrl[url] = {
425 url: url,
426 isUrl: url.startsWith('http'),
427 bestScore: 0,
428 usedForContext: false,
429 totalChunks: match.total_chunks || 1,
430 matchedChunks: [],
431 isChunked: match.is_chunk || false,
432 bestFusedRank: Infinity,
433 viaSet: {}
434 };
435 }
436
437 // Track best score
438 if (match.similarity_percentage > groupedByUrl[url].bestScore) {
439 groupedByUrl[url].bestScore = match.similarity_percentage;
440 }
441
442 // Track if any chunk was used for context
443 if (match.used_for_context) {
444 groupedByUrl[url].usedForContext = true;
445 }
446
447 if (match.fused_rank && match.fused_rank < groupedByUrl[url].bestFusedRank) {
448 groupedByUrl[url].bestFusedRank = match.fused_rank;
449 }
450 if (match.matched_via) {
451 groupedByUrl[url].viaSet[match.matched_via] = true;
452 }
453
454 // Add chunk info
455 groupedByUrl[url].matchedChunks.push({
456 chunkIndex: match.chunk_index,
457 score: match.similarity_percentage,
458 usedForContext: match.used_for_context,
459 aboveThreshold: match.above_threshold
460 });
461 });
462
463 // Convert to array: fused-rank order when hybrid is on, best cosine otherwise
464 const urlGroups = Object.values(groupedByUrl).sort((a, b) => {
465 if (hybridOn && a.bestFusedRank !== b.bestFusedRank) {
466 return a.bestFusedRank - b.bestFusedRank;
467 }
468 return b.bestScore - a.bestScore;
469 });
470
471 // Use backend counts if available, otherwise fall back to frontend calculation
472 const usedUrlCount = sourcesUsed > 0 ? sourcesUsed : urlGroups.filter(g => g.usedForContext).length;
473 const chunksInfo = totalChunksUsed > 0 ? `${totalChunksUsed} chunks sent to AI` : `${topMatches.length} chunk matches`;
474
475 let html = `<div class="matches-header">
476 <strong>${usedUrlCount} source${usedUrlCount === 1 ? '' : 's'} used for AI context</strong>
477 <span class="matches-subheader">(${chunksInfo})</span>
478 </div>`;
479
480 urlGroups.forEach((group, groupIndex) => {
481 const cardClass = group.usedForContext ? 'above-threshold' : 'below-threshold';
482 const statusIcon = group.usedForContext ? '✓' : '✗';
483 const contextLabel = group.usedForContext ? 'Used for AI context' : 'Not used';
484
485 // Build chunk summary
486 let chunkSummary = '';
487 if (group.isChunked && group.totalChunks > 1) {
488 const usedChunkCount = group.matchedChunks.filter(c => c.usedForContext).length;
489 chunkSummary = `<span class="chunk-summary">${usedChunkCount}/${group.totalChunks} chunks matched</span>`;
490 }
491
492 // Check if this entry has multiple matched chunks to show expand toggle
493 const hasMultipleChunks = group.matchedChunks.length > 1;
494 const expandToggle = hasMultipleChunks
495 ? `<span class="chunk-expand-toggle" data-group="${groupIndex}">Show chunks</span>`
496 : '';
497
498 let viaChip = '';
499 if (hybridOn) {
500 const vias = Object.keys(group.viaSet);
501 if (vias.length) {
502 const viaLabel = (vias.length > 1 || vias[0] === 'both') ? 'Both'
503 : (vias[0] === 'keyword' ? 'Keyword' : 'Vector');
504 viaChip = `<span class="mxch-rag-via-chip mxch-rag-via-${viaLabel.toLowerCase()}">${viaLabel}</span>`;
505 }
506 }
507
508 html += `
509 <div class="match-card ${cardClass}">
510 <div class="match-header">
511 <div class="match-title">
512 <span class="status-icon">${statusIcon}</span>
513 <span class="similarity-score">${group.bestScore}%</span>
514 ${viaChip}
515 ${chunkSummary}
516 </div>
517 <span class="context-label">${contextLabel}</span>
518 </div>
519 <div class="match-source">
520 ${group.isUrl ?
521 `<span class="source-icon link-icon">🔗</span> ${group.url}` :
522 `<span class="source-icon doc-icon">📄</span> ${group.url}`
523 }
524 </div>
525 ${expandToggle}
526 ${hasMultipleChunks ? this.renderChunkDetails(group.matchedChunks, groupIndex) : ''}
527 </div>
528 `;
529 });
530
531 scoresEl.innerHTML = html;
532
533 // Add click handlers for expand toggles
534 scoresEl.querySelectorAll('.chunk-expand-toggle').forEach(toggle => {
535 toggle.addEventListener('click', (e) => {
536 const groupId = e.target.dataset.group;
537 const details = scoresEl.querySelector(`.chunk-details[data-group="${groupId}"]`);
538 if (details) {
539 const isExpanded = details.classList.toggle('expanded');
540 e.target.textContent = isExpanded ? '▼ Hide chunks' : '▶ Show chunks';
541 }
542 });
543 });
544 }
545
546 renderChunkDetails(chunks, groupIndex) {
547 // Sort chunks by chunk index
548 const sortedChunks = [...chunks].sort((a, b) => (a.chunkIndex || 0) - (b.chunkIndex || 0));
549
550 let html = `<div class="chunk-details" data-group="${groupIndex}">`;
551
552 sortedChunks.forEach(chunk => {
553 const chunkNum = (chunk.chunkIndex !== null && chunk.chunkIndex !== undefined)
554 ? chunk.chunkIndex + 1
555 : '?';
556 const statusClass = chunk.usedForContext ? 'chunk-used' : 'chunk-not-used';
557 const statusIcon = chunk.usedForContext ? '✓' : '○';
558
559 html += `
560 <div class="chunk-detail-row ${statusClass}">
561 <span class="chunk-detail-icon">${statusIcon}</span>
562 <span class="chunk-detail-num">Chunk ${chunkNum}</span>
563 <span class="chunk-detail-score">${chunk.score}%</span>
564 </div>
565 `;
566 });
567
568 html += '</div>';
569 return html;
570 }
571
572 updateLastQuery(query, topMatches) {
573 const queryEl = this.panel.querySelector('#last-query');
574 queryEl.textContent = query;
575
576 this.lastQueryData = {
577 query,
578 topMatches,
579 timestamp: new Date()
580 };
581 }
582
583 clearChatSession() {
584 // Determine the active bot ID from MxChatInstances
585 let botId = 'default';
586 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getAllBotIds === 'function') {
587 const botIds = MxChatInstances.getAllBotIds();
588 if (botIds.length > 0) {
589 botId = botIds[0];
590 }
591 }
592
593 // Get current session ID using the correct bot-suffixed cookie name
594 const cookieName = 'mxchat_session_id_' + botId;
595 const sessionId = this.getCookie(cookieName) || this.getCookie('mxchat_session_id') || this.getCurrentSessionId();
596
597 if (!sessionId) {
598 this.log('No active session found');
599 return;
600 }
601
602 this.log('Current session ID: ' + sessionId);
603 this.log('Starting fresh session...');
604
605 // Use MxChatInstances.resetChatSession to properly reset in-memory state + cookie
606 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.resetChatSession === 'function') {
607 MxChatInstances.resetChatSession(botId);
608 }
609
610 // Get the new session ID that was just set by resetChatSession
611 let newSessionId = '';
612 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
613 newSessionId = MxChatInstances.getChatSession(botId);
614 }
615
616 // Fallback if MxChatInstances wasn't available
617 if (!newSessionId) {
618 newSessionId = 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
619 this.clearMxChatCookie(botId);
620 this.setChatSession(newSessionId, botId);
621 }
622
623 this.log('New session ID: ' + newSessionId);
624
625 // Call backend to clear old session data
626 fetch(mxchatTestData.ajaxUrl, {
627 method: 'POST',
628 headers: {
629 'Content-Type': 'application/x-www-form-urlencoded',
630 },
631 body: new URLSearchParams({
632 action: 'mxchat_start_fresh_session',
633 nonce: mxchatTestData.nonce,
634 old_session_id: sessionId,
635 new_session_id: newSessionId
636 })
637 })
638 .then(response => response.json())
639 .then(data => {
640 if (data.success) {
641 this.log('Backend session cleared: ' + data.data.message);
642
643 // Update the session ID everywhere in the DOM
644 this.updateSessionIdEverywhere(newSessionId);
645
646 // Clear the chat UI
647 this.clearChatUI();
648
649 // Show popular questions again
650 const popularQuestions = document.querySelector('#mxchat-popular-questions');
651 if (popularQuestions) {
652 popularQuestions.style.display = 'block';
653 }
654
655 // Clear testing data displays
656 this.updateLastQuery('New session started', []);
657 this.updateTopMatches([], 0);
658 this.updateApprovedUrls([]);
659
660 this.log('Fresh session started successfully');
661
662 } else {
663 this.log('Error clearing session: ' + (data.data?.message || 'Unknown error'));
664 }
665 })
666 .catch(error => {
667 console.error('Error clearing chat session:', error);
668 this.log('Connection error when clearing session');
669 });
670 }
671
672 // Helper function to get cookie (same as your existing one)
673 getCookie(name) {
674 let value = "; " + document.cookie;
675 let parts = value.split("; " + name + "=");
676 if (parts.length == 2) return parts.pop().split(";").shift();
677 }
678
679 // Helper function to set session cookie (matches chat-script.js format)
680 setChatSession(sessionId, botId) {
681 botId = botId || 'default';
682 document.cookie = 'mxchat_session_id_' + botId + '=' + sessionId + '; path=/; max-age=86400; SameSite=Lax';
683 }
684
685 // Helper function to clear the MxChat session cookie
686 clearMxChatCookie(botId) {
687 botId = botId || 'default';
688 // Clear both bot-suffixed and legacy cookie formats
689 document.cookie = 'mxchat_session_id_' + botId + '=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
690 document.cookie = 'mxchat_session_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
691 this.log('Session cookie cleared');
692 }
693
694 updateSessionIdEverywhere(newSessionId) {
695 // Update global session ID variable if it exists
696 if (window.mxchatSessionId) {
697 window.mxchatSessionId = newSessionId;
698 }
699
700 // Update session ID in chat input data attribute
701 const chatInput = document.querySelector('#chat-input');
702 if (chatInput) {
703 chatInput.dataset.sessionId = newSessionId;
704 }
705
706 // Update any hidden session ID fields
707 const sessionInputs = document.querySelectorAll('input[name="session_id"]');
708 sessionInputs.forEach(input => {
709 input.value = newSessionId;
710 });
711
712 // Update any data attributes that store session ID
713 const elementsWithSessionId = document.querySelectorAll('[data-session-id]');
714 elementsWithSessionId.forEach(element => {
715 element.dataset.sessionId = newSessionId;
716 });
717
718 // Update URL parameter if it exists
719 if (window.location.search.includes('session_id=')) {
720 const url = new URL(window.location);
721 url.searchParams.set('session_id', newSessionId);
722 window.history.replaceState({}, '', url);
723 }
724
725 this.log('🔄 Session ID updated everywhere in DOM');
726 }
727
728 clearChatUI() {
729 const chatBox = document.querySelector('#chat-box');
730 if (chatBox) {
731 // Remove all messages (both user and bot)
732 const allMessages = chatBox.querySelectorAll('.bot-message, .user-message');
733 allMessages.forEach(msg => {
734 // Keep the first bot message if it's a welcome message
735 if (msg === chatBox.querySelector('.bot-message') &&
736 msg.textContent.toLowerCase().includes('welcome')) {
737 return; // Keep welcome message
738 }
739 msg.remove();
740 });
741 this.log('🧹 Chat UI cleared');
742 }
743
744 // Clear chat input
745 const chatInput = document.querySelector('#chat-input');
746 if (chatInput) {
747 chatInput.value = '';
748 }
749 }
750
751 getCurrentSessionId() {
752 // Try MxChatInstances first (most reliable — matches chat-script.js)
753 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
754 const botIds = typeof MxChatInstances.getAllBotIds === 'function' ? MxChatInstances.getAllBotIds() : ['default'];
755 const botId = botIds.length > 0 ? botIds[0] : 'default';
756 const instanceSession = MxChatInstances.getChatSession(botId);
757 if (instanceSession) {
758 return instanceSession;
759 }
760 }
761
762 // Try bot-suffixed cookie, then legacy cookie
763 const botCookieId = this.getCookie('mxchat_session_id_default');
764 if (botCookieId) {
765 return botCookieId;
766 }
767 const cookieSessionId = this.getCookie('mxchat_session_id');
768 if (cookieSessionId) {
769 return cookieSessionId;
770 }
771
772 // Try to get session ID from various DOM sources
773 const chatInput = document.querySelector('#chat-input');
774 if (chatInput && chatInput.dataset.sessionId) {
775 return chatInput.dataset.sessionId;
776 }
777
778 // Try to get from URL parameters
779 const urlParams = new URLSearchParams(window.location.search);
780 const sessionFromUrl = urlParams.get('session_id');
781 if (sessionFromUrl) {
782 return sessionFromUrl;
783 }
784
785 // Try to get from global variables
786 if (window.chatSessionId) {
787 return window.chatSessionId;
788 }
789
790 // Generate a temporary session ID if none found
791 return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
792 }
793
794 loadSystemInfo() {
795 // Load system information from backend
796 this.updateSimilarityThreshold();
797 this.updateSystemPrompt();
798 this.updateKnowledgeBaseStatus();
799 }
800
801 updateSimilarityThreshold() {
802 fetch(mxchatTestData.ajaxUrl, {
803 method: 'POST',
804 headers: {
805 'Content-Type': 'application/x-www-form-urlencoded',
806 },
807 body: new URLSearchParams({
808 action: 'mxchat_get_similarity_threshold',
809 nonce: mxchatTestData.nonce
810 })
811 })
812 .then(response => response.json())
813 .then(data => {
814 const thresholdEl = this.panel.querySelector('#similarity-threshold');
815 if (data.success) {
816 thresholdEl.innerHTML = `<code>${data.data.threshold_percentage}</code>`;
817 } else {
818 thresholdEl.innerHTML = '<span class="error-text">Error loading threshold</span>';
819 }
820 })
821 .catch(error => {
822 console.error('Error fetching similarity threshold:', error);
823 const thresholdEl = this.panel.querySelector('#similarity-threshold');
824 thresholdEl.innerHTML = '<span class="error-text">Connection error</span>';
825 });
826 }
827
828 updateSystemPrompt() {
829 const promptEl = this.panel.querySelector('#system-prompt');
830 promptEl.textContent = 'Loading system prompt...';
831
832 fetch(mxchatTestData.ajaxUrl, {
833 method: 'POST',
834 headers: {
835 'Content-Type': 'application/x-www-form-urlencoded',
836 },
837 body: new URLSearchParams({
838 action: 'mxchat_get_system_info',
839 nonce: mxchatTestData.nonce
840 })
841 })
842 .then(response => response.json())
843 .then(data => {
844 if (data.success) {
845 promptEl.textContent = data.data.system_prompt || 'No system prompt configured';
846
847 // Enhanced model display with OpenRouter support
848 if (data.data.is_openrouter) {
849 this.log(`🤖 Model: OpenRouter`);
850 this.log(` └─ Using: ${data.data.openrouter_model}`);
851 } else {
852 this.log(`🤖 Model: ${data.data.selected_model}`);
853 }
854
855 // Enhanced API status with OpenRouter
856 const apiStatus = data.data.api_status;
857 const configuredApis = Object.keys(apiStatus).filter(key => apiStatus[key]);
858
859 if (configuredApis.length > 0) {
860 this.log(`🔑 Configured APIs: ${configuredApis.map(api => {
861 // Capitalize and format API names
862 if (api === 'openai') return 'OpenAI';
863 if (api === 'xai') return 'X.AI';
864 if (api === 'openrouter') return 'OpenRouter';
865 return api.charAt(0).toUpperCase() + api.slice(1);
866 }).join(', ')}`);
867 } else {
868 this.log(`⚠️ No API keys configured`);
869 }
870
871 // Specific warning for OpenRouter if selected but no key
872 if (data.data.is_openrouter && !apiStatus.openrouter) {
873 this.log(`WARNING: OpenRouter selected but no API key configured!`);
874 }
875 } else {
876 promptEl.textContent = 'Error loading system prompt';
877 }
878 })
879 .catch(error => {
880 console.error('Error fetching system info:', error);
881 promptEl.textContent = 'Connection error';
882 });
883 }
884
885 updateKnowledgeBaseStatus() {
886 const statusEl = this.panel.querySelector('#kb-status');
887 statusEl.innerHTML = 'Checking...';
888
889 fetch(mxchatTestData.ajaxUrl, {
890 method: 'POST',
891 headers: {
892 'Content-Type': 'application/x-www-form-urlencoded',
893 },
894 body: new URLSearchParams({
895 action: 'mxchat_get_kb_status',
896 nonce: mxchatTestData.nonce
897 })
898 })
899 .then(response => response.json())
900 .then(data => {
901 if (data.success) {
902 const kbData = data.data;
903 statusEl.innerHTML = `<span class="success-text">${kbData.status}</span> (${kbData.type} - ${kbData.documents})`;
904 } else {
905 statusEl.innerHTML = '<span class="error-text">Error loading KB status</span>';
906 }
907 })
908 .catch(error => {
909 console.error('Error fetching KB status:', error);
910 statusEl.innerHTML = '<span class="error-text">Connection error</span>';
911 });
912 }
913
914 clearDebugConsole() {
915 const console = this.panel.querySelector('#debug-console');
916 console.innerHTML = '<div class="debug-entry">Debug console cleared...</div>';
917 }
918
919 log(message) {
920 const console = this.panel.querySelector('#debug-console');
921 const timestamp = new Date().toLocaleTimeString();
922 const logEntry = document.createElement('div');
923 logEntry.className = 'debug-entry';
924 logEntry.innerHTML = `<span class="debug-timestamp">[${timestamp}]</span> ${message}`;
925 console.appendChild(logEntry);
926 console.scrollTop = console.scrollHeight;
927
928 // Keep only last 50 entries to prevent memory issues
929 const entries = console.querySelectorAll('.debug-entry');
930 if (entries.length > 50) {
931 entries[0].remove();
932 }
933 }
934 }
935
936 // Initialize the test panel when the script loads
937 document.addEventListener('DOMContentLoaded', function() {
938 // Only initialize if user is admin and testing is enabled
939 if (window.mxchatTestingEnabled) {
940 window.mxchatTestPanel = new MxChatTestPanel();
941 }
942 });
943
944 // Global function to enable testing mode programmatically
945 window.enableMxChatTesting = function() {
946 if (!window.mxchatTestPanel) {
947 window.mxchatTestPanel = new MxChatTestPanel();
948 }
949 };