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

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

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