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.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 51.1.51 All 36 releases
king-addons / includes / Admin.php

Admin.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.49, at includes/Admin.php

2,339 lines 93.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Admin class do all things for admin menu
5 */
6
7 namespace King_Addons;
8
9 use King_Addons\Wishlist\Wishlist_Settings;
10
11 if (!defined('ABSPATH')) {
12 exit; // Exit if accessed directly.
13 }
14
15 final class Admin
16 {
17 public function __construct()
18 {
19 if (is_admin()) {
20 // Use priority 5 to ensure the main menu is created BEFORE feature submenus (which use priority 10 or higher)
21 add_action('admin_menu', [$this, 'addAdminMenu'], 5);
22
23 // Always add the action, but check conditions inside addUpgradeMenu
24 add_action('admin_menu', [$this, 'addUpgradeMenu'], 9999999999); // Highest priority to add at the very end
25
26 // Reorder submenu items after ALL entries are registered.
27 add_action('admin_menu', [$this, 'reorderKingAddonsSubmenu'], 1000000000);
28
29 add_action('admin_init', [$this, 'createSettings']);
30 add_action('admin_init', [$this, 'createAiSettings']);
31
32 // Only register wishlist settings when the Wishlist extension is enabled.
33 $options = get_option('king_addons_options', []);
34 $wishlist_enabled = (!isset($options['ext_wishlist']) || $options['ext_wishlist'] === 'enabled')
35 && (defined('KING_ADDONS_EXT_WISHLIST') ? KING_ADDONS_EXT_WISHLIST : true);
36 if ($wishlist_enabled) {
37 add_action('admin_init', [$this, 'createWishlistSettings']);
38 }
39 add_action('admin_enqueue_scripts', [$this, 'enqueueUpgradeLinkScript']);
40 add_action('admin_enqueue_scripts', [$this, 'enqueueGlobalAdminStyles']);
41 }
42 }
43
44 function addAdminMenu(): void
45 {
46 global $menu;
47 $menu['54.0'] = array( '', 'read', 'separator-king-addons-top', '', 'wp-menu-separator elementor' );
48
49 add_menu_page(
50 'King Addons for Elementor',
51 'King Addons',
52 'manage_options',
53 'king-addons',
54 [$this, 'showDashboardV3'],
55 KING_ADDONS_URL . 'includes/admin/img/icon-for-admin.svg',
56 54.1
57 );
58
59 // Ensure the first submenu item is labeled "Dashboard" (instead of repeating "King Addons").
60 add_submenu_page(
61 'king-addons',
62 esc_html__('Dashboard', 'king-addons'),
63 esc_html__('Dashboard', 'king-addons'),
64 'manage_options',
65 'king-addons',
66 [$this, 'showDashboardV3']
67 );
68
69 add_submenu_page(
70 'king-addons',
71 'King Addons Settings',
72 'Settings',
73 'manage_options',
74 'king-addons-settings',
75 [$this, 'showSettingsPage']
76 );
77
78 // Add AI Settings submenu under King Addons (kept near Settings; final ordering is enforced later).
79 add_submenu_page(
80 'king-addons',
81 esc_html__('AI Settings', 'king-addons'),
82 esc_html__('AI Settings', 'king-addons'),
83 'manage_options',
84 'king-addons-ai-settings',
85 [$this, 'showAiSettingsPage']
86 );
87
88 // Get options for extension toggle checks (before any extension checks)
89 $options = get_option('king_addons_options', []);
90
91 // Check Wishlist extension toggle
92 $wishlist_enabled = (!isset($options['ext_wishlist']) || $options['ext_wishlist'] === 'enabled')
93 && (defined('KING_ADDONS_EXT_WISHLIST') ? KING_ADDONS_EXT_WISHLIST : true);
94 if ($wishlist_enabled) {
95 add_submenu_page(
96 'king-addons',
97 esc_html__('Wishlist', 'king-addons'),
98 esc_html__('Wishlist', 'king-addons'),
99 'manage_options',
100 'king-addons-wishlist',
101 [$this, 'renderWishlistPage']
102 );
103
104 add_submenu_page(
105 'king-addons',
106 esc_html__('Wishlist Analytics', 'king-addons'),
107 esc_html__('Wishlist Analytics', 'king-addons'),
108 'manage_options',
109 'king-addons-wishlist-analytics',
110 [$this, 'renderWishlistAnalyticsPage']
111 );
112 }
113
114 // Check Cookie / Consent Bar extension toggle
115 $cookie_consent_enabled = !isset($options['ext_cookie-consent']) || $options['ext_cookie-consent'] === 'enabled';
116 if ($cookie_consent_enabled && (defined('KING_ADDONS_EXT_COOKIE_CONSENT') ? KING_ADDONS_EXT_COOKIE_CONSENT : true) && class_exists('King_Addons\Cookie_Consent')) {
117 add_submenu_page(
118 'king-addons',
119 esc_html__('Cookie / Consent Bar', 'king-addons'),
120 esc_html__('Cookie / Consent Bar', 'king-addons'),
121 'manage_options',
122 'king-addons-cookie-consent',
123 [Cookie_Consent::instance(), 'render_admin_page']
124 );
125 }
126
127 // Check Age Gate extension toggle
128 $age_gate_enabled = !isset($options['ext_age-gate']) || $options['ext_age-gate'] === 'enabled';
129 if ($age_gate_enabled && (defined('KING_ADDONS_EXT_AGE_GATE') ? KING_ADDONS_EXT_AGE_GATE : true) && class_exists('King_Addons\Age_Gate')) {
130 add_submenu_page(
131 'king-addons',
132 esc_html__('Age Gate', 'king-addons'),
133 esc_html__('Age Gate', 'king-addons'),
134 'manage_options',
135 'king-addons-age-gate',
136 [Age_Gate::instance(), 'render_admin_page']
137 );
138 }
139
140 // Check Live Chat extension toggle
141 $live_chat_enabled = !isset($options['ext_live-chat']) || $options['ext_live-chat'] === 'enabled';
142 if ($live_chat_enabled && (defined('KING_ADDONS_EXT_LIVE_CHAT') ? KING_ADDONS_EXT_LIVE_CHAT : true) && class_exists('King_Addons\Live_Chat')) {
143 add_submenu_page(
144 'king-addons',
145 esc_html__('Live Chat', 'king-addons'),
146 esc_html__('Live Chat', 'king-addons'),
147 'manage_options',
148 'king-addons-live-chat',
149 [Live_Chat::instance(), 'render_admin_page']
150 );
151 }
152
153 // Check Docs & KB extension toggle
154 $docs_kb_enabled = !isset($options['ext_docs-kb']) || $options['ext_docs-kb'] === 'enabled';
155 if ($docs_kb_enabled && (defined('KING_ADDONS_EXT_DOCS_KB') ? KING_ADDONS_EXT_DOCS_KB : true) && class_exists('King_Addons\Docs_KB')) {
156 add_submenu_page(
157 'king-addons',
158 esc_html__('Docs & Knowledge Base', 'king-addons'),
159 esc_html__('Docs & Knowledge Base Builder', 'king-addons'),
160 'manage_options',
161 'king-addons-docs-kb',
162 [Docs_KB::instance(), 'render_admin_page']
163 );
164 }
165
166 // Check Activity Log extension toggle
167 $activity_log_enabled = !isset($options['ext_activity-log']) || $options['ext_activity-log'] === 'enabled';
168 if ($activity_log_enabled && class_exists('King_Addons\\Activity_Log')) {
169 add_submenu_page(
170 'king-addons',
171 esc_html__('Activity Log', 'king-addons'),
172 esc_html__('Activity Log', 'king-addons'),
173 'manage_options',
174 'king-addons-activity-log',
175 [Activity_Log::instance(), 'render_admin_page']
176 );
177 }
178
179 // Check if Form Builder widget is enabled
180 $form_builder_enabled = !isset($options['form-builder']) || $options['form-builder'] === 'enabled';
181 if (KING_ADDONS_WGT_FORM_BUILDER && $form_builder_enabled) {
182 add_submenu_page(
183 'king-addons',
184 esc_html__('Form Submissions', 'king-addons'),
185 esc_html__('Form Submissions', 'king-addons'),
186 'edit_posts',
187 'edit.php?post_type=king-addons-fb-sub',
188 );
189 }
190
191 // Check Templates Catalog extension toggle
192 $templates_enabled = !isset($options['ext_templates-catalog']) || $options['ext_templates-catalog'] === 'enabled';
193 if ($templates_enabled && (defined('KING_ADDONS_EXT_TEMPLATES_CATALOG') ? KING_ADDONS_EXT_TEMPLATES_CATALOG : true) && class_exists('King_Addons\Templates')) {
194 add_menu_page(
195 'King Addons for Elementor',
196 (!king_addons_freemius()->can_use_premium_code() ? esc_html__('Free Templates', 'king-addons') : esc_html__('Templates Pro', 'king-addons')),
197 'manage_options',
198 'king-addons-templates',
199 [Templates::instance(), 'render_template_catalog_page'],
200 KING_ADDONS_URL . (!king_addons_freemius()->can_use_premium_code() ? 'includes/admin/img/icon-for-menu-templates.svg' : 'includes/admin/img/icon-for-menu-templates-v2.svg'),
201 54.2
202 );
203 }
204
205 // Check Header & Footer Builder extension toggle
206 $header_footer_enabled = !isset($options['ext_header-footer-builder']) || $options['ext_header-footer-builder'] === 'enabled';
207 if ($header_footer_enabled && (defined('KING_ADDONS_EXT_HEADER_FOOTER_BUILDER') ? KING_ADDONS_EXT_HEADER_FOOTER_BUILDER : true) && class_exists('King_Addons\Header_Footer_Builder')) {
208 self::showHeaderFooterBuilder();
209 }
210
211 // Check Popup Builder extension toggle
212 $popup_builder_enabled = !isset($options['ext_popup-builder']) || $options['ext_popup-builder'] === 'enabled';
213 if ($popup_builder_enabled && (defined('KING_ADDONS_EXT_POPUP_BUILDER') ? KING_ADDONS_EXT_POPUP_BUILDER : true) && class_exists('King_Addons\Popup_Builder')) {
214 self::showPopupBuilder();
215 }
216
217 // Check WooCommerce Builder extension toggle
218 $woo_builder_enabled = !isset($options['ext_woo-builder']) || $options['ext_woo-builder'] === 'enabled';
219 if ($woo_builder_enabled && (defined('KING_ADDONS_EXT_WOO_BUILDER') ? KING_ADDONS_EXT_WOO_BUILDER : true)) {
220 $this->showWooBuilder();
221 }
222
223 $menu['54.8'] = array( '', 'read', 'separator-king-addons-bottom', '', 'wp-menu-separator elementor' );
224
225 }
226
227 /**
228 * Enforce submenu order for King Addons:
229 * 1) Dashboard
230 * 2) Settings
231 * 3) AI Settings
232 * 4) Everything else alphabetically
233 */
234 public function reorderKingAddonsSubmenu(): void
235 {
236 global $submenu;
237
238 if (!is_array($submenu) || empty($submenu['king-addons']) || !is_array($submenu['king-addons'])) {
239 return;
240 }
241
242 $priorityBySlug = [
243 'king-addons' => 0,
244 'king-addons-settings' => 1,
245 'king-addons-ai-settings' => 2,
246 ];
247
248 $items = $submenu['king-addons'];
249
250 usort($items, static function ($a, $b) use ($priorityBySlug): int {
251 $aSlug = isset($a[2]) ? (string) $a[2] : '';
252 $bSlug = isset($b[2]) ? (string) $b[2] : '';
253
254 $aPriority = array_key_exists($aSlug, $priorityBySlug) ? $priorityBySlug[$aSlug] : null;
255 $bPriority = array_key_exists($bSlug, $priorityBySlug) ? $priorityBySlug[$bSlug] : null;
256
257 if ($aPriority !== null || $bPriority !== null) {
258 $aPriority = $aPriority ?? 9999;
259 $bPriority = $bPriority ?? 9999;
260 if ($aPriority !== $bPriority) {
261 return $aPriority <=> $bPriority;
262 }
263 }
264
265 $aLabel = isset($a[0]) ? wp_strip_all_tags((string) $a[0]) : '';
266 $bLabel = isset($b[0]) ? wp_strip_all_tags((string) $b[0]) : '';
267
268 $cmp = strcasecmp($aLabel, $bLabel);
269 if ($cmp !== 0) {
270 return $cmp;
271 }
272
273 return strcasecmp($aSlug, $bSlug);
274 });
275
276 $submenu['king-addons'] = $items;
277 }
278
279 function addUpgradeMenu(): void
280 {
281 // Don't add menu if Freemius is showing opt-in/activation
282 $fs = king_addons_freemius();
283
284 // Check if we're on any Freemius-related page
285 if (
286 isset($_GET['fs_action']) ||
287 $fs->is_activation_mode() ||
288 (!$fs->is_registered() && !$fs->is_anonymous() && !$fs->is_tracking_prohibited())
289 ) {
290 return;
291 }
292
293 // Add Upgrade submenu under King Addons (only if premium is not active)
294 if (!$fs->can_use_premium_code()) {
295 add_submenu_page(
296 'king-addons',
297 esc_html__('Upgrade Now', 'king-addons'),
298 esc_html__('Upgrade Now', 'king-addons'),
299 'manage_options',
300 'https://kingaddons.com/pricing/?utm_source=kng-top-menu&utm_medium=plugin&utm_campaign=kng',
301 ''
302 );
303 }
304 }
305
306 function showPopupBuilder(): void
307 {
308 add_menu_page(
309 'Popup Builder',
310 'Popup Builder',
311 'manage_options',
312 'king-addons-popup-builder',
313 [Popup_Builder::instance(), 'renderPopupBuilder'],
314 KING_ADDONS_URL . 'includes/admin/img/icon-for-popup-builder.svg',
315 54.4
316 );
317 }
318
319 /**
320 * Register WooCommerce Builder menu item.
321 *
322 * @return void
323 */
324 public function showWooBuilder(): void
325 {
326 add_menu_page(
327 esc_html__('WooCommerce Builder', 'king-addons'),
328 esc_html__('WooCommerce Builder', 'king-addons'),
329 'manage_options',
330 'king-addons-woo-builder',
331 [$this, 'renderWooBuilderPage'],
332 'dashicons-cart',
333 54.5
334 );
335 }
336
337 /**
338 * Render WooCommerce Builder admin page.
339 *
340 * @return void
341 */
342 public function renderWooBuilderPage(): void
343 {
344 if (!current_user_can('manage_options')) {
345 return;
346 }
347
348 require_once KING_ADDONS_PATH . 'includes/admin/layouts/woo-builder-page.php';
349 }
350
351 function showHeaderFooterBuilder(): void
352 {
353 $post_type = 'king-addons-el-hf';
354 $menu_slug = 'edit.php?post_type=' . $post_type;
355
356 // Add Main Menu
357 add_menu_page(
358 esc_html__('Elementor Header & Footer Builder', 'king-addons'),
359 esc_html__('Header & Footer', 'king-addons'),
360 'manage_options',
361 $menu_slug, // Menu slug points to the custom post type edit screen
362 '', // No callback function needed
363 KING_ADDONS_URL . 'includes/admin/img/icon-for-header-footer-builder.svg',
364 54.3
365 );
366
367 // Add 'All Templates' Submenu - this will be the first submenu item
368 add_submenu_page(
369 $menu_slug, // Parent slug matches the main menu slug
370 esc_html__('All Templates', 'king-addons'),
371 esc_html__('All Templates', 'king-addons'),
372 'edit_posts',
373 $menu_slug
374 );
375 }
376
377 function showSettingsPage(): void
378 {
379 if (!current_user_can('manage_options')) {
380 return;
381 }
382
383 require_once(KING_ADDONS_PATH . 'includes/admin/layouts/settings-page.php');
384
385 self::enqueueSettingsAssets();
386 }
387
388 function showDashboardV3(): void
389 {
390 if (!current_user_can('manage_options')) {
391 return;
392 }
393
394 require_once(KING_ADDONS_PATH . 'includes/admin/layouts/dashboard-v3/dashboard-v3.php');
395 }
396
397 /**
398 * Render Wishlist admin page.
399 *
400 * @return void
401 */
402 public function renderWishlistPage(): void
403 {
404 if (!current_user_can('manage_options')) {
405 return;
406 }
407
408 $options = get_option('king_addons_options', []);
409 $wishlist_enabled = (!isset($options['ext_wishlist']) || $options['ext_wishlist'] === 'enabled')
410 && (defined('KING_ADDONS_EXT_WISHLIST') ? KING_ADDONS_EXT_WISHLIST : true);
411 if (!$wishlist_enabled || !class_exists(Wishlist_Settings::class)) {
412 return;
413 }
414
415 self::enqueueSettingsAssets();
416 require_once KING_ADDONS_PATH . 'includes/admin/layouts/wishlist-page.php';
417 }
418
419 /**
420 * Render Wishlist analytics page.
421 *
422 * @return void
423 */
424 public function renderWishlistAnalyticsPage(): void
425 {
426 if (!current_user_can('manage_options')) {
427 return;
428 }
429
430 $options = get_option('king_addons_options', []);
431 $wishlist_enabled = (!isset($options['ext_wishlist']) || $options['ext_wishlist'] === 'enabled')
432 && (defined('KING_ADDONS_EXT_WISHLIST') ? KING_ADDONS_EXT_WISHLIST : true);
433 if (!$wishlist_enabled || !class_exists(Wishlist_Settings::class)) {
434 return;
435 }
436
437 self::enqueueSettingsAssets();
438 require_once KING_ADDONS_PATH . 'includes/admin/layouts/wishlist-analytics.php';
439 }
440
441 function createSettings(): void
442 {
443 // Register a new setting for "king-addons" page.
444 register_setting('king_addons', 'king_addons_options');
445
446 // Register a new section in the "king-addons" page.
447 add_settings_section(
448 'king_addons_section_widgets',
449 '',
450 [$this, 'king_addons_section_widgets_callback'],
451 'king-addons'
452 );
453
454 // Register a new section in the "king-addons" page.
455 add_settings_section(
456 'king_addons_section_features',
457 '',
458 [$this, 'king_addons_section_features_callback'],
459 'king-addons'
460 );
461
462 foreach (ModulesMap::getModulesMapArray()['widgets'] as $widget_id => $widget_array) {
463 // Hide widgets hard-disabled via constants (QA rollout).
464 $widget_constant = 'KING_ADDONS_WGT_' . strtoupper(str_replace('-', '_', (string) $widget_id));
465 if (defined($widget_constant) && constant($widget_constant) === false) {
466 continue;
467 }
468
469 add_settings_field(
470 $widget_id,
471 $widget_array['title'],
472 '',
473 'king-addons',
474 'king_addons_section_widgets',
475 array(
476 'label_for' => $widget_id,
477 'description' => $widget_array['description'],
478 'docs_link' => $widget_array['docs-link'],
479 'demo_link' => $widget_array['demo-link'],
480 'class' => 'kng-tr kng-tr-' . $widget_id . (!empty($widget_array['has-pro']) ? ' kng-tr-freemium' : '')
481 )
482 );
483 }
484
485 foreach (ModulesMap::getModulesMapArray()['features'] as $feature_id => $feature_array) {
486 // Hide features hard-disabled via constants (QA rollout).
487 $feature_constant = 'KING_ADDONS_FEAT_' . strtoupper(str_replace('-', '_', (string) $feature_id));
488 if (defined($feature_constant) && constant($feature_constant) === false) {
489 continue;
490 }
491
492 add_settings_field(
493 $feature_id,
494 $feature_array['title'],
495 '',
496 'king-addons',
497 'king_addons_section_features',
498 array(
499 'label_for' => $feature_id,
500 'description' => $feature_array['description'],
501 'docs_link' => $feature_array['docs-link'],
502 'demo_link' => $feature_array['demo-link'],
503 'class' => 'kng-tr kng-tr-' . $feature_id
504 )
505 );
506 }
507 }
508
509 /**
510 * Register wishlist settings group.
511 *
512 * @return void
513 */
514 public function createWishlistSettings(): void
515 {
516 if (defined('KING_ADDONS_EXT_WISHLIST') && KING_ADDONS_EXT_WISHLIST === false) {
517 return;
518 }
519
520 if (!class_exists(Wishlist_Settings::class)) {
521 return;
522 }
523
524 register_setting(
525 'king_addons_wishlist',
526 'king_addons_wishlist_settings',
527 [
528 'type' => 'array',
529 'sanitize_callback' => [$this, 'sanitizeWishlistSettings'],
530 'default' => Wishlist_Settings::defaults(),
531 ]
532 );
533 }
534
535 /**
536 * Sanitize wishlist settings payload.
537 *
538 * @param array<string, mixed> $settings Raw settings.
539 * @return array<string, mixed> Sanitized settings.
540 */
541 public function sanitizeWishlistSettings(array $settings): array
542 {
543 if (!class_exists(Wishlist_Settings::class)) {
544 return [];
545 }
546
547 $defaults = Wishlist_Settings::defaults();
548
549 $sanitized = [
550 'enabled' => !empty($settings['enabled']),
551 'wishlist_page_id' => absint($settings['wishlist_page_id'] ?? 0),
552 'allow_guests' => !empty($settings['allow_guests']),
553 'guest_block_text' => sanitize_text_field($settings['guest_block_text'] ?? $defaults['guest_block_text']),
554 'button_add_text' => sanitize_text_field($settings['button_add_text'] ?? $defaults['button_add_text']),
555 'button_added_text' => sanitize_text_field($settings['button_added_text'] ?? $defaults['button_added_text']),
556 'button_display_mode' => in_array($settings['button_display_mode'] ?? 'icon_text', ['icon', 'icon_text'], true) ? $settings['button_display_mode'] : $defaults['button_display_mode'],
557 'button_position' => in_array($settings['button_position'] ?? 'after_add_to_cart', ['before_add_to_cart', 'after_add_to_cart'], true) ? $settings['button_position'] : $defaults['button_position'],
558 'show_in_archives' => !empty($settings['show_in_archives']),
559 'wishlist_columns' => array_values(
560 array_intersect(
561 (array) ($settings['wishlist_columns'] ?? []),
562 ['image', 'title', 'price', 'stock', 'notes', 'add_to_cart', 'remove']
563 )
564 ),
565 'cache_enabled' => !empty($settings['cache_enabled']),
566 'cache_ttl' => max(0, absint($settings['cache_ttl'] ?? 0)),
567 'icon_choice' => sanitize_text_field($settings['icon_choice'] ?? $defaults['icon_choice']),
568 ];
569
570 return wp_parse_args($sanitized, $defaults);
571 }
572
573 function king_addons_section_widgets_callback($args): void
574 {
575 ?>
576 <h2 id="<?php echo esc_attr($args['id']); ?>" class="kng-section-title"><?php esc_html_e('Elements', 'king-addons'); ?>
577 </h2>
578 <?php
579 }
580
581 function king_addons_section_features_callback($args): void
582 {
583 ?>
584 <div class="kng-section-separator"></div>
585 <h2 id="<?php echo esc_attr($args['id']); ?>" class="kng-section-title"><?php esc_html_e('Features', 'king-addons'); ?>
586 </h2>
587 <?php
588 }
589
590 function enqueueAdminAssets(): void
591 {
592 wp_enqueue_style('king-addons-admin', KING_ADDONS_URL . 'includes/admin/css/admin.css', '', KING_ADDONS_VERSION);
593 // Styles for AI Image Generation controls in Elementor
594 wp_enqueue_style('king-addons-ai-imagefield', KING_ADDONS_URL . 'includes/admin/css/ai-imagefield.css', array('king-addons-admin'), KING_ADDONS_VERSION);
595 }
596
597 function enqueueUpgradeLinkScript(): void
598 {
599 // Only add the script if premium is not active
600 if (!king_addons_freemius()->can_use_premium_code()) {
601 wp_enqueue_script('jquery');
602 wp_add_inline_script('jquery', "
603 jQuery(document).ready(function($) {
604 $('#adminmenu #toplevel_page_king-addons a[href=\"https://kingaddons.com/pricing/?utm_source=kng-top-menu&utm_medium=plugin&utm_campaign=kng\"]').attr('target', '_blank');
605 });
606 ");
607 }
608 }
609
610 public function enqueueGlobalAdminStyles(): void
611 {
612 wp_enqueue_style('king-addons-hide-spam', KING_ADDONS_URL . 'includes/admin/css/hide-spam-notifications.css', [], KING_ADDONS_VERSION);
613 }
614
615 function enqueueSettingsAssets(): void
616 {
617 wp_enqueue_style('king-addons-settings', KING_ADDONS_URL . 'includes/admin/css/settings.css', '', KING_ADDONS_VERSION);
618 wp_enqueue_style('wp-color-picker');
619 wp_enqueue_script('jquery');
620 wp_enqueue_script('wp-color-picker');
621 wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-wpcolorpicker-wpcolorpicker');
622 wp_enqueue_script('king-addons-settings', KING_ADDONS_URL . 'includes/admin/js/settings.js', '', KING_ADDONS_VERSION);
623 }
624
625 /**
626 * Registers AI Settings using the WordPress Settings API.
627 *
628 * @return void
629 */
630 public function createAiSettings(): void
631 {
632 register_setting(
633 'king_addons_ai',
634 'king_addons_ai_options',
635 [$this, 'sanitizeAiSettings']
636 );
637
638 add_settings_section(
639 'king_addons_ai_openai_section',
640 esc_html__('OpenAI API Settings', 'king-addons'),
641 [$this, 'renderAiOpenaiSection'],
642 'king-addons-ai-settings'
643 );
644
645 add_settings_field(
646 'openai_api_key',
647 esc_html__('OpenAI API Key', 'king-addons'),
648 [$this, 'renderAiApiKeyField'],
649 'king-addons-ai-settings',
650 'king_addons_ai_openai_section'
651 );
652
653 add_settings_field(
654 'openai_model',
655 esc_html__('OpenAI Model', 'king-addons'),
656 [$this, 'renderAiModelField'],
657 'king-addons-ai-settings',
658 'king_addons_ai_openai_section'
659 );
660
661 // Add image model selector field
662 add_settings_field(
663 'openai_image_model',
664 esc_html__('OpenAI Image Model', 'king-addons'),
665 [$this, 'renderAiImageModelField'],
666 'king-addons-ai-settings',
667 'king_addons_ai_openai_section'
668 );
669
670 // Add Editor Integration section and field
671 add_settings_section(
672 'king_addons_ai_editor_section',
673 esc_html__('Editor Integration', 'king-addons'),
674 [$this, 'renderAiEditorSection'],
675 'king-addons-ai-settings'
676 );
677 add_settings_field(
678 'enable_ai_buttons',
679 esc_html__('AI Text Editing Buttons', 'king-addons'),
680 [$this, 'renderAiEnableButtonsField'],
681 'king-addons-ai-settings',
682 'king_addons_ai_editor_section'
683 );
684 add_settings_field(
685 'enable_ai_image_generation_button',
686 esc_html__('AI Image Generation Button', 'king-addons'),
687 [$this, 'renderAiImageGenerationField'],
688 'king-addons-ai-settings',
689 'king_addons_ai_editor_section'
690 );
691
692 // Add Alt Text Settings section
693 add_settings_section(
694 'king_addons_ai_alt_text_section',
695 esc_html__('Alt Text Settings', 'king-addons'),
696 [$this, 'renderAiAltTextSection'],
697 'king-addons-ai-settings'
698 );
699 add_settings_field(
700 'enable_ai_alt_text_button',
701 esc_html__('AI Alt Text Button', 'king-addons'),
702 [$this, 'renderAiAltTextButtonField'],
703 'king-addons-ai-settings',
704 'king_addons_ai_alt_text_section'
705 );
706
707 add_settings_field(
708 'enable_ai_alt_text_auto_generation',
709 ((king_addons_freemius()->can_use_premium_code()) ? esc_html__('Auto Generate Alt Text', 'king-addons') : esc_html__('Auto Generate Alt Text (PRO feature)', 'king-addons')),
710 [$this, 'renderAiAltTextAutoGenerationField'],
711 'king-addons-ai-settings',
712 'king_addons_ai_alt_text_section'
713 );
714 add_settings_field(
715 'ai_alt_text_generation_interval',
716 esc_html__('Alt Text Generation Interval', 'king-addons'),
717 [$this, 'renderAiAltTextIntervalField'],
718 'king-addons-ai-settings',
719 'king_addons_ai_alt_text_section'
720 );
721 // Add Image Detail Level field
722 add_settings_field(
723 'ai_alt_text_image_detail_level',
724 esc_html__('Image Detail Level', 'king-addons'),
725 [$this, 'renderAiAltTextImageDetailLevelField'],
726 'king-addons-ai-settings',
727 'king_addons_ai_alt_text_section'
728 );
729
730 // Add Translation Settings section
731 add_settings_section(
732 'king_addons_ai_translation_section',
733 esc_html__('Translation Settings', 'king-addons'),
734 [$this, 'renderAiTranslationSection'],
735 'king-addons-ai-settings'
736 );
737
738 add_settings_field(
739 'enable_ai_page_translator',
740 esc_html__('AI Page Translator Button', 'king-addons'),
741 [$this, 'renderAiPageTranslatorField'],
742 'king-addons-ai-settings',
743 'king_addons_ai_translation_section'
744 );
745
746 // Add Usage Quota Settings section and field
747 add_settings_section(
748 'king_addons_ai_quota_section',
749 esc_html__('Usage Quota Settings', 'king-addons'),
750 [$this, 'renderAiQuotaSection'],
751 'king-addons-ai-settings'
752 );
753
754 add_settings_field(
755 'daily_token_limit',
756 esc_html__('Daily Token Limit', 'king-addons'),
757 [$this, 'renderAiDailyLimitField'],
758 'king-addons-ai-settings',
759 'king_addons_ai_quota_section'
760 );
761
762 // Add Usage Statistics section (read-only)
763 add_settings_section(
764 'king_addons_ai_stats_section',
765 esc_html__('Usage Statistics', 'king-addons'),
766 [$this, 'renderAiStatsSection'],
767 'king-addons-ai-settings'
768 );
769
770 // Clear models cache when options updated.
771 add_action('update_option_king_addons_ai_options', [$this, 'clearAiModelsCache']);
772
773 // AJAX handler for refreshing models.
774 add_action('wp_ajax_king_addons_ai_refresh_models', [$this, 'handleAiRefreshModels']);
775
776 // AJAX handler for generating text via AI
777 add_action('wp_ajax_king_addons_ai_generate_text', [$this, 'handleAiGenerateText']);
778
779 // AJAX handler to change text using AI based on user prompt and original text.
780 add_action('wp_ajax_king_addons_ai_change_text', [$this, 'handleAiChangeText']);
781
782 // AJAX handler to check token usage limits
783 add_action('wp_ajax_king_addons_ai_check_tokens', [$this, 'handleAiCheckTokens']);
784
785 // AJAX handler to check image generation limits
786 add_action('wp_ajax_king_addons_ai_image_check_limits', [$this, 'handleAiImageCheckLimits']);
787
788 // THIRD_EDIT: Register AJAX handler for AI image generation
789 add_action('wp_ajax_king_addons_ai_generate_image', [$this, 'handleAiGenerateImage']);
790
791 // AJAX handler for AI page translation
792 add_action('wp_ajax_king_addons_ai_translate_text', [$this, 'handleAiTranslateText']);
793 }
794
795 /**
796 * Sanitizes AI Settings options.
797 *
798 * @param array $input Raw input array.
799 * @return array Sanitized input.
800 */
801 public function sanitizeAiSettings(array $input): array
802 {
803 $sanitized = [];
804 $sanitized['openai_api_key'] = isset($input['openai_api_key'])
805 ? sanitize_text_field($input['openai_api_key'])
806 : '';
807 $sanitized['openai_model'] = isset($input['openai_model'])
808 ? sanitize_text_field($input['openai_model'])
809 : '';
810 $sanitized['openai_image_model'] = isset($input['openai_image_model'])
811 ? sanitize_text_field($input['openai_image_model'])
812 : 'gpt-image-1';
813
814 // Sanitize Daily Token Limit.
815 if (isset($input['daily_token_limit'])) {
816 $daily_limit = absint($input['daily_token_limit']);
817 $sanitized['daily_token_limit'] = max(0, $daily_limit); // Ensure non-negative
818 } else {
819 $sanitized['daily_token_limit'] = 1000000; // Default to 1 million tokens if not set
820 }
821
822 // Sanitize Enable AI Buttons option.
823 $sanitized['enable_ai_buttons'] = !empty($input['enable_ai_buttons']);
824
825 // Sanitize Enable AI Image Generation button option.
826 $sanitized['enable_ai_image_generation_button'] = !empty($input['enable_ai_image_generation_button']);
827
828 // Sanitize Enable AI Alt Text Button option.
829 $sanitized['enable_ai_alt_text_button'] = !empty($input['enable_ai_alt_text_button']);
830
831 // Sanitize Enable AI Alt Text Auto Generation option.
832 $sanitized['enable_ai_alt_text_auto_generation'] = !empty($input['enable_ai_alt_text_auto_generation']);
833
834 // Sanitize AI Alt Text Generation Interval.
835 if (isset($input['ai_alt_text_generation_interval'])) {
836 $interval = absint($input['ai_alt_text_generation_interval']);
837 $sanitized['ai_alt_text_generation_interval'] = max(10, min(3600, $interval)); // Between 10 seconds and 1 hour
838 } else {
839 $sanitized['ai_alt_text_generation_interval'] = 60; // Default to 60 seconds
840 }
841 // Sanitize Image Detail Level
842 $allowed_detail_levels = ['low', 'high'];
843 $sanitized['ai_alt_text_image_detail_level'] = in_array(($input['ai_alt_text_image_detail_level'] ?? 'low'), $allowed_detail_levels, true)
844 ? $input['ai_alt_text_image_detail_level']
845 : 'low';
846
847 // Sanitize Enable AI Page Translator option
848 $sanitized['enable_ai_page_translator'] = !empty($input['enable_ai_page_translator']);
849
850 // Validation / feedback (Settings API notice).
851 $ai_requires_key = (
852 !empty($sanitized['enable_ai_buttons'])
853 || !empty($sanitized['enable_ai_image_generation_button'])
854 || !empty($sanitized['enable_ai_alt_text_button'])
855 || !empty($sanitized['enable_ai_page_translator'])
856 );
857
858 if ($ai_requires_key && empty($sanitized['openai_api_key'])) {
859 add_settings_error(
860 'king_addons_ai',
861 'king_addons_ai_missing_api_key',
862 esc_html__('OpenAI API Key is required to enable AI features.', 'king-addons'),
863 'error'
864 );
865 }
866
867 return $sanitized;
868 }
869
870 /**
871 * Clears cached AI models list.
872 *
873 * @return void
874 */
875 public function clearAiModelsCache(): void
876 {
877 delete_transient('king_addons_ai_models_cache');
878 }
879
880 /**
881 * Renders the AI Settings page content.
882 *
883 * @return void
884 */
885 public function showAiSettingsPage(): void
886 {
887 if (!current_user_can('manage_options')) {
888 return;
889 }
890 require_once KING_ADDONS_PATH . 'includes/admin/layouts/ai-settings-page.php';
891 $this->enqueueAiSettingsAssets();
892 }
893
894 /**
895 * Enqueues scripts and styles for the AI Settings page.
896 *
897 * @return void
898 */
899 public function enqueueAiSettingsAssets(): void
900 {
901 // Enqueue admin base styles first for proper theming
902 wp_enqueue_style('king-addons-admin', KING_ADDONS_URL . 'includes/admin/css/admin.css', '', KING_ADDONS_VERSION);
903
904 wp_enqueue_style(
905 'king-addons-ai-settings',
906 KING_ADDONS_URL . 'includes/admin/css/ai-settings.css',
907 ['king-addons-admin'], // Depend on admin base styles
908 KING_ADDONS_VERSION
909 );
910
911 wp_enqueue_script(
912 'king-addons-ai-settings',
913 KING_ADDONS_URL . 'includes/admin/js/ai-settings.js',
914 ['jquery'],
915 KING_ADDONS_VERSION,
916 true
917 );
918
919 wp_localize_script(
920 'king-addons-ai-settings',
921 'KingAddonsAiSettings',
922 [
923 'ajax_url' => admin_url('admin-ajax.php'),
924 'nonce' => wp_create_nonce('king_addons_ai_refresh_models_nonce'),
925 'refreshing_text' => esc_html__('Refreshing...', 'king-addons'),
926 'refreshed_text' => esc_html__('List updated.', 'king-addons'),
927 'error_text' => esc_html__('Error updating list.', 'king-addons'),
928 ]
929 );
930 }
931
932 /**
933 * Renders description for OpenAI API Settings section.
934 *
935 * @return void
936 */
937 public function renderAiOpenaiSection(): void
938 {
939 echo '<p>' . esc_html__('Enter your OpenAI API key and select the model for AI features.', 'king-addons') . '</p>';
940 }
941
942 /**
943 * Renders the OpenAI API Key input field.
944 *
945 * @return void
946 */
947 public function renderAiApiKeyField(): void
948 {
949 $options = get_option('king_addons_ai_options', []);
950 $api_key = $options['openai_api_key'] ?? '';
951 printf(
952 '<input type="password" name="king_addons_ai_options[openai_api_key]" value="%s" class="regular-text" autocomplete="off" />',
953 esc_attr($api_key)
954 );
955 echo '<p class="description">';
956 printf(
957 esc_html__('Get your API key from the %1$sOpenAI Platform%2$s. Saving the key will attempt to fetch the available models.', 'king-addons'),
958 '<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">',
959 '</a>'
960 );
961 echo '</p>';
962 echo '<div class="ka-ai-notice ka-ai-notice-warning">';
963 echo '<strong>' . esc_html__('Important:', 'king-addons') . '</strong> ';
964 echo esc_html__('You must top up your OpenAI account balance by at least $5 for the API to work. Free accounts are not supported.', 'king-addons');
965 echo '</div>';
966 echo '<div class="ka-ai-notice ka-ai-notice-info">';
967 echo '<strong>' . esc_html__('Info:', 'king-addons') . '</strong> ';
968 echo esc_html__('With GPT-4o-mini, a $5 balance is enough for roughly 130,000–150,000 text generations.', 'king-addons');
969 echo '</div>';
970 echo '<div class="ka-ai-notice ka-ai-notice-info">';
971 echo '<strong class="ka-ai-notice-title">' . esc_html__('Useful OpenAI Links:', 'king-addons') . '</strong>';
972 echo '<ul class="ka-ai-links-list">';
973 $links = [
974 'API Pricing' => 'https://openai.com/api/pricing/',
975 'API Keys' => 'https://platform.openai.com/api-keys',
976 'Usage Dashboard' => 'https://platform.openai.com/account/usage',
977 'Billing Overview' => 'https://platform.openai.com/account/billing/overview',
978 'Rate Limits' => 'https://openai.com/pricing#rate-limits',
979 ];
980 foreach ($links as $label => $url) {
981 printf(
982 '<li><a href="%s" target="_blank" rel="noopener noreferrer">%s</a></li>',
983 esc_url($url),
984 esc_html($label)
985 );
986 }
987 echo '</ul></div>';
988 }
989
990 /**
991 * Renders the model selection dropdown field with refresh button.
992 *
993 * @return void
994 */
995 public function renderAiModelField(): void
996 {
997 $options = get_option('king_addons_ai_options', []);
998 $selected = $options['openai_model'] ?? '';
999 $models = $this->getAiAvailableModels();
1000 printf(
1001 '<select name="king_addons_ai_options[openai_model]" %s>',
1002 empty($models) ? 'disabled' : ''
1003 );
1004 if (!empty($models)) {
1005 foreach ($models as $id => $label) {
1006 printf(
1007 '<option value="%s" %s>%s</option>',
1008 esc_attr($id),
1009 selected($selected, $id, false),
1010 esc_html($label)
1011 );
1012 }
1013 } else {
1014 echo '<option value="">' . esc_html__('Could not fetch models. Check API key?', 'king-addons') . '</option>';
1015 }
1016 echo '</select>';
1017 echo '<button type="button" id="king-addons-ai-refresh-models-button" class="button button-secondary" style="margin-left:10px; vertical-align:middle;">' . esc_html__('Refresh List', 'king-addons') . '</button>';
1018 echo '<span class="spinner" id="king-addons-ai-refresh-models-spinner" style="float:none; vertical-align:middle;"></span>';
1019 echo '<span id="king-addons-ai-refresh-models-status" style="margin-left:5px; vertical-align:middle;"></span>';
1020 echo '<p class="description">' . esc_html__('Select an available OpenAI model capable of processing text. We recommend GPT-4o-mini or GPT-4.1-nano for best results. The list of models is cached indefinitely until manually refreshed.', 'king-addons') . '</p>';
1021 }
1022
1023 /**
1024 * Fetches the list of OpenAI models via API.
1025 *
1026 * @param string|null $api_key API key to use.
1027 * @return array|\WP_Error Model list or error.
1028 */
1029 private function fetchAiOpenaiModels(?string $api_key)
1030 {
1031 if (empty($api_key)) {
1032 return new \WP_Error('missing_key', esc_html__('API key is required to fetch models.', 'king-addons'));
1033 }
1034 $endpoint = 'https://api.openai.com/v1/models';
1035 $response = wp_remote_get($endpoint, [
1036 'headers' => ['Authorization' => 'Bearer ' . $api_key],
1037 'timeout' => 20,
1038 ]);
1039 if (is_wp_error($response)) {
1040 return $response;
1041 }
1042 $code = wp_remote_retrieve_response_code($response);
1043 $body = wp_remote_retrieve_body($response);
1044 $data = json_decode($body, true);
1045 if ($code !== 200 || empty($data['data']) || !is_array($data['data'])) {
1046 $message = $data['error']['message'] ?? esc_html__('Invalid response from API.', 'king-addons');
1047 return new \WP_Error('api_error', $message, ['status' => $code]);
1048 }
1049 $list = [];
1050 foreach ($data['data'] as $model) {
1051 if (isset($model['id'])) {
1052 $list[$model['id']] = $model['id'];
1053 }
1054 }
1055 ksort($list);
1056 if (empty($list)) {
1057 return new \WP_Error('no_models', esc_html__('No models found via API.', 'king-addons'));
1058 }
1059 return $list;
1060 }
1061
1062 /**
1063 * Retrieves available models, using cache if possible.
1064 *
1065 * @return array Model list.
1066 */
1067 private function getAiAvailableModels(): array
1068 {
1069 $cached = get_transient('king_addons_ai_models_cache');
1070 if (false !== $cached && is_array($cached)) {
1071 return $cached;
1072 }
1073 $options = get_option('king_addons_ai_options', []);
1074 $api_key = $options['openai_api_key'] ?? null;
1075 $fetched = $this->fetchAiOpenaiModels($api_key);
1076 if (!is_wp_error($fetched)) {
1077 set_transient('king_addons_ai_models_cache', $fetched, 0);
1078 return $fetched;
1079 }
1080 return ['gpt-4o-mini' => 'GPT-4o-mini', 'gpt-4.1-nano' => 'GPT-4.1-nano'];
1081 }
1082
1083 /**
1084 * Handles AJAX request to refresh model list.
1085 *
1086 * @return void
1087 */
1088 public function handleAiRefreshModels(): void
1089 {
1090 check_ajax_referer('king_addons_ai_refresh_models_nonce', 'nonce');
1091 if (!current_user_can('manage_options')) {
1092 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1093 }
1094 $options = get_option('king_addons_ai_options', []);
1095 $api_key = $options['openai_api_key'] ?? null;
1096 if (empty($api_key)) {
1097 wp_send_json_error(['message' => esc_html__('API key is not set.', 'king-addons')], 400);
1098 }
1099 $this->clearAiModelsCache();
1100 $models = $this->fetchAiOpenaiModels($api_key);
1101 if (is_wp_error($models)) {
1102 wp_send_json_error(['message' => $models->get_error_message()], 500);
1103 }
1104 if (empty($models)) {
1105 wp_send_json_error(['message' => esc_html__('No models returned by API.', 'king-addons')], 500);
1106 }
1107 set_transient('king_addons_ai_models_cache', $models, 0);
1108 wp_send_json_success(['models' => $models]);
1109 }
1110
1111 /**
1112 * AJAX handler to generate text using OpenAI.
1113 *
1114 * @return void
1115 */
1116 public function handleAiGenerateText(): void
1117 {
1118 check_ajax_referer('king_addons_ai_generate_nonce', 'nonce');
1119 if (!current_user_can('manage_options')) {
1120 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1121 }
1122 $field_name = sanitize_text_field($_POST['field_name'] ?? '');
1123 // Accept 'prompt' parameter (new) but fall back to 'value' parameter (old) for backwards compatibility
1124 $prompt = isset($_POST['prompt'])
1125 ? sanitize_textarea_field($_POST['prompt'])
1126 : sanitize_textarea_field($_POST['value'] ?? '');
1127
1128 // Get editor type if provided
1129 $editor_type = sanitize_text_field($_POST['editor_type'] ?? 'text');
1130
1131 $options = get_option('king_addons_ai_options', []);
1132 $api_key = $options['openai_api_key'] ?? '';
1133 $model = $options['openai_model'] ?? '';
1134
1135 if (empty($api_key) || empty($model)) {
1136 wp_send_json_error(['message' => esc_html__('API key or model not set.', 'king-addons')], 400);
1137 }
1138
1139 if (empty($prompt)) {
1140 wp_send_json_error(['message' => esc_html__('Please provide a prompt.', 'king-addons')], 400);
1141 }
1142
1143 // Check daily token limit
1144 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1145 $current_usage = $this->getAiDailyUsage();
1146
1147 if ($daily_limit > 0 && $current_usage >= $daily_limit) {
1148 wp_send_json_error([
1149 'message' => esc_html__('Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.', 'king-addons')
1150 ], 429);
1151 }
1152
1153 // System instruction based on editor type
1154 $system_instruction = 'You are a helpful content assistant. Provide concise, well-written content based on the user\'s request.';
1155
1156 // Enhanced instruction for WYSIWYG editor
1157 if ($editor_type === 'wysiwyg') {
1158 $system_instruction = 'You are a helpful content assistant for a rich text editor. Provide content with proper HTML formatting. Use <p> tags for paragraphs with appropriate spacing between them. If relevant, use other HTML formatting like <strong>, <em>, <ul>, <ol>, etc. for better readability and structure. IMPORTANT: Do NOT wrap your HTML in code fences (``` or ```html). Respond ONLY with the actual HTML content.';
1159 }
1160
1161 // Prepare request to OpenAI Chat Completions
1162 $messages = [
1163 ['role' => 'system', 'content' => $system_instruction],
1164 ['role' => 'user', 'content' => $prompt]
1165 ];
1166
1167 // Add format instruction for WYSIWYG
1168 if ($editor_type === 'wysiwyg') {
1169 $messages[1]['content'] .= "\n\nOutput should be properly formatted HTML with <p> tags for paragraphs, maintaining good spacing and readability. Do NOT use code fences (```html or ```) in your response - provide just the clean HTML.";
1170 }
1171
1172 $response = wp_remote_post(
1173 'https://api.openai.com/v1/chat/completions',
1174 [
1175 'headers' => [
1176 'Authorization' => 'Bearer ' . $api_key,
1177 'Content-Type' => 'application/json',
1178 ],
1179 'body' => wp_json_encode([
1180 'model' => $model,
1181 'messages' => $messages,
1182 'max_tokens' => 500,
1183 'temperature' => 0.7, // Slight creativity for better content
1184 ]),
1185 'timeout' => 30,
1186 ]
1187 );
1188
1189 if (is_wp_error($response)) {
1190 wp_send_json_error(['message' => $response->get_error_message()], 500);
1191 }
1192
1193 $code = wp_remote_retrieve_response_code($response);
1194 $data = json_decode(wp_remote_retrieve_body($response), true);
1195
1196 if ($code !== 200 || empty($data['choices'][0]['message']['content'])) {
1197 $error_msg = $data['error']['message'] ?? esc_html__('AI API error.', 'king-addons');
1198 wp_send_json_error(['message' => $error_msg], 500);
1199 }
1200
1201 $generated = trim($data['choices'][0]['message']['content']);
1202
1203 // Clean up any code fence markers for WYSIWYG editor
1204 if ($editor_type === 'wysiwyg') {
1205 // Remove code fence markers (```html and ```) that might be returned by AI
1206 $generated = preg_replace('/^```(?:html|HTML)?\s*/', '', $generated);
1207 $generated = preg_replace('/```\s*$/', '', $generated);
1208 }
1209
1210 // Update token usage statistics if present in the response
1211 if (isset($data['usage']['total_tokens'])) {
1212 $this->incrementAiDailyUsage(intval($data['usage']['total_tokens']));
1213 }
1214
1215 wp_send_json_success([
1216 'text' => $generated,
1217 'usage' => [
1218 'tokens_used' => $data['usage']['total_tokens'] ?? 0,
1219 'daily_used' => $this->getAiDailyUsage(),
1220 'daily_limit' => $daily_limit,
1221 ]
1222 ]);
1223 }
1224
1225 /**
1226 * AJAX handler to change text using AI based on user prompt and original text.
1227 *
1228 * @return void
1229 */
1230 public function handleAiChangeText(): void
1231 {
1232 check_ajax_referer('king_addons_ai_change_nonce', 'nonce');
1233 if (!current_user_can('manage_options')) {
1234 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1235 }
1236 $field_name = isset($_POST['field_name']) ? sanitize_text_field(wp_unslash($_POST['field_name'])) : '';
1237 $prompt = isset($_POST['prompt']) ? sanitize_text_field(wp_unslash($_POST['prompt'])) : '';
1238 $original = isset($_POST['original']) ? wp_kses_post(wp_unslash($_POST['original'])) : '';
1239 $instruction_context = isset($_POST['instruction_context']) ? sanitize_textarea_field(wp_unslash($_POST['instruction_context'])) : '';
1240
1241 $options = get_option('king_addons_ai_options', []);
1242 $api_key = $options['openai_api_key'] ?? '';
1243 $model = $options['openai_model'] ?? '';
1244
1245 if (empty($api_key) || empty($model) || empty($prompt) || empty($original)) {
1246 wp_send_json_error(['message' => esc_html__('Missing data for AI change.', 'king-addons')], 400);
1247 }
1248
1249 // Check daily token limit
1250 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1251 $current_usage = $this->getAiDailyUsage();
1252
1253 if ($daily_limit > 0 && $current_usage >= $daily_limit) {
1254 wp_send_json_error([
1255 'message' => esc_html__('Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.', 'king-addons')
1256 ], 429);
1257 }
1258
1259 // Default instruction if none provided
1260 if (empty($instruction_context)) {
1261 $instruction_context = 'You are an assistant that modifies text per user instruction. If the instruction mentions adding paragraphs or texts, make sure to KEEP the original text and ADD to it. If the instruction is about changing style, maintain the same information but change the tone. Return the complete modified text.';
1262
1263 // Add specific formatting instructions for WYSIWYG editor
1264 $editor_type = isset($_POST['editor_type']) ? sanitize_text_field(wp_unslash($_POST['editor_type'])) : 'text';
1265 if ($editor_type === 'wysiwyg') {
1266 $instruction_context = 'You are an assistant that modifies HTML content for a rich text editor. IMPORTANT: If asked to add a specific number of paragraphs (like "add 2 paragraphs" or "add 3 sections"), you MUST add EXACTLY that number of distinct paragraphs or sections - no more, no less. If asked to add "some" or "several" paragraphs, add at minimum 2-3 paragraphs. Always keep the original content intact and add the new paragraphs after the original content. Use proper HTML formatting with <p> tags for each paragraph. Ensure there is appropriate spacing between paragraphs. Preserve any existing HTML formatting (like <strong>, <em>, <a>, etc). DO NOT wrap your output in code fences (``` or ```html) - respond only with the actual HTML. Return the complete modified content with proper HTML structure.';
1267 }
1268 }
1269
1270 // Analyze if the prompt is likely requesting to add content rather than replace
1271 $add_content_keywords = [
1272 // English
1273 'add',
1274 'insert',
1275 'extend',
1276 'append',
1277 'more',
1278 'additional',
1279 'expand',
1280 // Russian
1281 'добавь',
1282 'вставь',
1283 'расширь',
1284 // Spanish
1285 'añadir',
1286 'agregar',
1287 'insertar',
1288 'adjuntar',
1289 'extender',
1290 // French
1291 'ajouter',
1292 'insérer',
1293 'étendre',
1294 'annexer',
1295 'joindre',
1296 // German
1297 'hinzufügen',
1298 'einfügen',
1299 'erweitern',
1300 'anhängen',
1301 'ergänzen',
1302 // Italian
1303 'aggiungere',
1304 'inserire',
1305 'allegare',
1306 'estendere',
1307 'appendere',
1308 // Portuguese
1309 'adicionar',
1310 'inserir',
1311 'acrescentar',
1312 'anexar',
1313 'estender',
1314 // Polish
1315 'dodać',
1316 'wstawić',
1317 'doł�
1318 czyć',
1319 'rozszerzyć',
1320 'zał�
1321 czyć'
1322 ];
1323
1324 // Also look for numeric patterns like "add 2 paragraphs" or "добавь 3 абзаца"
1325 // Enhanced pattern to find numeric paragraph requests in different languages
1326 $numeric_pattern = '/(?:' .
1327 // English verbs
1328 'add|append|insert|create|write|' .
1329 // Russian verbs
1330 'добавь|вставь|создай|напиши|' .
1331 // Spanish verbs
1332 'añadir|agregar|insertar|crear|escribir|' .
1333 // French verbs
1334 'ajouter|insérer|créer|écrire|' .
1335 // German verbs
1336 'hinzufügen|einfügen|erstellen|schreiben|' .
1337 // Italian verbs
1338 'aggiungere|inserire|creare|scrivere|' .
1339 // Portuguese verbs
1340 'adicionar|inserir|criar|escrever|' .
1341 // Polish verbs
1342 'dodać|wstawić|utworzyć|napisać' .
1343 ')\s+(\d+|' .
1344 // English quantifiers
1345 'several|few|couple|some|' .
1346 // Russian quantifiers
1347 'несколько|пару|еще|ещё|' .
1348 // Spanish quantifiers
1349 'varios|algunos|un par|unos|' .
1350 // French quantifiers
1351 'plusieurs|quelques|une paire|certains|' .
1352 // German quantifiers
1353 'mehrere|einige|ein paar|manche|' .
1354 // Italian quantifiers
1355 'diversi|alcuni|un paio|qualche|' .
1356 // Portuguese quantifiers
1357 'vários|alguns|um par|' .
1358 // Polish quantifiers
1359 'kilka|parę|pare|niektóre' .
1360 ')\s+(?:' .
1361 // English nouns
1362 'paragraph|paragraphs|section|sections|content|text|' .
1363 // Russian nouns
1364 'абзац|абзаца|абзацев|раздел|разделы|текст|контент|параграф|параграфа|параграфов|' .
1365 // Spanish nouns
1366 'párrafo|párrafos|sección|secciones|contenido|texto|' .
1367 // French nouns
1368 'paragraphe|paragraphes|section|sections|contenu|texte|' .
1369 // German nouns
1370 'absatz|absätze|abschnitt|abschnitte|inhalt|text|' .
1371 // Italian nouns
1372 'paragrafo|paragrafi|sezione|sezioni|contenuto|testo|' .
1373 // Portuguese nouns
1374 'parágrafo|parágrafos|seção|seções|conteúdo|texto|' .
1375 // Polish nouns
1376 'akapit|akapity|sekcja|sekcje|treść|tekst' .
1377 ')/i';
1378 $contains_add_keyword = false;
1379 $numeric_match = [];
1380 $requested_paragraphs = 0;
1381
1382 // First check for specific numeric requests
1383 if (preg_match($numeric_pattern, $prompt, $numeric_match)) {
1384 $contains_add_keyword = true;
1385 $number_text = $numeric_match[1] ?? '';
1386
1387 // Convert text numbers to digits
1388 if (is_numeric($number_text)) {
1389 $requested_paragraphs = (int) $number_text;
1390 } else {
1391 // For words like "several", "few", "couple", etc.
1392 switch (strtolower($number_text)) {
1393 // Words meaning approximately "2"
1394 case 'couple':
1395 case 'пару': // Russian
1396 case 'пара': // Russian
1397 case 'un par': // Spanish
1398 case 'une paire': // French
1399 case 'ein paar': // German
1400 case 'un paio': // Italian
1401 case 'um par': // Portuguese
1402 case 'parę': // Polish
1403 case 'pare': // Polish
1404 $requested_paragraphs = 2;
1405 break;
1406
1407 // Words meaning approximately "3-4" (several/few)
1408 case 'few':
1409 case 'several':
1410 case 'some':
1411 case 'несколько': // Russian
1412 case 'еще': // Russian
1413 case 'ещё': // Russian
1414 case 'varios': // Spanish
1415 case 'algunos': // Spanish
1416 case 'unos': // Spanish
1417 case 'plusieurs': // French
1418 case 'quelques': // French
1419 case 'certains': // French
1420 case 'mehrere': // German
1421 case 'einige': // German
1422 case 'manche': // German
1423 case 'diversi': // Italian
1424 case 'alcuni': // Italian
1425 case 'qualche': // Italian
1426 case 'vários': // Portuguese
1427 case 'alguns': // Portuguese
1428 case 'kilka': // Polish
1429 case 'niektóre': // Polish
1430 default:
1431 $requested_paragraphs = 3; // Default "several" = 3
1432 break;
1433 }
1434 }
1435 } else {
1436 // Then check for general add keywords
1437 foreach ($add_content_keywords as $keyword) {
1438 if (stripos($prompt, $keyword) !== false) {
1439 $contains_add_keyword = true;
1440 $requested_paragraphs = 2; // Default to 2 paragraphs if just "add paragraphs"
1441 break;
1442 }
1443 }
1444 }
1445
1446 // Build the system message dynamically based on the prompt analysis
1447 $system_message = $instruction_context;
1448 if ($contains_add_keyword) {
1449 if ($requested_paragraphs > 0) {
1450 // Request only new paragraphs, without modifying original content
1451 $system_message = 'You are an assistant that generates NEW content only, without modifying the original text. DO NOT repeat or return the original text in your response.';
1452 $system_message .= sprintf(
1453 ' IMPORTANT: You must generate EXACTLY %d NEW distinct paragraphs. Return ONLY these new paragraphs, properly formatted with HTML <p> tags around each paragraph. The generated paragraphs should be a logical continuation or addition to the original content.',
1454 $requested_paragraphs
1455 );
1456 } else {
1457 $system_message .= ' IMPORTANT: The user is asking you to ADD content, not replace it. Make sure to preserve all the original text and add to it with at least 2-3 new paragraphs or sections.';
1458 }
1459 }
1460
1461 $body = [
1462 'model' => $model,
1463 'messages' => [
1464 [
1465 'role' => 'system',
1466 'content' => $system_message,
1467 ],
1468 [
1469 'role' => 'user',
1470 'content' => ($contains_add_keyword && $requested_paragraphs > 0)
1471 ? sprintf(
1472 "Original Text for context: %s\n\nInstruction: Generate %d new paragraphs to add to this text, following the same style and continuing the topic. Return ONLY the new paragraphs.",
1473 $original,
1474 $requested_paragraphs
1475 )
1476 : sprintf(
1477 /* translators: %1$s: User's instruction prompt, %2$s: Original text to modify. */
1478 esc_html__("Instruction: %1\$s\nOriginal Text: %2\$s\n\nReturn the complete modified text that incorporates both the original content and your changes, unless explicitly asked to replace content.", 'king-addons'),
1479 $prompt,
1480 $original
1481 ),
1482 ],
1483 ],
1484 'max_tokens' => 10000, // Increased to allow for more content
1485 'temperature' => 0.7, // Slightly more creative
1486 ];
1487
1488 // Set append_mode flag for paragraph additions
1489 $append_mode = ($contains_add_keyword && $requested_paragraphs > 0);
1490
1491 // Modify request for WYSIWYG editor
1492 $editor_type = isset($_POST['editor_type']) ? sanitize_text_field(wp_unslash($_POST['editor_type'])) : 'text';
1493 if ($editor_type === 'wysiwyg') {
1494 // Add a specific instruction for formatting
1495 $body['messages'][0]['content'] .= ' Format the response as proper HTML with <p> tags for paragraphs and appropriate spacing. IMPORTANT: Do NOT use code fences (``` or ```html) in your response.';
1496
1497 if (!$append_mode) {
1498 // Only add this for non-append mode
1499 $body['messages'][1]['content'] .= "\n\nOutput should be properly formatted HTML with <p> tags for paragraphs, maintaining good spacing and readability. Do NOT use code fences (```html or ```) - provide just the clean HTML.";
1500 }
1501
1502 // Increase temperature for WYSIWYG to be more creative when creating paragraphs
1503 $body['temperature'] = 0.8;
1504
1505 // Increase max_tokens for longer responses with multiple paragraphs
1506 $body['max_tokens'] = 15000;
1507 }
1508
1509 $response = wp_remote_post(
1510 'https://api.openai.com/v1/chat/completions',
1511 [
1512 'headers' => [
1513 'Authorization' => 'Bearer ' . $api_key,
1514 'Content-Type' => 'application/json',
1515 ],
1516 'body' => wp_json_encode($body),
1517 'timeout' => 30,
1518 ]
1519 );
1520
1521 if (is_wp_error($response)) {
1522 wp_send_json_error(['message' => $response->get_error_message()], 500);
1523 }
1524
1525 $code = wp_remote_retrieve_response_code($response);
1526 $data = json_decode(wp_remote_retrieve_body($response), true);
1527
1528 if ($code !== 200 || empty($data['choices'][0]['message']['content'])) {
1529 $error_msg = $data['error']['message'] ?? esc_html__('AI change error.', 'king-addons');
1530 wp_send_json_error(['message' => $error_msg], 500);
1531 }
1532
1533 $changed = trim($data['choices'][0]['message']['content']);
1534
1535 // Clean up any code fence markers for WYSIWYG editor
1536 if ($editor_type === 'wysiwyg') {
1537 // Remove code fence markers (```html and ```) that might be returned by AI
1538 $changed = preg_replace('/^```(?:html|HTML)?\s*/', '', $changed);
1539 $changed = preg_replace('/```\s*$/', '', $changed);
1540 }
1541
1542 // Update token usage statistics if present in the response
1543 if (isset($data['usage']['total_tokens'])) {
1544 $this->incrementAiDailyUsage(intval($data['usage']['total_tokens']));
1545 }
1546
1547 // Send response with append mode flag
1548 wp_send_json_success([
1549 'text' => $changed,
1550 'append_mode' => $append_mode,
1551 'original' => $append_mode ? $original : '',
1552 'usage' => [
1553 'tokens_used' => $data['usage']['total_tokens'] ?? 0,
1554 'daily_used' => $this->getAiDailyUsage(),
1555 'daily_limit' => $daily_limit,
1556 ]
1557 ]);
1558 }
1559
1560 /**
1561 * Renders Usage Quota Settings section.
1562 *
1563 * @return void
1564 */
1565 public function renderAiQuotaSection(): void
1566 {
1567 echo '<p>' . esc_html__('Set the daily token limit for AI features.', 'king-addons') . '</p>';
1568 }
1569
1570 /**
1571 * Renders the Daily Token Limit input field.
1572 *
1573 * @return void
1574 */
1575 public function renderAiDailyLimitField(): void
1576 {
1577 $options = get_option('king_addons_ai_options', []);
1578 $daily_token_limit = $options['daily_token_limit'] ?? self::DEFAULT_DAILY_TOKEN_LIMIT;
1579
1580 echo '<div class="daily-token-limit-wrap">';
1581 printf(
1582 '<input type="number" name="king_addons_ai_options[daily_token_limit]" value="%s" class="regular-text" min="0" step="1000" style="margin-right: 10px;" />',
1583 esc_attr($daily_token_limit)
1584 );
1585 echo '<span>' . esc_html__('tokens', 'king-addons') . '</span>';
1586 echo '</div>';
1587
1588 echo '<p class="description">' . esc_html__('Set the maximum number of tokens allowed per day for AI features. Set to 0 for unlimited.', 'king-addons') . '</p>';
1589
1590 echo '<div class="king-addons-info-box">';
1591 echo '<p><strong>' . esc_html__('About tokens:', 'king-addons') . '</strong> ' .
1592 esc_html__('Tokens are the basic unit of text that the AI processes. As a rough guide:', 'king-addons') . '</p>';
1593 echo '<p>• ' . esc_html__('1 token ≈ 4 characters or 0.75 words in English', 'king-addons') . '</p>';
1594 echo '<p>• ' . esc_html__('A typical paragraph might use around 50-100 tokens', 'king-addons') . '</p>';
1595 echo '<p>• ' . esc_html__('A full page of text (500 words) is approximately 750 tokens', 'king_addons') . '</p>';
1596 echo '<p>• ' . esc_html__('Recommended daily limit: 10,000 - 50,000 tokens for moderate use', 'king-addons') . '</p>';
1597 echo '</div>';
1598 }
1599
1600 /**
1601 * Default daily token limit if not explicitly set.
1602 *
1603 * @var int
1604 */
1605 private const DEFAULT_DAILY_TOKEN_LIMIT = 1000000;
1606
1607 /**
1608 * Gets the current daily token usage.
1609 *
1610 * @return int Number of tokens used today.
1611 */
1612 private function getAiDailyUsage(): int
1613 {
1614 $usage_data = get_option('king_addons_ai_daily_usage', ['date' => '', 'count' => 0]);
1615 $today = current_time('Y-m-d');
1616 if (!isset($usage_data['date']) || $usage_data['date'] !== $today) {
1617 return 0;
1618 }
1619 return intval($usage_data['count']);
1620 }
1621
1622 /**
1623 * Increments the daily token usage count.
1624 *
1625 * @param int $tokens Number of tokens to add.
1626 * @return void
1627 */
1628 public function incrementAiDailyUsage(int $tokens): void
1629 {
1630 $today = current_time('Y-m-d');
1631 $usage_data = get_option('king_addons_ai_daily_usage', ['date' => '', 'count' => 0]);
1632 if (!isset($usage_data['date']) || $usage_data['date'] !== $today) {
1633 $usage_data = [
1634 'date' => $today,
1635 'count' => 0,
1636 ];
1637 }
1638 $usage_data['count'] = intval($usage_data['count']) + $tokens;
1639 update_option('king_addons_ai_daily_usage', $usage_data, false);
1640 }
1641
1642 /**
1643 * Renders Usage Statistics section.
1644 *
1645 * @return void
1646 */
1647 public function renderAiStatsSection(): void
1648 {
1649 $usage_data = get_option('king_addons_ai_daily_usage', ['date' => '', 'count' => 0]);
1650 $today = current_time('Y-m-d');
1651 $used = (isset($usage_data['date']) && $usage_data['date'] === $today) ? intval($usage_data['count']) : 0;
1652
1653 $options = get_option('king_addons_ai_options', []);
1654 $limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1655
1656 if ($limit > 0) {
1657 $limit_display = number_format_i18n($limit);
1658 $remaining = max(0, $limit - $used);
1659 $remaining_display = number_format_i18n($remaining);
1660
1661 $usage_percentage = ($limit > 0) ? min(100, round(($used / $limit) * 100)) : 0;
1662
1663 echo '<div class="king-addons-ai-usage-stats">';
1664 echo '<table class="form-table">';
1665 echo '<tr>';
1666 echo '<th>' . esc_html__('Tokens Used Today', 'king-addons') . '</th>';
1667 echo '<td><strong>' . esc_html(number_format_i18n($used)) . '</strong></td>';
1668 echo '</tr>';
1669 echo '<tr>';
1670 echo '<th>' . esc_html__('Daily Limit', 'king-addons') . '</th>';
1671 echo '<td>' . esc_html($limit_display) . '</td>';
1672 echo '</tr>';
1673 echo '<tr>';
1674 echo '<th>' . esc_html__('Remaining', 'king-addons') . '</th>';
1675 echo '<td>' . esc_html($remaining_display) . '</td>';
1676 echo '</tr>';
1677 echo '</table>';
1678
1679 // Add progress bar
1680 echo '<div class="king-addons-ai-usage-bar-container" style="background-color: #f0f0f0; height: 20px; border-radius: 10px; margin: 15px 0; overflow: hidden;">';
1681 echo '<div class="king-addons-ai-usage-bar" style="width: ' . esc_attr($usage_percentage) . '%; background-color: ' . esc_attr($usage_percentage > 80 ? '#ff5a5a' : ($usage_percentage > 60 ? '#ffa500' : '#4CAF50')) . '; height: 100%;"></div>';
1682 echo '</div>';
1683 echo '<p class="description">' . esc_html(sprintf(__('Usage: %d%%', 'king-addons'), $usage_percentage)) . '</p>';
1684 echo '</div>';
1685 } else {
1686 echo '<p>' . esc_html__('No daily token limit is set. All requests will be processed.', 'king-addons') . '</p>';
1687 echo '<p><strong>' . esc_html__('Tokens used today:', 'king-addons') . ' ' . esc_html(number_format_i18n($used)) . '</strong></p>';
1688 }
1689 }
1690
1691 /**
1692 * AJAX handler to check token usage limits.
1693 *
1694 * @return void
1695 */
1696 public function handleAiCheckTokens(): void
1697 {
1698 check_ajax_referer('king_addons_ai_generate_nonce', 'nonce');
1699 if (!current_user_can('manage_options')) {
1700 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1701 }
1702
1703 $options = get_option('king_addons_ai_options', []);
1704 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1705 $daily_used = $this->getAiDailyUsage();
1706 // Check if API key and model are set
1707 $api_key = $options['openai_api_key'] ?? '';
1708 $model = $options['openai_model'] ?? '';
1709 $api_key_valid = !empty($api_key) && !empty($model);
1710
1711 wp_send_json_success([
1712 'daily_used' => $daily_used,
1713 'daily_limit' => $daily_limit,
1714 'limit_reached' => ($daily_limit > 0 && $daily_used >= $daily_limit),
1715 'api_key_valid' => $api_key_valid,
1716 ]);
1717 }
1718
1719 /**
1720 * AJAX handler to check image generation limits.
1721 *
1722 * @return void
1723 */
1724 public function handleAiImageCheckLimits(): void
1725 {
1726 check_ajax_referer('king_addons_ai_generate_image_nonce', 'nonce');
1727 if (!current_user_can('manage_options')) {
1728 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1729 }
1730
1731 $options = get_option('king_addons_ai_options', []);
1732 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1733 $daily_used = $this->getAiDailyUsage();
1734 // Check if API key and model are set
1735 $api_key = $options['openai_api_key'] ?? '';
1736 $model = $options['openai_model'] ?? '';
1737 $api_key_valid = !empty($api_key) && !empty($model);
1738
1739 wp_send_json_success([
1740 'daily_used' => $daily_used,
1741 'daily_limit' => $daily_limit,
1742 'limit_reached' => ($daily_limit > 0 && $daily_used >= $daily_limit),
1743 'api_key_valid' => $api_key_valid,
1744 ]);
1745
1746 }
1747
1748
1749 /**
1750 * Renders the Editor Integration section description.
1751 *
1752 * @return void
1753 */
1754 public function renderAiEditorSection(): void
1755 {
1756 echo '<p>' . esc_html__('Control the integration of AI features in the Elementor editor.', 'king-addons') . '</p>';
1757 }
1758
1759 /**
1760 * Renders description for Alt Text Settings section.
1761 *
1762 * @return void
1763 */
1764 public function renderAiAltTextSection(): void
1765 {
1766 echo '<p>' . esc_html__('Configure automatic alt text generation for images in Media Library.', 'king-addons') . '</p>';
1767 }
1768
1769 /**
1770 * Renders the Enable AI Buttons checkbox field.
1771 *
1772 * @return void
1773 */
1774 public function renderAiEnableButtonsField(): void
1775 {
1776 $options = get_option('king_addons_ai_options', []);
1777 // Default to true if option has never been saved, otherwise use saved value
1778 $enabled = array_key_exists('enable_ai_buttons', $options) ? (bool) $options['enable_ai_buttons'] : true;
1779 printf(
1780 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_buttons]" value="1" %s /> %s</label>',
1781 checked($enabled, true, false),
1782 esc_html__('Enable AI Text Editing Buttons in Elementor Editor', 'king-addons')
1783 );
1784 }
1785
1786 /**
1787 * Renders the Enable AI Image Generation checkbox field.
1788 *
1789 * @return void
1790 */
1791 public function renderAiImageGenerationField(): void
1792 {
1793 $options = get_option('king_addons_ai_options', []);
1794 // Default to true if option has never been saved, otherwise use saved value
1795 $enabled = array_key_exists('enable_ai_image_generation_button', $options) ? (bool) $options['enable_ai_image_generation_button'] : true;
1796 printf(
1797 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_image_generation_button]" value="1" %s /> %s</label>',
1798 checked($enabled, true, false),
1799 esc_html__('Enable AI Image Generation Button in Elementor Editor', 'king-addons')
1800 );
1801 }
1802
1803 /**
1804 * Renders the Enable AI Alt Text Button checkbox field.
1805 *
1806 * @return void
1807 */
1808 public function renderAiAltTextButtonField(): void
1809 {
1810 $options = get_option('king_addons_ai_options', []);
1811 // Default to true if option has never been saved, otherwise use saved value
1812 $enabled = array_key_exists('enable_ai_alt_text_button', $options) ? (bool) $options['enable_ai_alt_text_button'] : true;
1813 printf(
1814 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_alt_text_button]" value="1" %s /> %s</label>',
1815 checked($enabled, true, false),
1816 esc_html__('Enable AI Alt Text Generation Button in Media Library', 'king-addons')
1817 );
1818 echo '<p class="description">' . esc_html__('Show "Generate" button in Media Library to manually create alt text for images using AI.', 'king-addons') . '</p>';
1819 }
1820
1821 /**
1822 * Renders the Enable AI Alt Text Auto Generation checkbox field.
1823 *
1824 * @return void
1825 */
1826 public function renderAiAltTextAutoGenerationField(): void
1827 {
1828 $options = get_option('king_addons_ai_options', []);
1829 $is_pro = !king_addons_freemius()->can_use_premium_code();
1830 // Default to false if option has never been saved, otherwise use saved value
1831 $enabled = array_key_exists('enable_ai_alt_text_auto_generation', $options) ? (bool) $options['enable_ai_alt_text_auto_generation'] : false;
1832 printf(
1833 '<label><input type="checkbox"' . ($is_pro ? ' disabled' : '') . ' name="king_addons_ai_options[enable_ai_alt_text_auto_generation]" value="1" %s /> %s</label>',
1834 checked($enabled, true, false),
1835 esc_html__('Automatically Generate Alt Text for New Images' . ($is_pro ? ' (PRO feature)' : ''), 'king-addons')
1836 );
1837 echo '<p class="description">' . esc_html__('Automatically generate alt text when new images are uploaded to Media Library. Great for SEO.', 'king-addons') . '</p>';
1838 }
1839
1840
1841 /**
1842 * Renders the AI Alt Text Generation Interval field.
1843 *
1844 * @return void
1845 */
1846 public function renderAiAltTextIntervalField(): void
1847 {
1848 $options = get_option('king_addons_ai_options', []);
1849 $interval = isset($options['ai_alt_text_generation_interval']) ? (int) $options['ai_alt_text_generation_interval'] : 60;
1850 printf(
1851 '<input type="number" name="king_addons_ai_options[ai_alt_text_generation_interval]" value="%d" min="10" max="3600" placeholder="60" />',
1852 $interval
1853 );
1854 echo '<p class="description">' . esc_html__('How often (in seconds) the system should process alt text generation queue. Recommended: 60 seconds to avoid OpenAI API rate limits. Lower values may cause API errors during high usage periods. Range: 10-3600 seconds.', 'king-addons') . '</p>';
1855 }
1856
1857 /**
1858 * Renders the image model selection dropdown field.
1859 *
1860 * @return void
1861 */
1862 public function renderAiImageModelField(): void
1863 {
1864 $options = get_option('king_addons_ai_options', []);
1865 $selected = $options['openai_image_model'] ?? 'dall-e-3';
1866 $models = [
1867 'dall-e-3' => esc_html__('DALL·E 3', 'king-addons'),
1868 'gpt-image-1' => esc_html__('GPT Image 1', 'king-addons'),
1869 ];
1870 printf(
1871 '<select name="king_addons_ai_options[openai_image_model]" %s>',
1872 ''
1873 );
1874 foreach ($models as $id => $label) {
1875 printf(
1876 '<option value="%s" %s>%s</option>',
1877 esc_attr($id),
1878 selected($selected, $id, false),
1879 esc_html($label)
1880 );
1881 }
1882 echo '</select>';
1883 echo '<p class="description">';
1884 printf(
1885 /* translators: %1$s: URL to OpenAI Organization Settings */
1886 wp_kses(
1887 __('Select the default model for AI image generation. By default, the model is DALL·E 3. For now, your organization must be verified to use the model GPT Image 1. Please go to <a href="%1$s" target="_blank" rel="noopener noreferrer">OpenAI Organization Settings</a> to verify. If you just verified, it can take up to 15 minutes for access to propagate.', 'king-addons'),
1888 ['a' => ['href' => [], 'target' => [], 'rel' => []]]
1889 ),
1890 esc_url('https://platform.openai.com/settings/organization/general')
1891 );
1892 echo '</p>';
1893 }
1894
1895 /**
1896 * AJAX handler to generate images using OpenAI.
1897 *
1898 * @return void
1899 */
1900 public function set_openai_curl_options($handle, $r, $url): void
1901 {
1902 if (!is_string($url) || $url === '') {
1903 return;
1904 }
1905
1906 // Apply these cURL options only to OpenAI requests to avoid affecting other outbound HTTP calls.
1907 if (strpos($url, 'openai.com') === false) {
1908 return;
1909 }
1910
1911 // Increase connect timeout to 60s, and total timeout to 5m.
1912 curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 60);
1913 curl_setopt($handle, CURLOPT_DNS_CACHE_TIMEOUT, 300);
1914 curl_setopt($handle, CURLOPT_TIMEOUT, 300);
1915 }
1916
1917 public function handleAiGenerateImage(): void
1918 {
1919 // Verify nonce and permissions
1920 check_ajax_referer('king_addons_ai_generate_image_nonce', 'nonce');
1921 if (!current_user_can('manage_options')) {
1922 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1923 }
1924
1925 // Gather input parameters
1926 $prompt = isset($_POST['prompt']) ? sanitize_textarea_field(wp_unslash($_POST['prompt'])) : '';
1927 $quality = isset($_POST['quality']) ? sanitize_text_field(wp_unslash($_POST['quality'])) : '';
1928 $size = isset($_POST['size']) ? sanitize_text_field(wp_unslash($_POST['size'])) : '';
1929 // Model from frontend selector
1930 $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : 'dall-e-3';
1931
1932 $options = get_option('king_addons_ai_options', []);
1933 $api_key = $options['openai_api_key'] ?? '';
1934 if (empty($api_key)) {
1935 wp_send_json_error(['message' => esc_html__('OpenAI API key is not set.', 'king-addons')], 400);
1936 }
1937 if (empty($prompt)) {
1938 wp_send_json_error(['message' => esc_html__('Please provide an image prompt.', 'king-addons')], 400);
1939 }
1940
1941 // Build request body based on selected model
1942 $body = [
1943 'model' => $model,
1944 'prompt' => $prompt,
1945 'size' => $size,
1946 ];
1947 if ($model === 'dall-e-3') {
1948 // DALL·E 3 parameters
1949 $body['n'] = 1;
1950 $body['quality'] = ($quality === 'hd') ? 'hd' : 'standard';
1951 } elseif ($model === 'gpt-image-1') {
1952 // GPT Image 1 parameters
1953 // Only include background when transparent is requested
1954 if (!empty($_POST['background']) && 'transparent' === sanitize_text_field(wp_unslash($_POST['background']))) {
1955 $body['background'] = 'transparent';
1956 }
1957 $body['quality'] = in_array($quality, ['low', 'medium', 'high', 'auto'], true)
1958 ? $quality
1959 : 'auto';
1960 }
1961
1962 add_action('http_api_curl', [$this, 'set_openai_curl_options'], 10, 3);
1963
1964 // Call OpenAI Image Generations API
1965 $response = wp_remote_post(
1966 'https://api.openai.com/v1/images/generations',
1967 [
1968 'headers' => [
1969 'Authorization' => 'Bearer ' . $api_key,
1970 'Content-Type' => 'application/json',
1971 ],
1972 'body' => wp_json_encode($body),
1973 'timeout' => 300,
1974 ]
1975 );
1976 if (is_wp_error($response)) {
1977 wp_send_json_error(['message' => $response->get_error_message()], 500);
1978 }
1979
1980 $image_url = '';
1981
1982 if ($model === 'gpt-image-1') {
1983 // Grab and decode the base64
1984
1985 $data = json_decode(wp_remote_retrieve_body($response), true);
1986
1987 $image_base64 = $data['data'][0]['b64_json'];
1988
1989 $bytes = base64_decode($image_base64);
1990 if (!$bytes) {
1991 wp_send_json_error(['message' => 'Invalid image data from API.'], 500);
1992 }
1993
1994 // Create a temp file and write it
1995 $tmp = wp_tempnam('gpt-image-1.png');
1996 if (!$tmp || !file_put_contents($tmp, $bytes)) {
1997 wp_send_json_error(['message' => 'Failed to write temp image file.'], 500);
1998 }
1999
2000 // Prepare for sideload
2001 $file = [
2002 'name' => substr(sanitize_file_name($prompt), 0, 100) . '.png',
2003 'tmp_name' => $tmp,
2004 ];
2005
2006 // Make sure these are loaded
2007 require_once ABSPATH . 'wp-admin/includes/image.php';
2008 require_once ABSPATH . 'wp-admin/includes/file.php';
2009 require_once ABSPATH . 'wp-admin/includes/media.php';
2010
2011 // Sideload into the Media Library
2012 $attach_id = media_handle_sideload($file, 0, $prompt);
2013 if (is_wp_error($attach_id)) {
2014 wp_send_json_error(['message' => $attach_id->get_error_message()], 500);
2015 }
2016
2017 $url = wp_get_attachment_url($attach_id);
2018 wp_send_json_success(['attachment_id' => $attach_id, 'url' => $url]);
2019 } else {
2020
2021
2022 $code = wp_remote_retrieve_response_code($response);
2023 $data = json_decode(wp_remote_retrieve_body($response), true);
2024 if ($code !== 200 || empty($data['data'][0]['url'])) {
2025 $error_msg = $data['error']['message'] ?? esc_html__('AI image generation error.', 'king-addons');
2026 wp_send_json_error(['message' => $error_msg], 500);
2027 }
2028
2029 // Sideload image into media library
2030 require_once ABSPATH . 'wp-admin/includes/image.php';
2031 require_once ABSPATH . 'wp-admin/includes/file.php';
2032 require_once ABSPATH . 'wp-admin/includes/media.php';
2033
2034 if ($model === 'dall-e-3') {
2035 $image_url = esc_url_raw($data['data'][0]['url']);
2036 }
2037
2038 $attachment_id = media_sideload_image($image_url, 0, $prompt, 'id');
2039
2040 if (is_wp_error($attachment_id)) {
2041 wp_send_json_error(['message' => $attachment_id->get_error_message()], 500);
2042 }
2043 $attachment_url = wp_get_attachment_url($attachment_id);
2044
2045 // Respond with attachment details
2046 wp_send_json_success([
2047 'attachment_id' => $attachment_id,
2048 'url' => $attachment_url,
2049 ]);
2050 }
2051 }
2052
2053 /**
2054 * Renders the Image Detail Level dropdown for Alt Text Settings.
2055 *
2056 * @return void
2057 */
2058 public function renderAiAltTextImageDetailLevelField(): void
2059 {
2060 $options = get_option('king_addons_ai_options', []);
2061 $selected = $options['ai_alt_text_image_detail_level'] ?? 'low';
2062 echo '<select name="king_addons_ai_options[ai_alt_text_image_detail_level]">';
2063 echo '<option value="low"' . selected($selected, 'low', false) . '>' . esc_html__('Low', 'king-addons') . '</option>';
2064 echo '<option value="high"' . selected($selected, 'high', false) . '>' . esc_html__('High', 'king-addons') . '</option>';
2065 echo '</select>';
2066 echo '<p class="description">' . esc_html__("Controls the detail level OpenAI uses to analyze images. 'Low' uses a fixed, lower token cost. 'High' uses more tokens based on image size (potentially more accurate analysis, but costs more). See OpenAI pricing for details.", 'king-addons') . '</p>';
2067 }
2068
2069 /**
2070 * Renders the Translation Settings section description.
2071 *
2072 * @return void
2073 */
2074 public function renderAiTranslationSection(): void
2075 {
2076 echo '<p>' . esc_html__('Configure AI Page Translator settings for Elementor editor.', 'king-addons') . '</p>';
2077 }
2078
2079 /**
2080 * Renders the Enable AI Page Translator checkbox field.
2081 *
2082 * @return void
2083 */
2084 public function renderAiPageTranslatorField(): void
2085 {
2086 $options = get_option('king_addons_ai_options', []);
2087 // Default to true if option has never been saved, otherwise use saved value
2088 $enabled = array_key_exists('enable_ai_page_translator', $options) ? (bool) $options['enable_ai_page_translator'] : true;
2089 printf(
2090 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_page_translator]" value="1" %s /> %s</label>',
2091 checked($enabled, true, false),
2092 esc_html__('Show AI Page Translator button in Elementor editor toolbar', 'king-addons')
2093 );
2094 echo '<p class="description">' . esc_html__('When enabled, adds an AI Page Translator button to the Elementor editor top toolbar that allows you to translate entire pages with one click. Automatically detects and translates all text content in widgets including advanced repeater fields.', 'king-addons') . '</p>';
2095 }
2096
2097 /**
2098 * AJAX handler to translate text using OpenAI.
2099 *
2100 * @return void
2101 */
2102 public function handleAiTranslateText(): void
2103 {
2104 check_ajax_referer('king_addons_ai_generate_nonce', 'nonce');
2105 if (!current_user_can('manage_options')) {
2106 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
2107 }
2108
2109 $text = isset($_POST['text']) ? sanitize_textarea_field(wp_unslash($_POST['text'])) : '';
2110 $from_lang = isset($_POST['from_lang']) ? sanitize_text_field(wp_unslash($_POST['from_lang'])) : 'auto';
2111 $to_lang = isset($_POST['to_lang']) ? sanitize_text_field(wp_unslash($_POST['to_lang'])) : 'en';
2112
2113 $options = get_option('king_addons_ai_options', []);
2114 $api_key = $options['openai_api_key'] ?? '';
2115 $model = $options['openai_model'] ?? '';
2116
2117 if (empty($api_key) || empty($model)) {
2118 wp_send_json_error(['message' => esc_html__('API key or model not set.', 'king-addons')], 400);
2119 }
2120
2121 if (empty($text)) {
2122 wp_send_json_error(['message' => esc_html__('No text provided for translation.', 'king-addons')], 400);
2123 }
2124
2125 // Check daily token limit
2126 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
2127 $current_usage = $this->getAiDailyUsage();
2128
2129 if ($daily_limit > 0 && $current_usage >= $daily_limit) {
2130 wp_send_json_error([
2131 'message' => esc_html__('Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.', 'king-addons')
2132 ], 429);
2133 }
2134
2135 // Prepare translation prompt
2136 $from_lang_name = ($from_lang === 'auto') ? 'auto-detected language' : $this->getLanguageName($from_lang);
2137 $to_lang_name = $this->getLanguageName($to_lang);
2138
2139 // Enhanced system message for better custom language and prompt handling
2140 $system_message = 'You are a professional translator with expertise in languages, dialects, writing styles, and custom translation approaches. You can handle:
2141
2142 1. Standard languages (English, Spanish, etc.)
2143 2. Fictional/constructed languages (Klingon, Dothraki, Elvish, etc.)
2144 3. Historical language variants (Old English, Latin, etc.)
2145 4. Writing styles and tones (formal, casual, academic, business, etc.)
2146 5. Special communication styles (pirate speak, baby talk, technical jargon, etc.)
2147
2148 When translating:
2149 - Maintain the original meaning, tone, and formatting
2150 - Preserve HTML tags exactly as they appear
2151 - For custom languages, apply consistent linguistic rules
2152 - For style prompts, adapt the tone and vocabulary appropriately
2153 - Only return the translated/adapted text without explanations
2154
2155 If the target is a style rather than a language, transform the text to match that style while keeping the same language.';
2156
2157 // Enhanced user message with better context for custom languages and prompts
2158 if ($from_lang === 'auto') {
2159 $user_message = "Transform the following text to {$to_lang_name}:\n\n{$text}";
2160 } else {
2161 // Check if it looks like a style prompt rather than a language
2162 $is_style_prompt = $this->isStylePrompt($to_lang_name);
2163
2164 if ($is_style_prompt) {
2165 $user_message = "Transform the following text from {$from_lang_name} using this style/approach: {$to_lang_name}:\n\n{$text}";
2166 } else {
2167 $user_message = "Translate the following text from {$from_lang_name} to {$to_lang_name}:\n\n{$text}";
2168 }
2169 }
2170
2171 $messages = [
2172 ['role' => 'system', 'content' => $system_message],
2173 ['role' => 'user', 'content' => $user_message]
2174 ];
2175
2176 $response = wp_remote_post(
2177 'https://api.openai.com/v1/chat/completions',
2178 [
2179 'headers' => [
2180 'Authorization' => 'Bearer ' . $api_key,
2181 'Content-Type' => 'application/json',
2182 ],
2183 'body' => wp_json_encode([
2184 'model' => $model,
2185 'messages' => $messages,
2186 'max_tokens' => 1000,
2187 'temperature' => 0.3, // Lower temperature for more consistent translations
2188 ]),
2189 'timeout' => 30,
2190 ]
2191 );
2192
2193 if (is_wp_error($response)) {
2194 wp_send_json_error(['message' => $response->get_error_message()], 500);
2195 }
2196
2197 $code = wp_remote_retrieve_response_code($response);
2198 $data = json_decode(wp_remote_retrieve_body($response), true);
2199
2200 if ($code !== 200 || empty($data['choices'][0]['message']['content'])) {
2201 $error_msg = $data['error']['message'] ?? esc_html__('AI translation error.', 'king-addons');
2202 wp_send_json_error(['message' => $error_msg], 500);
2203 }
2204
2205 $translated_text = trim($data['choices'][0]['message']['content']);
2206
2207 // Update token usage statistics if present in the response
2208 if (isset($data['usage']['total_tokens'])) {
2209 $this->incrementAiDailyUsage(intval($data['usage']['total_tokens']));
2210 }
2211
2212 wp_send_json_success([
2213 'translated_text' => $translated_text,
2214 'usage' => [
2215 'tokens_used' => $data['usage']['total_tokens'] ?? 0,
2216 'daily_used' => $this->getAiDailyUsage(),
2217 'daily_limit' => $daily_limit,
2218 ]
2219 ]);
2220 }
2221
2222 /**
2223 * Get language name by code
2224 *
2225 * @param string $code Language code
2226 * @return string Language name
2227 */
2228 private function getLanguageName(string $code): string
2229 {
2230 $languages = [
2231 'en' => 'English',
2232 'es' => 'Spanish',
2233 'fr' => 'French',
2234 'de' => 'German',
2235 'it' => 'Italian',
2236 'pt' => 'Portuguese',
2237 'ru' => 'Russian',
2238 'ja' => 'Japanese',
2239 'ko' => 'Korean',
2240 'zh' => 'Chinese',
2241 'ar' => 'Arabic',
2242 'hi' => 'Hindi',
2243 'nl' => 'Dutch',
2244 'pl' => 'Polish',
2245 'tr' => 'Turkish',
2246 'uk' => 'Ukrainian',
2247 'cs' => 'Czech',
2248 'sv' => 'Swedish',
2249 'no' => 'Norwegian',
2250 'da' => 'Danish',
2251 'fi' => 'Finnish'
2252 ];
2253
2254 return $languages[$code] ?? $code;
2255 }
2256
2257 /**
2258 * Check if the given text appears to be a style prompt rather than a language
2259 *
2260 * @param string $text The text to check
2261 * @return bool True if it looks like a style prompt
2262 */
2263 private function isStylePrompt(string $text): bool
2264 {
2265 $text_lower = strtolower($text);
2266
2267 // Common style/tone indicators
2268 $style_indicators = [
2269 'formal',
2270 'casual',
2271 'professional',
2272 'business',
2273 'academic',
2274 'technical',
2275 'friendly',
2276 'serious',
2277 'humorous',
2278 'dramatic',
2279 'poetic',
2280 'simple',
2281 'complex',
2282 'detailed',
2283 'brief',
2284 'conversational',
2285 'literary',
2286 'scientific',
2287 'medical',
2288 'legal',
2289 'marketing',
2290 'sales',
2291 'tone',
2292 'style',
2293 'manner',
2294 'approach',
2295 'way',
2296 'voice',
2297 'pirate',
2298 'shakespeare',
2299 'baby',
2300 'child',
2301 'elderly',
2302 'slang',
2303 'jargon',
2304 'dialect',
2305 'accent'
2306 ];
2307
2308 // Check if any style indicators are present
2309 foreach ($style_indicators as $indicator) {
2310 if (strpos($text_lower, $indicator) !== false) {
2311 return true;
2312 }
2313 }
2314
2315 // Check if it contains descriptive phrases
2316 $descriptive_patterns = [
2317 'for ',
2318 'like ',
2319 'as if ',
2320 'in the style of',
2321 'in a ',
2322 'with a ',
2323 'using ',
2324 'speaking ',
2325 'written ',
2326 'sound like',
2327 'talk like'
2328 ];
2329
2330 foreach ($descriptive_patterns as $pattern) {
2331 if (strpos($text_lower, $pattern) !== false) {
2332 return true;
2333 }
2334 }
2335
2336 return false;
2337 }
2338 }
2339