').html(html);
messages.push({
index: index,
content: $temp.text().trim()
});
});
// Send translation request
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'mxchat_translate_messages',
session_id: currentSessionId,
target_lang: targetLang,
messages: JSON.stringify(messages),
security: mxchatAdmin.translate_nonce || ''
},
success: function(response) {
if (response.success && response.translations) {
currentTranslationLang = response.language;
applyTranslations(response.translations);
$btn.find('.mxch-translate-text').text('Translate');
} else {
alert(response.error || 'Translation failed. Please try again.');
$btn.find('.mxch-translate-text').text('Translate');
}
},
error: function() {
alert('Translation request failed. Please try again.');
$btn.find('.mxch-translate-text').text('Translate');
},
complete: function() {
$btn.prop('disabled', false);
$btn.find('svg').removeClass('mxch-translate-spinner');
}
});
});
// Show original button click handler
$('#mxch-show-original-btn').on('click', function() {
if (!originalMessages) return;
// Restore original messages
$('#mxch-messages-area .mxch-message-bubble').each(function(index) {
if (originalMessages[index]) {
$(this).html(originalMessages[index]);
$(this).removeClass('translated');
}
});
isTranslated = false;
$(this).hide();
});
// Reset translation state (called when selecting new chat)
function resetTranslationState() {
originalMessages = null;
isTranslated = false;
currentTranslationLang = null;
$('#mxch-show-original-btn').hide();
}
// Make functions available to selectChat
window.resetTranslationState = resetTranslationState;
window.loadSavedTranslation = loadSavedTranslation;
// ==========================================================================
// RAG Context Modal (Sources & Actions Tabs)
// ==========================================================================
function openRagContextModal(messageId) {
const $modal = $('#mxch-rag-modal');
const $loading = $modal.find('.mxch-rag-loading');
const $sourcesContent = $modal.find('.mxch-rag-content');
const $actionsContent = $modal.find('.mxch-actions-content');
// Reset to Sources tab
$modal.find('.mxch-context-tab').removeClass('active');
$modal.find('.mxch-context-tab[data-tab="sources"]').addClass('active');
$('#mxch-tab-sources').show();
$('#mxch-tab-actions').hide();
// Reset badge counts
$('#mxch-sources-count, #mxch-actions-count').hide().text('0');
$modal.fadeIn(200);
$loading.show();
$sourcesContent.html('');
$actionsContent.html('');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'mxchat_get_rag_context',
message_id: messageId
},
success: function(response) {
$loading.hide();
if (response.success && response.data) {
// Render sources tab
renderRagContext(response.data, $sourcesContent);
// Render actions tab
renderActionsContext(response.data, $actionsContent);
// Update badge counts
const sourcesCount = response.data.top_matches ? response.data.top_matches.length : 0;
// 470f68: the Actions tab now carries two mechanisms — tools
// that ran and trigger phrases that scored. Badge counts both.
const toolCallsCount = response.data.tool_calls ? response.data.tool_calls.length : 0;
const actionsCount = (response.data.action_analysis ? response.data.action_analysis.length : 0) + toolCallsCount;
if (sourcesCount > 0) {
$('#mxch-sources-count').text(sourcesCount).show();
}
if (actionsCount > 0) {
$('#mxch-actions-count').text(actionsCount).show();
}
} else {
$sourcesContent.html('
Unable to load document context.
');
$actionsContent.html('
No action data available.
');
}
},
error: function() {
$loading.hide();
$sourcesContent.html('
Error loading context. Please try again.
');
$actionsContent.html('
Error loading context. Please try again.
');
}
});
}
// Tab switching
$(document).on('click', '.mxch-context-tab', function() {
const $tab = $(this);
const tabName = $tab.data('tab');
// Update active tab
$('.mxch-context-tab').removeClass('active');
$tab.addClass('active');
// Show/hide content
$('.mxch-tab-content').hide();
$('#mxch-tab-' + tabName).show();
});
function renderRagContext(data, $container) {
let html = '';
// 67fc92: "the KB was searched and nothing matched" and "nothing was
// recorded for this row" are different facts. Retrieval keys present
// means the search ran and was recorded (top_matches may be empty);
// no retrieval keys means there is simply no record (older rows, or
// turns that never consulted the knowledge base).
const retrievalRecorded = typeof data.total_documents_checked !== 'undefined'
|| (data.top_matches && data.top_matches.length > 0);
if (!retrievalRecorded) {
html += '
No retrieval data was recorded for this response.
';
$container.html(html);
return;
}
// Hybrid keyword boost (38ffa1): rows carry matched_via + fused_rank
// when hybrid retrieval was on for this response. Cosine % stays the
// anchor; the chip explains WHY a low-% row ranked high.
const topMatches = data.top_matches || [];
const hybridOn = topMatches.some(function(m) { return m.matched_via; });
html += '
';
html += '
Knowledge Base: ' + escapeHtml(data.knowledge_base_type || 'WordPress Database') + '
';
html += '
Similarity Threshold: ' + Math.round((data.similarity_threshold || 0.35) * 100) + '%
';
html += '
Documents Checked: ' + (data.total_documents_checked || 0) + '
';
if (hybridOn) {
html += '
Retrieval: Hybrid
';
}
html += '
';
if (topMatches.length === 0) {
html += '
The knowledge base was searched — no documents matched this response.
';
$container.html(html);
return;
}
const groupedByUrl = {};
data.top_matches.forEach(function(match) {
const url = match.source_display || 'Unknown';
if (!groupedByUrl[url]) {
groupedByUrl[url] = {
url: url,
isUrl: url.startsWith('http'),
bestScore: 0,
usedForContext: false,
matchedChunks: [],
bestFusedRank: Infinity,
viaSet: {}
};
}
if (match.similarity_percentage > groupedByUrl[url].bestScore) {
groupedByUrl[url].bestScore = match.similarity_percentage;
}
if (match.used_for_context) {
groupedByUrl[url].usedForContext = true;
}
if (match.fused_rank && match.fused_rank < groupedByUrl[url].bestFusedRank) {
groupedByUrl[url].bestFusedRank = match.fused_rank;
}
if (match.matched_via) {
groupedByUrl[url].viaSet[match.matched_via] = true;
}
groupedByUrl[url].matchedChunks.push({
chunkIndex: match.chunk_index,
score: match.similarity_percentage,
usedForContext: match.used_for_context
});
});
// Hybrid on: order by fused rank (rows without one sort last);
// otherwise by cosine, exactly as before.
const urlGroups = Object.values(groupedByUrl).sort(function(a, b) {
if (hybridOn && a.bestFusedRank !== b.bestFusedRank) {
return a.bestFusedRank - b.bestFusedRank;
}
return b.bestScore - a.bestScore;
});
const usedUrlCount = data.sources_used > 0 ? data.sources_used : urlGroups.filter(g => g.usedForContext).length;
const chunksInfo = data.total_chunks_used > 0 ? data.total_chunks_used + ' chunks sent to AI' : '';
html += '
';
html += '
Retrieved Documents
';
html += '
' + usedUrlCount + ' source' + (usedUrlCount === 1 ? '' : 's') + ' used for response' + (chunksInfo ? ' · ' + chunksInfo : '') + '
';
urlGroups.forEach(function(group) {
const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below';
const statusIcon = group.usedForContext ? '✓' : '✗';
const statusLabel = group.usedForContext ? 'Used' : 'Not Used';
html += '
';
html += '';
html += '
';
html += '
';
});
html += '
';
$container.html(html);
}
// AI Tools trace (470f68): one card per tool EXECUTION for this message.
// Rendered ABOVE the trigger-phrase scores — a tool that actually ran
// outranks a phrase that merely scored.
function renderToolCalls(toolCalls) {
const failed = toolCalls.filter(function(t) { return !t.ok; }).length;
let html = '
';
html += '
Tools Used: ' + toolCalls.length + '
';
if (failed > 0) {
html += '
Failed: ' + failed + '
';
}
html += '
';
html += '
';
html += '
AI Tools
';
html += '
Tools the assistant chose and ran for this message, in order.
';
toolCalls.forEach(function(tool) {
const ok = !!tool.ok;
const cardClass = ok ? 'mxch-rag-match-used' : 'mxch-tool-failed';
const statusClass = ok ? 'status-used' : 'status-failed';
const statusIcon = ok ? '✓' : '✗';
const statusLabel = ok ? 'Ran' : 'Failed';
html += '
';
html += '';
html += '
';
html += '
' + escapeHtml(tool.label || tool.name) + '
';
html += '
Tool: ' + escapeHtml(tool.name) + '
';
html += '
';
if (!ok && tool.error) {
html += '
' + escapeHtml(tool.error) + '
';
}
if (tool.args_redacted === 'sensitive') {
html += '
Arguments not recorded — this tool is marked sensitive.
';
} else if (tool.args_excerpt) {
html += '
Arguments
';
html += '' + escapeHtml(tool.args_excerpt) + '
';
}
html += '
';
});
html += '
';
return html;
}
function renderActionsContext(data, $container) {
let html = '';
const toolCalls = (data.tool_calls && data.tool_calls.length) ? data.tool_calls : [];
const actions = (data.action_analysis && data.action_analysis.length) ? data.action_analysis : [];
// 95d79d's empty state now shows only when NEITHER mechanism produced
// anything — the string itself is unchanged.
if (toolCalls.length === 0 && actions.length === 0) {
html += '
No trigger-phrase analysis for this message.
This panel shows how your Trigger Phrases scored — it stays empty if you have none enabled for this bot, or if the answer came from AI Tools, which don\'t produce similarity scores. It\'s recorded when the answer is generated, so it won\'t appear on older conversations.
';
$container.html(html);
return;
}
if (toolCalls.length > 0) {
html += renderToolCalls(toolCalls);
}
if (actions.length === 0) {
$container.html(html);
return;
}
const triggeredAction = actions.find(a => a.triggered);
const actionsAboveThreshold = actions.filter(a => a.above_threshold).length;
// Summary section
html += '
';
html += '
Actions Evaluated: ' + actions.length + '
';
html += '
Above Threshold: ' + actionsAboveThreshold + '
';
if (triggeredAction) {
html += '
Triggered: ' + escapeHtml(triggeredAction.intent_label) + '
';
}
html += '
';
// Actions list
html += '
';
html += '
Action Scores
';
html += '
Showing all evaluated actions sorted by similarity score
';
actions.forEach(function(action) {
let cardClass = 'mxch-rag-match-below';
let statusIcon = '✗';
let statusLabel = 'Below Threshold';
if (action.triggered) {
cardClass = 'mxch-action-triggered';
statusIcon = '⚡';
statusLabel = 'Triggered';
} else if (action.above_threshold) {
cardClass = 'mxch-rag-match-used';
statusIcon = '✓';
statusLabel = 'Above Threshold';
}
html += '
';
html += '';
html += '
';
html += '
' + escapeHtml(action.intent_label) + '
';
html += '
Callback: ' + escapeHtml(action.callback_function) + '
';
html += '
';
// Score bar visualization
const scoreBarWidth = Math.min(action.similarity_percentage, 100);
const thresholdPos = Math.min(action.threshold_percentage, 100);
html += '
';
html += '
';
html += '
';
html += '
';
html += '
';
});
html += '
';
$container.html(html);
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Close RAG modal
$('.mxch-modal-close').on('click', function() {
$(this).closest('.mxch-modal-overlay').fadeOut(200);
});
$('.mxch-modal-overlay').on('click', function(e) {
if ($(e.target).is('.mxch-modal-overlay')) {
$(this).fadeOut(200);
}
});
$(document).on('keydown', function(e) {
if (e.key === 'Escape') {
$('.mxch-modal-overlay').fadeOut(200);
}
});
// ==========================================================================
// Activity Chart
// ==========================================================================
// Simple chart implementation (no external dependencies)
class SimpleChart {
constructor(canvas, config) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.config = config;
this.padding = { top: 20, right: 20, bottom: 40, left: 50 };
this.render();
}
destroy() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
render() {
const dpr = window.devicePixelRatio || 1;
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width * dpr;
this.canvas.height = rect.height * dpr;
this.ctx.scale(dpr, dpr);
this.canvas.style.width = rect.width + 'px';
this.canvas.style.height = rect.height + 'px';
const width = rect.width - this.padding.left - this.padding.right;
const height = rect.height - this.padding.top - this.padding.bottom;
// Find max value
let maxValue = 0;
this.config.datasets.forEach(dataset => {
const max = Math.max(...dataset.data);
if (max > maxValue) maxValue = max;
});
// Add some padding to max value
maxValue = Math.ceil(maxValue * 1.1);
if (maxValue === 0) maxValue = 10;
// Draw grid lines
this.ctx.strokeStyle = '#e5e7eb';
this.ctx.lineWidth = 1;
const gridLines = 5;
for (let i = 0; i <= gridLines; i++) {
const y = this.padding.top + (height / gridLines) * i;
this.ctx.beginPath();
this.ctx.moveTo(this.padding.left, y);
this.ctx.lineTo(this.padding.left + width, y);
this.ctx.stroke();
// Draw y-axis labels
const value = maxValue - (maxValue / gridLines) * i;
this.ctx.fillStyle = '#6b7280';
this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
this.ctx.textAlign = 'right';
this.ctx.fillText(Math.round(value), this.padding.left - 10, y + 4);
}
// Draw datasets
this.config.datasets.forEach(dataset => {
const points = [];
const xStep = width / (this.config.labels.length - 1 || 1);
dataset.data.forEach((value, index) => {
const x = this.padding.left + (xStep * index);
const y = this.padding.top + height - (value / maxValue * height);
points.push({ x, y, value });
});
// Draw filled area
if (dataset.fill && dataset.backgroundColor) {
this.ctx.fillStyle = dataset.backgroundColor;
this.ctx.beginPath();
this.ctx.moveTo(points[0].x, this.padding.top + height);
points.forEach(point => {
this.ctx.lineTo(point.x, point.y);
});
this.ctx.lineTo(points[points.length - 1].x, this.padding.top + height);
this.ctx.closePath();
this.ctx.fill();
}
// Draw line
this.ctx.strokeStyle = dataset.borderColor;
this.ctx.lineWidth = 3;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.beginPath();
points.forEach((point, index) => {
if (index === 0) {
this.ctx.moveTo(point.x, point.y);
} else {
this.ctx.lineTo(point.x, point.y);
}
});
this.ctx.stroke();
// Draw points
points.forEach(point => {
this.ctx.fillStyle = '#ffffff';
this.ctx.beginPath();
this.ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.strokeStyle = dataset.borderColor;
this.ctx.lineWidth = 2;
this.ctx.stroke();
});
});
// Draw x-axis labels
const xStep = width / (this.config.labels.length - 1 || 1);
this.ctx.fillStyle = '#6b7280';
this.ctx.font = '12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
this.ctx.textAlign = 'center';
this.config.labels.forEach((label, index) => {
const x = this.padding.left + (xStep * index);
this.ctx.fillText(label, x, this.padding.top + height + 20);
});
}
}
// Initialize activity chart
function initActivityChart() {
console.log('[MxChat Chart] initActivityChart called');
const canvas = document.getElementById('mxchat-activity-chart');
console.log('[MxChat Chart] Canvas element:', canvas);
if (!canvas) {
console.log('[MxChat Chart] Canvas not found, aborting');
return;
}
console.log('[MxChat Chart] mxchatChartData exists:', typeof mxchatChartData !== 'undefined');
if (typeof mxchatChartData === 'undefined') {
console.log('[MxChat Chart] mxchatChartData is undefined, aborting');
return;
}
console.log('[MxChat Chart] Raw mxchatChartData:', mxchatChartData);
// Check if chart already exists and destroy it
if (canvas.chartInstance) {
canvas.chartInstance.destroy();
}
const ctx = canvas.getContext('2d');
console.log('[MxChat Chart] Canvas context:', ctx);
console.log('[MxChat Chart] Canvas dimensions:', canvas.getBoundingClientRect());
// Create gradient for chats line
const chatsGradient = ctx.createLinearGradient(0, 0, 0, 300);
chatsGradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
chatsGradient.addColorStop(1, 'rgba(102, 126, 234, 0.05)');
// Create gradient for messages line
const messagesGradient = ctx.createLinearGradient(0, 0, 0, 300);
messagesGradient.addColorStop(0, 'rgba(118, 75, 162, 0.3)');
messagesGradient.addColorStop(1, 'rgba(118, 75, 162, 0.05)');
// Convert wp_localize_script objects to arrays (WordPress converts indexed arrays to objects)
const labels = Object.values(mxchatChartData.labels);
const chatsData = Object.values(mxchatChartData.chats).map(Number);
const messagesData = Object.values(mxchatChartData.messages).map(Number);
console.log('[MxChat Chart] Processed labels:', labels);
console.log('[MxChat Chart] Processed chatsData:', chatsData);
console.log('[MxChat Chart] Processed messagesData:', messagesData);
// Create chart
try {
canvas.chartInstance = new SimpleChart(canvas, {
labels: labels,
datasets: [
{
label: 'Chats',
data: chatsData,
borderColor: '#667eea',
backgroundColor: chatsGradient,
fill: true
},
{
label: 'Messages',
data: messagesData,
borderColor: '#764ba2',
backgroundColor: messagesGradient,
fill: true
}
]
});
console.log('[MxChat Chart] Chart created successfully');
} catch (error) {
console.error('[MxChat Chart] Error creating chart:', error);
}
}
// Initialize chart on page load (dashboard is shown by default)
setTimeout(function() {
initActivityChart();
}, 100);
// Reinitialize chart on window resize
let resizeTimeout;
$(window).on('resize', function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function() {
initActivityChart();
}, 250);
});
// ==========================================================================
// Leads Tab
// ==========================================================================
const leadsState = {
loaded: false,
page: 1,
perPage: 25,
totalPages: 1,
totalCount: 0,
selected: new Set(),
filters: {
search: '',
dateRange: 'all',
status: 'all',
pageUrl: '',
pageTitle: ''
},
pendingDelete: [],
leadsRows: [] // last-rendered rows for quick lookup
};
function $leads() { return $('#leads'); }
// Called after a transcript delete from the All Chats side. Marks the Leads tab
// data stale so the next tab visit re-fetches, and refreshes immediately if the
// Leads tab happens to already be visible.
function invalidateLeadsData() {
leadsState.loaded = false;
if ($('#leads').hasClass('active')) {
loadLeads(1);
}
}
function escapeHtmlLeads(s) {
if (s === null || typeof s === 'undefined') return '';
return String(s)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function leadsFiltersActive() {
const f = leadsState.filters;
return f.search !== '' || f.dateRange !== 'all' || f.status !== 'all' || f.pageUrl !== '';
}
function updateClearFiltersButton() {
if (leadsFiltersActive()) {
$('#mxch-leads-clear-filters').show();
} else {
$('#mxch-leads-clear-filters').hide();
}
}
function setPageFilterChip(url, title) {
leadsState.filters.pageUrl = url || '';
leadsState.filters.pageTitle = title || url || '';
const $chip = $('#mxch-leads-active-page-filter');
if (url) {
$chip.find('.mxch-leads-page-chip-label').text('Page: ' + (title || url));
$chip.show();
} else {
$chip.hide();
}
updateClearFiltersButton();
}
function loadLeads(page) {
if (typeof page === 'number') leadsState.page = page;
const $tbody = $('#mxch-leads-tbody');
$tbody.html('
|
');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'mxchat_fetch_leads',
page: leadsState.page,
per_page: leadsState.perPage,
search: leadsState.filters.search,
date_range: leadsState.filters.dateRange,
status: leadsState.filters.status,
page_url: leadsState.filters.pageUrl
},
success: function(response) {
leadsState.loaded = true;
if (!response || !response.success) {
$tbody.html('
| Error loading leads |
');
return;
}
leadsState.totalPages = response.total_pages || 1;
leadsState.totalCount = response.total_count || 0;
leadsState.leadsRows = response.leads || [];
renderLeadsStats(response.stats || {});
renderLeadsTopPages(response.top_pages || []);
renderLeadsTable(response.leads || []);
renderLeadsCount(response.showing_start, response.showing_end, response.total_count);
renderLeadsPagination(response.page, response.total_pages);
// Nav badge
if (response.stats && typeof response.stats.total_leads === 'number') {
const $badge = $('#mxch-leads-nav-badge');
if (response.stats.total_leads > 0) {
$badge.text(response.stats.total_leads).show();
} else {
$badge.hide();
}
}
},
error: function() {
$tbody.html('
| Error loading leads |
');
}
});
}
function renderLeadsStats(stats) {
$('#mxch-leads-stat-total').text(stats.total_leads || 0);
$('#mxch-leads-stat-new').text(stats.new_this_week || 0);
$('#mxch-leads-stat-avg').text(stats.avg_convos || 0);
const pct = stats.orphan_pct || 0;
$('#mxch-leads-stat-orphan').text(pct + '%');
const orphanCount = stats.orphan_count || 0;
$('#mxch-leads-stat-orphan-sub').text(orphanCount + (orphanCount === 1 ? ' lead captured but never chatted' : ' leads captured but never chatted'));
}
function renderLeadsTopPages(pages) {
const $wrap = $('#mxch-leads-toppages-list');
if (!pages || pages.length === 0) {
$wrap.html('
No page data yet.
');
return;
}
let html = '';
pages.forEach(function(p) {
const isActive = leadsState.filters.pageUrl === p.url ? ' is-active' : '';
html += `
`;
});
$wrap.html(html);
}
function renderLeadsTable(rows) {
const $tbody = $('#mxch-leads-tbody');
if (!rows || rows.length === 0) {
$tbody.html(`
No leads match the current filters.
|
`);
return;
}
let html = '';
rows.forEach(function(r) {
const emailKey = (r.email || '').toLowerCase();
const isChecked = leadsState.selected.has(emailKey) ? ' checked' : '';
// Status: 'active' (has conversations), 'chat_deleted' (admin removed the chat), 'orphan' (no chat ever).
const status = r.status || (r.is_orphan ? 'orphan' : 'active');
const isOrphan = (status === 'orphan');
const isChatDeleted = (status === 'chat_deleted');
const nameLine = r.name
? `
${escapeHtmlLeads(r.name)}`
: '';
const leadCell = `
${escapeHtmlLeads(r.email)}
${nameLine}
`;
let countCell;
if (isOrphan) {
countCell = `
Orphan`;
} else if (isChatDeleted) {
countCell = `
Chat deleted`;
} else {
countCell = `
${r.conversation_count}`;
}
const lastCell = escapeHtmlLeads(r.last_seen_display || (isOrphan ? 'No conversation yet' : ''));
const pageCell = r.top_page_url
? `
${escapeHtmlLeads(r.top_page_title || r.top_page_url)}`
: '
—';
// Consent (b062c4): 'yes'/'no' are recorded decisions; '' means the
// capture predates the checkbox — "Not recorded", never "No".
let consentCell;
if (r.consent === 'yes' || r.consent === 'no') {
const consentTitle = (r.consent_at ? 'Recorded ' + r.consent_at : '') +
(r.consent_label ? (r.consent_at ? ' — ' : '') + '"' + r.consent_label + '"' : '');
consentCell = `
${r.consent === 'yes' ? 'Yes' : 'No'}`;
} else {
consentCell = '
Not recorded';
}
// View Convo only for active leads (orphans and chat_deleted have no viewable session).
const viewBtn = (status === 'active' && r.latest_session_id)
? `
`
: '';
const deleteBtn = `
`;
const rowStateClass = isOrphan ? ' is-orphan' : (isChatDeleted ? ' is-chat-deleted' : '');
html += `
|
${leadCell} |
${countCell} |
${lastCell} |
${pageCell} |
${consentCell} |
${viewBtn}${deleteBtn} |
`;
});
$tbody.html(html);
updateLeadsSelectionUI();
}
function renderLeadsCount(start, end, total) {
if (!total) {
$('#mxch-leads-count').text('0 leads');
} else {
$('#mxch-leads-count').text(start + '-' + end + ' / ' + total + ' leads');
}
}
function renderLeadsPagination(currentPage, totalPages) {
const $c = $('#mxch-leads-pagination');
if (!totalPages || totalPages <= 1) { $c.html(''); return; }
let html = '';
$c.html(html);
}
function updateLeadsSelectionUI() {
const count = leadsState.selected.size;
const $countEl = $('#mxch-leads-selected-count');
const $del = $('#mxch-leads-delete-selected');
if (count > 0) {
$countEl.text(count + ' selected').addClass('has-selection');
$del.prop('disabled', false);
} else {
$countEl.text('0').removeClass('has-selection');
$del.prop('disabled', true);
}
// Selected-scope export menu items
$('#mxch-leads-export-menu button[data-scope="selected"]').prop('disabled', count === 0);
// Select-all checkbox state
const $checks = $('.mxch-leads-rowcheck');
const checked = $checks.filter(':checked').length;
const total = $checks.length;
$('#mxch-leads-select-all').prop('checked', total > 0 && checked === total);
$('#mxch-leads-select-all').prop('indeterminate', checked > 0 && checked < total);
}
// Trigger leads load when switching to the tab (works alongside the main nav handler above).
$('.mxch-nav-link[data-target="leads"], .mxch-mobile-nav-link[data-target="leads"]').on('click', function() {
if (!leadsState.loaded) {
loadLeads(1);
}
});
// Filter: search (debounced)
let leadsSearchTimer;
$('#mxch-leads-search').on('input', function() {
clearTimeout(leadsSearchTimer);
const val = $(this).val();
leadsSearchTimer = setTimeout(function() {
leadsState.filters.search = (val || '').trim();
updateClearFiltersButton();
loadLeads(1);
}, 300);
});
// Filter: date range
$('#mxch-leads-date-range').on('change', function() {
leadsState.filters.dateRange = $(this).val();
updateClearFiltersButton();
loadLeads(1);
});
// Filter: status
$('#mxch-leads-status').on('change', function() {
leadsState.filters.status = $(this).val();
updateClearFiltersButton();
loadLeads(1);
});
// Clear filters
$('#mxch-leads-clear-filters').on('click', function() {
leadsState.filters = { search: '', dateRange: 'all', status: 'all', pageUrl: '', pageTitle: '' };
$('#mxch-leads-search').val('');
$('#mxch-leads-date-range').val('all');
$('#mxch-leads-status').val('all');
setPageFilterChip('', '');
loadLeads(1);
});
// Remove page chip
$leads().on('click', '.mxch-leads-page-chip-remove', function() {
setPageFilterChip('', '');
loadLeads(1);
});
// Top Pages click -> set filter
$leads().on('click', '.mxch-leads-toppage-row', function() {
const url = $(this).data('url') || '';
const title = $(this).data('title') || '';
setPageFilterChip(url, title);
loadLeads(1);
});
// Pagination click
$leads().on('click', '#mxch-leads-pagination .mxch-page-btn', function() {
const p = parseInt($(this).data('page'), 10);
if (p > 0) loadLeads(p);
});
// Select-all
$('#mxch-leads-select-all').on('change', function() {
const on = $(this).is(':checked');
$('.mxch-leads-rowcheck').prop('checked', on);
$('.mxch-leads-row').each(function() {
const email = ($(this).data('email') || '').toString().toLowerCase();
if (on) {
leadsState.selected.add(email);
} else {
leadsState.selected.delete(email);
}
});
updateLeadsSelectionUI();
});
// Row checkbox
$leads().on('change', '.mxch-leads-rowcheck', function() {
const email = ($(this).closest('.mxch-leads-row').data('email') || '').toString().toLowerCase();
if ($(this).is(':checked')) {
leadsState.selected.add(email);
} else {
leadsState.selected.delete(email);
}
updateLeadsSelectionUI();
});
// View convo -> jump to All Chats tab and open the session
$leads().on('click', '.mxch-leads-view', function() {
const sid = $(this).attr('data-session-id');
if (!sid) return;
$('.mxch-nav-link[data-target="all-chats"]').trigger('click');
// selectChat is defined earlier in this closure
if (typeof selectChat === 'function') {
setTimeout(function() { selectChat(sid); }, 30);
}
});
// Row delete -> confirm for one
$leads().on('click', '.mxch-leads-delete-row', function() {
const email = $(this).data('email');
if (!email) return;
openLeadsConfirm([String(email)]);
});
// Bulk delete -> confirm for N
$('#mxch-leads-delete-selected').on('click', function() {
if (leadsState.selected.size === 0) return;
openLeadsConfirm(Array.from(leadsState.selected));
});
function openLeadsConfirm(emails) {
leadsState.pendingDelete = emails;
const count = emails.length;
const msg = count === 1
? 'Delete lead "' + emails[0] + '" and all of their conversations?'
: 'Delete ' + count + ' leads and all of their conversations?';
$('#mxch-leads-confirm-body').text(msg);
$('#mxch-leads-confirm').fadeIn(120);
}
function closeLeadsConfirm() {
$('#mxch-leads-confirm').fadeOut(120);
leadsState.pendingDelete = [];
}
$leads().on('click', '[data-mxch-leads-close]', closeLeadsConfirm);
$('#mxch-leads-confirm-go').on('click', function() {
const emails = leadsState.pendingDelete.slice();
if (!emails.length) { closeLeadsConfirm(); return; }
const $btn = $(this).prop('disabled', true).text('Deleting...');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'mxchat_delete_leads',
security: $('#mxchat_leads_delete_nonce').val(),
emails: emails
},
success: function(response) {
$btn.prop('disabled', false).text('Delete permanently');
closeLeadsConfirm();
if (response && response.success) {
emails.forEach(function(e) { leadsState.selected.delete(e.toLowerCase()); });
loadLeads(leadsState.page);
} else {
alert((response && response.data && response.data.message) || 'Failed to delete leads.');
}
},
error: function() {
$btn.prop('disabled', false).text('Delete permanently');
alert('Network error while deleting.');
}
});
});
// Export dropdown
$('#mxch-leads-export-btn').on('click', function(e) {
e.stopPropagation();
$('#mxch-leads-export-menu').toggleClass('is-open');
});
$(document).on('click', function() {
$('#mxch-leads-export-menu').removeClass('is-open');
});
$('#mxch-leads-export-menu').on('click', function(e) { e.stopPropagation(); });
$('#mxch-leads-export-menu button').on('click', function() {
if ($(this).prop('disabled')) return;
const scope = $(this).data('scope') || 'all';
const fields = $(this).data('fields') || 'email_and_name';
submitLeadsExport(scope, fields);
$('#mxch-leads-export-menu').removeClass('is-open');
});
function submitLeadsExport(scope, fields) {
const $form = $('