| 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">×</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 |
}; |