PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.49
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.49
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.49, at includes/admin/js/ai-textfield.js

973 lines 48.2 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 // Add CSS for animations directly to the document
96 function injectAnimationStyles() {
97 if ($('#king-addons-ai-animations').length === 0) {
98 const animationStyles = `
99 <style id="king-addons-ai-animations">
100 @keyframes kingAddonsPulse {
101 0% { box-shadow: 0 0 0 0 rgba(91,3,255,0.4), inset 0 0 0 1px rgba(91,3,255,0.4); }
102 50% { box-shadow: 0 0 0 5px rgba(91,3,255,0.2), inset 0 0 0 1px rgba(91,3,255,0.6); }
103 100% { box-shadow: 0 0 0 0 rgba(91,3,255,0.1), inset 0 0 0 1px rgba(91,3,255,0.4); }
104 }
105 @keyframes kingAddonsShine {
106 0% { background-position: -100px; }
107 40%, 100% { background-position: 140px; }
108 }
109 @keyframes kingAddonsRotatePulse {
110 0% { transform: scale(1) rotate(0deg); filter: brightness(1); }
111 50% { transform: scale(1.15) rotate(180deg); filter: brightness(1.2) drop-shadow(0 0 3px rgba(91,3,255,0.7)); }
112 100% { transform: scale(1) rotate(360deg); filter: brightness(1); }
113 }
114 @keyframes kingAddonsGlow {
115 0% { filter: brightness(1) drop-shadow(0 0 1px rgba(91,3,255,0.5)); }
116 50% { filter: brightness(1.3) drop-shadow(0 0 3px rgba(91,3,255,0.8)); }
117 100% { filter: brightness(1) drop-shadow(0 0 1px rgba(91,3,255,0.5)); }
118 }
119 .king-addons-field-pulsing {
120 animation: kingAddonsPulse 1.5s infinite cubic-bezier(0.66, 0, 0, 1) !important;
121 border-color: #5B03FF !important;
122 }
123 .king-addons-field-shine {
124 background-image: linear-gradient(90deg,
125 rgba(91,3,255,0) 0%,
126 rgba(91,3,255,0.2) 25%,
127 rgba(225,203,255,0.3) 50%,
128 rgba(91,3,255,0.2) 75%,
129 rgba(91,3,255,0) 100%);
130 background-position: -100px;
131 background-size: 140px 100%;
132 background-repeat: no-repeat;
133 animation: kingAddonsShine 2s infinite linear;
134 }
135 .king-addons-wysiwyg-active {
136 outline: 2px solid #5B03FF !important;
137 outline-offset: -2px;
138 transition: all 0.3s ease;
139 }
140 .king-addons-ai-buttons-wrapper {
141 display: inline-flex;
142 align-items: center;
143 gap: 12px;
144 position: relative;
145 z-index: 5;
146 transition: all 0.3s ease;
147 margin-top: 8px;
148 width: 100%;
149 }
150 .king-addons-ai-buttons-wrapper.is-processing .ai-generate-btn,
151 .king-addons-ai-buttons-wrapper.is-processing .ai-change-btn {
152 background: linear-gradient(135deg, #d6d6d6, #a0a0a0) !important;
153 opacity: 0.7;
154 cursor: default;
155 box-shadow: none !important;
156 pointer-events: none;
157 }
158 .king-addons-ai-buttons-wrapper.is-processing img {
159 filter: grayscale(100%);
160 }
161 .ai-prompt-container {
162 display: flex;
163 align-items: center;
164 gap: 6px;
165 margin: 8px 0;
166 position: relative;
167 z-index: 4;
168 }
169 .elementor-control-type-wysiwyg .ai-prompt-container {
170 margin-top: 6px;
171 margin-bottom: 10px;
172 width: 100%;
173 }
174 .ai-prompt-input {
175 flex: 1;
176 min-width: 0;
177 padding: 6px 10px;
178 border: 1px solid #ccc;
179 border-radius: 4px;
180 font-size: 12px;
181 }
182 .ai-prompt-input:focus {
183 border-color: #5B03FF;
184 box-shadow: 0 0 0 1px rgba(91,3,255,0.3);
185 outline: none;
186 }
187 .ai-prompt-examples {
188 font-size: 11px;
189 color: #6d7882;
190 margin-top: 4px;
191 line-height: 1.4;
192 }
193 .ai-prompt-examples strong {
194 color: #556068;
195 font-weight: 500;
196 }
197 .ai-prompt-cancel {
198 width: 24px;
199 height: 24px;
200 display: flex;
201 align-items: center;
202 justify-content: center;
203 border: 1px solid #ccc;
204 background: #fff;
205 border-radius: 4px;
206 cursor: pointer;
207 font-size: 14px;
208 color: #555;
209 }
210 .ai-prompt-submit {
211 position: relative;
212 display: flex;
213 align-items: center;
214 justify-content: center;
215 width: 32px;
216 height: 32px;
217 padding: 4px;
218 background-color: #fff;
219 border: 1px solid #ccc;
220 border-radius: 4px;
221 cursor: pointer;
222 transition: all 0.3s ease;
223 }
224 .ai-prompt-submit.is-processing {
225 background: linear-gradient(135deg, #E1CBFF, #5B03FF) !important;
226 border-color: transparent !important;
227 }
228 .ai-prompt-submit.is-processing img {
229 animation: kingAddonsRotatePulse 2s infinite ease-in-out;
230 }
231 .ai-prompt-submit.is-processing:after {
232 content: '';
233 position: absolute;
234 top: -2px;
235 left: -2px;
236 right: -2px;
237 bottom: -2px;
238 border-radius: 6px;
239 background: transparent;
240 animation: kingAddonsPulse 1.5s infinite cubic-bezier(0.66, 0, 0, 1);
241 z-index: -1;
242 }
243 </style>
244 `;
245 $('head').append(animationStyles);
246 }
247 }
248
249 // Function to inject AI buttons
250 function injectAiButtons($container) {
251
252 // Inject animation styles first
253 injectAnimationStyles();
254
255 // Handle standard text/textarea controls
256 $container.find('.elementor-control-type-text label.elementor-control-title, .elementor-control-type-textarea label.elementor-control-title').each(function(){
257 var $label = $(this);
258 var $ctrlWrap = $label.closest('.elementor-control');
259 // Skip CSS ID, CSS Classes, and inline-label controls
260 if (
261 $ctrlWrap.hasClass('elementor-control-_element_id') ||
262 $ctrlWrap.hasClass('elementor-control-_css_classes') ||
263 $ctrlWrap.hasClass('elementor-label-inline')
264 ) {
265 return;
266 }
267 var $input = $ctrlWrap.find('input[type="text"], textarea');
268
269 // Check if we already have buttons wrapper after this label
270 if (!$input.length || $label.next('.king-addons-ai-buttons-wrapper').length) {
271 return;
272 }
273
274 createAndAttachButtons($label, $input, $ctrlWrap, true);
275 });
276
277 // Handle WYSIWYG controls
278 $container.find('.elementor-control-type-wysiwyg .elementor-control-input-wrapper').each(function(){
279 var $inputWrapper = $(this);
280 var $ctrlWrap = $inputWrapper.closest('.elementor-control');
281 var $textarea = $ctrlWrap.find('textarea.elementor-wp-editor'); // Find the actual textarea
282
283 // Check if we already have buttons wrapper before this input wrapper
284 if (!$textarea.length || $ctrlWrap.find('.king-addons-ai-buttons-wrapper').length) {
285 return;
286 }
287
288 // For WYSIWYG, add buttons to the control wrapper before the input wrapper
289 createAndAttachButtons($ctrlWrap, $textarea, $ctrlWrap, false);
290 });
291 }
292
293 // Function to create and attach AI buttons
294 function createAndAttachButtons($attachTarget, $field, $ctrlWrap, attachAfterLabel) {
295 var fieldName = $field.attr('name') || $field.attr('id') || ''; // Use ID for WYSIWYG if name is not present
296 var isWysiwyg = $ctrlWrap.hasClass('elementor-control-type-wysiwyg');
297
298 // Create wrapper div for buttons
299 var $buttonsWrapper = $('<div class="king-addons-ai-buttons-wrapper"></div>');
300
301 // For WYSIWYG we need different placement
302 if (isWysiwyg) {
303 // Find the input wrapper which contains the editor
304 var $inputWrapper = $ctrlWrap.find('.elementor-control-input-wrapper');
305 if (!$inputWrapper.length) return;
306 }
307
308 // Create "Generate" button
309 var $btnLabel = $('<button type="button" class="ai-generate-btn ai-generate-btn--futuristic" title="AI Generate"></button>').css({
310 verticalAlign: 'middle',
311 padding: '4px 8px',
312 fontSize: '12px',
313 cursor: 'pointer',
314 background: 'linear-gradient(135deg, #E1CBFF, #5B03FF)',
315 border: 'none',
316 borderRadius: '4px',
317 color: '#ffffff',
318 display: 'inline-flex',
319 alignItems: 'center',
320 // boxShadow: '0 0 6px rgba(225,203,255,0.7), 0 0 12px rgba(91,3,255,0.5)',
321 boxShadow: 'none',
322 transition: 'box-shadow 0.3s ease'
323 });
324 $btnLabel.append(
325 $('<img>').attr('src', KingAddonsAiField.icon_url).css({ marginRight: '6px', width: '16px', height: '16px', verticalAlign: 'middle' }),
326 $('<span>').addClass('ai-generate-btn__label').text('AI Generate')
327 );
328 $btnLabel.hover(
329 function() { $(this).css('boxShadow', '0 0 8px rgba(225,203,255,0.9), 0 0 16px rgba(91,3,255,0.7)'); },
330 function() { $(this).css('boxShadow', 'none'); }
331 );
332
333 // Create "Change" button
334 var $changeBtn = $btnLabel.clone();
335 $changeBtn.removeClass('ai-generate-btn').addClass('ai-change-btn');
336 $changeBtn.find('span.ai-generate-btn__label').text('AI Change');
337 $changeBtn.attr('title', 'AI Change');
338 $changeBtn.find('img').attr('src', KingAddonsAiField.rewrite_icon_url || KingAddonsAiField.plugin_url + '/includes/admin/img/ai-rewrite.svg');
339
340 // Add hover effect to the change button - cloning doesn't preserve hover handlers
341 $changeBtn.hover(
342 function() { $(this).css('boxShadow', '0 0 8px rgba(225,203,255,0.9), 0 0 16px rgba(91,3,255,0.7)'); },
343 function() { $(this).css('boxShadow', 'none'); }
344 );
345
346 // Add buttons to wrapper
347 $buttonsWrapper.append($btnLabel).append($changeBtn);
348
349 // Add wrapper to DOM with proper placement
350 if (isWysiwyg) {
351 // For WYSIWYG, insert before the input wrapper
352 $inputWrapper.before($buttonsWrapper);
353 } else if (attachAfterLabel) {
354 // For regular text/textarea, after the label
355 $attachTarget.after($buttonsWrapper);
356 } else {
357 // Fallback (shouldn't normally happen)
358 $attachTarget.before($buttonsWrapper);
359 }
360
361
362 // Attach "Generate" button click handler
363 $btnLabel.on('click', function(e){
364 e.preventDefault();
365 var $originalBtn = $(this);
366 var $buttonsWrapper = $originalBtn.closest('.king-addons-ai-buttons-wrapper');
367
368 // Hide the buttons wrapper while loading
369 $buttonsWrapper.addClass('is-processing');
370
371 // Check API key validity and token limit first
372 checkTokenLimit(function(status) {
373 if (!status.apiKeyValid) {
374 // API key missing or invalid
375 var $errorMessage = $('<div class="king-addons-ai-error-message"></div>').css({
376 background: '#e7f3fe',
377 color: '#084d7a',
378 padding: '10px 15px',
379 borderRadius: '4px',
380 border: '1px solid #b6e0fe',
381 marginTop: '8px',
382 marginBottom: '8px',
383 fontSize: '13px',
384 fontWeight: '500',
385 lineHeight: '1.4',
386 display: 'flex',
387 alignItems: 'center',
388 justifyContent: 'space-between'
389 });
390 var settingsUrl = window.KingAddonsAiField && window.KingAddonsAiField.settings_url
391 ? window.KingAddonsAiField.settings_url
392 : '/wp-admin/admin.php?page=king-addons-ai-settings';
393 $errorMessage.html(
394 '<span>OpenAI API key is missing or invalid. Please configure your API key in AI Settings.</span>' +
395 '<a href="' + settingsUrl + '" style="color:#0073aa;text-decoration:underline;white-space:nowrap;margin-left:10px;" target="_blank">Settings</a>'
396 );
397 $errorMessage.insertAfter($buttonsWrapper).hide().fadeIn(200);
398 setTimeout(function() { $errorMessage.fadeOut(200, function() { $(this).remove(); }); }, 5000);
399 $buttonsWrapper.removeClass('is-processing');
400 return;
401 }
402 if (status.limitReached) {
403 // Show error if daily token limit reached
404 var $errorMessage = $('<div class="king-addons-ai-error-message"></div>').css({
405 background: '#ffecec',
406 color: '#d63638',
407 padding: '10px 15px',
408 borderRadius: '4px',
409 border: '1px solid #d63638',
410 marginTop: '8px',
411 marginBottom: '8px',
412 fontSize: '13px',
413 fontWeight: '500',
414 display: 'flex',
415 alignItems: 'center',
416 justifyContent: 'space-between'
417 });
418 var settingsUrl = window.KingAddonsAiField && window.KingAddonsAiField.settings_url
419 ? window.KingAddonsAiField.settings_url
420 : '/wp-admin/admin.php?page=king-addons-ai-settings';
421 $errorMessage.html(
422 '<span>Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.</span>' +
423 '<a href="' + settingsUrl + '" style="color:#0073aa;text-decoration:underline;white-space:nowrap;margin-left:10px;" target="_blank">Settings</a>'
424 );
425 $errorMessage.insertAfter($buttonsWrapper).hide().fadeIn(200);
426 setTimeout(function() { $errorMessage.fadeOut(200, function() { $(this).remove(); }); }, 5000);
427 $buttonsWrapper.removeClass('is-processing');
428 return;
429 }
430 // Continue with regular flow if preconditions met
431 var $promptContainer = $('<div class="ai-prompt-container"></div>');
432 var $promptInput = $('<input type="text" class="ai-prompt-input" placeholder="Enter your prompt..."/>');
433
434 // Create examples text based on field type
435 var $examplesText;
436 if (isWysiwyg) {
437 $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>');
438 } else {
439 $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>');
440 }
441
442 // Create the submit button - use our CSS class for styling instead of inline styles
443 var $submitBtn = $('<button type="button" class="ai-prompt-submit" title="Submit"></button>');
444 // Only add the image, no inline styling
445 $submitBtn.append($('<img>').attr('src', KingAddonsAiField.icon_url).css({ width: '16px', height: '16px' }));
446 var $cancelBtn = $('<button type="button" class="ai-prompt-cancel" title="Cancel">✕</button>');
447
448 // For WYSIWYG, place the prompt container after the buttons wrapper but before the editor
449 if (isWysiwyg) {
450 // Find the input wrapper which contains the editor
451 var $inputWrapper = $ctrlWrap.find('.elementor-control-input-wrapper');
452 if (!$inputWrapper.length) return;
453
454 // Append prompt container elements and place before the editor input wrapper
455 $promptContainer.append($promptInput, $submitBtn, $cancelBtn);
456 $inputWrapper.before($promptContainer);
457 // Add examples text after the prompt container
458 $promptContainer.after($examplesText);
459 $promptContainer.hide().fadeIn(200);
460 $examplesText.hide().fadeIn(200);
461 } else {
462 // For regular fields, place after the buttons wrapper
463 $promptContainer.append($promptInput, $submitBtn, $cancelBtn)
464 .insertAfter($buttonsWrapper).hide().fadeIn(200);
465 // Add examples text after the prompt container
466 $promptContainer.after($examplesText);
467 $examplesText.hide().fadeIn(200);
468 }
469
470 $promptInput.focus();
471 $promptInput.on('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); $submitBtn.trigger('click'); } });
472
473 $cancelBtn.on('click', function(){
474 $promptContainer.fadeOut(200, function(){
475 $(this).remove();
476 $examplesText.remove();
477 $buttonsWrapper.removeClass('is-processing');
478 });
479 });
480
481 $submitBtn.on('click', function(){
482 var userPrompt = $promptInput.val().trim();
483 if (!userPrompt) { $promptInput.css('borderColor', 'red'); return; }
484 $promptInput.prop('disabled', true);
485 // Instead of replacing with spinner, add processing class
486 $submitBtn.prop('disabled', true).addClass('is-processing');
487
488 // Add animation to the target field
489 if (isWysiwyg) {
490 // For WYSIWYG we need to target both the iframe and the wrapper
491 var $editorWrap = $field.closest('.wp-editor-container');
492 var $iframe = $editorWrap.find('iframe');
493
494 $editorWrap.addClass('king-addons-field-pulsing');
495 if ($iframe.length) {
496 $iframe.addClass('king-addons-wysiwyg-active');
497 // Also try to add a class to the iframe body
498 try {
499 $($iframe[0].contentDocument.body).addClass('king-addons-field-shine');
500 } catch(e) {
501 console.error('Could not access iframe body', e);
502 }
503 }
504 } else {
505 // For regular text inputs and textareas
506 $field.addClass('king-addons-field-pulsing');
507 $field.addClass('king-addons-field-shine');
508 }
509
510 // Use consistent parameter names with the Change API
511 $.post( KingAddonsAiField.ajax_url, {
512 action: KingAddonsAiField.generate_action || 'king_addons_ai_generate_text',
513 nonce: KingAddonsAiField.generate_nonce || KingAddonsAiField.nonce,
514 field_name: fieldName,
515 prompt: userPrompt, // Changed from 'value' to 'prompt' for consistency
516 editor_type: isWysiwyg ? 'wysiwyg' : 'text' // Explicitly tell backend what type of field this is
517 }, function(response){
518 if (response.success && response.data.text) {
519 updateFieldValue($field, response.data.text, isWysiwyg, response);
520
521 // Update token usage data if available
522 if (response.data.usage) {
523 updateTokenUsage(response.data.usage);
524 }
525 } else if (response.data && response.data.message) {
526 alert(response.data.message);
527 }
528 }
529 ).always(function(){
530 // Remove animation classes
531 if (isWysiwyg) {
532 var $editorWrap = $field.closest('.wp-editor-container');
533 var $iframe = $editorWrap.find('iframe');
534 $editorWrap.removeClass('king-addons-field-pulsing');
535 if ($iframe.length) {
536 $iframe.removeClass('king-addons-wysiwyg-active');
537 try {
538 $($iframe[0].contentDocument.body).removeClass('king-addons-field-shine');
539 } catch(e) {
540 console.error('Could not access iframe body', e);
541 }
542 }
543 } else {
544 $field.removeClass('king-addons-field-pulsing king-addons-field-shine');
545 }
546
547 // Remove button processing state
548 $submitBtn.removeClass('is-processing');
549
550 // Complete and clean up the UI
551 $promptContainer.fadeOut(200, function(){
552 $(this).remove();
553 $examplesText.remove();
554 $buttonsWrapper.removeClass('is-processing');
555 });
556 });
557 });
558 });
559 });
560
561 // Attach "Change" button click handler
562 $changeBtn.on('click', function(e){
563 e.preventDefault();
564 var $originalBtn = $(this);
565 var $buttonsWrapper = $originalBtn.closest('.king-addons-ai-buttons-wrapper');
566
567 // Add processing class instead of hiding
568 $buttonsWrapper.addClass('is-processing');
569
570 // Check API key validity and token limit first
571 checkTokenLimit(function(status) {
572 if (!status.apiKeyValid) {
573 // API key missing or invalid
574 var $errorMessage = $('<div class="king-addons-ai-error-message"></div>').css({
575 background: '#ffecec',
576 color: '#d63638',
577 padding: '10px 15px',
578 borderRadius: '4px',
579 border: '1px solid #d63638',
580 marginTop: '8px',
581 marginBottom: '8px',
582 fontSize: '13px',
583 fontWeight: '500',
584 display: 'flex',
585 alignItems: 'center',
586 justifyContent: 'space-between'
587 });
588 var settingsUrl = window.KingAddonsAiField && window.KingAddonsAiField.settings_url
589 ? window.KingAddonsAiField.settings_url
590 : '/wp-admin/admin.php?page=king-addons-ai-settings';
591 $errorMessage.html(
592 '<span>OpenAI API key is missing or invalid. Please configure your API key in AI Settings.</span>' +
593 '<a href="' + settingsUrl + '" style="color:#0073aa;text-decoration:underline;white-space:nowrap;margin-left:10px;" target="_blank">Settings</a>'
594 );
595 $errorMessage.insertAfter($buttonsWrapper).hide().fadeIn(200);
596 setTimeout(function() { $errorMessage.fadeOut(200, function() { $(this).remove(); }); }, 5000);
597 $buttonsWrapper.removeClass('is-processing');
598 return;
599 }
600 if (status.limitReached) {
601 // Show error if daily token limit reached
602 var $errorMessage = $('<div class="king-addons-ai-error-message"></div>').css({
603 background: '#ffecec',
604 color: '#d63638',
605 padding: '10px 15px',
606 borderRadius: '4px',
607 border: '1px solid #d63638',
608 marginTop: '8px',
609 marginBottom: '8px',
610 fontSize: '13px',
611 fontWeight: '500',
612 display: 'flex',
613 alignItems: 'center',
614 justifyContent: 'space-between'
615 });
616 var settingsUrl = window.KingAddonsAiField && window.KingAddonsAiField.settings_url
617 ? window.KingAddonsAiField.settings_url
618 : '/wp-admin/admin.php?page=king-addons-ai-settings';
619 $errorMessage.html(
620 '<span>Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.</span>' +
621 '<a href="' + settingsUrl + '" style="color:#0073aa;text-decoration:underline;white-space:nowrap;margin-left:10px;" target="_blank">Settings</a>'
622 );
623 $errorMessage.insertAfter($buttonsWrapper).hide().fadeIn(200);
624 setTimeout(function() { $errorMessage.fadeOut(200, function() { $(this).remove(); }); }, 5000);
625 $buttonsWrapper.removeClass('is-processing');
626 return;
627 }
628 // Continue with regular flow if preconditions met
629 var originalText = getFieldValue($field, isWysiwyg);
630
631 var $promptContainer = $('<div class="ai-prompt-container"></div>');
632 var $promptInput = $('<input type="text" class="ai-prompt-input" placeholder="Enter change prompt..."/>');
633
634 // Create examples text based on field type
635 var $examplesText;
636 if (isWysiwyg) {
637 $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>');
638 } else {
639 $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>');
640 }
641
642 var $submitBtn = $('<button type="button" class="ai-prompt-submit" title="Apply"></button>');
643 $submitBtn.append($('<img>').attr('src', KingAddonsAiField.icon_url).css({ width: '16px', height: '16px' }));
644 var $cancelBtn = $('<button type="button" class="ai-prompt-cancel" title="Cancel">✕</button>');
645
646 // For WYSIWYG, place the prompt container after the buttons wrapper but before the editor
647 if (isWysiwyg) {
648 // Find the input wrapper which contains the editor
649 var $inputWrapper = $ctrlWrap.find('.elementor-control-input-wrapper');
650 if (!$inputWrapper.length) return;
651
652 // Append prompt container elements and place before the editor input wrapper
653 $promptContainer.append($promptInput, $submitBtn, $cancelBtn);
654 $inputWrapper.before($promptContainer);
655 // Add examples text after the prompt container
656 $promptContainer.after($examplesText);
657 $promptContainer.hide().fadeIn(200);
658 $examplesText.hide().fadeIn(200);
659 } else {
660 // For regular fields, place after the buttons wrapper
661 $promptContainer.append($promptInput, $submitBtn, $cancelBtn)
662 .insertAfter($buttonsWrapper).hide().fadeIn(200);
663 // Add examples text after the prompt container
664 $promptContainer.after($examplesText);
665 $examplesText.hide().fadeIn(200);
666 }
667
668 $promptInput.focus().on('keydown', function(ev){ if(ev.key==='Enter'){ ev.preventDefault(); $submitBtn.click(); }});
669
670 $cancelBtn.on('click', function(){
671 $promptContainer.fadeOut(200, function(){
672 $(this).remove();
673 $examplesText.remove();
674 $buttonsWrapper.removeClass('is-processing');
675 });
676 });
677
678 $submitBtn.on('click', function(){
679 var promptVal = $promptInput.val().trim();
680 if(!promptVal){ $promptInput.css('borderColor','red'); return; }
681 $promptInput.prop('disabled',true);
682 $submitBtn.prop('disabled',true).addClass('is-processing');
683
684 // Add animation to the target field
685 if (isWysiwyg) {
686 // For WYSIWYG we need to target both the iframe and the wrapper
687 var $editorWrap = $field.closest('.wp-editor-container');
688 var $iframe = $editorWrap.find('iframe');
689
690 $editorWrap.addClass('king-addons-field-pulsing');
691 if ($iframe.length) {
692 $iframe.addClass('king-addons-wysiwyg-active');
693 // Also try to add a class to the iframe body
694 try {
695 $($iframe[0].contentDocument.body).addClass('king-addons-field-shine');
696 } catch(e) {
697 console.error('Could not access iframe body', e);
698 }
699 }
700 } else {
701 // For regular text inputs and textareas
702 $field.addClass('king-addons-field-pulsing');
703 $field.addClass('king-addons-field-shine');
704 }
705
706 $.post( KingAddonsAiField.ajax_url, {
707 action: KingAddonsAiField.change_action,
708 nonce: KingAddonsAiField.change_nonce,
709 field_name: fieldName,
710 prompt: promptVal,
711 original: originalText,
712 editor_type: isWysiwyg ? 'wysiwyg' : 'text', // Explicitly tell backend what type of field this is
713 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
714 }, function(resp){
715 if(resp.success && resp.data.text){
716 updateFieldValue($field, resp.data.text, isWysiwyg, resp);
717
718 // Update token usage data if available
719 if (resp.data.usage) {
720 updateTokenUsage(resp.data.usage);
721 }
722 } else if(resp.data && resp.data.message){
723 alert(resp.data.message);
724 }
725 }
726 ).always(function(){
727 // Remove animation classes
728 if (isWysiwyg) {
729 var $editorWrap = $field.closest('.wp-editor-container');
730 var $iframe = $editorWrap.find('iframe');
731 $editorWrap.removeClass('king-addons-field-pulsing');
732 if ($iframe.length) {
733 $iframe.removeClass('king-addons-wysiwyg-active');
734 try {
735 $($iframe[0].contentDocument.body).removeClass('king-addons-field-shine');
736 } catch(e) {
737 console.error('Could not access iframe body', e);
738 }
739 }
740 } else {
741 $field.removeClass('king-addons-field-pulsing king-addons-field-shine');
742 }
743
744 // Remove button processing state
745 $submitBtn.removeClass('is-processing');
746
747 // Complete and clean up the UI
748 $promptContainer.fadeOut(200, function(){
749 $(this).remove();
750 $examplesText.remove();
751 $buttonsWrapper.removeClass('is-processing');
752 });
753 });
754 });
755 });
756 });
757 }
758
759 // Function to update field value (handles WYSIWYG)
760 function updateFieldValue($field, value, isWysiwyg, response) {
761 // Check if we need to append instead of replace
762 var appendMode = response && response.data && response.data.append_mode === true;
763 var originalContent = appendMode ? (response.data.original || '') : '';
764
765
766 if (isWysiwyg) {
767 var editorId = $field.attr('id');
768
769 // Log for debugging
770
771 // Check if we have a valid editor ID
772 if (!editorId) {
773 console.error('King Addons: No editor ID found for WYSIWYG field');
774 if (appendMode) {
775 // Append the new content to the original
776 $field.val(originalContent + '\n\n' + value);
777 } else {
778 $field.val(value);
779 }
780 $field.trigger('input');
781 return;
782 }
783
784 // Additional client-side cleanup for WYSIWYG
785 if (value) {
786 // Remove any code fence markers that might have been returned from API
787 value = value.replace(/^```(?:html|HTML)?\s*/g, '');
788 value = value.replace(/```\s*$/g, '');
789
790 // Ensure proper paragraph formatting for WYSIWYG
791 // Only add paragraph tags if they're not already present
792 if (!value.includes('<p>') && !value.includes('<div>')) {
793 // First, normalize all types of line breaks
794 value = value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
795
796 // Look for patterns that might indicate paragraphs
797 var paragraphDelimiter = /\n\s*\n/;
798
799 // If no double line breaks, look for single line breaks that might be paragraph breaks
800 if (!paragraphDelimiter.test(value) && value.includes('\n')) {
801 // Split by newlines and wrap each non-empty line in paragraph tags
802 value = value.split('\n')
803 .filter(function(para) { return para.trim().length > 0; })
804 .map(function(para) { return '<p>' + para.trim() + '</p>'; })
805 .join('');
806 } else {
807 // Split by double newlines and wrap in paragraph tags
808 value = value.split(paragraphDelimiter).map(function(paragraph) {
809 // Handle single newlines within a paragraph as <br> tags
810 return '<p>' + paragraph.trim().replace(/\n/g, '<br>') + '</p>';
811 }).join('');
812 }
813
814 }
815 }
816
817 try {
818 // Check if TinyMCE is available and the editor exists
819 if (window.tinymce && tinymce.get(editorId)) {
820 var editor = tinymce.get(editorId);
821
822 if (!editor.isHidden()) { // Visual mode
823
824 if (appendMode) {
825 // Get current content and append the new content
826 // Make sure we use the original from the server, not the current content
827 // which may have been modified by the user after the request was sent
828 // Add a proper separator between existing and new content for HTML
829 var existingContent = originalContent || '';
830 var newContent = value || '';
831
832 // Only add paragraph separator if one doesn't already exist at the end of original content
833 if (existingContent && !existingContent.trim().endsWith('</p>')) {
834 existingContent = existingContent + '<p></p>';
835 }
836
837 editor.setContent(existingContent + newContent);
838 } else {
839 editor.setContent(value);
840 }
841
842 editor.save(); // Sync with textarea
843 } else { // Text mode
844
845 if (appendMode) {
846 // Append the new content to the original with proper HTML separation
847 var existingContent = originalContent || '';
848 var newContent = value || '';
849
850 // For text mode, we should still preserve HTML structure
851 if (existingContent && !existingContent.trim().endsWith('</p>') &&
852 (existingContent.includes('<p>') || newContent.includes('<p>'))) {
853 existingContent = existingContent + '<p></p>';
854 } else if (existingContent) {
855 // Simple text mode, add double line break
856 existingContent = existingContent + '\n\n';
857 }
858
859 $field.val(existingContent + newContent);
860 } else {
861 $field.val(value);
862 }
863 }
864 } else {
865 // TinyMCE not available or editor not initialized
866
867 if (appendMode) {
868 // Append the new content to the original
869 $field.val(originalContent + '\n\n' + value);
870 } else {
871 $field.val(value);
872 }
873 }
874 } catch (e) {
875 console.error('King Addons: Error updating WYSIWYG content', e);
876 // Fallback - set the textarea value directly
877 if (appendMode) {
878 // Maintain HTML structure in fallback case
879 var existingContent = originalContent || '';
880 var newContent = value || '';
881
882 // Add paragraph separator if needed
883 if (existingContent && !existingContent.trim().endsWith('</p>') &&
884 (existingContent.includes('<p>') || newContent.includes('<p>'))) {
885 existingContent = existingContent + '<p></p>';
886 } else if (existingContent) {
887 existingContent = existingContent + '\n\n';
888 }
889
890 $field.val(existingContent + newContent);
891 } else {
892 $field.val(value);
893 }
894 }
895 } else {
896 // Standard text field
897 if (appendMode) {
898 // Append the new content to the original for text fields
899 $field.val(originalContent + '\n\n' + value);
900 } else {
901 $field.val(value);
902 }
903 }
904
905 // Trigger change events to ensure Elementor detects the change
906 $field.trigger('input');
907 $field.trigger('change');
908
909 // For WYSIWYG, also try to trigger a TinyMCE change event if available
910 if (isWysiwyg && window.tinymce && tinymce.get($field.attr('id'))) {
911 try {
912 tinymce.get($field.attr('id')).fire('change');
913 } catch (e) {
914 console.error('King Addons: Error triggering TinyMCE change event', e);
915 }
916 }
917 }
918
919 // Function to get field value (handles WYSIWYG)
920 function getFieldValue($field, isWysiwyg) {
921 if (isWysiwyg) {
922 var editorId = $field.attr('id');
923 if (window.tinymce && tinymce.get(editorId) && !tinymce.get(editorId).isHidden()) { // Visual mode
924 return tinymce.get(editorId).getContent();
925 } else { // Text mode or editor not initialized
926 return $field.val();
927 }
928 }
929 return $field.val();
930 }
931
932 // Function to setup MutationObserver to detect controls changes
933 function setupControlsObserver(panel) {
934 // Observe the entire panel for any control changes (e.g., section tabs)
935 var $controlsContainer = panel.$el;
936
937
938 activeObservers.forEach(function(observer) { observer.disconnect(); });
939 activeObservers = [];
940
941 var observer = new MutationObserver(function(mutations) {
942 // Use a small delay to allow Elementor to finish rendering, especially for complex controls
943 setTimeout(function() {
944 injectAiButtons($controlsContainer);
945 }, 50);
946 });
947
948 observer.observe($controlsContainer[0], { childList: true, subtree: true });
949 activeObservers.push(observer);
950
951 // Initial injection, with a delay
952 setTimeout(function() {
953 injectAiButtons($controlsContainer);
954 }, 150);
955 }
956
957 // On widget panel open, setup the observer
958 elementor.hooks.addAction('panel/open_editor/widget', function(panel) {
959 setTimeout(function() { setupControlsObserver(panel); }, 250); // Increased delay for initial setup
960 });
961
962 // Also monitor section changes
963 elementor.channels.editor.on('section:activated', function(sectionName, editor) {
964 var panel = editor.getOption('editedElementView').getContainer().panel;
965 if (panel && panel.$el) {
966 // When a section is activated, reinitialize observer and injection
967 setTimeout(function() {
968 setupControlsObserver(panel);
969 }, 150); // Delay for section rendering
970 }
971 });
972
973 })(jQuery, window.elementor);