PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.4.6
MxChat – AI Chatbot & Content Generation for WordPress v2.4.6
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
← All changes | js/test-panel.js +88 -334 3.2.212.4.6 View file →
@@ -71,14 +71,8 @@
71 71 <strong>User Query:</strong>
72 72 <div id="last-query" class="query-display">Waiting for next query...</div>
73 73 </div>
74 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 75 <strong>Document Matches:</strong>
82 76 <div class="mxchat-test-results similarity-container" id="similarity-scores">
83 77 <div class="no-data-message">No query data yet</div>
84 78 </div>
@@ -253,14 +247,10 @@
253 247 this.log('📊 Chat data captured from response');
254 248
255 249 // Update query analysis section
256 250 this.updateLastQuery(testingData.query || 'No query', testingData.top_matches || []);
251 + this.updateTopMatches(testingData.top_matches || [], testingData.similarity_threshold || 0.75);
257 252
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 253 // NEW: Update action matches
264 254 this.updateActionMatches(testingData.action_matches || []);
265 255
266 256 // Log additional info
@@ -270,24 +260,8 @@
270 260 if (testingData.similarity_threshold) {
271 261 this.log(`🎯 Similarity Threshold: ${(testingData.similarity_threshold * 100)}%`);
272 262 }
273 263
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 264 // Show summary in debug console
291 265 if (testingData.top_matches && testingData.top_matches.length > 0) {
292 266 const aboveThreshold = testingData.top_matches.filter(match => match.above_threshold).length;
293 267 const belowThreshold = testingData.top_matches.length - aboveThreshold;
@@ -312,50 +286,8 @@
312 286 this.log('📝 No actions checked');
313 287 }
314 288 }
315 289
316 -updateApprovedUrls(approvedUrls) {
317 - const urlsEl = this.panel.querySelector('#approved-urls');
318 -
319 - // Safety check: ensure approvedUrls is an array
320 - if (!approvedUrls || !Array.isArray(approvedUrls) || approvedUrls.length === 0) {
321 - urlsEl.innerHTML = '<div class="no-data-message">No approved URLs (AI cannot cite links)</div>';
322 - return;
323 - }
324 -
325 - let html = `<div class="urls-header">
326 - <strong>${approvedUrls.length} URL${approvedUrls.length !== 1 ? 's' : ''} approved for AI citations</strong>
327 - </div>`;
328 -
329 - approvedUrls.forEach((url, index) => {
330 - // Extract domain for display
331 - let displayUrl = url;
332 - try {
333 - const urlObj = new URL(url);
334 - displayUrl = urlObj.hostname + urlObj.pathname;
335 - } catch (e) {
336 - // Keep original if URL parsing fails
337 - }
338 -
339 - html += `
340 - <div class="url-card">
341 - <div class="url-line">
342 - <span class="url-icon">🔗</span>
343 - <a href="${url}" target="_blank" rel="noopener noreferrer" class="url-link" title="${url}">
344 - ${displayUrl}
345 - </a>
346 - </div>
347 - </div>
348 - `;
349 - });
350 -
351 - html += `<div class="urls-note">
352 - ℹ️ The AI can only cite these URLs. Any other URLs will be automatically removed from responses.
353 - </div>`;
354 -
355 - urlsEl.innerHTML = html;
356 -}
357 -
358 290 updateActionMatches(actionMatches) {
359 291 const actionsEl = this.panel.querySelector('#action-scores');
360 292
361 293 if (!actionMatches || actionMatches.length === 0) {
@@ -402,173 +334,60 @@
402 334
403 335 actionsEl.innerHTML = html;
404 336 }
405 337
406 - updateTopMatches(topMatches, threshold, sourcesUsed = 0, totalChunksUsed = 0) {
338 + updateTopMatches(topMatches, threshold) {
407 339 const scoresEl = this.panel.querySelector('#similarity-scores');
408 -
340 +
409 341 if (!topMatches || topMatches.length === 0) {
410 342 scoresEl.innerHTML = '<div class="no-data-message">No similarity data available</div>';
411 343 return;
412 344 }
413 -
414 - // Hybrid keyword boost (38ffa1): rows carry matched_via + fused_rank
415 - // when hybrid retrieval was on for this message.
416 - const hybridOn = topMatches.some(m => m.matched_via);
417 -
418 - // Group matches by source URL
419 - const groupedByUrl = {};
420 - topMatches.forEach((match) => {
421 - const url = match.source_display || 'Unknown';
422 - if (!groupedByUrl[url]) {
423 - groupedByUrl[url] = {
424 - url: url,
425 - isUrl: url.startsWith('http'),
426 - bestScore: 0,
427 - usedForContext: false,
428 - totalChunks: match.total_chunks || 1,
429 - matchedChunks: [],
430 - isChunked: match.is_chunk || false,
431 - bestFusedRank: Infinity,
432 - viaSet: {}
433 - };
434 - }
435 -
436 - // Track best score
437 - if (match.similarity_percentage > groupedByUrl[url].bestScore) {
438 - groupedByUrl[url].bestScore = match.similarity_percentage;
439 - }
440 -
441 - // Track if any chunk was used for context
442 - if (match.used_for_context) {
443 - groupedByUrl[url].usedForContext = true;
444 - }
445 -
446 - if (match.fused_rank && match.fused_rank < groupedByUrl[url].bestFusedRank) {
447 - groupedByUrl[url].bestFusedRank = match.fused_rank;
448 - }
449 - if (match.matched_via) {
450 - groupedByUrl[url].viaSet[match.matched_via] = true;
451 - }
452 -
453 - // Add chunk info
454 - groupedByUrl[url].matchedChunks.push({
455 - chunkIndex: match.chunk_index,
456 - score: match.similarity_percentage,
457 - usedForContext: match.used_for_context,
458 - aboveThreshold: match.above_threshold
459 - });
460 - });
461 -
462 - // Convert to array: fused-rank order when hybrid is on, best cosine otherwise
463 - const urlGroups = Object.values(groupedByUrl).sort((a, b) => {
464 - if (hybridOn && a.bestFusedRank !== b.bestFusedRank) {
465 - return a.bestFusedRank - b.bestFusedRank;
466 - }
467 - return b.bestScore - a.bestScore;
468 - });
469 -
470 - // Use backend counts if available, otherwise fall back to frontend calculation
471 - const usedUrlCount = sourcesUsed > 0 ? sourcesUsed : urlGroups.filter(g => g.usedForContext).length;
472 - const chunksInfo = totalChunksUsed > 0 ? `${totalChunksUsed} chunks sent to AI` : `${topMatches.length} chunk matches`;
473 -
345 +
474 346 let html = `<div class="matches-header">
475 - <strong>${usedUrlCount} source${usedUrlCount === 1 ? '' : 's'} used for AI context</strong>
476 - <span class="matches-subheader">(${chunksInfo})</span>
347 + <strong>Top ${topMatches.length} matches</strong>
477 348 </div>`;
478 -
479 - urlGroups.forEach((group, groupIndex) => {
480 - const cardClass = group.usedForContext ? 'above-threshold' : 'below-threshold';
481 - const statusIcon = group.usedForContext ? '✓' : '✗';
482 - const contextLabel = group.usedForContext ? 'Used for AI context' : 'Not used';
483 -
484 - // Build chunk summary
485 - let chunkSummary = '';
486 - if (group.isChunked && group.totalChunks > 1) {
487 - const usedChunkCount = group.matchedChunks.filter(c => c.usedForContext).length;
488 - chunkSummary = `<span class="chunk-summary">${usedChunkCount}/${group.totalChunks} chunks matched</span>`;
349 +
350 + topMatches.forEach((match, index) => {
351 + const isAboveThreshold = match.above_threshold;
352 + const isUsedForContext = match.used_for_context; // Use the actual flag from PHP
353 + const statusIcon = isAboveThreshold ? '✓' : '✗';
354 +
355 + // Determine the correct label based on actual usage
356 + let contextLabel;
357 + if (isUsedForContext) {
358 + contextLabel = 'Used for AI context';
359 + } else if (isAboveThreshold) {
360 + contextLabel = 'Above threshold (not used)';
361 + } else {
362 + contextLabel = 'Below threshold';
489 363 }
490 -
491 - // Check if this entry has multiple matched chunks to show expand toggle
492 - const hasMultipleChunks = group.matchedChunks.length > 1;
493 - const expandToggle = hasMultipleChunks
494 - ? `<span class="chunk-expand-toggle" data-group="${groupIndex}">▶ Show chunks</span>`
495 - : '';
496 -
497 - let viaChip = '';
498 - if (hybridOn) {
499 - const vias = Object.keys(group.viaSet);
500 - if (vias.length) {
501 - const viaLabel = (vias.length > 1 || vias[0] === 'both') ? 'Both'
502 - : (vias[0] === 'keyword' ? 'Keyword' : 'Vector');
503 - viaChip = `<span class="mxch-rag-via-chip mxch-rag-via-${viaLabel.toLowerCase()}">${viaLabel}</span>`;
504 - }
505 - }
506 -
364 +
365 + // Determine card styling - should be based on whether it was actually used
366 + const cardClass = isUsedForContext ? 'above-threshold' : 'below-threshold';
367 +
507 368 html += `
508 369 <div class="match-card ${cardClass}">
509 370 <div class="match-header">
510 371 <div class="match-title">
511 372 <span class="status-icon">${statusIcon}</span>
512 - <span class="similarity-score">${group.bestScore}%</span>
513 - ${viaChip}
514 - ${chunkSummary}
373 + <span class="similarity-score">${match.similarity_percentage}%</span>
515 374 </div>
516 375 <span class="context-label">${contextLabel}</span>
517 376 </div>
518 377 <div class="match-source">
519 - ${group.isUrl ?
520 - `<span class="source-icon link-icon">🔗</span> ${group.url}` :
521 - `<span class="source-icon doc-icon">📄</span> ${group.url}`
378 + ${match.source_display.startsWith('http') ?
379 + `<span class="source-icon link-icon">🔗</span> ${match.source_display}` :
380 + `<span class="source-icon doc-icon">📄</span> ${match.source_display}`
522 381 }
523 382 </div>
524 - ${expandToggle}
525 - ${hasMultipleChunks ? this.renderChunkDetails(group.matchedChunks, groupIndex) : ''}
526 383 </div>
527 384 `;
528 385 });
529 -
386 +
530 387 scoresEl.innerHTML = html;
531 -
532 - // Add click handlers for expand toggles
533 - scoresEl.querySelectorAll('.chunk-expand-toggle').forEach(toggle => {
534 - toggle.addEventListener('click', (e) => {
535 - const groupId = e.target.dataset.group;
536 - const details = scoresEl.querySelector(`.chunk-details[data-group="${groupId}"]`);
537 - if (details) {
538 - const isExpanded = details.classList.toggle('expanded');
539 - e.target.textContent = isExpanded ? '▼ Hide chunks' : '▶ Show chunks';
540 - }
541 - });
542 - });
543 388 }
544 389
545 - renderChunkDetails(chunks, groupIndex) {
546 - // Sort chunks by chunk index
547 - const sortedChunks = [...chunks].sort((a, b) => (a.chunkIndex || 0) - (b.chunkIndex || 0));
548 -
549 - let html = `<div class="chunk-details" data-group="${groupIndex}">`;
550 -
551 - sortedChunks.forEach(chunk => {
552 - const chunkNum = (chunk.chunkIndex !== null && chunk.chunkIndex !== undefined)
553 - ? chunk.chunkIndex + 1
554 - : '?';
555 - const statusClass = chunk.usedForContext ? 'chunk-used' : 'chunk-not-used';
556 - const statusIcon = chunk.usedForContext ? '✓' : '○';
557 -
558 - html += `
559 - <div class="chunk-detail-row ${statusClass}">
560 - <span class="chunk-detail-icon">${statusIcon}</span>
561 - <span class="chunk-detail-num">Chunk ${chunkNum}</span>
562 - <span class="chunk-detail-score">${chunk.score}%</span>
563 - </div>
564 - `;
565 - });
566 -
567 - html += '</div>';
568 - return html;
569 - }
570 -
571 390 updateLastQuery(query, topMatches) {
572 391 const queryEl = this.panel.querySelector('#last-query');
573 392 queryEl.textContent = query;
574 393
@@ -579,49 +398,28 @@
579 398 };
580 399 }
581 400
582 401 clearChatSession() {
583 - // Determine the active bot ID from MxChatInstances
584 - let botId = 'default';
585 - if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getAllBotIds === 'function') {
586 - const botIds = MxChatInstances.getAllBotIds();
587 - if (botIds.length > 0) {
588 - botId = botIds[0];
589 - }
590 - }
591 -
592 - // Get current session ID using the correct bot-suffixed cookie name
593 - const cookieName = 'mxchat_session_id_' + botId;
594 - const sessionId = this.getCookie(cookieName) || this.getCookie('mxchat_session_id') || this.getCurrentSessionId();
595 -
402 + // Get current session ID from cookie (most reliable)
403 + const sessionId = this.getCookie('mxchat_session_id') || this.getCurrentSessionId();
404 +
596 405 if (!sessionId) {
597 - this.log('No active session found');
406 + this.log('❌ No active session found');
598 407 return;
599 408 }
600 -
601 - this.log('Current session ID: ' + sessionId);
602 - this.log('Starting fresh session...');
603 -
604 - // Use MxChatInstances.resetChatSession to properly reset in-memory state + cookie
605 - if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.resetChatSession === 'function') {
606 - MxChatInstances.resetChatSession(botId);
607 - }
608 -
609 - // Get the new session ID that was just set by resetChatSession
610 - let newSessionId = '';
611 - if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
612 - newSessionId = MxChatInstances.getChatSession(botId);
613 - }
614 -
615 - // Fallback if MxChatInstances wasn't available
616 - if (!newSessionId) {
617 - newSessionId = 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
618 - this.clearMxChatCookie(botId);
619 - this.setChatSession(newSessionId, botId);
620 - }
621 -
622 - this.log('New session ID: ' + newSessionId);
623 -
409 +
410 + this.log(`🔍 Current session ID: ${sessionId}`);
411 + this.log('🧹 Starting fresh session...');
412 +
413 + // Generate new session ID (using your existing format)
414 + const newSessionId = 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
415 +
416 + // Clear the old cookie and set new one immediately
417 + this.clearMxChatCookie();
418 + this.setChatSession(newSessionId);
419 +
420 + this.log(`🆕 New session ID: ${newSessionId}`);
421 +
624 422 // Call backend to clear old session data
625 423 fetch(mxchatTestData.ajaxUrl, {
626 424 method: 'POST',
627 425 headers: {
@@ -636,36 +434,35 @@
636 434 })
637 435 .then(response => response.json())
638 436 .then(data => {
639 437 if (data.success) {
640 - this.log('Backend session cleared: ' + data.data.message);
641 -
438 + this.log('✅ Backend session cleared: ' + data.data.message);
439 +
642 440 // Update the session ID everywhere in the DOM
643 441 this.updateSessionIdEverywhere(newSessionId);
644 -
442 +
645 443 // Clear the chat UI
646 444 this.clearChatUI();
647 -
445 +
648 446 // Show popular questions again
649 447 const popularQuestions = document.querySelector('#mxchat-popular-questions');
650 448 if (popularQuestions) {
651 449 popularQuestions.style.display = 'block';
652 450 }
653 -
451 +
654 452 // Clear testing data displays
655 453 this.updateLastQuery('New session started', []);
656 454 this.updateTopMatches([], 0);
657 - this.updateApprovedUrls([]);
658 -
659 - this.log('Fresh session started successfully');
660 -
455 +
456 + this.log('🎉 Fresh session started successfully');
457 +
661 458 } else {
662 - this.log('Error clearing session: ' + (data.data?.message || 'Unknown error'));
459 + this.log('❌ Error clearing session: ' + (data.data?.message || 'Unknown error'));
663 460 }
664 461 })
665 462 .catch(error => {
666 463 console.error('Error clearing chat session:', error);
667 - this.log('Connection error when clearing session');
464 + this.log('🔌 Connection error when clearing session');
668 465 });
669 466 }
670 467
671 468 // Helper function to get cookie (same as your existing one)
@@ -674,21 +471,19 @@
674 471 let parts = value.split("; " + name + "=");
675 472 if (parts.length == 2) return parts.pop().split(";").shift();
676 473 }
677 474
678 - // Helper function to set session cookie (matches chat-script.js format)
679 - setChatSession(sessionId, botId) {
680 - botId = botId || 'default';
681 - document.cookie = 'mxchat_session_id_' + botId + '=' + sessionId + '; path=/; max-age=86400; SameSite=Lax';
475 + // Helper function to set session cookie (same as your existing one)
476 + setChatSession(sessionId) {
477 + // Set the cookie with a 24-hour expiration (86400 seconds)
478 + document.cookie = "mxchat_session_id=" + sessionId + "; path=/; max-age=86400; SameSite=Lax";
682 479 }
683 480
684 481 // Helper function to clear the MxChat session cookie
685 - clearMxChatCookie(botId) {
686 - botId = botId || 'default';
687 - // Clear both bot-suffixed and legacy cookie formats
688 - document.cookie = 'mxchat_session_id_' + botId + '=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
689 - document.cookie = 'mxchat_session_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
690 - this.log('Session cookie cleared');
482 + clearMxChatCookie() {
483 + // Clear the cookie by setting it to expire in the past
484 + document.cookie = "mxchat_session_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax";
485 + this.log('🍪 Session cookie cleared');
691 486 }
692 487
693 488 updateSessionIdEverywhere(newSessionId) {
694 489 // Update global session ID variable if it exists
@@ -747,23 +542,9 @@
747 542 }
748 543 }
749 544
750 545 getCurrentSessionId() {
751 - // Try MxChatInstances first (most reliable — matches chat-script.js)
752 - if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
753 - const botIds = typeof MxChatInstances.getAllBotIds === 'function' ? MxChatInstances.getAllBotIds() : ['default'];
754 - const botId = botIds.length > 0 ? botIds[0] : 'default';
755 - const instanceSession = MxChatInstances.getChatSession(botId);
756 - if (instanceSession) {
757 - return instanceSession;
758 - }
759 - }
760 -
761 - // Try bot-suffixed cookie, then legacy cookie
762 - const botCookieId = this.getCookie('mxchat_session_id_default');
763 - if (botCookieId) {
764 - return botCookieId;
765 - }
546 + // First try to get from cookie (most reliable)
766 547 const cookieSessionId = this.getCookie('mxchat_session_id');
767 548 if (cookieSessionId) {
768 549 return cookieSessionId;
769 550 }
@@ -824,63 +605,36 @@
824 605 });
825 606 }
826 607
827 608 updateSystemPrompt() {
828 - const promptEl = this.panel.querySelector('#system-prompt');
829 - promptEl.textContent = 'Loading system prompt...';
830 -
831 - fetch(mxchatTestData.ajaxUrl, {
832 - method: 'POST',
833 - headers: {
834 - 'Content-Type': 'application/x-www-form-urlencoded',
835 - },
836 - body: new URLSearchParams({
837 - action: 'mxchat_get_system_info',
838 - nonce: mxchatTestData.nonce
609 + const promptEl = this.panel.querySelector('#system-prompt');
610 + promptEl.textContent = 'Loading system prompt...';
611 +
612 + fetch(mxchatTestData.ajaxUrl, {
613 + method: 'POST',
614 + headers: {
615 + 'Content-Type': 'application/x-www-form-urlencoded',
616 + },
617 + body: new URLSearchParams({
618 + action: 'mxchat_get_system_info',
619 + nonce: mxchatTestData.nonce
620 + })
839 621 })
840 - })
841 - .then(response => response.json())
842 - .then(data => {
843 - if (data.success) {
844 - promptEl.textContent = data.data.system_prompt || 'No system prompt configured';
845 -
846 - // Enhanced model display with OpenRouter support
847 - if (data.data.is_openrouter) {
848 - this.log(`🤖 Model: OpenRouter`);
849 - this.log(` └─ Using: ${data.data.openrouter_model}`);
850 - } else {
622 + .then(response => response.json())
623 + .then(data => {
624 + if (data.success) {
625 + promptEl.textContent = data.data.system_prompt || 'No system prompt configured';
851 626 this.log(`🤖 Model: ${data.data.selected_model}`);
852 - }
853 -
854 - // Enhanced API status with OpenRouter
855 - const apiStatus = data.data.api_status;
856 - const configuredApis = Object.keys(apiStatus).filter(key => apiStatus[key]);
857 -
858 - if (configuredApis.length > 0) {
859 - this.log(`🔑 Configured APIs: ${configuredApis.map(api => {
860 - // Capitalize and format API names
861 - if (api === 'openai') return 'OpenAI';
862 - if (api === 'xai') return 'X.AI';
863 - if (api === 'openrouter') return 'OpenRouter';
864 - return api.charAt(0).toUpperCase() + api.slice(1);
865 - }).join(', ')}`);
627 + this.log(`🔑 API Status: ${JSON.stringify(data.data.api_status)}`);
866 628 } else {
867 - this.log(`⚠️ No API keys configured`);
629 + promptEl.textContent = 'Error loading system prompt';
868 630 }
869 -
870 - // Specific warning for OpenRouter if selected but no key
871 - if (data.data.is_openrouter && !apiStatus.openrouter) {
872 - this.log(`❌ WARNING: OpenRouter selected but no API key configured!`);
873 - }
874 - } else {
875 - promptEl.textContent = 'Error loading system prompt';
876 - }
877 - })
878 - .catch(error => {
879 - console.error('Error fetching system info:', error);
880 - promptEl.textContent = 'Connection error';
881 - });
882 -}
631 + })
632 + .catch(error => {
633 + console.error('Error fetching system info:', error);
634 + promptEl.textContent = 'Connection error';
635 + });
636 + }
883 637
884 638 updateKnowledgeBaseStatus() {
885 639 const statusEl = this.panel.querySelector('#kb-status');
886 640 statusEl.innerHTML = 'Checking...';