PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.87
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.87
51.1.87 51.1.86 51.1.84 51.1.85 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 All 41 releases
← All changes | includes/admin/js/ai-textfield.js +1074 -0 51.1.2 → 51.1.87 View file →
@@ -1,0 +1,1074 @@
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);