PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.82
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.82
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.82, at includes/Admin.php

2,860 lines 116.6 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 add_action('admin_menu', [$this, 'reorderWooBuilderSubmenu'], 1000000000);
29
30 add_action('admin_init', [$this, 'createSettings']);
31 add_action('admin_init', [$this, 'createAiSettings']);
32
33 // Only register wishlist settings when the Wishlist extension is enabled.
34 $options = get_option('king_addons_options', []);
35 $wishlist_enabled = (!isset($options['ext_wishlist']) || $options['ext_wishlist'] === 'enabled')
36 && (defined('KING_ADDONS_EXT_WISHLIST') ? KING_ADDONS_EXT_WISHLIST : true);
37 if ($wishlist_enabled) {
38 add_action('admin_init', [$this, 'createWishlistSettings']);
39 }
40 add_action('admin_enqueue_scripts', [$this, 'enqueueUpgradeLinkScript']);
41 add_action('admin_enqueue_scripts', [$this, 'enqueueGlobalAdminStyles']);
42
43 // AJAX handler for Trinity Backup plugin installation
44 add_action('wp_ajax_king_addons_install_trinity_backup', [$this, 'handleInstallTrinityBackup']);
45 }
46 }
47
48 function addAdminMenu(): void
49 {
50 global $menu;
51 $menu['54.0'] = array( '', 'read', 'separator-king-addons-top', '', 'wp-menu-separator elementor' );
52
53 add_menu_page(
54 'King Addons for Elementor',
55 'King Addons',
56 'manage_options',
57 'king-addons',
58 [$this, 'showDashboardV3'],
59 KING_ADDONS_URL . 'includes/admin/img/icon-for-admin.svg',
60 54.1
61 );
62
63 // Ensure the first submenu item is labeled "Dashboard" (instead of repeating "King Addons").
64 add_submenu_page(
65 'king-addons',
66 esc_html__('Dashboard', 'king-addons'),
67 esc_html__('Dashboard', 'king-addons'),
68 'manage_options',
69 'king-addons',
70 [$this, 'showDashboardV3']
71 );
72
73 add_submenu_page(
74 'king-addons',
75 'King Addons Settings',
76 'Settings',
77 'manage_options',
78 'king-addons-settings',
79 [$this, 'showSettingsPage']
80 );
81
82 // Add AI Settings submenu under King Addons (kept near Settings; final ordering is enforced later).
83 add_submenu_page(
84 'king-addons',
85 esc_html__('AI Settings', 'king-addons'),
86 esc_html__('AI Settings', 'king-addons'),
87 'manage_options',
88 'king-addons-ai-settings',
89 [$this, 'showAiSettingsPage']
90 );
91
92 // Get options for extension toggle checks (before any extension checks)
93 $options = get_option('king_addons_options', []);
94
95 // Check Cookie / Consent Bar extension toggle
96 $cookie_consent_enabled = !isset($options['ext_cookie-consent']) || $options['ext_cookie-consent'] === 'enabled';
97 if ($cookie_consent_enabled && (defined('KING_ADDONS_EXT_COOKIE_CONSENT') ? KING_ADDONS_EXT_COOKIE_CONSENT : true) && class_exists('King_Addons\Cookie_Consent')) {
98 add_submenu_page(
99 'king-addons',
100 esc_html__('Cookie / Consent Bar', 'king-addons'),
101 esc_html__('Cookie / Consent Bar', 'king-addons'),
102 'manage_options',
103 'king-addons-cookie-consent',
104 [Cookie_Consent::instance(), 'render_admin_page']
105 );
106 }
107
108 // Check Age Gate extension toggle
109 $age_gate_enabled = !isset($options['ext_age-gate']) || $options['ext_age-gate'] === 'enabled';
110 if ($age_gate_enabled && (defined('KING_ADDONS_EXT_AGE_GATE') ? KING_ADDONS_EXT_AGE_GATE : true) && class_exists('King_Addons\Age_Gate')) {
111 add_submenu_page(
112 'king-addons',
113 esc_html__('Age Gate', 'king-addons'),
114 esc_html__('Age Gate', 'king-addons'),
115 'manage_options',
116 'king-addons-age-gate',
117 [Age_Gate::instance(), 'render_admin_page']
118 );
119 }
120
121 // Check Live Chat extension toggle
122 $live_chat_enabled = !isset($options['ext_live-chat']) || $options['ext_live-chat'] === 'enabled';
123 if ($live_chat_enabled && (defined('KING_ADDONS_EXT_LIVE_CHAT') ? KING_ADDONS_EXT_LIVE_CHAT : true) && class_exists('King_Addons\Live_Chat')) {
124 add_submenu_page(
125 'king-addons',
126 esc_html__('Live Chat', 'king-addons'),
127 esc_html__('Live Chat', 'king-addons'),
128 'manage_options',
129 'king-addons-live-chat',
130 [Live_Chat::instance(), 'render_admin_page']
131 );
132 }
133
134 // Check Docs & KB extension toggle
135 $docs_kb_enabled = !isset($options['ext_docs-kb']) || $options['ext_docs-kb'] === 'enabled';
136 if ($docs_kb_enabled && (defined('KING_ADDONS_EXT_DOCS_KB') ? KING_ADDONS_EXT_DOCS_KB : true) && class_exists('King_Addons\Docs_KB')) {
137 add_submenu_page(
138 'king-addons',
139 esc_html__('Docs & Knowledge Base', 'king-addons'),
140 esc_html__('Docs & Knowledge Base Builder', 'king-addons'),
141 'manage_options',
142 'king-addons-docs-kb',
143 [Docs_KB::instance(), 'render_admin_page']
144 );
145 }
146
147 // Check Activity Log extension toggle
148 $activity_log_enabled = !isset($options['ext_activity-log']) || $options['ext_activity-log'] === 'enabled';
149 if ($activity_log_enabled && class_exists('King_Addons\\Activity_Log')) {
150 add_submenu_page(
151 'king-addons',
152 esc_html__('Activity Log', 'king-addons'),
153 esc_html__('Activity Log', 'king-addons'),
154 'manage_options',
155 'king-addons-activity-log',
156 [Activity_Log::instance(), 'render_admin_page']
157 );
158 }
159
160 // Check if Form Builder widget is enabled
161 $form_builder_enabled = !isset($options['form-builder']) || $options['form-builder'] === 'enabled';
162 if (KING_ADDONS_WGT_FORM_BUILDER && $form_builder_enabled) {
163 add_submenu_page(
164 'king-addons',
165 esc_html__('Form Submissions', 'king-addons'),
166 esc_html__('Form Submissions', 'king-addons'),
167 'edit_posts',
168 'edit.php?post_type=king-addons-fb-sub',
169 );
170 }
171
172 // Check Templates Catalog extension toggle
173 $templates_enabled = !isset($options['ext_templates-catalog']) || $options['ext_templates-catalog'] === 'enabled';
174 if ($templates_enabled && (defined('KING_ADDONS_EXT_TEMPLATES_CATALOG') ? KING_ADDONS_EXT_TEMPLATES_CATALOG : true) && class_exists('King_Addons\Templates')) {
175 add_menu_page(
176 'King Addons for Elementor',
177 (!king_addons_freemius()->can_use_premium_code() ? esc_html__('Free Templates', 'king-addons') : esc_html__('Templates Pro', 'king-addons')),
178 'manage_options',
179 'king-addons-templates',
180 [Templates::instance(), 'render_template_catalog_page'],
181 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'),
182 54.2
183 );
184 }
185
186 // Check Header & Footer Builder extension toggle
187 $header_footer_enabled = !isset($options['ext_header-footer-builder']) || $options['ext_header-footer-builder'] === 'enabled';
188 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')) {
189 self::showHeaderFooterBuilder();
190 }
191
192 // Check Popup Builder extension toggle
193 $popup_builder_enabled = !isset($options['ext_popup-builder']) || $options['ext_popup-builder'] === 'enabled';
194 if ($popup_builder_enabled && (defined('KING_ADDONS_EXT_POPUP_BUILDER') ? KING_ADDONS_EXT_POPUP_BUILDER : true) && class_exists('King_Addons\Popup_Builder')) {
195 self::showPopupBuilder();
196 }
197
198 // WooCommerce Builder is a top-level menu. Store screens hang off it
199 // when it exists; otherwise they stay under King Addons. Page slugs
200 // do not change.
201 if (king_addons_woo_builder_menu_is_active()) {
202 $this->showWooBuilder();
203 }
204
205 $wishlist_enabled = (!isset($options['ext_wishlist']) || $options['ext_wishlist'] === 'enabled')
206 && (defined('KING_ADDONS_EXT_WISHLIST') ? KING_ADDONS_EXT_WISHLIST : true);
207 if ($wishlist_enabled) {
208 $woo_parent = king_addons_woo_admin_parent_slug();
209 add_submenu_page(
210 $woo_parent,
211 esc_html__('Wishlist', 'king-addons'),
212 esc_html__('Wishlist', 'king-addons'),
213 'manage_options',
214 'king-addons-wishlist',
215 [$this, 'renderWishlistPage']
216 );
217
218 add_submenu_page(
219 $woo_parent,
220 esc_html__('Wishlist Analytics', 'king-addons'),
221 esc_html__('Wishlist Analytics', 'king-addons'),
222 'manage_options',
223 'king-addons-wishlist-analytics',
224 [$this, 'renderWishlistAnalyticsPage']
225 );
226 }
227
228 $menu['54.8'] = array( '', 'read', 'separator-king-addons-bottom', '', 'wp-menu-separator elementor' );
229
230 }
231
232 /**
233 * Enforce submenu order for King Addons:
234 * 1) Dashboard
235 * 2) Settings
236 * 3) AI Settings
237 * 4) Account (Freemius)
238 * 5) Contact Us (Freemius)
239 * 6) Everything else alphabetically
240 */
241 public function reorderKingAddonsSubmenu(): void
242 {
243 global $submenu;
244
245 if (!is_array($submenu) || empty($submenu['king-addons']) || !is_array($submenu['king-addons'])) {
246 return;
247 }
248
249 $priorityBySlug = [
250 'king-addons' => 0,
251 'king-addons-settings' => 1,
252 'king-addons-ai-settings' => 2,
253 'king-addons-account' => 3,
254 'king-addons-contact' => 4,
255 ];
256
257 $items = $submenu['king-addons'];
258
259 usort($items, static function ($a, $b) use ($priorityBySlug): int {
260 $aSlug = isset($a[2]) ? (string) $a[2] : '';
261 $bSlug = isset($b[2]) ? (string) $b[2] : '';
262
263 $aPriority = array_key_exists($aSlug, $priorityBySlug) ? $priorityBySlug[$aSlug] : null;
264 $bPriority = array_key_exists($bSlug, $priorityBySlug) ? $priorityBySlug[$bSlug] : null;
265
266 if ($aPriority !== null || $bPriority !== null) {
267 $aPriority = $aPriority ?? 9999;
268 $bPriority = $bPriority ?? 9999;
269 if ($aPriority !== $bPriority) {
270 return $aPriority <=> $bPriority;
271 }
272 }
273
274 $aLabel = isset($a[0]) ? wp_strip_all_tags((string) $a[0]) : '';
275 $bLabel = isset($b[0]) ? wp_strip_all_tags((string) $b[0]) : '';
276
277 $cmp = strcasecmp($aLabel, $bLabel);
278 if ($cmp !== 0) {
279 return $cmp;
280 }
281
282 return strcasecmp($aSlug, $bSlug);
283 });
284
285 $submenu['king-addons'] = $items;
286 }
287
288 /**
289 * WooCommerce Builder submenu:
290 * 1) Dashboard (same slug as the top-level page)
291 * 2) Wishlist
292 * 3) Wishlist Analytics
293 * 4) Free Shipping Bar
294 * 5) Sticky Add To Cart
295 * 6) My Account Endpoints
296 */
297 public function reorderWooBuilderSubmenu(): void
298 {
299 global $submenu;
300
301 if (!is_array($submenu) || empty($submenu['king-addons-woo-builder']) || !is_array($submenu['king-addons-woo-builder'])) {
302 return;
303 }
304
305 $priorityBySlug = [
306 'king-addons-woo-builder' => 0,
307 'king-addons-wishlist' => 1,
308 'king-addons-wishlist-analytics' => 2,
309 'king-addons-free-shipping-bar' => 3,
310 'king-addons-sticky-add-to-cart' => 4,
311 'king-addons-myaccount-endpoints' => 5,
312 ];
313
314 $items = $submenu['king-addons-woo-builder'];
315
316 usort($items, static function ($a, $b) use ($priorityBySlug): int {
317 $aSlug = isset($a[2]) ? (string) $a[2] : '';
318 $bSlug = isset($b[2]) ? (string) $b[2] : '';
319
320 $aPriority = array_key_exists($aSlug, $priorityBySlug) ? $priorityBySlug[$aSlug] : 9999;
321 $bPriority = array_key_exists($bSlug, $priorityBySlug) ? $priorityBySlug[$bSlug] : 9999;
322
323 if ($aPriority !== $bPriority) {
324 return $aPriority <=> $bPriority;
325 }
326
327 $aLabel = isset($a[0]) ? wp_strip_all_tags((string) $a[0]) : '';
328 $bLabel = isset($b[0]) ? wp_strip_all_tags((string) $b[0]) : '';
329
330 $cmp = strcasecmp($aLabel, $bLabel);
331 if ($cmp !== 0) {
332 return $cmp;
333 }
334
335 return strcasecmp($aSlug, $bSlug);
336 });
337
338 $submenu['king-addons-woo-builder'] = $items;
339 }
340
341 function addUpgradeMenu(): void
342 {
343 // Don't add menu if Freemius is showing opt-in/activation
344 $fs = king_addons_freemius();
345
346 // Check if we're on any Freemius-related page
347 if (
348 isset($_GET['fs_action']) ||
349 $fs->is_activation_mode() ||
350 (!$fs->is_registered() && !$fs->is_anonymous() && !$fs->is_tracking_prohibited())
351 ) {
352 return;
353 }
354
355 // Add Upgrade submenu under King Addons (only if premium is not active)
356 if (!$fs->can_use_premium_code()) {
357 add_submenu_page(
358 'king-addons',
359 esc_html__('Upgrade Now', 'king-addons'),
360 esc_html__('Upgrade Now', 'king-addons'),
361 'manage_options',
362 'https://kingaddons.com/pricing/?utm_source=kng-top-menu&utm_medium=plugin&utm_campaign=kng',
363 ''
364 );
365 }
366 }
367
368 function showPopupBuilder(): void
369 {
370 add_menu_page(
371 'Popup Builder',
372 'Popup Builder',
373 'manage_options',
374 'king-addons-popup-builder',
375 [Popup_Builder::instance(), 'renderPopupBuilder'],
376 KING_ADDONS_URL . 'includes/admin/img/icon-for-popup-builder.svg',
377 54.4
378 );
379 }
380
381 /**
382 * Register WooCommerce Builder menu item.
383 *
384 * @return void
385 */
386 public function showWooBuilder(): void
387 {
388 add_menu_page(
389 esc_html__('WooCommerce Builder', 'king-addons'),
390 esc_html__('WooCommerce Builder', 'king-addons'),
391 'manage_options',
392 'king-addons-woo-builder',
393 [$this, 'renderWooBuilderPage'],
394 'dashicons-cart',
395 54.5
396 );
397
398 // Same slug as the parent so the first submenu item is "Dashboard"
399 // instead of repeating "WooCommerce Builder".
400 add_submenu_page(
401 'king-addons-woo-builder',
402 esc_html__('WooCommerce Builder', 'king-addons'),
403 esc_html__('Dashboard', 'king-addons'),
404 'manage_options',
405 'king-addons-woo-builder',
406 [$this, 'renderWooBuilderPage']
407 );
408 }
409
410 /**
411 * Render WooCommerce Builder admin page.
412 *
413 * @return void
414 */
415 public function renderWooBuilderPage(): void
416 {
417 if (!current_user_can('manage_options')) {
418 return;
419 }
420
421 require_once KING_ADDONS_PATH . 'includes/admin/layouts/woo-builder-page.php';
422 }
423
424 function showHeaderFooterBuilder(): void
425 {
426 $menu_slug = 'king-addons-el-hf';
427 $callback = [\King_Addons\Header_Footer_Builder::instance(), 'renderAdminPage'];
428
429 // Add Main Menu (new unified UI)
430 add_menu_page(
431 esc_html__('Elementor Header & Footer Builder', 'king-addons'),
432 esc_html__('Header & Footer', 'king-addons'),
433 'manage_options',
434 $menu_slug,
435 $callback,
436 KING_ADDONS_URL . 'includes/admin/img/icon-for-header-footer-builder.svg',
437 54.3
438 );
439
440 // Ensure the first submenu item is labeled "Templates"
441 add_submenu_page(
442 $menu_slug,
443 esc_html__('Templates', 'king-addons'),
444 esc_html__('Templates', 'king-addons'),
445 'manage_options',
446 $menu_slug,
447 $callback
448 );
449
450 // Display Settings submenu (same page, different tab)
451 add_submenu_page(
452 $menu_slug,
453 esc_html__('Display Settings', 'king-addons'),
454 esc_html__('Display Settings', 'king-addons'),
455 'manage_options',
456 $menu_slug . '&tab=settings',
457 $callback
458 );
459 }
460
461 function showSettingsPage(): void
462 {
463 if (!current_user_can('manage_options')) {
464 return;
465 }
466
467 require_once(KING_ADDONS_PATH . 'includes/admin/layouts/settings-page.php');
468
469 self::enqueueSettingsAssets();
470 }
471
472 function showDashboardV3(): void
473 {
474 if (!current_user_can('manage_options')) {
475 return;
476 }
477
478 require_once(KING_ADDONS_PATH . 'includes/admin/layouts/dashboard-v3/dashboard-v3.php');
479 }
480
481 /**
482 * Handle AJAX request to install/activate Trinity Backup plugin.
483 *
484 * @return void
485 */
486 public function handleInstallTrinityBackup(): void
487 {
488 check_ajax_referer('king_addons_install_plugin', 'nonce');
489
490 if (!current_user_can('install_plugins')) {
491 wp_send_json_error(esc_html__('Permission denied.', 'king-addons'));
492 }
493
494 $plugin_action = isset($_POST['plugin_action']) ? sanitize_text_field($_POST['plugin_action']) : '';
495 $plugin_slug = 'trinity-backup';
496 $plugin_file = 'trinity-backup/trinity-backup.php';
497 $plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
498
499 // If action is activate and plugin exists, just activate it
500 if ($plugin_action === 'activate') {
501 if (file_exists($plugin_path)) {
502 $result = activate_plugin($plugin_file);
503 if (is_wp_error($result)) {
504 wp_send_json_error($result->get_error_message());
505 }
506 wp_send_json_success(['message' => esc_html__('Plugin activated!', 'king-addons')]);
507 } else {
508 wp_send_json_error(esc_html__('Plugin not found.', 'king-addons'));
509 }
510 return;
511 }
512
513 // Install plugin
514 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
515 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
516 require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
517
518 // Get plugin info from WordPress.org
519 $api = plugins_api('plugin_information', [
520 'slug' => $plugin_slug,
521 'fields' => [
522 'short_description' => false,
523 'sections' => false,
524 'requires' => false,
525 'rating' => false,
526 'ratings' => false,
527 'downloaded' => false,
528 'last_updated' => false,
529 'added' => false,
530 'tags' => false,
531 'compatibility' => false,
532 'homepage' => false,
533 'donate_link' => false,
534 ],
535 ]);
536
537 if (is_wp_error($api)) {
538 wp_send_json_error($api->get_error_message());
539 }
540
541 $skin = new \WP_Ajax_Upgrader_Skin();
542 $upgrader = new \Plugin_Upgrader($skin);
543 $result = $upgrader->install($api->download_link);
544
545 if (is_wp_error($result)) {
546 wp_send_json_error($result->get_error_message());
547 }
548
549 if ($result === false) {
550 wp_send_json_error(esc_html__('Installation failed.', 'king-addons'));
551 }
552
553 // Activate the plugin after installation
554 $activate_result = activate_plugin($plugin_file);
555 if (is_wp_error($activate_result)) {
556 // Installed but not activated
557 wp_send_json_success(['message' => esc_html__('Plugin installed! Please activate manually.', 'king-addons')]);
558 }
559
560 wp_send_json_success(['message' => esc_html__('Plugin installed and activated!', 'king-addons')]);
561 }
562
563 /**
564 * Render Wishlist admin page.
565 *
566 * @return void
567 */
568 public function renderWishlistPage(): void
569 {
570 if (!current_user_can('manage_options')) {
571 return;
572 }
573
574 $options = get_option('king_addons_options', []);
575 $wishlist_enabled = (!isset($options['ext_wishlist']) || $options['ext_wishlist'] === 'enabled')
576 && (defined('KING_ADDONS_EXT_WISHLIST') ? KING_ADDONS_EXT_WISHLIST : true);
577 if (!$wishlist_enabled || !class_exists(Wishlist_Settings::class)) {
578 return;
579 }
580
581 self::enqueueSettingsAssets();
582 require_once KING_ADDONS_PATH . 'includes/admin/layouts/wishlist-page.php';
583 }
584
585 /**
586 * Render Wishlist analytics page.
587 *
588 * @return void
589 */
590 public function renderWishlistAnalyticsPage(): void
591 {
592 if (!current_user_can('manage_options')) {
593 return;
594 }
595
596 $options = get_option('king_addons_options', []);
597 $wishlist_enabled = (!isset($options['ext_wishlist']) || $options['ext_wishlist'] === 'enabled')
598 && (defined('KING_ADDONS_EXT_WISHLIST') ? KING_ADDONS_EXT_WISHLIST : true);
599 if (!$wishlist_enabled || !class_exists(Wishlist_Settings::class)) {
600 return;
601 }
602
603 self::enqueueSettingsAssets();
604 require_once KING_ADDONS_PATH . 'includes/admin/layouts/wishlist-analytics.php';
605 }
606
607 function createSettings(): void
608 {
609 // Register a new setting for "king-addons" page.
610 register_setting('king_addons', 'king_addons_options');
611
612 // Register a new section in the "king-addons" page.
613 add_settings_section(
614 'king_addons_section_widgets',
615 '',
616 [$this, 'king_addons_section_widgets_callback'],
617 'king-addons'
618 );
619
620 // Register a new section in the "king-addons" page.
621 add_settings_section(
622 'king_addons_section_features',
623 '',
624 [$this, 'king_addons_section_features_callback'],
625 'king-addons'
626 );
627
628 foreach (ModulesMap::getModulesMapArray()['widgets'] as $widget_id => $widget_array) {
629 // Hide widgets hard-disabled via constants (QA rollout).
630 $widget_constant = 'KING_ADDONS_WGT_' . strtoupper(str_replace('-', '_', (string) $widget_id));
631 if (defined($widget_constant) && constant($widget_constant) === false) {
632 continue;
633 }
634
635 add_settings_field(
636 $widget_id,
637 $widget_array['title'],
638 '',
639 'king-addons',
640 'king_addons_section_widgets',
641 array(
642 'label_for' => $widget_id,
643 'description' => $widget_array['description'],
644 'docs_link' => $widget_array['docs-link'],
645 'demo_link' => $widget_array['demo-link'],
646 'class' => 'kng-tr kng-tr-' . $widget_id . (!empty($widget_array['has-pro']) ? ' kng-tr-freemium' : '')
647 )
648 );
649 }
650
651 foreach (ModulesMap::getModulesMapArray()['features'] as $feature_id => $feature_array) {
652 // Hide features hard-disabled via constants (QA rollout).
653 $feature_constant = 'KING_ADDONS_FEAT_' . strtoupper(str_replace('-', '_', (string) $feature_id));
654 if (defined($feature_constant) && constant($feature_constant) === false) {
655 continue;
656 }
657
658 add_settings_field(
659 $feature_id,
660 $feature_array['title'],
661 '',
662 'king-addons',
663 'king_addons_section_features',
664 array(
665 'label_for' => $feature_id,
666 'description' => $feature_array['description'],
667 'docs_link' => $feature_array['docs-link'],
668 'demo_link' => $feature_array['demo-link'],
669 'class' => 'kng-tr kng-tr-' . $feature_id
670 )
671 );
672 }
673 }
674
675 /**
676 * Register wishlist settings group.
677 *
678 * @return void
679 */
680 public function createWishlistSettings(): void
681 {
682 if (defined('KING_ADDONS_EXT_WISHLIST') && KING_ADDONS_EXT_WISHLIST === false) {
683 return;
684 }
685
686 if (!class_exists(Wishlist_Settings::class)) {
687 return;
688 }
689
690 register_setting(
691 'king_addons_wishlist',
692 'king_addons_wishlist_settings',
693 [
694 'type' => 'array',
695 'sanitize_callback' => [$this, 'sanitizeWishlistSettings'],
696 'default' => Wishlist_Settings::defaults(),
697 ]
698 );
699 }
700
701 /**
702 * Sanitize wishlist settings payload.
703 *
704 * @param array<string, mixed> $settings Raw settings.
705 * @return array<string, mixed> Sanitized settings.
706 */
707 public function sanitizeWishlistSettings(array $settings): array
708 {
709 if (!class_exists(Wishlist_Settings::class)) {
710 return [];
711 }
712
713 $defaults = Wishlist_Settings::defaults();
714
715 $sanitized = [
716 'enabled' => !empty($settings['enabled']),
717 'wishlist_page_id' => absint($settings['wishlist_page_id'] ?? 0),
718 'allow_guests' => !empty($settings['allow_guests']),
719 'guest_block_text' => sanitize_text_field($settings['guest_block_text'] ?? $defaults['guest_block_text']),
720 'button_add_text' => sanitize_text_field($settings['button_add_text'] ?? $defaults['button_add_text']),
721 'button_added_text' => sanitize_text_field($settings['button_added_text'] ?? $defaults['button_added_text']),
722 'button_display_mode' => in_array($settings['button_display_mode'] ?? 'icon_text', ['icon', 'icon_text'], true) ? $settings['button_display_mode'] : $defaults['button_display_mode'],
723 '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'],
724 'show_in_archives' => !empty($settings['show_in_archives']),
725 'wishlist_columns' => array_values(
726 array_intersect(
727 (array) ($settings['wishlist_columns'] ?? []),
728 ['image', 'title', 'price', 'stock', 'notes', 'add_to_cart', 'remove']
729 )
730 ),
731 'cache_enabled' => !empty($settings['cache_enabled']),
732 'cache_ttl' => max(0, absint($settings['cache_ttl'] ?? 0)),
733 'icon_choice' => sanitize_text_field($settings['icon_choice'] ?? $defaults['icon_choice']),
734 ];
735
736 return wp_parse_args($sanitized, $defaults);
737 }
738
739 function king_addons_section_widgets_callback($args): void
740 {
741 ?>
742 <h2 id="<?php echo esc_attr($args['id']); ?>" class="kng-section-title"><?php esc_html_e('Elements', 'king-addons'); ?>
743 </h2>
744 <?php
745 }
746
747 function king_addons_section_features_callback($args): void
748 {
749 ?>
750 <div class="kng-section-separator"></div>
751 <h2 id="<?php echo esc_attr($args['id']); ?>" class="kng-section-title"><?php esc_html_e('Features', 'king-addons'); ?>
752 </h2>
753 <?php
754 }
755
756 function enqueueAdminAssets(): void
757 {
758 wp_enqueue_style('king-addons-admin', KING_ADDONS_URL . 'includes/admin/css/admin.css', '', KING_ADDONS_VERSION);
759 // Styles for AI Image Generation controls in Elementor
760 wp_enqueue_style('king-addons-ai-imagefield', KING_ADDONS_URL . 'includes/admin/css/ai-imagefield.css', array('king-addons-admin'), KING_ADDONS_VERSION);
761 }
762
763 function enqueueUpgradeLinkScript(): void
764 {
765 // Only add the script if premium is not active
766 if (!king_addons_freemius()->can_use_premium_code()) {
767 wp_enqueue_script('jquery');
768 wp_add_inline_script('jquery', "
769 jQuery(document).ready(function($) {
770 $('#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');
771 });
772 ");
773 }
774 }
775
776 public function enqueueGlobalAdminStyles(): void
777 {
778 wp_enqueue_style('king-addons-hide-spam', KING_ADDONS_URL . 'includes/admin/css/hide-spam-notifications.css', [], KING_ADDONS_VERSION);
779 }
780
781 function enqueueSettingsAssets(): void
782 {
783 wp_enqueue_style('king-addons-settings', KING_ADDONS_URL . 'includes/admin/css/settings.css', '', KING_ADDONS_VERSION);
784 wp_enqueue_style('wp-color-picker');
785 wp_enqueue_script('jquery');
786 wp_enqueue_script('wp-color-picker');
787 wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-wpcolorpicker-wpcolorpicker');
788 wp_enqueue_script('king-addons-settings', KING_ADDONS_URL . 'includes/admin/js/settings.js', '', KING_ADDONS_VERSION);
789 }
790
791 /**
792 * Registers AI Settings using the WordPress Settings API.
793 *
794 * @return void
795 */
796 public function createAiSettings(): void
797 {
798 register_setting(
799 'king_addons_ai',
800 'king_addons_ai_options',
801 [$this, 'sanitizeAiSettings']
802 );
803
804 add_settings_section(
805 'king_addons_ai_openai_section',
806 esc_html__('AI Provider Settings', 'king-addons'),
807 [$this, 'renderAiOpenaiSection'],
808 'king-addons-ai-settings'
809 );
810
811 add_settings_field(
812 'ai_provider',
813 esc_html__('AI Provider', 'king-addons'),
814 [$this, 'renderAiProviderField'],
815 'king-addons-ai-settings',
816 'king_addons_ai_openai_section'
817 );
818
819 // Provider specific rows. The row classes let the settings page show only
820 // the fields belonging to the provider that is currently selected; the
821 // inactive ones start hidden so they never flash before the script runs.
822 $active_provider = AI_Provider::getProvider();
823 $row_class = static function (string $provider) use ($active_provider): array {
824 $classes = 'ka-ai-provider-row ka-ai-provider-' . $provider;
825 if ($provider !== $active_provider) {
826 $classes .= ' ka-ai-row-hidden';
827 }
828 return ['class' => $classes];
829 };
830
831 add_settings_field(
832 'openai_api_key',
833 esc_html__('OpenAI API Key', 'king-addons'),
834 [$this, 'renderAiApiKeyField'],
835 'king-addons-ai-settings',
836 'king_addons_ai_openai_section',
837 $row_class(AI_Provider::OPENAI)
838 );
839
840 add_settings_field(
841 'openrouter_api_key',
842 esc_html__('OpenRouter API Key', 'king-addons'),
843 [$this, 'renderAiOpenRouterApiKeyField'],
844 'king-addons-ai-settings',
845 'king_addons_ai_openai_section',
846 $row_class(AI_Provider::OPENROUTER)
847 );
848
849 add_settings_field(
850 'ai_connection_test',
851 esc_html__('Connection', 'king-addons'),
852 [$this, 'renderAiConnectionTestField'],
853 'king-addons-ai-settings',
854 'king_addons_ai_openai_section'
855 );
856
857 add_settings_field(
858 'openai_model',
859 esc_html__('OpenAI Model (for text generation)', 'king-addons'),
860 [$this, 'renderAiModelField'],
861 'king-addons-ai-settings',
862 'king_addons_ai_openai_section',
863 $row_class(AI_Provider::OPENAI)
864 );
865
866 add_settings_field(
867 'openai_vision_model',
868 esc_html__('OpenAI Model (for image recognition)', 'king-addons'),
869 [$this, 'renderAiVisionModelField'],
870 'king-addons-ai-settings',
871 'king_addons_ai_openai_section',
872 $row_class(AI_Provider::OPENAI)
873 );
874
875 // Add image model selector field
876 add_settings_field(
877 'openai_image_model',
878 esc_html__('OpenAI Image Model (for image generation)', 'king-addons'),
879 [$this, 'renderAiImageModelField'],
880 'king-addons-ai-settings',
881 'king_addons_ai_openai_section',
882 $row_class(AI_Provider::OPENAI)
883 );
884
885 add_settings_field(
886 'openrouter_model',
887 esc_html__('OpenRouter Model (for text generation)', 'king-addons'),
888 [$this, 'renderAiOpenRouterModelField'],
889 'king-addons-ai-settings',
890 'king_addons_ai_openai_section',
891 $row_class(AI_Provider::OPENROUTER)
892 );
893
894 add_settings_field(
895 'openrouter_vision_model',
896 esc_html__('OpenRouter Model (for image recognition)', 'king-addons'),
897 [$this, 'renderAiOpenRouterVisionModelField'],
898 'king-addons-ai-settings',
899 'king_addons_ai_openai_section',
900 $row_class(AI_Provider::OPENROUTER)
901 );
902
903 add_settings_field(
904 'openrouter_image_model',
905 esc_html__('OpenRouter Image Model (for image generation)', 'king-addons'),
906 [$this, 'renderAiOpenRouterImageModelField'],
907 'king-addons-ai-settings',
908 'king_addons_ai_openai_section',
909 $row_class(AI_Provider::OPENROUTER)
910 );
911
912 // Global content language
913 add_settings_field(
914 'content_language_custom_enable',
915 esc_html__('Content Language', 'king-addons'),
916 [$this, 'renderAiContentLanguageField'],
917 'king-addons-ai-settings',
918 'king_addons_ai_openai_section'
919 );
920
921 // Add Editor Integration section and field
922 add_settings_section(
923 'king_addons_ai_editor_section',
924 esc_html__('Editor Integration', 'king-addons'),
925 [$this, 'renderAiEditorSection'],
926 'king-addons-ai-settings'
927 );
928 add_settings_field(
929 'enable_ai_buttons',
930 esc_html__('AI Text Editing Buttons', 'king-addons'),
931 [$this, 'renderAiEnableButtonsField'],
932 'king-addons-ai-settings',
933 'king_addons_ai_editor_section'
934 );
935 add_settings_field(
936 'enable_ai_image_generation_button',
937 esc_html__('AI Image Generation Button', 'king-addons'),
938 [$this, 'renderAiImageGenerationField'],
939 'king-addons-ai-settings',
940 'king_addons_ai_editor_section'
941 );
942
943 // Add Alt Text Settings section
944 add_settings_section(
945 'king_addons_ai_alt_text_section',
946 esc_html__('Alt Text Settings', 'king-addons'),
947 [$this, 'renderAiAltTextSection'],
948 'king-addons-ai-settings'
949 );
950 add_settings_field(
951 'enable_ai_alt_text_button',
952 esc_html__('AI Alt Text Button', 'king-addons'),
953 [$this, 'renderAiAltTextButtonField'],
954 'king-addons-ai-settings',
955 'king_addons_ai_alt_text_section'
956 );
957
958 add_settings_field(
959 'enable_ai_alt_text_auto_generation',
960 ((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')),
961 [$this, 'renderAiAltTextAutoGenerationField'],
962 'king-addons-ai-settings',
963 'king_addons_ai_alt_text_section'
964 );
965 add_settings_field(
966 'ai_alt_text_generation_interval',
967 esc_html__('Alt Text Generation Interval', 'king-addons'),
968 [$this, 'renderAiAltTextIntervalField'],
969 'king-addons-ai-settings',
970 'king_addons_ai_alt_text_section'
971 );
972 // Add Image Detail Level field
973 add_settings_field(
974 'ai_alt_text_image_detail_level',
975 esc_html__('Image Detail Level', 'king-addons'),
976 [$this, 'renderAiAltTextImageDetailLevelField'],
977 'king-addons-ai-settings',
978 'king_addons_ai_alt_text_section'
979 );
980 // Add Translation Settings section
981 add_settings_section(
982 'king_addons_ai_translation_section',
983 esc_html__('Translation Settings', 'king-addons'),
984 [$this, 'renderAiTranslationSection'],
985 'king-addons-ai-settings'
986 );
987
988 add_settings_field(
989 'enable_ai_page_translator',
990 esc_html__('AI Page Translator Button', 'king-addons'),
991 [$this, 'renderAiPageTranslatorField'],
992 'king-addons-ai-settings',
993 'king_addons_ai_translation_section'
994 );
995
996 // Add Usage Quota Settings section and field
997 add_settings_section(
998 'king_addons_ai_quota_section',
999 esc_html__('Usage Quota Settings', 'king-addons'),
1000 [$this, 'renderAiQuotaSection'],
1001 'king-addons-ai-settings'
1002 );
1003
1004 add_settings_field(
1005 'daily_token_limit',
1006 esc_html__('Daily Token Limit', 'king-addons'),
1007 [$this, 'renderAiDailyLimitField'],
1008 'king-addons-ai-settings',
1009 'king_addons_ai_quota_section'
1010 );
1011
1012 // Add Usage Statistics section (read-only)
1013 add_settings_section(
1014 'king_addons_ai_stats_section',
1015 esc_html__('Usage Statistics', 'king-addons'),
1016 [$this, 'renderAiStatsSection'],
1017 'king-addons-ai-settings'
1018 );
1019
1020 // Clear models cache when options updated.
1021 add_action('update_option_king_addons_ai_options', [$this, 'clearAiModelsCache']);
1022
1023 // AJAX handler for refreshing models.
1024 add_action('wp_ajax_king_addons_ai_refresh_models', [$this, 'handleAiRefreshModels']);
1025
1026 // AJAX handler for testing the provider connection.
1027 add_action('wp_ajax_king_addons_ai_test_connection', [$this, 'handleAiTestConnection']);
1028
1029 // AJAX handler for generating text via AI
1030 add_action('wp_ajax_king_addons_ai_generate_text', [$this, 'handleAiGenerateText']);
1031
1032 // AJAX handler to change text using AI based on user prompt and original text.
1033 add_action('wp_ajax_king_addons_ai_change_text', [$this, 'handleAiChangeText']);
1034
1035 // AJAX handler to check token usage limits
1036 add_action('wp_ajax_king_addons_ai_check_tokens', [$this, 'handleAiCheckTokens']);
1037
1038 // AJAX handler to check image generation limits
1039 add_action('wp_ajax_king_addons_ai_image_check_limits', [$this, 'handleAiImageCheckLimits']);
1040
1041 // THIRD_EDIT: Register AJAX handler for AI image generation
1042 add_action('wp_ajax_king_addons_ai_generate_image', [$this, 'handleAiGenerateImage']);
1043
1044 // AJAX handler for AI page translation
1045 add_action('wp_ajax_king_addons_ai_translate_text', [$this, 'handleAiTranslateText']);
1046 }
1047
1048 /**
1049 * Sanitizes AI Settings options.
1050 *
1051 * @param array $input Raw input array.
1052 * @return array Sanitized input.
1053 */
1054 public function sanitizeAiSettings(array $input): array
1055 {
1056 $sanitized = [];
1057
1058 // An absent value resolves to the default rather than being pinned to OpenAI.
1059 $sanitized['ai_provider'] = AI_Provider::normalizeProvider($input['ai_provider'] ?? '');
1060
1061 $sanitized['openai_api_key'] = isset($input['openai_api_key'])
1062 ? sanitize_text_field($input['openai_api_key'])
1063 : '';
1064 $sanitized['openai_model'] = isset($input['openai_model'])
1065 ? sanitize_text_field($input['openai_model'])
1066 : '';
1067 $sanitized['openai_vision_model'] = isset($input['openai_vision_model'])
1068 ? sanitize_text_field($input['openai_vision_model'])
1069 : ($sanitized['openai_model'] ?: 'gpt-4o-mini');
1070 $sanitized['openai_image_model'] = isset($input['openai_image_model'])
1071 ? sanitize_text_field($input['openai_image_model'])
1072 : 'gpt-image-1';
1073
1074 // OpenRouter is configured independently, so switching providers back
1075 // and forth keeps both sets of credentials and models intact.
1076 $sanitized['openrouter_api_key'] = isset($input['openrouter_api_key'])
1077 ? sanitize_text_field($input['openrouter_api_key'])
1078 : '';
1079 $sanitized['openrouter_model'] = isset($input['openrouter_model'])
1080 ? sanitize_text_field($input['openrouter_model'])
1081 : '';
1082 $sanitized['openrouter_vision_model'] = isset($input['openrouter_vision_model'])
1083 ? sanitize_text_field($input['openrouter_vision_model'])
1084 : ($sanitized['openrouter_model'] ?: '');
1085 $sanitized['openrouter_image_model'] = isset($input['openrouter_image_model'])
1086 ? sanitize_text_field($input['openrouter_image_model'])
1087 : '';
1088
1089 // Content language is rendered by this section, so it has to survive the save.
1090 $sanitized['content_language_custom_enable'] = !empty($input['content_language_custom_enable']);
1091 $sanitized['content_language_custom'] = isset($input['content_language_custom'])
1092 ? sanitize_text_field($input['content_language_custom'])
1093 : '';
1094
1095 // Sanitize Daily Token Limit.
1096 if (isset($input['daily_token_limit'])) {
1097 $daily_limit = absint($input['daily_token_limit']);
1098 $sanitized['daily_token_limit'] = max(0, $daily_limit); // Ensure non-negative
1099 } else {
1100 $sanitized['daily_token_limit'] = 1000000; // Default to 1 million tokens if not set
1101 }
1102
1103 // Sanitize Enable AI Buttons option.
1104 $sanitized['enable_ai_buttons'] = !empty($input['enable_ai_buttons']);
1105
1106 // Sanitize Enable AI Image Generation button option.
1107 $sanitized['enable_ai_image_generation_button'] = !empty($input['enable_ai_image_generation_button']);
1108
1109 // Sanitize Enable AI Alt Text Button option.
1110 $sanitized['enable_ai_alt_text_button'] = !empty($input['enable_ai_alt_text_button']);
1111
1112 // Sanitize Enable AI Alt Text Auto Generation option.
1113 $sanitized['enable_ai_alt_text_auto_generation'] = !empty($input['enable_ai_alt_text_auto_generation']);
1114
1115 // Sanitize AI Alt Text Generation Interval.
1116 if (isset($input['ai_alt_text_generation_interval'])) {
1117 $interval = absint($input['ai_alt_text_generation_interval']);
1118 $sanitized['ai_alt_text_generation_interval'] = max(10, min(3600, $interval)); // Between 10 seconds and 1 hour
1119 } else {
1120 $sanitized['ai_alt_text_generation_interval'] = 20; // Default to 20 seconds
1121 }
1122 // Sanitize Image Detail Level
1123 $allowed_detail_levels = ['low', 'high'];
1124 $sanitized['ai_alt_text_image_detail_level'] = in_array(($input['ai_alt_text_image_detail_level'] ?? 'low'), $allowed_detail_levels, true)
1125 ? ($input['ai_alt_text_image_detail_level'] ?? 'low')
1126 : 'low';
1127
1128 // Sanitize Auto Tagging settings.
1129 $sanitized['auto_tagging_max_tags'] = isset($input['auto_tagging_max_tags'])
1130 ? max(1, min(20, absint($input['auto_tagging_max_tags'])))
1131 : 5;
1132 $sanitized['auto_tagging_confidence_threshold'] = isset($input['auto_tagging_confidence_threshold'])
1133 ? max(0.0, min(1.0, (float) $input['auto_tagging_confidence_threshold']))
1134 : 0.75;
1135 $sanitized['auto_tagging_stop_words'] = isset($input['auto_tagging_stop_words'])
1136 ? sanitize_text_field($input['auto_tagging_stop_words'])
1137 : '';
1138
1139 // Sanitize Enable AI Page Translator option
1140 $sanitized['enable_ai_page_translator'] = !empty($input['enable_ai_page_translator']);
1141
1142 // Validation / feedback (Settings API notice).
1143 $ai_requires_key = (
1144 !empty($sanitized['enable_ai_buttons'])
1145 || !empty($sanitized['enable_ai_image_generation_button'])
1146 || !empty($sanitized['enable_ai_alt_text_button'])
1147 || !empty($sanitized['enable_ai_page_translator'])
1148 );
1149
1150 $active_key = ($sanitized['ai_provider'] === AI_Provider::OPENROUTER)
1151 ? $sanitized['openrouter_api_key']
1152 : $sanitized['openai_api_key'];
1153
1154 if ($ai_requires_key && empty($active_key)) {
1155 add_settings_error(
1156 'king_addons_ai',
1157 'king_addons_ai_missing_api_key',
1158 sprintf(
1159 /* translators: %s: provider name */
1160 esc_html__('%s API Key is required to enable AI features.', 'king-addons'),
1161 AI_Provider::getLabel($sanitized['ai_provider'])
1162 ),
1163 'error'
1164 );
1165 }
1166
1167 return $sanitized;
1168 }
1169
1170 /**
1171 * Clears cached AI models list.
1172 *
1173 * @return void
1174 */
1175 public function clearAiModelsCache(): void
1176 {
1177 AI_Provider::clearModelsCache();
1178 }
1179
1180 /**
1181 * Renders the AI Settings page content.
1182 *
1183 * @return void
1184 */
1185 public function showAiSettingsPage(): void
1186 {
1187 if (!current_user_can('manage_options')) {
1188 return;
1189 }
1190 require_once KING_ADDONS_PATH . 'includes/admin/layouts/ai-settings-page.php';
1191 $this->enqueueAiSettingsAssets();
1192 }
1193
1194 /**
1195 * Enqueues scripts and styles for the AI Settings page.
1196 *
1197 * @return void
1198 */
1199 public function enqueueAiSettingsAssets(): void
1200 {
1201 // Enqueue admin base styles first for proper theming
1202 wp_enqueue_style('king-addons-admin', KING_ADDONS_URL . 'includes/admin/css/admin.css', '', KING_ADDONS_VERSION);
1203
1204 wp_enqueue_style(
1205 'king-addons-ai-settings',
1206 KING_ADDONS_URL . 'includes/admin/css/ai-settings.css',
1207 ['king-addons-admin'], // Depend on admin base styles
1208 KING_ADDONS_VERSION
1209 );
1210
1211 wp_enqueue_script(
1212 'king-addons-ai-settings',
1213 KING_ADDONS_URL . 'includes/admin/js/ai-settings.js',
1214 ['jquery'],
1215 KING_ADDONS_VERSION,
1216 true
1217 );
1218
1219 wp_localize_script(
1220 'king-addons-ai-settings',
1221 'KingAddonsAiSettings',
1222 [
1223 'ajax_url' => admin_url('admin-ajax.php'),
1224 'nonce' => wp_create_nonce('king_addons_ai_refresh_models_nonce'),
1225 'refreshing_text' => esc_html__('Refreshing...', 'king-addons'),
1226 'refreshed_text' => esc_html__('List updated.', 'king-addons'),
1227 'error_text' => esc_html__('Error updating list.', 'king-addons'),
1228 'testing_text' => esc_html__('Testing connection...', 'king-addons'),
1229 'test_failed_text' => esc_html__('Connection failed.', 'king-addons'),
1230 'free_models_label' => esc_html__('Free models', 'king-addons'),
1231 'paid_models_label' => esc_html__('Paid models', 'king-addons'),
1232 ]
1233 );
1234 }
1235
1236 /**
1237 * Renders description for the AI Provider Settings section.
1238 *
1239 * @return void
1240 */
1241 public function renderAiOpenaiSection(): void
1242 {
1243 echo '<p>' . esc_html__('Choose an AI provider, enter its API key and select the models used by the AI features.', 'king-addons') . '</p>';
1244 }
1245
1246 /**
1247 * Renders the AI provider selector.
1248 *
1249 * @return void
1250 */
1251 public function renderAiProviderField(): void
1252 {
1253 $selected = AI_Provider::getProvider();
1254
1255 echo '<select name="king_addons_ai_options[ai_provider]" id="king-addons-ai-provider">';
1256 foreach (AI_Provider::getProviders() as $provider => $label) {
1257 printf(
1258 '<option value="%s" %s>%s</option>',
1259 esc_attr($provider),
1260 selected($selected, $provider, false),
1261 esc_html($label)
1262 );
1263 }
1264 echo '</select>';
1265
1266 echo '<p class="description">' . esc_html__('OpenAI talks to the OpenAI API directly. OpenRouter is a gateway to models from many vendors, including a number of free ones, through a single API key.', 'king-addons') . '</p>';
1267 }
1268
1269 /**
1270 * Renders the OpenRouter API Key input field.
1271 *
1272 * @return void
1273 */
1274 public function renderAiOpenRouterApiKeyField(): void
1275 {
1276 $options = get_option('king_addons_ai_options', []);
1277 $api_key = $options['openrouter_api_key'] ?? '';
1278 printf(
1279 '<input type="password" name="king_addons_ai_options[openrouter_api_key]" id="king-addons-openrouter-api-key" value="%s" class="regular-text" autocomplete="off" />',
1280 esc_attr($api_key)
1281 );
1282 echo '<p class="description">';
1283 printf(
1284 /* translators: %1$s: opening link tag, %2$s: closing link tag */
1285 esc_html__('Get your API key from %1$sOpenRouter Keys%2$s. Saving the key will attempt to fetch the available models.', 'king-addons'),
1286 '<a href="https://openrouter.ai/keys" target="_blank" rel="noopener noreferrer">',
1287 '</a>'
1288 );
1289 echo '</p>';
1290 echo '<div class="ka-ai-notice ka-ai-notice-info">';
1291 echo '<strong>' . esc_html__('Info:', 'king-addons') . '</strong> ';
1292 echo esc_html__('Free models can be used without adding credit. Paid models, including every image generation model, require credit on your OpenRouter account.', 'king-addons');
1293 echo '</div>';
1294 echo '<div class="ka-ai-notice ka-ai-notice-info">';
1295 echo '<strong class="ka-ai-notice-title">' . esc_html__('Useful OpenRouter Links:', 'king-addons') . '</strong>';
1296 echo '<ul class="ka-ai-links-list">';
1297 $links = [
1298 'Models & Pricing' => 'https://openrouter.ai/models',
1299 'API Keys' => 'https://openrouter.ai/keys',
1300 'Activity Dashboard' => 'https://openrouter.ai/activity',
1301 'Credits' => 'https://openrouter.ai/settings/credits',
1302 'Limits' => 'https://openrouter.ai/docs/api-reference/limits',
1303 ];
1304 foreach ($links as $label => $url) {
1305 printf(
1306 '<li><a href="%s" target="_blank" rel="noopener noreferrer">%s</a></li>',
1307 esc_url($url),
1308 esc_html($label)
1309 );
1310 }
1311 echo '</ul></div>';
1312 }
1313
1314 /**
1315 * Renders the "Test Connection" control.
1316 *
1317 * @return void
1318 */
1319 public function renderAiConnectionTestField(): void
1320 {
1321 echo '<button type="button" id="king-addons-ai-test-connection-button" class="button button-secondary">' . esc_html__('Test Connection', 'king-addons') . '</button>';
1322 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>';
1323 echo '<span class="spinner" id="king-addons-ai-test-connection-spinner" style="float:none; vertical-align:middle;"></span>';
1324 echo '<span class="spinner" id="king-addons-ai-refresh-models-spinner" style="float:none; vertical-align:middle;"></span>';
1325 echo '<span id="king-addons-ai-test-connection-status" style="margin-left:5px; vertical-align:middle;"></span>';
1326 echo '<span id="king-addons-ai-refresh-models-status" style="margin-left:5px; vertical-align:middle;"></span>';
1327 echo '<p class="description">' . esc_html__('Test Connection checks the API key currently typed above against the selected provider — it does not have to be saved first. Refresh List re-fetches that provider\'s model catalogue.', 'king-addons') . '</p>';
1328 }
1329
1330 /**
1331 * Renders the OpenRouter text model dropdown.
1332 *
1333 * @return void
1334 */
1335 public function renderAiOpenRouterModelField(): void
1336 {
1337 $selected = AI_Provider::getModel('text', AI_Provider::OPENROUTER);
1338 AI_Provider::renderModelSelect(
1339 'king_addons_ai_options[openrouter_model]',
1340 $selected,
1341 AI_Provider::getModelsFor('text', AI_Provider::OPENROUTER),
1342 ['class' => 'ka-ai-model-select', 'data-ka-model-type' => 'text']
1343 );
1344 echo '<p class="description">' . esc_html__('Free models are listed first, then paid ones, each block in alphabetical order. Use the Refresh List button to pull the latest catalogue.', 'king-addons') . '</p>';
1345 }
1346
1347 /**
1348 * Renders the OpenRouter vision model dropdown.
1349 *
1350 * @return void
1351 */
1352 public function renderAiOpenRouterVisionModelField(): void
1353 {
1354 $selected = AI_Provider::getModel('vision', AI_Provider::OPENROUTER);
1355 AI_Provider::renderModelSelect(
1356 'king_addons_ai_options[openrouter_vision_model]',
1357 $selected,
1358 AI_Provider::getModelsFor('vision', AI_Provider::OPENROUTER),
1359 ['class' => 'ka-ai-model-select', 'data-ka-model-type' => 'vision']
1360 );
1361 echo '<p class="description">' . esc_html__('Only models that accept image input are listed. Used for the Alt Text Generator and other vision requests.', 'king-addons') . '</p>';
1362 }
1363
1364 /**
1365 * Renders the OpenRouter image generation model dropdown.
1366 *
1367 * @return void
1368 */
1369 public function renderAiOpenRouterImageModelField(): void
1370 {
1371 $selected = AI_Provider::getModel('image', AI_Provider::OPENROUTER);
1372 AI_Provider::renderModelSelect(
1373 'king_addons_ai_options[openrouter_image_model]',
1374 $selected,
1375 AI_Provider::getModelsFor('image', AI_Provider::OPENROUTER),
1376 ['class' => 'ka-ai-model-select', 'data-ka-model-type' => 'image']
1377 );
1378 echo '<p class="description">' . esc_html__('Only models that return images are listed. Image generation is never free — your OpenRouter account needs credit.', 'king-addons') . '</p>';
1379 }
1380
1381 /**
1382 * Renders the OpenAI API Key input field.
1383 *
1384 * @return void
1385 */
1386 public function renderAiApiKeyField(): void
1387 {
1388 $options = get_option('king_addons_ai_options', []);
1389 $api_key = $options['openai_api_key'] ?? '';
1390 printf(
1391 '<input type="password" name="king_addons_ai_options[openai_api_key]" value="%s" class="regular-text" autocomplete="off" />',
1392 esc_attr($api_key)
1393 );
1394 echo '<p class="description">';
1395 printf(
1396 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'),
1397 '<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">',
1398 '</a>'
1399 );
1400 echo '</p>';
1401 echo '<div class="ka-ai-notice ka-ai-notice-warning">';
1402 echo '<strong>' . esc_html__('Important:', 'king-addons') . '</strong> ';
1403 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');
1404 echo '</div>';
1405 echo '<div class="ka-ai-notice ka-ai-notice-info">';
1406 echo '<strong>' . esc_html__('Info:', 'king-addons') . '</strong> ';
1407 echo esc_html__('With GPT-4o-mini, a $5 balance is enough for roughly 130,000–150,000 text generations.', 'king-addons');
1408 echo '</div>';
1409 echo '<div class="ka-ai-notice ka-ai-notice-info">';
1410 echo '<strong class="ka-ai-notice-title">' . esc_html__('Useful OpenAI Links:', 'king-addons') . '</strong>';
1411 echo '<ul class="ka-ai-links-list">';
1412 $links = [
1413 'API Pricing' => 'https://openai.com/api/pricing/',
1414 'API Keys' => 'https://platform.openai.com/api-keys',
1415 'Usage Dashboard' => 'https://platform.openai.com/account/usage',
1416 'Billing Overview' => 'https://platform.openai.com/account/billing/overview',
1417 'Rate Limits' => 'https://openai.com/pricing#rate-limits',
1418 ];
1419 foreach ($links as $label => $url) {
1420 printf(
1421 '<li><a href="%s" target="_blank" rel="noopener noreferrer">%s</a></li>',
1422 esc_url($url),
1423 esc_html($label)
1424 );
1425 }
1426 echo '</ul></div>';
1427 }
1428
1429 /**
1430 * Renders the model selection dropdown field with refresh button.
1431 *
1432 * @return void
1433 */
1434 public function renderAiModelField(): void
1435 {
1436 $selected = AI_Provider::getModel('text', AI_Provider::OPENAI);
1437 AI_Provider::renderModelSelect(
1438 'king_addons_ai_options[openai_model]',
1439 $selected,
1440 AI_Provider::getModelsFor('text', AI_Provider::OPENAI),
1441 ['class' => 'ka-ai-model-select', 'data-ka-model-type' => 'text']
1442 );
1443 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>';
1444 }
1445
1446 /**
1447 * Renders the vision model selection dropdown field.
1448 *
1449 * @return void
1450 */
1451 public function renderAiVisionModelField(): void
1452 {
1453 $options = get_option('king_addons_ai_options', []);
1454 $selected = $options['openai_vision_model'] ?? ($options['openai_model'] ?? 'gpt-4o-mini');
1455 AI_Provider::renderModelSelect(
1456 'king_addons_ai_options[openai_vision_model]',
1457 (string) $selected,
1458 AI_Provider::getModelsFor('vision', AI_Provider::OPENAI),
1459 ['class' => 'ka-ai-model-select', 'data-ka-model-type' => 'vision']
1460 );
1461 echo '<p class="description">' . esc_html__('Select the default model used for AI image analysis tasks (Alt Text Generator and related vision requests).', 'king-addons') . '</p>';
1462 }
1463
1464 /**
1465 * Builds the model lists the settings page needs, keyed by purpose.
1466 *
1467 * @param string $provider Provider slug.
1468 * @return array<string, array<int, array<string, mixed>>>
1469 */
1470 private function getAiModelPayload(string $provider): array
1471 {
1472 return [
1473 'text' => AI_Provider::getModelsFor('text', $provider),
1474 'vision' => AI_Provider::getModelsFor('vision', $provider),
1475 'image' => AI_Provider::getModelsFor('image', $provider),
1476 ];
1477 }
1478
1479 /**
1480 * Handles AJAX request to refresh the model list of the selected provider.
1481 *
1482 * @return void
1483 */
1484 public function handleAiRefreshModels(): void
1485 {
1486 check_ajax_referer('king_addons_ai_refresh_models_nonce', 'nonce');
1487 if (!current_user_can('manage_options')) {
1488 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1489 }
1490
1491 $provider = AI_Provider::normalizeProvider(
1492 isset($_POST['provider']) ? sanitize_text_field(wp_unslash($_POST['provider'])) : AI_Provider::getProvider()
1493 );
1494
1495 // Allow refreshing against a key that has been typed but not saved yet.
1496 $posted_key = isset($_POST['api_key']) ? trim(sanitize_text_field(wp_unslash($_POST['api_key']))) : '';
1497 $api_key = ($posted_key !== '') ? $posted_key : AI_Provider::getApiKey($provider);
1498
1499 // OpenRouter publishes its catalogue without authentication.
1500 if ($api_key === '' && $provider !== AI_Provider::OPENROUTER) {
1501 wp_send_json_error(['message' => esc_html__('API key is not set.', 'king-addons')], 400);
1502 }
1503
1504 $models = AI_Provider::fetchModels($provider, $api_key);
1505 if (is_wp_error($models)) {
1506 wp_send_json_error(['message' => $models->get_error_message()], 500);
1507 }
1508
1509 set_transient(
1510 AI_Provider::getCacheKey($provider),
1511 $models,
1512 ($provider === AI_Provider::OPENROUTER) ? DAY_IN_SECONDS : 0
1513 );
1514
1515 wp_send_json_success([
1516 'provider' => $provider,
1517 'models' => $this->getAiModelPayload($provider),
1518 ]);
1519 }
1520
1521 /**
1522 * Handles AJAX request to verify the provider connection.
1523 *
1524 * @return void
1525 */
1526 public function handleAiTestConnection(): void
1527 {
1528 check_ajax_referer('king_addons_ai_refresh_models_nonce', 'nonce');
1529 if (!current_user_can('manage_options')) {
1530 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1531 }
1532
1533 $provider = AI_Provider::normalizeProvider(
1534 isset($_POST['provider']) ? sanitize_text_field(wp_unslash($_POST['provider'])) : AI_Provider::getProvider()
1535 );
1536
1537 // Test what is in the field right now, so a key can be verified before
1538 // it is saved. An empty field falls back to the stored key.
1539 $posted_key = isset($_POST['api_key']) ? trim(sanitize_text_field(wp_unslash($_POST['api_key']))) : '';
1540 $stored_key = AI_Provider::getApiKey($provider);
1541 $api_key = ($posted_key !== '') ? $posted_key : $stored_key;
1542 $unsaved = ($posted_key !== '' && $posted_key !== $stored_key);
1543
1544 $result = AI_Provider::testConnection($provider, $api_key);
1545 if (is_wp_error($result)) {
1546 wp_send_json_error(['message' => $result->get_error_message()], 400);
1547 }
1548
1549 if ($unsaved) {
1550 $result .= ' ' . esc_html__('Save the settings to start using it.', 'king-addons');
1551 }
1552
1553 wp_send_json_success(['message' => $result, 'unsaved' => $unsaved]);
1554 }
1555
1556 /**
1557 * AJAX handler to generate text using the configured AI provider.
1558 *
1559 * @return void
1560 */
1561 public function handleAiGenerateText(): void
1562 {
1563 check_ajax_referer('king_addons_ai_generate_nonce', 'nonce');
1564 if (!current_user_can('manage_options')) {
1565 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1566 }
1567 $field_name = sanitize_text_field($_POST['field_name'] ?? '');
1568 // Accept 'prompt' parameter (new) but fall back to 'value' parameter (old) for backwards compatibility
1569 $prompt = isset($_POST['prompt'])
1570 ? sanitize_textarea_field($_POST['prompt'])
1571 : sanitize_textarea_field($_POST['value'] ?? '');
1572
1573 // Get editor type if provided
1574 $editor_type = sanitize_text_field($_POST['editor_type'] ?? 'text');
1575
1576 $options = get_option('king_addons_ai_options', []);
1577 $api_key = AI_Provider::getApiKey();
1578 $model = AI_Provider::getTextModel();
1579
1580 if (empty($api_key) || empty($model)) {
1581 wp_send_json_error(['message' => esc_html__('API key or model not set.', 'king-addons')], 400);
1582 }
1583
1584 if (empty($prompt)) {
1585 wp_send_json_error(['message' => esc_html__('Please provide a prompt.', 'king-addons')], 400);
1586 }
1587
1588 // Check daily token limit
1589 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1590 $current_usage = $this->getAiDailyUsage();
1591
1592 if ($daily_limit > 0 && $current_usage >= $daily_limit) {
1593 wp_send_json_error([
1594 'message' => esc_html__('Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.', 'king-addons'),
1595 // This is the plugin's own cap, not the provider's, so the
1596 // client must not retry it or blame the AI provider.
1597 'code' => 'local_limit',
1598 'retryable' => false,
1599 ], 429);
1600 }
1601
1602 // System instruction based on editor type
1603 $system_instruction = 'You are a helpful content assistant. Provide concise, well-written content based on the user\'s request.';
1604
1605 // Enhanced instruction for WYSIWYG editor
1606 if ($editor_type === 'wysiwyg') {
1607 $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.';
1608 }
1609
1610 // Prepare request to the provider's Chat Completions endpoint
1611 $messages = [
1612 ['role' => 'system', 'content' => $system_instruction],
1613 ['role' => 'user', 'content' => $prompt]
1614 ];
1615
1616 // Add format instruction for WYSIWYG
1617 if ($editor_type === 'wysiwyg') {
1618 $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.";
1619 }
1620
1621 $response = wp_remote_post(
1622 AI_Provider::getChatEndpoint(),
1623 [
1624 'headers' => AI_Provider::getHeaders(),
1625 'body' => wp_json_encode(AI_Provider::prepareChatPayload([
1626 'model' => $model,
1627 'messages' => $messages,
1628 'max_tokens' => 500,
1629 'temperature' => 0.7, // Slight creativity for better content
1630 ])),
1631 'timeout' => 30,
1632 ]
1633 );
1634
1635 if (is_wp_error($response)) {
1636 wp_send_json_error(['message' => $response->get_error_message()], 500);
1637 }
1638
1639 $code = wp_remote_retrieve_response_code($response);
1640 $data = json_decode(wp_remote_retrieve_body($response), true);
1641
1642 if ($code !== 200 || empty($data['choices'][0]['message']['content'])) {
1643 $error_msg = AI_Provider::extractErrorMessage($data, esc_html__('AI API error.', 'king-addons'));
1644 wp_send_json_error(['message' => $error_msg], 500);
1645 }
1646
1647 $generated = trim($data['choices'][0]['message']['content']);
1648
1649 // Clean up any code fence markers for WYSIWYG editor
1650 if ($editor_type === 'wysiwyg') {
1651 // Remove code fence markers (```html and ```) that might be returned by AI
1652 $generated = preg_replace('/^```(?:html|HTML)?\s*/', '', $generated);
1653 $generated = preg_replace('/```\s*$/', '', $generated);
1654 }
1655
1656 // Update token usage statistics if present in the response
1657 if (isset($data['usage']['total_tokens'])) {
1658 $this->incrementAiDailyUsage(intval($data['usage']['total_tokens']));
1659 }
1660
1661 wp_send_json_success([
1662 'text' => $generated,
1663 'usage' => [
1664 'tokens_used' => $data['usage']['total_tokens'] ?? 0,
1665 'daily_used' => $this->getAiDailyUsage(),
1666 'daily_limit' => $daily_limit,
1667 ]
1668 ]);
1669 }
1670
1671 /**
1672 * AJAX handler to change text using AI based on user prompt and original text.
1673 *
1674 * @return void
1675 */
1676 public function handleAiChangeText(): void
1677 {
1678 check_ajax_referer('king_addons_ai_change_nonce', 'nonce');
1679 if (!current_user_can('manage_options')) {
1680 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1681 }
1682 $field_name = isset($_POST['field_name']) ? sanitize_text_field(wp_unslash($_POST['field_name'])) : '';
1683 $prompt = isset($_POST['prompt']) ? sanitize_text_field(wp_unslash($_POST['prompt'])) : '';
1684 $original = isset($_POST['original']) ? wp_kses_post(wp_unslash($_POST['original'])) : '';
1685 $instruction_context = isset($_POST['instruction_context']) ? sanitize_textarea_field(wp_unslash($_POST['instruction_context'])) : '';
1686
1687 $options = get_option('king_addons_ai_options', []);
1688 $api_key = AI_Provider::getApiKey();
1689 $model = AI_Provider::getTextModel();
1690
1691 if (empty($api_key) || empty($model) || empty($prompt) || empty($original)) {
1692 wp_send_json_error(['message' => esc_html__('Missing data for AI change.', 'king-addons')], 400);
1693 }
1694
1695 // Check daily token limit
1696 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1697 $current_usage = $this->getAiDailyUsage();
1698
1699 if ($daily_limit > 0 && $current_usage >= $daily_limit) {
1700 wp_send_json_error([
1701 'message' => esc_html__('Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.', 'king-addons'),
1702 // This is the plugin's own cap, not the provider's, so the
1703 // client must not retry it or blame the AI provider.
1704 'code' => 'local_limit',
1705 'retryable' => false,
1706 ], 429);
1707 }
1708
1709 // Default instruction if none provided
1710 if (empty($instruction_context)) {
1711 $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.';
1712
1713 // Add specific formatting instructions for WYSIWYG editor
1714 $editor_type = isset($_POST['editor_type']) ? sanitize_text_field(wp_unslash($_POST['editor_type'])) : 'text';
1715 if ($editor_type === 'wysiwyg') {
1716 $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.';
1717 }
1718 }
1719
1720 // Analyze if the prompt is likely requesting to add content rather than replace
1721 $add_content_keywords = [
1722 // English
1723 'add',
1724 'insert',
1725 'extend',
1726 'append',
1727 'more',
1728 'additional',
1729 'expand',
1730 // Russian
1731 'добавь',
1732 'вставь',
1733 'расширь',
1734 // Spanish
1735 'añadir',
1736 'agregar',
1737 'insertar',
1738 'adjuntar',
1739 'extender',
1740 // French
1741 'ajouter',
1742 'insérer',
1743 'étendre',
1744 'annexer',
1745 'joindre',
1746 // German
1747 'hinzufügen',
1748 'einfügen',
1749 'erweitern',
1750 'anhängen',
1751 'ergänzen',
1752 // Italian
1753 'aggiungere',
1754 'inserire',
1755 'allegare',
1756 'estendere',
1757 'appendere',
1758 // Portuguese
1759 'adicionar',
1760 'inserir',
1761 'acrescentar',
1762 'anexar',
1763 'estender',
1764 // Polish
1765 'dodać',
1766 'wstawić',
1767 'doł�
1768 czyć',
1769 'rozszerzyć',
1770 'zał�
1771 czyć'
1772 ];
1773
1774 // Also look for numeric patterns like "add 2 paragraphs" or "добавь 3 абзаца"
1775 // Enhanced pattern to find numeric paragraph requests in different languages
1776 $numeric_pattern = '/(?:' .
1777 // English verbs
1778 'add|append|insert|create|write|' .
1779 // Russian verbs
1780 'добавь|вставь|создай|напиши|' .
1781 // Spanish verbs
1782 'añadir|agregar|insertar|crear|escribir|' .
1783 // French verbs
1784 'ajouter|insérer|créer|écrire|' .
1785 // German verbs
1786 'hinzufügen|einfügen|erstellen|schreiben|' .
1787 // Italian verbs
1788 'aggiungere|inserire|creare|scrivere|' .
1789 // Portuguese verbs
1790 'adicionar|inserir|criar|escrever|' .
1791 // Polish verbs
1792 'dodać|wstawić|utworzyć|napisać' .
1793 ')\s+(\d+|' .
1794 // English quantifiers
1795 'several|few|couple|some|' .
1796 // Russian quantifiers
1797 'несколько|пару|еще|ещё|' .
1798 // Spanish quantifiers
1799 'varios|algunos|un par|unos|' .
1800 // French quantifiers
1801 'plusieurs|quelques|une paire|certains|' .
1802 // German quantifiers
1803 'mehrere|einige|ein paar|manche|' .
1804 // Italian quantifiers
1805 'diversi|alcuni|un paio|qualche|' .
1806 // Portuguese quantifiers
1807 'vários|alguns|um par|' .
1808 // Polish quantifiers
1809 'kilka|parę|pare|niektóre' .
1810 ')\s+(?:' .
1811 // English nouns
1812 'paragraph|paragraphs|section|sections|content|text|' .
1813 // Russian nouns
1814 'абзац|абзаца|абзацев|раздел|разделы|текст|контент|параграф|параграфа|параграфов|' .
1815 // Spanish nouns
1816 'párrafo|párrafos|sección|secciones|contenido|texto|' .
1817 // French nouns
1818 'paragraphe|paragraphes|section|sections|contenu|texte|' .
1819 // German nouns
1820 'absatz|absätze|abschnitt|abschnitte|inhalt|text|' .
1821 // Italian nouns
1822 'paragrafo|paragrafi|sezione|sezioni|contenuto|testo|' .
1823 // Portuguese nouns
1824 'parágrafo|parágrafos|seção|seções|conteúdo|texto|' .
1825 // Polish nouns
1826 'akapit|akapity|sekcja|sekcje|treść|tekst' .
1827 ')/i';
1828 $contains_add_keyword = false;
1829 $numeric_match = [];
1830 $requested_paragraphs = 0;
1831
1832 // First check for specific numeric requests
1833 if (preg_match($numeric_pattern, $prompt, $numeric_match)) {
1834 $contains_add_keyword = true;
1835 $number_text = $numeric_match[1] ?? '';
1836
1837 // Convert text numbers to digits
1838 if (is_numeric($number_text)) {
1839 $requested_paragraphs = (int) $number_text;
1840 } else {
1841 // For words like "several", "few", "couple", etc.
1842 switch (strtolower($number_text)) {
1843 // Words meaning approximately "2"
1844 case 'couple':
1845 case 'пару': // Russian
1846 case 'пара': // Russian
1847 case 'un par': // Spanish
1848 case 'une paire': // French
1849 case 'ein paar': // German
1850 case 'un paio': // Italian
1851 case 'um par': // Portuguese
1852 case 'parę': // Polish
1853 case 'pare': // Polish
1854 $requested_paragraphs = 2;
1855 break;
1856
1857 // Words meaning approximately "3-4" (several/few)
1858 case 'few':
1859 case 'several':
1860 case 'some':
1861 case 'несколько': // Russian
1862 case 'еще': // Russian
1863 case 'ещё': // Russian
1864 case 'varios': // Spanish
1865 case 'algunos': // Spanish
1866 case 'unos': // Spanish
1867 case 'plusieurs': // French
1868 case 'quelques': // French
1869 case 'certains': // French
1870 case 'mehrere': // German
1871 case 'einige': // German
1872 case 'manche': // German
1873 case 'diversi': // Italian
1874 case 'alcuni': // Italian
1875 case 'qualche': // Italian
1876 case 'vários': // Portuguese
1877 case 'alguns': // Portuguese
1878 case 'kilka': // Polish
1879 case 'niektóre': // Polish
1880 default:
1881 $requested_paragraphs = 3; // Default "several" = 3
1882 break;
1883 }
1884 }
1885 } else {
1886 // Then check for general add keywords
1887 foreach ($add_content_keywords as $keyword) {
1888 if (stripos($prompt, $keyword) !== false) {
1889 $contains_add_keyword = true;
1890 $requested_paragraphs = 2; // Default to 2 paragraphs if just "add paragraphs"
1891 break;
1892 }
1893 }
1894 }
1895
1896 // Build the system message dynamically based on the prompt analysis
1897 $system_message = $instruction_context;
1898 if ($contains_add_keyword) {
1899 if ($requested_paragraphs > 0) {
1900 // Request only new paragraphs, without modifying original content
1901 $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.';
1902 $system_message .= sprintf(
1903 ' 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.',
1904 $requested_paragraphs
1905 );
1906 } else {
1907 $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.';
1908 }
1909 }
1910
1911 $body = [
1912 'model' => $model,
1913 'messages' => [
1914 [
1915 'role' => 'system',
1916 'content' => $system_message,
1917 ],
1918 [
1919 'role' => 'user',
1920 'content' => ($contains_add_keyword && $requested_paragraphs > 0)
1921 ? sprintf(
1922 "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.",
1923 $original,
1924 $requested_paragraphs
1925 )
1926 : sprintf(
1927 /* translators: %1$s: User's instruction prompt, %2$s: Original text to modify. */
1928 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'),
1929 $prompt,
1930 $original
1931 ),
1932 ],
1933 ],
1934 'max_tokens' => 10000, // Increased to allow for more content
1935 'temperature' => 0.7, // Slightly more creative
1936 ];
1937
1938 // Set append_mode flag for paragraph additions
1939 $append_mode = ($contains_add_keyword && $requested_paragraphs > 0);
1940
1941 // Modify request for WYSIWYG editor
1942 $editor_type = isset($_POST['editor_type']) ? sanitize_text_field(wp_unslash($_POST['editor_type'])) : 'text';
1943 if ($editor_type === 'wysiwyg') {
1944 // Add a specific instruction for formatting
1945 $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.';
1946
1947 if (!$append_mode) {
1948 // Only add this for non-append mode
1949 $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.";
1950 }
1951
1952 // Increase temperature for WYSIWYG to be more creative when creating paragraphs
1953 $body['temperature'] = 0.8;
1954
1955 // Increase max_tokens for longer responses with multiple paragraphs
1956 $body['max_tokens'] = 15000;
1957 }
1958
1959 $response = wp_remote_post(
1960 AI_Provider::getChatEndpoint(),
1961 [
1962 'headers' => AI_Provider::getHeaders(),
1963 'body' => wp_json_encode(AI_Provider::prepareChatPayload($body)),
1964 'timeout' => 30,
1965 ]
1966 );
1967
1968 if (is_wp_error($response)) {
1969 wp_send_json_error(['message' => $response->get_error_message()], 500);
1970 }
1971
1972 $code = wp_remote_retrieve_response_code($response);
1973 $data = json_decode(wp_remote_retrieve_body($response), true);
1974
1975 if ($code !== 200 || empty($data['choices'][0]['message']['content'])) {
1976 $error_msg = AI_Provider::extractErrorMessage($data, esc_html__('AI change error.', 'king-addons'));
1977 wp_send_json_error(['message' => $error_msg], 500);
1978 }
1979
1980 $changed = trim($data['choices'][0]['message']['content']);
1981
1982 // Clean up any code fence markers for WYSIWYG editor
1983 if ($editor_type === 'wysiwyg') {
1984 // Remove code fence markers (```html and ```) that might be returned by AI
1985 $changed = preg_replace('/^```(?:html|HTML)?\s*/', '', $changed);
1986 $changed = preg_replace('/```\s*$/', '', $changed);
1987 }
1988
1989 // Update token usage statistics if present in the response
1990 if (isset($data['usage']['total_tokens'])) {
1991 $this->incrementAiDailyUsage(intval($data['usage']['total_tokens']));
1992 }
1993
1994 // Send response with append mode flag
1995 wp_send_json_success([
1996 'text' => $changed,
1997 'append_mode' => $append_mode,
1998 'original' => $append_mode ? $original : '',
1999 'usage' => [
2000 'tokens_used' => $data['usage']['total_tokens'] ?? 0,
2001 'daily_used' => $this->getAiDailyUsage(),
2002 'daily_limit' => $daily_limit,
2003 ]
2004 ]);
2005 }
2006
2007 /**
2008 * Renders Usage Quota Settings section.
2009 *
2010 * @return void
2011 */
2012 public function renderAiQuotaSection(): void
2013 {
2014 echo '<p>' . esc_html__('Set the daily token limit for AI features.', 'king-addons') . '</p>';
2015 }
2016
2017 /**
2018 * Renders the Daily Token Limit input field.
2019 *
2020 * @return void
2021 */
2022 public function renderAiDailyLimitField(): void
2023 {
2024 $options = get_option('king_addons_ai_options', []);
2025 $daily_token_limit = $options['daily_token_limit'] ?? self::DEFAULT_DAILY_TOKEN_LIMIT;
2026
2027 echo '<div class="daily-token-limit-wrap">';
2028 printf(
2029 '<input type="number" name="king_addons_ai_options[daily_token_limit]" value="%s" class="regular-text" min="0" step="1000" style="margin-right: 10px;" />',
2030 esc_attr($daily_token_limit)
2031 );
2032 echo '<span>' . esc_html__('tokens', 'king-addons') . '</span>';
2033 echo '</div>';
2034
2035 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>';
2036
2037 echo '<div class="king-addons-info-box">';
2038 echo '<p><strong>' . esc_html__('About tokens:', 'king-addons') . '</strong> ' .
2039 esc_html__('Tokens are the basic unit of text that the AI processes. As a rough guide:', 'king-addons') . '</p>';
2040 echo '<p>• ' . esc_html__('1 token ≈ 4 characters or 0.75 words in English', 'king-addons') . '</p>';
2041 echo '<p>• ' . esc_html__('A typical paragraph might use around 50-100 tokens', 'king-addons') . '</p>';
2042 echo '<p>• ' . esc_html__('A full page of text (500 words) is approximately 750 tokens', 'king_addons') . '</p>';
2043 echo '<p>• ' . esc_html__('Recommended daily limit: 10,000 - 50,000 tokens for moderate use', 'king-addons') . '</p>';
2044 echo '</div>';
2045 }
2046
2047 /**
2048 * Default daily token limit if not explicitly set.
2049 *
2050 * @var int
2051 */
2052 private const DEFAULT_DAILY_TOKEN_LIMIT = 1000000;
2053
2054 /**
2055 * Gets the current daily token usage.
2056 *
2057 * @return int Number of tokens used today.
2058 */
2059 private function getAiDailyUsage(): int
2060 {
2061 $usage_data = get_option('king_addons_ai_daily_usage', ['date' => '', 'count' => 0]);
2062 $today = current_time('Y-m-d');
2063 if (!isset($usage_data['date']) || $usage_data['date'] !== $today) {
2064 return 0;
2065 }
2066 return intval($usage_data['count']);
2067 }
2068
2069 /**
2070 * Increments the daily token usage count.
2071 *
2072 * @param int $tokens Number of tokens to add.
2073 * @return void
2074 */
2075 public function incrementAiDailyUsage(int $tokens): void
2076 {
2077 $today = current_time('Y-m-d');
2078 $usage_data = get_option('king_addons_ai_daily_usage', ['date' => '', 'count' => 0]);
2079 if (!isset($usage_data['date']) || $usage_data['date'] !== $today) {
2080 $usage_data = [
2081 'date' => $today,
2082 'count' => 0,
2083 ];
2084 }
2085 $usage_data['count'] = intval($usage_data['count']) + $tokens;
2086 update_option('king_addons_ai_daily_usage', $usage_data, false);
2087 }
2088
2089 /**
2090 * Renders Usage Statistics section.
2091 *
2092 * @return void
2093 */
2094 public function renderAiStatsSection(): void
2095 {
2096 $usage_data = get_option('king_addons_ai_daily_usage', ['date' => '', 'count' => 0]);
2097 $today = current_time('Y-m-d');
2098 $used = (isset($usage_data['date']) && $usage_data['date'] === $today) ? intval($usage_data['count']) : 0;
2099
2100 $options = get_option('king_addons_ai_options', []);
2101 $limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
2102
2103 if ($limit > 0) {
2104 $limit_display = number_format_i18n($limit);
2105 $remaining = max(0, $limit - $used);
2106 $remaining_display = number_format_i18n($remaining);
2107
2108 $usage_percentage = ($limit > 0) ? min(100, round(($used / $limit) * 100)) : 0;
2109
2110 echo '<div class="king-addons-ai-usage-stats">';
2111 echo '<table class="form-table">';
2112 echo '<tr>';
2113 echo '<th>' . esc_html__('Tokens Used Today', 'king-addons') . '</th>';
2114 echo '<td><strong>' . esc_html(number_format_i18n($used)) . '</strong></td>';
2115 echo '</tr>';
2116 echo '<tr>';
2117 echo '<th>' . esc_html__('Daily Limit', 'king-addons') . '</th>';
2118 echo '<td>' . esc_html($limit_display) . '</td>';
2119 echo '</tr>';
2120 echo '<tr>';
2121 echo '<th>' . esc_html__('Remaining', 'king-addons') . '</th>';
2122 echo '<td>' . esc_html($remaining_display) . '</td>';
2123 echo '</tr>';
2124 echo '</table>';
2125
2126 // Add progress bar
2127 echo '<div class="king-addons-ai-usage-bar-container" style="background-color: #f0f0f0; height: 20px; border-radius: 10px; margin: 15px 0; overflow: hidden;">';
2128 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>';
2129 echo '</div>';
2130 echo '<p class="description">' . esc_html(sprintf(__('Usage: %d%%', 'king-addons'), $usage_percentage)) . '</p>';
2131 echo '</div>';
2132 } else {
2133 echo '<p>' . esc_html__('No daily token limit is set. All requests will be processed.', 'king-addons') . '</p>';
2134 echo '<p><strong>' . esc_html__('Tokens used today:', 'king-addons') . ' ' . esc_html(number_format_i18n($used)) . '</strong></p>';
2135 }
2136 }
2137
2138 /**
2139 * AJAX handler to check token usage limits.
2140 *
2141 * @return void
2142 */
2143 public function handleAiCheckTokens(): void
2144 {
2145 check_ajax_referer('king_addons_ai_generate_nonce', 'nonce');
2146 if (!current_user_can('manage_options')) {
2147 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
2148 }
2149
2150 $options = get_option('king_addons_ai_options', []);
2151 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
2152 $daily_used = $this->getAiDailyUsage();
2153 // Check if API key and model are set
2154 $api_key = AI_Provider::getApiKey();
2155 $model = AI_Provider::getTextModel();
2156 $api_key_valid = !empty($api_key) && !empty($model);
2157
2158 wp_send_json_success([
2159 'daily_used' => $daily_used,
2160 'daily_limit' => $daily_limit,
2161 'limit_reached' => ($daily_limit > 0 && $daily_used >= $daily_limit),
2162 'api_key_valid' => $api_key_valid,
2163 ]);
2164 }
2165
2166 /**
2167 * AJAX handler to check image generation limits.
2168 *
2169 * @return void
2170 */
2171 public function handleAiImageCheckLimits(): void
2172 {
2173 check_ajax_referer('king_addons_ai_generate_image_nonce', 'nonce');
2174 if (!current_user_can('manage_options')) {
2175 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
2176 }
2177
2178 $options = get_option('king_addons_ai_options', []);
2179 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
2180 $daily_used = $this->getAiDailyUsage();
2181 // Check if API key and model are set
2182 $api_key = AI_Provider::getApiKey();
2183 $model = AI_Provider::getTextModel();
2184 $api_key_valid = !empty($api_key) && !empty($model);
2185
2186 wp_send_json_success([
2187 'daily_used' => $daily_used,
2188 'daily_limit' => $daily_limit,
2189 'limit_reached' => ($daily_limit > 0 && $daily_used >= $daily_limit),
2190 'api_key_valid' => $api_key_valid,
2191 ]);
2192
2193 }
2194
2195
2196 /**
2197 * Renders the Editor Integration section description.
2198 *
2199 * @return void
2200 */
2201 public function renderAiEditorSection(): void
2202 {
2203 echo '<p>' . esc_html__('Control the integration of AI features in the Elementor editor.', 'king-addons') . '</p>';
2204 }
2205
2206 /**
2207 * Renders description for Alt Text Settings section.
2208 *
2209 * @return void
2210 */
2211 public function renderAiAltTextSection(): void
2212 {
2213 echo '<p>' . esc_html__('Configure automatic alt text generation for images in Media Library.', 'king-addons') . '</p>';
2214 }
2215
2216 /**
2217 * Renders the Enable AI Buttons checkbox field.
2218 *
2219 * @return void
2220 */
2221 public function renderAiEnableButtonsField(): void
2222 {
2223 $options = get_option('king_addons_ai_options', []);
2224 // Default to true if option has never been saved, otherwise use saved value
2225 $enabled = array_key_exists('enable_ai_buttons', $options) ? (bool) $options['enable_ai_buttons'] : true;
2226 printf(
2227 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_buttons]" value="1" %s /> %s</label>',
2228 checked($enabled, true, false),
2229 esc_html__('Enable AI Text Editing Buttons in Elementor Editor', 'king-addons')
2230 );
2231 }
2232
2233 /**
2234 * Renders the Enable AI Image Generation checkbox field.
2235 *
2236 * @return void
2237 */
2238 public function renderAiImageGenerationField(): void
2239 {
2240 $options = get_option('king_addons_ai_options', []);
2241 // Default to true if option has never been saved, otherwise use saved value
2242 $enabled = array_key_exists('enable_ai_image_generation_button', $options) ? (bool) $options['enable_ai_image_generation_button'] : true;
2243 printf(
2244 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_image_generation_button]" value="1" %s /> %s</label>',
2245 checked($enabled, true, false),
2246 esc_html__('Enable AI Image Generation Button in Elementor Editor', 'king-addons')
2247 );
2248 }
2249
2250 /**
2251 * Renders the Enable AI Alt Text Button checkbox field.
2252 *
2253 * @return void
2254 */
2255 public function renderAiAltTextButtonField(): void
2256 {
2257 $options = get_option('king_addons_ai_options', []);
2258 // Default to true if option has never been saved, otherwise use saved value
2259 $enabled = array_key_exists('enable_ai_alt_text_button', $options) ? (bool) $options['enable_ai_alt_text_button'] : true;
2260 printf(
2261 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_alt_text_button]" value="1" %s /> %s</label>',
2262 checked($enabled, true, false),
2263 esc_html__('Enable AI Alt Text Generation Button in Media Library', 'king-addons')
2264 );
2265 echo '<p class="description">' . esc_html__('Show "Generate" button in Media Library to manually create alt text for images using AI.', 'king-addons') . '</p>';
2266 }
2267
2268 /**
2269 * Renders the Enable AI Alt Text Auto Generation checkbox field.
2270 *
2271 * @return void
2272 */
2273 public function renderAiAltTextAutoGenerationField(): void
2274 {
2275 $options = get_option('king_addons_ai_options', []);
2276 $is_pro = !king_addons_freemius()->can_use_premium_code();
2277 // Default to false if option has never been saved, otherwise use saved value
2278 $enabled = array_key_exists('enable_ai_alt_text_auto_generation', $options) ? (bool) $options['enable_ai_alt_text_auto_generation'] : false;
2279 printf(
2280 '<label><input type="checkbox"' . ($is_pro ? ' disabled' : '') . ' name="king_addons_ai_options[enable_ai_alt_text_auto_generation]" value="1" %s /> %s</label>',
2281 checked($enabled, true, false),
2282 esc_html__('Automatically Generate Alt Text for New Images' . ($is_pro ? ' (PRO feature)' : ''), 'king-addons')
2283 );
2284 echo '<p class="description">' . esc_html__('Automatically generate alt text when new images are uploaded to Media Library. Great for SEO.', 'king-addons') . '</p>';
2285 }
2286
2287
2288 /**
2289 * Renders the AI Alt Text Generation Interval field.
2290 *
2291 * @return void
2292 */
2293 public function renderAiAltTextIntervalField(): void
2294 {
2295 $options = get_option('king_addons_ai_options', []);
2296 $interval = isset($options['ai_alt_text_generation_interval']) ? (int) $options['ai_alt_text_generation_interval'] : 20;
2297 printf(
2298 '<input type="number" name="king_addons_ai_options[ai_alt_text_generation_interval]" value="%d" min="10" max="3600" placeholder="20" />',
2299 $interval
2300 );
2301 echo '<p class="description">' . esc_html__('How often (in seconds) the system should process alt text generation queue. Recommended: 20 seconds. Lower values process faster; higher values reduce API load. Range: 10-3600 seconds.', 'king-addons') . '</p>';
2302 }
2303
2304 /**
2305 * Renders the image model selection dropdown field.
2306 *
2307 * @return void
2308 */
2309 public function renderAiImageModelField(): void
2310 {
2311 $options = get_option('king_addons_ai_options', []);
2312 $selected = $options['openai_image_model'] ?? 'dall-e-3';
2313 $models = [
2314 'dall-e-3' => esc_html__('DALL·E 3', 'king-addons'),
2315 'gpt-image-1' => esc_html__('GPT Image 1', 'king-addons'),
2316 ];
2317 printf(
2318 '<select name="king_addons_ai_options[openai_image_model]" %s>',
2319 ''
2320 );
2321 foreach ($models as $id => $label) {
2322 printf(
2323 '<option value="%s" %s>%s</option>',
2324 esc_attr($id),
2325 selected($selected, $id, false),
2326 esc_html($label)
2327 );
2328 }
2329 echo '</select>';
2330 echo '<p class="description">';
2331 printf(
2332 /* translators: %1$s: URL to OpenAI Organization Settings */
2333 wp_kses(
2334 __('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'),
2335 ['a' => ['href' => [], 'target' => [], 'rel' => []]]
2336 ),
2337 esc_url('https://platform.openai.com/settings/organization/general')
2338 );
2339 echo '</p>';
2340 }
2341
2342 /**
2343 * AJAX handler to generate images using the configured AI provider.
2344 *
2345 * @return void
2346 */
2347 public function set_openai_curl_options($handle, $r, $url): void
2348 {
2349 if (!is_string($url) || $url === '') {
2350 return;
2351 }
2352
2353 // Apply these cURL options only to AI provider requests to avoid affecting other outbound HTTP calls.
2354 if (!AI_Provider::isProviderUrl($url)) {
2355 return;
2356 }
2357
2358 // Increase connect timeout to 60s, and total timeout to 5m.
2359 curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 60);
2360 curl_setopt($handle, CURLOPT_DNS_CACHE_TIMEOUT, 300);
2361 curl_setopt($handle, CURLOPT_TIMEOUT, 300);
2362 }
2363
2364 public function handleAiGenerateImage(): void
2365 {
2366 // Verify nonce and permissions
2367 check_ajax_referer('king_addons_ai_generate_image_nonce', 'nonce');
2368 if (!current_user_can('manage_options')) {
2369 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
2370 }
2371
2372 // Gather input parameters
2373 $prompt = isset($_POST['prompt']) ? sanitize_textarea_field(wp_unslash($_POST['prompt'])) : '';
2374 $quality = isset($_POST['quality']) ? sanitize_text_field(wp_unslash($_POST['quality'])) : '';
2375 $size = isset($_POST['size']) ? sanitize_text_field(wp_unslash($_POST['size'])) : '';
2376 // Model from frontend selector
2377 $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : '';
2378 if ($model === '') {
2379 $model = AI_Provider::getImageModel();
2380 }
2381
2382 $api_key = AI_Provider::getApiKey();
2383 if (empty($api_key)) {
2384 wp_send_json_error([
2385 'message' => sprintf(
2386 /* translators: %s: provider name */
2387 esc_html__('%s API key is not set.', 'king-addons'),
2388 AI_Provider::getLabel()
2389 )
2390 ], 400);
2391 }
2392 if (empty($prompt)) {
2393 wp_send_json_error(['message' => esc_html__('Please provide an image prompt.', 'king-addons')], 400);
2394 }
2395
2396 // Build the request body. OpenAI's image models take vendor specific
2397 // quality/size/background options; OpenRouter fans out to many vendors
2398 // that do not share that vocabulary, so only portable fields are sent.
2399 $body = [
2400 'model' => $model,
2401 'prompt' => $prompt,
2402 ];
2403
2404 if (AI_Provider::isOpenRouter()) {
2405 $body['n'] = 1;
2406 } else {
2407 $body['size'] = $size;
2408
2409 if ($model === 'dall-e-3') {
2410 // DALL·E 3 parameters
2411 $body['n'] = 1;
2412 $body['quality'] = ($quality === 'hd') ? 'hd' : 'standard';
2413 } elseif ($model === 'gpt-image-1') {
2414 // GPT Image 1 parameters
2415 // Only include background when transparent is requested
2416 if (!empty($_POST['background']) && 'transparent' === sanitize_text_field(wp_unslash($_POST['background']))) {
2417 $body['background'] = 'transparent';
2418 }
2419 $body['quality'] = in_array($quality, ['low', 'medium', 'high', 'auto'], true)
2420 ? $quality
2421 : 'auto';
2422 }
2423 }
2424
2425 add_action('http_api_curl', [$this, 'set_openai_curl_options'], 10, 3);
2426
2427 // Call the provider's image generation API.
2428 $response = wp_remote_post(
2429 AI_Provider::getImagesEndpoint(),
2430 [
2431 'headers' => AI_Provider::getHeaders(),
2432 'body' => wp_json_encode($body),
2433 'timeout' => 300,
2434 ]
2435 );
2436
2437 $data = AI_Provider::decodeResponse($response, esc_html__('AI image generation error.', 'king-addons'));
2438 if (is_wp_error($data)) {
2439 wp_send_json_error(['message' => $data->get_error_message()], 500);
2440 }
2441
2442 // Depending on the model, an image comes back either as a hosted URL or
2443 // as inline base64, so both shapes are accepted from either provider.
2444 $item = (isset($data['data'][0]) && is_array($data['data'][0])) ? $data['data'][0] : [];
2445 $base64 = '';
2446 $remote_url = '';
2447 $mime = 'image/png';
2448
2449 if (!empty($item['b64_json']) && is_string($item['b64_json'])) {
2450 $base64 = $item['b64_json'];
2451 if (!empty($item['media_type']) && is_string($item['media_type'])) {
2452 $mime = $item['media_type'];
2453 }
2454 } elseif (!empty($item['url']) && is_string($item['url'])) {
2455 $remote_url = $item['url'];
2456 } elseif (!empty($item['image_url']['url']) && is_string($item['image_url']['url'])) {
2457 $remote_url = $item['image_url']['url'];
2458 }
2459
2460 // Some providers hand back a data: URI in the url field.
2461 if ($remote_url !== '' && strpos($remote_url, 'data:') === 0 && preg_match('#^data:([^;,]+);base64,(.+)$#s', $remote_url, $matches)) {
2462 $mime = $matches[1];
2463 $base64 = $matches[2];
2464 $remote_url = '';
2465 }
2466
2467 if ($base64 === '' && $remote_url === '') {
2468 wp_send_json_error([
2469 'message' => sprintf(
2470 /* translators: %s: model id */
2471 esc_html__('The model %s did not return an image. Pick an image-capable model in AI Settings.', 'king-addons'),
2472 $model
2473 )
2474 ], 500);
2475 }
2476
2477 require_once ABSPATH . 'wp-admin/includes/image.php';
2478 require_once ABSPATH . 'wp-admin/includes/file.php';
2479 require_once ABSPATH . 'wp-admin/includes/media.php';
2480
2481 $filename = substr(sanitize_file_name($prompt), 0, 100);
2482 if ($filename === '') {
2483 $filename = 'ai-image';
2484 }
2485
2486 if ($base64 !== '') {
2487 $bytes = base64_decode($base64, true);
2488 if (!$bytes) {
2489 wp_send_json_error(['message' => esc_html__('Invalid image data from API.', 'king-addons')], 500);
2490 }
2491
2492 $extension = 'png';
2493 $type_map = ['image/jpeg' => 'jpg', 'image/jpg' => 'jpg', 'image/webp' => 'webp', 'image/gif' => 'gif'];
2494 if (isset($type_map[$mime])) {
2495 $extension = $type_map[$mime];
2496 }
2497
2498 // Create a temp file and write it
2499 $tmp = wp_tempnam($filename . '.' . $extension);
2500 if (!$tmp || !file_put_contents($tmp, $bytes)) {
2501 wp_send_json_error(['message' => esc_html__('Failed to write temp image file.', 'king-addons')], 500);
2502 }
2503
2504 // Sideload into the Media Library
2505 $attachment_id = media_handle_sideload(
2506 ['name' => $filename . '.' . $extension, 'tmp_name' => $tmp],
2507 0,
2508 $prompt
2509 );
2510
2511 if (is_wp_error($attachment_id)) {
2512 @unlink($tmp);
2513 wp_send_json_error(['message' => $attachment_id->get_error_message()], 500);
2514 }
2515 } else {
2516 $attachment_id = media_sideload_image(esc_url_raw($remote_url), 0, $prompt, 'id');
2517 if (is_wp_error($attachment_id)) {
2518 wp_send_json_error(['message' => $attachment_id->get_error_message()], 500);
2519 }
2520 }
2521
2522 // Respond with attachment details
2523 wp_send_json_success([
2524 'attachment_id' => $attachment_id,
2525 'url' => wp_get_attachment_url($attachment_id),
2526 ]);
2527 }
2528
2529 /**
2530 * Renders the global content language field.
2531 *
2532 * @return void
2533 */
2534 public function renderAiContentLanguageField(): void
2535 {
2536 $options = get_option('king_addons_ai_options', []);
2537 $enabled = !empty($options['content_language_custom_enable']);
2538 $custom_lang = $options['content_language_custom'] ?? '';
2539 echo '<label><input type="checkbox" name="king_addons_ai_options[content_language_custom_enable]" value="1" ' . checked($enabled, true, false) . ' id="ka-content-lang-enable-checkbox" /> ' . esc_html__('Generate content in a non-English language', 'king-addons') . '</label>';
2540 echo '<div id="ka-content-lang-custom-wrap"' . ($enabled ? '' : ' hidden') . ' style="margin-top:8px;">';
2541 echo '<input type="text" name="king_addons_ai_options[content_language_custom]" value="' . esc_attr($custom_lang) . '" placeholder="' . esc_attr__('language name', 'king-addons') . '" class="regular-text" id="ka-content-lang-custom-input" />';
2542 echo '<p class="description">' . esc_html__('Applies to all AI-generated content: alt text, tags, blog posts. Type your language name, for example: Spanish, French, German, Polish, Italian, Russian, Hindi, Arabic, Portuguese, etc.', 'king-addons') . '</p>';
2543 echo '</div>';
2544 echo '<script>document.getElementById("ka-content-lang-enable-checkbox").addEventListener("change",function(){var w=document.getElementById("ka-content-lang-custom-wrap");if(this.checked){w.removeAttribute("hidden");}else{w.setAttribute("hidden","");}});</script>';
2545 }
2546
2547 /**
2548 * Renders the Image Detail Level dropdown for Alt Text Settings.
2549 *
2550 * @return void
2551 */
2552 public function renderAiAltTextImageDetailLevelField(): void
2553 {
2554 $options = get_option('king_addons_ai_options', []);
2555 $selected = $options['ai_alt_text_image_detail_level'] ?? 'low';
2556 echo '<select name="king_addons_ai_options[ai_alt_text_image_detail_level]">';
2557 echo '<option value="low"' . selected($selected, 'low', false) . '>' . esc_html__('Low', 'king-addons') . '</option>';
2558 echo '<option value="high"' . selected($selected, 'high', false) . '>' . esc_html__('High', 'king-addons') . '</option>';
2559 echo '</select>';
2560 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>';
2561 }
2562
2563 /**
2564 * Renders the Translation Settings section description.
2565 *
2566 * @return void
2567 */
2568 public function renderAiTranslationSection(): void
2569 {
2570 echo '<p>' . esc_html__('Configure AI Page Translator settings for Elementor editor.', 'king-addons') . '</p>';
2571 }
2572
2573 /**
2574 * Renders the Enable AI Page Translator checkbox field.
2575 *
2576 * @return void
2577 */
2578 public function renderAiPageTranslatorField(): void
2579 {
2580 $options = get_option('king_addons_ai_options', []);
2581 // Default to true if option has never been saved, otherwise use saved value
2582 $enabled = array_key_exists('enable_ai_page_translator', $options) ? (bool) $options['enable_ai_page_translator'] : true;
2583 printf(
2584 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_page_translator]" value="1" %s /> %s</label>',
2585 checked($enabled, true, false),
2586 esc_html__('Show AI Page Translator button in Elementor editor toolbar', 'king-addons')
2587 );
2588 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>';
2589 }
2590
2591 /**
2592 * AJAX handler to translate text using the configured AI provider.
2593 *
2594 * @return void
2595 */
2596 public function handleAiTranslateText(): void
2597 {
2598 check_ajax_referer('king_addons_ai_generate_nonce', 'nonce');
2599 if (!current_user_can('manage_options')) {
2600 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
2601 }
2602
2603 $text = isset($_POST['text']) ? sanitize_textarea_field(wp_unslash($_POST['text'])) : '';
2604 $from_lang = isset($_POST['from_lang']) ? sanitize_text_field(wp_unslash($_POST['from_lang'])) : 'auto';
2605 $to_lang = isset($_POST['to_lang']) ? sanitize_text_field(wp_unslash($_POST['to_lang'])) : 'en';
2606
2607 $options = get_option('king_addons_ai_options', []);
2608 $api_key = AI_Provider::getApiKey();
2609 $model = AI_Provider::getTextModel();
2610
2611 if (empty($api_key) || empty($model)) {
2612 wp_send_json_error(['message' => esc_html__('API key or model not set.', 'king-addons')], 400);
2613 }
2614
2615 if (empty($text)) {
2616 wp_send_json_error(['message' => esc_html__('No text provided for translation.', 'king-addons')], 400);
2617 }
2618
2619 // Check daily token limit
2620 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
2621 $current_usage = $this->getAiDailyUsage();
2622
2623 if ($daily_limit > 0 && $current_usage >= $daily_limit) {
2624 wp_send_json_error([
2625 'message' => esc_html__('Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.', 'king-addons'),
2626 // This is the plugin's own cap, not the provider's, so the
2627 // client must not retry it or blame the AI provider.
2628 'code' => 'local_limit',
2629 'retryable' => false,
2630 ], 429);
2631 }
2632
2633 // Prepare translation prompt
2634 $from_lang_name = ($from_lang === 'auto') ? 'auto-detected language' : $this->getLanguageName($from_lang);
2635 $to_lang_name = $this->getLanguageName($to_lang);
2636
2637 // Enhanced system message for better custom language and prompt handling
2638 $system_message = 'You are a professional translator with expertise in languages, dialects, writing styles, and custom translation approaches. You can handle:
2639
2640 1. Standard languages (English, Spanish, etc.)
2641 2. Fictional/constructed languages (Klingon, Dothraki, Elvish, etc.)
2642 3. Historical language variants (Old English, Latin, etc.)
2643 4. Writing styles and tones (formal, casual, academic, business, etc.)
2644 5. Special communication styles (pirate speak, baby talk, technical jargon, etc.)
2645
2646 When translating:
2647 - Maintain the original meaning, tone, and formatting
2648 - Preserve HTML tags exactly as they appear
2649 - For custom languages, apply consistent linguistic rules
2650 - For style prompts, adapt the tone and vocabulary appropriately
2651 - Only return the translated/adapted text without explanations
2652
2653 If the target is a style rather than a language, transform the text to match that style while keeping the same language.';
2654
2655 // Enhanced user message with better context for custom languages and prompts
2656 if ($from_lang === 'auto') {
2657 $user_message = "Transform the following text to {$to_lang_name}:\n\n{$text}";
2658 } else {
2659 // Check if it looks like a style prompt rather than a language
2660 $is_style_prompt = $this->isStylePrompt($to_lang_name);
2661
2662 if ($is_style_prompt) {
2663 $user_message = "Transform the following text from {$from_lang_name} using this style/approach: {$to_lang_name}:\n\n{$text}";
2664 } else {
2665 $user_message = "Translate the following text from {$from_lang_name} to {$to_lang_name}:\n\n{$text}";
2666 }
2667 }
2668
2669 $messages = [
2670 ['role' => 'system', 'content' => $system_message],
2671 ['role' => 'user', 'content' => $user_message]
2672 ];
2673
2674 $data = AI_Provider::decodeResponse(
2675 wp_remote_post(
2676 AI_Provider::getChatEndpoint(),
2677 [
2678 'headers' => AI_Provider::getHeaders(),
2679 'body' => wp_json_encode(AI_Provider::prepareChatPayload([
2680 'model' => $model,
2681 'messages' => $messages,
2682 'max_tokens' => 1000,
2683 'temperature' => 0.3, // Lower temperature for more consistent translations
2684 ])),
2685 'timeout' => 60, // Free and reasoning models are routinely slower than 30s.
2686 ]
2687 ),
2688 esc_html__('AI translation error.', 'king-addons')
2689 );
2690
2691 // The translator runs hundreds of these back to back, so it needs to
2692 // know whether a failure is worth retrying or is a dead end.
2693 if (is_wp_error($data)) {
2694 $this->sendAiProviderError($data);
2695 }
2696
2697 $translated_text = AI_Provider::extractMessageContent($data);
2698 if (is_wp_error($translated_text)) {
2699 $this->sendAiProviderError($translated_text);
2700 }
2701
2702 // Update token usage statistics if present in the response
2703 if (isset($data['usage']['total_tokens'])) {
2704 $this->incrementAiDailyUsage(intval($data['usage']['total_tokens']));
2705 }
2706
2707 wp_send_json_success([
2708 'translated_text' => $translated_text,
2709 'usage' => [
2710 'tokens_used' => $data['usage']['total_tokens'] ?? 0,
2711 'daily_used' => $this->getAiDailyUsage(),
2712 'daily_limit' => $daily_limit,
2713 ]
2714 ]);
2715 }
2716
2717 /**
2718 * Ends an AJAX request with a classified provider error.
2719 *
2720 * Sends the closest meaningful HTTP status instead of a blanket 500, plus a
2721 * machine readable code and a retryable flag so the caller can back off and
2722 * retry transient failures rather than aborting a long run.
2723 *
2724 * @param \WP_Error $error Error from the provider layer.
2725 * @return void
2726 */
2727 private function sendAiProviderError(\WP_Error $error): void
2728 {
2729 $classified = AI_Provider::classifyError($error);
2730
2731 wp_send_json_error(
2732 [
2733 'message' => $classified['message'],
2734 'code' => $classified['code'],
2735 'retryable' => $classified['retryable'],
2736 'provider' => AI_Provider::getProvider(),
2737 'provider_label' => AI_Provider::getLabel(),
2738 ],
2739 AI_Provider::getResponseStatus($classified)
2740 );
2741 }
2742
2743 /**
2744 * Get language name by code
2745 *
2746 * @param string $code Language code
2747 * @return string Language name
2748 */
2749 private function getLanguageName(string $code): string
2750 {
2751 $languages = [
2752 'en' => 'English',
2753 'es' => 'Spanish',
2754 'fr' => 'French',
2755 'de' => 'German',
2756 'it' => 'Italian',
2757 'pt' => 'Portuguese',
2758 'ru' => 'Russian',
2759 'ja' => 'Japanese',
2760 'ko' => 'Korean',
2761 'zh' => 'Chinese',
2762 'ar' => 'Arabic',
2763 'hi' => 'Hindi',
2764 'nl' => 'Dutch',
2765 'pl' => 'Polish',
2766 'tr' => 'Turkish',
2767 'uk' => 'Ukrainian',
2768 'cs' => 'Czech',
2769 'sv' => 'Swedish',
2770 'no' => 'Norwegian',
2771 'da' => 'Danish',
2772 'fi' => 'Finnish'
2773 ];
2774
2775 return $languages[$code] ?? $code;
2776 }
2777
2778 /**
2779 * Check if the given text appears to be a style prompt rather than a language
2780 *
2781 * @param string $text The text to check
2782 * @return bool True if it looks like a style prompt
2783 */
2784 private function isStylePrompt(string $text): bool
2785 {
2786 $text_lower = strtolower($text);
2787
2788 // Common style/tone indicators
2789 $style_indicators = [
2790 'formal',
2791 'casual',
2792 'professional',
2793 'business',
2794 'academic',
2795 'technical',
2796 'friendly',
2797 'serious',
2798 'humorous',
2799 'dramatic',
2800 'poetic',
2801 'simple',
2802 'complex',
2803 'detailed',
2804 'brief',
2805 'conversational',
2806 'literary',
2807 'scientific',
2808 'medical',
2809 'legal',
2810 'marketing',
2811 'sales',
2812 'tone',
2813 'style',
2814 'manner',
2815 'approach',
2816 'way',
2817 'voice',
2818 'pirate',
2819 'shakespeare',
2820 'baby',
2821 'child',
2822 'elderly',
2823 'slang',
2824 'jargon',
2825 'dialect',
2826 'accent'
2827 ];
2828
2829 // Check if any style indicators are present
2830 foreach ($style_indicators as $indicator) {
2831 if (strpos($text_lower, $indicator) !== false) {
2832 return true;
2833 }
2834 }
2835
2836 // Check if it contains descriptive phrases
2837 $descriptive_patterns = [
2838 'for ',
2839 'like ',
2840 'as if ',
2841 'in the style of',
2842 'in a ',
2843 'with a ',
2844 'using ',
2845 'speaking ',
2846 'written ',
2847 'sound like',
2848 'talk like'
2849 ];
2850
2851 foreach ($descriptive_patterns as $pattern) {
2852 if (strpos($text_lower, $pattern) !== false) {
2853 return true;
2854 }
2855 }
2856
2857 return false;
2858 }
2859 }
2860