PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.1
MxChat – AI Chatbot & Content Generation for WordPress v3.1.1
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.1, at js/mxchat-content.js

2,015 lines 89.8 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 // Auto-analyze when switching to SEO tab if content exists
867 $(document).on('click', '.mxch-cg-left-tab[data-tab="seo"]', function() {
868 if (state.postId && !seoState.analyzed && !seoState.analyzing) runSeoAnalysis();
869 });
870 }
871
872 function runSeoAnalysis() {
873 if (!state.postId) return;
874 seoState.analyzing = true;
875 $('#mxch-seo-analyze').addClass('mxch-spinning');
876 $.post(ajaxurl, {
877 action: 'mxchat_seo_analyze',
878 nonce: mxchatContent.nonce,
879 post_id: state.postId,
880 }).done(function(res) {
881 if (res.success) {
882 seoState.analyzed = true;
883 seoState.score = res.data.score;
884 seoState.checks = res.data.checks;
885 renderSeoResults(res.data);
886 } else {
887 renderSeoError(res.data || 'Analysis failed');
888 }
889 }).fail(function() {
890 renderSeoError('Connection error');
891 }).always(function() {
892 seoState.analyzing = false;
893 $('#mxch-seo-analyze').removeClass('mxch-spinning');
894 });
895 }
896
897 function renderSeoResults(data) {
898 var score = data.score, checks = data.checks, summary = data.summary;
899 // Score ring
900 var offset = 163.36 - (score / 100) * 163.36;
901 var $ring = $('#mxch-seo-ring');
902 $ring.css('stroke-dashoffset', offset).removeClass('mxch-seo-good mxch-seo-ok mxch-seo-bad');
903 if (score >= 80) $ring.addClass('mxch-seo-good');
904 else if (score >= 50) $ring.addClass('mxch-seo-ok');
905 else $ring.addClass('mxch-seo-bad');
906 $('#mxch-seo-score').text(score);
907 $('#mxch-seo-score-label').text(score >= 80 ? 'Great' : score >= 60 ? 'Good' : score >= 40 ? 'Needs Work' : 'Poor');
908 var parts = [];
909 if (summary.pass) parts.push(summary.pass + ' passed');
910 if (summary.warn) parts.push(summary.warn + ' warnings');
911 if (summary.fail) parts.push(summary.fail + ' issues');
912 $('#mxch-seo-score-summary').text(parts.join(' \u00b7 '));
913 // Checklist
914 var $list = $('#mxch-seo-checklist').empty();
915 var icons = {
916 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>',
917 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>',
918 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>',
919 };
920 // Checks that require the Advanced Content Editor add-on to fix
921 var addonChecks = { readability: true, internal_links: true, img_alt: true, featured_img: true };
922 var sorted = Object.keys(checks).sort(function(a, b) {
923 var o = { fail: 0, warn: 1, pass: 2 };
924 return (o[checks[a].status] || 2) - (o[checks[b].status] || 2);
925 });
926 var last = null;
927 sorted.forEach(function(key) {
928 var c = checks[key];
929 if (last && last !== 'pass' && c.status === 'pass') {
930 $list.append('<div class="mxch-seo-separator"></div>');
931 }
932 last = c.status;
933
934 // Show addon/pro badge for gated checks that aren't passing
935 var badge = '';
936 if (addonChecks[key] && c.status !== 'pass' && !mxchatContent.hasAdvancedContent) {
937 if (mxchatContent.isActivated) {
938 badge = ' <a href="https://mxchat.ai/advanced-content-editor/" target="_blank" class="mxch-seod-addon-badge">ADD-ON</a>';
939 } else {
940 badge = ' <a href="https://mxchat.ai/" target="_blank" class="mxch-seod-addon-badge mxch-seod-pro-badge">PRO</a>';
941 }
942 }
943
944 $list.append(
945 '<div class="mxch-seo-check" data-check="' + key + '">' +
946 '<div class="mxch-seo-check-icon mxch-seo-' + c.status + '">' + icons[c.status] + '</div>' +
947 '<div class="mxch-seo-check-content">' +
948 '<span class="mxch-seo-check-label">' + escapeHtml(c.label) + badge + '</span>' +
949 '<span class="mxch-seo-check-detail">' + escapeHtml(c.detail) + '</span>' +
950 '</div>' +
951 '</div>'
952 );
953 });
954 $('#mxch-seo-actions').toggle(summary.fail > 0 || summary.warn > 0);
955 }
956
957 function renderSeoError(msg) {
958 $('#mxch-seo-checklist').html('<div class="mxch-seo-empty"><span style="color:#ef4444;">' + escapeHtml(msg) + '</span></div>');
959 }
960
961 function runAiOptimize() {
962 if (!state.postId || seoState.fixing) return;
963 seoState.fixing = true;
964 var $btn = $('#mxch-seo-ai-optimize'), origHtml = $btn.html();
965 $btn.addClass('mxch-seo-fixing').prop('disabled', true)
966 .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');
967 var prefs = mxchatContent.seoOptimize || {};
968 var fields = [];
969 if (seoState.checks) {
970 if (prefs.meta_description !== false && seoState.checks.meta_desc && seoState.checks.meta_desc.status !== 'pass') fields.push('meta_description');
971 if (prefs.seo_title !== false && seoState.checks.title_length && seoState.checks.title_length.status !== 'pass') fields.push('seo_title');
972 if (prefs.slug !== false && seoState.checks.slug && seoState.checks.slug.status !== 'pass') fields.push('slug');
973 }
974 if (!fields.length) fields.push('meta_description');
975 var done = 0;
976 fields.forEach(function(field) {
977 $.post(ajaxurl, {
978 action: 'mxchat_seo_suggest',
979 nonce: mxchatContent.nonce,
980 post_id: state.postId,
981 field: field,
982 }).done(function(res) {
983 if (res.success) {
984 if (field === 'meta_description') $('#mxch-cg-meta-description').val(res.data.suggestion).trigger('input');
985 else if (field === 'seo_title') $('#mxch-cg-meta-title').val(res.data.suggestion);
986 else if (field === 'excerpt') $('#mxch-cg-meta-excerpt').val(res.data.suggestion);
987 $('.mxch-cg-left-tab[data-tab="meta"]').addClass('mxch-cg-tab-flash');
988 setTimeout(function() { $('.mxch-cg-left-tab[data-tab="meta"]').removeClass('mxch-cg-tab-flash'); }, 2000);
989 }
990 }).always(function() {
991 if (++done >= fields.length) {
992 seoState.fixing = false;
993 $btn.removeClass('mxch-seo-fixing').prop('disabled', false).html(origHtml);
994 runSeoAnalysis();
995 }
996 });
997 });
998 }
999
1000 function resetSeoPanel() {
1001 seoState = { analyzed: false, analyzing: false, fixing: false, score: null, checks: null };
1002 $('#mxch-seo-score').text('\u2014');
1003 $('#mxch-seo-score-label').text('SEO Score');
1004 $('#mxch-seo-score-summary').text('Generate content to analyze');
1005 $('#mxch-seo-ring').css('stroke-dashoffset', '163.36').removeClass('mxch-seo-good mxch-seo-ok mxch-seo-bad');
1006 $('#mxch-seo-checklist').html(
1007 '<div class="mxch-seo-empty" id="mxch-seo-empty">' +
1008 '<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>' +
1009 '<span>SEO analysis will appear here after content is generated</span></div>'
1010 );
1011 $('#mxch-seo-actions').hide();
1012 }
1013
1014 function escapeHtml(s) {
1015 var d = document.createElement('div');
1016 d.appendChild(document.createTextNode(s));
1017 return d.innerHTML;
1018 }
1019
1020 // ─── Left Column Tabs ──────────────────────────────────────────────
1021
1022 function initLeftTabs() {
1023 $(document).on('click', '.mxch-cg-left-tab', function() {
1024 var tab = $(this).data('tab');
1025 $('.mxch-cg-left-tab').removeClass('active');
1026 $(this).addClass('active');
1027 $('.mxch-cg-left-panel').removeClass('active');
1028 $('#mxch-cg-panel-' + tab).addClass('active');
1029 });
1030
1031 // Character counter for meta description
1032 $(document).on('input', '#mxch-cg-meta-description', updateCharCount);
1033 }
1034
1035 function populateMetaPanel(data) {
1036 $('#mxch-cg-meta-title').val(data.title || '');
1037 if (data.meta) {
1038 $('#mxch-cg-meta-description').val(data.meta.description || '');
1039 $('#mxch-cg-meta-keyword').val(data.meta.keyword || '');
1040 $('#mxch-cg-meta-excerpt').val(data.meta.excerpt || '');
1041 }
1042 updateCharCount();
1043 }
1044
1045 function updateCharCount() {
1046 var len = ($('#mxch-cg-meta-description').val() || '').length;
1047 var $counter = $('.mxch-cg-meta-charcount');
1048 $counter.text(len + ' / 160');
1049 if (len > 160) {
1050 $counter.addClass('mxch-cg-meta-charcount-over');
1051 } else {
1052 $counter.removeClass('mxch-cg-meta-charcount-over');
1053 }
1054 }
1055
1056 // ─── History Tab ──────────────────────────────────────────────────
1057
1058 function initHistory() {
1059 $(document).on('click', '[data-target="content-history"]', function() {
1060 if (!state.historyLoaded) {
1061 loadHistory(1);
1062 }
1063 });
1064
1065 $(document).on('click', '.mxch-cg-history-page-btn[data-page]', function() {
1066 var page = $(this).data('page');
1067 if (page && !state.historyLoading) {
1068 loadHistory(page);
1069 }
1070 });
1071
1072 $(document).on('click', '.mxch-cg-history-edit-btn', function() {
1073 var postId = $(this).data('post-id');
1074 if (postId) {
1075 loadPostForEdit(postId, $(this));
1076 }
1077 });
1078
1079 $(document).on('click', '.mxch-cg-history-delete-btn', function() {
1080 var $btn = $(this);
1081 var postId = $btn.data('post-id');
1082 var $item = $btn.closest('.mxch-cg-history-item');
1083 var title = $item.find('.mxch-cg-history-title').text();
1084
1085 if (!confirm('Move "' + title + '" to trash?')) return;
1086
1087 $btn.prop('disabled', true);
1088 $.ajax({
1089 url: mxchatContent.ajaxUrl,
1090 type: 'POST',
1091 data: {
1092 action: 'mxchat_delete_content',
1093 nonce: mxchatContent.nonce,
1094 post_id: postId
1095 },
1096 success: function(response) {
1097 if (response.success) {
1098 $item.slideUp(200, function() { $(this).remove(); });
1099 } else {
1100 alert(response.data.message || 'Failed to delete.');
1101 $btn.prop('disabled', false);
1102 }
1103 },
1104 error: function() {
1105 alert('Request failed. Please try again.');
1106 $btn.prop('disabled', false);
1107 }
1108 });
1109 });
1110 }
1111
1112 function loadHistory(page) {
1113 state.historyLoading = true;
1114 state.historyPage = page;
1115
1116 var $loading = $('#mxch-cg-history-loading');
1117 var $empty = $('#mxch-cg-history-empty');
1118 var $list = $('#mxch-cg-history-list');
1119 var $pag = $('#mxch-cg-history-pagination');
1120
1121 $loading.show();
1122 $empty.hide();
1123 $list.hide();
1124 $pag.hide();
1125
1126 $.ajax({
1127 url: mxchatContent.ajaxUrl,
1128 type: 'POST',
1129 data: {
1130 action: 'mxchat_content_history',
1131 nonce: mxchatContent.nonce,
1132 page: page
1133 },
1134 success: function(response) {
1135 state.historyLoading = false;
1136 state.historyLoaded = true;
1137 $loading.hide();
1138
1139 if (!response.success || !response.data.items.length) {
1140 $empty.show();
1141 return;
1142 }
1143
1144 renderHistoryList(response.data.items);
1145 renderHistoryPagination(response.data.current_page, response.data.total_pages);
1146 $list.show();
1147
1148 if (response.data.total_pages > 1) {
1149 $pag.show();
1150 }
1151 },
1152 error: function() {
1153 state.historyLoading = false;
1154 $loading.hide();
1155 $empty.show();
1156 }
1157 });
1158 }
1159
1160 function renderHistoryList(items) {
1161 var $list = $('#mxch-cg-history-list');
1162 $list.empty();
1163
1164 var statusLabels = {
1165 draft: 'Draft',
1166 publish: 'Published',
1167 future: 'Scheduled',
1168 pending: 'Pending',
1169 'private': 'Private'
1170 };
1171
1172 $.each(items, function(i, item) {
1173 var thumbHtml = item.thumbnail
1174 ? '<img src="' + escapeAttr(item.thumbnail) + '" alt="" class="mxch-cg-history-thumb-img">'
1175 : '<div class="mxch-cg-history-thumb-placeholder">' +
1176 '<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>' +
1177 '</div>';
1178
1179 var statusClass = 'mxch-cg-badge-' + item.status;
1180 var statusText = statusLabels[item.status] || item.status;
1181 var typeLabel = item.post_type === 'page' ? 'Page' : 'Post';
1182
1183 var $row = $(
1184 '<div class="mxch-cg-history-item">' +
1185 '<div class="mxch-cg-history-thumb">' + thumbHtml + '</div>' +
1186 '<div class="mxch-cg-history-info">' +
1187 '<div class="mxch-cg-history-title">' + escapeHtml(item.title) + '</div>' +
1188 '<div class="mxch-cg-history-meta">' +
1189 '<span class="mxch-cg-status-badge ' + statusClass + '">' + escapeHtml(statusText) + '</span>' +
1190 '<span class="mxch-cg-history-type">' + escapeHtml(typeLabel) + '</span>' +
1191 '<span class="mxch-cg-history-date">' + escapeHtml(item.date) + '</span>' +
1192 '</div>' +
1193 '</div>' +
1194 '<div class="mxch-cg-history-actions">' +
1195 '<a href="' + escapeAttr(item.permalink) + '" target="_blank" class="mxch-cg-history-action-btn mxch-cg-history-view-btn" title="View">' +
1196 '<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>' +
1197 '</a>' +
1198 '<button type="button" class="mxch-cg-history-action-btn mxch-cg-history-edit-btn" data-post-id="' + item.post_id + '" title="Edit">' +
1199 '<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>' +
1200 '</button>' +
1201 '<button type="button" class="mxch-cg-history-action-btn mxch-cg-history-delete-btn" data-post-id="' + item.post_id + '" title="Delete">' +
1202 '<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>' +
1203 '</button>' +
1204 '</div>' +
1205 '</div>'
1206 );
1207
1208 $list.append($row);
1209 });
1210 }
1211
1212 function renderHistoryPagination(current, total) {
1213 var $pag = $('#mxch-cg-history-pagination');
1214 $pag.empty();
1215
1216 if (total <= 1) return;
1217
1218 var html = '';
1219
1220 if (current > 1) {
1221 html += '<button type="button" class="mxch-cg-history-page-btn mxch-cg-history-page-prev" data-page="' + (current - 1) + '">&laquo; Prev</button>';
1222 }
1223
1224 for (var p = 1; p <= total; p++) {
1225 if (p === current) {
1226 html += '<span class="mxch-cg-history-page-btn mxch-cg-history-page-current">' + p + '</span>';
1227 } else if (p === 1 || p === total || (p >= current - 1 && p <= current + 1)) {
1228 html += '<button type="button" class="mxch-cg-history-page-btn" data-page="' + p + '">' + p + '</button>';
1229 } else if (p === current - 2 || p === current + 2) {
1230 html += '<span class="mxch-cg-history-page-ellipsis">&hellip;</span>';
1231 }
1232 }
1233
1234 if (current < total) {
1235 html += '<button type="button" class="mxch-cg-history-page-btn mxch-cg-history-page-next" data-page="' + (current + 1) + '">Next &raquo;</button>';
1236 }
1237
1238 $pag.html(html);
1239 }
1240
1241 function loadPostForEdit(postId, $btn) {
1242 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';
1243
1244 $btn.prop('disabled', true).text('Loading...');
1245
1246 $.ajax({
1247 url: mxchatContent.ajaxUrl,
1248 type: 'POST',
1249 data: {
1250 action: 'mxchat_load_post_for_edit',
1251 nonce: mxchatContent.nonce,
1252 post_id: postId
1253 },
1254 success: function(response) {
1255 $btn.prop('disabled', false).html(editBtnHtml);
1256
1257 if (response.success) {
1258 // Switch to Generate tab
1259 switchSection('content-generate');
1260 $('.mxch-nav-link, .mxch-nav-sub-link').removeClass('active');
1261 $('[data-target="content-generate"]').addClass('active');
1262 $('.mxch-mobile-nav-link').removeClass('active');
1263 $('.mxch-mobile-nav-link[data-target="content-generate"]').addClass('active');
1264
1265 // Load post into the same editor state as fresh generation
1266 onGenerationSuccess(response.data);
1267 } else {
1268 alert(response.data && response.data.message ? response.data.message : 'Failed to load post.');
1269 }
1270 },
1271 error: function() {
1272 $btn.prop('disabled', false).html(editBtnHtml);
1273 alert('Request failed. Please try again.');
1274 }
1275 });
1276 }
1277
1278 // ─── Status Dropdown ──────────────────────────────────────────────
1279
1280 function initStatusDropdown() {
1281 // Toggle dropdown on badge click
1282 $(document).on('click', '#mxch-cg-status-badge', function(e) {
1283 e.stopPropagation();
1284 var $dropdown = $('#mxch-cg-status-dropdown');
1285 if ($dropdown.hasClass('mxch-cg-dropdown-open')) {
1286 closeStatusDropdown();
1287 } else {
1288 openStatusDropdown();
1289 }
1290 });
1291
1292 // Close on outside click
1293 $(document).on('click', function(e) {
1294 if (!$(e.target).closest('#mxch-cg-status-dropdown').length) {
1295 closeStatusDropdown();
1296 }
1297 });
1298
1299 // Close on Escape
1300 $(document).on('keydown', function(e) {
1301 if (e.key === 'Escape') {
1302 closeStatusDropdown();
1303 }
1304 });
1305
1306 // Draft / Publish — immediate status change
1307 $(document).on('click', '.mxch-cg-status-option[data-status="draft"], .mxch-cg-status-option[data-status="publish"]', function() {
1308 var newStatus = $(this).data('status');
1309 if (newStatus === state.postStatus) {
1310 closeStatusDropdown();
1311 return;
1312 }
1313 updatePostStatus(newStatus, '');
1314 });
1315
1316 // Scheduled — show datetime picker
1317 $(document).on('click', '.mxch-cg-status-option[data-status="future"]', function() {
1318 var $scheduleRow = $('.mxch-cg-status-schedule-row');
1319 if ($scheduleRow.is(':visible')) {
1320 $scheduleRow.hide();
1321 return;
1322 }
1323 // Pre-fill with tomorrow at 9am if empty
1324 var $input = $('#mxch-cg-status-schedule-input');
1325 if (!$input.val()) {
1326 var tomorrow = new Date();
1327 tomorrow.setDate(tomorrow.getDate() + 1);
1328 tomorrow.setHours(9, 0, 0, 0);
1329 $input.val(tomorrow.toISOString().slice(0, 16));
1330 }
1331 $scheduleRow.show();
1332 $input.focus();
1333 // Highlight scheduled option
1334 $('.mxch-cg-status-option').removeClass('mxch-cg-status-active');
1335 $(this).addClass('mxch-cg-status-active');
1336 });
1337
1338 // Confirm schedule
1339 $(document).on('click', '#mxch-cg-status-schedule-confirm', function() {
1340 var scheduleDate = $('#mxch-cg-status-schedule-input').val();
1341 if (!scheduleDate) {
1342 $('#mxch-cg-status-schedule-input').focus();
1343 return;
1344 }
1345 // Convert datetime-local value to WordPress format (Y-m-d H:i:s)
1346 var wpDate = scheduleDate.replace('T', ' ') + ':00';
1347 updatePostStatus('future', wpDate);
1348 });
1349
1350 // Enter key on datetime input confirms
1351 $(document).on('keydown', '#mxch-cg-status-schedule-input', function(e) {
1352 if (e.key === 'Enter') {
1353 e.preventDefault();
1354 $('#mxch-cg-status-schedule-confirm').trigger('click');
1355 }
1356 });
1357 }
1358
1359 function openStatusDropdown() {
1360 var $dropdown = $('#mxch-cg-status-dropdown');
1361 $dropdown.addClass('mxch-cg-dropdown-open');
1362 $('.mxch-cg-status-menu').show();
1363 // Highlight current status
1364 $('.mxch-cg-status-option').removeClass('mxch-cg-status-active');
1365 $('.mxch-cg-status-option[data-status="' + state.postStatus + '"]').addClass('mxch-cg-status-active');
1366 // Hide schedule row unless current status is future
1367 if (state.postStatus !== 'future') {
1368 $('.mxch-cg-status-schedule-row').hide();
1369 }
1370 }
1371
1372 function closeStatusDropdown() {
1373 $('#mxch-cg-status-dropdown').removeClass('mxch-cg-dropdown-open');
1374 $('.mxch-cg-status-menu').hide();
1375 $('.mxch-cg-status-schedule-row').hide();
1376 }
1377
1378 function updatePostStatus(newStatus, scheduleDate) {
1379 var $badge = $('#mxch-cg-status-badge');
1380 $badge.addClass('mxch-cg-status-updating');
1381 closeStatusDropdown();
1382
1383 $.ajax({
1384 url: mxchatContent.ajaxUrl,
1385 type: 'POST',
1386 data: {
1387 action: 'mxchat_update_post_status',
1388 nonce: mxchatContent.nonce,
1389 post_id: state.postId,
1390 new_status: newStatus,
1391 schedule_date: scheduleDate || ''
1392 },
1393 success: function(response) {
1394 $badge.removeClass('mxch-cg-status-updating');
1395
1396 if (response.success) {
1397 var confirmedStatus = response.data.status;
1398 state.postStatus = confirmedStatus;
1399
1400 // Update badge appearance
1401 var statusLabels = { draft: 'Draft', publish: 'Published', future: 'Scheduled' };
1402 $badge.find('.mxch-cg-status-badge-text').text(statusLabels[confirmedStatus] || confirmedStatus);
1403 $badge.removeClass('mxch-cg-badge-draft mxch-cg-badge-publish mxch-cg-badge-future')
1404 .addClass('mxch-cg-badge-' + confirmedStatus);
1405
1406 // Mark history as stale so it reloads on next visit
1407 state.historyLoaded = false;
1408
1409 // Refresh preview (URL may differ between draft/published)
1410 refreshPreview();
1411 } else {
1412 alert(response.data && response.data.message ? response.data.message : 'Failed to update status.');
1413 }
1414 },
1415 error: function() {
1416 $badge.removeClass('mxch-cg-status-updating');
1417 alert('Request failed. Please try again.');
1418 }
1419 });
1420 }
1421
1422 // ─── Utilities ─────────────────────────────────────────────────────
1423
1424 function escapeHtml(str) {
1425 var div = document.createElement('div');
1426 div.appendChild(document.createTextNode(str));
1427 return div.innerHTML;
1428 }
1429
1430 // ─── SEO Dashboard (Site-wide) ─────────────────────────────────────
1431
1432 var seodState = {
1433 loaded: false,
1434 loading: false,
1435 page: 1,
1436 pages: 1,
1437 total: 0,
1438 filter: 'all',
1439 postType: 'any',
1440 search: '',
1441 searchTimer: null,
1442 scanning: false,
1443 expandedId: null,
1444 expandAnalyzing: false,
1445 sortBy: 'date',
1446 sortOrder: 'DESC',
1447 };
1448
1449 function initSeoSection() {
1450 // Lazy-load: fetch posts when user first visits SEO section
1451 $(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() {
1452 if (!seodState.loaded && !seodState.loading) {
1453 loadSeoPosts();
1454 }
1455 });
1456
1457 // Filter pills
1458 $(document).on('click', '.mxch-seod-pill', function() {
1459 $('.mxch-seod-pill').removeClass('active');
1460 $(this).addClass('active');
1461 seodState.filter = $(this).data('filter');
1462 seodState.page = 1;
1463 loadSeoPosts();
1464 });
1465
1466 // Post type dropdown
1467 $(document).on('change', '#mxch-seod-post-type', function() {
1468 seodState.postType = $(this).val();
1469 seodState.page = 1;
1470 loadSeoPosts();
1471 });
1472
1473 // Search with debounce
1474 $(document).on('input', '#mxch-seod-search', function() {
1475 var val = $(this).val();
1476 clearTimeout(seodState.searchTimer);
1477 seodState.searchTimer = setTimeout(function() {
1478 seodState.search = val;
1479 seodState.page = 1;
1480 loadSeoPosts();
1481 }, 400);
1482 });
1483
1484 // Pagination
1485 $(document).on('click', '.mxch-seod-page-btn', function() {
1486 var p = $(this).data('page');
1487 if (p && p !== seodState.page) {
1488 seodState.page = p;
1489 loadSeoPosts();
1490 }
1491 });
1492
1493 // Open detail modal on row click
1494 $(document).on('click', '.mxch-seod-row', function() {
1495 var postId = $(this).data('post-id');
1496 openSeoModal(postId);
1497 });
1498
1499 // Close modal
1500 $(document).on('click', '.mxch-seod-modal-overlay', function(e) {
1501 if ($(e.target).hasClass('mxch-seod-modal-overlay')) closeSeoModal();
1502 });
1503 $(document).on('click', '.mxch-seod-modal-close', function() {
1504 closeSeoModal();
1505 });
1506 $(document).on('keydown', function(e) {
1507 if (e.key === 'Escape' && seodState.expandedId) closeSeoModal();
1508 });
1509
1510 // Scan Unscored button
1511 $(document).on('click', '#mxch-seod-scan-all', function() {
1512 if (!seodState.scanning) bulkSeoScan();
1513 });
1514 // Stop scan button
1515 $(document).on('click', '#mxch-seod-scan-stop', function() {
1516 seodState.scanAborted = true;
1517 $(this).prop('disabled', true).find('span').text('Stopping...');
1518 });
1519
1520 // Sortable column headers (all columns sort server-side)
1521 $(document).on('click', '.mxch-seod-header-cell[data-sort]', function() {
1522 var col = $(this).data('sort');
1523 if (seodState.sortBy === col) {
1524 seodState.sortOrder = seodState.sortOrder === 'DESC' ? 'ASC' : 'DESC';
1525 } else {
1526 seodState.sortBy = col;
1527 seodState.sortOrder = col === 'title' ? 'ASC' : 'DESC';
1528 }
1529 seodState.page = 1;
1530 loadSeoPosts();
1531 });
1532
1533 // AI Optimize within detail modal
1534 $(document).on('click', '.mxch-seod-optimize-btn', function(e) {
1535 e.stopPropagation();
1536 var postId = $(this).closest('.mxch-seod-detail').data('post-id');
1537 runSeodOptimize(postId, $(this));
1538 });
1539 }
1540
1541 function loadSeoPosts() {
1542 seodState.loading = true;
1543 $('#mxch-seod-loading').show();
1544 $('#mxch-seod-empty').hide();
1545 $('#mxch-seod-table').empty();
1546
1547 $.post(ajaxurl, {
1548 action: 'mxchat_seo_list_posts',
1549 nonce: mxchatContent.nonce,
1550 page: seodState.page,
1551 post_type: seodState.postType,
1552 filter: seodState.filter,
1553 search: seodState.search,
1554 sort_by: seodState.sortBy,
1555 sort_order: seodState.sortOrder,
1556 }).done(function(res) {
1557 if (res.success) {
1558 seodState.loaded = true;
1559 seodState.page = res.data.page;
1560 seodState.pages = res.data.pages;
1561 seodState.total = res.data.total;
1562 renderSeodTable(res.data.posts);
1563 renderSeodPagination();
1564 updateSeodScanBtn(res.data.unscored_count);
1565 $('#mxch-seod-footer').show();
1566 if (!res.data.posts.length) {
1567 $('#mxch-seod-empty').show();
1568 }
1569 }
1570 }).fail(function() {
1571 $('#mxch-seod-table').html('<div class="mxch-seod-error">Failed to load posts. Please try again.</div>');
1572 }).always(function() {
1573 seodState.loading = false;
1574 $('#mxch-seod-loading').hide();
1575 });
1576 }
1577
1578 function seodFormatNum(n) {
1579 if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
1580 if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
1581 return n;
1582 }
1583
1584 function seodSortArrow(col) {
1585 if (seodState.sortBy !== col) return '';
1586 return ' <span class="mxch-seod-sort-arrow">' + (seodState.sortOrder === 'ASC' ? '&#9650;' : '&#9660;') + '</span>';
1587 }
1588
1589 function renderSeodTable(posts) {
1590 var $table = $('#mxch-seod-table');
1591 $table.empty();
1592 seodState.expandedId = null;
1593
1594 // Column headers
1595 var activeClass = function(col) { return seodState.sortBy === col ? ' mxch-seod-header-active' : ''; };
1596 $table.append(
1597 '<div class="mxch-seod-header">' +
1598 '<div class="mxch-seod-header-cell mxch-seod-header-title' + activeClass('title') + '" data-sort="title">Title' + seodSortArrow('title') + '</div>' +
1599 '<div class="mxch-seod-header-cell mxch-seod-header-date' + activeClass('date') + '" data-sort="date">Date' + seodSortArrow('date') + '</div>' +
1600 '<div class="mxch-seod-header-cell mxch-seod-header-score' + activeClass('score') + '" data-sort="score">Score' + seodSortArrow('score') + '</div>' +
1601 '<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>' +
1602 '<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>' +
1603 '</div>'
1604 );
1605
1606 posts.forEach(function(p) {
1607 var scoreHtml;
1608 if (p.score !== null) {
1609 var cls = p.score >= 80 ? 'mxch-seod-good' : p.score >= 50 ? 'mxch-seod-ok' : 'mxch-seod-bad';
1610 scoreHtml = '<div class="mxch-seod-score-badge ' + cls + '">' + p.score + '</div>';
1611 } else {
1612 scoreHtml = '<div class="mxch-seod-score-badge mxch-seod-unscored">&mdash;</div>';
1613 }
1614
1615 var typeLabel = p.type === 'page' ? 'Page' : 'Post';
1616
1617 $table.append(
1618 '<div class="mxch-seod-row" data-post-id="' + p.id + '" data-score="' + (p.score !== null ? p.score : -1) + '">' +
1619 '<div class="mxch-seod-row-main">' +
1620 '<div class="mxch-seod-cell mxch-seod-cell-title">' +
1621 '<span class="mxch-seod-title">' + escapeHtml(p.title) + '</span>' +
1622 '<span class="mxch-seod-meta">' + typeLabel + '</span>' +
1623 '</div>' +
1624 '<div class="mxch-seod-cell mxch-seod-cell-date">' +
1625 '<span class="mxch-seod-date">' + escapeHtml(p.date) + '</span>' +
1626 '</div>' +
1627 '<div class="mxch-seod-cell mxch-seod-cell-score">' + scoreHtml + '</div>' +
1628 '<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>' +
1629 '<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>' +
1630 '</div>' +
1631 '</div>'
1632 );
1633 });
1634 }
1635
1636 function renderSeodPagination() {
1637 var $pag = $('#mxch-seod-pagination');
1638 $pag.empty();
1639
1640 if (seodState.pages <= 1) return;
1641
1642 var p = seodState.page, total = seodState.pages;
1643
1644 if (p > 1) {
1645 $pag.append('<button type="button" class="mxch-seod-page-btn" data-page="' + (p - 1) + '">&larr; Prev</button>');
1646 }
1647 $pag.append('<span class="mxch-seod-page-info">Page ' + p + ' of ' + total + '</span>');
1648 if (p < total) {
1649 $pag.append('<button type="button" class="mxch-seod-page-btn" data-page="' + (p + 1) + '">Next &rarr;</button>');
1650 }
1651 }
1652
1653 function updateSeodScanBtn(unscoredCount) {
1654 var $btn = $('#mxch-seod-scan-all');
1655 if (unscoredCount > 0) {
1656 $btn.show().text('Scan Unscored (' + unscoredCount + ')');
1657 } else {
1658 $btn.hide();
1659 }
1660 $('#mxch-seod-scan-status').text('');
1661 }
1662
1663 function openSeoModal(postId) {
1664 closeSeoModal(); // close any existing modal
1665 seodState.expandedId = postId;
1666
1667 var $row = $('.mxch-seod-row[data-post-id="' + postId + '"]');
1668 var title = $row.find('.mxch-seod-title').text() || 'Post #' + postId;
1669
1670 var $overlay = $(
1671 '<div class="mxch-seod-modal-overlay">' +
1672 '<div class="mxch-seod-modal">' +
1673 '<div class="mxch-seod-modal-header">' +
1674 '<h3 class="mxch-seod-modal-title">' + escapeHtml(title) + '</h3>' +
1675 '<button type="button" class="mxch-seod-modal-close" title="Close">' +
1676 '<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>' +
1677 '</button>' +
1678 '</div>' +
1679 '<div class="mxch-seod-modal-body">' +
1680 '<div class="mxch-seod-detail" data-post-id="' + postId + '">' +
1681 '<div class="mxch-seod-detail-loading">' +
1682 '<div class="mxch-seod-spinner"></div>' +
1683 '<span>Analyzing&hellip;</span>' +
1684 '</div>' +
1685 '</div>' +
1686 '</div>' +
1687 '</div>' +
1688 '</div>'
1689 );
1690
1691 $('body').append($overlay);
1692 // Trigger reflow then add visible class for animation
1693 $overlay[0].offsetHeight;
1694 $overlay.addClass('mxch-seod-modal-visible');
1695
1696 var $detail = $overlay.find('.mxch-seod-detail');
1697
1698 // Run analysis
1699 seodState.expandAnalyzing = true;
1700 $.post(ajaxurl, {
1701 action: 'mxchat_seo_analyze',
1702 nonce: mxchatContent.nonce,
1703 post_id: postId,
1704 }).done(function(res) {
1705 if (res.success) {
1706 renderSeodDetail($detail, res.data, postId);
1707 // Update the row's score badge in the table too
1708 var score = res.data.score;
1709 var cls = score >= 80 ? 'mxch-seod-good' : score >= 50 ? 'mxch-seod-ok' : 'mxch-seod-bad';
1710 $row.find('.mxch-seod-score-badge')
1711 .removeClass('mxch-seod-good mxch-seod-ok mxch-seod-bad mxch-seod-unscored')
1712 .addClass(cls).text(score);
1713 } else {
1714 $detail.html('<div class="mxch-seod-detail-error">Analysis failed. Please try again.</div>');
1715 }
1716 }).fail(function() {
1717 $detail.html('<div class="mxch-seod-detail-error">Connection error. Please try again.</div>');
1718 }).always(function() {
1719 seodState.expandAnalyzing = false;
1720 });
1721 }
1722
1723 function renderSeodDetail($detail, data, postId) {
1724 var checks = data.checks, score = data.score, summary = data.summary;
1725 var icons = {
1726 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>',
1727 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>',
1728 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>',
1729 };
1730
1731 // Checks that require the Advanced Content Editor add-on to fix
1732 var addonChecks = { readability: true, internal_links: true, img_alt: true, featured_img: true };
1733
1734 var sorted = Object.keys(checks).sort(function(a, b) {
1735 var o = { fail: 0, warn: 1, pass: 2 };
1736 return (o[checks[a].status] || 2) - (o[checks[b].status] || 2);
1737 });
1738
1739 var html = '<div class="mxch-seod-checks">';
1740 var last = null;
1741 sorted.forEach(function(key) {
1742 var c = checks[key];
1743 if (last && last !== 'pass' && c.status === 'pass') {
1744 html += '<div class="mxch-seod-check-sep"></div>';
1745 }
1746 last = c.status;
1747
1748 // Show addon/pro badge for gated checks that aren't passing
1749 var badge = '';
1750 if (addonChecks[key] && c.status !== 'pass' && !mxchatContent.hasAdvancedContent) {
1751 if (mxchatContent.isActivated) {
1752 badge = ' <a href="https://mxchat.ai/advanced-content-editor/" target="_blank" class="mxch-seod-addon-badge">ADD-ON</a>';
1753 } else {
1754 badge = ' <a href="https://mxchat.ai/" target="_blank" class="mxch-seod-addon-badge mxch-seod-pro-badge">PRO</a>';
1755 }
1756 }
1757
1758 html += '<div class="mxch-seod-check mxch-seod-check-' + c.status + '">' +
1759 '<div class="mxch-seod-check-icon">' + icons[c.status] + '</div>' +
1760 '<div class="mxch-seod-check-text">' +
1761 '<span class="mxch-seod-check-label">' + escapeHtml(c.label) + badge + '</span>' +
1762 '<span class="mxch-seod-check-detail">' + escapeHtml(c.detail) + '</span>' +
1763 '</div>' +
1764 '</div>';
1765 });
1766 html += '</div>';
1767
1768 // AI Optimize button (only if there are issues)
1769 if (summary.fail > 0 || summary.warn > 0) {
1770 html += '<div class="mxch-seod-detail-actions">' +
1771 '<button type="button" class="mxch-seod-optimize-btn">' +
1772 '<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>' +
1773 ' AI Optimize' +
1774 '</button>' +
1775 '</div>';
1776 }
1777
1778 $detail.html(html);
1779
1780 // GSC placeholder for free/non-addon users
1781 if (!mxchatContent.hasGSC) {
1782 var badgeLabel = mxchatContent.isActivated ? 'ADD-ON' : 'PRO';
1783 var badgeClass = mxchatContent.isActivated ? '' : ' mxch-seod-pro-badge';
1784 var upgradeText = mxchatContent.isActivated ? 'Install Add-on' : 'Upgrade to Pro';
1785 var gscHtml =
1786 '<div class="mxch-gsc-placeholder">' +
1787 '<h4 class="mxch-gsc-placeholder-title">' +
1788 '<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>' +
1789 ' Search Performance' +
1790 '</h4>' +
1791 '<div class="mxch-gsc-placeholder-content">' +
1792 '<div class="mxch-gsc-stats">' +
1793 '<div class="mxch-gsc-stat"><span class="mxch-gsc-stat-value">42</span><span class="mxch-gsc-stat-label">Clicks</span></div>' +
1794 '<div class="mxch-gsc-stat"><span class="mxch-gsc-stat-value">1.2K</span><span class="mxch-gsc-stat-label">Impressions</span></div>' +
1795 '<div class="mxch-gsc-stat"><span class="mxch-gsc-stat-value">3.5%</span><span class="mxch-gsc-stat-label">CTR</span></div>' +
1796 '<div class="mxch-gsc-stat"><span class="mxch-gsc-stat-value">8.2</span><span class="mxch-gsc-stat-label">Avg Position</span></div>' +
1797 '</div>' +
1798 '<table class="mxch-gsc-table">' +
1799 '<thead><tr><th>Keyword</th><th>Clicks</th><th>Impr.</th><th>Position</th></tr></thead>' +
1800 '<tbody>' +
1801 '<tr><td>example keyword one</td><td>18</td><td>420</td><td>5.3</td></tr>' +
1802 '<tr><td>sample search term</td><td>14</td><td>380</td><td>7.1</td></tr>' +
1803 '<tr><td>another query phrase</td><td>10</td><td>290</td><td>12.4</td></tr>' +
1804 '</tbody>' +
1805 '</table>' +
1806 '</div>' +
1807 '<div class="mxch-gsc-placeholder-overlay">' +
1808 '<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>' +
1809 '<a href="https://mxchat.ai/" target="_blank" class="mxch-seod-addon-badge' + badgeClass + '">' + badgeLabel + '</a>' +
1810 '<a href="https://mxchat.ai/" target="_blank" class="mxch-cg-pro-upgrade-link">' + upgradeText + '</a>' +
1811 '</div>' +
1812 '</div>';
1813 $detail.append(gscHtml);
1814 }
1815 }
1816
1817 function closeSeoModal() {
1818 seodState.expandedId = null;
1819 var $overlay = $('.mxch-seod-modal-overlay');
1820 if ($overlay.length) {
1821 $overlay.removeClass('mxch-seod-modal-visible');
1822 setTimeout(function() { $overlay.remove(); }, 200);
1823 }
1824 }
1825
1826 function bulkSeoScan() {
1827 seodState.scanning = true;
1828 seodState.scanAborted = false;
1829 var $btn = $('#mxch-seod-scan-all');
1830 var $status = $('#mxch-seod-scan-status');
1831 $btn.hide();
1832
1833 // Show stop button
1834 if (!$('#mxch-seod-scan-stop').length) {
1835 $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>');
1836 }
1837 $('#mxch-seod-scan-stop').show();
1838 $status.text('Loading unscored posts...');
1839
1840 // Fetch ALL unscored post IDs across all pages
1841 var allIds = [];
1842 function fetchPage(page) {
1843 $.post(ajaxurl, {
1844 action: 'mxchat_seo_list_posts',
1845 nonce: mxchatContent.nonce,
1846 page: page,
1847 post_type: 'any',
1848 filter: 'unscored',
1849 search: '',
1850 }).done(function(res) {
1851 if (!res.success || !res.data.posts.length) {
1852 if (allIds.length === 0) {
1853 finishScan('All posts have been scanned.');
1854 return;
1855 }
1856 startScanning(allIds);
1857 return;
1858 }
1859 res.data.posts.forEach(function(p) { allIds.push(p.id); });
1860 if (page < res.data.pages) {
1861 $status.text('Loading unscored posts... (' + allIds.length + ' found)');
1862 fetchPage(page + 1);
1863 } else {
1864 startScanning(allIds);
1865 }
1866 }).fail(function() {
1867 finishScan('Error loading posts.');
1868 });
1869 }
1870
1871 function startScanning(ids) {
1872 var total = ids.length;
1873 var scanned = 0;
1874 var batchSize = 10;
1875 $status.html('<span class="mxch-seod-scan-progress">0 / ' + total + '</span>');
1876
1877 function updateRows(results) {
1878 $.each(results, function(pid, data) {
1879 var $row = $('.mxch-seod-row[data-post-id="' + pid + '"]');
1880 if ($row.length) {
1881 var score = data.score;
1882 var cls = score >= 80 ? 'mxch-seod-good' : score >= 50 ? 'mxch-seod-ok' : 'mxch-seod-bad';
1883 $row.find('.mxch-seod-score-badge')
1884 .removeClass('mxch-seod-unscored').addClass(cls).text(score);
1885 }
1886 });
1887 }
1888
1889 function scanNextBatch() {
1890 if (seodState.scanAborted) {
1891 finishScan('Stopped — ' + scanned + ' of ' + total + ' scanned.');
1892 loadSeoPosts();
1893 return;
1894 }
1895 if (scanned >= total) {
1896 finishScan('Done! ' + total + ' posts scanned.');
1897 loadSeoPosts();
1898 return;
1899 }
1900 var batch = ids.slice(scanned, scanned + batchSize);
1901 $status.html('<span class="mxch-seod-scan-progress">' + (scanned + 1) + ' / ' + total + '</span>');
1902 $.post(ajaxurl, {
1903 action: 'mxchat_seo_analyze_batch',
1904 nonce: mxchatContent.nonce,
1905 'post_ids[]': batch,
1906 }).done(function(res) {
1907 if (res.success && res.data.results) {
1908 updateRows(res.data.results);
1909 }
1910 }).always(function() {
1911 scanned += batch.length;
1912 $status.html('<span class="mxch-seod-scan-progress">' + scanned + ' / ' + total + '</span>');
1913 scanNextBatch();
1914 });
1915 }
1916 scanNextBatch();
1917 }
1918
1919 function finishScan(msg) {
1920 seodState.scanning = false;
1921 seodState.scanAborted = false;
1922 $('#mxch-seod-scan-stop').hide();
1923 $btn.show().prop('disabled', false);
1924 $status.text(msg);
1925 }
1926
1927 fetchPage(1);
1928 }
1929
1930 function runSeodOptimize(postId, $btn) {
1931 var origHtml = $btn.html();
1932 $btn.prop('disabled', true).html(
1933 '<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>' +
1934 ' Optimizing&hellip;'
1935 );
1936
1937 // Get the current checks to find what needs fixing
1938 $.post(ajaxurl, {
1939 action: 'mxchat_seo_analyze',
1940 nonce: mxchatContent.nonce,
1941 post_id: postId,
1942 }).done(function(res) {
1943 if (!res.success) {
1944 $btn.prop('disabled', false).html(origHtml);
1945 return;
1946 }
1947 var checks = res.data.checks;
1948 var prefs = mxchatContent.seoOptimize || {};
1949 var fields = [];
1950 if (prefs.meta_description !== false && checks.meta_desc && checks.meta_desc.status !== 'pass') fields.push('meta_description');
1951 if (prefs.seo_title !== false && checks.title_length && checks.title_length.status !== 'pass') fields.push('seo_title');
1952 if (prefs.slug !== false && checks.slug && checks.slug.status !== 'pass') fields.push('slug');
1953 // Readability, internal links, images require Advanced Content Editor add-on
1954 if (mxchatContent.hasAdvancedContent) {
1955 if (prefs.readability !== false && checks.readability && checks.readability.status !== 'pass') fields.push('readability');
1956 if (prefs.internal_links !== false && checks.internal_links && checks.internal_links.status !== 'pass') fields.push('internal_links');
1957 if (prefs.img_alt !== false && checks.img_alt && checks.img_alt.status !== 'pass') fields.push('img_alt');
1958 if (prefs.featured_img !== false && checks.featured_img && checks.featured_img.status !== 'pass') fields.push('featured_img');
1959 }
1960 if (!fields.length) fields.push('meta_description');
1961
1962 var done = 0;
1963 fields.forEach(function(field) {
1964 $.post(ajaxurl, {
1965 action: 'mxchat_seo_suggest',
1966 nonce: mxchatContent.nonce,
1967 post_id: postId,
1968 field: field,
1969 }).always(function() {
1970 if (++done >= fields.length) {
1971 // Re-analyze and re-render in modal
1972 if (seodState.expandedId === postId) {
1973 openSeoModal(postId);
1974 }
1975 $btn.prop('disabled', false).html(origHtml);
1976 }
1977 });
1978 });
1979 }).fail(function() {
1980 $btn.prop('disabled', false).html(origHtml);
1981 });
1982 }
1983
1984 function showNotice(message, type) {
1985 $('.mxch-cg-notice').remove();
1986 var typeClass = type === 'error' ? 'mxch-cg-notice-error' : 'mxch-cg-notice-success';
1987 var $notice = $('<div class="mxch-cg-notice ' + typeClass + '">' + escapeHtml(message) + '</div>');
1988 $('#mxch-cg-inline-form .mxch-cg-form').prepend($notice);
1989 setTimeout(function() { $notice.fadeOut(300, function() { $(this).remove(); }); }, 4000);
1990 }
1991
1992 // ─── Initialize ────────────────────────────────────────────────────
1993
1994 $(document).ready(function() {
1995 initNavigation();
1996 initInlineForm();
1997 initGeneration();
1998 initPreview();
1999 initChat();
2000 initSettingsAutoSave();
2001 initLeftTabs();
2002 initSeo();
2003 initSeoSection();
2004 initHistory();
2005 initStatusDropdown();
2006
2007 // Prevent interaction with locked pro feature toggles
2008 $('.mxch-cg-pro-locked .mxch-toggle-input').on('click', function(e) {
2009 e.preventDefault();
2010 return false;
2011 });
2012 });
2013
2014 })(jQuery);
2015