/** * MxChat Test Panel JavaScript * Handles the testing interface for admins - always active when panel is open */ class MxChatTestPanel { constructor() { this.panel = null; this.tab = null; this.isOpen = false; this.lastQueryData = null; this.init(); } init() { // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.setup()); } else { this.setup(); } } setup() { this.createElements(); this.bindEvents(); this.setupChatInterception(); } createElements() { // Create the test tab this.tab = document.createElement('div'); this.tab.className = 'mxchat-test-tab'; this.tab.innerHTML = 'MXCHAT DEBUGGING'; this.tab.title = 'Open MxChat Debug Panel'; document.body.appendChild(this.tab); // Create the test panel this.panel = document.createElement('div'); this.panel.className = 'mxchat-test-panel'; this.panel.innerHTML = this.getPanelHTML(); document.body.appendChild(this.panel); } getPanelHTML() { return `

MxChat Debug Panel

Always-on debugging for administrators

Quick Actions

Last Query Analysis

Similarity Threshold: Loading...
User Query:
Waiting for next query...
Approved URLs for Citations:
No URL data yet
Document Matches:
No query data yet
Actions Triggered:
No action data yet

System Information

System Prompt:
Loading...
Knowledge Base: Checking...

Debug Log

Debug panel ready - monitoring chat activity...
`; } bindEvents() { // Tab click to toggle panel this.tab.addEventListener('click', () => this.togglePanel()); // Close button const closeBtn = this.panel.querySelector('.mxchat-test-close'); closeBtn.addEventListener('click', () => this.closePanel()); // Action buttons this.bindActionButtons(); // Keep escape key to close (useful shortcut) document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen) { this.closePanel(); } }); } bindActionButtons() { // Clear chat session this.panel.querySelector('#clear-chat-session').addEventListener('click', () => { this.clearChatSession(); }); // Clear debug console this.panel.querySelector('#clear-debug').addEventListener('click', () => { this.clearDebugConsole(); }); } togglePanel() { if (this.isOpen) { this.closePanel(); } else { this.openPanel(); } } openPanel() { this.panel.classList.add('open'); this.isOpen = true; this.tab.style.display = 'none'; this.loadSystemInfo(); this.log('Debug panel opened - capturing chat data automatically'); } closePanel() { this.panel.classList.remove('open'); this.isOpen = false; this.tab.style.display = 'block'; } setupChatInterception() { // Set up interception for chat responses to capture testing data this.interceptChatResponses(); this.log('Chat monitoring initialized'); } interceptChatResponses() { // Store reference to the test panel instance window.mxchatTestPanelInstance = this; // Intercept jQuery AJAX calls (for regular chat) if (window.jQuery) { const originalAjax = jQuery.ajax; jQuery.ajax = function(options) { const originalSuccess = options.success; options.success = function(data, textStatus, jqXHR) { // Check if this is a chat request if (options.data && (options.data.action === 'mxchat_handle_chat_request' || options.data.action === 'mxchat_stream_chat')) { const testPanel = window.mxchatTestPanelInstance; // Always try to handle testing data if it exists (no toggle check) if (testPanel && data && data.testing_data) { testPanel.handleTestingData(data.testing_data); } else if (testPanel && data && data.data && data.data.testing_data) { // Check if data is nested testPanel.handleTestingData(data.data.testing_data); } } // Call original success handler if (originalSuccess) { originalSuccess.call(this, data, textStatus, jqXHR); } }; return originalAjax.call(this, options); }; } // Also intercept fetch API calls (for streaming) const originalFetch = window.fetch; window.fetch = (...args) => { return originalFetch(...args).then(response => { // Check if this is a chat request if (args[0].includes('admin-ajax.php') || args[0].includes('mxchat')) { // For streaming responses that return JSON instead of streams const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { response.clone().json().then(data => { const testPanel = window.mxchatTestPanelInstance; // Always try to handle testing data if it exists (no toggle check) if (testPanel && data && data.testing_data) { testPanel.handleTestingData(data.testing_data); } else if (testPanel && data && data.data && data.data.testing_data) { // Check nested data testPanel.handleTestingData(data.data.testing_data); } }).catch(() => { // Ignore JSON parsing errors }); } } return response; }); }; this.log('Chat interception active for jQuery and fetch requests'); } handleTestingData(testingData) { this.log('๐Ÿ“Š Chat data captured from response'); // Update query analysis section this.updateLastQuery(testingData.query || 'No query', testingData.top_matches || []); // NEW: Update approved URLs this.updateApprovedUrls(testingData.approved_urls || []); this.updateTopMatches(testingData.top_matches || [], testingData.similarity_threshold || 0.75, testingData.sources_used || 0, testingData.total_chunks_used || 0); // NEW: Update action matches this.updateActionMatches(testingData.action_matches || []); // Log additional info if (testingData.knowledge_base_type) { this.log(`๐Ÿ“š Knowledge Base: ${testingData.knowledge_base_type}`); } if (testingData.similarity_threshold) { this.log(`๐ŸŽฏ Similarity Threshold: ${(testingData.similarity_threshold * 100)}%`); } // NEW: Log approved URLs count if (testingData.approved_urls && testingData.approved_urls.length > 0) { this.log(`๐Ÿ”— Approved URLs for citations: ${testingData.approved_urls.length}`); } // Show summary in debug console if (testingData.top_matches && testingData.top_matches.length > 0) { const aboveThreshold = testingData.top_matches.filter(match => match.above_threshold).length; const belowThreshold = testingData.top_matches.length - aboveThreshold; const highestScore = testingData.top_matches[0].similarity_percentage; this.log(`โœ… Analysis: ${aboveThreshold} above threshold, ${belowThreshold} below threshold`); this.log(`๐Ÿ† Highest similarity: ${highestScore}%`); } else { this.log('โš ๏ธ No document matches found'); } // NEW: Log action summary if (testingData.action_matches && testingData.action_matches.length > 0) { const triggeredAction = testingData.action_matches.find(action => action.triggered); if (triggeredAction) { this.log(`๐ŸŽฏ Action Triggered: ${triggeredAction.intent_label} (${triggeredAction.similarity_percentage}%)`); } else { const highestAction = testingData.action_matches[0]; this.log(`๐Ÿšซ No actions triggered - Highest: ${highestAction.intent_label} (${highestAction.similarity_percentage}%)`); } } else { this.log('๐Ÿ“ No actions checked'); } } updateApprovedUrls(approvedUrls) { const urlsEl = this.panel.querySelector('#approved-urls'); // Safety check: ensure approvedUrls is an array if (!approvedUrls || !Array.isArray(approvedUrls) || approvedUrls.length === 0) { urlsEl.innerHTML = '
No approved URLs (AI cannot cite links)
'; return; } let html = `
${approvedUrls.length} URL${approvedUrls.length !== 1 ? 's' : ''} approved for AI citations
`; approvedUrls.forEach((url, index) => { // Extract domain for display let displayUrl = url; try { const urlObj = new URL(url); displayUrl = urlObj.hostname + urlObj.pathname; } catch (e) { // Keep original if URL parsing fails } html += `
๐Ÿ”— ${displayUrl}
`; }); html += `
โ„น๏ธ The AI can only cite these URLs. Any other URLs will be automatically removed from responses.
`; urlsEl.innerHTML = html; } updateActionMatches(actionMatches) { const actionsEl = this.panel.querySelector('#action-scores'); if (!actionMatches || actionMatches.length === 0) { actionsEl.innerHTML = '
No actions checked
'; return; } let html = `
Top ${actionMatches.length} actions checked
`; actionMatches.forEach((action, index) => { const isTriggered = action.triggered; const isAboveThreshold = action.above_threshold; const statusIcon = isTriggered ? '๐ŸŽฏ' : (isAboveThreshold ? 'โš ๏ธ' : 'โŒ'); // Determine the correct label based on status let statusLabel; if (isTriggered) { statusLabel = 'TRIGGERED'; } else if (isAboveThreshold) { statusLabel = 'Above threshold'; } else { statusLabel = 'Below threshold'; } // Determine card styling const cardClass = isTriggered ? 'action-triggered' : (isAboveThreshold ? 'action-above-threshold' : 'action-below-threshold'); html += `
${statusIcon} ${action.intent_label} ${action.similarity_percentage}%
${statusLabel} Threshold: ${action.threshold_percentage}%
`; }); actionsEl.innerHTML = html; } updateTopMatches(topMatches, threshold, sourcesUsed = 0, totalChunksUsed = 0) { const scoresEl = this.panel.querySelector('#similarity-scores'); if (!topMatches || topMatches.length === 0) { scoresEl.innerHTML = '
No similarity data available
'; return; } // Group matches by source URL const groupedByUrl = {}; topMatches.forEach((match) => { const url = match.source_display || 'Unknown'; if (!groupedByUrl[url]) { groupedByUrl[url] = { url: url, isUrl: url.startsWith('http'), bestScore: 0, usedForContext: false, totalChunks: match.total_chunks || 1, matchedChunks: [], isChunked: match.is_chunk || false }; } // Track best score if (match.similarity_percentage > groupedByUrl[url].bestScore) { groupedByUrl[url].bestScore = match.similarity_percentage; } // Track if any chunk was used for context if (match.used_for_context) { groupedByUrl[url].usedForContext = true; } // Add chunk info groupedByUrl[url].matchedChunks.push({ chunkIndex: match.chunk_index, score: match.similarity_percentage, usedForContext: match.used_for_context, aboveThreshold: match.above_threshold }); }); // Convert to array and sort by best score const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore); // Use backend counts if available, otherwise fall back to frontend calculation const usedUrlCount = sourcesUsed > 0 ? sourcesUsed : urlGroups.filter(g => g.usedForContext).length; const chunksInfo = totalChunksUsed > 0 ? `${totalChunksUsed} chunks sent to AI` : `${topMatches.length} chunk matches`; let html = `
${usedUrlCount} source${usedUrlCount === 1 ? '' : 's'} used for AI context (${chunksInfo})
`; urlGroups.forEach((group, groupIndex) => { const cardClass = group.usedForContext ? 'above-threshold' : 'below-threshold'; const statusIcon = group.usedForContext ? 'โœ“' : 'โœ—'; const contextLabel = group.usedForContext ? 'Used for AI context' : 'Not used'; // Build chunk summary let chunkSummary = ''; if (group.isChunked && group.totalChunks > 1) { const usedChunkCount = group.matchedChunks.filter(c => c.usedForContext).length; chunkSummary = `${usedChunkCount}/${group.totalChunks} chunks matched`; } // Check if this entry has multiple matched chunks to show expand toggle const hasMultipleChunks = group.matchedChunks.length > 1; const expandToggle = hasMultipleChunks ? `โ–ถ Show chunks` : ''; html += `
${statusIcon} ${group.bestScore}% ${chunkSummary}
${contextLabel}
${group.isUrl ? `๐Ÿ”— ${group.url}` : `๐Ÿ“„ ${group.url}` }
${expandToggle} ${hasMultipleChunks ? this.renderChunkDetails(group.matchedChunks, groupIndex) : ''}
`; }); scoresEl.innerHTML = html; // Add click handlers for expand toggles scoresEl.querySelectorAll('.chunk-expand-toggle').forEach(toggle => { toggle.addEventListener('click', (e) => { const groupId = e.target.dataset.group; const details = scoresEl.querySelector(`.chunk-details[data-group="${groupId}"]`); if (details) { const isExpanded = details.classList.toggle('expanded'); e.target.textContent = isExpanded ? 'โ–ผ Hide chunks' : 'โ–ถ Show chunks'; } }); }); } renderChunkDetails(chunks, groupIndex) { // Sort chunks by chunk index const sortedChunks = [...chunks].sort((a, b) => (a.chunkIndex || 0) - (b.chunkIndex || 0)); let html = `
`; sortedChunks.forEach(chunk => { const chunkNum = (chunk.chunkIndex !== null && chunk.chunkIndex !== undefined) ? chunk.chunkIndex + 1 : '?'; const statusClass = chunk.usedForContext ? 'chunk-used' : 'chunk-not-used'; const statusIcon = chunk.usedForContext ? 'โœ“' : 'โ—‹'; html += `
${statusIcon} Chunk ${chunkNum} ${chunk.score}%
`; }); html += '
'; return html; } updateLastQuery(query, topMatches) { const queryEl = this.panel.querySelector('#last-query'); queryEl.textContent = query; this.lastQueryData = { query, topMatches, timestamp: new Date() }; } clearChatSession() { // Determine the active bot ID from MxChatInstances let botId = 'default'; if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getAllBotIds === 'function') { const botIds = MxChatInstances.getAllBotIds(); if (botIds.length > 0) { botId = botIds[0]; } } // Get current session ID using the correct bot-suffixed cookie name const cookieName = 'mxchat_session_id_' + botId; const sessionId = this.getCookie(cookieName) || this.getCookie('mxchat_session_id') || this.getCurrentSessionId(); if (!sessionId) { this.log('No active session found'); return; } this.log('Current session ID: ' + sessionId); this.log('Starting fresh session...'); // Use MxChatInstances.resetChatSession to properly reset in-memory state + cookie if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.resetChatSession === 'function') { MxChatInstances.resetChatSession(botId); } // Get the new session ID that was just set by resetChatSession let newSessionId = ''; if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') { newSessionId = MxChatInstances.getChatSession(botId); } // Fallback if MxChatInstances wasn't available if (!newSessionId) { newSessionId = 'mxchat_chat_' + Math.random().toString(36).substr(2, 9); this.clearMxChatCookie(botId); this.setChatSession(newSessionId, botId); } this.log('New session ID: ' + newSessionId); // Call backend to clear old session data fetch(mxchatTestData.ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'mxchat_start_fresh_session', nonce: mxchatTestData.nonce, old_session_id: sessionId, new_session_id: newSessionId }) }) .then(response => response.json()) .then(data => { if (data.success) { this.log('Backend session cleared: ' + data.data.message); // Update the session ID everywhere in the DOM this.updateSessionIdEverywhere(newSessionId); // Clear the chat UI this.clearChatUI(); // Show popular questions again const popularQuestions = document.querySelector('#mxchat-popular-questions'); if (popularQuestions) { popularQuestions.style.display = 'block'; } // Clear testing data displays this.updateLastQuery('New session started', []); this.updateTopMatches([], 0); this.updateApprovedUrls([]); this.log('Fresh session started successfully'); } else { this.log('Error clearing session: ' + (data.data?.message || 'Unknown error')); } }) .catch(error => { console.error('Error clearing chat session:', error); this.log('Connection error when clearing session'); }); } // Helper function to get cookie (same as your existing one) getCookie(name) { let value = "; " + document.cookie; let parts = value.split("; " + name + "="); if (parts.length == 2) return parts.pop().split(";").shift(); } // Helper function to set session cookie (matches chat-script.js format) setChatSession(sessionId, botId) { botId = botId || 'default'; document.cookie = 'mxchat_session_id_' + botId + '=' + sessionId + '; path=/; max-age=86400; SameSite=Lax'; } // Helper function to clear the MxChat session cookie clearMxChatCookie(botId) { botId = botId || 'default'; // Clear both bot-suffixed and legacy cookie formats document.cookie = 'mxchat_session_id_' + botId + '=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax'; document.cookie = 'mxchat_session_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax'; this.log('Session cookie cleared'); } updateSessionIdEverywhere(newSessionId) { // Update global session ID variable if it exists if (window.mxchatSessionId) { window.mxchatSessionId = newSessionId; } // Update session ID in chat input data attribute const chatInput = document.querySelector('#chat-input'); if (chatInput) { chatInput.dataset.sessionId = newSessionId; } // Update any hidden session ID fields const sessionInputs = document.querySelectorAll('input[name="session_id"]'); sessionInputs.forEach(input => { input.value = newSessionId; }); // Update any data attributes that store session ID const elementsWithSessionId = document.querySelectorAll('[data-session-id]'); elementsWithSessionId.forEach(element => { element.dataset.sessionId = newSessionId; }); // Update URL parameter if it exists if (window.location.search.includes('session_id=')) { const url = new URL(window.location); url.searchParams.set('session_id', newSessionId); window.history.replaceState({}, '', url); } this.log('๐Ÿ”„ Session ID updated everywhere in DOM'); } clearChatUI() { const chatBox = document.querySelector('#chat-box'); if (chatBox) { // Remove all messages (both user and bot) const allMessages = chatBox.querySelectorAll('.bot-message, .user-message'); allMessages.forEach(msg => { // Keep the first bot message if it's a welcome message if (msg === chatBox.querySelector('.bot-message') && msg.textContent.toLowerCase().includes('welcome')) { return; // Keep welcome message } msg.remove(); }); this.log('๐Ÿงน Chat UI cleared'); } // Clear chat input const chatInput = document.querySelector('#chat-input'); if (chatInput) { chatInput.value = ''; } } getCurrentSessionId() { // Try MxChatInstances first (most reliable โ€” matches chat-script.js) if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') { const botIds = typeof MxChatInstances.getAllBotIds === 'function' ? MxChatInstances.getAllBotIds() : ['default']; const botId = botIds.length > 0 ? botIds[0] : 'default'; const instanceSession = MxChatInstances.getChatSession(botId); if (instanceSession) { return instanceSession; } } // Try bot-suffixed cookie, then legacy cookie const botCookieId = this.getCookie('mxchat_session_id_default'); if (botCookieId) { return botCookieId; } const cookieSessionId = this.getCookie('mxchat_session_id'); if (cookieSessionId) { return cookieSessionId; } // Try to get session ID from various DOM sources const chatInput = document.querySelector('#chat-input'); if (chatInput && chatInput.dataset.sessionId) { return chatInput.dataset.sessionId; } // Try to get from URL parameters const urlParams = new URLSearchParams(window.location.search); const sessionFromUrl = urlParams.get('session_id'); if (sessionFromUrl) { return sessionFromUrl; } // Try to get from global variables if (window.chatSessionId) { return window.chatSessionId; } // Generate a temporary session ID if none found return 'mxchat_chat_' + Math.random().toString(36).substr(2, 9); } loadSystemInfo() { // Load system information from backend this.updateSimilarityThreshold(); this.updateSystemPrompt(); this.updateKnowledgeBaseStatus(); } updateSimilarityThreshold() { fetch(mxchatTestData.ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'mxchat_get_similarity_threshold', nonce: mxchatTestData.nonce }) }) .then(response => response.json()) .then(data => { const thresholdEl = this.panel.querySelector('#similarity-threshold'); if (data.success) { thresholdEl.innerHTML = `${data.data.threshold_percentage}`; } else { thresholdEl.innerHTML = 'Error loading threshold'; } }) .catch(error => { console.error('Error fetching similarity threshold:', error); const thresholdEl = this.panel.querySelector('#similarity-threshold'); thresholdEl.innerHTML = 'Connection error'; }); } updateSystemPrompt() { const promptEl = this.panel.querySelector('#system-prompt'); promptEl.textContent = 'Loading system prompt...'; fetch(mxchatTestData.ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'mxchat_get_system_info', nonce: mxchatTestData.nonce }) }) .then(response => response.json()) .then(data => { if (data.success) { promptEl.textContent = data.data.system_prompt || 'No system prompt configured'; // Enhanced model display with OpenRouter support if (data.data.is_openrouter) { this.log(`๐Ÿค– Model: OpenRouter`); this.log(` โ””โ”€ Using: ${data.data.openrouter_model}`); } else { this.log(`๐Ÿค– Model: ${data.data.selected_model}`); } // Enhanced API status with OpenRouter const apiStatus = data.data.api_status; const configuredApis = Object.keys(apiStatus).filter(key => apiStatus[key]); if (configuredApis.length > 0) { this.log(`๐Ÿ”‘ Configured APIs: ${configuredApis.map(api => { // Capitalize and format API names if (api === 'openai') return 'OpenAI'; if (api === 'xai') return 'X.AI'; if (api === 'openrouter') return 'OpenRouter'; return api.charAt(0).toUpperCase() + api.slice(1); }).join(', ')}`); } else { this.log(`โš ๏ธ No API keys configured`); } // Specific warning for OpenRouter if selected but no key if (data.data.is_openrouter && !apiStatus.openrouter) { this.log(`โŒ WARNING: OpenRouter selected but no API key configured!`); } } else { promptEl.textContent = 'Error loading system prompt'; } }) .catch(error => { console.error('Error fetching system info:', error); promptEl.textContent = 'Connection error'; }); } updateKnowledgeBaseStatus() { const statusEl = this.panel.querySelector('#kb-status'); statusEl.innerHTML = 'Checking...'; fetch(mxchatTestData.ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: 'mxchat_get_kb_status', nonce: mxchatTestData.nonce }) }) .then(response => response.json()) .then(data => { if (data.success) { const kbData = data.data; statusEl.innerHTML = `โœ“ ${kbData.status} (${kbData.type} - ${kbData.documents})`; } else { statusEl.innerHTML = 'Error loading KB status'; } }) .catch(error => { console.error('Error fetching KB status:', error); statusEl.innerHTML = 'Connection error'; }); } clearDebugConsole() { const console = this.panel.querySelector('#debug-console'); console.innerHTML = '
Debug console cleared...
'; } log(message) { const console = this.panel.querySelector('#debug-console'); const timestamp = new Date().toLocaleTimeString(); const logEntry = document.createElement('div'); logEntry.className = 'debug-entry'; logEntry.innerHTML = `[${timestamp}] ${message}`; console.appendChild(logEntry); console.scrollTop = console.scrollHeight; // Keep only last 50 entries to prevent memory issues const entries = console.querySelectorAll('.debug-entry'); if (entries.length > 50) { entries[0].remove(); } } } // Initialize the test panel when the script loads document.addEventListener('DOMContentLoaded', function() { // Only initialize if user is admin and testing is enabled if (window.mxchatTestingEnabled) { window.mxchatTestPanel = new MxChatTestPanel(); } }); // Global function to enable testing mode programmatically window.enableMxChatTesting = function() { if (!window.mxchatTestPanel) { window.mxchatTestPanel = new MxChatTestPanel(); } };