PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.2
MxChat – AI Chatbot & Content Generation for WordPress v3.1.2
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / js / mxchat-content.js

mxchat-content.js in MxChat – AI Chatbot & Content Generation for WordPress 3.1.2, at js/mxchat-content.js

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