| @@ -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> |