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