PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / admin / js / ai-textfield.js

ai-textfield.js in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.83, at includes/admin/js/ai-textfield.js

1,074 lines 52.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function($, elementor){
2 'use strict';
3
4
5 // Debug check for KingAddonsAiField
6
7 // Check if settings exist
8 if (!window.KingAddonsAiField || typeof window.KingAddonsAiField !== 'object') {
9 window.KingAddonsAiField = window.KingAddonsAiField || {
10 ajax_url: ajaxurl || '',
11 generate_action: 'king_addons_ai_generate_text',
12 generate_nonce: '',
13 change_action: 'king_addons_ai_change_text',
14 change_nonce: '',
15 settings_url: ''
16 };
17 }
18
19 // Track active observers to avoid duplicates
20 var activeObservers = [];
21
22 // Track token usage
23 var tokenUsageData = {
24 initialized: false,
25 dailyUsed: 0,
26 dailyLimit: 0,
27 limitReached: false,
28 apiKeyValid: false
29 };
30
31 // Function to check if the token limit has been reached
32 function checkTokenLimit(callback) {
33 if (tokenUsageData.initialized) {
34 if (callback) {
35 callback({
36 limitReached: tokenUsageData.limitReached,
37 apiKeyValid: Boolean(tokenUsageData.apiKeyValid),
38 });
39 }
40 // Return true if either the token limit is reached or the API key is invalid
41 return tokenUsageData.limitReached || !tokenUsageData.apiKeyValid;
42 }
43
44 // Fetch current token usage data
45 $.post(KingAddonsAiField.ajax_url, {
46 action: 'king_addons_ai_check_tokens',
47 nonce: KingAddonsAiField.generate_nonce
48 }, function(response) {
49 if (response.success && response.data) {
50 tokenUsageData.initialized = true;
51 tokenUsageData.dailyUsed = parseInt(response.data.daily_used || 0);
52 tokenUsageData.dailyLimit = parseInt(response.data.daily_limit || 0);
53 tokenUsageData.limitReached = response.data.limit_reached === true;
54 tokenUsageData.apiKeyValid = response.data.api_key_valid === true;
55
56
57 if (callback) {
58 callback({
59 limitReached: tokenUsageData.limitReached,
60 apiKeyValid: tokenUsageData.apiKeyValid,
61 });
62 }
63 } else {
64 // If there's an error, assume both token limit and API key are OK
65 if (callback) {
66 callback({
67 limitReached: false,
68 apiKeyValid: true,
69 });
70 }
71 }
72 }).fail(function() {
73 // On failure, assume both token limit and API key are OK
74 if (callback) {
75 callback({
76 limitReached: false,
77 apiKeyValid: true,
78 });
79 }
80 });
81 }
82
83 // Update token usage data after API calls
84 function updateTokenUsage(usageData) {
85 if (usageData && typeof usageData === 'object') {
86 tokenUsageData.initialized = true;
87 tokenUsageData.dailyUsed = parseInt(usageData.daily_used || 0);
88 tokenUsageData.dailyLimit = parseInt(usageData.daily_limit || 0);
89 tokenUsageData.limitReached =
90 tokenUsageData.dailyLimit > 0 && tokenUsageData.dailyUsed >= tokenUsageData.dailyLimit;
91
92 }
93 }
94
95 /**
96 * Flag the editor body when Elementor's panel is dark.
97 *
98 * Elementor has no stable "dark panel" class here, and its ui_theme
99 * preference can be light, dark, or auto (following the OS), so the panel's
100 * own background is measured instead. That keeps the AI controls readable
101 * whichever way the theme was arrived at.
102 */
103 function syncPanelTheme() {
104 var isDark = null;
105
106 try {
107 // Walk up from the control area to the first element that actually
108 // paints a background. The panel's inner wrappers are transparent,
109 // so asking any single one of them tells us nothing.
110 var node = document.querySelector('#elementor-controls')
111 || document.querySelector('#elementor-panel-content-wrapper')
112 || document.querySelector('#elementor-panel');
113
114 while (node) {
115 var parts = getComputedStyle(node).backgroundColor.match(/[\d.]+/g);
116 if (parts && parts.length >= 3 && (parts.length < 4 || parseFloat(parts[3]) > 0)) {
117 var luminance = (0.2126 * parts[0] + 0.7152 * parts[1] + 0.0722 * parts[2]) / 255;
118 isDark = luminance < 0.5;
119 break;
120 }
121 node = node.parentElement;
122 }
123 } catch (e) {
124 // Fall through to the media query below.
125 }
126
127 if (isDark === null) {
128 try {
129 isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
130 } catch (e) {
131 isDark = false;
132 }
133 }
134
135 $('body').toggleClass('king-addons-ai-dark', !!isDark);
136 }
137
138 // Keep up with the OS switching themes while the editor is open.
139 try {
140 var colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
141 var onSchemeChange = function() { syncPanelTheme(); };
142 if (colorSchemeQuery.addEventListener) {
143 colorSchemeQuery.addEventListener('change', onSchemeChange);
144 } else if (colorSchemeQuery.addListener) {
145 colorSchemeQuery.addListener(onSchemeChange);
146 }
147 } catch (e) {
148 // No media query support: the measured panel background still applies.
149 }
150
151 // Add CSS for animations directly to the document
152 function injectAnimationStyles() {
153 if ($('#king-addons-ai-animations').length === 0) {
154 const animationStyles = `
155 <style id="king-addons-ai-animations">
156 @keyframes kingAddonsPulse {
157 0% { box-shadow: 0 0 0 0 rgba(91,3,255,0.4), inset 0 0 0 1px rgba(91,3,255,0.4); }
158 50% { box-shadow: 0 0 0 5px rgba(91,3,255,0.2), inset 0 0 0 1px rgba(91,3,255,0.6); }
159 100% { box-shadow: 0 0 0 0 rgba(91,3,255,0.1), inset 0 0 0 1px rgba(91,3,255,0.4); }
160 }
161 @keyframes kingAddonsShine {
162 0% { background-position: -100px; }
163 40%, 100% { background-position: 140px; }
164 }
165 @keyframes kingAddonsRotatePulse {
166 0% { transform: scale(1) rotate(0deg); filter: brightness(1); }
167 50% { transform: scale(1.15) rotate(180deg); filter: brightness(1.2) drop-shadow(0 0 3px rgba(91,3,255,0.7)); }
168 100% { transform: scale(1) rotate(360deg); filter: brightness(1); }
169 }
170 @keyframes kingAddonsGlow {
171 0% { filter: brightness(1) drop-shadow(0 0 1px rgba(91,3,255,0.5)); }
172 50% { filter: brightness(1.3) drop-shadow(0 0 3px rgba(91,3,255,0.8)); }
173 100% { filter: brightness(1) drop-shadow(0 0 1px rgba(91,3,255,0.5)); }
174 }
175 .king-addons-field-pulsing {
176 animation: kingAddonsPulse 1.5s infinite cubic-bezier(0.66, 0, 0, 1) !important;
177 border-color: #5B03FF !important;
178 }
179 .king-addons-field-shine {
180 background-image: linear-gradient(90deg,
181 rgba(91,3,255,0) 0%,
182 rgba(91,3,255,0.14) 50%,
183 rgba(91,3,255,0) 100%);
184 background-position: -100px;
185 background-size: 140px 100%;
186 background-repeat: no-repeat;
187 animation: kingAddonsShine 2s infinite linear;
188 }
189 .king-addons-wysiwyg-active {
190 outline: 2px solid #5B03FF !important;
191 outline-offset: -2px;
192 transition: all 0.3s ease;
193 }
194 .king-addons-ai-buttons-wrapper {
195 display: inline-flex;
196 align-items: center;
197 gap: 12px;
198 position: relative;
199 z-index: 5;
200 transition: all 0.3s ease;
201 margin-top: 8px;
202 width: 100%;
203 }
204 /* These two are unavailable while the prompt is open. Keeping
205 the violet, only dimmed, reads as "not right now" instead of
206 a grey block that looks like a styling failure. */
207 .king-addons-ai-buttons-wrapper.is-processing .ai-generate-btn,
208 .king-addons-ai-buttons-wrapper.is-processing .ai-change-btn {
209 background: rgba(91, 3, 255, .05) !important;
210 color: rgba(74, 2, 214, .42) !important;
211 cursor: default;
212 box-shadow: none !important;
213 transform: none !important;
214 pointer-events: none;
215 }
216 .king-addons-ai-buttons-wrapper.is-processing img {
217 opacity: .4 !important;
218 }
219 .ai-prompt-container {
220 display: flex;
221 align-items: center;
222 gap: 6px;
223 margin: 8px 0;
224 position: relative;
225 z-index: 4;
226 }
227 .elementor-control-type-wysiwyg .ai-prompt-container {
228 margin-top: 6px;
229 margin-bottom: 10px;
230 width: 100%;
231 }
232 .ai-prompt-input {
233 flex: 1;
234 min-width: 0;
235 padding: 6px 10px;
236 border: 1px solid #ccc;
237 border-radius: 4px;
238 font-size: 12px;
239 }
240 .ai-prompt-input:focus {
241 border-color: #5B03FF;
242 box-shadow: 0 0 0 1px rgba(91,3,255,0.3);
243 outline: none;
244 }
245 .ai-prompt-examples {
246 font-size: 11px;
247 color: #5c666f;
248 margin-top: 4px;
249 line-height: 1.4;
250 }
251 .ai-prompt-examples strong {
252 color: #3f4750;
253 font-weight: 600;
254 }
255
256 /*
257 * Elementor's panel can be light or dark, and the setting
258 * follows the OS when it is set to Auto, so the theme is
259 * detected at runtime and flagged on <body>. Without this
260 * the hint text is near-invisible on the dark panel.
261 */
262 body.king-addons-ai-dark .ai-prompt-examples {
263 color: #a7aeb6;
264 }
265 body.king-addons-ai-dark .ai-prompt-examples strong {
266 color: #e6e9ed;
267 }
268 body.king-addons-ai-dark .ai-prompt-input {
269 background: #2a2d32;
270 border-color: #45494f;
271 color: #e6e9ed;
272 }
273 body.king-addons-ai-dark .ai-prompt-input::placeholder {
274 color: #8d949c;
275 }
276 body.king-addons-ai-dark .ai-prompt-cancel {
277 background: #2a2d32;
278 border-color: #45494f;
279 color: #d5d9de;
280 }
281 body.king-addons-ai-dark .ai-prompt-cancel:hover {
282 background: #35393f;
283 }
284 body.king-addons-ai-dark .king-addons-ai-buttons-wrapper.is-processing .ai-generate-btn,
285 body.king-addons-ai-dark .king-addons-ai-buttons-wrapper.is-processing .ai-change-btn {
286 background: rgba(124, 77, 255, .09) !important;
287 color: rgba(201, 182, 255, .45) !important;
288 }
289
290 /* The tint needs a lighter hue and a brighter label on the
291 dark panel; the light-panel values are in ai-textfield.css. */
292 body.king-addons-ai-dark .ai-generate-btn,
293 body.king-addons-ai-dark .ai-change-btn {
294 --ka-ai-btn-bg: rgba(124, 77, 255, .22);
295 --ka-ai-btn-bg-hover: rgba(124, 77, 255, .32);
296 --ka-ai-btn-bg-active: rgba(124, 77, 255, .40);
297 --ka-ai-btn-fg: #C9B6FF;
298 --ka-ai-btn-ring: #8C7DFF;
299 }
300
301 body.king-addons-ai-dark .ai-generate-btn img,
302 body.king-addons-ai-dark .ai-change-btn img {
303 /* The icon files are white already - let them through. */
304 filter: none;
305 opacity: .92;
306 }
307
308 body.king-addons-ai-dark .ai-prompt-submit {
309 background: #7C4DFF;
310 color: #ffffff;
311 border-color: transparent;
312 }
313
314 body.king-addons-ai-dark .ai-prompt-submit:hover {
315 background: #6B3DEE;
316 }
317 .ai-prompt-cancel {
318 width: 24px;
319 height: 24px;
320 display: flex;
321 align-items: center;
322 justify-content: center;
323 border: 1px solid #ccc;
324 background: #fff;
325 border-radius: 4px;
326 cursor: pointer;
327 font-size: 14px;
328 color: #555;
329 }
330 .ai-prompt-submit {
331 position: relative;
332 display: flex;
333 align-items: center;
334 justify-content: center;
335 width: 32px;
336 height: 32px;
337 padding: 4px;
338 background-color: #fff;
339 border: 1px solid #ccc;
340 border-radius: 4px;
341 cursor: pointer;
342 transition: all 0.3s ease;
343 }
344 .ai-prompt-submit.is-processing {
345 background: #5B03FF !important;
346 border-color: transparent !important;
347 }
348 .ai-prompt-submit.is-processing img {
349 animation: kingAddonsRotatePulse 2s infinite ease-in-out;
350 }
351 .ai-prompt-submit.is-processing:after {
352 content: '';
353 position: absolute;
354 top: -2px;
355 left: -2px;
356 right: -2px;
357 bottom: -2px;
358 border-radius: 6px;
359 background: transparent;
360 animation: kingAddonsPulse 1.5s infinite cubic-bezier(0.66, 0, 0, 1);
361 z-index: -1;
362 }
363 </style>
364 `;
365 $('head').append(animationStyles);
366 }
367 }
368
369 // Function to inject AI buttons
370 function injectAiButtons($container) {
371
372 // Inject animation styles first
373 injectAnimationStyles();
374
375 // The panel is rebuilt on every open, so re-check the theme here.
376 syncPanelTheme();
377
378 // Handle standard text/textarea controls
379 $container.find('.elementor-control-type-text label.elementor-control-title, .elementor-control-type-textarea label.elementor-control-title').each(function(){
380 var $label = $(this);
381 var $ctrlWrap = $label.closest('.elementor-control');
382 // Skip CSS ID, CSS Classes, and inline-label controls
383 if (
384 $ctrlWrap.hasClass('elementor-control-_element_id') ||
385 $ctrlWrap.hasClass('elementor-control-_css_classes') ||
386 $ctrlWrap.hasClass('elementor-label-inline')
387 ) {
388 return;
389 }
390 var $input = $ctrlWrap.find('input[type="text"], textarea');
391
392 // Check if we already have buttons wrapper after this label
393 if (!$input.length || $label.next('.king-addons-ai-buttons-wrapper').length) {
394 return;
395 }
396
397 createAndAttachButtons($label, $input, $ctrlWrap, true);
398 });
399
400 // Handle WYSIWYG controls
401 $container.find('.elementor-control-type-wysiwyg .elementor-control-input-wrapper').each(function(){
402 var $inputWrapper = $(this);
403 var $ctrlWrap = $inputWrapper.closest('.elementor-control');
404 var $textarea = $ctrlWrap.find('textarea.elementor-wp-editor'); // Find the actual textarea
405
406 // Check if we already have buttons wrapper before this input wrapper
407 if (!$textarea.length || $ctrlWrap.find('.king-addons-ai-buttons-wrapper').length) {
408 return;
409 }
410
411 // For WYSIWYG, add buttons to the control wrapper before the input wrapper
412 createAndAttachButtons($ctrlWrap, $textarea, $ctrlWrap, false);
413 });
414 }
415
416 // Function to create and attach AI buttons
417 function createAndAttachButtons($attachTarget, $field, $ctrlWrap, attachAfterLabel) {
418 var fieldName = $field.attr('name') || $field.attr('id') || ''; // Use ID for WYSIWYG if name is not present
419 var isWysiwyg = $ctrlWrap.hasClass('elementor-control-type-wysiwyg');
420
421 // Create wrapper div for buttons
422 var $buttonsWrapper = $('<div class="king-addons-ai-buttons-wrapper"></div>');
423
424 // For WYSIWYG we need different placement
425 if (isWysiwyg) {
426 // Find the input wrapper which contains the editor
427 var $inputWrapper = $ctrlWrap.find('.elementor-control-input-wrapper');
428 if (!$inputWrapper.length) return;
429 }
430
431 // Create "Generate" button
432 // Appearance lives in ai-textfield.css so the buttons get real hover,
433 // active, focus and disabled states instead of inline overrides.
434 var $btnLabel = $('<button type="button" class="ai-generate-btn" title="AI Generate"></button>');
435 $btnLabel.append(
436 $('<img>').attr({ src: KingAddonsAiField.icon_url, alt: '' }),
437 $('<span>').addClass('ai-generate-btn__label').text('AI Generate')
438 );
439
440 // Create "Change" button
441 var $changeBtn = $btnLabel.clone();
442 $changeBtn.removeClass('ai-generate-btn').addClass('ai-change-btn');
443 $changeBtn.find('span.ai-generate-btn__label').text('AI Change');
444 $changeBtn.attr('title', 'AI Change');
445 $changeBtn.find('img').attr('src', KingAddonsAiField.rewrite_icon_url || KingAddonsAiField.plugin_url + '/includes/admin/img/ai-rewrite.svg');
446
447 // Add buttons to wrapper
448 $buttonsWrapper.append($btnLabel).append($changeBtn);
449
450 // Add wrapper to DOM with proper placement
451 if (isWysiwyg) {
452 // For WYSIWYG, insert before the input wrapper
453 $inputWrapper.before($buttonsWrapper);
454 } else if (attachAfterLabel) {
455 // For regular text/textarea, after the label
456 $attachTarget.after($buttonsWrapper);
457 } else {
458 // Fallback (shouldn't normally happen)
459 $attachTarget.before($buttonsWrapper);
460 }
461
462
463 // Attach "Generate" button click handler
464 $btnLabel.on('click', function(e){
465 e.preventDefault();
466 var $originalBtn = $(this);
467 var $buttonsWrapper = $originalBtn.closest('.king-addons-ai-buttons-wrapper');
468
469 // Hide the buttons wrapper while loading
470 $buttonsWrapper.addClass('is-processing');
471
472 // Check API key validity and token limit first
473 checkTokenLimit(function(status) {
474 if (!status.apiKeyValid) {
475 // API key missing or invalid
476 var $errorMessage = $('<div class="king-addons-ai-error-message"></div>').css({
477 background: '#e7f3fe',
478 color: '#084d7a',
479 padding: '10px 15px',
480 borderRadius: '4px',
481 border: '1px solid #b6e0fe',
482 marginTop: '8px',
483 marginBottom: '8px',
484 fontSize: '13px',
485 fontWeight: '500',
486 lineHeight: '1.4',
487 display: 'flex',
488 alignItems: 'center',
489 justifyContent: 'space-between'
490 });
491 var settingsUrl = window.KingAddonsAiField && window.KingAddonsAiField.settings_url
492 ? window.KingAddonsAiField.settings_url
493 : '/wp-admin/admin.php?page=king-addons-ai-settings';
494 $errorMessage.html(
495 '<span>' + ((window.KingAddonsAiField && KingAddonsAiField.missing_key_message) || 'The AI API key is missing or invalid. Please configure your API key in AI Settings.') + '</span>' +
496 '<a href="' + settingsUrl + '" style="color:#0073aa;text-decoration:underline;white-space:nowrap;margin-left:10px;" target="_blank">Settings</a>'
497 );
498 $errorMessage.insertAfter($buttonsWrapper).hide().fadeIn(200);
499 setTimeout(function() { $errorMessage.fadeOut(200, function() { $(this).remove(); }); }, 5000);
500 $buttonsWrapper.removeClass('is-processing');
501 return;
502 }
503 if (status.limitReached) {
504 // Show error if daily token limit reached
505 var $errorMessage = $('<div class="king-addons-ai-error-message"></div>').css({
506 background: '#ffecec',
507 color: '#d63638',
508 padding: '10px 15px',
509 borderRadius: '4px',
510 border: '1px solid #d63638',
511 marginTop: '8px',
512 marginBottom: '8px',
513 fontSize: '13px',
514 fontWeight: '500',
515 display: 'flex',
516 alignItems: 'center',
517 justifyContent: 'space-between'
518 });
519 var settingsUrl = window.KingAddonsAiField && window.KingAddonsAiField.settings_url
520 ? window.KingAddonsAiField.settings_url
521 : '/wp-admin/admin.php?page=king-addons-ai-settings';
522 $errorMessage.html(
523 '<span>Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.</span>' +
524 '<a href="' + settingsUrl + '" style="color:#0073aa;text-decoration:underline;white-space:nowrap;margin-left:10px;" target="_blank">Settings</a>'
525 );
526 $errorMessage.insertAfter($buttonsWrapper).hide().fadeIn(200);
527 setTimeout(function() { $errorMessage.fadeOut(200, function() { $(this).remove(); }); }, 5000);
528 $buttonsWrapper.removeClass('is-processing');
529 return;
530 }
531 // Continue with regular flow if preconditions met
532 var $promptContainer = $('<div class="ai-prompt-container"></div>');
533 var $promptInput = $('<input type="text" class="ai-prompt-input" placeholder="Enter your prompt..."/>');
534
535 // Create examples text based on field type
536 var $examplesText;
537 if (isWysiwyg) {
538 $examplesText = $('<div class="ai-prompt-examples">Examples: <strong>"Write about product benefits"</strong>, <strong>"Create a FAQ section with 3 questions"</strong>, <strong>"Write a product description for beginners"</strong></div>');
539 } else {
540 $examplesText = $('<div class="ai-prompt-examples">Examples: <strong>"Create a compelling headline"</strong>, <strong>"Write a call to action"</strong>, <strong>"Generate a short bio"</strong></div>');
541 }
542
543 // Create the submit button - use our CSS class for styling instead of inline styles
544 var $submitBtn = $('<button type="button" class="ai-prompt-submit" title="Submit"></button>');
545 // Only add the image, no inline styling
546 $submitBtn.append($('<img>').attr('src', KingAddonsAiField.icon_url).css({ width: '16px', height: '16px' }));
547 var $cancelBtn = $('<button type="button" class="ai-prompt-cancel" title="Cancel">✕</button>');
548
549 // For WYSIWYG, place the prompt container after the buttons wrapper but before the editor
550 if (isWysiwyg) {
551 // Find the input wrapper which contains the editor
552 var $inputWrapper = $ctrlWrap.find('.elementor-control-input-wrapper');
553 if (!$inputWrapper.length) return;
554
555 // Append prompt container elements and place before the editor input wrapper
556 $promptContainer.append($promptInput, $submitBtn, $cancelBtn);
557 $inputWrapper.before($promptContainer);
558 // Add examples text after the prompt container
559 $promptContainer.after($examplesText);
560 $promptContainer.hide().fadeIn(200);
561 $examplesText.hide().fadeIn(200);
562 } else {
563 // For regular fields, place after the buttons wrapper
564 $promptContainer.append($promptInput, $submitBtn, $cancelBtn)
565 .insertAfter($buttonsWrapper).hide().fadeIn(200);
566 // Add examples text after the prompt container
567 $promptContainer.after($examplesText);
568 $examplesText.hide().fadeIn(200);
569 }
570
571 $promptInput.focus();
572 $promptInput.on('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); $submitBtn.trigger('click'); } });
573
574 $cancelBtn.on('click', function(){
575 $promptContainer.fadeOut(200, function(){
576 $(this).remove();
577 $examplesText.remove();
578 $buttonsWrapper.removeClass('is-processing');
579 });
580 });
581
582 $submitBtn.on('click', function(){
583 var userPrompt = $promptInput.val().trim();
584 if (!userPrompt) { $promptInput.css('borderColor', 'red'); return; }
585 $promptInput.prop('disabled', true);
586 // Instead of replacing with spinner, add processing class
587 $submitBtn.prop('disabled', true).addClass('is-processing');
588
589 // Add animation to the target field
590 if (isWysiwyg) {
591 // For WYSIWYG we need to target both the iframe and the wrapper
592 var $editorWrap = $field.closest('.wp-editor-container');
593 var $iframe = $editorWrap.find('iframe');
594
595 $editorWrap.addClass('king-addons-field-pulsing');
596 if ($iframe.length) {
597 $iframe.addClass('king-addons-wysiwyg-active');
598 // Also try to add a class to the iframe body
599 try {
600 $($iframe[0].contentDocument.body).addClass('king-addons-field-shine');
601 } catch(e) {
602 // console.error('Could not access iframe body', e);
603 }
604 }
605 } else {
606 // For regular text inputs and textareas
607 $field.addClass('king-addons-field-pulsing');
608 $field.addClass('king-addons-field-shine');
609 }
610
611 // Use consistent parameter names with the Change API
612 $.post( KingAddonsAiField.ajax_url, {
613 action: KingAddonsAiField.generate_action || 'king_addons_ai_generate_text',
614 nonce: KingAddonsAiField.generate_nonce || KingAddonsAiField.nonce,
615 field_name: fieldName,
616 prompt: userPrompt, // Changed from 'value' to 'prompt' for consistency
617 editor_type: isWysiwyg ? 'wysiwyg' : 'text' // Explicitly tell backend what type of field this is
618 }, function(response){
619 if (response.success && response.data.text) {
620 updateFieldValue($field, response.data.text, isWysiwyg, response);
621
622 // Update token usage data if available
623 if (response.data.usage) {
624 updateTokenUsage(response.data.usage);
625 }
626 } else if (response.data && response.data.message) {
627 alert(response.data.message);
628 }
629 }
630 ).always(function(){
631 // Remove animation classes
632 if (isWysiwyg) {
633 var $editorWrap = $field.closest('.wp-editor-container');
634 var $iframe = $editorWrap.find('iframe');
635 $editorWrap.removeClass('king-addons-field-pulsing');
636 if ($iframe.length) {
637 $iframe.removeClass('king-addons-wysiwyg-active');
638 try {
639 $($iframe[0].contentDocument.body).removeClass('king-addons-field-shine');
640 } catch(e) {
641 // console.error('Could not access iframe body', e);
642 }
643 }
644 } else {
645 $field.removeClass('king-addons-field-pulsing king-addons-field-shine');
646 }
647
648 // Remove button processing state
649 $submitBtn.removeClass('is-processing');
650
651 // Complete and clean up the UI
652 $promptContainer.fadeOut(200, function(){
653 $(this).remove();
654 $examplesText.remove();
655 $buttonsWrapper.removeClass('is-processing');
656 });
657 });
658 });
659 });
660 });
661
662 // Attach "Change" button click handler
663 $changeBtn.on('click', function(e){
664 e.preventDefault();
665 var $originalBtn = $(this);
666 var $buttonsWrapper = $originalBtn.closest('.king-addons-ai-buttons-wrapper');
667
668 // Add processing class instead of hiding
669 $buttonsWrapper.addClass('is-processing');
670
671 // Check API key validity and token limit first
672 checkTokenLimit(function(status) {
673 if (!status.apiKeyValid) {
674 // API key missing or invalid
675 var $errorMessage = $('<div class="king-addons-ai-error-message"></div>').css({
676 background: '#ffecec',
677 color: '#d63638',
678 padding: '10px 15px',
679 borderRadius: '4px',
680 border: '1px solid #d63638',
681 marginTop: '8px',
682 marginBottom: '8px',
683 fontSize: '13px',
684 fontWeight: '500',
685 display: 'flex',
686 alignItems: 'center',
687 justifyContent: 'space-between'
688 });
689 var settingsUrl = window.KingAddonsAiField && window.KingAddonsAiField.settings_url
690 ? window.KingAddonsAiField.settings_url
691 : '/wp-admin/admin.php?page=king-addons-ai-settings';
692 $errorMessage.html(
693 '<span>' + ((window.KingAddonsAiField && KingAddonsAiField.missing_key_message) || 'The AI API key is missing or invalid. Please configure your API key in AI Settings.') + '</span>' +
694 '<a href="' + settingsUrl + '" style="color:#0073aa;text-decoration:underline;white-space:nowrap;margin-left:10px;" target="_blank">Settings</a>'
695 );
696 $errorMessage.insertAfter($buttonsWrapper).hide().fadeIn(200);
697 setTimeout(function() { $errorMessage.fadeOut(200, function() { $(this).remove(); }); }, 5000);
698 $buttonsWrapper.removeClass('is-processing');
699 return;
700 }
701 if (status.limitReached) {
702 // Show error if daily token limit reached
703 var $errorMessage = $('<div class="king-addons-ai-error-message"></div>').css({
704 background: '#ffecec',
705 color: '#d63638',
706 padding: '10px 15px',
707 borderRadius: '4px',
708 border: '1px solid #d63638',
709 marginTop: '8px',
710 marginBottom: '8px',
711 fontSize: '13px',
712 fontWeight: '500',
713 display: 'flex',
714 alignItems: 'center',
715 justifyContent: 'space-between'
716 });
717 var settingsUrl = window.KingAddonsAiField && window.KingAddonsAiField.settings_url
718 ? window.KingAddonsAiField.settings_url
719 : '/wp-admin/admin.php?page=king-addons-ai-settings';
720 $errorMessage.html(
721 '<span>Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.</span>' +
722 '<a href="' + settingsUrl + '" style="color:#0073aa;text-decoration:underline;white-space:nowrap;margin-left:10px;" target="_blank">Settings</a>'
723 );
724 $errorMessage.insertAfter($buttonsWrapper).hide().fadeIn(200);
725 setTimeout(function() { $errorMessage.fadeOut(200, function() { $(this).remove(); }); }, 5000);
726 $buttonsWrapper.removeClass('is-processing');
727 return;
728 }
729 // Continue with regular flow if preconditions met
730 var originalText = getFieldValue($field, isWysiwyg);
731
732 var $promptContainer = $('<div class="ai-prompt-container"></div>');
733 var $promptInput = $('<input type="text" class="ai-prompt-input" placeholder="Enter change prompt..."/>');
734
735 // Create examples text based on field type
736 var $examplesText;
737 if (isWysiwyg) {
738 $examplesText = $('<div class="ai-prompt-examples">Examples: <strong>"Add 2 paragraphs about benefits"</strong>, <strong>"Add 3 paragraphs about features"</strong>, <strong>"Make content more professional"</strong></div>');
739 } else {
740 $examplesText = $('<div class="ai-prompt-examples">Examples: <strong>"Make it more persuasive"</strong>, <strong>"Make it shorter and direct"</strong>, <strong>"Change tone to friendly"</strong></div>');
741 }
742
743 var $submitBtn = $('<button type="button" class="ai-prompt-submit" title="Apply"></button>');
744 $submitBtn.append($('<img>').attr('src', KingAddonsAiField.icon_url).css({ width: '16px', height: '16px' }));
745 var $cancelBtn = $('<button type="button" class="ai-prompt-cancel" title="Cancel">✕</button>');
746
747 // For WYSIWYG, place the prompt container after the buttons wrapper but before the editor
748 if (isWysiwyg) {
749 // Find the input wrapper which contains the editor
750 var $inputWrapper = $ctrlWrap.find('.elementor-control-input-wrapper');
751 if (!$inputWrapper.length) return;
752
753 // Append prompt container elements and place before the editor input wrapper
754 $promptContainer.append($promptInput, $submitBtn, $cancelBtn);
755 $inputWrapper.before($promptContainer);
756 // Add examples text after the prompt container
757 $promptContainer.after($examplesText);
758 $promptContainer.hide().fadeIn(200);
759 $examplesText.hide().fadeIn(200);
760 } else {
761 // For regular fields, place after the buttons wrapper
762 $promptContainer.append($promptInput, $submitBtn, $cancelBtn)
763 .insertAfter($buttonsWrapper).hide().fadeIn(200);
764 // Add examples text after the prompt container
765 $promptContainer.after($examplesText);
766 $examplesText.hide().fadeIn(200);
767 }
768
769 $promptInput.focus().on('keydown', function(ev){ if(ev.key==='Enter'){ ev.preventDefault(); $submitBtn.click(); }});
770
771 $cancelBtn.on('click', function(){
772 $promptContainer.fadeOut(200, function(){
773 $(this).remove();
774 $examplesText.remove();
775 $buttonsWrapper.removeClass('is-processing');
776 });
777 });
778
779 $submitBtn.on('click', function(){
780 var promptVal = $promptInput.val().trim();
781 if(!promptVal){ $promptInput.css('borderColor','red'); return; }
782 $promptInput.prop('disabled',true);
783 $submitBtn.prop('disabled',true).addClass('is-processing');
784
785 // Add animation to the target field
786 if (isWysiwyg) {
787 // For WYSIWYG we need to target both the iframe and the wrapper
788 var $editorWrap = $field.closest('.wp-editor-container');
789 var $iframe = $editorWrap.find('iframe');
790
791 $editorWrap.addClass('king-addons-field-pulsing');
792 if ($iframe.length) {
793 $iframe.addClass('king-addons-wysiwyg-active');
794 // Also try to add a class to the iframe body
795 try {
796 $($iframe[0].contentDocument.body).addClass('king-addons-field-shine');
797 } catch(e) {
798 // console.error('Could not access iframe body', e);
799 }
800 }
801 } else {
802 // For regular text inputs and textareas
803 $field.addClass('king-addons-field-pulsing');
804 $field.addClass('king-addons-field-shine');
805 }
806
807 $.post( KingAddonsAiField.ajax_url, {
808 action: KingAddonsAiField.change_action,
809 nonce: KingAddonsAiField.change_nonce,
810 field_name: fieldName,
811 prompt: promptVal,
812 original: originalText,
813 editor_type: isWysiwyg ? 'wysiwyg' : 'text', // Explicitly tell backend what type of field this is
814 instruction_context: 'Modify the original text based on the user instructions. If asked to add content, keep the original and expand it. If asked to change style, maintain the same information but change the tone. Return the complete modified text.' // Add clear context for the AI
815 }, function(resp){
816 if(resp.success && resp.data.text){
817 updateFieldValue($field, resp.data.text, isWysiwyg, resp);
818
819 // Update token usage data if available
820 if (resp.data.usage) {
821 updateTokenUsage(resp.data.usage);
822 }
823 } else if(resp.data && resp.data.message){
824 alert(resp.data.message);
825 }
826 }
827 ).always(function(){
828 // Remove animation classes
829 if (isWysiwyg) {
830 var $editorWrap = $field.closest('.wp-editor-container');
831 var $iframe = $editorWrap.find('iframe');
832 $editorWrap.removeClass('king-addons-field-pulsing');
833 if ($iframe.length) {
834 $iframe.removeClass('king-addons-wysiwyg-active');
835 try {
836 $($iframe[0].contentDocument.body).removeClass('king-addons-field-shine');
837 } catch(e) {
838 // console.error('Could not access iframe body', e);
839 }
840 }
841 } else {
842 $field.removeClass('king-addons-field-pulsing king-addons-field-shine');
843 }
844
845 // Remove button processing state
846 $submitBtn.removeClass('is-processing');
847
848 // Complete and clean up the UI
849 $promptContainer.fadeOut(200, function(){
850 $(this).remove();
851 $examplesText.remove();
852 $buttonsWrapper.removeClass('is-processing');
853 });
854 });
855 });
856 });
857 });
858 }
859
860 // Function to update field value (handles WYSIWYG)
861 function updateFieldValue($field, value, isWysiwyg, response) {
862 // Check if we need to append instead of replace
863 var appendMode = response && response.data && response.data.append_mode === true;
864 var originalContent = appendMode ? (response.data.original || '') : '';
865
866
867 if (isWysiwyg) {
868 var editorId = $field.attr('id');
869
870 // Log for debugging
871
872 // Check if we have a valid editor ID
873 if (!editorId) {
874 // console.error('King Addons: No editor ID found for WYSIWYG field');
875 if (appendMode) {
876 // Append the new content to the original
877 $field.val(originalContent + '\n\n' + value);
878 } else {
879 $field.val(value);
880 }
881 $field.trigger('input');
882 return;
883 }
884
885 // Additional client-side cleanup for WYSIWYG
886 if (value) {
887 // Remove any code fence markers that might have been returned from API
888 value = value.replace(/^```(?:html|HTML)?\s*/g, '');
889 value = value.replace(/```\s*$/g, '');
890
891 // Ensure proper paragraph formatting for WYSIWYG
892 // Only add paragraph tags if they're not already present
893 if (!value.includes('<p>') && !value.includes('<div>')) {
894 // First, normalize all types of line breaks
895 value = value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
896
897 // Look for patterns that might indicate paragraphs
898 var paragraphDelimiter = /\n\s*\n/;
899
900 // If no double line breaks, look for single line breaks that might be paragraph breaks
901 if (!paragraphDelimiter.test(value) && value.includes('\n')) {
902 // Split by newlines and wrap each non-empty line in paragraph tags
903 value = value.split('\n')
904 .filter(function(para) { return para.trim().length > 0; })
905 .map(function(para) { return '<p>' + para.trim() + '</p>'; })
906 .join('');
907 } else {
908 // Split by double newlines and wrap in paragraph tags
909 value = value.split(paragraphDelimiter).map(function(paragraph) {
910 // Handle single newlines within a paragraph as <br> tags
911 return '<p>' + paragraph.trim().replace(/\n/g, '<br>') + '</p>';
912 }).join('');
913 }
914
915 }
916 }
917
918 try {
919 // Check if TinyMCE is available and the editor exists
920 if (window.tinymce && tinymce.get(editorId)) {
921 var editor = tinymce.get(editorId);
922
923 if (!editor.isHidden()) { // Visual mode
924
925 if (appendMode) {
926 // Get current content and append the new content
927 // Make sure we use the original from the server, not the current content
928 // which may have been modified by the user after the request was sent
929 // Add a proper separator between existing and new content for HTML
930 var existingContent = originalContent || '';
931 var newContent = value || '';
932
933 // Only add paragraph separator if one doesn't already exist at the end of original content
934 if (existingContent && !existingContent.trim().endsWith('</p>')) {
935 existingContent = existingContent + '<p></p>';
936 }
937
938 editor.setContent(existingContent + newContent);
939 } else {
940 editor.setContent(value);
941 }
942
943 editor.save(); // Sync with textarea
944 } else { // Text mode
945
946 if (appendMode) {
947 // Append the new content to the original with proper HTML separation
948 var existingContent = originalContent || '';
949 var newContent = value || '';
950
951 // For text mode, we should still preserve HTML structure
952 if (existingContent && !existingContent.trim().endsWith('</p>') &&
953 (existingContent.includes('<p>') || newContent.includes('<p>'))) {
954 existingContent = existingContent + '<p></p>';
955 } else if (existingContent) {
956 // Simple text mode, add double line break
957 existingContent = existingContent + '\n\n';
958 }
959
960 $field.val(existingContent + newContent);
961 } else {
962 $field.val(value);
963 }
964 }
965 } else {
966 // TinyMCE not available or editor not initialized
967
968 if (appendMode) {
969 // Append the new content to the original
970 $field.val(originalContent + '\n\n' + value);
971 } else {
972 $field.val(value);
973 }
974 }
975 } catch (e) {
976 // console.error('King Addons: Error updating WYSIWYG content', e);
977 // Fallback - set the textarea value directly
978 if (appendMode) {
979 // Maintain HTML structure in fallback case
980 var existingContent = originalContent || '';
981 var newContent = value || '';
982
983 // Add paragraph separator if needed
984 if (existingContent && !existingContent.trim().endsWith('</p>') &&
985 (existingContent.includes('<p>') || newContent.includes('<p>'))) {
986 existingContent = existingContent + '<p></p>';
987 } else if (existingContent) {
988 existingContent = existingContent + '\n\n';
989 }
990
991 $field.val(existingContent + newContent);
992 } else {
993 $field.val(value);
994 }
995 }
996 } else {
997 // Standard text field
998 if (appendMode) {
999 // Append the new content to the original for text fields
1000 $field.val(originalContent + '\n\n' + value);
1001 } else {
1002 $field.val(value);
1003 }
1004 }
1005
1006 // Trigger change events to ensure Elementor detects the change
1007 $field.trigger('input');
1008 $field.trigger('change');
1009
1010 // For WYSIWYG, also try to trigger a TinyMCE change event if available
1011 if (isWysiwyg && window.tinymce && tinymce.get($field.attr('id'))) {
1012 try {
1013 tinymce.get($field.attr('id')).fire('change');
1014 } catch (e) {
1015 // console.error('King Addons: Error triggering TinyMCE change event', e);
1016 }
1017 }
1018 }
1019
1020 // Function to get field value (handles WYSIWYG)
1021 function getFieldValue($field, isWysiwyg) {
1022 if (isWysiwyg) {
1023 var editorId = $field.attr('id');
1024 if (window.tinymce && tinymce.get(editorId) && !tinymce.get(editorId).isHidden()) { // Visual mode
1025 return tinymce.get(editorId).getContent();
1026 } else { // Text mode or editor not initialized
1027 return $field.val();
1028 }
1029 }
1030 return $field.val();
1031 }
1032
1033 // Function to setup MutationObserver to detect controls changes
1034 function setupControlsObserver(panel) {
1035 // Observe the entire panel for any control changes (e.g., section tabs)
1036 var $controlsContainer = panel.$el;
1037
1038
1039 activeObservers.forEach(function(observer) { observer.disconnect(); });
1040 activeObservers = [];
1041
1042 var observer = new MutationObserver(function(mutations) {
1043 // Use a small delay to allow Elementor to finish rendering, especially for complex controls
1044 setTimeout(function() {
1045 injectAiButtons($controlsContainer);
1046 }, 50);
1047 });
1048
1049 observer.observe($controlsContainer[0], { childList: true, subtree: true });
1050 activeObservers.push(observer);
1051
1052 // Initial injection, with a delay
1053 setTimeout(function() {
1054 injectAiButtons($controlsContainer);
1055 }, 150);
1056 }
1057
1058 // On widget panel open, setup the observer
1059 elementor.hooks.addAction('panel/open_editor/widget', function(panel) {
1060 setTimeout(function() { setupControlsObserver(panel); }, 250); // Increased delay for initial setup
1061 });
1062
1063 // Also monitor section changes
1064 elementor.channels.editor.on('section:activated', function(sectionName, editor) {
1065 var panel = editor.getOption('editedElementView').getContainer().panel;
1066 if (panel && panel.$el) {
1067 // When a section is activated, reinitialize observer and injection
1068 setTimeout(function() {
1069 setupControlsObserver(panel);
1070 }, 150); // Delay for section rendering
1071 }
1072 });
1073
1074 })(jQuery, window.elementor);