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