PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / trunk
MxChat – AI Chatbot & Content Generation for WordPress vtrunk
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 +43 -3 3.2.6trunk View file →
@@ -274,8 +274,19 @@
274 274 // NEW: Log approved URLs count
275 275 if (testingData.approved_urls && testingData.approved_urls.length > 0) {
276 276 this.log(`🔗 Approved URLs for citations: ${testingData.approved_urls.length}`);
277 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 + }
278 289
279 290 // Show summary in debug console
280 291 if (testingData.top_matches && testingData.top_matches.length > 0) {
281 292 const aboveThreshold = testingData.top_matches.filter(match => match.above_threshold).length;
@@ -399,8 +410,12 @@
399 410 scoresEl.innerHTML = '<div class="no-data-message">No similarity data available</div>';
400 411 return;
401 412 }
402 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 +
403 418 // Group matches by source URL
404 419 const groupedByUrl = {};
405 420 topMatches.forEach((match) => {
406 421 const url = match.source_display || 'Unknown';
@@ -411,9 +426,11 @@
411 426 bestScore: 0,
412 427 usedForContext: false,
413 428 totalChunks: match.total_chunks || 1,
414 429 matchedChunks: [],
415 - isChunked: match.is_chunk || false
430 + isChunked: match.is_chunk || false,
431 + bestFusedRank: Infinity,
432 + viaSet: {}
416 433 };
417 434 }
418 435
419 436 // Track best score
@@ -425,8 +442,15 @@
425 442 if (match.used_for_context) {
426 443 groupedByUrl[url].usedForContext = true;
427 444 }
428 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 +
429 453 // Add chunk info
430 454 groupedByUrl[url].matchedChunks.push({
431 455 chunkIndex: match.chunk_index,
432 456 score: match.similarity_percentage,
@@ -434,10 +458,15 @@
434 458 aboveThreshold: match.above_threshold
435 459 });
436 460 });
437 461
438 - // Convert to array and sort by best score
439 - const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore);
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 + });
440 469
441 470 // Use backend counts if available, otherwise fall back to frontend calculation
442 471 const usedUrlCount = sourcesUsed > 0 ? sourcesUsed : urlGroups.filter(g => g.usedForContext).length;
443 472 const chunksInfo = totalChunksUsed > 0 ? `${totalChunksUsed} chunks sent to AI` : `${topMatches.length} chunk matches`;
@@ -464,8 +493,18 @@
464 493 const expandToggle = hasMultipleChunks
465 494 ? `<span class="chunk-expand-toggle" data-group="${groupIndex}">▶ Show chunks</span>`
466 495 : '';
467 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 +
468 507 html += `
469 508 <div class="match-card ${cardClass}">
470 509 <div class="match-header">
471 510 <div class="match-title">
@@ -470,8 +509,9 @@
470 509 <div class="match-header">
471 510 <div class="match-title">
472 511 <span class="status-icon">${statusIcon}</span>
473 512 <span class="similarity-score">${group.bestScore}%</span>
513 + ${viaChip}
474 514 ${chunkSummary}
475 515 </div>
476 516 <span class="context-label">${contextLabel}</span>
477 517 </div>