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

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

2,861 lines 116.7 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 'show_on_single' => !empty($settings['show_on_single']),
726 'wishlist_columns' => array_values(
727 array_intersect(
728 (array) ($settings['wishlist_columns'] ?? []),
729 ['image', 'title', 'price', 'stock', 'notes', 'add_to_cart', 'remove']
730 )
731 ),
732 'cache_enabled' => !empty($settings['cache_enabled']),
733 'cache_ttl' => max(0, absint($settings['cache_ttl'] ?? 0)),
734 'icon_choice' => sanitize_text_field($settings['icon_choice'] ?? $defaults['icon_choice']),
735 ];
736
737 return wp_parse_args($sanitized, $defaults);
738 }
739
740 function king_addons_section_widgets_callback($args): void
741 {
742 ?>
743 <h2 id="<?php echo esc_attr($args['id']); ?>" class="kng-section-title"><?php esc_html_e('Elements', 'king-addons'); ?>
744 </h2>
745 <?php
746 }
747
748 function king_addons_section_features_callback($args): void
749 {
750 ?>
751 <div class="kng-section-separator"></div>
752 <h2 id="<?php echo esc_attr($args['id']); ?>" class="kng-section-title"><?php esc_html_e('Features', 'king-addons'); ?>
753 </h2>
754 <?php
755 }
756
757 function enqueueAdminAssets(): void
758 {
759 wp_enqueue_style('king-addons-admin', KING_ADDONS_URL . 'includes/admin/css/admin.css', '', KING_ADDONS_VERSION);
760 // Styles for AI Image Generation controls in Elementor
761 wp_enqueue_style('king-addons-ai-imagefield', KING_ADDONS_URL . 'includes/admin/css/ai-imagefield.css', array('king-addons-admin'), KING_ADDONS_VERSION);
762 }
763
764 function enqueueUpgradeLinkScript(): void
765 {
766 // Only add the script if premium is not active
767 if (!king_addons_freemius()->can_use_premium_code()) {
768 wp_enqueue_script('jquery');
769 wp_add_inline_script('jquery', "
770 jQuery(document).ready(function($) {
771 $('#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');
772 });
773 ");
774 }
775 }
776
777 public function enqueueGlobalAdminStyles(): void
778 {
779 wp_enqueue_style('king-addons-hide-spam', KING_ADDONS_URL . 'includes/admin/css/hide-spam-notifications.css', [], KING_ADDONS_VERSION);
780 }
781
782 function enqueueSettingsAssets(): void
783 {
784 wp_enqueue_style('king-addons-settings', KING_ADDONS_URL . 'includes/admin/css/settings.css', '', KING_ADDONS_VERSION);
785 wp_enqueue_style('wp-color-picker');
786 wp_enqueue_script('jquery');
787 wp_enqueue_script('wp-color-picker');
788 wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-wpcolorpicker-wpcolorpicker');
789 wp_enqueue_script('king-addons-settings', KING_ADDONS_URL . 'includes/admin/js/settings.js', '', KING_ADDONS_VERSION);
790 }
791
792 /**
793 * Registers AI Settings using the WordPress Settings API.
794 *
795 * @return void
796 */
797 public function createAiSettings(): void
798 {
799 register_setting(
800 'king_addons_ai',
801 'king_addons_ai_options',
802 [$this, 'sanitizeAiSettings']
803 );
804
805 add_settings_section(
806 'king_addons_ai_openai_section',
807 esc_html__('AI Provider Settings', 'king-addons'),
808 [$this, 'renderAiOpenaiSection'],
809 'king-addons-ai-settings'
810 );
811
812 add_settings_field(
813 'ai_provider',
814 esc_html__('AI Provider', 'king-addons'),
815 [$this, 'renderAiProviderField'],
816 'king-addons-ai-settings',
817 'king_addons_ai_openai_section'
818 );
819
820 // Provider specific rows. The row classes let the settings page show only
821 // the fields belonging to the provider that is currently selected; the
822 // inactive ones start hidden so they never flash before the script runs.
823 $active_provider = AI_Provider::getProvider();
824 $row_class = static function (string $provider) use ($active_provider): array {
825 $classes = 'ka-ai-provider-row ka-ai-provider-' . $provider;
826 if ($provider !== $active_provider) {
827 $classes .= ' ka-ai-row-hidden';
828 }
829 return ['class' => $classes];
830 };
831
832 add_settings_field(
833 'openai_api_key',
834 esc_html__('OpenAI API Key', 'king-addons'),
835 [$this, 'renderAiApiKeyField'],
836 'king-addons-ai-settings',
837 'king_addons_ai_openai_section',
838 $row_class(AI_Provider::OPENAI)
839 );
840
841 add_settings_field(
842 'openrouter_api_key',
843 esc_html__('OpenRouter API Key', 'king-addons'),
844 [$this, 'renderAiOpenRouterApiKeyField'],
845 'king-addons-ai-settings',
846 'king_addons_ai_openai_section',
847 $row_class(AI_Provider::OPENROUTER)
848 );
849
850 add_settings_field(
851 'ai_connection_test',
852 esc_html__('Connection', 'king-addons'),
853 [$this, 'renderAiConnectionTestField'],
854 'king-addons-ai-settings',
855 'king_addons_ai_openai_section'
856 );
857
858 add_settings_field(
859 'openai_model',
860 esc_html__('OpenAI Model (for text generation)', 'king-addons'),
861 [$this, 'renderAiModelField'],
862 'king-addons-ai-settings',
863 'king_addons_ai_openai_section',
864 $row_class(AI_Provider::OPENAI)
865 );
866
867 add_settings_field(
868 'openai_vision_model',
869 esc_html__('OpenAI Model (for image recognition)', 'king-addons'),
870 [$this, 'renderAiVisionModelField'],
871 'king-addons-ai-settings',
872 'king_addons_ai_openai_section',
873 $row_class(AI_Provider::OPENAI)
874 );
875
876 // Add image model selector field
877 add_settings_field(
878 'openai_image_model',
879 esc_html__('OpenAI Image Model (for image generation)', 'king-addons'),
880 [$this, 'renderAiImageModelField'],
881 'king-addons-ai-settings',
882 'king_addons_ai_openai_section',
883 $row_class(AI_Provider::OPENAI)
884 );
885
886 add_settings_field(
887 'openrouter_model',
888 esc_html__('OpenRouter Model (for text generation)', 'king-addons'),
889 [$this, 'renderAiOpenRouterModelField'],
890 'king-addons-ai-settings',
891 'king_addons_ai_openai_section',
892 $row_class(AI_Provider::OPENROUTER)
893 );
894
895 add_settings_field(
896 'openrouter_vision_model',
897 esc_html__('OpenRouter Model (for image recognition)', 'king-addons'),
898 [$this, 'renderAiOpenRouterVisionModelField'],
899 'king-addons-ai-settings',
900 'king_addons_ai_openai_section',
901 $row_class(AI_Provider::OPENROUTER)
902 );
903
904 add_settings_field(
905 'openrouter_image_model',
906 esc_html__('OpenRouter Image Model (for image generation)', 'king-addons'),
907 [$this, 'renderAiOpenRouterImageModelField'],
908 'king-addons-ai-settings',
909 'king_addons_ai_openai_section',
910 $row_class(AI_Provider::OPENROUTER)
911 );
912
913 // Global content language
914 add_settings_field(
915 'content_language_custom_enable',
916 esc_html__('Content Language', 'king-addons'),
917 [$this, 'renderAiContentLanguageField'],
918 'king-addons-ai-settings',
919 'king_addons_ai_openai_section'
920 );
921
922 // Add Editor Integration section and field
923 add_settings_section(
924 'king_addons_ai_editor_section',
925 esc_html__('Editor Integration', 'king-addons'),
926 [$this, 'renderAiEditorSection'],
927 'king-addons-ai-settings'
928 );
929 add_settings_field(
930 'enable_ai_buttons',
931 esc_html__('AI Text Editing Buttons', 'king-addons'),
932 [$this, 'renderAiEnableButtonsField'],
933 'king-addons-ai-settings',
934 'king_addons_ai_editor_section'
935 );
936 add_settings_field(
937 'enable_ai_image_generation_button',
938 esc_html__('AI Image Generation Button', 'king-addons'),
939 [$this, 'renderAiImageGenerationField'],
940 'king-addons-ai-settings',
941 'king_addons_ai_editor_section'
942 );
943
944 // Add Alt Text Settings section
945 add_settings_section(
946 'king_addons_ai_alt_text_section',
947 esc_html__('Alt Text Settings', 'king-addons'),
948 [$this, 'renderAiAltTextSection'],
949 'king-addons-ai-settings'
950 );
951 add_settings_field(
952 'enable_ai_alt_text_button',
953 esc_html__('AI Alt Text Button', 'king-addons'),
954 [$this, 'renderAiAltTextButtonField'],
955 'king-addons-ai-settings',
956 'king_addons_ai_alt_text_section'
957 );
958
959 add_settings_field(
960 'enable_ai_alt_text_auto_generation',
961 ((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')),
962 [$this, 'renderAiAltTextAutoGenerationField'],
963 'king-addons-ai-settings',
964 'king_addons_ai_alt_text_section'
965 );
966 add_settings_field(
967 'ai_alt_text_generation_interval',
968 esc_html__('Alt Text Generation Interval', 'king-addons'),
969 [$this, 'renderAiAltTextIntervalField'],
970 'king-addons-ai-settings',
971 'king_addons_ai_alt_text_section'
972 );
973 // Add Image Detail Level field
974 add_settings_field(
975 'ai_alt_text_image_detail_level',
976 esc_html__('Image Detail Level', 'king-addons'),
977 [$this, 'renderAiAltTextImageDetailLevelField'],
978 'king-addons-ai-settings',
979 'king_addons_ai_alt_text_section'
980 );
981 // Add Translation Settings section
982 add_settings_section(
983 'king_addons_ai_translation_section',
984 esc_html__('Translation Settings', 'king-addons'),
985 [$this, 'renderAiTranslationSection'],
986 'king-addons-ai-settings'
987 );
988
989 add_settings_field(
990 'enable_ai_page_translator',
991 esc_html__('AI Page Translator Button', 'king-addons'),
992 [$this, 'renderAiPageTranslatorField'],
993 'king-addons-ai-settings',
994 'king_addons_ai_translation_section'
995 );
996
997 // Add Usage Quota Settings section and field
998 add_settings_section(
999 'king_addons_ai_quota_section',
1000 esc_html__('Usage Quota Settings', 'king-addons'),
1001 [$this, 'renderAiQuotaSection'],
1002 'king-addons-ai-settings'
1003 );
1004
1005 add_settings_field(
1006 'daily_token_limit',
1007 esc_html__('Daily Token Limit', 'king-addons'),
1008 [$this, 'renderAiDailyLimitField'],
1009 'king-addons-ai-settings',
1010 'king_addons_ai_quota_section'
1011 );
1012
1013 // Add Usage Statistics section (read-only)
1014 add_settings_section(
1015 'king_addons_ai_stats_section',
1016 esc_html__('Usage Statistics', 'king-addons'),
1017 [$this, 'renderAiStatsSection'],
1018 'king-addons-ai-settings'
1019 );
1020
1021 // Clear models cache when options updated.
1022 add_action('update_option_king_addons_ai_options', [$this, 'clearAiModelsCache']);
1023
1024 // AJAX handler for refreshing models.
1025 add_action('wp_ajax_king_addons_ai_refresh_models', [$this, 'handleAiRefreshModels']);
1026
1027 // AJAX handler for testing the provider connection.
1028 add_action('wp_ajax_king_addons_ai_test_connection', [$this, 'handleAiTestConnection']);
1029
1030 // AJAX handler for generating text via AI
1031 add_action('wp_ajax_king_addons_ai_generate_text', [$this, 'handleAiGenerateText']);
1032
1033 // AJAX handler to change text using AI based on user prompt and original text.
1034 add_action('wp_ajax_king_addons_ai_change_text', [$this, 'handleAiChangeText']);
1035
1036 // AJAX handler to check token usage limits
1037 add_action('wp_ajax_king_addons_ai_check_tokens', [$this, 'handleAiCheckTokens']);
1038
1039 // AJAX handler to check image generation limits
1040 add_action('wp_ajax_king_addons_ai_image_check_limits', [$this, 'handleAiImageCheckLimits']);
1041
1042 // THIRD_EDIT: Register AJAX handler for AI image generation
1043 add_action('wp_ajax_king_addons_ai_generate_image', [$this, 'handleAiGenerateImage']);
1044
1045 // AJAX handler for AI page translation
1046 add_action('wp_ajax_king_addons_ai_translate_text', [$this, 'handleAiTranslateText']);
1047 }
1048
1049 /**
1050 * Sanitizes AI Settings options.
1051 *
1052 * @param array $input Raw input array.
1053 * @return array Sanitized input.
1054 */
1055 public function sanitizeAiSettings(array $input): array
1056 {
1057 $sanitized = [];
1058
1059 // An absent value resolves to the default rather than being pinned to OpenAI.
1060 $sanitized['ai_provider'] = AI_Provider::normalizeProvider($input['ai_provider'] ?? '');
1061
1062 $sanitized['openai_api_key'] = isset($input['openai_api_key'])
1063 ? sanitize_text_field($input['openai_api_key'])
1064 : '';
1065 $sanitized['openai_model'] = isset($input['openai_model'])
1066 ? sanitize_text_field($input['openai_model'])
1067 : '';
1068 $sanitized['openai_vision_model'] = isset($input['openai_vision_model'])
1069 ? sanitize_text_field($input['openai_vision_model'])
1070 : ($sanitized['openai_model'] ?: 'gpt-4o-mini');
1071 $sanitized['openai_image_model'] = isset($input['openai_image_model'])
1072 ? sanitize_text_field($input['openai_image_model'])
1073 : 'gpt-image-1';
1074
1075 // OpenRouter is configured independently, so switching providers back
1076 // and forth keeps both sets of credentials and models intact.
1077 $sanitized['openrouter_api_key'] = isset($input['openrouter_api_key'])
1078 ? sanitize_text_field($input['openrouter_api_key'])
1079 : '';
1080 $sanitized['openrouter_model'] = isset($input['openrouter_model'])
1081 ? sanitize_text_field($input['openrouter_model'])
1082 : '';
1083 $sanitized['openrouter_vision_model'] = isset($input['openrouter_vision_model'])
1084 ? sanitize_text_field($input['openrouter_vision_model'])
1085 : ($sanitized['openrouter_model'] ?: '');
1086 $sanitized['openrouter_image_model'] = isset($input['openrouter_image_model'])
1087 ? sanitize_text_field($input['openrouter_image_model'])
1088 : '';
1089
1090 // Content language is rendered by this section, so it has to survive the save.
1091 $sanitized['content_language_custom_enable'] = !empty($input['content_language_custom_enable']);
1092 $sanitized['content_language_custom'] = isset($input['content_language_custom'])
1093 ? sanitize_text_field($input['content_language_custom'])
1094 : '';
1095
1096 // Sanitize Daily Token Limit.
1097 if (isset($input['daily_token_limit'])) {
1098 $daily_limit = absint($input['daily_token_limit']);
1099 $sanitized['daily_token_limit'] = max(0, $daily_limit); // Ensure non-negative
1100 } else {
1101 $sanitized['daily_token_limit'] = 1000000; // Default to 1 million tokens if not set
1102 }
1103
1104 // Sanitize Enable AI Buttons option.
1105 $sanitized['enable_ai_buttons'] = !empty($input['enable_ai_buttons']);
1106
1107 // Sanitize Enable AI Image Generation button option.
1108 $sanitized['enable_ai_image_generation_button'] = !empty($input['enable_ai_image_generation_button']);
1109
1110 // Sanitize Enable AI Alt Text Button option.
1111 $sanitized['enable_ai_alt_text_button'] = !empty($input['enable_ai_alt_text_button']);
1112
1113 // Sanitize Enable AI Alt Text Auto Generation option.
1114 $sanitized['enable_ai_alt_text_auto_generation'] = !empty($input['enable_ai_alt_text_auto_generation']);
1115
1116 // Sanitize AI Alt Text Generation Interval.
1117 if (isset($input['ai_alt_text_generation_interval'])) {
1118 $interval = absint($input['ai_alt_text_generation_interval']);
1119 $sanitized['ai_alt_text_generation_interval'] = max(10, min(3600, $interval)); // Between 10 seconds and 1 hour
1120 } else {
1121 $sanitized['ai_alt_text_generation_interval'] = 20; // Default to 20 seconds
1122 }
1123 // Sanitize Image Detail Level
1124 $allowed_detail_levels = ['low', 'high'];
1125 $sanitized['ai_alt_text_image_detail_level'] = in_array(($input['ai_alt_text_image_detail_level'] ?? 'low'), $allowed_detail_levels, true)
1126 ? ($input['ai_alt_text_image_detail_level'] ?? 'low')
1127 : 'low';
1128
1129 // Sanitize Auto Tagging settings.
1130 $sanitized['auto_tagging_max_tags'] = isset($input['auto_tagging_max_tags'])
1131 ? max(1, min(20, absint($input['auto_tagging_max_tags'])))
1132 : 5;
1133 $sanitized['auto_tagging_confidence_threshold'] = isset($input['auto_tagging_confidence_threshold'])
1134 ? max(0.0, min(1.0, (float) $input['auto_tagging_confidence_threshold']))
1135 : 0.75;
1136 $sanitized['auto_tagging_stop_words'] = isset($input['auto_tagging_stop_words'])
1137 ? sanitize_text_field($input['auto_tagging_stop_words'])
1138 : '';
1139
1140 // Sanitize Enable AI Page Translator option
1141 $sanitized['enable_ai_page_translator'] = !empty($input['enable_ai_page_translator']);
1142
1143 // Validation / feedback (Settings API notice).
1144 $ai_requires_key = (
1145 !empty($sanitized['enable_ai_buttons'])
1146 || !empty($sanitized['enable_ai_image_generation_button'])
1147 || !empty($sanitized['enable_ai_alt_text_button'])
1148 || !empty($sanitized['enable_ai_page_translator'])
1149 );
1150
1151 $active_key = ($sanitized['ai_provider'] === AI_Provider::OPENROUTER)
1152 ? $sanitized['openrouter_api_key']
1153 : $sanitized['openai_api_key'];
1154
1155 if ($ai_requires_key && empty($active_key)) {
1156 add_settings_error(
1157 'king_addons_ai',
1158 'king_addons_ai_missing_api_key',
1159 sprintf(
1160 /* translators: %s: provider name */
1161 esc_html__('%s API Key is required to enable AI features.', 'king-addons'),
1162 AI_Provider::getLabel($sanitized['ai_provider'])
1163 ),
1164 'error'
1165 );
1166 }
1167
1168 return $sanitized;
1169 }
1170
1171 /**
1172 * Clears cached AI models list.
1173 *
1174 * @return void
1175 */
1176 public function clearAiModelsCache(): void
1177 {
1178 AI_Provider::clearModelsCache();
1179 }
1180
1181 /**
1182 * Renders the AI Settings page content.
1183 *
1184 * @return void
1185 */
1186 public function showAiSettingsPage(): void
1187 {
1188 if (!current_user_can('manage_options')) {
1189 return;
1190 }
1191 require_once KING_ADDONS_PATH . 'includes/admin/layouts/ai-settings-page.php';
1192 $this->enqueueAiSettingsAssets();
1193 }
1194
1195 /**
1196 * Enqueues scripts and styles for the AI Settings page.
1197 *
1198 * @return void
1199 */
1200 public function enqueueAiSettingsAssets(): void
1201 {
1202 // Enqueue admin base styles first for proper theming
1203 wp_enqueue_style('king-addons-admin', KING_ADDONS_URL . 'includes/admin/css/admin.css', '', KING_ADDONS_VERSION);
1204
1205 wp_enqueue_style(
1206 'king-addons-ai-settings',
1207 KING_ADDONS_URL . 'includes/admin/css/ai-settings.css',
1208 ['king-addons-admin'], // Depend on admin base styles
1209 KING_ADDONS_VERSION
1210 );
1211
1212 wp_enqueue_script(
1213 'king-addons-ai-settings',
1214 KING_ADDONS_URL . 'includes/admin/js/ai-settings.js',
1215 ['jquery'],
1216 KING_ADDONS_VERSION,
1217 true
1218 );
1219
1220 wp_localize_script(
1221 'king-addons-ai-settings',
1222 'KingAddonsAiSettings',
1223 [
1224 'ajax_url' => admin_url('admin-ajax.php'),
1225 'nonce' => wp_create_nonce('king_addons_ai_refresh_models_nonce'),
1226 'refreshing_text' => esc_html__('Refreshing...', 'king-addons'),
1227 'refreshed_text' => esc_html__('List updated.', 'king-addons'),
1228 'error_text' => esc_html__('Error updating list.', 'king-addons'),
1229 'testing_text' => esc_html__('Testing connection...', 'king-addons'),
1230 'test_failed_text' => esc_html__('Connection failed.', 'king-addons'),
1231 'free_models_label' => esc_html__('Free models', 'king-addons'),
1232 'paid_models_label' => esc_html__('Paid models', 'king-addons'),
1233 ]
1234 );
1235 }
1236
1237 /**
1238 * Renders description for the AI Provider Settings section.
1239 *
1240 * @return void
1241 */
1242 public function renderAiOpenaiSection(): void
1243 {
1244 echo '<p>' . esc_html__('Choose an AI provider, enter its API key and select the models used by the AI features.', 'king-addons') . '</p>';
1245 }
1246
1247 /**
1248 * Renders the AI provider selector.
1249 *
1250 * @return void
1251 */
1252 public function renderAiProviderField(): void
1253 {
1254 $selected = AI_Provider::getProvider();
1255
1256 echo '<select name="king_addons_ai_options[ai_provider]" id="king-addons-ai-provider">';
1257 foreach (AI_Provider::getProviders() as $provider => $label) {
1258 printf(
1259 '<option value="%s" %s>%s</option>',
1260 esc_attr($provider),
1261 selected($selected, $provider, false),
1262 esc_html($label)
1263 );
1264 }
1265 echo '</select>';
1266
1267 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>';
1268 }
1269
1270 /**
1271 * Renders the OpenRouter API Key input field.
1272 *
1273 * @return void
1274 */
1275 public function renderAiOpenRouterApiKeyField(): void
1276 {
1277 $options = get_option('king_addons_ai_options', []);
1278 $api_key = $options['openrouter_api_key'] ?? '';
1279 printf(
1280 '<input type="password" name="king_addons_ai_options[openrouter_api_key]" id="king-addons-openrouter-api-key" value="%s" class="regular-text" autocomplete="off" />',
1281 esc_attr($api_key)
1282 );
1283 echo '<p class="description">';
1284 printf(
1285 /* translators: %1$s: opening link tag, %2$s: closing link tag */
1286 esc_html__('Get your API key from %1$sOpenRouter Keys%2$s. Saving the key will attempt to fetch the available models.', 'king-addons'),
1287 '<a href="https://openrouter.ai/keys" target="_blank" rel="noopener noreferrer">',
1288 '</a>'
1289 );
1290 echo '</p>';
1291 echo '<div class="ka-ai-notice ka-ai-notice-info">';
1292 echo '<strong>' . esc_html__('Info:', 'king-addons') . '</strong> ';
1293 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');
1294 echo '</div>';
1295 echo '<div class="ka-ai-notice ka-ai-notice-info">';
1296 echo '<strong class="ka-ai-notice-title">' . esc_html__('Useful OpenRouter Links:', 'king-addons') . '</strong>';
1297 echo '<ul class="ka-ai-links-list">';
1298 $links = [
1299 'Models & Pricing' => 'https://openrouter.ai/models',
1300 'API Keys' => 'https://openrouter.ai/keys',
1301 'Activity Dashboard' => 'https://openrouter.ai/activity',
1302 'Credits' => 'https://openrouter.ai/settings/credits',
1303 'Limits' => 'https://openrouter.ai/docs/api-reference/limits',
1304 ];
1305 foreach ($links as $label => $url) {
1306 printf(
1307 '<li><a href="%s" target="_blank" rel="noopener noreferrer">%s</a></li>',
1308 esc_url($url),
1309 esc_html($label)
1310 );
1311 }
1312 echo '</ul></div>';
1313 }
1314
1315 /**
1316 * Renders the "Test Connection" control.
1317 *
1318 * @return void
1319 */
1320 public function renderAiConnectionTestField(): void
1321 {
1322 echo '<button type="button" id="king-addons-ai-test-connection-button" class="button button-secondary">' . esc_html__('Test Connection', 'king-addons') . '</button>';
1323 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>';
1324 echo '<span class="spinner" id="king-addons-ai-test-connection-spinner" style="float:none; vertical-align:middle;"></span>';
1325 echo '<span class="spinner" id="king-addons-ai-refresh-models-spinner" style="float:none; vertical-align:middle;"></span>';
1326 echo '<span id="king-addons-ai-test-connection-status" style="margin-left:5px; vertical-align:middle;"></span>';
1327 echo '<span id="king-addons-ai-refresh-models-status" style="margin-left:5px; vertical-align:middle;"></span>';
1328 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>';
1329 }
1330
1331 /**
1332 * Renders the OpenRouter text model dropdown.
1333 *
1334 * @return void
1335 */
1336 public function renderAiOpenRouterModelField(): void
1337 {
1338 $selected = AI_Provider::getModel('text', AI_Provider::OPENROUTER);
1339 AI_Provider::renderModelSelect(
1340 'king_addons_ai_options[openrouter_model]',
1341 $selected,
1342 AI_Provider::getModelsFor('text', AI_Provider::OPENROUTER),
1343 ['class' => 'ka-ai-model-select', 'data-ka-model-type' => 'text']
1344 );
1345 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>';
1346 }
1347
1348 /**
1349 * Renders the OpenRouter vision model dropdown.
1350 *
1351 * @return void
1352 */
1353 public function renderAiOpenRouterVisionModelField(): void
1354 {
1355 $selected = AI_Provider::getModel('vision', AI_Provider::OPENROUTER);
1356 AI_Provider::renderModelSelect(
1357 'king_addons_ai_options[openrouter_vision_model]',
1358 $selected,
1359 AI_Provider::getModelsFor('vision', AI_Provider::OPENROUTER),
1360 ['class' => 'ka-ai-model-select', 'data-ka-model-type' => 'vision']
1361 );
1362 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>';
1363 }
1364
1365 /**
1366 * Renders the OpenRouter image generation model dropdown.
1367 *
1368 * @return void
1369 */
1370 public function renderAiOpenRouterImageModelField(): void
1371 {
1372 $selected = AI_Provider::getModel('image', AI_Provider::OPENROUTER);
1373 AI_Provider::renderModelSelect(
1374 'king_addons_ai_options[openrouter_image_model]',
1375 $selected,
1376 AI_Provider::getModelsFor('image', AI_Provider::OPENROUTER),
1377 ['class' => 'ka-ai-model-select', 'data-ka-model-type' => 'image']
1378 );
1379 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>';
1380 }
1381
1382 /**
1383 * Renders the OpenAI API Key input field.
1384 *
1385 * @return void
1386 */
1387 public function renderAiApiKeyField(): void
1388 {
1389 $options = get_option('king_addons_ai_options', []);
1390 $api_key = $options['openai_api_key'] ?? '';
1391 printf(
1392 '<input type="password" name="king_addons_ai_options[openai_api_key]" value="%s" class="regular-text" autocomplete="off" />',
1393 esc_attr($api_key)
1394 );
1395 echo '<p class="description">';
1396 printf(
1397 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'),
1398 '<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">',
1399 '</a>'
1400 );
1401 echo '</p>';
1402 echo '<div class="ka-ai-notice ka-ai-notice-warning">';
1403 echo '<strong>' . esc_html__('Important:', 'king-addons') . '</strong> ';
1404 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');
1405 echo '</div>';
1406 echo '<div class="ka-ai-notice ka-ai-notice-info">';
1407 echo '<strong>' . esc_html__('Info:', 'king-addons') . '</strong> ';
1408 echo esc_html__('With GPT-4o-mini, a $5 balance is enough for roughly 130,000–150,000 text generations.', 'king-addons');
1409 echo '</div>';
1410 echo '<div class="ka-ai-notice ka-ai-notice-info">';
1411 echo '<strong class="ka-ai-notice-title">' . esc_html__('Useful OpenAI Links:', 'king-addons') . '</strong>';
1412 echo '<ul class="ka-ai-links-list">';
1413 $links = [
1414 'API Pricing' => 'https://openai.com/api/pricing/',
1415 'API Keys' => 'https://platform.openai.com/api-keys',
1416 'Usage Dashboard' => 'https://platform.openai.com/account/usage',
1417 'Billing Overview' => 'https://platform.openai.com/account/billing/overview',
1418 'Rate Limits' => 'https://openai.com/pricing#rate-limits',
1419 ];
1420 foreach ($links as $label => $url) {
1421 printf(
1422 '<li><a href="%s" target="_blank" rel="noopener noreferrer">%s</a></li>',
1423 esc_url($url),
1424 esc_html($label)
1425 );
1426 }
1427 echo '</ul></div>';
1428 }
1429
1430 /**
1431 * Renders the model selection dropdown field with refresh button.
1432 *
1433 * @return void
1434 */
1435 public function renderAiModelField(): void
1436 {
1437 $selected = AI_Provider::getModel('text', AI_Provider::OPENAI);
1438 AI_Provider::renderModelSelect(
1439 'king_addons_ai_options[openai_model]',
1440 $selected,
1441 AI_Provider::getModelsFor('text', AI_Provider::OPENAI),
1442 ['class' => 'ka-ai-model-select', 'data-ka-model-type' => 'text']
1443 );
1444 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>';
1445 }
1446
1447 /**
1448 * Renders the vision model selection dropdown field.
1449 *
1450 * @return void
1451 */
1452 public function renderAiVisionModelField(): void
1453 {
1454 $options = get_option('king_addons_ai_options', []);
1455 $selected = $options['openai_vision_model'] ?? ($options['openai_model'] ?? 'gpt-4o-mini');
1456 AI_Provider::renderModelSelect(
1457 'king_addons_ai_options[openai_vision_model]',
1458 (string) $selected,
1459 AI_Provider::getModelsFor('vision', AI_Provider::OPENAI),
1460 ['class' => 'ka-ai-model-select', 'data-ka-model-type' => 'vision']
1461 );
1462 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>';
1463 }
1464
1465 /**
1466 * Builds the model lists the settings page needs, keyed by purpose.
1467 *
1468 * @param string $provider Provider slug.
1469 * @return array<string, array<int, array<string, mixed>>>
1470 */
1471 private function getAiModelPayload(string $provider): array
1472 {
1473 return [
1474 'text' => AI_Provider::getModelsFor('text', $provider),
1475 'vision' => AI_Provider::getModelsFor('vision', $provider),
1476 'image' => AI_Provider::getModelsFor('image', $provider),
1477 ];
1478 }
1479
1480 /**
1481 * Handles AJAX request to refresh the model list of the selected provider.
1482 *
1483 * @return void
1484 */
1485 public function handleAiRefreshModels(): void
1486 {
1487 check_ajax_referer('king_addons_ai_refresh_models_nonce', 'nonce');
1488 if (!current_user_can('manage_options')) {
1489 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1490 }
1491
1492 $provider = AI_Provider::normalizeProvider(
1493 isset($_POST['provider']) ? sanitize_text_field(wp_unslash($_POST['provider'])) : AI_Provider::getProvider()
1494 );
1495
1496 // Allow refreshing against a key that has been typed but not saved yet.
1497 $posted_key = isset($_POST['api_key']) ? trim(sanitize_text_field(wp_unslash($_POST['api_key']))) : '';
1498 $api_key = ($posted_key !== '') ? $posted_key : AI_Provider::getApiKey($provider);
1499
1500 // OpenRouter publishes its catalogue without authentication.
1501 if ($api_key === '' && $provider !== AI_Provider::OPENROUTER) {
1502 wp_send_json_error(['message' => esc_html__('API key is not set.', 'king-addons')], 400);
1503 }
1504
1505 $models = AI_Provider::fetchModels($provider, $api_key);
1506 if (is_wp_error($models)) {
1507 wp_send_json_error(['message' => $models->get_error_message()], 500);
1508 }
1509
1510 set_transient(
1511 AI_Provider::getCacheKey($provider),
1512 $models,
1513 ($provider === AI_Provider::OPENROUTER) ? DAY_IN_SECONDS : 0
1514 );
1515
1516 wp_send_json_success([
1517 'provider' => $provider,
1518 'models' => $this->getAiModelPayload($provider),
1519 ]);
1520 }
1521
1522 /**
1523 * Handles AJAX request to verify the provider connection.
1524 *
1525 * @return void
1526 */
1527 public function handleAiTestConnection(): void
1528 {
1529 check_ajax_referer('king_addons_ai_refresh_models_nonce', 'nonce');
1530 if (!current_user_can('manage_options')) {
1531 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1532 }
1533
1534 $provider = AI_Provider::normalizeProvider(
1535 isset($_POST['provider']) ? sanitize_text_field(wp_unslash($_POST['provider'])) : AI_Provider::getProvider()
1536 );
1537
1538 // Test what is in the field right now, so a key can be verified before
1539 // it is saved. An empty field falls back to the stored key.
1540 $posted_key = isset($_POST['api_key']) ? trim(sanitize_text_field(wp_unslash($_POST['api_key']))) : '';
1541 $stored_key = AI_Provider::getApiKey($provider);
1542 $api_key = ($posted_key !== '') ? $posted_key : $stored_key;
1543 $unsaved = ($posted_key !== '' && $posted_key !== $stored_key);
1544
1545 $result = AI_Provider::testConnection($provider, $api_key);
1546 if (is_wp_error($result)) {
1547 wp_send_json_error(['message' => $result->get_error_message()], 400);
1548 }
1549
1550 if ($unsaved) {
1551 $result .= ' ' . esc_html__('Save the settings to start using it.', 'king-addons');
1552 }
1553
1554 wp_send_json_success(['message' => $result, 'unsaved' => $unsaved]);
1555 }
1556
1557 /**
1558 * AJAX handler to generate text using the configured AI provider.
1559 *
1560 * @return void
1561 */
1562 public function handleAiGenerateText(): void
1563 {
1564 check_ajax_referer('king_addons_ai_generate_nonce', 'nonce');
1565 if (!current_user_can('manage_options')) {
1566 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1567 }
1568 $field_name = sanitize_text_field($_POST['field_name'] ?? '');
1569 // Accept 'prompt' parameter (new) but fall back to 'value' parameter (old) for backwards compatibility
1570 $prompt = isset($_POST['prompt'])
1571 ? sanitize_textarea_field($_POST['prompt'])
1572 : sanitize_textarea_field($_POST['value'] ?? '');
1573
1574 // Get editor type if provided
1575 $editor_type = sanitize_text_field($_POST['editor_type'] ?? 'text');
1576
1577 $options = get_option('king_addons_ai_options', []);
1578 $api_key = AI_Provider::getApiKey();
1579 $model = AI_Provider::getTextModel();
1580
1581 if (empty($api_key) || empty($model)) {
1582 wp_send_json_error(['message' => esc_html__('API key or model not set.', 'king-addons')], 400);
1583 }
1584
1585 if (empty($prompt)) {
1586 wp_send_json_error(['message' => esc_html__('Please provide a prompt.', 'king-addons')], 400);
1587 }
1588
1589 // Check daily token limit
1590 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1591 $current_usage = $this->getAiDailyUsage();
1592
1593 if ($daily_limit > 0 && $current_usage >= $daily_limit) {
1594 wp_send_json_error([
1595 'message' => esc_html__('Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.', 'king-addons'),
1596 // This is the plugin's own cap, not the provider's, so the
1597 // client must not retry it or blame the AI provider.
1598 'code' => 'local_limit',
1599 'retryable' => false,
1600 ], 429);
1601 }
1602
1603 // System instruction based on editor type
1604 $system_instruction = 'You are a helpful content assistant. Provide concise, well-written content based on the user\'s request.';
1605
1606 // Enhanced instruction for WYSIWYG editor
1607 if ($editor_type === 'wysiwyg') {
1608 $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.';
1609 }
1610
1611 // Prepare request to the provider's Chat Completions endpoint
1612 $messages = [
1613 ['role' => 'system', 'content' => $system_instruction],
1614 ['role' => 'user', 'content' => $prompt]
1615 ];
1616
1617 // Add format instruction for WYSIWYG
1618 if ($editor_type === 'wysiwyg') {
1619 $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.";
1620 }
1621
1622 $response = wp_remote_post(
1623 AI_Provider::getChatEndpoint(),
1624 [
1625 'headers' => AI_Provider::getHeaders(),
1626 'body' => wp_json_encode(AI_Provider::prepareChatPayload([
1627 'model' => $model,
1628 'messages' => $messages,
1629 'max_tokens' => 500,
1630 'temperature' => 0.7, // Slight creativity for better content
1631 ])),
1632 'timeout' => 30,
1633 ]
1634 );
1635
1636 if (is_wp_error($response)) {
1637 wp_send_json_error(['message' => $response->get_error_message()], 500);
1638 }
1639
1640 $code = wp_remote_retrieve_response_code($response);
1641 $data = json_decode(wp_remote_retrieve_body($response), true);
1642
1643 if ($code !== 200 || empty($data['choices'][0]['message']['content'])) {
1644 $error_msg = AI_Provider::extractErrorMessage($data, esc_html__('AI API error.', 'king-addons'));
1645 wp_send_json_error(['message' => $error_msg], 500);
1646 }
1647
1648 $generated = trim($data['choices'][0]['message']['content']);
1649
1650 // Clean up any code fence markers for WYSIWYG editor
1651 if ($editor_type === 'wysiwyg') {
1652 // Remove code fence markers (```html and ```) that might be returned by AI
1653 $generated = preg_replace('/^```(?:html|HTML)?\s*/', '', $generated);
1654 $generated = preg_replace('/```\s*$/', '', $generated);
1655 }
1656
1657 // Update token usage statistics if present in the response
1658 if (isset($data['usage']['total_tokens'])) {
1659 $this->incrementAiDailyUsage(intval($data['usage']['total_tokens']));
1660 }
1661
1662 wp_send_json_success([
1663 'text' => $generated,
1664 'usage' => [
1665 'tokens_used' => $data['usage']['total_tokens'] ?? 0,
1666 'daily_used' => $this->getAiDailyUsage(),
1667 'daily_limit' => $daily_limit,
1668 ]
1669 ]);
1670 }
1671
1672 /**
1673 * AJAX handler to change text using AI based on user prompt and original text.
1674 *
1675 * @return void
1676 */
1677 public function handleAiChangeText(): void
1678 {
1679 check_ajax_referer('king_addons_ai_change_nonce', 'nonce');
1680 if (!current_user_can('manage_options')) {
1681 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1682 }
1683 $field_name = isset($_POST['field_name']) ? sanitize_text_field(wp_unslash($_POST['field_name'])) : '';
1684 $prompt = isset($_POST['prompt']) ? sanitize_text_field(wp_unslash($_POST['prompt'])) : '';
1685 $original = isset($_POST['original']) ? wp_kses_post(wp_unslash($_POST['original'])) : '';
1686 $instruction_context = isset($_POST['instruction_context']) ? sanitize_textarea_field(wp_unslash($_POST['instruction_context'])) : '';
1687
1688 $options = get_option('king_addons_ai_options', []);
1689 $api_key = AI_Provider::getApiKey();
1690 $model = AI_Provider::getTextModel();
1691
1692 if (empty($api_key) || empty($model) || empty($prompt) || empty($original)) {
1693 wp_send_json_error(['message' => esc_html__('Missing data for AI change.', 'king-addons')], 400);
1694 }
1695
1696 // Check daily token limit
1697 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1698 $current_usage = $this->getAiDailyUsage();
1699
1700 if ($daily_limit > 0 && $current_usage >= $daily_limit) {
1701 wp_send_json_error([
1702 'message' => esc_html__('Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.', 'king-addons'),
1703 // This is the plugin's own cap, not the provider's, so the
1704 // client must not retry it or blame the AI provider.
1705 'code' => 'local_limit',
1706 'retryable' => false,
1707 ], 429);
1708 }
1709
1710 // Default instruction if none provided
1711 if (empty($instruction_context)) {
1712 $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.';
1713
1714 // Add specific formatting instructions for WYSIWYG editor
1715 $editor_type = isset($_POST['editor_type']) ? sanitize_text_field(wp_unslash($_POST['editor_type'])) : 'text';
1716 if ($editor_type === 'wysiwyg') {
1717 $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.';
1718 }
1719 }
1720
1721 // Analyze if the prompt is likely requesting to add content rather than replace
1722 $add_content_keywords = [
1723 // English
1724 'add',
1725 'insert',
1726 'extend',
1727 'append',
1728 'more',
1729 'additional',
1730 'expand',
1731 // Russian
1732 'добавь',
1733 'вставь',
1734 'расширь',
1735 // Spanish
1736 'añadir',
1737 'agregar',
1738 'insertar',
1739 'adjuntar',
1740 'extender',
1741 // French
1742 'ajouter',
1743 'insérer',
1744 'étendre',
1745 'annexer',
1746 'joindre',
1747 // German
1748 'hinzufügen',
1749 'einfügen',
1750 'erweitern',
1751 'anhängen',
1752 'ergänzen',
1753 // Italian
1754 'aggiungere',
1755 'inserire',
1756 'allegare',
1757 'estendere',
1758 'appendere',
1759 // Portuguese
1760 'adicionar',
1761 'inserir',
1762 'acrescentar',
1763 'anexar',
1764 'estender',
1765 // Polish
1766 'dodać',
1767 'wstawić',
1768 'doł�
1769 czyć',
1770 'rozszerzyć',
1771 'zał�
1772 czyć'
1773 ];
1774
1775 // Also look for numeric patterns like "add 2 paragraphs" or "добавь 3 абзаца"
1776 // Enhanced pattern to find numeric paragraph requests in different languages
1777 $numeric_pattern = '/(?:' .
1778 // English verbs
1779 'add|append|insert|create|write|' .
1780 // Russian verbs
1781 'добавь|вставь|создай|напиши|' .
1782 // Spanish verbs
1783 'añadir|agregar|insertar|crear|escribir|' .
1784 // French verbs
1785 'ajouter|insérer|créer|écrire|' .
1786 // German verbs
1787 'hinzufügen|einfügen|erstellen|schreiben|' .
1788 // Italian verbs
1789 'aggiungere|inserire|creare|scrivere|' .
1790 // Portuguese verbs
1791 'adicionar|inserir|criar|escrever|' .
1792 // Polish verbs
1793 'dodać|wstawić|utworzyć|napisać' .
1794 ')\s+(\d+|' .
1795 // English quantifiers
1796 'several|few|couple|some|' .
1797 // Russian quantifiers
1798 'несколько|пару|еще|ещё|' .
1799 // Spanish quantifiers
1800 'varios|algunos|un par|unos|' .
1801 // French quantifiers
1802 'plusieurs|quelques|une paire|certains|' .
1803 // German quantifiers
1804 'mehrere|einige|ein paar|manche|' .
1805 // Italian quantifiers
1806 'diversi|alcuni|un paio|qualche|' .
1807 // Portuguese quantifiers
1808 'vários|alguns|um par|' .
1809 // Polish quantifiers
1810 'kilka|parę|pare|niektóre' .
1811 ')\s+(?:' .
1812 // English nouns
1813 'paragraph|paragraphs|section|sections|content|text|' .
1814 // Russian nouns
1815 'абзац|абзаца|абзацев|раздел|разделы|текст|контент|параграф|параграфа|параграфов|' .
1816 // Spanish nouns
1817 'párrafo|párrafos|sección|secciones|contenido|texto|' .
1818 // French nouns
1819 'paragraphe|paragraphes|section|sections|contenu|texte|' .
1820 // German nouns
1821 'absatz|absätze|abschnitt|abschnitte|inhalt|text|' .
1822 // Italian nouns
1823 'paragrafo|paragrafi|sezione|sezioni|contenuto|testo|' .
1824 // Portuguese nouns
1825 'parágrafo|parágrafos|seção|seções|conteúdo|texto|' .
1826 // Polish nouns
1827 'akapit|akapity|sekcja|sekcje|treść|tekst' .
1828 ')/i';
1829 $contains_add_keyword = false;
1830 $numeric_match = [];
1831 $requested_paragraphs = 0;
1832
1833 // First check for specific numeric requests
1834 if (preg_match($numeric_pattern, $prompt, $numeric_match)) {
1835 $contains_add_keyword = true;
1836 $number_text = $numeric_match[1] ?? '';
1837
1838 // Convert text numbers to digits
1839 if (is_numeric($number_text)) {
1840 $requested_paragraphs = (int) $number_text;
1841 } else {
1842 // For words like "several", "few", "couple", etc.
1843 switch (strtolower($number_text)) {
1844 // Words meaning approximately "2"
1845 case 'couple':
1846 case 'пару': // Russian
1847 case 'пара': // Russian
1848 case 'un par': // Spanish
1849 case 'une paire': // French
1850 case 'ein paar': // German
1851 case 'un paio': // Italian
1852 case 'um par': // Portuguese
1853 case 'parę': // Polish
1854 case 'pare': // Polish
1855 $requested_paragraphs = 2;
1856 break;
1857
1858 // Words meaning approximately "3-4" (several/few)
1859 case 'few':
1860 case 'several':
1861 case 'some':
1862 case 'несколько': // Russian
1863 case 'еще': // Russian
1864 case 'ещё': // Russian
1865 case 'varios': // Spanish
1866 case 'algunos': // Spanish
1867 case 'unos': // Spanish
1868 case 'plusieurs': // French
1869 case 'quelques': // French
1870 case 'certains': // French
1871 case 'mehrere': // German
1872 case 'einige': // German
1873 case 'manche': // German
1874 case 'diversi': // Italian
1875 case 'alcuni': // Italian
1876 case 'qualche': // Italian
1877 case 'vários': // Portuguese
1878 case 'alguns': // Portuguese
1879 case 'kilka': // Polish
1880 case 'niektóre': // Polish
1881 default:
1882 $requested_paragraphs = 3; // Default "several" = 3
1883 break;
1884 }
1885 }
1886 } else {
1887 // Then check for general add keywords
1888 foreach ($add_content_keywords as $keyword) {
1889 if (stripos($prompt, $keyword) !== false) {
1890 $contains_add_keyword = true;
1891 $requested_paragraphs = 2; // Default to 2 paragraphs if just "add paragraphs"
1892 break;
1893 }
1894 }
1895 }
1896
1897 // Build the system message dynamically based on the prompt analysis
1898 $system_message = $instruction_context;
1899 if ($contains_add_keyword) {
1900 if ($requested_paragraphs > 0) {
1901 // Request only new paragraphs, without modifying original content
1902 $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.';
1903 $system_message .= sprintf(
1904 ' 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.',
1905 $requested_paragraphs
1906 );
1907 } else {
1908 $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.';
1909 }
1910 }
1911
1912 $body = [
1913 'model' => $model,
1914 'messages' => [
1915 [
1916 'role' => 'system',
1917 'content' => $system_message,
1918 ],
1919 [
1920 'role' => 'user',
1921 'content' => ($contains_add_keyword && $requested_paragraphs > 0)
1922 ? sprintf(
1923 "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.",
1924 $original,
1925 $requested_paragraphs
1926 )
1927 : sprintf(
1928 /* translators: %1$s: User's instruction prompt, %2$s: Original text to modify. */
1929 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'),
1930 $prompt,
1931 $original
1932 ),
1933 ],
1934 ],
1935 'max_tokens' => 10000, // Increased to allow for more content
1936 'temperature' => 0.7, // Slightly more creative
1937 ];
1938
1939 // Set append_mode flag for paragraph additions
1940 $append_mode = ($contains_add_keyword && $requested_paragraphs > 0);
1941
1942 // Modify request for WYSIWYG editor
1943 $editor_type = isset($_POST['editor_type']) ? sanitize_text_field(wp_unslash($_POST['editor_type'])) : 'text';
1944 if ($editor_type === 'wysiwyg') {
1945 // Add a specific instruction for formatting
1946 $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.';
1947
1948 if (!$append_mode) {
1949 // Only add this for non-append mode
1950 $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.";
1951 }
1952
1953 // Increase temperature for WYSIWYG to be more creative when creating paragraphs
1954 $body['temperature'] = 0.8;
1955
1956 // Increase max_tokens for longer responses with multiple paragraphs
1957 $body['max_tokens'] = 15000;
1958 }
1959
1960 $response = wp_remote_post(
1961 AI_Provider::getChatEndpoint(),
1962 [
1963 'headers' => AI_Provider::getHeaders(),
1964 'body' => wp_json_encode(AI_Provider::prepareChatPayload($body)),
1965 'timeout' => 30,
1966 ]
1967 );
1968
1969 if (is_wp_error($response)) {
1970 wp_send_json_error(['message' => $response->get_error_message()], 500);
1971 }
1972
1973 $code = wp_remote_retrieve_response_code($response);
1974 $data = json_decode(wp_remote_retrieve_body($response), true);
1975
1976 if ($code !== 200 || empty($data['choices'][0]['message']['content'])) {
1977 $error_msg = AI_Provider::extractErrorMessage($data, esc_html__('AI change error.', 'king-addons'));
1978 wp_send_json_error(['message' => $error_msg], 500);
1979 }
1980
1981 $changed = trim($data['choices'][0]['message']['content']);
1982
1983 // Clean up any code fence markers for WYSIWYG editor
1984 if ($editor_type === 'wysiwyg') {
1985 // Remove code fence markers (```html and ```) that might be returned by AI
1986 $changed = preg_replace('/^```(?:html|HTML)?\s*/', '', $changed);
1987 $changed = preg_replace('/```\s*$/', '', $changed);
1988 }
1989
1990 // Update token usage statistics if present in the response
1991 if (isset($data['usage']['total_tokens'])) {
1992 $this->incrementAiDailyUsage(intval($data['usage']['total_tokens']));
1993 }
1994
1995 // Send response with append mode flag
1996 wp_send_json_success([
1997 'text' => $changed,
1998 'append_mode' => $append_mode,
1999 'original' => $append_mode ? $original : '',
2000 'usage' => [
2001 'tokens_used' => $data['usage']['total_tokens'] ?? 0,
2002 'daily_used' => $this->getAiDailyUsage(),
2003 'daily_limit' => $daily_limit,
2004 ]
2005 ]);
2006 }
2007
2008 /**
2009 * Renders Usage Quota Settings section.
2010 *
2011 * @return void
2012 */
2013 public function renderAiQuotaSection(): void
2014 {
2015 echo '<p>' . esc_html__('Set the daily token limit for AI features.', 'king-addons') . '</p>';
2016 }
2017
2018 /**
2019 * Renders the Daily Token Limit input field.
2020 *
2021 * @return void
2022 */
2023 public function renderAiDailyLimitField(): void
2024 {
2025 $options = get_option('king_addons_ai_options', []);
2026 $daily_token_limit = $options['daily_token_limit'] ?? self::DEFAULT_DAILY_TOKEN_LIMIT;
2027
2028 echo '<div class="daily-token-limit-wrap">';
2029 printf(
2030 '<input type="number" name="king_addons_ai_options[daily_token_limit]" value="%s" class="regular-text" min="0" step="1000" style="margin-right: 10px;" />',
2031 esc_attr($daily_token_limit)
2032 );
2033 echo '<span>' . esc_html__('tokens', 'king-addons') . '</span>';
2034 echo '</div>';
2035
2036 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>';
2037
2038 echo '<div class="king-addons-info-box">';
2039 echo '<p><strong>' . esc_html__('About tokens:', 'king-addons') . '</strong> ' .
2040 esc_html__('Tokens are the basic unit of text that the AI processes. As a rough guide:', 'king-addons') . '</p>';
2041 echo '<p>• ' . esc_html__('1 token ≈ 4 characters or 0.75 words in English', 'king-addons') . '</p>';
2042 echo '<p>• ' . esc_html__('A typical paragraph might use around 50-100 tokens', 'king-addons') . '</p>';
2043 echo '<p>• ' . esc_html__('A full page of text (500 words) is approximately 750 tokens', 'king_addons') . '</p>';
2044 echo '<p>• ' . esc_html__('Recommended daily limit: 10,000 - 50,000 tokens for moderate use', 'king-addons') . '</p>';
2045 echo '</div>';
2046 }
2047
2048 /**
2049 * Default daily token limit if not explicitly set.
2050 *
2051 * @var int
2052 */
2053 private const DEFAULT_DAILY_TOKEN_LIMIT = 1000000;
2054
2055 /**
2056 * Gets the current daily token usage.
2057 *
2058 * @return int Number of tokens used today.
2059 */
2060 private function getAiDailyUsage(): int
2061 {
2062 $usage_data = get_option('king_addons_ai_daily_usage', ['date' => '', 'count' => 0]);
2063 $today = current_time('Y-m-d');
2064 if (!isset($usage_data['date']) || $usage_data['date'] !== $today) {
2065 return 0;
2066 }
2067 return intval($usage_data['count']);
2068 }
2069
2070 /**
2071 * Increments the daily token usage count.
2072 *
2073 * @param int $tokens Number of tokens to add.
2074 * @return void
2075 */
2076 public function incrementAiDailyUsage(int $tokens): void
2077 {
2078 $today = current_time('Y-m-d');
2079 $usage_data = get_option('king_addons_ai_daily_usage', ['date' => '', 'count' => 0]);
2080 if (!isset($usage_data['date']) || $usage_data['date'] !== $today) {
2081 $usage_data = [
2082 'date' => $today,
2083 'count' => 0,
2084 ];
2085 }
2086 $usage_data['count'] = intval($usage_data['count']) + $tokens;
2087 update_option('king_addons_ai_daily_usage', $usage_data, false);
2088 }
2089
2090 /**
2091 * Renders Usage Statistics section.
2092 *
2093 * @return void
2094 */
2095 public function renderAiStatsSection(): void
2096 {
2097 $usage_data = get_option('king_addons_ai_daily_usage', ['date' => '', 'count' => 0]);
2098 $today = current_time('Y-m-d');
2099 $used = (isset($usage_data['date']) && $usage_data['date'] === $today) ? intval($usage_data['count']) : 0;
2100
2101 $options = get_option('king_addons_ai_options', []);
2102 $limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
2103
2104 if ($limit > 0) {
2105 $limit_display = number_format_i18n($limit);
2106 $remaining = max(0, $limit - $used);
2107 $remaining_display = number_format_i18n($remaining);
2108
2109 $usage_percentage = ($limit > 0) ? min(100, round(($used / $limit) * 100)) : 0;
2110
2111 echo '<div class="king-addons-ai-usage-stats">';
2112 echo '<table class="form-table">';
2113 echo '<tr>';
2114 echo '<th>' . esc_html__('Tokens Used Today', 'king-addons') . '</th>';
2115 echo '<td><strong>' . esc_html(number_format_i18n($used)) . '</strong></td>';
2116 echo '</tr>';
2117 echo '<tr>';
2118 echo '<th>' . esc_html__('Daily Limit', 'king-addons') . '</th>';
2119 echo '<td>' . esc_html($limit_display) . '</td>';
2120 echo '</tr>';
2121 echo '<tr>';
2122 echo '<th>' . esc_html__('Remaining', 'king-addons') . '</th>';
2123 echo '<td>' . esc_html($remaining_display) . '</td>';
2124 echo '</tr>';
2125 echo '</table>';
2126
2127 // Add progress bar
2128 echo '<div class="king-addons-ai-usage-bar-container" style="background-color: #f0f0f0; height: 20px; border-radius: 10px; margin: 15px 0; overflow: hidden;">';
2129 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>';
2130 echo '</div>';
2131 echo '<p class="description">' . esc_html(sprintf(__('Usage: %d%%', 'king-addons'), $usage_percentage)) . '</p>';
2132 echo '</div>';
2133 } else {
2134 echo '<p>' . esc_html__('No daily token limit is set. All requests will be processed.', 'king-addons') . '</p>';
2135 echo '<p><strong>' . esc_html__('Tokens used today:', 'king-addons') . ' ' . esc_html(number_format_i18n($used)) . '</strong></p>';
2136 }
2137 }
2138
2139 /**
2140 * AJAX handler to check token usage limits.
2141 *
2142 * @return void
2143 */
2144 public function handleAiCheckTokens(): void
2145 {
2146 check_ajax_referer('king_addons_ai_generate_nonce', 'nonce');
2147 if (!current_user_can('manage_options')) {
2148 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
2149 }
2150
2151 $options = get_option('king_addons_ai_options', []);
2152 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
2153 $daily_used = $this->getAiDailyUsage();
2154 // Check if API key and model are set
2155 $api_key = AI_Provider::getApiKey();
2156 $model = AI_Provider::getTextModel();
2157 $api_key_valid = !empty($api_key) && !empty($model);
2158
2159 wp_send_json_success([
2160 'daily_used' => $daily_used,
2161 'daily_limit' => $daily_limit,
2162 'limit_reached' => ($daily_limit > 0 && $daily_used >= $daily_limit),
2163 'api_key_valid' => $api_key_valid,
2164 ]);
2165 }
2166
2167 /**
2168 * AJAX handler to check image generation limits.
2169 *
2170 * @return void
2171 */
2172 public function handleAiImageCheckLimits(): void
2173 {
2174 check_ajax_referer('king_addons_ai_generate_image_nonce', 'nonce');
2175 if (!current_user_can('manage_options')) {
2176 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
2177 }
2178
2179 $options = get_option('king_addons_ai_options', []);
2180 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
2181 $daily_used = $this->getAiDailyUsage();
2182 // Check if API key and model are set
2183 $api_key = AI_Provider::getApiKey();
2184 $model = AI_Provider::getTextModel();
2185 $api_key_valid = !empty($api_key) && !empty($model);
2186
2187 wp_send_json_success([
2188 'daily_used' => $daily_used,
2189 'daily_limit' => $daily_limit,
2190 'limit_reached' => ($daily_limit > 0 && $daily_used >= $daily_limit),
2191 'api_key_valid' => $api_key_valid,
2192 ]);
2193
2194 }
2195
2196
2197 /**
2198 * Renders the Editor Integration section description.
2199 *
2200 * @return void
2201 */
2202 public function renderAiEditorSection(): void
2203 {
2204 echo '<p>' . esc_html__('Control the integration of AI features in the Elementor editor.', 'king-addons') . '</p>';
2205 }
2206
2207 /**
2208 * Renders description for Alt Text Settings section.
2209 *
2210 * @return void
2211 */
2212 public function renderAiAltTextSection(): void
2213 {
2214 echo '<p>' . esc_html__('Configure automatic alt text generation for images in Media Library.', 'king-addons') . '</p>';
2215 }
2216
2217 /**
2218 * Renders the Enable AI Buttons checkbox field.
2219 *
2220 * @return void
2221 */
2222 public function renderAiEnableButtonsField(): void
2223 {
2224 $options = get_option('king_addons_ai_options', []);
2225 // Default to true if option has never been saved, otherwise use saved value
2226 $enabled = array_key_exists('enable_ai_buttons', $options) ? (bool) $options['enable_ai_buttons'] : true;
2227 printf(
2228 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_buttons]" value="1" %s /> %s</label>',
2229 checked($enabled, true, false),
2230 esc_html__('Enable AI Text Editing Buttons in Elementor Editor', 'king-addons')
2231 );
2232 }
2233
2234 /**
2235 * Renders the Enable AI Image Generation checkbox field.
2236 *
2237 * @return void
2238 */
2239 public function renderAiImageGenerationField(): void
2240 {
2241 $options = get_option('king_addons_ai_options', []);
2242 // Default to true if option has never been saved, otherwise use saved value
2243 $enabled = array_key_exists('enable_ai_image_generation_button', $options) ? (bool) $options['enable_ai_image_generation_button'] : true;
2244 printf(
2245 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_image_generation_button]" value="1" %s /> %s</label>',
2246 checked($enabled, true, false),
2247 esc_html__('Enable AI Image Generation Button in Elementor Editor', 'king-addons')
2248 );
2249 }
2250
2251 /**
2252 * Renders the Enable AI Alt Text Button checkbox field.
2253 *
2254 * @return void
2255 */
2256 public function renderAiAltTextButtonField(): void
2257 {
2258 $options = get_option('king_addons_ai_options', []);
2259 // Default to true if option has never been saved, otherwise use saved value
2260 $enabled = array_key_exists('enable_ai_alt_text_button', $options) ? (bool) $options['enable_ai_alt_text_button'] : true;
2261 printf(
2262 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_alt_text_button]" value="1" %s /> %s</label>',
2263 checked($enabled, true, false),
2264 esc_html__('Enable AI Alt Text Generation Button in Media Library', 'king-addons')
2265 );
2266 echo '<p class="description">' . esc_html__('Show "Generate" button in Media Library to manually create alt text for images using AI.', 'king-addons') . '</p>';
2267 }
2268
2269 /**
2270 * Renders the Enable AI Alt Text Auto Generation checkbox field.
2271 *
2272 * @return void
2273 */
2274 public function renderAiAltTextAutoGenerationField(): void
2275 {
2276 $options = get_option('king_addons_ai_options', []);
2277 $is_pro = !king_addons_freemius()->can_use_premium_code();
2278 // Default to false if option has never been saved, otherwise use saved value
2279 $enabled = array_key_exists('enable_ai_alt_text_auto_generation', $options) ? (bool) $options['enable_ai_alt_text_auto_generation'] : false;
2280 printf(
2281 '<label><input type="checkbox"' . ($is_pro ? ' disabled' : '') . ' name="king_addons_ai_options[enable_ai_alt_text_auto_generation]" value="1" %s /> %s</label>',
2282 checked($enabled, true, false),
2283 esc_html__('Automatically Generate Alt Text for New Images' . ($is_pro ? ' (PRO feature)' : ''), 'king-addons')
2284 );
2285 echo '<p class="description">' . esc_html__('Automatically generate alt text when new images are uploaded to Media Library. Great for SEO.', 'king-addons') . '</p>';
2286 }
2287
2288
2289 /**
2290 * Renders the AI Alt Text Generation Interval field.
2291 *
2292 * @return void
2293 */
2294 public function renderAiAltTextIntervalField(): void
2295 {
2296 $options = get_option('king_addons_ai_options', []);
2297 $interval = isset($options['ai_alt_text_generation_interval']) ? (int) $options['ai_alt_text_generation_interval'] : 20;
2298 printf(
2299 '<input type="number" name="king_addons_ai_options[ai_alt_text_generation_interval]" value="%d" min="10" max="3600" placeholder="20" />',
2300 $interval
2301 );
2302 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>';
2303 }
2304
2305 /**
2306 * Renders the image model selection dropdown field.
2307 *
2308 * @return void
2309 */
2310 public function renderAiImageModelField(): void
2311 {
2312 $options = get_option('king_addons_ai_options', []);
2313 $selected = $options['openai_image_model'] ?? 'dall-e-3';
2314 $models = [
2315 'dall-e-3' => esc_html__('DALL·E 3', 'king-addons'),
2316 'gpt-image-1' => esc_html__('GPT Image 1', 'king-addons'),
2317 ];
2318 printf(
2319 '<select name="king_addons_ai_options[openai_image_model]" %s>',
2320 ''
2321 );
2322 foreach ($models as $id => $label) {
2323 printf(
2324 '<option value="%s" %s>%s</option>',
2325 esc_attr($id),
2326 selected($selected, $id, false),
2327 esc_html($label)
2328 );
2329 }
2330 echo '</select>';
2331 echo '<p class="description">';
2332 printf(
2333 /* translators: %1$s: URL to OpenAI Organization Settings */
2334 wp_kses(
2335 __('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'),
2336 ['a' => ['href' => [], 'target' => [], 'rel' => []]]
2337 ),
2338 esc_url('https://platform.openai.com/settings/organization/general')
2339 );
2340 echo '</p>';
2341 }
2342
2343 /**
2344 * AJAX handler to generate images using the configured AI provider.
2345 *
2346 * @return void
2347 */
2348 public function set_openai_curl_options($handle, $r, $url): void
2349 {
2350 if (!is_string($url) || $url === '') {
2351 return;
2352 }
2353
2354 // Apply these cURL options only to AI provider requests to avoid affecting other outbound HTTP calls.
2355 if (!AI_Provider::isProviderUrl($url)) {
2356 return;
2357 }
2358
2359 // Increase connect timeout to 60s, and total timeout to 5m.
2360 curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 60);
2361 curl_setopt($handle, CURLOPT_DNS_CACHE_TIMEOUT, 300);
2362 curl_setopt($handle, CURLOPT_TIMEOUT, 300);
2363 }
2364
2365 public function handleAiGenerateImage(): void
2366 {
2367 // Verify nonce and permissions
2368 check_ajax_referer('king_addons_ai_generate_image_nonce', 'nonce');
2369 if (!current_user_can('manage_options')) {
2370 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
2371 }
2372
2373 // Gather input parameters
2374 $prompt = isset($_POST['prompt']) ? sanitize_textarea_field(wp_unslash($_POST['prompt'])) : '';
2375 $quality = isset($_POST['quality']) ? sanitize_text_field(wp_unslash($_POST['quality'])) : '';
2376 $size = isset($_POST['size']) ? sanitize_text_field(wp_unslash($_POST['size'])) : '';
2377 // Model from frontend selector
2378 $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : '';
2379 if ($model === '') {
2380 $model = AI_Provider::getImageModel();
2381 }
2382
2383 $api_key = AI_Provider::getApiKey();
2384 if (empty($api_key)) {
2385 wp_send_json_error([
2386 'message' => sprintf(
2387 /* translators: %s: provider name */
2388 esc_html__('%s API key is not set.', 'king-addons'),
2389 AI_Provider::getLabel()
2390 )
2391 ], 400);
2392 }
2393 if (empty($prompt)) {
2394 wp_send_json_error(['message' => esc_html__('Please provide an image prompt.', 'king-addons')], 400);
2395 }
2396
2397 // Build the request body. OpenAI's image models take vendor specific
2398 // quality/size/background options; OpenRouter fans out to many vendors
2399 // that do not share that vocabulary, so only portable fields are sent.
2400 $body = [
2401 'model' => $model,
2402 'prompt' => $prompt,
2403 ];
2404
2405 if (AI_Provider::isOpenRouter()) {
2406 $body['n'] = 1;
2407 } else {
2408 $body['size'] = $size;
2409
2410 if ($model === 'dall-e-3') {
2411 // DALL·E 3 parameters
2412 $body['n'] = 1;
2413 $body['quality'] = ($quality === 'hd') ? 'hd' : 'standard';
2414 } elseif ($model === 'gpt-image-1') {
2415 // GPT Image 1 parameters
2416 // Only include background when transparent is requested
2417 if (!empty($_POST['background']) && 'transparent' === sanitize_text_field(wp_unslash($_POST['background']))) {
2418 $body['background'] = 'transparent';
2419 }
2420 $body['quality'] = in_array($quality, ['low', 'medium', 'high', 'auto'], true)
2421 ? $quality
2422 : 'auto';
2423 }
2424 }
2425
2426 add_action('http_api_curl', [$this, 'set_openai_curl_options'], 10, 3);
2427
2428 // Call the provider's image generation API.
2429 $response = wp_remote_post(
2430 AI_Provider::getImagesEndpoint(),
2431 [
2432 'headers' => AI_Provider::getHeaders(),
2433 'body' => wp_json_encode($body),
2434 'timeout' => 300,
2435 ]
2436 );
2437
2438 $data = AI_Provider::decodeResponse($response, esc_html__('AI image generation error.', 'king-addons'));
2439 if (is_wp_error($data)) {
2440 wp_send_json_error(['message' => $data->get_error_message()], 500);
2441 }
2442
2443 // Depending on the model, an image comes back either as a hosted URL or
2444 // as inline base64, so both shapes are accepted from either provider.
2445 $item = (isset($data['data'][0]) && is_array($data['data'][0])) ? $data['data'][0] : [];
2446 $base64 = '';
2447 $remote_url = '';
2448 $mime = 'image/png';
2449
2450 if (!empty($item['b64_json']) && is_string($item['b64_json'])) {
2451 $base64 = $item['b64_json'];
2452 if (!empty($item['media_type']) && is_string($item['media_type'])) {
2453 $mime = $item['media_type'];
2454 }
2455 } elseif (!empty($item['url']) && is_string($item['url'])) {
2456 $remote_url = $item['url'];
2457 } elseif (!empty($item['image_url']['url']) && is_string($item['image_url']['url'])) {
2458 $remote_url = $item['image_url']['url'];
2459 }
2460
2461 // Some providers hand back a data: URI in the url field.
2462 if ($remote_url !== '' && strpos($remote_url, 'data:') === 0 && preg_match('#^data:([^;,]+);base64,(.+)$#s', $remote_url, $matches)) {
2463 $mime = $matches[1];
2464 $base64 = $matches[2];
2465 $remote_url = '';
2466 }
2467
2468 if ($base64 === '' && $remote_url === '') {
2469 wp_send_json_error([
2470 'message' => sprintf(
2471 /* translators: %s: model id */
2472 esc_html__('The model %s did not return an image. Pick an image-capable model in AI Settings.', 'king-addons'),
2473 $model
2474 )
2475 ], 500);
2476 }
2477
2478 require_once ABSPATH . 'wp-admin/includes/image.php';
2479 require_once ABSPATH . 'wp-admin/includes/file.php';
2480 require_once ABSPATH . 'wp-admin/includes/media.php';
2481
2482 $filename = substr(sanitize_file_name($prompt), 0, 100);
2483 if ($filename === '') {
2484 $filename = 'ai-image';
2485 }
2486
2487 if ($base64 !== '') {
2488 $bytes = base64_decode($base64, true);
2489 if (!$bytes) {
2490 wp_send_json_error(['message' => esc_html__('Invalid image data from API.', 'king-addons')], 500);
2491 }
2492
2493 $extension = 'png';
2494 $type_map = ['image/jpeg' => 'jpg', 'image/jpg' => 'jpg', 'image/webp' => 'webp', 'image/gif' => 'gif'];
2495 if (isset($type_map[$mime])) {
2496 $extension = $type_map[$mime];
2497 }
2498
2499 // Create a temp file and write it
2500 $tmp = wp_tempnam($filename . '.' . $extension);
2501 if (!$tmp || !file_put_contents($tmp, $bytes)) {
2502 wp_send_json_error(['message' => esc_html__('Failed to write temp image file.', 'king-addons')], 500);
2503 }
2504
2505 // Sideload into the Media Library
2506 $attachment_id = media_handle_sideload(
2507 ['name' => $filename . '.' . $extension, 'tmp_name' => $tmp],
2508 0,
2509 $prompt
2510 );
2511
2512 if (is_wp_error($attachment_id)) {
2513 @unlink($tmp);
2514 wp_send_json_error(['message' => $attachment_id->get_error_message()], 500);
2515 }
2516 } else {
2517 $attachment_id = media_sideload_image(esc_url_raw($remote_url), 0, $prompt, 'id');
2518 if (is_wp_error($attachment_id)) {
2519 wp_send_json_error(['message' => $attachment_id->get_error_message()], 500);
2520 }
2521 }
2522
2523 // Respond with attachment details
2524 wp_send_json_success([
2525 'attachment_id' => $attachment_id,
2526 'url' => wp_get_attachment_url($attachment_id),
2527 ]);
2528 }
2529
2530 /**
2531 * Renders the global content language field.
2532 *
2533 * @return void
2534 */
2535 public function renderAiContentLanguageField(): void
2536 {
2537 $options = get_option('king_addons_ai_options', []);
2538 $enabled = !empty($options['content_language_custom_enable']);
2539 $custom_lang = $options['content_language_custom'] ?? '';
2540 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>';
2541 echo '<div id="ka-content-lang-custom-wrap"' . ($enabled ? '' : ' hidden') . ' style="margin-top:8px;">';
2542 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" />';
2543 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>';
2544 echo '</div>';
2545 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>';
2546 }
2547
2548 /**
2549 * Renders the Image Detail Level dropdown for Alt Text Settings.
2550 *
2551 * @return void
2552 */
2553 public function renderAiAltTextImageDetailLevelField(): void
2554 {
2555 $options = get_option('king_addons_ai_options', []);
2556 $selected = $options['ai_alt_text_image_detail_level'] ?? 'low';
2557 echo '<select name="king_addons_ai_options[ai_alt_text_image_detail_level]">';
2558 echo '<option value="low"' . selected($selected, 'low', false) . '>' . esc_html__('Low', 'king-addons') . '</option>';
2559 echo '<option value="high"' . selected($selected, 'high', false) . '>' . esc_html__('High', 'king-addons') . '</option>';
2560 echo '</select>';
2561 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>';
2562 }
2563
2564 /**
2565 * Renders the Translation Settings section description.
2566 *
2567 * @return void
2568 */
2569 public function renderAiTranslationSection(): void
2570 {
2571 echo '<p>' . esc_html__('Configure AI Page Translator settings for Elementor editor.', 'king-addons') . '</p>';
2572 }
2573
2574 /**
2575 * Renders the Enable AI Page Translator checkbox field.
2576 *
2577 * @return void
2578 */
2579 public function renderAiPageTranslatorField(): void
2580 {
2581 $options = get_option('king_addons_ai_options', []);
2582 // Default to true if option has never been saved, otherwise use saved value
2583 $enabled = array_key_exists('enable_ai_page_translator', $options) ? (bool) $options['enable_ai_page_translator'] : true;
2584 printf(
2585 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_page_translator]" value="1" %s /> %s</label>',
2586 checked($enabled, true, false),
2587 esc_html__('Show AI Page Translator button in Elementor editor toolbar', 'king-addons')
2588 );
2589 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>';
2590 }
2591
2592 /**
2593 * AJAX handler to translate text using the configured AI provider.
2594 *
2595 * @return void
2596 */
2597 public function handleAiTranslateText(): void
2598 {
2599 check_ajax_referer('king_addons_ai_generate_nonce', 'nonce');
2600 if (!current_user_can('manage_options')) {
2601 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
2602 }
2603
2604 $text = isset($_POST['text']) ? sanitize_textarea_field(wp_unslash($_POST['text'])) : '';
2605 $from_lang = isset($_POST['from_lang']) ? sanitize_text_field(wp_unslash($_POST['from_lang'])) : 'auto';
2606 $to_lang = isset($_POST['to_lang']) ? sanitize_text_field(wp_unslash($_POST['to_lang'])) : 'en';
2607
2608 $options = get_option('king_addons_ai_options', []);
2609 $api_key = AI_Provider::getApiKey();
2610 $model = AI_Provider::getTextModel();
2611
2612 if (empty($api_key) || empty($model)) {
2613 wp_send_json_error(['message' => esc_html__('API key or model not set.', 'king-addons')], 400);
2614 }
2615
2616 if (empty($text)) {
2617 wp_send_json_error(['message' => esc_html__('No text provided for translation.', 'king-addons')], 400);
2618 }
2619
2620 // Check daily token limit
2621 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
2622 $current_usage = $this->getAiDailyUsage();
2623
2624 if ($daily_limit > 0 && $current_usage >= $daily_limit) {
2625 wp_send_json_error([
2626 'message' => esc_html__('Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.', 'king-addons'),
2627 // This is the plugin's own cap, not the provider's, so the
2628 // client must not retry it or blame the AI provider.
2629 'code' => 'local_limit',
2630 'retryable' => false,
2631 ], 429);
2632 }
2633
2634 // Prepare translation prompt
2635 $from_lang_name = ($from_lang === 'auto') ? 'auto-detected language' : $this->getLanguageName($from_lang);
2636 $to_lang_name = $this->getLanguageName($to_lang);
2637
2638 // Enhanced system message for better custom language and prompt handling
2639 $system_message = 'You are a professional translator with expertise in languages, dialects, writing styles, and custom translation approaches. You can handle:
2640
2641 1. Standard languages (English, Spanish, etc.)
2642 2. Fictional/constructed languages (Klingon, Dothraki, Elvish, etc.)
2643 3. Historical language variants (Old English, Latin, etc.)
2644 4. Writing styles and tones (formal, casual, academic, business, etc.)
2645 5. Special communication styles (pirate speak, baby talk, technical jargon, etc.)
2646
2647 When translating:
2648 - Maintain the original meaning, tone, and formatting
2649 - Preserve HTML tags exactly as they appear
2650 - For custom languages, apply consistent linguistic rules
2651 - For style prompts, adapt the tone and vocabulary appropriately
2652 - Only return the translated/adapted text without explanations
2653
2654 If the target is a style rather than a language, transform the text to match that style while keeping the same language.';
2655
2656 // Enhanced user message with better context for custom languages and prompts
2657 if ($from_lang === 'auto') {
2658 $user_message = "Transform the following text to {$to_lang_name}:\n\n{$text}";
2659 } else {
2660 // Check if it looks like a style prompt rather than a language
2661 $is_style_prompt = $this->isStylePrompt($to_lang_name);
2662
2663 if ($is_style_prompt) {
2664 $user_message = "Transform the following text from {$from_lang_name} using this style/approach: {$to_lang_name}:\n\n{$text}";
2665 } else {
2666 $user_message = "Translate the following text from {$from_lang_name} to {$to_lang_name}:\n\n{$text}";
2667 }
2668 }
2669
2670 $messages = [
2671 ['role' => 'system', 'content' => $system_message],
2672 ['role' => 'user', 'content' => $user_message]
2673 ];
2674
2675 $data = AI_Provider::decodeResponse(
2676 wp_remote_post(
2677 AI_Provider::getChatEndpoint(),
2678 [
2679 'headers' => AI_Provider::getHeaders(),
2680 'body' => wp_json_encode(AI_Provider::prepareChatPayload([
2681 'model' => $model,
2682 'messages' => $messages,
2683 'max_tokens' => 1000,
2684 'temperature' => 0.3, // Lower temperature for more consistent translations
2685 ])),
2686 'timeout' => 60, // Free and reasoning models are routinely slower than 30s.
2687 ]
2688 ),
2689 esc_html__('AI translation error.', 'king-addons')
2690 );
2691
2692 // The translator runs hundreds of these back to back, so it needs to
2693 // know whether a failure is worth retrying or is a dead end.
2694 if (is_wp_error($data)) {
2695 $this->sendAiProviderError($data);
2696 }
2697
2698 $translated_text = AI_Provider::extractMessageContent($data);
2699 if (is_wp_error($translated_text)) {
2700 $this->sendAiProviderError($translated_text);
2701 }
2702
2703 // Update token usage statistics if present in the response
2704 if (isset($data['usage']['total_tokens'])) {
2705 $this->incrementAiDailyUsage(intval($data['usage']['total_tokens']));
2706 }
2707
2708 wp_send_json_success([
2709 'translated_text' => $translated_text,
2710 'usage' => [
2711 'tokens_used' => $data['usage']['total_tokens'] ?? 0,
2712 'daily_used' => $this->getAiDailyUsage(),
2713 'daily_limit' => $daily_limit,
2714 ]
2715 ]);
2716 }
2717
2718 /**
2719 * Ends an AJAX request with a classified provider error.
2720 *
2721 * Sends the closest meaningful HTTP status instead of a blanket 500, plus a
2722 * machine readable code and a retryable flag so the caller can back off and
2723 * retry transient failures rather than aborting a long run.
2724 *
2725 * @param \WP_Error $error Error from the provider layer.
2726 * @return void
2727 */
2728 private function sendAiProviderError(\WP_Error $error): void
2729 {
2730 $classified = AI_Provider::classifyError($error);
2731
2732 wp_send_json_error(
2733 [
2734 'message' => $classified['message'],
2735 'code' => $classified['code'],
2736 'retryable' => $classified['retryable'],
2737 'provider' => AI_Provider::getProvider(),
2738 'provider_label' => AI_Provider::getLabel(),
2739 ],
2740 AI_Provider::getResponseStatus($classified)
2741 );
2742 }
2743
2744 /**
2745 * Get language name by code
2746 *
2747 * @param string $code Language code
2748 * @return string Language name
2749 */
2750 private function getLanguageName(string $code): string
2751 {
2752 $languages = [
2753 'en' => 'English',
2754 'es' => 'Spanish',
2755 'fr' => 'French',
2756 'de' => 'German',
2757 'it' => 'Italian',
2758 'pt' => 'Portuguese',
2759 'ru' => 'Russian',
2760 'ja' => 'Japanese',
2761 'ko' => 'Korean',
2762 'zh' => 'Chinese',
2763 'ar' => 'Arabic',
2764 'hi' => 'Hindi',
2765 'nl' => 'Dutch',
2766 'pl' => 'Polish',
2767 'tr' => 'Turkish',
2768 'uk' => 'Ukrainian',
2769 'cs' => 'Czech',
2770 'sv' => 'Swedish',
2771 'no' => 'Norwegian',
2772 'da' => 'Danish',
2773 'fi' => 'Finnish'
2774 ];
2775
2776 return $languages[$code] ?? $code;
2777 }
2778
2779 /**
2780 * Check if the given text appears to be a style prompt rather than a language
2781 *
2782 * @param string $text The text to check
2783 * @return bool True if it looks like a style prompt
2784 */
2785 private function isStylePrompt(string $text): bool
2786 {
2787 $text_lower = strtolower($text);
2788
2789 // Common style/tone indicators
2790 $style_indicators = [
2791 'formal',
2792 'casual',
2793 'professional',
2794 'business',
2795 'academic',
2796 'technical',
2797 'friendly',
2798 'serious',
2799 'humorous',
2800 'dramatic',
2801 'poetic',
2802 'simple',
2803 'complex',
2804 'detailed',
2805 'brief',
2806 'conversational',
2807 'literary',
2808 'scientific',
2809 'medical',
2810 'legal',
2811 'marketing',
2812 'sales',
2813 'tone',
2814 'style',
2815 'manner',
2816 'approach',
2817 'way',
2818 'voice',
2819 'pirate',
2820 'shakespeare',
2821 'baby',
2822 'child',
2823 'elderly',
2824 'slang',
2825 'jargon',
2826 'dialect',
2827 'accent'
2828 ];
2829
2830 // Check if any style indicators are present
2831 foreach ($style_indicators as $indicator) {
2832 if (strpos($text_lower, $indicator) !== false) {
2833 return true;
2834 }
2835 }
2836
2837 // Check if it contains descriptive phrases
2838 $descriptive_patterns = [
2839 'for ',
2840 'like ',
2841 'as if ',
2842 'in the style of',
2843 'in a ',
2844 'with a ',
2845 'using ',
2846 'speaking ',
2847 'written ',
2848 'sound like',
2849 'talk like'
2850 ];
2851
2852 foreach ($descriptive_patterns as $pattern) {
2853 if (strpos($text_lower, $pattern) !== false) {
2854 return true;
2855 }
2856 }
2857
2858 return false;
2859 }
2860 }
2861