| 1 |
/** |
| 2 |
* MxChat Content Generator |
| 3 |
* |
| 4 |
* Handles modal generation flow, full-width preview with iframe scaling, |
| 5 |
* floating chat panel, sidebar navigation, and settings auto-save. |
| 6 |
* |
| 7 |
* @package MxChat |
| 8 |
* @since 3.1.0 |
| 9 |
*/ |
| 10 |
(function($) { |
| 11 |
'use strict'; |
| 12 |
|
| 13 |
var state = { |
| 14 |
postId: null, |
| 15 |
previewUrl: null, |
| 16 |
editUrl: null, |
| 17 |
permalink: null, |
| 18 |
progressKey: null, |
| 19 |
progressPoll: null, |
| 20 |
isGenerating: false, |
| 21 |
isEditing: false, |
| 22 |
chatMessages: [], |
| 23 |
chatOpen: false, |
| 24 |
iframeScale: 1, |
| 25 |
historyLoaded: false, |
| 26 |
historyPage: 1, |
| 27 |
historyLoading: false, |
| 28 |
phraseTimer: null, |
| 29 |
phraseIndex: 0, |
| 30 |
pollStartTime: null, |
| 31 |
customSystemPrompt: '', |
| 32 |
lastProgressTime: null, |
| 33 |
postStatus: null |
| 34 |
}; |
| 35 |
|
| 36 |
var loadingPhrases = [ |
| 37 |
'Consulting our AI overlords...', |
| 38 |
'Teaching pixels to paint...', |
| 39 |
'Brewing a fresh pot of creativity...', |
| 40 |
'Convincing the robots to cooperate...', |
| 41 |
'Warming up the content engines...', |
| 42 |
'Negotiating with the algorithm...', |
| 43 |
'Sprinkling some digital magic...', |
| 44 |
'Asking ChatGPT to hold our beer...', |
| 45 |
'Running it through the vibe check...', |
| 46 |
'Assembling the word wizards...', |
| 47 |
'Translating brain waves to HTML...', |
| 48 |
'Polishing every last pixel...', |
| 49 |
'Taking a quick coffee break...', |
| 50 |
'Man, this is going to be good...', |
| 51 |
'Almost there... probably...', |
| 52 |
'Generating something awesome...', |
| 53 |
'Feeding the hamsters that power our servers...', |
| 54 |
'Doing that thing where we look busy...', |
| 55 |
'Hold tight, genius at work...', |
| 56 |
'Making the internet a little bit cooler...', |
| 57 |
'Crafting content so good it should be illegal...', |
| 58 |
'Our AI designer just said "trust the process"...' |
| 59 |
]; |
| 60 |
|
| 61 |
// ─── Sidebar Navigation ──────────────────────────────────────────── |
| 62 |
|
| 63 |
function initNavigation() { |
| 64 |
$(document).on('click', '.mxch-nav-link[data-target], .mxch-nav-sub-link[data-target]', function(e) { |
| 65 |
e.preventDefault(); |
| 66 |
var target = $(this).data('target'); |
| 67 |
switchSection(target); |
| 68 |
$('.mxch-nav-link, .mxch-nav-sub-link').removeClass('active'); |
| 69 |
$(this).addClass('active'); |
| 70 |
}); |
| 71 |
|
| 72 |
$(document).on('click', '.mxch-mobile-nav-link[data-target]', function(e) { |
| 73 |
e.preventDefault(); |
| 74 |
var target = $(this).data('target'); |
| 75 |
switchSection(target); |
| 76 |
$('.mxch-mobile-nav-link').removeClass('active'); |
| 77 |
$(this).addClass('active'); |
| 78 |
closeMobileMenu(); |
| 79 |
}); |
| 80 |
|
| 81 |
$(document).on('click', '.mxch-mobile-menu-btn', function() { |
| 82 |
$('.mxch-mobile-menu, .mxch-mobile-overlay').addClass('open'); |
| 83 |
}); |
| 84 |
$(document).on('click', '.mxch-mobile-menu-close, .mxch-mobile-overlay', function() { |
| 85 |
closeMobileMenu(); |
| 86 |
}); |
| 87 |
} |
| 88 |
|
| 89 |
function switchSection(target) { |
| 90 |
$('.mxch-section').removeClass('active'); |
| 91 |
$('#' + target).addClass('active'); |
| 92 |
} |
| 93 |
|
| 94 |
function closeMobileMenu() { |
| 95 |
$('.mxch-mobile-menu, .mxch-mobile-overlay').removeClass('open'); |
| 96 |
} |
| 97 |
|
| 98 |
// ─── Inline Form ──────────────────────────────────────────────────── |
| 99 |
|
| 100 |
function initInlineForm() { |
| 101 |
// "Create New" button in toolbar — resets to inline form |
| 102 |
$('#mxch-cg-new-btn').on('click', function() { |
| 103 |
resetToForm(); |
| 104 |
}); |
| 105 |
|
| 106 |
// On initial load: form is already visible, hide toolbar and preview-wrap chrome |
| 107 |
$('#mxch-cg-new-btn').hide(); |
| 108 |
$('.mxch-cg-toolbar').addClass('mxch-cg-toolbar-minimal'); |
| 109 |
$('.mxch-cg-preview-wrap').addClass('mxch-cg-preview-wrap-form'); |
| 110 |
} |
| 111 |
|
| 112 |
function showInlineForm() { |
| 113 |
var $form = $('#mxch-cg-inline-form'); |
| 114 |
$form.removeClass('mxch-cg-form-collapsing').show(); |
| 115 |
|
| 116 |
// Hide preview and loading |
| 117 |
$('#mxch-cg-preview-iframe').hide(); |
| 118 |
$('#mxch-cg-loading-indicator').hide(); |
| 119 |
$('.mxch-cg-preview-wrap').css('height', ''); |
| 120 |
|
| 121 |
// Toolbar: hidden; preview-wrap: transparent |
| 122 |
$('.mxch-cg-toolbar').addClass('mxch-cg-toolbar-minimal'); |
| 123 |
$('.mxch-cg-preview-wrap').addClass('mxch-cg-preview-wrap-form'); |
| 124 |
$('#mxch-cg-new-btn').hide(); |
| 125 |
$('.mxch-cg-toolbar-right').hide(); |
| 126 |
$('#mxch-cg-status-dropdown').hide(); |
| 127 |
$('#mxch-cg-preview-title').text('Content Generator'); |
| 128 |
|
| 129 |
setTimeout(function() { $('#mxch-cg-prompt').focus(); }, 100); |
| 130 |
} |
| 131 |
|
| 132 |
function hideInlineForm() { |
| 133 |
var $form = $('#mxch-cg-inline-form'); |
| 134 |
$form.addClass('mxch-cg-form-collapsing'); |
| 135 |
setTimeout(function() { |
| 136 |
$form.hide().removeClass('mxch-cg-form-collapsing'); |
| 137 |
}, 300); |
| 138 |
|
| 139 |
$('.mxch-cg-toolbar').removeClass('mxch-cg-toolbar-minimal'); |
| 140 |
$('.mxch-cg-preview-wrap').removeClass('mxch-cg-preview-wrap-form'); |
| 141 |
} |
| 142 |
|
| 143 |
function resetToForm() { |
| 144 |
// Reset state |
| 145 |
state.postId = null; |
| 146 |
state.previewUrl = null; |
| 147 |
state.editUrl = null; |
| 148 |
state.permalink = null; |
| 149 |
state.postStatus = null; |
| 150 |
state.chatMessages = []; |
| 151 |
|
| 152 |
closeChatPanel(); |
| 153 |
closeStatusDropdown(); |
| 154 |
resetSeoPanel(); |
| 155 |
$('#mxch-cg-prompt').val(''); |
| 156 |
showInlineForm(); |
| 157 |
} |
| 158 |
|
| 159 |
// ─── Generation Flow ─────────────────────────────────────────────── |
| 160 |
|
| 161 |
function initGeneration() { |
| 162 |
// Show/hide schedule date picker |
| 163 |
$('#mxch-cg-status').on('change', function() { |
| 164 |
if ($(this).val() === 'future') { |
| 165 |
$('.mxch-cg-schedule-wrap').show(); |
| 166 |
if (!$('#mxch-cg-schedule').val()) { |
| 167 |
var tomorrow = new Date(); |
| 168 |
tomorrow.setDate(tomorrow.getDate() + 1); |
| 169 |
tomorrow.setHours(9, 0, 0, 0); |
| 170 |
$('#mxch-cg-schedule').val(tomorrow.toISOString().slice(0, 16)); |
| 171 |
} |
| 172 |
} else { |
| 173 |
$('.mxch-cg-schedule-wrap').hide(); |
| 174 |
} |
| 175 |
}); |
| 176 |
|
| 177 |
// Generate button |
| 178 |
$('#mxch-cg-generate-btn').on('click', function() { |
| 179 |
if (state.isGenerating) return; |
| 180 |
startGeneration(); |
| 181 |
}); |
| 182 |
} |
| 183 |
|
| 184 |
// ── Edit Default Prompt Modal ────────────────────────────── |
| 185 |
function initPromptModal() { |
| 186 |
var $modal = $('#mxch-cg-prompt-modal'); |
| 187 |
var $editor = $('#mxch-cg-system-prompt-editor'); |
| 188 |
var $btn = $('#mxch-cg-edit-prompt-btn'); |
| 189 |
var currentDefault = ''; |
| 190 |
|
| 191 |
function fetchPromptData(callback) { |
| 192 |
var contentType = $('#mxch-cg-type').val() || 'post'; |
| 193 |
$.post(mxchatContent.ajaxUrl, { |
| 194 |
action: 'mxchat_get_default_prompt', |
| 195 |
nonce: mxchatContent.nonce, |
| 196 |
content_type: contentType |
| 197 |
}, function(response) { |
| 198 |
if (response.success) { |
| 199 |
currentDefault = response.data.default_prompt; |
| 200 |
var saved = response.data.saved_prompt || ''; |
| 201 |
state.customSystemPrompt = saved; |
| 202 |
updateButtonState(); |
| 203 |
if (callback) callback(currentDefault, saved); |
| 204 |
} |
| 205 |
}); |
| 206 |
} |
| 207 |
|
| 208 |
function updateButtonState() { |
| 209 |
if (state.customSystemPrompt) { |
| 210 |
$btn.addClass('mxch-cg-prompt-modified'); |
| 211 |
} else { |
| 212 |
$btn.removeClass('mxch-cg-prompt-modified'); |
| 213 |
} |
| 214 |
} |
| 215 |
|
| 216 |
function openModal() { |
| 217 |
fetchPromptData(function(def, saved) { |
| 218 |
$editor.val(saved || def); |
| 219 |
$modal.fadeIn(200); |
| 220 |
}); |
| 221 |
} |
| 222 |
|
| 223 |
function closeModal() { |
| 224 |
$modal.fadeOut(200); |
| 225 |
} |
| 226 |
|
| 227 |
function saveToServer(promptText, callback) { |
| 228 |
var contentType = $('#mxch-cg-type').val() || 'post'; |
| 229 |
$.post(mxchatContent.ajaxUrl, { |
| 230 |
action: 'mxchat_save_custom_prompt', |
| 231 |
nonce: mxchatContent.nonce, |
| 232 |
content_type: contentType, |
| 233 |
custom_prompt: promptText |
| 234 |
}, function(response) { |
| 235 |
if (callback) callback(response.success); |
| 236 |
}); |
| 237 |
} |
| 238 |
|
| 239 |
$btn.on('click', openModal); |
| 240 |
$('#mxch-cg-prompt-modal-close, #mxch-cg-prompt-cancel, .mxch-cg-prompt-modal-overlay').on('click', closeModal); |
| 241 |
|
| 242 |
$('#mxch-cg-prompt-save').on('click', function() { |
| 243 |
var edited = $editor.val().trim(); |
| 244 |
var customValue = (edited && edited !== currentDefault) ? edited : ''; |
| 245 |
state.customSystemPrompt = customValue; |
| 246 |
saveToServer(customValue); |
| 247 |
updateButtonState(); |
| 248 |
closeModal(); |
| 249 |
}); |
| 250 |
|
| 251 |
$('#mxch-cg-prompt-reset').on('click', function() { |
| 252 |
$editor.val(currentDefault); |
| 253 |
state.customSystemPrompt = ''; |
| 254 |
saveToServer(''); |
| 255 |
updateButtonState(); |
| 256 |
}); |
| 257 |
|
| 258 |
// Load saved state for initial content type on page load |
| 259 |
fetchPromptData(); |
| 260 |
|
| 261 |
// When content type changes, load the saved prompt for that type |
| 262 |
$('#mxch-cg-type').on('change', function() { |
| 263 |
fetchPromptData(); |
| 264 |
}); |
| 265 |
} |
| 266 |
|
| 267 |
function startGeneration() { |
| 268 |
var prompt = $('#mxch-cg-prompt').val().trim(); |
| 269 |
if (!prompt) { |
| 270 |
showNotice('Please enter a prompt describing the content you want to generate.', 'error'); |
| 271 |
return; |
| 272 |
} |
| 273 |
|
| 274 |
state.isGenerating = true; |
| 275 |
|
| 276 |
// Immediately hide form and show loading indicator |
| 277 |
$('#mxch-cg-inline-form').hide().removeClass('mxch-cg-form-collapsing'); |
| 278 |
$('.mxch-cg-toolbar').removeClass('mxch-cg-toolbar-minimal'); |
| 279 |
$('.mxch-cg-preview-wrap').removeClass('mxch-cg-preview-wrap-form'); |
| 280 |
$('#mxch-cg-preview-title').text('Generating...'); |
| 281 |
$('#mxch-cg-new-btn').hide(); |
| 282 |
$('.mxch-cg-toolbar-right').hide(); |
| 283 |
$('#mxch-cg-status-dropdown').hide(); |
| 284 |
closeStatusDropdown(); |
| 285 |
closeChatPanel(); |
| 286 |
showLoadingIndicator(); |
| 287 |
|
| 288 |
var data = { |
| 289 |
action: 'mxchat_generate_content', |
| 290 |
nonce: mxchatContent.nonce, |
| 291 |
prompt: prompt, |
| 292 |
content_type: $('#mxch-cg-type').val(), |
| 293 |
post_status: $('#mxch-cg-status').val(), |
| 294 |
schedule_date: $('#mxch-cg-schedule').val() || '', |
| 295 |
layout: $('#mxch-cg-layout').val() || 'fullwidth', |
| 296 |
title_display: $('#mxch-cg-title-display').val() || 'hide', |
| 297 |
template_mode: $('#mxch-cg-template-mode').val() || 'off', |
| 298 |
custom_system_prompt: state.customSystemPrompt || '' |
| 299 |
}; |
| 300 |
|
| 301 |
$.ajax({ |
| 302 |
url: mxchatContent.ajaxUrl, |
| 303 |
type: 'POST', |
| 304 |
data: data, |
| 305 |
timeout: 60000, |
| 306 |
success: function(response) { |
| 307 |
if (response.success && response.data.progress_key) { |
| 308 |
// Async mode — loading indicator already showing |
| 309 |
state.progressKey = response.data.progress_key; |
| 310 |
startProgressPoll(); |
| 311 |
} else if (response.success) { |
| 312 |
// Sync fallback — full result returned directly |
| 313 |
onGenerationSuccess(response.data); |
| 314 |
} else { |
| 315 |
onGenerationError(response.data && response.data.message ? response.data.message : 'Generation failed.'); |
| 316 |
} |
| 317 |
}, |
| 318 |
error: function(xhr, status, error) { |
| 319 |
onGenerationError('Request failed: ' + (error || status)); |
| 320 |
} |
| 321 |
}); |
| 322 |
} |
| 323 |
|
| 324 |
function onGenerationSuccess(data) { |
| 325 |
state.isGenerating = false; |
| 326 |
state.postId = data.post_id; |
| 327 |
state.previewUrl = data.preview_url; |
| 328 |
state.editUrl = data.edit_url; |
| 329 |
state.permalink = data.permalink; |
| 330 |
state.postStatus = data.status; |
| 331 |
state.chatMessages = []; |
| 332 |
state.historyLoaded = false; |
| 333 |
|
| 334 |
// Ensure form and its chrome are hidden |
| 335 |
hideInlineForm(); |
| 336 |
|
| 337 |
// Show success state on loading indicator briefly before showing preview |
| 338 |
var $loadingIndicator = $('#mxch-cg-loading-indicator'); |
| 339 |
if ($loadingIndicator.is(':visible')) { |
| 340 |
stopPhraseRotation(); |
| 341 |
$('#mxch-cg-loading-phrase').text('Your content is ready!'); |
| 342 |
updateLoadingProgress(100, 'Complete!'); |
| 343 |
$loadingIndicator.addClass('mxch-cg-loading-success'); |
| 344 |
|
| 345 |
setTimeout(function() { |
| 346 |
hideLoadingIndicator(); |
| 347 |
finishPreviewLoad(data); |
| 348 |
}, 1200); |
| 349 |
} else { |
| 350 |
// Direct/sync flow — no loading indicator was shown |
| 351 |
finishPreviewLoad(data); |
| 352 |
} |
| 353 |
} |
| 354 |
|
| 355 |
function finishPreviewLoad(data) { |
| 356 |
// Update toolbar title |
| 357 |
$('#mxch-cg-preview-title').text(data.title || 'Preview'); |
| 358 |
|
| 359 |
// Show status dropdown |
| 360 |
var statusLabels = { draft: 'Draft', publish: 'Published', future: 'Scheduled' }; |
| 361 |
var $dropdown = $('#mxch-cg-status-dropdown'); |
| 362 |
var $badge = $('#mxch-cg-status-badge'); |
| 363 |
$badge.find('.mxch-cg-status-badge-text').text(statusLabels[data.status] || data.status); |
| 364 |
$badge.removeClass('mxch-cg-badge-draft mxch-cg-badge-publish mxch-cg-badge-future') |
| 365 |
.addClass('mxch-cg-badge-' + data.status); |
| 366 |
$dropdown.show(); |
| 367 |
closeStatusDropdown(); |
| 368 |
$('.mxch-cg-status-option').removeClass('mxch-cg-status-active'); |
| 369 |
$('.mxch-cg-status-option[data-status="' + data.status + '"]').addClass('mxch-cg-status-active'); |
| 370 |
state.postStatus = data.status; |
| 371 |
|
| 372 |
// Show toolbar actions |
| 373 |
$('.mxch-cg-toolbar-right').show(); |
| 374 |
$('#mxch-cg-view-post').attr('href', data.permalink); |
| 375 |
|
| 376 |
// Show "Create New" button in toolbar |
| 377 |
var $newBtn = $('#mxch-cg-new-btn'); |
| 378 |
$newBtn.find('span').text('Create New'); |
| 379 |
$newBtn.show(); |
| 380 |
|
| 381 |
// Load preview |
| 382 |
loadPreview(data.preview_url); |
| 383 |
|
| 384 |
// Store post ID on the chat panel for add-on access |
| 385 |
$('#mxch-cg-chat').attr('data-post-id', data.post_id); |
| 386 |
|
| 387 |
// Populate image panel with generated images |
| 388 |
populateImagePanel(data.images || []); |
| 389 |
|
| 390 |
// Populate meta panel with SEO data |
| 391 |
populateMetaPanel(data); |
| 392 |
|
| 393 |
// Auto-run SEO analysis |
| 394 |
resetSeoPanel(); |
| 395 |
setTimeout(function() { runSeoAnalysis(); }, 500); |
| 396 |
|
| 397 |
// Pre-populate chat |
| 398 |
$('#mxch-cg-chat-messages').empty(); |
| 399 |
addChatMessage('assistant', 'Content generated! Request edits like "change the heading to..." or "make the background blue".'); |
| 400 |
} |
| 401 |
|
| 402 |
function onGenerationError(message) { |
| 403 |
state.isGenerating = false; |
| 404 |
|
| 405 |
var $btn = $('#mxch-cg-generate-btn'); |
| 406 |
$btn.prop('disabled', false).removeClass('mxch-cg-loading'); |
| 407 |
|
| 408 |
var $loadingIndicator = $('#mxch-cg-loading-indicator'); |
| 409 |
|
| 410 |
if ($loadingIndicator.is(':visible')) { |
| 411 |
// Error while loading indicator is showing (async flow) |
| 412 |
stopPhraseRotation(); |
| 413 |
$loadingIndicator.addClass('mxch-cg-loading-error'); |
| 414 |
$('#mxch-cg-loading-phrase').text('Oops! Something went wrong.'); |
| 415 |
updateLoadingProgress(0, message); |
| 416 |
$('#mxch-cg-loading-progress-fill').css('background', '#ef4444'); |
| 417 |
|
| 418 |
// Add retry and dismiss buttons |
| 419 |
if (!$loadingIndicator.find('.mxch-cg-loading-error-actions').length) { |
| 420 |
var $actions = $( |
| 421 |
'<div class="mxch-cg-loading-error-actions">' + |
| 422 |
'<button type="button" class="mxch-cg-generate-btn" id="mxch-cg-loading-retry">' + |
| 423 |
'<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="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>' + |
| 424 |
' Try Again' + |
| 425 |
'</button>' + |
| 426 |
'<button type="button" class="mxch-cg-action-btn" id="mxch-cg-loading-dismiss">Dismiss</button>' + |
| 427 |
'</div>' |
| 428 |
); |
| 429 |
$loadingIndicator.append($actions); |
| 430 |
|
| 431 |
$actions.find('#mxch-cg-loading-retry').on('click', function() { |
| 432 |
hideLoadingIndicator(); |
| 433 |
showInlineForm(); |
| 434 |
}); |
| 435 |
$actions.find('#mxch-cg-loading-dismiss').on('click', function() { |
| 436 |
hideLoadingIndicator(); |
| 437 |
showInlineForm(); |
| 438 |
}); |
| 439 |
} |
| 440 |
} else { |
| 441 |
// Error while modal is still open (pre-async or sync flow) |
| 442 |
updateProgress(0, 'Error: ' + message); |
| 443 |
$('#mxch-cg-progress .mxch-cg-progress-fill').css('background', '#ef4444'); |
| 444 |
|
| 445 |
setTimeout(function() { |
| 446 |
$('#mxch-cg-progress').fadeOut(300); |
| 447 |
$('#mxch-cg-progress .mxch-cg-progress-fill').css('background', ''); |
| 448 |
}, 4000); |
| 449 |
} |
| 450 |
} |
| 451 |
|
| 452 |
function updateProgress(percent, message) { |
| 453 |
$('#mxch-cg-progress .mxch-cg-progress-fill').css('width', percent + '%'); |
| 454 |
$('#mxch-cg-progress .mxch-cg-progress-text').text(message); |
| 455 |
} |
| 456 |
|
| 457 |
function startProgressPoll() { |
| 458 |
// Clear any existing poll interval (but preserve progressKey — it was just set) |
| 459 |
if (state.progressPoll) { |
| 460 |
clearInterval(state.progressPoll); |
| 461 |
state.progressPoll = null; |
| 462 |
} |
| 463 |
state.pollStartTime = Date.now(); |
| 464 |
state.lastProgressTime = Date.now(); |
| 465 |
state.progressPoll = setInterval(pollProgress, 2500); |
| 466 |
} |
| 467 |
|
| 468 |
function pollProgress() { |
| 469 |
if (!state.progressKey) return; |
| 470 |
|
| 471 |
// Activity-based timeout: if no progress update received for 3 minutes, stop. |
| 472 |
// This allows long generations (many images + long content) to run as long as |
| 473 |
// the backend is still making progress, while still catching truly stalled jobs. |
| 474 |
var inactiveMs = Date.now() - (state.lastProgressTime || state.pollStartTime); |
| 475 |
if (inactiveMs > 180000) { |
| 476 |
stopProgressPoll(); |
| 477 |
onGenerationError('Generation is taking longer than expected. Check your History tab — the post may have been created.'); |
| 478 |
return; |
| 479 |
} |
| 480 |
|
| 481 |
$.ajax({ |
| 482 |
url: mxchatContent.ajaxUrl, |
| 483 |
type: 'POST', |
| 484 |
data: { |
| 485 |
action: 'mxchat_content_progress', |
| 486 |
nonce: mxchatContent.nonce, |
| 487 |
progress_key: state.progressKey |
| 488 |
}, |
| 489 |
timeout: 10000, |
| 490 |
success: function(response) { |
| 491 |
if (!response.success) return; |
| 492 |
|
| 493 |
var d = response.data; |
| 494 |
|
| 495 |
// Any non-waiting response means the backend is alive — reset inactivity timer |
| 496 |
if (d.step && d.step !== 'waiting') { |
| 497 |
state.lastProgressTime = Date.now(); |
| 498 |
} |
| 499 |
|
| 500 |
updateProgress(d.percent || 0, d.message || 'Processing...'); |
| 501 |
updateLoadingProgress(d.percent || 0, d.message || 'Processing...'); |
| 502 |
|
| 503 |
if (d.step === 'done' && d.result) { |
| 504 |
stopProgressPoll(); |
| 505 |
onGenerationSuccess(d.result); |
| 506 |
} else if (d.step === 'error') { |
| 507 |
stopProgressPoll(); |
| 508 |
onGenerationError(d.message || 'Generation failed.'); |
| 509 |
} |
| 510 |
}, |
| 511 |
error: function() { |
| 512 |
// Silently retry on poll failure — don't stop polling |
| 513 |
} |
| 514 |
}); |
| 515 |
} |
| 516 |
|
| 517 |
function stopProgressPoll() { |
| 518 |
if (state.progressPoll) { |
| 519 |
clearInterval(state.progressPoll); |
| 520 |
state.progressPoll = null; |
| 521 |
} |
| 522 |
state.progressKey = null; |
| 523 |
state.pollStartTime = null; |
| 524 |
state.lastProgressTime = null; |
| 525 |
} |
| 526 |
|
| 527 |
// ─── Loading Indicator ──────────────────────────────────────────── |
| 528 |
|
| 529 |
function showLoadingIndicator() { |
| 530 |
$('#mxch-cg-inline-form').hide().removeClass('mxch-cg-form-collapsing'); |
| 531 |
$('#mxch-cg-preview-iframe').hide(); |
| 532 |
$('.mxch-cg-preview-wrap').css('height', ''); |
| 533 |
|
| 534 |
var $loading = $('#mxch-cg-loading-indicator'); |
| 535 |
$loading |
| 536 |
.removeClass('mxch-cg-loading-error mxch-cg-loading-success') |
| 537 |
.show(); |
| 538 |
|
| 539 |
// Reset mini progress |
| 540 |
$('#mxch-cg-loading-progress-fill').css({ 'width': '0%', 'background': '' }); |
| 541 |
$('#mxch-cg-loading-progress-text').text('Starting...'); |
| 542 |
|
| 543 |
// Remove any leftover error actions |
| 544 |
$loading.find('.mxch-cg-loading-error-actions').remove(); |
| 545 |
|
| 546 |
startPhraseRotation(); |
| 547 |
} |
| 548 |
|
| 549 |
function hideLoadingIndicator() { |
| 550 |
stopPhraseRotation(); |
| 551 |
$('#mxch-cg-loading-indicator').hide(); |
| 552 |
} |
| 553 |
|
| 554 |
function startPhraseRotation() { |
| 555 |
stopPhraseRotation(); |
| 556 |
|
| 557 |
// Fisher-Yates shuffle for variety |
| 558 |
var shuffled = loadingPhrases.slice(); |
| 559 |
for (var i = shuffled.length - 1; i > 0; i--) { |
| 560 |
var j = Math.floor(Math.random() * (i + 1)); |
| 561 |
var temp = shuffled[i]; |
| 562 |
shuffled[i] = shuffled[j]; |
| 563 |
shuffled[j] = temp; |
| 564 |
} |
| 565 |
|
| 566 |
state.phraseIndex = 0; |
| 567 |
var $phrase = $('#mxch-cg-loading-phrase'); |
| 568 |
|
| 569 |
// Show first phrase immediately |
| 570 |
$phrase.text(shuffled[0]).removeClass('mxch-cg-phrase-exit mxch-cg-phrase-enter'); |
| 571 |
|
| 572 |
state.phraseTimer = setInterval(function() { |
| 573 |
state.phraseIndex = (state.phraseIndex + 1) % shuffled.length; |
| 574 |
var nextText = shuffled[state.phraseIndex]; |
| 575 |
|
| 576 |
// Fade out (slide up) |
| 577 |
$phrase.addClass('mxch-cg-phrase-exit'); |
| 578 |
|
| 579 |
setTimeout(function() { |
| 580 |
// Swap text and prepare enter state (below) |
| 581 |
$phrase |
| 582 |
.text(nextText) |
| 583 |
.removeClass('mxch-cg-phrase-exit') |
| 584 |
.addClass('mxch-cg-phrase-enter'); |
| 585 |
|
| 586 |
// Force reflow then remove enter class to trigger transition |
| 587 |
$phrase[0].offsetHeight; |
| 588 |
$phrase.removeClass('mxch-cg-phrase-enter'); |
| 589 |
}, 400); // matches CSS transition duration |
| 590 |
|
| 591 |
}, 4500); |
| 592 |
} |
| 593 |
|
| 594 |
function stopPhraseRotation() { |
| 595 |
if (state.phraseTimer) { |
| 596 |
clearInterval(state.phraseTimer); |
| 597 |
state.phraseTimer = null; |
| 598 |
} |
| 599 |
} |
| 600 |
|
| 601 |
function updateLoadingProgress(percent, message) { |
| 602 |
$('#mxch-cg-loading-progress-fill').css('width', percent + '%'); |
| 603 |
if (message) { |
| 604 |
$('#mxch-cg-loading-progress-text').text(message); |
| 605 |
} |
| 606 |
} |
| 607 |
|
| 608 |
// ─── Preview ─────────────────────────────────────────────────────── |
| 609 |
|
| 610 |
function initPreview() { |
| 611 |
// Viewport toggle |
| 612 |
$(document).on('click', '.mxch-cg-viewport-btn', function() { |
| 613 |
var viewport = $(this).data('viewport'); |
| 614 |
$('.mxch-cg-viewport-btn').removeClass('active'); |
| 615 |
$(this).addClass('active'); |
| 616 |
|
| 617 |
var $container = $('#mxch-cg-preview-container'); |
| 618 |
if (viewport === 'mobile') { |
| 619 |
$container.addClass('mxch-cg-viewport-mobile'); |
| 620 |
// Reset iframe to natural size for mobile |
| 621 |
$('#mxch-cg-preview-iframe').css({ |
| 622 |
width: '375px', |
| 623 |
transform: 'none' |
| 624 |
}); |
| 625 |
} else { |
| 626 |
$container.removeClass('mxch-cg-viewport-mobile'); |
| 627 |
scaleIframe(); |
| 628 |
} |
| 629 |
}); |
| 630 |
|
| 631 |
// Recalculate scale on window resize |
| 632 |
$(window).on('resize', function() { |
| 633 |
if (!$('#mxch-cg-preview-container').hasClass('mxch-cg-viewport-mobile')) { |
| 634 |
scaleIframe(); |
| 635 |
} |
| 636 |
}); |
| 637 |
} |
| 638 |
|
| 639 |
function scaleIframe() { |
| 640 |
var $iframe = $('#mxch-cg-preview-iframe'); |
| 641 |
if (!$iframe.is(':visible')) return; |
| 642 |
|
| 643 |
var $wrap = $('.mxch-cg-preview-wrap'); |
| 644 |
var containerWidth = $wrap.innerWidth(); |
| 645 |
var iframeNativeWidth = 1400; |
| 646 |
|
| 647 |
if (containerWidth < iframeNativeWidth) { |
| 648 |
var scale = containerWidth / iframeNativeWidth; |
| 649 |
state.iframeScale = scale; |
| 650 |
$iframe.css({ |
| 651 |
width: iframeNativeWidth + 'px', |
| 652 |
transform: 'scale(' + scale + ')', |
| 653 |
height: (Math.max(700, $(window).height() - 220) / scale) + 'px' |
| 654 |
}); |
| 655 |
// Set container height to match scaled iframe |
| 656 |
$wrap.css('height', ($iframe.outerHeight() * scale) + 'px'); |
| 657 |
} else { |
| 658 |
state.iframeScale = 1; |
| 659 |
$iframe.css({ |
| 660 |
width: '100%', |
| 661 |
transform: 'none', |
| 662 |
height: Math.max(700, $(window).height() - 220) + 'px' |
| 663 |
}); |
| 664 |
$wrap.css('height', ''); |
| 665 |
} |
| 666 |
} |
| 667 |
|
| 668 |
function loadPreview(url) { |
| 669 |
var $iframe = $('#mxch-cg-preview-iframe'); |
| 670 |
var $empty = $('.mxch-cg-preview-empty'); |
| 671 |
|
| 672 |
$empty.hide(); |
| 673 |
$iframe.show(); |
| 674 |
|
| 675 |
// Attach load handler BEFORE setting src to avoid race condition |
| 676 |
$iframe.off('load.scale').on('load.scale', function() { |
| 677 |
scaleIframe(); |
| 678 |
}); |
| 679 |
|
| 680 |
// Add mxchat_preview param so PHP hides admin bar in <head> before render |
| 681 |
var separator = url.indexOf('?') !== -1 ? '&' : '?'; |
| 682 |
$iframe.attr('src', url + separator + 'mxchat_preview=1&_t=' + Date.now()); |
| 683 |
|
| 684 |
// Also scale immediately for initial sizing |
| 685 |
setTimeout(scaleIframe, 100); |
| 686 |
} |
| 687 |
|
| 688 |
function refreshPreview() { |
| 689 |
if (state.previewUrl) { |
| 690 |
loadPreview(state.previewUrl); |
| 691 |
} |
| 692 |
} |
| 693 |
|
| 694 |
function showPreviewEmpty() { |
| 695 |
showInlineForm(); |
| 696 |
} |
| 697 |
|
| 698 |
// ─── Chat Panel ──────────────────────────────────────────────────── |
| 699 |
|
| 700 |
function initChat() { |
| 701 |
// Toggle chat panel |
| 702 |
$('#mxch-cg-chat-toggle').on('click', function() { |
| 703 |
if (state.chatOpen) { |
| 704 |
closeChatPanel(); |
| 705 |
} else { |
| 706 |
openChatPanel(); |
| 707 |
} |
| 708 |
}); |
| 709 |
|
| 710 |
// Close chat panel |
| 711 |
$('#mxch-cg-chat-close').on('click', function() { |
| 712 |
closeChatPanel(); |
| 713 |
}); |
| 714 |
|
| 715 |
// Enable/disable send button + auto-resize textarea |
| 716 |
$('#mxch-cg-chat-input').on('input', function() { |
| 717 |
var hasText = $(this).val().trim().length > 0; |
| 718 |
$('#mxch-cg-chat-send').prop('disabled', !hasText || state.isEditing); |
| 719 |
// Auto-resize |
| 720 |
this.style.height = 'auto'; |
| 721 |
this.style.height = this.scrollHeight + 'px'; |
| 722 |
}); |
| 723 |
|
| 724 |
// Send on Enter, Shift+Enter for newline |
| 725 |
$('#mxch-cg-chat-input').on('keydown', function(e) { |
| 726 |
if (e.key === 'Enter' && !e.shiftKey) { |
| 727 |
e.preventDefault(); |
| 728 |
if (!$(this).val().trim() || state.isEditing) return; |
| 729 |
sendEdit(); |
| 730 |
} |
| 731 |
}); |
| 732 |
|
| 733 |
// Send button click |
| 734 |
$('#mxch-cg-chat-send').on('click', function() { |
| 735 |
if (state.isEditing) return; |
| 736 |
sendEdit(); |
| 737 |
}); |
| 738 |
} |
| 739 |
|
| 740 |
function openChatPanel() { |
| 741 |
state.chatOpen = true; |
| 742 |
// Use flex display for two-column layout |
| 743 |
$('#mxch-cg-chat').css('display', 'flex'); |
| 744 |
$('#mxch-cg-chat-input').focus(); |
| 745 |
scrollChatToBottom(); |
| 746 |
} |
| 747 |
|
| 748 |
function closeChatPanel() { |
| 749 |
state.chatOpen = false; |
| 750 |
$('#mxch-cg-chat').hide(); |
| 751 |
} |
| 752 |
|
| 753 |
function sendEdit() { |
| 754 |
var input = $('#mxch-cg-chat-input').val().trim(); |
| 755 |
if (!input || !state.postId) return; |
| 756 |
|
| 757 |
state.isEditing = true; |
| 758 |
$('#mxch-cg-chat-input').val('').css('height', 'auto'); |
| 759 |
$('#mxch-cg-chat-send').prop('disabled', true); |
| 760 |
|
| 761 |
addChatMessage('user', input); |
| 762 |
|
| 763 |
var $loading = $('<div class="mxch-cg-chat-msg mxch-cg-chat-assistant"><div class="mxch-cg-chat-bubble mxch-cg-chat-loading"><span></span><span></span><span></span></div></div>'); |
| 764 |
$('#mxch-cg-chat-messages').append($loading); |
| 765 |
scrollChatToBottom(); |
| 766 |
|
| 767 |
$.ajax({ |
| 768 |
url: mxchatContent.ajaxUrl, |
| 769 |
type: 'POST', |
| 770 |
data: { |
| 771 |
action: 'mxchat_content_edit', |
| 772 |
nonce: mxchatContent.nonce, |
| 773 |
post_id: state.postId, |
| 774 |
edit_instruction: input |
| 775 |
}, |
| 776 |
timeout: 120000, |
| 777 |
success: function(response) { |
| 778 |
$loading.remove(); |
| 779 |
state.isEditing = false; |
| 780 |
|
| 781 |
if (response.success) { |
| 782 |
addChatMessage('assistant', response.data.message || 'Content updated.'); |
| 783 |
if (response.data.preview_url) { |
| 784 |
state.previewUrl = response.data.preview_url; |
| 785 |
} |
| 786 |
refreshPreview(); |
| 787 |
if (response.data.title) { |
| 788 |
$('#mxch-cg-preview-title').text(response.data.title); |
| 789 |
} |
| 790 |
if (response.data.meta) { |
| 791 |
populateMetaPanel(response.data); |
| 792 |
} |
| 793 |
if (response.data.images) { |
| 794 |
populateImagePanel(response.data.images); |
| 795 |
} |
| 796 |
} else { |
| 797 |
addChatMessage('assistant', 'Error: ' + (response.data && response.data.message ? response.data.message : 'Edit failed.')); |
| 798 |
} |
| 799 |
}, |
| 800 |
error: function() { |
| 801 |
$loading.remove(); |
| 802 |
state.isEditing = false; |
| 803 |
addChatMessage('assistant', 'Error: Request failed. Please try again.'); |
| 804 |
} |
| 805 |
}); |
| 806 |
} |
| 807 |
|
| 808 |
function addChatMessage(role, content) { |
| 809 |
state.chatMessages.push({ role: role, content: content }); |
| 810 |
var roleClass = role === 'user' ? 'mxch-cg-chat-user' : 'mxch-cg-chat-assistant'; |
| 811 |
var $msg = $('<div class="mxch-cg-chat-msg ' + roleClass + '">' + |
| 812 |
'<div class="mxch-cg-chat-bubble">' + escapeHtml(content) + '</div>' + |
| 813 |
'</div>'); |
| 814 |
$('#mxch-cg-chat-messages').append($msg); |
| 815 |
scrollChatToBottom(); |
| 816 |
} |
| 817 |
|
| 818 |
function scrollChatToBottom() { |
| 819 |
var el = document.getElementById('mxch-cg-chat-messages'); |
| 820 |
if (el) el.scrollTop = el.scrollHeight; |
| 821 |
} |
| 822 |
|
| 823 |
// ─── Settings Auto-Save ──────────────────────────────────────────── |
| 824 |
|
| 825 |
function initSettingsAutoSave() { |
| 826 |
// Use event delegation so dynamically-enabled fields (e.g. pro toggles |
| 827 |
// unlocked by add-ons after page load) still trigger saves. |
| 828 |
$('#content-settings').on('change', '[data-field]', function() { |
| 829 |
var $field = $(this); |
| 830 |
var field = $field.data('field'); |
| 831 |
var value; |
| 832 |
|
| 833 |
if ($field.is(':checkbox')) { |
| 834 |
value = $field.is(':checked') ? 'on' : 'off'; |
| 835 |
} else { |
| 836 |
value = $field.val(); |
| 837 |
} |
| 838 |
|
| 839 |
saveContentSetting(field, value, $field); |
| 840 |
|
| 841 |
// Keep seoOptimize prefs in sync without page reload |
| 842 |
var seoMap = { |
| 843 |
seo_optimize_meta_desc: 'meta_description', |
| 844 |
seo_optimize_seo_title: 'seo_title', |
| 845 |
seo_optimize_slug: 'slug', |
| 846 |
seo_optimize_readability: 'readability', |
| 847 |
seo_optimize_internal_links: 'internal_links', |
| 848 |
seo_optimize_img_alt: 'img_alt', |
| 849 |
seo_optimize_featured_img: 'featured_img' |
| 850 |
}; |
| 851 |
if (seoMap[field] && mxchatContent.seoOptimize) { |
| 852 |
mxchatContent.seoOptimize[seoMap[field]] = (value === 'on'); |
| 853 |
} |
| 854 |
}); |
| 855 |
} |
| 856 |
|
| 857 |
function saveContentSetting(field, value, $field) { |
| 858 |
var $label = $field.closest('.mxch-field').find('.mxch-field-label'); |
| 859 |
if (!$label.length) { |
| 860 |
$label = $field.closest('.mxch-field').find('.mxch-toggle-label'); |
| 861 |
} |
| 862 |
|
| 863 |
// Show saving spinner |
| 864 |
if ($label.length) { |
| 865 |
$label.removeClass('mxch-saved').addClass('mxch-saving'); |
| 866 |
} |
| 867 |
|
| 868 |
$.ajax({ |
| 869 |
url: mxchatContent.ajaxUrl, |
| 870 |
type: 'POST', |
| 871 |
data: { |
| 872 |
action: 'mxchat_save_content_setting', |
| 873 |
nonce: mxchatContent.nonce, |
| 874 |
field: field, |
| 875 |
value: value |
| 876 |
}, |
| 877 |
success: function(response) { |
| 878 |
if ($label.length) { |
| 879 |
$label.removeClass('mxch-saving'); |
| 880 |
if (response.success) { |
| 881 |
$label.addClass('mxch-saved'); |
| 882 |
setTimeout(function() { |
| 883 |
$label.removeClass('mxch-saved'); |
| 884 |
}, 1500); |
| 885 |
} |
| 886 |
} |
| 887 |
}, |
| 888 |
error: function(xhr, status, error) { |
| 889 |
if ($label.length) { |
| 890 |
$label.removeClass('mxch-saving'); |
| 891 |
} |
| 892 |
if (window.console) { |
| 893 |
console.warn('MxChat content setting save failed:', field, status, error); |
| 894 |
} |
| 895 |
} |
| 896 |
}); |
| 897 |
} |
| 898 |
|
| 899 |
// ─── Image Panel ──────────────────────────────────────────────────── |
| 900 |
|
| 901 |
function populateImagePanel(images) { |
| 902 |
var $grid = $('#mxch-cg-images-grid'); |
| 903 |
var $empty = $('#mxch-cg-images-empty'); |
| 904 |
var isLocked = $('.mxch-cg-images-col').hasClass('mxch-cg-pro-locked'); |
| 905 |
|
| 906 |
// Clear any previous images (keep the empty state element) |
| 907 |
$grid.find('.mxch-cg-image-thumb').remove(); |
| 908 |
|
| 909 |
if (!images || images.length === 0) { |
| 910 |
$empty.show(); |
| 911 |
return; |
| 912 |
} |
| 913 |
|
| 914 |
$empty.hide(); |
| 915 |
|
| 916 |
$.each(images, function(i, img) { |
| 917 |
var $thumb = $( |
| 918 |
'<div class="mxch-cg-image-thumb">' + |
| 919 |
'<img src="' + escapeAttr(img.thumbnail) + '" alt="Image ' + (i + 1) + '">' + |
| 920 |
'<div class="mxch-cg-image-actions' + (isLocked ? ' mxch-cg-image-actions-locked' : '') + '">' + |
| 921 |
'<button type="button" class="mxch-cg-image-action-btn mxch-cg-image-upload-btn"' + (isLocked ? ' disabled' : '') + ' data-attachment-id="' + (img.attachment_id || '') + '">' + |
| 922 |
'<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>' + |
| 923 |
' Upload' + |
| 924 |
'</button>' + |
| 925 |
'<button type="button" class="mxch-cg-image-action-btn mxch-cg-image-regen-btn"' + (isLocked ? ' disabled' : '') + ' data-attachment-id="' + (img.attachment_id || '') + '">' + |
| 926 |
'<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>' + |
| 927 |
' Regenerate' + |
| 928 |
'</button>' + |
| 929 |
'</div>' + |
| 930 |
(isLocked ? '<div class="mxch-cg-image-lock-badge"><svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> PRO</div>' : '') + |
| 931 |
'</div>' |
| 932 |
); |
| 933 |
$grid.append($thumb); |
| 934 |
}); |
| 935 |
} |
| 936 |
|
| 937 |
function escapeAttr(str) { |
| 938 |
return String(str).replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>'); |
| 939 |
} |
| 940 |
|
| 941 |
// ─── SEO Tab ──────────────────────────────────────────────────── |
| 942 |
|
| 943 |
var seoState = { analyzed: false, analyzing: false, fixing: false, score: null, checks: null }; |
| 944 |
|
| 945 |
function initSeo() { |
| 946 |
$('#mxch-seo-analyze').on('click', function() { |
| 947 |
if (state.postId && !seoState.analyzing) runSeoAnalysis(); |
| 948 |
}); |
| 949 |
$('#mxch-seo-ai-optimize').on('click', function() { |
| 950 |
if (state.postId && !seoState.fixing) runAiOptimize(); |
| 951 |
}); |
| 952 |
// Per-check AI fix buttons (content editor panel only, not dashboard modal) |
| 953 |
$(document).on('click', '.mxch-seo-check-fix:not(.mxch-seod-check-fix-btn)', function(e) { |
| 954 |
e.stopPropagation(); |
| 955 |
var $btn = $(this); |
| 956 |
if ($btn.hasClass('mxch-seo-check-fixing') || !state.postId) return; |
| 957 |
runSeoFixSingle($btn.data('field'), $btn); |
| 958 |
}); |
| 959 |
// Auto-analyze when switching to SEO tab if content exists |
| 960 |
$(document).on('click', '.mxch-cg-left-tab[data-tab="seo"]', function() { |
| 961 |
if (state.postId && !seoState.analyzed && !seoState.analyzing) runSeoAnalysis(); |
| 962 |
}); |
| 963 |
} |
| 964 |
|
| 965 |
function runSeoAnalysis() { |
| 966 |
if (!state.postId) return; |
| 967 |
seoState.analyzing = true; |
| 968 |
$('#mxch-seo-analyze').addClass('mxch-spinning'); |
| 969 |
$.post(ajaxurl, { |
| 970 |
action: 'mxchat_seo_analyze', |
| 971 |
nonce: mxchatContent.nonce, |
| 972 |
post_id: state.postId, |
| 973 |
}).done(function(res) { |
| 974 |
if (res.success) { |
| 975 |
seoState.analyzed = true; |
| 976 |
seoState.score = res.data.score; |
| 977 |
seoState.checks = res.data.checks; |
| 978 |
renderSeoResults(res.data); |
| 979 |
} else { |
| 980 |
renderSeoError(res.data || 'Analysis failed'); |
| 981 |
} |
| 982 |
}).fail(function() { |
| 983 |
renderSeoError('Connection error'); |
| 984 |
}).always(function() { |
| 985 |
seoState.analyzing = false; |
| 986 |
$('#mxch-seo-analyze').removeClass('mxch-spinning'); |
| 987 |
}); |
| 988 |
} |
| 989 |
|
| 990 |
function renderSeoResults(data) { |
| 991 |
var score = data.score, checks = data.checks, summary = data.summary; |
| 992 |
// Score ring |
| 993 |
var offset = 163.36 - (score / 100) * 163.36; |
| 994 |
var $ring = $('#mxch-seo-ring'); |
| 995 |
$ring.css('stroke-dashoffset', offset).removeClass('mxch-seo-good mxch-seo-ok mxch-seo-bad'); |
| 996 |
if (score >= 80) $ring.addClass('mxch-seo-good'); |
| 997 |
else if (score >= 50) $ring.addClass('mxch-seo-ok'); |
| 998 |
else $ring.addClass('mxch-seo-bad'); |
| 999 |
$('#mxch-seo-score').text(score); |
| 1000 |
$('#mxch-seo-score-label').text(score >= 80 ? 'Great' : score >= 60 ? 'Good' : score >= 40 ? 'Needs Work' : 'Poor'); |
| 1001 |
var parts = []; |
| 1002 |
if (summary.pass) parts.push(summary.pass + ' passed'); |
| 1003 |
if (summary.warn) parts.push(summary.warn + ' warnings'); |
| 1004 |
if (summary.fail) parts.push(summary.fail + ' issues'); |
| 1005 |
$('#mxch-seo-score-summary').text(parts.join(' \u00b7 ')); |
| 1006 |
// Checklist |
| 1007 |
var $list = $('#mxch-seo-checklist').empty(); |
| 1008 |
var icons = { |
| 1009 |
pass: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>', |
| 1010 |
warn: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>', |
| 1011 |
fail: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>', |
| 1012 |
}; |
| 1013 |
// Checks that require the Advanced Content Editor add-on to fix |
| 1014 |
var addonChecks = { readability: true, internal_links: true, img_alt: true, featured_img: true }; |
| 1015 |
// Map check key → optimize field name for per-check fix buttons |
| 1016 |
var fixableMap = { meta_desc: 'meta_description', title_length: 'seo_title', slug: 'slug', readability: 'readability', internal_links: 'internal_links', img_alt: 'img_alt', featured_img: 'featured_img' }; |
| 1017 |
var sparkleIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.287 1.288L3 12l5.8 1.9a2 2 0 0 1 1.288 1.287L12 21l1.9-5.8a2 2 0 0 1 1.287-1.288L21 12l-5.8-1.9a2 2 0 0 1-1.288-1.287Z"/></svg>'; |
| 1018 |
var sorted = Object.keys(checks).sort(function(a, b) { |
| 1019 |
var o = { fail: 0, warn: 1, pass: 2 }; |
| 1020 |
return (o[checks[a].status] || 2) - (o[checks[b].status] || 2); |
| 1021 |
}); |
| 1022 |
var last = null; |
| 1023 |
sorted.forEach(function(key) { |
| 1024 |
var c = checks[key]; |
| 1025 |
if (last && last !== 'pass' && c.status === 'pass') { |
| 1026 |
$list.append('<div class="mxch-seo-separator"></div>'); |
| 1027 |
} |
| 1028 |
last = c.status; |
| 1029 |
|
| 1030 |
// Show addon/pro badge for gated checks that aren't passing |
| 1031 |
var badge = ''; |
| 1032 |
if (addonChecks[key] && c.status !== 'pass' && !mxchatContent.hasAdvancedContent) { |
| 1033 |
if (mxchatContent.isActivated) { |
| 1034 |
badge = ' <a href="https://mxchat.ai/advanced-content-editor/" target="_blank" class="mxch-seod-addon-badge">ADD-ON</a>'; |
| 1035 |
} else { |
| 1036 |
badge = ' <a href="https://mxchat.ai/" target="_blank" class="mxch-seod-addon-badge mxch-seod-pro-badge">PRO</a>'; |
| 1037 |
} |
| 1038 |
} |
| 1039 |
|
| 1040 |
// Per-check AI fix button for non-passing, fixable checks |
| 1041 |
var fixBtn = ''; |
| 1042 |
if (c.status !== 'pass' && fixableMap[key]) { |
| 1043 |
var canFix = !addonChecks[key] || mxchatContent.hasAdvancedContent; |
| 1044 |
if (canFix) { |
| 1045 |
fixBtn = '<button type="button" class="mxch-seo-check-fix" data-field="' + fixableMap[key] + '" title="AI Fix">' + sparkleIcon + '</button>'; |
| 1046 |
} |
| 1047 |
} |
| 1048 |
|
| 1049 |
$list.append( |
| 1050 |
'<div class="mxch-seo-check" data-check="' + key + '">' + |
| 1051 |
'<div class="mxch-seo-check-icon mxch-seo-' + c.status + '">' + icons[c.status] + '</div>' + |
| 1052 |
'<div class="mxch-seo-check-content">' + |
| 1053 |
'<span class="mxch-seo-check-label">' + escapeHtml(c.label) + badge + '</span>' + |
| 1054 |
'<span class="mxch-seo-check-detail">' + escapeHtml(c.detail) + '</span>' + |
| 1055 |
'</div>' + |
| 1056 |
fixBtn + |
| 1057 |
'</div>' |
| 1058 |
); |
| 1059 |
}); |
| 1060 |
$('#mxch-seo-actions').toggle(summary.fail > 0 || summary.warn > 0); |
| 1061 |
} |
| 1062 |
|
| 1063 |
function renderSeoError(msg) { |
| 1064 |
$('#mxch-seo-checklist').html('<div class="mxch-seo-empty"><span style="color:#ef4444;">' + escapeHtml(msg) + '</span></div>'); |
| 1065 |
} |
| 1066 |
|
| 1067 |
function runSeoFixSingle(field, $btn) { |
| 1068 |
$btn.addClass('mxch-seo-check-fixing').prop('disabled', true); |
| 1069 |
$.post(ajaxurl, { |
| 1070 |
action: 'mxchat_seo_suggest', |
| 1071 |
nonce: mxchatContent.nonce, |
| 1072 |
post_id: state.postId, |
| 1073 |
field: field, |
| 1074 |
}).done(function(res) { |
| 1075 |
if (res.success) { |
| 1076 |
if (field === 'meta_description') $('#mxch-cg-meta-description').val(res.data.suggestion).trigger('input'); |
| 1077 |
else if (field === 'seo_title') $('#mxch-cg-meta-title').val(res.data.suggestion); |
| 1078 |
$('.mxch-cg-left-tab[data-tab="meta"]').addClass('mxch-cg-tab-flash'); |
| 1079 |
setTimeout(function() { $('.mxch-cg-left-tab[data-tab="meta"]').removeClass('mxch-cg-tab-flash'); }, 2000); |
| 1080 |
} |
| 1081 |
}).always(function() { |
| 1082 |
$btn.removeClass('mxch-seo-check-fixing').prop('disabled', false); |
| 1083 |
runSeoAnalysis(); |
| 1084 |
}); |
| 1085 |
} |
| 1086 |
|
| 1087 |
function runAiOptimize() { |
| 1088 |
if (!state.postId || seoState.fixing) return; |
| 1089 |
seoState.fixing = true; |
| 1090 |
var $btn = $('#mxch-seo-ai-optimize'), origHtml = $btn.html(); |
| 1091 |
$btn.addClass('mxch-seo-fixing').prop('disabled', true) |
| 1092 |
.html('<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="m12 3-1.9 5.8a2 2 0 0 1-1.287 1.288L3 12l5.8 1.9a2 2 0 0 1 1.288 1.287L12 21l1.9-5.8a2 2 0 0 1 1.287-1.288L21 12l-5.8-1.9a2 2 0 0 1-1.288-1.287Z"/></svg> Optimizing\u2026'); |
| 1093 |
var prefs = mxchatContent.seoOptimize || {}; |
| 1094 |
var fields = []; |
| 1095 |
if (seoState.checks) { |
| 1096 |
if (prefs.meta_description !== false && seoState.checks.meta_desc && seoState.checks.meta_desc.status !== 'pass') fields.push('meta_description'); |
| 1097 |
if (prefs.seo_title !== false && seoState.checks.title_length && seoState.checks.title_length.status !== 'pass') fields.push('seo_title'); |
| 1098 |
if (prefs.slug !== false && seoState.checks.slug && seoState.checks.slug.status !== 'pass') fields.push('slug'); |
| 1099 |
// Readability, internal links, images require Advanced Content Editor add-on |
| 1100 |
if (mxchatContent.hasAdvancedContent) { |
| 1101 |
if (prefs.readability !== false && seoState.checks.readability && seoState.checks.readability.status !== 'pass') fields.push('readability'); |
| 1102 |
if (prefs.internal_links !== false && seoState.checks.internal_links && seoState.checks.internal_links.status !== 'pass') fields.push('internal_links'); |
| 1103 |
if (prefs.img_alt !== false && seoState.checks.img_alt && seoState.checks.img_alt.status !== 'pass') fields.push('img_alt'); |
| 1104 |
if (prefs.featured_img !== false && seoState.checks.featured_img && seoState.checks.featured_img.status !== 'pass') fields.push('featured_img'); |
| 1105 |
} |
| 1106 |
} |
| 1107 |
if (!fields.length) fields.push('meta_description'); |
| 1108 |
// Run fields sequentially to avoid race conditions |
| 1109 |
// (multiple optimizers read/write post_content) |
| 1110 |
var idx = 0; |
| 1111 |
function runNext() { |
| 1112 |
if (idx >= fields.length) { |
| 1113 |
seoState.fixing = false; |
| 1114 |
$btn.removeClass('mxch-seo-fixing').prop('disabled', false).html(origHtml); |
| 1115 |
runSeoAnalysis(); |
| 1116 |
return; |
| 1117 |
} |
| 1118 |
var field = fields[idx]; |
| 1119 |
$.post(ajaxurl, { |
| 1120 |
action: 'mxchat_seo_suggest', |
| 1121 |
nonce: mxchatContent.nonce, |
| 1122 |
post_id: state.postId, |
| 1123 |
field: field, |
| 1124 |
}).done(function(res) { |
| 1125 |
if (res.success) { |
| 1126 |
if (field === 'meta_description') $('#mxch-cg-meta-description').val(res.data.suggestion).trigger('input'); |
| 1127 |
else if (field === 'seo_title') $('#mxch-cg-meta-title').val(res.data.suggestion); |
| 1128 |
else if (field === 'excerpt') $('#mxch-cg-meta-excerpt').val(res.data.suggestion); |
| 1129 |
$('.mxch-cg-left-tab[data-tab="meta"]').addClass('mxch-cg-tab-flash'); |
| 1130 |
setTimeout(function() { $('.mxch-cg-left-tab[data-tab="meta"]').removeClass('mxch-cg-tab-flash'); }, 2000); |
| 1131 |
} |
| 1132 |
}).always(function() { |
| 1133 |
idx++; |
| 1134 |
runNext(); |
| 1135 |
}); |
| 1136 |
} |
| 1137 |
runNext(); |
| 1138 |
} |
| 1139 |
|
| 1140 |
function resetSeoPanel() { |
| 1141 |
seoState = { analyzed: false, analyzing: false, fixing: false, score: null, checks: null }; |
| 1142 |
$('#mxch-seo-score').text('\u2014'); |
| 1143 |
$('#mxch-seo-score-label').text('SEO Score'); |
| 1144 |
$('#mxch-seo-score-summary').text('Generate content to analyze'); |
| 1145 |
$('#mxch-seo-ring').css('stroke-dashoffset', '163.36').removeClass('mxch-seo-good mxch-seo-ok mxch-seo-bad'); |
| 1146 |
$('#mxch-seo-checklist').html( |
| 1147 |
'<div class="mxch-seo-empty" id="mxch-seo-empty">' + |
| 1148 |
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' + |
| 1149 |
'<span>SEO analysis will appear here after content is generated</span></div>' |
| 1150 |
); |
| 1151 |
$('#mxch-seo-actions').hide(); |
| 1152 |
} |
| 1153 |
|
| 1154 |
function escapeHtml(s) { |
| 1155 |
var d = document.createElement('div'); |
| 1156 |
d.appendChild(document.createTextNode(s)); |
| 1157 |
return d.innerHTML; |
| 1158 |
} |
| 1159 |
|
| 1160 |
// ─── Left Column Tabs ────────────────────────────────────────────── |
| 1161 |
|
| 1162 |
function initLeftTabs() { |
| 1163 |
$(document).on('click', '.mxch-cg-left-tab', function() { |
| 1164 |
var tab = $(this).data('tab'); |
| 1165 |
$('.mxch-cg-left-tab').removeClass('active'); |
| 1166 |
$(this).addClass('active'); |
| 1167 |
$('.mxch-cg-left-panel').removeClass('active'); |
| 1168 |
$('#mxch-cg-panel-' + tab).addClass('active'); |
| 1169 |
}); |
| 1170 |
|
| 1171 |
// Character counter for meta description |
| 1172 |
$(document).on('input', '#mxch-cg-meta-description', updateCharCount); |
| 1173 |
} |
| 1174 |
|
| 1175 |
function populateMetaPanel(data) { |
| 1176 |
$('#mxch-cg-meta-title').val(data.title || ''); |
| 1177 |
if (data.meta) { |
| 1178 |
$('#mxch-cg-meta-description').val(data.meta.description || ''); |
| 1179 |
$('#mxch-cg-meta-keyword').val(data.meta.keyword || ''); |
| 1180 |
$('#mxch-cg-meta-excerpt').val(data.meta.excerpt || ''); |
| 1181 |
} |
| 1182 |
updateCharCount(); |
| 1183 |
} |
| 1184 |
|
| 1185 |
function updateCharCount() { |
| 1186 |
var len = ($('#mxch-cg-meta-description').val() || '').length; |
| 1187 |
var $counter = $('.mxch-cg-meta-charcount'); |
| 1188 |
$counter.text(len + ' / 160'); |
| 1189 |
if (len > 160) { |
| 1190 |
$counter.addClass('mxch-cg-meta-charcount-over'); |
| 1191 |
} else { |
| 1192 |
$counter.removeClass('mxch-cg-meta-charcount-over'); |
| 1193 |
} |
| 1194 |
} |
| 1195 |
|
| 1196 |
// ─── History Tab ────────────────────────────────────────────────── |
| 1197 |
|
| 1198 |
function initHistory() { |
| 1199 |
$(document).on('click', '[data-target="content-history"]', function() { |
| 1200 |
if (!state.historyLoaded) { |
| 1201 |
loadHistory(1); |
| 1202 |
} |
| 1203 |
}); |
| 1204 |
|
| 1205 |
$(document).on('click', '.mxch-cg-history-page-btn[data-page]', function() { |
| 1206 |
var page = $(this).data('page'); |
| 1207 |
if (page && !state.historyLoading) { |
| 1208 |
loadHistory(page); |
| 1209 |
} |
| 1210 |
}); |
| 1211 |
|
| 1212 |
$(document).on('click', '.mxch-cg-history-edit-btn', function() { |
| 1213 |
var postId = $(this).data('post-id'); |
| 1214 |
if (postId) { |
| 1215 |
loadPostForEdit(postId, $(this)); |
| 1216 |
} |
| 1217 |
}); |
| 1218 |
|
| 1219 |
$(document).on('click', '.mxch-cg-history-delete-btn', function() { |
| 1220 |
var $btn = $(this); |
| 1221 |
var postId = $btn.data('post-id'); |
| 1222 |
var $item = $btn.closest('.mxch-cg-history-item'); |
| 1223 |
var title = $item.find('.mxch-cg-history-title').text(); |
| 1224 |
|
| 1225 |
if (!confirm('Move "' + title + '" to trash?')) return; |
| 1226 |
|
| 1227 |
$btn.prop('disabled', true); |
| 1228 |
$.ajax({ |
| 1229 |
url: mxchatContent.ajaxUrl, |
| 1230 |
type: 'POST', |
| 1231 |
data: { |
| 1232 |
action: 'mxchat_delete_content', |
| 1233 |
nonce: mxchatContent.nonce, |
| 1234 |
post_id: postId |
| 1235 |
}, |
| 1236 |
success: function(response) { |
| 1237 |
if (response.success) { |
| 1238 |
$item.slideUp(200, function() { $(this).remove(); }); |
| 1239 |
} else { |
| 1240 |
alert(response.data.message || 'Failed to delete.'); |
| 1241 |
$btn.prop('disabled', false); |
| 1242 |
} |
| 1243 |
}, |
| 1244 |
error: function() { |
| 1245 |
alert('Request failed. Please try again.'); |
| 1246 |
$btn.prop('disabled', false); |
| 1247 |
} |
| 1248 |
}); |
| 1249 |
}); |
| 1250 |
} |
| 1251 |
|
| 1252 |
function loadHistory(page) { |
| 1253 |
state.historyLoading = true; |
| 1254 |
state.historyPage = page; |
| 1255 |
|
| 1256 |
var $loading = $('#mxch-cg-history-loading'); |
| 1257 |
var $empty = $('#mxch-cg-history-empty'); |
| 1258 |
var $list = $('#mxch-cg-history-list'); |
| 1259 |
var $pag = $('#mxch-cg-history-pagination'); |
| 1260 |
|
| 1261 |
$loading.show(); |
| 1262 |
$empty.hide(); |
| 1263 |
$list.hide(); |
| 1264 |
$pag.hide(); |
| 1265 |
|
| 1266 |
$.ajax({ |
| 1267 |
url: mxchatContent.ajaxUrl, |
| 1268 |
type: 'POST', |
| 1269 |
data: { |
| 1270 |
action: 'mxchat_content_history', |
| 1271 |
nonce: mxchatContent.nonce, |
| 1272 |
page: page |
| 1273 |
}, |
| 1274 |
success: function(response) { |
| 1275 |
state.historyLoading = false; |
| 1276 |
state.historyLoaded = true; |
| 1277 |
$loading.hide(); |
| 1278 |
|
| 1279 |
if (!response.success || !response.data.items.length) { |
| 1280 |
$empty.show(); |
| 1281 |
return; |
| 1282 |
} |
| 1283 |
|
| 1284 |
renderHistoryList(response.data.items); |
| 1285 |
renderHistoryPagination(response.data.current_page, response.data.total_pages); |
| 1286 |
$list.show(); |
| 1287 |
|
| 1288 |
if (response.data.total_pages > 1) { |
| 1289 |
$pag.show(); |
| 1290 |
} |
| 1291 |
}, |
| 1292 |
error: function() { |
| 1293 |
state.historyLoading = false; |
| 1294 |
$loading.hide(); |
| 1295 |
$empty.show(); |
| 1296 |
} |
| 1297 |
}); |
| 1298 |
} |
| 1299 |
|
| 1300 |
function renderHistoryList(items) { |
| 1301 |
var $list = $('#mxch-cg-history-list'); |
| 1302 |
$list.empty(); |
| 1303 |
|
| 1304 |
var statusLabels = { |
| 1305 |
draft: 'Draft', |
| 1306 |
publish: 'Published', |
| 1307 |
future: 'Scheduled', |
| 1308 |
pending: 'Pending', |
| 1309 |
'private': 'Private' |
| 1310 |
}; |
| 1311 |
|
| 1312 |
$.each(items, function(i, item) { |
| 1313 |
var thumbHtml = item.thumbnail |
| 1314 |
? '<img src="' + escapeAttr(item.thumbnail) + '" alt="" class="mxch-cg-history-thumb-img">' |
| 1315 |
: '<div class="mxch-cg-history-thumb-placeholder">' + |
| 1316 |
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>' + |
| 1317 |
'</div>'; |
| 1318 |
|
| 1319 |
var statusClass = 'mxch-cg-badge-' + item.status; |
| 1320 |
var statusText = statusLabels[item.status] || item.status; |
| 1321 |
var typeLabel = item.post_type === 'page' ? 'Page' : 'Post'; |
| 1322 |
|
| 1323 |
var $row = $( |
| 1324 |
'<div class="mxch-cg-history-item">' + |
| 1325 |
'<div class="mxch-cg-history-thumb">' + thumbHtml + '</div>' + |
| 1326 |
'<div class="mxch-cg-history-info">' + |
| 1327 |
'<div class="mxch-cg-history-title">' + escapeHtml(item.title) + '</div>' + |
| 1328 |
'<div class="mxch-cg-history-meta">' + |
| 1329 |
'<span class="mxch-cg-status-badge ' + statusClass + '">' + escapeHtml(statusText) + '</span>' + |
| 1330 |
'<span class="mxch-cg-history-type">' + escapeHtml(typeLabel) + '</span>' + |
| 1331 |
'<span class="mxch-cg-history-date">' + escapeHtml(item.date) + '</span>' + |
| 1332 |
'</div>' + |
| 1333 |
'</div>' + |
| 1334 |
'<div class="mxch-cg-history-actions">' + |
| 1335 |
'<a href="' + escapeAttr(item.permalink) + '" target="_blank" class="mxch-cg-history-action-btn mxch-cg-history-view-btn" title="View">' + |
| 1336 |
'<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="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>' + |
| 1337 |
'</a>' + |
| 1338 |
'<button type="button" class="mxch-cg-history-action-btn mxch-cg-history-edit-btn" data-post-id="' + item.post_id + '" title="Edit">' + |
| 1339 |
'<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="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>' + |
| 1340 |
'</button>' + |
| 1341 |
'<button type="button" class="mxch-cg-history-action-btn mxch-cg-history-delete-btn" data-post-id="' + item.post_id + '" title="Delete">' + |
| 1342 |
'<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>' + |
| 1343 |
'</button>' + |
| 1344 |
'</div>' + |
| 1345 |
'</div>' |
| 1346 |
); |
| 1347 |
|
| 1348 |
$list.append($row); |
| 1349 |
}); |
| 1350 |
} |
| 1351 |
|
| 1352 |
function renderHistoryPagination(current, total) { |
| 1353 |
var $pag = $('#mxch-cg-history-pagination'); |
| 1354 |
$pag.empty(); |
| 1355 |
|
| 1356 |
if (total <= 1) return; |
| 1357 |
|
| 1358 |
var html = ''; |
| 1359 |
|
| 1360 |
if (current > 1) { |
| 1361 |
html += '<button type="button" class="mxch-cg-history-page-btn mxch-cg-history-page-prev" data-page="' + (current - 1) + '">« Prev</button>'; |
| 1362 |
} |
| 1363 |
|
| 1364 |
for (var p = 1; p <= total; p++) { |
| 1365 |
if (p === current) { |
| 1366 |
html += '<span class="mxch-cg-history-page-btn mxch-cg-history-page-current">' + p + '</span>'; |
| 1367 |
} else if (p === 1 || p === total || (p >= current - 1 && p <= current + 1)) { |
| 1368 |
html += '<button type="button" class="mxch-cg-history-page-btn" data-page="' + p + '">' + p + '</button>'; |
| 1369 |
} else if (p === current - 2 || p === current + 2) { |
| 1370 |
html += '<span class="mxch-cg-history-page-ellipsis">…</span>'; |
| 1371 |
} |
| 1372 |
} |
| 1373 |
|
| 1374 |
if (current < total) { |
| 1375 |
html += '<button type="button" class="mxch-cg-history-page-btn mxch-cg-history-page-next" data-page="' + (current + 1) + '">Next »</button>'; |
| 1376 |
} |
| 1377 |
|
| 1378 |
$pag.html(html); |
| 1379 |
} |
| 1380 |
|
| 1381 |
function loadPostForEdit(postId, $btn) { |
| 1382 |
var editBtnHtml = '<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="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg> Edit'; |
| 1383 |
|
| 1384 |
$btn.prop('disabled', true).text('Loading...'); |
| 1385 |
|
| 1386 |
$.ajax({ |
| 1387 |
url: mxchatContent.ajaxUrl, |
| 1388 |
type: 'POST', |
| 1389 |
data: { |
| 1390 |
action: 'mxchat_load_post_for_edit', |
| 1391 |
nonce: mxchatContent.nonce, |
| 1392 |
post_id: postId |
| 1393 |
}, |
| 1394 |
success: function(response) { |
| 1395 |
$btn.prop('disabled', false).html(editBtnHtml); |
| 1396 |
|
| 1397 |
if (response.success) { |
| 1398 |
// Switch to Generate tab |
| 1399 |
switchSection('content-generate'); |
| 1400 |
$('.mxch-nav-link, .mxch-nav-sub-link').removeClass('active'); |
| 1401 |
$('[data-target="content-generate"]').addClass('active'); |
| 1402 |
$('.mxch-mobile-nav-link').removeClass('active'); |
| 1403 |
$('.mxch-mobile-nav-link[data-target="content-generate"]').addClass('active'); |
| 1404 |
|
| 1405 |
// Load post into the same editor state as fresh generation |
| 1406 |
onGenerationSuccess(response.data); |
| 1407 |
} else { |
| 1408 |
alert(response.data && response.data.message ? response.data.message : 'Failed to load post.'); |
| 1409 |
} |
| 1410 |
}, |
| 1411 |
error: function() { |
| 1412 |
$btn.prop('disabled', false).html(editBtnHtml); |
| 1413 |
alert('Request failed. Please try again.'); |
| 1414 |
} |
| 1415 |
}); |
| 1416 |
} |
| 1417 |
|
| 1418 |
// ─── Status Dropdown ────────────────────────────────────────────── |
| 1419 |
|
| 1420 |
function initStatusDropdown() { |
| 1421 |
// Toggle dropdown on badge click |
| 1422 |
$(document).on('click', '#mxch-cg-status-badge', function(e) { |
| 1423 |
e.stopPropagation(); |
| 1424 |
var $dropdown = $('#mxch-cg-status-dropdown'); |
| 1425 |
if ($dropdown.hasClass('mxch-cg-dropdown-open')) { |
| 1426 |
closeStatusDropdown(); |
| 1427 |
} else { |
| 1428 |
openStatusDropdown(); |
| 1429 |
} |
| 1430 |
}); |
| 1431 |
|
| 1432 |
// Close on outside click |
| 1433 |
$(document).on('click', function(e) { |
| 1434 |
if (!$(e.target).closest('#mxch-cg-status-dropdown').length) { |
| 1435 |
closeStatusDropdown(); |
| 1436 |
} |
| 1437 |
}); |
| 1438 |
|
| 1439 |
// Close on Escape |
| 1440 |
$(document).on('keydown', function(e) { |
| 1441 |
if (e.key === 'Escape') { |
| 1442 |
closeStatusDropdown(); |
| 1443 |
} |
| 1444 |
}); |
| 1445 |
|
| 1446 |
// Draft / Publish — immediate status change |
| 1447 |
$(document).on('click', '.mxch-cg-status-option[data-status="draft"], .mxch-cg-status-option[data-status="publish"]', function() { |
| 1448 |
var newStatus = $(this).data('status'); |
| 1449 |
if (newStatus === state.postStatus) { |
| 1450 |
closeStatusDropdown(); |
| 1451 |
return; |
| 1452 |
} |
| 1453 |
updatePostStatus(newStatus, ''); |
| 1454 |
}); |
| 1455 |
|
| 1456 |
// Scheduled — show datetime picker |
| 1457 |
$(document).on('click', '.mxch-cg-status-option[data-status="future"]', function() { |
| 1458 |
var $scheduleRow = $('.mxch-cg-status-schedule-row'); |
| 1459 |
if ($scheduleRow.is(':visible')) { |
| 1460 |
$scheduleRow.hide(); |
| 1461 |
return; |
| 1462 |
} |
| 1463 |
// Pre-fill with tomorrow at 9am if empty |
| 1464 |
var $input = $('#mxch-cg-status-schedule-input'); |
| 1465 |
if (!$input.val()) { |
| 1466 |
var tomorrow = new Date(); |
| 1467 |
tomorrow.setDate(tomorrow.getDate() + 1); |
| 1468 |
tomorrow.setHours(9, 0, 0, 0); |
| 1469 |
$input.val(tomorrow.toISOString().slice(0, 16)); |
| 1470 |
} |
| 1471 |
$scheduleRow.show(); |
| 1472 |
$input.focus(); |
| 1473 |
// Highlight scheduled option |
| 1474 |
$('.mxch-cg-status-option').removeClass('mxch-cg-status-active'); |
| 1475 |
$(this).addClass('mxch-cg-status-active'); |
| 1476 |
}); |
| 1477 |
|
| 1478 |
// Confirm schedule |
| 1479 |
$(document).on('click', '#mxch-cg-status-schedule-confirm', function() { |
| 1480 |
var scheduleDate = $('#mxch-cg-status-schedule-input').val(); |
| 1481 |
if (!scheduleDate) { |
| 1482 |
$('#mxch-cg-status-schedule-input').focus(); |
| 1483 |
return; |
| 1484 |
} |
| 1485 |
// Convert datetime-local value to WordPress format (Y-m-d H:i:s) |
| 1486 |
var wpDate = scheduleDate.replace('T', ' ') + ':00'; |
| 1487 |
updatePostStatus('future', wpDate); |
| 1488 |
}); |
| 1489 |
|
| 1490 |
// Enter key on datetime input confirms |
| 1491 |
$(document).on('keydown', '#mxch-cg-status-schedule-input', function(e) { |
| 1492 |
if (e.key === 'Enter') { |
| 1493 |
e.preventDefault(); |
| 1494 |
$('#mxch-cg-status-schedule-confirm').trigger('click'); |
| 1495 |
} |
| 1496 |
}); |
| 1497 |
} |
| 1498 |
|
| 1499 |
function openStatusDropdown() { |
| 1500 |
var $dropdown = $('#mxch-cg-status-dropdown'); |
| 1501 |
$dropdown.addClass('mxch-cg-dropdown-open'); |
| 1502 |
$('.mxch-cg-status-menu').show(); |
| 1503 |
// Highlight current status |
| 1504 |
$('.mxch-cg-status-option').removeClass('mxch-cg-status-active'); |
| 1505 |
$('.mxch-cg-status-option[data-status="' + state.postStatus + '"]').addClass('mxch-cg-status-active'); |
| 1506 |
// Hide schedule row unless current status is future |
| 1507 |
if (state.postStatus !== 'future') { |
| 1508 |
$('.mxch-cg-status-schedule-row').hide(); |
| 1509 |
} |
| 1510 |
} |
| 1511 |
|
| 1512 |
function closeStatusDropdown() { |
| 1513 |
$('#mxch-cg-status-dropdown').removeClass('mxch-cg-dropdown-open'); |
| 1514 |
$('.mxch-cg-status-menu').hide(); |
| 1515 |
$('.mxch-cg-status-schedule-row').hide(); |
| 1516 |
} |
| 1517 |
|
| 1518 |
function updatePostStatus(newStatus, scheduleDate) { |
| 1519 |
var $badge = $('#mxch-cg-status-badge'); |
| 1520 |
$badge.addClass('mxch-cg-status-updating'); |
| 1521 |
closeStatusDropdown(); |
| 1522 |
|
| 1523 |
$.ajax({ |
| 1524 |
url: mxchatContent.ajaxUrl, |
| 1525 |
type: 'POST', |
| 1526 |
data: { |
| 1527 |
action: 'mxchat_update_post_status', |
| 1528 |
nonce: mxchatContent.nonce, |
| 1529 |
post_id: state.postId, |
| 1530 |
new_status: newStatus, |
| 1531 |
schedule_date: scheduleDate || '' |
| 1532 |
}, |
| 1533 |
success: function(response) { |
| 1534 |
$badge.removeClass('mxch-cg-status-updating'); |
| 1535 |
|
| 1536 |
if (response.success) { |
| 1537 |
var confirmedStatus = response.data.status; |
| 1538 |
state.postStatus = confirmedStatus; |
| 1539 |
|
| 1540 |
// Update badge appearance |
| 1541 |
var statusLabels = { draft: 'Draft', publish: 'Published', future: 'Scheduled' }; |
| 1542 |
$badge.find('.mxch-cg-status-badge-text').text(statusLabels[confirmedStatus] || confirmedStatus); |
| 1543 |
$badge.removeClass('mxch-cg-badge-draft mxch-cg-badge-publish mxch-cg-badge-future') |
| 1544 |
.addClass('mxch-cg-badge-' + confirmedStatus); |
| 1545 |
|
| 1546 |
// Mark history as stale so it reloads on next visit |
| 1547 |
state.historyLoaded = false; |
| 1548 |
|
| 1549 |
// Refresh preview (URL may differ between draft/published) |
| 1550 |
refreshPreview(); |
| 1551 |
} else { |
| 1552 |
alert(response.data && response.data.message ? response.data.message : 'Failed to update status.'); |
| 1553 |
} |
| 1554 |
}, |
| 1555 |
error: function() { |
| 1556 |
$badge.removeClass('mxch-cg-status-updating'); |
| 1557 |
alert('Request failed. Please try again.'); |
| 1558 |
} |
| 1559 |
}); |
| 1560 |
} |
| 1561 |
|
| 1562 |
// ─── Utilities ───────────────────────────────────────────────────── |
| 1563 |
|
| 1564 |
function escapeHtml(str) { |
| 1565 |
var div = document.createElement('div'); |
| 1566 |
div.appendChild(document.createTextNode(str)); |
| 1567 |
return div.innerHTML; |
| 1568 |
} |
| 1569 |
|
| 1570 |
// ─── SEO Dashboard (Site-wide) ───────────────────────────────────── |
| 1571 |
|
| 1572 |
var seodState = { |
| 1573 |
loaded: false, |
| 1574 |
loading: false, |
| 1575 |
page: 1, |
| 1576 |
pages: 1, |
| 1577 |
total: 0, |
| 1578 |
filter: 'all', |
| 1579 |
postType: 'any', |
| 1580 |
search: '', |
| 1581 |
searchTimer: null, |
| 1582 |
scanning: false, |
| 1583 |
expandedId: null, |
| 1584 |
expandAnalyzing: false, |
| 1585 |
sortBy: 'date', |
| 1586 |
sortOrder: 'DESC', |
| 1587 |
}; |
| 1588 |
|
| 1589 |
function initSeoSection() { |
| 1590 |
// Lazy-load: fetch posts when user first visits SEO section |
| 1591 |
$(document).on('click', '.mxch-nav-link[data-target="content-seo"], .mxch-nav-sub-link[data-target="content-seo"], .mxch-mobile-nav-link[data-target="content-seo"]', function() { |
| 1592 |
if (!seodState.loaded && !seodState.loading) { |
| 1593 |
loadSeoPosts(); |
| 1594 |
} |
| 1595 |
}); |
| 1596 |
|
| 1597 |
// Filter pills |
| 1598 |
$(document).on('click', '.mxch-seod-pill', function() { |
| 1599 |
$('.mxch-seod-pill').removeClass('active'); |
| 1600 |
$(this).addClass('active'); |
| 1601 |
seodState.filter = $(this).data('filter'); |
| 1602 |
seodState.page = 1; |
| 1603 |
loadSeoPosts(); |
| 1604 |
}); |
| 1605 |
|
| 1606 |
// Post type dropdown |
| 1607 |
$(document).on('change', '#mxch-seod-post-type', function() { |
| 1608 |
seodState.postType = $(this).val(); |
| 1609 |
seodState.page = 1; |
| 1610 |
loadSeoPosts(); |
| 1611 |
}); |
| 1612 |
|
| 1613 |
// Search with debounce |
| 1614 |
$(document).on('input', '#mxch-seod-search', function() { |
| 1615 |
var val = $(this).val(); |
| 1616 |
clearTimeout(seodState.searchTimer); |
| 1617 |
seodState.searchTimer = setTimeout(function() { |
| 1618 |
seodState.search = val; |
| 1619 |
seodState.page = 1; |
| 1620 |
loadSeoPosts(); |
| 1621 |
}, 400); |
| 1622 |
}); |
| 1623 |
|
| 1624 |
// Pagination |
| 1625 |
$(document).on('click', '.mxch-seod-page-btn', function() { |
| 1626 |
var p = $(this).data('page'); |
| 1627 |
if (p && p !== seodState.page) { |
| 1628 |
seodState.page = p; |
| 1629 |
loadSeoPosts(); |
| 1630 |
} |
| 1631 |
}); |
| 1632 |
|
| 1633 |
// Open detail modal on row click |
| 1634 |
$(document).on('click', '.mxch-seod-row', function() { |
| 1635 |
var postId = $(this).data('post-id'); |
| 1636 |
openSeoModal(postId); |
| 1637 |
}); |
| 1638 |
|
| 1639 |
// Close modal |
| 1640 |
$(document).on('click', '.mxch-seod-modal-overlay', function(e) { |
| 1641 |
if ($(e.target).hasClass('mxch-seod-modal-overlay')) closeSeoModal(); |
| 1642 |
}); |
| 1643 |
$(document).on('click', '.mxch-seod-modal-close', function() { |
| 1644 |
closeSeoModal(); |
| 1645 |
}); |
| 1646 |
$(document).on('keydown', function(e) { |
| 1647 |
if (e.key === 'Escape' && seodState.expandedId) closeSeoModal(); |
| 1648 |
}); |
| 1649 |
|
| 1650 |
// Scan Unscored button |
| 1651 |
$(document).on('click', '#mxch-seod-scan-all', function() { |
| 1652 |
if (!seodState.scanning) bulkSeoScan(); |
| 1653 |
}); |
| 1654 |
// Stop scan button |
| 1655 |
$(document).on('click', '#mxch-seod-scan-stop', function() { |
| 1656 |
seodState.scanAborted = true; |
| 1657 |
$(this).prop('disabled', true).find('span').text('Stopping...'); |
| 1658 |
}); |
| 1659 |
|
| 1660 |
// Sortable column headers (all columns sort server-side) |
| 1661 |
$(document).on('click', '.mxch-seod-header-cell[data-sort]', function() { |
| 1662 |
var col = $(this).data('sort'); |
| 1663 |
if (seodState.sortBy === col) { |
| 1664 |
seodState.sortOrder = seodState.sortOrder === 'DESC' ? 'ASC' : 'DESC'; |
| 1665 |
} else { |
| 1666 |
seodState.sortBy = col; |
| 1667 |
seodState.sortOrder = col === 'title' ? 'ASC' : 'DESC'; |
| 1668 |
} |
| 1669 |
seodState.page = 1; |
| 1670 |
loadSeoPosts(); |
| 1671 |
}); |
| 1672 |
|
| 1673 |
// AI Optimize within detail modal |
| 1674 |
$(document).on('click', '.mxch-seod-optimize-btn', function(e) { |
| 1675 |
e.stopPropagation(); |
| 1676 |
var postId = $(this).closest('.mxch-seod-detail').data('post-id'); |
| 1677 |
runSeodOptimize(postId, $(this)); |
| 1678 |
}); |
| 1679 |
|
| 1680 |
// Per-check AI fix buttons in detail modal |
| 1681 |
$(document).on('click', '.mxch-seod-check-fix-btn', function(e) { |
| 1682 |
e.stopPropagation(); |
| 1683 |
var $btn = $(this); |
| 1684 |
if ($btn.hasClass('mxch-seo-check-fixing')) return; |
| 1685 |
var field = $btn.data('field'); |
| 1686 |
var postId = $btn.data('post-id'); |
| 1687 |
$btn.addClass('mxch-seo-check-fixing').prop('disabled', true); |
| 1688 |
$.post(ajaxurl, { |
| 1689 |
action: 'mxchat_seo_suggest', |
| 1690 |
nonce: mxchatContent.nonce, |
| 1691 |
post_id: postId, |
| 1692 |
field: field, |
| 1693 |
}).always(function() { |
| 1694 |
$btn.removeClass('mxch-seo-check-fixing').prop('disabled', false); |
| 1695 |
if (seodState.expandedId === postId) { |
| 1696 |
openSeoModal(postId); |
| 1697 |
} |
| 1698 |
}); |
| 1699 |
}); |
| 1700 |
|
| 1701 |
// Checkbox: prevent row click when clicking checkbox |
| 1702 |
$(document).on('click', '.mxch-seod-cell-check, .mxch-seod-header-check', function(e) { |
| 1703 |
e.stopPropagation(); |
| 1704 |
}); |
| 1705 |
|
| 1706 |
// Select all checkbox |
| 1707 |
$(document).on('change', '.mxch-seod-check-all', function() { |
| 1708 |
var checked = $(this).prop('checked'); |
| 1709 |
$('.mxch-seod-row-check').prop('checked', checked); |
| 1710 |
updateOptimizeSelectedBtn(); |
| 1711 |
}); |
| 1712 |
|
| 1713 |
// Individual row checkbox |
| 1714 |
$(document).on('change', '.mxch-seod-row-check', function() { |
| 1715 |
var allChecked = $('.mxch-seod-row-check').length === $('.mxch-seod-row-check:checked').length; |
| 1716 |
$('.mxch-seod-check-all').prop('checked', allChecked); |
| 1717 |
updateOptimizeSelectedBtn(); |
| 1718 |
}); |
| 1719 |
} |
| 1720 |
|
| 1721 |
function updateOptimizeSelectedBtn() { |
| 1722 |
var count = $('.mxch-seod-row-check:checked').length; |
| 1723 |
var $btn = $('#mxch-seod-optimize-selected'); |
| 1724 |
var $note = $('#mxch-seod-bulk-note'); |
| 1725 |
var isLocked = $btn.hasClass('mxch-seod-bulk-locked'); |
| 1726 |
if (count > 0) { |
| 1727 |
$btn.find('span').first().text(isLocked ? 'Bulk Optimize' : 'Optimize Selected (' + count + ')'); |
| 1728 |
$btn.show(); |
| 1729 |
if (isLocked) $note.show(); |
| 1730 |
} else { |
| 1731 |
$btn.hide(); |
| 1732 |
$note.hide(); |
| 1733 |
} |
| 1734 |
} |
| 1735 |
|
| 1736 |
function loadSeoPosts() { |
| 1737 |
seodState.loading = true; |
| 1738 |
$('#mxch-seod-loading').show(); |
| 1739 |
$('#mxch-seod-empty').hide(); |
| 1740 |
$('#mxch-seod-table').empty(); |
| 1741 |
|
| 1742 |
$.post(ajaxurl, { |
| 1743 |
action: 'mxchat_seo_list_posts', |
| 1744 |
nonce: mxchatContent.nonce, |
| 1745 |
page: seodState.page, |
| 1746 |
post_type: seodState.postType, |
| 1747 |
filter: seodState.filter, |
| 1748 |
search: seodState.search, |
| 1749 |
sort_by: seodState.sortBy, |
| 1750 |
sort_order: seodState.sortOrder, |
| 1751 |
}).done(function(res) { |
| 1752 |
if (res.success) { |
| 1753 |
seodState.loaded = true; |
| 1754 |
seodState.page = res.data.page; |
| 1755 |
seodState.pages = res.data.pages; |
| 1756 |
seodState.total = res.data.total; |
| 1757 |
renderSeodTable(res.data.posts); |
| 1758 |
renderSeodPagination(); |
| 1759 |
updateSeodScanBtn(res.data.unscored_count); |
| 1760 |
$('#mxch-seod-footer').show(); |
| 1761 |
if (!res.data.posts.length) { |
| 1762 |
$('#mxch-seod-empty').show(); |
| 1763 |
} |
| 1764 |
} |
| 1765 |
}).fail(function() { |
| 1766 |
$('#mxch-seod-table').html('<div class="mxch-seod-error">Failed to load posts. Please try again.</div>'); |
| 1767 |
}).always(function() { |
| 1768 |
seodState.loading = false; |
| 1769 |
$('#mxch-seod-loading').hide(); |
| 1770 |
}); |
| 1771 |
} |
| 1772 |
|
| 1773 |
function seodFormatNum(n) { |
| 1774 |
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; |
| 1775 |
if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; |
| 1776 |
return n; |
| 1777 |
} |
| 1778 |
|
| 1779 |
function seodSortArrow(col) { |
| 1780 |
if (seodState.sortBy !== col) return ''; |
| 1781 |
return ' <span class="mxch-seod-sort-arrow">' + (seodState.sortOrder === 'ASC' ? '▲' : '▼') + '</span>'; |
| 1782 |
} |
| 1783 |
|
| 1784 |
function renderSeodTable(posts) { |
| 1785 |
var $table = $('#mxch-seod-table'); |
| 1786 |
$table.empty(); |
| 1787 |
seodState.expandedId = null; |
| 1788 |
|
| 1789 |
// Column headers |
| 1790 |
var activeClass = function(col) { return seodState.sortBy === col ? ' mxch-seod-header-active' : ''; }; |
| 1791 |
$table.append( |
| 1792 |
'<div class="mxch-seod-header">' + |
| 1793 |
'<div class="mxch-seod-header-cell mxch-seod-header-check"><input type="checkbox" class="mxch-seod-check-all" title="Select all"></div>' + |
| 1794 |
'<div class="mxch-seod-header-cell mxch-seod-header-title' + activeClass('title') + '" data-sort="title">Title' + seodSortArrow('title') + '</div>' + |
| 1795 |
'<div class="mxch-seod-header-cell mxch-seod-header-date' + activeClass('date') + '" data-sort="date">Date' + seodSortArrow('date') + '</div>' + |
| 1796 |
'<div class="mxch-seod-header-cell mxch-seod-header-score' + activeClass('score') + '" data-sort="score">Score' + seodSortArrow('score') + '</div>' + |
| 1797 |
'<div class="mxch-seod-header-cell mxch-seod-header-clicks' + (!mxchatContent.hasGSC ? ' mxch-seod-header-locked' : '') + '"' + (mxchatContent.hasGSC ? ' data-sort="clicks"' : '') + '>Clicks' + (mxchatContent.hasGSC ? seodSortArrow('clicks') : ' <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>') + '</div>' + |
| 1798 |
'<div class="mxch-seod-header-cell mxch-seod-header-impressions' + (!mxchatContent.hasGSC ? ' mxch-seod-header-locked' : '') + '"' + (mxchatContent.hasGSC ? ' data-sort="impressions"' : '') + '>Impr.' + (mxchatContent.hasGSC ? seodSortArrow('impressions') : ' <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>') + '</div>' + |
| 1799 |
'</div>' |
| 1800 |
); |
| 1801 |
|
| 1802 |
posts.forEach(function(p) { |
| 1803 |
var scoreHtml; |
| 1804 |
if (p.score !== null) { |
| 1805 |
var cls = p.score >= 80 ? 'mxch-seod-good' : p.score >= 50 ? 'mxch-seod-ok' : 'mxch-seod-bad'; |
| 1806 |
scoreHtml = '<div class="mxch-seod-score-badge ' + cls + '">' + p.score + '</div>'; |
| 1807 |
} else { |
| 1808 |
scoreHtml = '<div class="mxch-seod-score-badge mxch-seod-unscored">—</div>'; |
| 1809 |
} |
| 1810 |
|
| 1811 |
var typeLabel = p.type.charAt(0).toUpperCase() + p.type.slice(1); |
| 1812 |
|
| 1813 |
$table.append( |
| 1814 |
'<div class="mxch-seod-row" data-post-id="' + p.id + '" data-score="' + (p.score !== null ? p.score : -1) + '" data-permalink="' + escapeAttr(p.permalink) + '">' + |
| 1815 |
'<div class="mxch-seod-row-main">' + |
| 1816 |
'<div class="mxch-seod-cell mxch-seod-cell-check"><input type="checkbox" class="mxch-seod-row-check" data-post-id="' + p.id + '"></div>' + |
| 1817 |
'<div class="mxch-seod-cell mxch-seod-cell-title">' + |
| 1818 |
'<span class="mxch-seod-title">' + escapeHtml(p.title) + '</span>' + |
| 1819 |
'<span class="mxch-seod-meta">' + typeLabel + '</span>' + |
| 1820 |
'</div>' + |
| 1821 |
'<div class="mxch-seod-cell mxch-seod-cell-date">' + |
| 1822 |
'<span class="mxch-seod-date">' + escapeHtml(p.date) + '</span>' + |
| 1823 |
'</div>' + |
| 1824 |
'<div class="mxch-seod-cell mxch-seod-cell-score">' + scoreHtml + '</div>' + |
| 1825 |
'<div class="mxch-seod-cell mxch-seod-cell-clicks' + (!mxchatContent.hasGSC ? ' mxch-seod-cell-locked' : '') + '" data-clicks="' + (p.clicks !== null ? p.clicks : 0) + '">' + (!mxchatContent.hasGSC ? '—' : (p.clicks !== null ? p.clicks : '—')) + '</div>' + |
| 1826 |
'<div class="mxch-seod-cell mxch-seod-cell-impressions' + (!mxchatContent.hasGSC ? ' mxch-seod-cell-locked' : '') + '" data-impressions="' + (p.impressions !== null ? p.impressions : 0) + '">' + (!mxchatContent.hasGSC ? '—' : (p.impressions !== null ? seodFormatNum(p.impressions) : '—')) + '</div>' + |
| 1827 |
'</div>' + |
| 1828 |
'</div>' |
| 1829 |
); |
| 1830 |
}); |
| 1831 |
} |
| 1832 |
|
| 1833 |
function renderSeodPagination() { |
| 1834 |
var $pag = $('#mxch-seod-pagination'); |
| 1835 |
$pag.empty(); |
| 1836 |
|
| 1837 |
if (seodState.pages <= 1) return; |
| 1838 |
|
| 1839 |
var p = seodState.page, total = seodState.pages; |
| 1840 |
|
| 1841 |
if (p > 1) { |
| 1842 |
$pag.append('<button type="button" class="mxch-seod-page-btn" data-page="' + (p - 1) + '">← Prev</button>'); |
| 1843 |
} |
| 1844 |
$pag.append('<span class="mxch-seod-page-info">Page ' + p + ' of ' + total + '</span>'); |
| 1845 |
if (p < total) { |
| 1846 |
$pag.append('<button type="button" class="mxch-seod-page-btn" data-page="' + (p + 1) + '">Next →</button>'); |
| 1847 |
} |
| 1848 |
} |
| 1849 |
|
| 1850 |
function updateSeodScanBtn(unscoredCount) { |
| 1851 |
var $btn = $('#mxch-seod-scan-all'); |
| 1852 |
if (unscoredCount > 0) { |
| 1853 |
$btn.show().text('Scan Unscored (' + unscoredCount + ')'); |
| 1854 |
} else { |
| 1855 |
$btn.hide(); |
| 1856 |
} |
| 1857 |
$('#mxch-seod-scan-status').text(''); |
| 1858 |
} |
| 1859 |
|
| 1860 |
function openSeoModal(postId) { |
| 1861 |
closeSeoModal(); // close any existing modal |
| 1862 |
seodState.expandedId = postId; |
| 1863 |
|
| 1864 |
var $row = $('.mxch-seod-row[data-post-id="' + postId + '"]'); |
| 1865 |
var title = $row.find('.mxch-seod-title').text() || 'Post #' + postId; |
| 1866 |
var permalink = $row.attr('data-permalink') || ''; |
| 1867 |
|
| 1868 |
var $overlay = $( |
| 1869 |
'<div class="mxch-seod-modal-overlay">' + |
| 1870 |
'<div class="mxch-seod-modal">' + |
| 1871 |
'<div class="mxch-seod-modal-header">' + |
| 1872 |
'<h3 class="mxch-seod-modal-title">' + escapeHtml(title) + '</h3>' + |
| 1873 |
(permalink ? '<a href="' + escapeAttr(permalink) + '" target="_blank" class="mxch-seod-modal-view-page" title="View Page">' + |
| 1874 |
'<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="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>' + |
| 1875 |
' View Page</a>' : '') + |
| 1876 |
'<button type="button" class="mxch-seod-modal-close" title="Close">' + |
| 1877 |
'<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>' + |
| 1878 |
'</button>' + |
| 1879 |
'</div>' + |
| 1880 |
'<div class="mxch-seod-modal-body">' + |
| 1881 |
'<div class="mxch-seod-detail" data-post-id="' + postId + '">' + |
| 1882 |
'<div class="mxch-seod-detail-loading">' + |
| 1883 |
'<div class="mxch-seod-spinner"></div>' + |
| 1884 |
'<span>Analyzing…</span>' + |
| 1885 |
'</div>' + |
| 1886 |
'</div>' + |
| 1887 |
'</div>' + |
| 1888 |
'</div>' + |
| 1889 |
'</div>' |
| 1890 |
); |
| 1891 |
|
| 1892 |
$('body').append($overlay); |
| 1893 |
// Trigger reflow then add visible class for animation |
| 1894 |
$overlay[0].offsetHeight; |
| 1895 |
$overlay.addClass('mxch-seod-modal-visible'); |
| 1896 |
|
| 1897 |
var $detail = $overlay.find('.mxch-seod-detail'); |
| 1898 |
|
| 1899 |
// Run analysis |
| 1900 |
seodState.expandAnalyzing = true; |
| 1901 |
$.post(ajaxurl, { |
| 1902 |
action: 'mxchat_seo_analyze', |
| 1903 |
nonce: mxchatContent.nonce, |
| 1904 |
post_id: postId, |
| 1905 |
}).done(function(res) { |
| 1906 |
if (res.success) { |
| 1907 |
renderSeodDetail($detail, res.data, postId); |
| 1908 |
// Update the row's score badge in the table too |
| 1909 |
var score = res.data.score; |
| 1910 |
var cls = score >= 80 ? 'mxch-seod-good' : score >= 50 ? 'mxch-seod-ok' : 'mxch-seod-bad'; |
| 1911 |
$row.find('.mxch-seod-score-badge') |
| 1912 |
.removeClass('mxch-seod-good mxch-seod-ok mxch-seod-bad mxch-seod-unscored') |
| 1913 |
.addClass(cls).text(score); |
| 1914 |
} else { |
| 1915 |
$detail.html('<div class="mxch-seod-detail-error">Analysis failed. Please try again.</div>'); |
| 1916 |
} |
| 1917 |
}).fail(function() { |
| 1918 |
$detail.html('<div class="mxch-seod-detail-error">Connection error. Please try again.</div>'); |
| 1919 |
}).always(function() { |
| 1920 |
seodState.expandAnalyzing = false; |
| 1921 |
}); |
| 1922 |
} |
| 1923 |
|
| 1924 |
function renderSeodDetail($detail, data, postId) { |
| 1925 |
var checks = data.checks, score = data.score, summary = data.summary; |
| 1926 |
var icons = { |
| 1927 |
pass: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>', |
| 1928 |
warn: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>', |
| 1929 |
fail: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>', |
| 1930 |
}; |
| 1931 |
|
| 1932 |
// Checks that require the Advanced Content Editor add-on to fix |
| 1933 |
var addonChecks = { readability: true, internal_links: true, img_alt: true, featured_img: true }; |
| 1934 |
var fixableMap = { meta_desc: 'meta_description', title_length: 'seo_title', slug: 'slug', readability: 'readability', internal_links: 'internal_links', img_alt: 'img_alt', featured_img: 'featured_img' }; |
| 1935 |
var sparkleIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.287 1.288L3 12l5.8 1.9a2 2 0 0 1 1.288 1.287L12 21l1.9-5.8a2 2 0 0 1 1.287-1.288L21 12l-5.8-1.9a2 2 0 0 1-1.288-1.287Z"/></svg>'; |
| 1936 |
|
| 1937 |
var sorted = Object.keys(checks).sort(function(a, b) { |
| 1938 |
var o = { fail: 0, warn: 1, pass: 2 }; |
| 1939 |
return (o[checks[a].status] || 2) - (o[checks[b].status] || 2); |
| 1940 |
}); |
| 1941 |
|
| 1942 |
var html = '<div class="mxch-seod-checks">'; |
| 1943 |
var last = null; |
| 1944 |
sorted.forEach(function(key) { |
| 1945 |
var c = checks[key]; |
| 1946 |
if (last && last !== 'pass' && c.status === 'pass') { |
| 1947 |
html += '<div class="mxch-seod-check-sep"></div>'; |
| 1948 |
} |
| 1949 |
last = c.status; |
| 1950 |
|
| 1951 |
// Show addon/pro badge for gated checks that aren't passing |
| 1952 |
var badge = ''; |
| 1953 |
if (addonChecks[key] && c.status !== 'pass' && !mxchatContent.hasAdvancedContent) { |
| 1954 |
if (mxchatContent.isActivated) { |
| 1955 |
badge = ' <a href="https://mxchat.ai/advanced-content-editor/" target="_blank" class="mxch-seod-addon-badge">ADD-ON</a>'; |
| 1956 |
} else { |
| 1957 |
badge = ' <a href="https://mxchat.ai/" target="_blank" class="mxch-seod-addon-badge mxch-seod-pro-badge">PRO</a>'; |
| 1958 |
} |
| 1959 |
} |
| 1960 |
|
| 1961 |
// Per-check AI fix button |
| 1962 |
var fixBtn = ''; |
| 1963 |
if (c.status !== 'pass' && fixableMap[key]) { |
| 1964 |
var canFix = !addonChecks[key] || mxchatContent.hasAdvancedContent; |
| 1965 |
if (canFix) { |
| 1966 |
fixBtn = '<button type="button" class="mxch-seo-check-fix mxch-seod-check-fix-btn" data-field="' + fixableMap[key] + '" data-post-id="' + postId + '" title="AI Fix">' + sparkleIcon + '</button>'; |
| 1967 |
} |
| 1968 |
} |
| 1969 |
|
| 1970 |
html += '<div class="mxch-seod-check mxch-seod-check-' + c.status + '">' + |
| 1971 |
'<div class="mxch-seod-check-icon">' + icons[c.status] + '</div>' + |
| 1972 |
'<div class="mxch-seod-check-text">' + |
| 1973 |
'<span class="mxch-seod-check-label">' + escapeHtml(c.label) + badge + '</span>' + |
| 1974 |
'<span class="mxch-seod-check-detail">' + escapeHtml(c.detail) + '</span>' + |
| 1975 |
'</div>' + |
| 1976 |
fixBtn + |
| 1977 |
'</div>'; |
| 1978 |
}); |
| 1979 |
html += '</div>'; |
| 1980 |
|
| 1981 |
// Optimize All button (only if there are issues) |
| 1982 |
if (summary.fail > 0 || summary.warn > 0) { |
| 1983 |
html += '<div class="mxch-seod-detail-actions">' + |
| 1984 |
'<button type="button" class="mxch-seod-optimize-btn">' + |
| 1985 |
'<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="m12 3-1.9 5.8a2 2 0 0 1-1.287 1.288L3 12l5.8 1.9a2 2 0 0 1 1.288 1.287L12 21l1.9-5.8a2 2 0 0 1 1.287-1.288L21 12l-5.8-1.9a2 2 0 0 1-1.288-1.287Z"/></svg>' + |
| 1986 |
' Optimize All' + |
| 1987 |
'</button>' + |
| 1988 |
'</div>'; |
| 1989 |
} |
| 1990 |
|
| 1991 |
$detail.html(html); |
| 1992 |
|
| 1993 |
// GSC placeholder for free/non-addon users |
| 1994 |
if (!mxchatContent.hasGSC) { |
| 1995 |
var badgeLabel = mxchatContent.isActivated ? 'ADD-ON' : 'PRO'; |
| 1996 |
var badgeClass = mxchatContent.isActivated ? '' : ' mxch-seod-pro-badge'; |
| 1997 |
var upgradeText = mxchatContent.isActivated ? 'Install Add-on' : 'Upgrade to Pro'; |
| 1998 |
var gscHtml = |
| 1999 |
'<div class="mxch-gsc-placeholder">' + |
| 2000 |
'<h4 class="mxch-gsc-placeholder-title">' + |
| 2001 |
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>' + |
| 2002 |
' Search Performance' + |
| 2003 |
'</h4>' + |
| 2004 |
'<div class="mxch-gsc-placeholder-content">' + |
| 2005 |
'<div class="mxch-gsc-stats">' + |
| 2006 |
'<div class="mxch-gsc-stat"><span class="mxch-gsc-stat-value">42</span><span class="mxch-gsc-stat-label">Clicks</span></div>' + |
| 2007 |
'<div class="mxch-gsc-stat"><span class="mxch-gsc-stat-value">1.2K</span><span class="mxch-gsc-stat-label">Impressions</span></div>' + |
| 2008 |
'<div class="mxch-gsc-stat"><span class="mxch-gsc-stat-value">3.5%</span><span class="mxch-gsc-stat-label">CTR</span></div>' + |
| 2009 |
'<div class="mxch-gsc-stat"><span class="mxch-gsc-stat-value">8.2</span><span class="mxch-gsc-stat-label">Avg Position</span></div>' + |
| 2010 |
'</div>' + |
| 2011 |
'<table class="mxch-gsc-table">' + |
| 2012 |
'<thead><tr><th>Keyword</th><th>Clicks</th><th>Impr.</th><th>Position</th></tr></thead>' + |
| 2013 |
'<tbody>' + |
| 2014 |
'<tr><td>example keyword one</td><td>18</td><td>420</td><td>5.3</td></tr>' + |
| 2015 |
'<tr><td>sample search term</td><td>14</td><td>380</td><td>7.1</td></tr>' + |
| 2016 |
'<tr><td>another query phrase</td><td>10</td><td>290</td><td>12.4</td></tr>' + |
| 2017 |
'</tbody>' + |
| 2018 |
'</table>' + |
| 2019 |
'</div>' + |
| 2020 |
'<div class="mxch-gsc-placeholder-overlay">' + |
| 2021 |
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>' + |
| 2022 |
'<a href="https://mxchat.ai/" target="_blank" class="mxch-seod-addon-badge' + badgeClass + '">' + badgeLabel + '</a>' + |
| 2023 |
'<a href="https://mxchat.ai/" target="_blank" class="mxch-cg-pro-upgrade-link">' + upgradeText + '</a>' + |
| 2024 |
'</div>' + |
| 2025 |
'</div>'; |
| 2026 |
$detail.append(gscHtml); |
| 2027 |
} |
| 2028 |
} |
| 2029 |
|
| 2030 |
function closeSeoModal() { |
| 2031 |
seodState.expandedId = null; |
| 2032 |
var $overlay = $('.mxch-seod-modal-overlay'); |
| 2033 |
if ($overlay.length) { |
| 2034 |
$overlay.removeClass('mxch-seod-modal-visible'); |
| 2035 |
setTimeout(function() { $overlay.remove(); }, 200); |
| 2036 |
} |
| 2037 |
} |
| 2038 |
|
| 2039 |
function bulkSeoScan() { |
| 2040 |
seodState.scanning = true; |
| 2041 |
seodState.scanAborted = false; |
| 2042 |
var $btn = $('#mxch-seod-scan-all'); |
| 2043 |
var $status = $('#mxch-seod-scan-status'); |
| 2044 |
$btn.hide(); |
| 2045 |
|
| 2046 |
// Show stop button |
| 2047 |
if (!$('#mxch-seod-scan-stop').length) { |
| 2048 |
$btn.after('<button type="button" class="mxch-seod-scan-btn mxch-seod-scan-stop-btn" id="mxch-seod-scan-stop"><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"><rect x="6" y="6" width="12" height="12" rx="2"/></svg> <span>Stop</span></button>'); |
| 2049 |
} |
| 2050 |
$('#mxch-seod-scan-stop').show(); |
| 2051 |
$status.text('Loading unscored posts...'); |
| 2052 |
|
| 2053 |
// Fetch ALL unscored post IDs across all pages |
| 2054 |
var allIds = []; |
| 2055 |
function fetchPage(page) { |
| 2056 |
$.post(ajaxurl, { |
| 2057 |
action: 'mxchat_seo_list_posts', |
| 2058 |
nonce: mxchatContent.nonce, |
| 2059 |
page: page, |
| 2060 |
post_type: 'any', |
| 2061 |
filter: 'unscored', |
| 2062 |
search: '', |
| 2063 |
}).done(function(res) { |
| 2064 |
if (!res.success || !res.data.posts.length) { |
| 2065 |
if (allIds.length === 0) { |
| 2066 |
finishScan('All posts have been scanned.'); |
| 2067 |
return; |
| 2068 |
} |
| 2069 |
startScanning(allIds); |
| 2070 |
return; |
| 2071 |
} |
| 2072 |
res.data.posts.forEach(function(p) { allIds.push(p.id); }); |
| 2073 |
if (page < res.data.pages) { |
| 2074 |
$status.text('Loading unscored posts... (' + allIds.length + ' found)'); |
| 2075 |
fetchPage(page + 1); |
| 2076 |
} else { |
| 2077 |
startScanning(allIds); |
| 2078 |
} |
| 2079 |
}).fail(function() { |
| 2080 |
finishScan('Error loading posts.'); |
| 2081 |
}); |
| 2082 |
} |
| 2083 |
|
| 2084 |
function startScanning(ids) { |
| 2085 |
var total = ids.length; |
| 2086 |
var scanned = 0; |
| 2087 |
var batchSize = 10; |
| 2088 |
$status.html('<span class="mxch-seod-scan-progress">0 / ' + total + '</span>'); |
| 2089 |
|
| 2090 |
function updateRows(results) { |
| 2091 |
$.each(results, function(pid, data) { |
| 2092 |
var $row = $('.mxch-seod-row[data-post-id="' + pid + '"]'); |
| 2093 |
if ($row.length) { |
| 2094 |
var score = data.score; |
| 2095 |
var cls = score >= 80 ? 'mxch-seod-good' : score >= 50 ? 'mxch-seod-ok' : 'mxch-seod-bad'; |
| 2096 |
$row.find('.mxch-seod-score-badge') |
| 2097 |
.removeClass('mxch-seod-unscored').addClass(cls).text(score); |
| 2098 |
} |
| 2099 |
}); |
| 2100 |
} |
| 2101 |
|
| 2102 |
function scanNextBatch() { |
| 2103 |
if (seodState.scanAborted) { |
| 2104 |
finishScan('Stopped — ' + scanned + ' of ' + total + ' scanned.'); |
| 2105 |
loadSeoPosts(); |
| 2106 |
return; |
| 2107 |
} |
| 2108 |
if (scanned >= total) { |
| 2109 |
finishScan('Done! ' + total + ' posts scanned.'); |
| 2110 |
loadSeoPosts(); |
| 2111 |
return; |
| 2112 |
} |
| 2113 |
var batch = ids.slice(scanned, scanned + batchSize); |
| 2114 |
$status.html('<span class="mxch-seod-scan-progress">' + (scanned + 1) + ' / ' + total + '</span>'); |
| 2115 |
$.post(ajaxurl, { |
| 2116 |
action: 'mxchat_seo_analyze_batch', |
| 2117 |
nonce: mxchatContent.nonce, |
| 2118 |
'post_ids[]': batch, |
| 2119 |
}).done(function(res) { |
| 2120 |
if (res.success && res.data.results) { |
| 2121 |
updateRows(res.data.results); |
| 2122 |
} |
| 2123 |
}).always(function() { |
| 2124 |
scanned += batch.length; |
| 2125 |
$status.html('<span class="mxch-seod-scan-progress">' + scanned + ' / ' + total + '</span>'); |
| 2126 |
scanNextBatch(); |
| 2127 |
}); |
| 2128 |
} |
| 2129 |
scanNextBatch(); |
| 2130 |
} |
| 2131 |
|
| 2132 |
function finishScan(msg) { |
| 2133 |
seodState.scanning = false; |
| 2134 |
seodState.scanAborted = false; |
| 2135 |
$('#mxch-seod-scan-stop').hide(); |
| 2136 |
$btn.show().prop('disabled', false); |
| 2137 |
$status.text(msg); |
| 2138 |
} |
| 2139 |
|
| 2140 |
fetchPage(1); |
| 2141 |
} |
| 2142 |
|
| 2143 |
function runSeodOptimize(postId, $btn) { |
| 2144 |
var origHtml = $btn.html(); |
| 2145 |
$btn.prop('disabled', true).html( |
| 2146 |
'<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="m12 3-1.9 5.8a2 2 0 0 1-1.287 1.288L3 12l5.8 1.9a2 2 0 0 1 1.288 1.287L12 21l1.9-5.8a2 2 0 0 1 1.287-1.288L21 12l-5.8-1.9a2 2 0 0 1-1.288-1.287Z"/></svg>' + |
| 2147 |
' Optimizing…' |
| 2148 |
); |
| 2149 |
|
| 2150 |
// Get the current checks to find what needs fixing |
| 2151 |
$.post(ajaxurl, { |
| 2152 |
action: 'mxchat_seo_analyze', |
| 2153 |
nonce: mxchatContent.nonce, |
| 2154 |
post_id: postId, |
| 2155 |
}).done(function(res) { |
| 2156 |
if (!res.success) { |
| 2157 |
$btn.prop('disabled', false).html(origHtml); |
| 2158 |
return; |
| 2159 |
} |
| 2160 |
var checks = res.data.checks; |
| 2161 |
var prefs = mxchatContent.seoOptimize || {}; |
| 2162 |
var fields = []; |
| 2163 |
if (prefs.meta_description !== false && checks.meta_desc && checks.meta_desc.status !== 'pass') fields.push('meta_description'); |
| 2164 |
if (prefs.seo_title !== false && checks.title_length && checks.title_length.status !== 'pass') fields.push('seo_title'); |
| 2165 |
if (prefs.slug !== false && checks.slug && checks.slug.status !== 'pass') fields.push('slug'); |
| 2166 |
// Readability, internal links, images require Advanced Content Editor add-on |
| 2167 |
if (mxchatContent.hasAdvancedContent) { |
| 2168 |
if (prefs.readability !== false && checks.readability && checks.readability.status !== 'pass') fields.push('readability'); |
| 2169 |
if (prefs.internal_links !== false && checks.internal_links && checks.internal_links.status !== 'pass') fields.push('internal_links'); |
| 2170 |
if (prefs.img_alt !== false && checks.img_alt && checks.img_alt.status !== 'pass') fields.push('img_alt'); |
| 2171 |
if (prefs.featured_img !== false && checks.featured_img && checks.featured_img.status !== 'pass') fields.push('featured_img'); |
| 2172 |
} |
| 2173 |
if (!fields.length) fields.push('meta_description'); |
| 2174 |
|
| 2175 |
// Run fields sequentially to avoid race conditions |
| 2176 |
// (multiple optimizers read/write post_content) |
| 2177 |
var idx = 0; |
| 2178 |
function runNext() { |
| 2179 |
if (idx >= fields.length) { |
| 2180 |
if (seodState.expandedId === postId) { |
| 2181 |
openSeoModal(postId); |
| 2182 |
} |
| 2183 |
$btn.prop('disabled', false).html(origHtml); |
| 2184 |
return; |
| 2185 |
} |
| 2186 |
$.post(ajaxurl, { |
| 2187 |
action: 'mxchat_seo_suggest', |
| 2188 |
nonce: mxchatContent.nonce, |
| 2189 |
post_id: postId, |
| 2190 |
field: fields[idx], |
| 2191 |
}).always(function() { |
| 2192 |
idx++; |
| 2193 |
runNext(); |
| 2194 |
}); |
| 2195 |
} |
| 2196 |
runNext(); |
| 2197 |
}).fail(function() { |
| 2198 |
$btn.prop('disabled', false).html(origHtml); |
| 2199 |
}); |
| 2200 |
} |
| 2201 |
|
| 2202 |
function showNotice(message, type) { |
| 2203 |
$('.mxch-cg-notice').remove(); |
| 2204 |
var typeClass = type === 'error' ? 'mxch-cg-notice-error' : 'mxch-cg-notice-success'; |
| 2205 |
var $notice = $('<div class="mxch-cg-notice ' + typeClass + '">' + escapeHtml(message) + '</div>'); |
| 2206 |
$('#mxch-cg-inline-form .mxch-cg-form').prepend($notice); |
| 2207 |
setTimeout(function() { $notice.fadeOut(300, function() { $(this).remove(); }); }, 4000); |
| 2208 |
} |
| 2209 |
|
| 2210 |
// ─── Initialize ──────────────────────────────────────────────────── |
| 2211 |
|
| 2212 |
$(document).ready(function() { |
| 2213 |
initNavigation(); |
| 2214 |
initInlineForm(); |
| 2215 |
initGeneration(); |
| 2216 |
initPromptModal(); |
| 2217 |
initPreview(); |
| 2218 |
initChat(); |
| 2219 |
initSettingsAutoSave(); |
| 2220 |
initLeftTabs(); |
| 2221 |
initSeo(); |
| 2222 |
initSeoSection(); |
| 2223 |
initHistory(); |
| 2224 |
initStatusDropdown(); |
| 2225 |
|
| 2226 |
// Prevent interaction with locked pro feature toggles |
| 2227 |
$('.mxch-cg-pro-locked .mxch-toggle-input').on('click', function(e) { |
| 2228 |
e.preventDefault(); |
| 2229 |
return false; |
| 2230 |
}); |
| 2231 |
}); |
| 2232 |
|
| 2233 |
})(jQuery); |
| 2234 |
|