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