| @@ -984,11 +984,18 @@ | ||
| 984 | 984 | |
| 985 | 985 | function renderRagContext(data, $container) { |
| 986 | 986 | let html = ''; |
| 987 | 987 | |
| 988 | - // Check if we have any source data | |
| 989 | - if (!data.top_matches || data.top_matches.length === 0) { | |
| 990 | - html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>'; | |
| 988 | + // 67fc92: "the KB was searched and nothing matched" and "nothing was | |
| 989 | + // recorded for this row" are different facts. Retrieval keys present | |
| 990 | + // means the search ran and was recorded (top_matches may be empty); | |
| 991 | + // no retrieval keys means there is simply no record (older rows, or | |
| 992 | + // turns that never consulted the knowledge base). | |
| 993 | + const retrievalRecorded = typeof data.total_documents_checked !== 'undefined' | |
| 994 | + || (data.top_matches && data.top_matches.length > 0); | |
| 995 | + | |
| 996 | + if (!retrievalRecorded) { | |
| 997 | + html += '<div class="mxch-no-results"><p>No retrieval data was recorded for this response.</p></div>'; | |
| 991 | 998 | $container.html(html); |
| 992 | 999 | return; |
| 993 | 1000 | } |
| 994 | 1001 | |
| @@ -994,9 +1001,10 @@ | ||
| 994 | 1001 | |
| 995 | 1002 | // Hybrid keyword boost (38ffa1): rows carry matched_via + fused_rank |
| 996 | 1003 | // when hybrid retrieval was on for this response. Cosine % stays the |
| 997 | 1004 | // anchor; the chip explains WHY a low-% row ranked high. |
| 998 | - const hybridOn = data.top_matches.some(function(m) { return m.matched_via; }); | |
| 1005 | + const topMatches = data.top_matches || []; | |
| 1006 | + const hybridOn = topMatches.some(function(m) { return m.matched_via; }); | |
| 999 | 1007 | |
| 1000 | 1008 | html += '<div class="mxch-rag-summary">'; |
| 1001 | 1009 | html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Knowledge Base:</span> <span class="mxch-rag-value">' + escapeHtml(data.knowledge_base_type || 'WordPress Database') + '</span></div>'; |
| 1002 | 1010 | html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Similarity Threshold:</span> <span class="mxch-rag-value">' + Math.round((data.similarity_threshold || 0.35) * 100) + '%</span></div>'; |
| @@ -1005,8 +1013,14 @@ | ||
| 1005 | 1013 | html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Retrieval:</span> <span class="mxch-rag-value">Hybrid</span></div>'; |
| 1006 | 1014 | } |
| 1007 | 1015 | html += '</div>'; |
| 1008 | 1016 | |
| 1017 | + if (topMatches.length === 0) { | |
| 1018 | + html += '<div class="mxch-no-results"><p>The knowledge base was searched — no documents matched this response.</p></div>'; | |
| 1019 | + $container.html(html); | |
| 1020 | + return; | |
| 1021 | + } | |
| 1022 | + | |
| 1009 | 1023 | const groupedByUrl = {}; |
| 1010 | 1024 | |
| 1011 | 1025 | data.top_matches.forEach(function(match) { |
| 1012 | 1026 | const url = match.source_display || 'Unknown'; |
| @@ -1624,13 +1638,31 @@ | ||
| 1624 | 1638 | }); |
| 1625 | 1639 | $wrap.html(html); |
| 1626 | 1640 | } |
| 1627 | 1641 | |
| 1642 | + // A lead's key is its email address, or "session:<id>" when the capture | |
| 1643 | + // had no address (c0cfaf). Email keys are lowercased exactly as before; | |
| 1644 | + // session keys pass through untouched, because the server resolves them | |
| 1645 | + // back to a real session id and case matters there. Both shapes travel on | |
| 1646 | + // the same wire field the tab has always used, so the selection set, the | |
| 1647 | + // delete call and the export call need no new parameter. | |
| 1648 | + function leadKeyOf(r) { | |
| 1649 | + const email = (r && r.email ? String(r.email) : '').trim(); | |
| 1650 | + if (email) return email.toLowerCase(); | |
| 1651 | + if (r && r.lead_key) return String(r.lead_key); | |
| 1652 | + return 'session:' + (r && r.latest_session_id ? r.latest_session_id : ''); | |
| 1653 | + } | |
| 1654 | + | |
| 1655 | + function normalizeLeadKey(value) { | |
| 1656 | + const key = (value === undefined || value === null) ? '' : String(value); | |
| 1657 | + return key.indexOf('session:') === 0 ? key : key.toLowerCase(); | |
| 1658 | + } | |
| 1659 | + | |
| 1628 | 1660 | function renderLeadsTable(rows) { |
| 1629 | 1661 | const $tbody = $('#mxch-leads-tbody'); |
| 1630 | 1662 | if (!rows || rows.length === 0) { |
| 1631 | 1663 | $tbody.html(` |
| 1632 | - <tr><td colspan="6" class="mxch-leads-empty"> | |
| 1664 | + <tr><td colspan="7" class="mxch-leads-empty"> | |
| 1633 | 1665 | <div class="mxch-leads-empty-wrap"> |
| 1634 | 1666 | <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg> |
| 1635 | 1667 | <p>No leads match the current filters.</p> |
| 1636 | 1668 | </div> |
| @@ -1640,9 +1672,9 @@ | ||
| 1640 | 1672 | } |
| 1641 | 1673 | |
| 1642 | 1674 | let html = ''; |
| 1643 | 1675 | rows.forEach(function(r) { |
| 1644 | - const emailKey = (r.email || '').toLowerCase(); | |
| 1676 | + const emailKey = leadKeyOf(r); | |
| 1645 | 1677 | const isChecked = leadsState.selected.has(emailKey) ? ' checked' : ''; |
| 1646 | 1678 | // Status: 'active' (has conversations), 'chat_deleted' (admin removed the chat), 'orphan' (no chat ever). |
| 1647 | 1679 | const status = r.status || (r.is_orphan ? 'orphan' : 'active'); |
| 1648 | 1680 | const isOrphan = (status === 'orphan'); |
| @@ -1649,12 +1681,21 @@ | ||
| 1649 | 1681 | const isChatDeleted = (status === 'chat_deleted'); |
| 1650 | 1682 | const nameLine = r.name |
| 1651 | 1683 | ? `<span class="mxch-leads-lead-name">${escapeHtmlLeads(r.name)}</span>` |
| 1652 | 1684 | : ''; |
| 1685 | + // A lead captured with no email address (c0cfaf) still has a | |
| 1686 | + // name; show that on the primary line rather than an empty cell, | |
| 1687 | + // and say plainly that there is no address. | |
| 1688 | + const primaryLine = r.email | |
| 1689 | + ? `<span class="mxch-leads-lead-email" title="${escapeHtmlLeads(r.email)}">${escapeHtmlLeads(r.email)}</span>` | |
| 1690 | + : `<span class="mxch-leads-lead-email">${escapeHtmlLeads(r.name || 'Unnamed lead')}</span>`; | |
| 1691 | + const secondaryLine = r.email | |
| 1692 | + ? nameLine | |
| 1693 | + : '<span class="mxch-leads-muted">No email address</span>'; | |
| 1653 | 1694 | const leadCell = ` |
| 1654 | 1695 | <div class="mxch-leads-lead-cell"> |
| 1655 | - <span class="mxch-leads-lead-email" title="${escapeHtmlLeads(r.email)}">${escapeHtmlLeads(r.email)}</span> | |
| 1656 | - ${nameLine} | |
| 1696 | + ${primaryLine} | |
| 1697 | + ${secondaryLine} | |
| 1657 | 1698 | </div>`; |
| 1658 | 1699 | let countCell; |
| 1659 | 1700 | if (isOrphan) { |
| 1660 | 1701 | countCell = `<span class="mxch-leads-pill mxch-leads-pill-orphan">Orphan</span>`; |
| @@ -1666,8 +1707,18 @@ | ||
| 1666 | 1707 | const lastCell = escapeHtmlLeads(r.last_seen_display || (isOrphan ? 'No conversation yet' : '')); |
| 1667 | 1708 | const pageCell = r.top_page_url |
| 1668 | 1709 | ? `<a href="${escapeHtmlLeads(r.top_page_url)}" target="_blank" rel="noopener" class="mxch-leads-page-link" title="${escapeHtmlLeads(r.top_page_url)}">${escapeHtmlLeads(r.top_page_title || r.top_page_url)}</a>` |
| 1669 | 1710 | : '<span class="mxch-leads-muted">—</span>'; |
| 1711 | + // Consent (b062c4): 'yes'/'no' are recorded decisions; '' means the | |
| 1712 | + // capture predates the checkbox — "Not recorded", never "No". | |
| 1713 | + let consentCell; | |
| 1714 | + if (r.consent === 'yes' || r.consent === 'no') { | |
| 1715 | + const consentTitle = (r.consent_at ? 'Recorded ' + r.consent_at : '') + | |
| 1716 | + (r.consent_label ? (r.consent_at ? ' — ' : '') + '"' + r.consent_label + '"' : ''); | |
| 1717 | + consentCell = `<span class="mxch-leads-pill${r.consent === 'yes' ? ' mxch-leads-pill-consent-yes' : ' mxch-leads-pill-consent-no'}" title="${escapeHtmlLeads(consentTitle)}">${r.consent === 'yes' ? 'Yes' : 'No'}</span>`; | |
| 1718 | + } else { | |
| 1719 | + consentCell = '<span class="mxch-leads-muted" title="Captured before the consent checkbox was enabled">Not recorded</span>'; | |
| 1720 | + } | |
| 1670 | 1721 | // View Convo only for active leads (orphans and chat_deleted have no viewable session). |
| 1671 | 1722 | const viewBtn = (status === 'active' && r.latest_session_id) |
| 1672 | 1723 | ? `<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxch-leads-view" data-session-id="${escapeHtmlLeads(r.latest_session_id)}" title="View latest conversation"> |
| 1673 | 1724 | <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> |
| @@ -1673,20 +1724,21 @@ | ||
| 1673 | 1724 | <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> |
| 1674 | 1725 | <span>View convo</span> |
| 1675 | 1726 | </button>` |
| 1676 | 1727 | : ''; |
| 1677 | - const deleteBtn = `<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxch-btn-danger-ghost mxch-leads-delete-row" data-email="${escapeHtmlLeads(r.email)}" title="Delete lead"> | |
| 1728 | + const deleteBtn = `<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxch-btn-danger-ghost mxch-leads-delete-row" data-email="${escapeHtmlLeads(emailKey)}" title="Delete lead"> | |
| 1678 | 1729 | <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> |
| 1679 | 1730 | </button>`; |
| 1680 | 1731 | |
| 1681 | 1732 | const rowStateClass = isOrphan ? ' is-orphan' : (isChatDeleted ? ' is-chat-deleted' : ''); |
| 1682 | 1733 | html += ` |
| 1683 | - <tr class="mxch-leads-row${rowStateClass}" data-email="${escapeHtmlLeads(r.email)}"> | |
| 1734 | + <tr class="mxch-leads-row${rowStateClass}" data-email="${escapeHtmlLeads(emailKey)}"> | |
| 1684 | 1735 | <td class="mxch-leads-col-check"><input type="checkbox" class="mxch-leads-rowcheck"${isChecked}></td> |
| 1685 | 1736 | <td class="mxch-leads-col-lead">${leadCell}</td> |
| 1686 | 1737 | <td class="mxch-leads-col-count">${countCell}</td> |
| 1687 | 1738 | <td class="mxch-leads-col-last">${lastCell}</td> |
| 1688 | 1739 | <td class="mxch-leads-col-page">${pageCell}</td> |
| 1740 | + <td class="mxch-leads-col-consent">${consentCell}</td> | |
| 1689 | 1741 | <td class="mxch-leads-col-actions">${viewBtn}${deleteBtn}</td> |
| 1690 | 1742 | </tr> |
| 1691 | 1743 | `; |
| 1692 | 1744 | }); |
| @@ -1807,9 +1859,9 @@ | ||
| 1807 | 1859 | $('#mxch-leads-select-all').on('change', function() { |
| 1808 | 1860 | const on = $(this).is(':checked'); |
| 1809 | 1861 | $('.mxch-leads-rowcheck').prop('checked', on); |
| 1810 | 1862 | $('.mxch-leads-row').each(function() { |
| 1811 | - const email = ($(this).data('email') || '').toString().toLowerCase(); | |
| 1863 | + const email = normalizeLeadKey($(this).data('email')); | |
| 1812 | 1864 | if (on) { |
| 1813 | 1865 | leadsState.selected.add(email); |
| 1814 | 1866 | } else { |
| 1815 | 1867 | leadsState.selected.delete(email); |
| @@ -1819,9 +1871,9 @@ | ||
| 1819 | 1871 | }); |
| 1820 | 1872 | |
| 1821 | 1873 | // Row checkbox |
| 1822 | 1874 | $leads().on('change', '.mxch-leads-rowcheck', function() { |
| 1823 | - const email = ($(this).closest('.mxch-leads-row').data('email') || '').toString().toLowerCase(); | |
| 1875 | + const email = normalizeLeadKey($(this).closest('.mxch-leads-row').data('email')); | |
| 1824 | 1876 | if ($(this).is(':checked')) { |
| 1825 | 1877 | leadsState.selected.add(email); |
| 1826 | 1878 | } else { |
| 1827 | 1879 | leadsState.selected.delete(email); |
| @@ -1887,9 +1939,9 @@ | ||
| 1887 | 1939 | success: function(response) { |
| 1888 | 1940 | $btn.prop('disabled', false).text('Delete permanently'); |
| 1889 | 1941 | closeLeadsConfirm(); |
| 1890 | 1942 | if (response && response.success) { |
| 1891 | - emails.forEach(function(e) { leadsState.selected.delete(e.toLowerCase()); }); | |
| 1943 | + emails.forEach(function(e) { leadsState.selected.delete(normalizeLeadKey(e)); }); | |
| 1892 | 1944 | loadLeads(leadsState.page); |
| 1893 | 1945 | } else { |
| 1894 | 1946 | alert((response && response.data && response.data.message) || 'Failed to delete leads.'); |
| 1895 | 1947 | } |