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

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

1,927 lines 78.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Admin class do all things for admin menu
5 */
6
7 namespace King_Addons;
8
9 if (!defined('ABSPATH')) {
10 exit; // Exit if accessed directly.
11 }
12
13 final class Admin
14 {
15 public function __construct()
16 {
17 if (is_admin()) {
18 add_action('admin_menu', [$this, 'addAdminMenu']);
19
20 // Always add the action, but check conditions inside addUpgradeMenu
21 add_action('admin_menu', [$this, 'addUpgradeMenu'], 999999999); // Highest priority to add at the very end
22
23 add_action('admin_init', [$this, 'createSettings']);
24 add_action('admin_init', [$this, 'createAiSettings']);
25 add_action('admin_enqueue_scripts', [$this, 'enqueueUpgradeLinkScript']);
26 }
27 }
28
29 function addAdminMenu(): void
30 {
31 add_menu_page(
32 'King Addons for Elementor',
33 'King Addons',
34 'manage_options',
35 'king-addons',
36 [$this, 'showAdminPage'],
37 KING_ADDONS_URL . 'includes/admin/img/icon-for-admin.svg',
38 58.7
39 );
40
41 add_submenu_page(
42 'king-addons',
43 'King Addons Settings',
44 'Settings',
45 'manage_options',
46 'king-addons-settings',
47 [$this, 'showSettingsPage']
48 );
49
50 if (KING_ADDONS_WGT_FORM_BUILDER) {
51 add_submenu_page(
52 'king-addons',
53 esc_html__('Form Submissions', 'king-addons'),
54 esc_html__('Form Submissions', 'king-addons'),
55 'edit_posts',
56 'edit.php?post_type=king-addons-fb-sub',
57 );
58 }
59
60 if (KING_ADDONS_EXT_TEMPLATES_CATALOG) {
61 add_menu_page(
62 'King Addons for Elementor',
63 (!king_addons_freemius()->can_use_premium_code() ? esc_html__('Free Templates', 'king-addons') : esc_html__('Templates Pro', 'king-addons')),
64 'manage_options',
65 'king-addons-templates',
66 [Templates::instance(), 'render_template_catalog_page'],
67 KING_ADDONS_URL . 'includes/admin/img/icon-for-menu-templates.svg',
68 58.71
69 );
70 }
71
72 if (KING_ADDONS_EXT_HEADER_FOOTER_BUILDER) {
73 self::showHeaderFooterBuilder();
74 }
75
76 if (KING_ADDONS_EXT_POPUP_BUILDER) {
77 self::showPopupBuilder();
78 }
79
80 // Add AI Settings submenu under King Addons
81 add_submenu_page(
82 'king-addons',
83 esc_html__('AI Settings', 'king-addons'),
84 esc_html__('AI Settings', 'king-addons'),
85 'manage_options',
86 'king-addons-ai-settings',
87 [$this, 'showAiSettingsPage']
88 );
89 }
90
91 function addUpgradeMenu(): void
92 {
93 // Don't add menu if Freemius is showing opt-in/activation
94 $fs = king_addons_freemius();
95
96 // Check if we're on any Freemius-related page
97 if (isset($_GET['fs_action']) ||
98 $fs->is_activation_mode() ||
99 (!$fs->is_registered() && !$fs->is_anonymous() && !$fs->is_tracking_prohibited())) {
100 return;
101 }
102
103 // Add Upgrade submenu under King Addons (only if premium is not active)
104 if (!$fs->can_use_premium_code()) {
105 add_submenu_page(
106 'king-addons',
107 esc_html__('Upgrade Now', 'king-addons'),
108 esc_html__('Upgrade Now', 'king-addons'),
109 'manage_options',
110 'https://kingaddons.com/pricing/?utm_source=kng-top-menu&utm_medium=plugin&utm_campaign=kng',
111 ''
112 );
113 }
114 }
115
116 function showPopupBuilder(): void
117 {
118 add_menu_page(
119 'Popup Builder',
120 'Popup Builder',
121 'manage_options',
122 'king-addons-popup-builder',
123 [Popup_Builder::instance(), 'renderPopupBuilder'],
124 KING_ADDONS_URL . 'includes/admin/img/icon-for-popup-builder.svg',
125 58.73
126 );
127 }
128
129 function showHeaderFooterBuilder(): void
130 {
131 $post_type = 'king-addons-el-hf';
132 $menu_slug = 'edit.php?post_type=' . $post_type;
133
134 // Add Main Menu
135 add_menu_page(
136 esc_html__('Elementor Header & Footer Builder', 'king-addons'),
137 esc_html__('Header & Footer', 'king-addons'),
138 'manage_options',
139 $menu_slug, // Menu slug points to the custom post type edit screen
140 '', // No callback function needed
141 KING_ADDONS_URL . 'includes/admin/img/icon-for-header-footer-builder.svg',
142 58.72
143 );
144
145 // Add 'All Templates' Submenu - this will be the first submenu item
146 add_submenu_page(
147 $menu_slug, // Parent slug matches the main menu slug
148 esc_html__('All Templates', 'king-addons'),
149 esc_html__('All Templates', 'king-addons'),
150 'edit_posts',
151 $menu_slug
152 );
153 }
154
155 function showAdminPage(): void
156 {
157 if (!current_user_can('manage_options')) {
158 return;
159 }
160
161 self::enqueueAdminAssets();
162
163 require_once(KING_ADDONS_PATH . 'includes/admin/layouts/admin-page.php');
164 }
165
166 function showSettingsPage(): void
167 {
168 if (!current_user_can('manage_options')) {
169 return;
170 }
171
172 require_once(KING_ADDONS_PATH . 'includes/admin/layouts/settings-page.php');
173
174 self::enqueueSettingsAssets();
175 }
176
177 function createSettings(): void
178 {
179 // Register a new setting for "king-addons" page.
180 register_setting('king_addons', 'king_addons_options');
181
182 // Register a new section in the "king-addons" page.
183 add_settings_section(
184 'king_addons_section_widgets',
185 '',
186 [$this, 'king_addons_section_widgets_callback'],
187 'king-addons'
188 );
189
190 // Register a new section in the "king-addons" page.
191 add_settings_section(
192 'king_addons_section_features',
193 '',
194 [$this, 'king_addons_section_features_callback'],
195 'king-addons'
196 );
197
198 foreach (ModulesMap::getModulesMapArray()['widgets'] as $widget_id => $widget_array) {
199 add_settings_field(
200 $widget_id,
201 $widget_array['title'],
202 '',
203 'king-addons',
204 'king_addons_section_widgets',
205 array(
206 'label_for' => $widget_id,
207 'description' => $widget_array['description'],
208 'docs_link' => $widget_array['docs-link'],
209 'demo_link' => $widget_array['demo-link'],
210 'class' => 'kng-tr kng-tr-' . $widget_id . (!empty($widget_array['has-pro']) ? ' kng-tr-freemium' : '')
211 )
212 );
213 }
214
215 foreach (ModulesMap::getModulesMapArray()['features'] as $feature_id => $feature_array) {
216 add_settings_field(
217 $feature_id,
218 $feature_array['title'],
219 '',
220 'king-addons',
221 'king_addons_section_features',
222 array(
223 'label_for' => $feature_id,
224 'description' => $feature_array['description'],
225 'docs_link' => $feature_array['docs-link'],
226 'demo_link' => $feature_array['demo-link'],
227 'class' => 'kng-tr kng-tr-' . $feature_id
228 )
229 );
230 }
231 }
232
233 function king_addons_section_widgets_callback($args): void
234 {
235 ?>
236 <h2 id="<?php echo esc_attr($args['id']); ?>"
237 class="kng-section-title"><?php esc_html_e('Elements', 'king-addons'); ?></h2>
238 <?php
239 }
240
241 function king_addons_section_features_callback($args): void
242 {
243 ?>
244 <div class="kng-section-separator"></div>
245 <h2 id="<?php echo esc_attr($args['id']); ?>"
246 class="kng-section-title"><?php esc_html_e('Features', 'king-addons'); ?></h2>
247 <?php
248 }
249
250 function enqueueAdminAssets(): void
251 {
252 wp_enqueue_style('king-addons-admin', KING_ADDONS_URL . 'includes/admin/css/admin.css', '', KING_ADDONS_VERSION);
253 // Styles for AI Image Generation controls in Elementor
254 wp_enqueue_style('king-addons-ai-imagefield', KING_ADDONS_URL . 'includes/admin/css/ai-imagefield.css', array('king-addons-admin'), KING_ADDONS_VERSION);
255 }
256
257 function enqueueUpgradeLinkScript(): void
258 {
259 // Only add the script if premium is not active
260 if (!king_addons_freemius()->can_use_premium_code()) {
261 wp_enqueue_script('jquery');
262 wp_add_inline_script('jquery', "
263 jQuery(document).ready(function($) {
264 $('#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');
265 });
266 ");
267 }
268 }
269
270 function enqueueSettingsAssets(): void
271 {
272 wp_enqueue_style('king-addons-settings', KING_ADDONS_URL . 'includes/admin/css/settings.css', '', KING_ADDONS_VERSION);
273 wp_enqueue_style('wp-color-picker');
274 wp_enqueue_script('jquery');
275 wp_enqueue_script('wp-color-picker');
276 wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-wpcolorpicker-wpcolorpicker');
277 wp_enqueue_script('king-addons-settings', KING_ADDONS_URL . 'includes/admin/js/settings.js', '', KING_ADDONS_VERSION);
278 }
279
280 /**
281 * Registers AI Settings using the WordPress Settings API.
282 *
283 * @return void
284 */
285 public function createAiSettings(): void
286 {
287 register_setting(
288 'king_addons_ai',
289 'king_addons_ai_options',
290 [$this, 'sanitizeAiSettings']
291 );
292
293 add_settings_section(
294 'king_addons_ai_openai_section',
295 esc_html__('OpenAI API Settings', 'king-addons'),
296 [$this, 'renderAiOpenaiSection'],
297 'king-addons-ai-settings'
298 );
299
300 add_settings_field(
301 'openai_api_key',
302 esc_html__('OpenAI API Key', 'king-addons'),
303 [$this, 'renderAiApiKeyField'],
304 'king-addons-ai-settings',
305 'king_addons_ai_openai_section'
306 );
307
308 add_settings_field(
309 'openai_model',
310 esc_html__('OpenAI Model', 'king-addons'),
311 [$this, 'renderAiModelField'],
312 'king-addons-ai-settings',
313 'king_addons_ai_openai_section'
314 );
315
316 // Add image model selector field
317 add_settings_field(
318 'openai_image_model',
319 esc_html__('OpenAI Image Model', 'king-addons'),
320 [$this, 'renderAiImageModelField'],
321 'king-addons-ai-settings',
322 'king_addons_ai_openai_section'
323 );
324
325 // Add Editor Integration section and field
326 add_settings_section(
327 'king_addons_ai_editor_section',
328 esc_html__('Editor Integration', 'king-addons'),
329 [$this, 'renderAiEditorSection'],
330 'king-addons-ai-settings'
331 );
332 add_settings_field(
333 'enable_ai_buttons',
334 esc_html__('AI Text Editing Buttons', 'king-addons'),
335 [$this, 'renderAiEnableButtonsField'],
336 'king-addons-ai-settings',
337 'king_addons_ai_editor_section'
338 );
339 add_settings_field(
340 'enable_ai_image_generation_button',
341 esc_html__('AI Image Generation Button', 'king-addons'),
342 [$this, 'renderAiImageGenerationField'],
343 'king-addons-ai-settings',
344 'king_addons_ai_editor_section'
345 );
346
347 // Add Alt Text Settings section
348 add_settings_section(
349 'king_addons_ai_alt_text_section',
350 esc_html__('Alt Text Settings', 'king-addons'),
351 [$this, 'renderAiAltTextSection'],
352 'king-addons-ai-settings'
353 );
354 add_settings_field(
355 'enable_ai_alt_text_button',
356 esc_html__('AI Alt Text Button', 'king-addons'),
357 [$this, 'renderAiAltTextButtonField'],
358 'king-addons-ai-settings',
359 'king_addons_ai_alt_text_section'
360 );
361
362 add_settings_field(
363 'enable_ai_alt_text_auto_generation',
364 ((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')),
365 [$this, 'renderAiAltTextAutoGenerationField'],
366 'king-addons-ai-settings',
367 'king_addons_ai_alt_text_section'
368 );
369 add_settings_field(
370 'ai_alt_text_generation_interval',
371 esc_html__('Alt Text Generation Interval', 'king-addons'),
372 [$this, 'renderAiAltTextIntervalField'],
373 'king-addons-ai-settings',
374 'king_addons_ai_alt_text_section'
375 );
376 // Add Image Detail Level field
377 add_settings_field(
378 'ai_alt_text_image_detail_level',
379 esc_html__('Image Detail Level', 'king-addons'),
380 [$this, 'renderAiAltTextImageDetailLevelField'],
381 'king-addons-ai-settings',
382 'king_addons_ai_alt_text_section'
383 );
384
385 // Add Translation Settings section
386 add_settings_section(
387 'king_addons_ai_translation_section',
388 esc_html__('Translation Settings', 'king-addons'),
389 [$this, 'renderAiTranslationSection'],
390 'king-addons-ai-settings'
391 );
392
393 add_settings_field(
394 'enable_ai_page_translator',
395 esc_html__('AI Page Translator Button', 'king-addons'),
396 [$this, 'renderAiPageTranslatorField'],
397 'king-addons-ai-settings',
398 'king_addons_ai_translation_section'
399 );
400
401 // Add Usage Quota Settings section and field
402 add_settings_section(
403 'king_addons_ai_quota_section',
404 esc_html__('Usage Quota Settings', 'king-addons'),
405 [$this, 'renderAiQuotaSection'],
406 'king-addons-ai-settings'
407 );
408
409 add_settings_field(
410 'daily_token_limit',
411 esc_html__('Daily Token Limit', 'king-addons'),
412 [$this, 'renderAiDailyLimitField'],
413 'king-addons-ai-settings',
414 'king_addons_ai_quota_section'
415 );
416
417 // Add Usage Statistics section (read-only)
418 add_settings_section(
419 'king_addons_ai_stats_section',
420 esc_html__('Usage Statistics', 'king-addons'),
421 [$this, 'renderAiStatsSection'],
422 'king-addons-ai-settings'
423 );
424
425 // Clear models cache when options updated.
426 add_action('update_option_king_addons_ai_options', [$this, 'clearAiModelsCache']);
427
428 // AJAX handler for refreshing models.
429 add_action('wp_ajax_king_addons_ai_refresh_models', [$this, 'handleAiRefreshModels']);
430
431 // AJAX handler for generating text via AI
432 add_action('wp_ajax_king_addons_ai_generate_text', [$this, 'handleAiGenerateText']);
433
434 // AJAX handler to change text using AI based on user prompt and original text.
435 add_action('wp_ajax_king_addons_ai_change_text', [$this, 'handleAiChangeText']);
436
437 // AJAX handler to check token usage limits
438 add_action('wp_ajax_king_addons_ai_check_tokens', [$this, 'handleAiCheckTokens']);
439
440 // AJAX handler to check image generation limits
441 add_action('wp_ajax_king_addons_ai_image_check_limits', [$this, 'handleAiImageCheckLimits']);
442
443 // THIRD_EDIT: Register AJAX handler for AI image generation
444 add_action('wp_ajax_king_addons_ai_generate_image', [$this, 'handleAiGenerateImage']);
445
446 // AJAX handler for AI page translation
447 add_action('wp_ajax_king_addons_ai_translate_text', [$this, 'handleAiTranslateText']);
448 }
449
450 /**
451 * Sanitizes AI Settings options.
452 *
453 * @param array $input Raw input array.
454 * @return array Sanitized input.
455 */
456 public function sanitizeAiSettings(array $input): array
457 {
458 $sanitized = [];
459 $sanitized['openai_api_key'] = isset($input['openai_api_key'])
460 ? sanitize_text_field($input['openai_api_key'])
461 : '';
462 $sanitized['openai_model'] = isset($input['openai_model'])
463 ? sanitize_text_field($input['openai_model'])
464 : '';
465 $sanitized['openai_image_model'] = isset($input['openai_image_model'])
466 ? sanitize_text_field($input['openai_image_model'])
467 : 'gpt-image-1';
468
469 // Sanitize Daily Token Limit.
470 if (isset($input['daily_token_limit'])) {
471 $daily_limit = absint($input['daily_token_limit']);
472 $sanitized['daily_token_limit'] = max(0, $daily_limit); // Ensure non-negative
473 } else {
474 $sanitized['daily_token_limit'] = 1000000; // Default to 1 million tokens if not set
475 }
476
477 // Sanitize Enable AI Buttons option.
478 $sanitized['enable_ai_buttons'] = ! empty($input['enable_ai_buttons']);
479
480 // Sanitize Enable AI Image Generation button option.
481 $sanitized['enable_ai_image_generation_button'] = ! empty($input['enable_ai_image_generation_button']);
482
483 // Sanitize Enable AI Alt Text Button option.
484 $sanitized['enable_ai_alt_text_button'] = ! empty($input['enable_ai_alt_text_button']);
485
486 // Sanitize Enable AI Alt Text Auto Generation option.
487 $sanitized['enable_ai_alt_text_auto_generation'] = ! empty($input['enable_ai_alt_text_auto_generation']);
488
489 // Sanitize AI Alt Text Generation Interval.
490 if (isset($input['ai_alt_text_generation_interval'])) {
491 $interval = absint($input['ai_alt_text_generation_interval']);
492 $sanitized['ai_alt_text_generation_interval'] = max(10, min(3600, $interval)); // Between 10 seconds and 1 hour
493 } else {
494 $sanitized['ai_alt_text_generation_interval'] = 60; // Default to 60 seconds
495 }
496 // Sanitize Image Detail Level
497 $allowed_detail_levels = ['low', 'high'];
498 $sanitized['ai_alt_text_image_detail_level'] = in_array(($input['ai_alt_text_image_detail_level'] ?? 'low'), $allowed_detail_levels, true)
499 ? $input['ai_alt_text_image_detail_level']
500 : 'low';
501
502 // Sanitize Enable AI Page Translator option
503 $sanitized['enable_ai_page_translator'] = ! empty($input['enable_ai_page_translator']);
504
505 return $sanitized;
506 }
507
508 /**
509 * Clears cached AI models list.
510 *
511 * @return void
512 */
513 public function clearAiModelsCache(): void
514 {
515 delete_transient('king_addons_ai_models_cache');
516 }
517
518 /**
519 * Renders the AI Settings page content.
520 *
521 * @return void
522 */
523 public function showAiSettingsPage(): void
524 {
525 if (! current_user_can('manage_options')) {
526 return;
527 }
528 require_once KING_ADDONS_PATH . 'includes/admin/layouts/ai-settings-page.php';
529 $this->enqueueAiSettingsAssets();
530 }
531
532 /**
533 * Enqueues scripts and styles for the AI Settings page.
534 *
535 * @return void
536 */
537 public function enqueueAiSettingsAssets(): void
538 {
539 // Enqueue admin base styles first for proper theming
540 wp_enqueue_style('king-addons-admin', KING_ADDONS_URL . 'includes/admin/css/admin.css', '', KING_ADDONS_VERSION);
541
542 wp_enqueue_style(
543 'king-addons-ai-settings',
544 KING_ADDONS_URL . 'includes/admin/css/ai-settings.css',
545 ['king-addons-admin'], // Depend on admin base styles
546 KING_ADDONS_VERSION
547 );
548
549 wp_enqueue_script(
550 'king-addons-ai-settings',
551 KING_ADDONS_URL . 'includes/admin/js/ai-settings.js',
552 ['jquery'],
553 KING_ADDONS_VERSION,
554 true
555 );
556
557 wp_localize_script(
558 'king-addons-ai-settings',
559 'KingAddonsAiSettings',
560 [
561 'ajax_url' => admin_url('admin-ajax.php'),
562 'nonce' => wp_create_nonce('king_addons_ai_refresh_models_nonce'),
563 'refreshing_text' => esc_html__('Refreshing...', 'king-addons'),
564 'refreshed_text' => esc_html__('List updated.', 'king-addons'),
565 'error_text' => esc_html__('Error updating list.', 'king-addons'),
566 ]
567 );
568 }
569
570 /**
571 * Renders description for OpenAI API Settings section.
572 *
573 * @return void
574 */
575 public function renderAiOpenaiSection(): void
576 {
577 echo '<p>' . esc_html__('Enter your OpenAI API key and select the model for AI features.', 'king-addons') . '</p>';
578 }
579
580 /**
581 * Renders the OpenAI API Key input field.
582 *
583 * @return void
584 */
585 public function renderAiApiKeyField(): void
586 {
587 $options = get_option('king_addons_ai_options', []);
588 $api_key = $options['openai_api_key'] ?? '';
589 printf(
590 '<input type="password" name="king_addons_ai_options[openai_api_key]" value="%s" class="regular-text" autocomplete="off" />',
591 esc_attr($api_key)
592 );
593 echo '<p class="description">';
594 printf(
595 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'),
596 '<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">',
597 '</a>'
598 );
599 echo '</p>';
600 echo '<div style="background: #ffebe8; color: #a00; border: 1px solid #a00; padding: 10px; margin: 10px 0; border-radius: 4px;">';
601 echo '<strong>' . esc_html__('Important:', 'king-addons') . '</strong> ';
602 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');
603 echo '</div>';
604 echo '<div style="background: #e7f3fe; color: #084d7a; border: 1px solid #b6e0fe; padding: 10px; margin: 10px 0; border-radius: 4px;">';
605 echo '<span style="font-weight: bold; color: #084d7a;">' . esc_html__('Info:', 'king-addons') . '</span> ';
606 echo esc_html__('With GPT-4o-mini, a $5 balance is enough for roughly 130,000–150,000 text generations.', 'king-addons');
607 echo '</div>';
608 echo '<div style="background: #e7f3fe; color: #084d7a; border: 1px solid #b6e0fe; padding: 10px; margin: 10px 0; border-radius: 4px;">';
609 echo '<span style="font-weight: bold; color: #084d7a; display: block; margin-bottom: 4px;">' . esc_html__('Useful OpenAI Links:', 'king-addons') . '</span>';
610 echo '<ul style="margin: 0 0 0 18px; padding: 0; list-style: disc;">';
611 $links = [
612 'API Pricing' => 'https://openai.com/api/pricing/',
613 // 'API Documentation' => 'https://platform.openai.com/docs',
614 'API Keys' => 'https://platform.openai.com/api-keys',
615 'Usage Dashboard' => 'https://platform.openai.com/account/usage',
616 'Billing Overview' => 'https://platform.openai.com/account/billing/overview',
617 'Rate Limits' => 'https://openai.com/pricing#rate-limits',
618 ];
619 foreach ($links as $label => $url) {
620 printf(
621 '<li><a href="%s" target="_blank" rel="noopener noreferrer" style="color: #084d7a; text-decoration: underline;">%s</a></li>',
622 esc_url($url),
623 esc_html($label)
624 );
625 }
626 echo '</ul></div>';
627 }
628
629 /**
630 * Renders the model selection dropdown field with refresh button.
631 *
632 * @return void
633 */
634 public function renderAiModelField(): void
635 {
636 $options = get_option('king_addons_ai_options', []);
637 $selected = $options['openai_model'] ?? '';
638 $models = $this->getAiAvailableModels();
639 printf(
640 '<select name="king_addons_ai_options[openai_model]" %s>',
641 empty($models) ? 'disabled' : ''
642 );
643 if (!empty($models)) {
644 foreach ($models as $id => $label) {
645 printf(
646 '<option value="%s" %s>%s</option>',
647 esc_attr($id),
648 selected($selected, $id, false),
649 esc_html($label)
650 );
651 }
652 } else {
653 echo '<option value="">' . esc_html__('Could not fetch models. Check API key?', 'king-addons') . '</option>';
654 }
655 echo '</select>';
656 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>';
657 echo '<span class="spinner" id="king-addons-ai-refresh-models-spinner" style="float:none; vertical-align:middle;"></span>';
658 echo '<span id="king-addons-ai-refresh-models-status" style="margin-left:5px; vertical-align:middle;"></span>';
659 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>';
660 }
661
662 /**
663 * Fetches the list of OpenAI models via API.
664 *
665 * @param string|null $api_key API key to use.
666 * @return array|\WP_Error Model list or error.
667 */
668 private function fetchAiOpenaiModels(?string $api_key)
669 {
670 if (empty($api_key)) {
671 return new \WP_Error('missing_key', esc_html__('API key is required to fetch models.', 'king-addons'));
672 }
673 $endpoint = 'https://api.openai.com/v1/models';
674 $response = wp_remote_get($endpoint, [
675 'headers' => ['Authorization' => 'Bearer ' . $api_key],
676 'timeout' => 20,
677 ]);
678 if (is_wp_error($response)) {
679 return $response;
680 }
681 $code = wp_remote_retrieve_response_code($response);
682 $body = wp_remote_retrieve_body($response);
683 $data = json_decode($body, true);
684 if ($code !== 200 || empty($data['data']) || !is_array($data['data'])) {
685 $message = $data['error']['message'] ?? esc_html__('Invalid response from API.', 'king-addons');
686 return new \WP_Error('api_error', $message, ['status' => $code]);
687 }
688 $list = [];
689 foreach ($data['data'] as $model) {
690 if (isset($model['id'])) {
691 $list[$model['id']] = $model['id'];
692 }
693 }
694 ksort($list);
695 if (empty($list)) {
696 return new \WP_Error('no_models', esc_html__('No models found via API.', 'king-addons'));
697 }
698 return $list;
699 }
700
701 /**
702 * Retrieves available models, using cache if possible.
703 *
704 * @return array Model list.
705 */
706 private function getAiAvailableModels(): array
707 {
708 $cached = get_transient('king_addons_ai_models_cache');
709 if (false !== $cached && is_array($cached)) {
710 return $cached;
711 }
712 $options = get_option('king_addons_ai_options', []);
713 $api_key = $options['openai_api_key'] ?? null;
714 $fetched = $this->fetchAiOpenaiModels($api_key);
715 if (!is_wp_error($fetched)) {
716 set_transient('king_addons_ai_models_cache', $fetched, 0);
717 return $fetched;
718 }
719 return ['gpt-4o-mini' => 'GPT-4o-mini', 'gpt-4.1-nano' => 'GPT-4.1-nano'];
720 }
721
722 /**
723 * Handles AJAX request to refresh model list.
724 *
725 * @return void
726 */
727 public function handleAiRefreshModels(): void
728 {
729 check_ajax_referer('king_addons_ai_refresh_models_nonce', 'nonce');
730 if (!current_user_can('manage_options')) {
731 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
732 }
733 $options = get_option('king_addons_ai_options', []);
734 $api_key = $options['openai_api_key'] ?? null;
735 if (empty($api_key)) {
736 wp_send_json_error(['message' => esc_html__('API key is not set.', 'king-addons')], 400);
737 }
738 $this->clearAiModelsCache();
739 $models = $this->fetchAiOpenaiModels($api_key);
740 if (is_wp_error($models)) {
741 wp_send_json_error(['message' => $models->get_error_message()], 500);
742 }
743 if (empty($models)) {
744 wp_send_json_error(['message' => esc_html__('No models returned by API.', 'king-addons')], 500);
745 }
746 set_transient('king_addons_ai_models_cache', $models, 0);
747 wp_send_json_success(['models' => $models]);
748 }
749
750 /**
751 * AJAX handler to generate text using OpenAI.
752 *
753 * @return void
754 */
755 public function handleAiGenerateText(): void
756 {
757 check_ajax_referer('king_addons_ai_generate_nonce', 'nonce');
758 if (! current_user_can('manage_options')) {
759 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
760 }
761 $field_name = sanitize_text_field($_POST['field_name'] ?? '');
762 // Accept 'prompt' parameter (new) but fall back to 'value' parameter (old) for backwards compatibility
763 $prompt = isset($_POST['prompt'])
764 ? sanitize_textarea_field($_POST['prompt'])
765 : sanitize_textarea_field($_POST['value'] ?? '');
766
767 // Get editor type if provided
768 $editor_type = sanitize_text_field($_POST['editor_type'] ?? 'text');
769
770 $options = get_option('king_addons_ai_options', []);
771 $api_key = $options['openai_api_key'] ?? '';
772 $model = $options['openai_model'] ?? '';
773
774 if (empty($api_key) || empty($model)) {
775 wp_send_json_error(['message' => esc_html__('API key or model not set.', 'king-addons')], 400);
776 }
777
778 if (empty($prompt)) {
779 wp_send_json_error(['message' => esc_html__('Please provide a prompt.', 'king-addons')], 400);
780 }
781
782 // Check daily token limit
783 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
784 $current_usage = $this->getAiDailyUsage();
785
786 if ($daily_limit > 0 && $current_usage >= $daily_limit) {
787 wp_send_json_error([
788 'message' => esc_html__('Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.', 'king-addons')
789 ], 429);
790 }
791
792 // System instruction based on editor type
793 $system_instruction = 'You are a helpful content assistant. Provide concise, well-written content based on the user\'s request.';
794
795 // Enhanced instruction for WYSIWYG editor
796 if ($editor_type === 'wysiwyg') {
797 $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.';
798 }
799
800 // Prepare request to OpenAI Chat Completions
801 $messages = [
802 ['role' => 'system', 'content' => $system_instruction],
803 ['role' => 'user', 'content' => $prompt]
804 ];
805
806 // Add format instruction for WYSIWYG
807 if ($editor_type === 'wysiwyg') {
808 $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.";
809 }
810
811 $response = wp_remote_post(
812 'https://api.openai.com/v1/chat/completions',
813 [
814 'headers' => [
815 'Authorization' => 'Bearer ' . $api_key,
816 'Content-Type' => 'application/json',
817 ],
818 'body' => wp_json_encode([
819 'model' => $model,
820 'messages' => $messages,
821 'max_tokens' => 500,
822 'temperature' => 0.7, // Slight creativity for better content
823 ]),
824 'timeout' => 30,
825 ]
826 );
827
828 if (is_wp_error($response)) {
829 wp_send_json_error(['message' => $response->get_error_message()], 500);
830 }
831
832 $code = wp_remote_retrieve_response_code($response);
833 $data = json_decode(wp_remote_retrieve_body($response), true);
834
835 if ($code !== 200 || empty($data['choices'][0]['message']['content'])) {
836 $error_msg = $data['error']['message'] ?? esc_html__('AI API error.', 'king-addons');
837 wp_send_json_error(['message' => $error_msg], 500);
838 }
839
840 $generated = trim($data['choices'][0]['message']['content']);
841
842 // Clean up any code fence markers for WYSIWYG editor
843 if ($editor_type === 'wysiwyg') {
844 // Remove code fence markers (```html and ```) that might be returned by AI
845 $generated = preg_replace('/^```(?:html|HTML)?\s*/', '', $generated);
846 $generated = preg_replace('/```\s*$/', '', $generated);
847 }
848
849 // Update token usage statistics if present in the response
850 if (isset($data['usage']['total_tokens'])) {
851 $this->incrementAiDailyUsage(intval($data['usage']['total_tokens']));
852 }
853
854 wp_send_json_success([
855 'text' => $generated,
856 'usage' => [
857 'tokens_used' => $data['usage']['total_tokens'] ?? 0,
858 'daily_used' => $this->getAiDailyUsage(),
859 'daily_limit' => $daily_limit,
860 ]
861 ]);
862 }
863
864 /**
865 * AJAX handler to change text using AI based on user prompt and original text.
866 *
867 * @return void
868 */
869 public function handleAiChangeText(): void
870 {
871 check_ajax_referer('king_addons_ai_change_nonce', 'nonce');
872 if (! current_user_can('manage_options')) {
873 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
874 }
875 $field_name = isset($_POST['field_name']) ? sanitize_text_field(wp_unslash($_POST['field_name'])) : '';
876 $prompt = isset($_POST['prompt']) ? sanitize_text_field(wp_unslash($_POST['prompt'])) : '';
877 $original = isset($_POST['original']) ? wp_kses_post(wp_unslash($_POST['original'])) : '';
878 $instruction_context = isset($_POST['instruction_context']) ? sanitize_textarea_field(wp_unslash($_POST['instruction_context'])) : '';
879
880 $options = get_option('king_addons_ai_options', []);
881 $api_key = $options['openai_api_key'] ?? '';
882 $model = $options['openai_model'] ?? '';
883
884 if (empty($api_key) || empty($model) || empty($prompt) || empty($original)) {
885 wp_send_json_error(['message' => esc_html__('Missing data for AI change.', 'king-addons')], 400);
886 }
887
888 // Check daily token limit
889 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
890 $current_usage = $this->getAiDailyUsage();
891
892 if ($daily_limit > 0 && $current_usage >= $daily_limit) {
893 wp_send_json_error([
894 'message' => esc_html__('Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.', 'king-addons')
895 ], 429);
896 }
897
898 // Default instruction if none provided
899 if (empty($instruction_context)) {
900 $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.';
901
902 // Add specific formatting instructions for WYSIWYG editor
903 $editor_type = isset($_POST['editor_type']) ? sanitize_text_field(wp_unslash($_POST['editor_type'])) : 'text';
904 if ($editor_type === 'wysiwyg') {
905 $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.';
906 }
907 }
908
909 // Analyze if the prompt is likely requesting to add content rather than replace
910 $add_content_keywords = [
911 // English
912 'add',
913 'insert',
914 'extend',
915 'append',
916 'more',
917 'additional',
918 'expand',
919 // Russian
920 'добавь',
921 'вставь',
922 'расширь',
923 // Spanish
924 'añadir',
925 'agregar',
926 'insertar',
927 'adjuntar',
928 'extender',
929 // French
930 'ajouter',
931 'insérer',
932 'étendre',
933 'annexer',
934 'joindre',
935 // German
936 'hinzufügen',
937 'einfügen',
938 'erweitern',
939 'anhängen',
940 'ergänzen',
941 // Italian
942 'aggiungere',
943 'inserire',
944 'allegare',
945 'estendere',
946 'appendere',
947 // Portuguese
948 'adicionar',
949 'inserir',
950 'acrescentar',
951 'anexar',
952 'estender',
953 // Polish
954 'dodać',
955 'wstawić',
956 'doł�
957 czyć',
958 'rozszerzyć',
959 'zał�
960 czyć'
961 ];
962
963 // Also look for numeric patterns like "add 2 paragraphs" or "добавь 3 абзаца"
964 // Enhanced pattern to find numeric paragraph requests in different languages
965 $numeric_pattern = '/(?:' .
966 // English verbs
967 'add|append|insert|create|write|' .
968 // Russian verbs
969 'добавь|вставь|создай|напиши|' .
970 // Spanish verbs
971 'añadir|agregar|insertar|crear|escribir|' .
972 // French verbs
973 'ajouter|insérer|créer|écrire|' .
974 // German verbs
975 'hinzufügen|einfügen|erstellen|schreiben|' .
976 // Italian verbs
977 'aggiungere|inserire|creare|scrivere|' .
978 // Portuguese verbs
979 'adicionar|inserir|criar|escrever|' .
980 // Polish verbs
981 'dodać|wstawić|utworzyć|napisać' .
982 ')\s+(\d+|' .
983 // English quantifiers
984 'several|few|couple|some|' .
985 // Russian quantifiers
986 'несколько|пару|еще|ещё|' .
987 // Spanish quantifiers
988 'varios|algunos|un par|unos|' .
989 // French quantifiers
990 'plusieurs|quelques|une paire|certains|' .
991 // German quantifiers
992 'mehrere|einige|ein paar|manche|' .
993 // Italian quantifiers
994 'diversi|alcuni|un paio|qualche|' .
995 // Portuguese quantifiers
996 'vários|alguns|um par|' .
997 // Polish quantifiers
998 'kilka|parę|pare|niektóre' .
999 ')\s+(?:' .
1000 // English nouns
1001 'paragraph|paragraphs|section|sections|content|text|' .
1002 // Russian nouns
1003 'абзац|абзаца|абзацев|раздел|разделы|текст|контент|параграф|параграфа|параграфов|' .
1004 // Spanish nouns
1005 'párrafo|párrafos|sección|secciones|contenido|texto|' .
1006 // French nouns
1007 'paragraphe|paragraphes|section|sections|contenu|texte|' .
1008 // German nouns
1009 'absatz|absätze|abschnitt|abschnitte|inhalt|text|' .
1010 // Italian nouns
1011 'paragrafo|paragrafi|sezione|sezioni|contenuto|testo|' .
1012 // Portuguese nouns
1013 'parágrafo|parágrafos|seção|seções|conteúdo|texto|' .
1014 // Polish nouns
1015 'akapit|akapity|sekcja|sekcje|treść|tekst' .
1016 ')/i';
1017 $contains_add_keyword = false;
1018 $numeric_match = [];
1019 $requested_paragraphs = 0;
1020
1021 // First check for specific numeric requests
1022 if (preg_match($numeric_pattern, $prompt, $numeric_match)) {
1023 $contains_add_keyword = true;
1024 $number_text = $numeric_match[1] ?? '';
1025
1026 // Convert text numbers to digits
1027 if (is_numeric($number_text)) {
1028 $requested_paragraphs = (int)$number_text;
1029 } else {
1030 // For words like "several", "few", "couple", etc.
1031 switch (strtolower($number_text)) {
1032 // Words meaning approximately "2"
1033 case 'couple':
1034 case 'пару': // Russian
1035 case 'пара': // Russian
1036 case 'un par': // Spanish
1037 case 'une paire': // French
1038 case 'ein paar': // German
1039 case 'un paio': // Italian
1040 case 'um par': // Portuguese
1041 case 'parę': // Polish
1042 case 'pare': // Polish
1043 $requested_paragraphs = 2;
1044 break;
1045
1046 // Words meaning approximately "3-4" (several/few)
1047 case 'few':
1048 case 'several':
1049 case 'some':
1050 case 'несколько': // Russian
1051 case 'еще': // Russian
1052 case 'ещё': // Russian
1053 case 'varios': // Spanish
1054 case 'algunos': // Spanish
1055 case 'unos': // Spanish
1056 case 'plusieurs': // French
1057 case 'quelques': // French
1058 case 'certains': // French
1059 case 'mehrere': // German
1060 case 'einige': // German
1061 case 'manche': // German
1062 case 'diversi': // Italian
1063 case 'alcuni': // Italian
1064 case 'qualche': // Italian
1065 case 'vários': // Portuguese
1066 case 'alguns': // Portuguese
1067 case 'kilka': // Polish
1068 case 'niektóre': // Polish
1069 default:
1070 $requested_paragraphs = 3; // Default "several" = 3
1071 break;
1072 }
1073 }
1074 } else {
1075 // Then check for general add keywords
1076 foreach ($add_content_keywords as $keyword) {
1077 if (stripos($prompt, $keyword) !== false) {
1078 $contains_add_keyword = true;
1079 $requested_paragraphs = 2; // Default to 2 paragraphs if just "add paragraphs"
1080 break;
1081 }
1082 }
1083 }
1084
1085 // Build the system message dynamically based on the prompt analysis
1086 $system_message = $instruction_context;
1087 if ($contains_add_keyword) {
1088 if ($requested_paragraphs > 0) {
1089 // Request only new paragraphs, without modifying original content
1090 $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.';
1091 $system_message .= sprintf(
1092 ' 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.',
1093 $requested_paragraphs
1094 );
1095 } else {
1096 $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.';
1097 }
1098 }
1099
1100 $body = [
1101 'model' => $model,
1102 'messages' => [
1103 [
1104 'role' => 'system',
1105 'content' => $system_message,
1106 ],
1107 [
1108 'role' => 'user',
1109 'content' => ($contains_add_keyword && $requested_paragraphs > 0)
1110 ? sprintf(
1111 "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.",
1112 $original,
1113 $requested_paragraphs
1114 )
1115 : sprintf(
1116 /* translators: %1$s: User's instruction prompt, %2$s: Original text to modify. */
1117 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'),
1118 $prompt,
1119 $original
1120 ),
1121 ],
1122 ],
1123 'max_tokens' => 10000, // Increased to allow for more content
1124 'temperature' => 0.7, // Slightly more creative
1125 ];
1126
1127 // Set append_mode flag for paragraph additions
1128 $append_mode = ($contains_add_keyword && $requested_paragraphs > 0);
1129
1130 // Modify request for WYSIWYG editor
1131 $editor_type = isset($_POST['editor_type']) ? sanitize_text_field(wp_unslash($_POST['editor_type'])) : 'text';
1132 if ($editor_type === 'wysiwyg') {
1133 // Add a specific instruction for formatting
1134 $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.';
1135
1136 if (!$append_mode) {
1137 // Only add this for non-append mode
1138 $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.";
1139 }
1140
1141 // Increase temperature for WYSIWYG to be more creative when creating paragraphs
1142 $body['temperature'] = 0.8;
1143
1144 // Increase max_tokens for longer responses with multiple paragraphs
1145 $body['max_tokens'] = 15000;
1146 }
1147
1148 $response = wp_remote_post(
1149 'https://api.openai.com/v1/chat/completions',
1150 [
1151 'headers' => [
1152 'Authorization' => 'Bearer ' . $api_key,
1153 'Content-Type' => 'application/json',
1154 ],
1155 'body' => wp_json_encode($body),
1156 'timeout' => 30,
1157 ]
1158 );
1159
1160 if (is_wp_error($response)) {
1161 wp_send_json_error(['message' => $response->get_error_message()], 500);
1162 }
1163
1164 $code = wp_remote_retrieve_response_code($response);
1165 $data = json_decode(wp_remote_retrieve_body($response), true);
1166
1167 if ($code !== 200 || empty($data['choices'][0]['message']['content'])) {
1168 $error_msg = $data['error']['message'] ?? esc_html__('AI change error.', 'king-addons');
1169 wp_send_json_error(['message' => $error_msg], 500);
1170 }
1171
1172 $changed = trim($data['choices'][0]['message']['content']);
1173
1174 // Clean up any code fence markers for WYSIWYG editor
1175 if ($editor_type === 'wysiwyg') {
1176 // Remove code fence markers (```html and ```) that might be returned by AI
1177 $changed = preg_replace('/^```(?:html|HTML)?\s*/', '', $changed);
1178 $changed = preg_replace('/```\s*$/', '', $changed);
1179 }
1180
1181 // Update token usage statistics if present in the response
1182 if (isset($data['usage']['total_tokens'])) {
1183 $this->incrementAiDailyUsage(intval($data['usage']['total_tokens']));
1184 }
1185
1186 // Send response with append mode flag
1187 wp_send_json_success([
1188 'text' => $changed,
1189 'append_mode' => $append_mode,
1190 'original' => $append_mode ? $original : '',
1191 'usage' => [
1192 'tokens_used' => $data['usage']['total_tokens'] ?? 0,
1193 'daily_used' => $this->getAiDailyUsage(),
1194 'daily_limit' => $daily_limit,
1195 ]
1196 ]);
1197 }
1198
1199 /**
1200 * Renders Usage Quota Settings section.
1201 *
1202 * @return void
1203 */
1204 public function renderAiQuotaSection(): void
1205 {
1206 echo '<p>' . esc_html__('Set the daily token limit for AI features.', 'king-addons') . '</p>';
1207 }
1208
1209 /**
1210 * Renders the Daily Token Limit input field.
1211 *
1212 * @return void
1213 */
1214 public function renderAiDailyLimitField(): void
1215 {
1216 $options = get_option('king_addons_ai_options', []);
1217 $daily_token_limit = $options['daily_token_limit'] ?? self::DEFAULT_DAILY_TOKEN_LIMIT;
1218
1219 echo '<div class="daily-token-limit-wrap">';
1220 printf(
1221 '<input type="number" name="king_addons_ai_options[daily_token_limit]" value="%s" class="regular-text" min="0" step="1000" style="margin-right: 10px;" />',
1222 esc_attr($daily_token_limit)
1223 );
1224 echo '<span>' . esc_html__('tokens', 'king-addons') . '</span>';
1225 echo '</div>';
1226
1227 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>';
1228
1229 echo '<div class="king-addons-info-box">';
1230 echo '<p><strong>' . esc_html__('About tokens:', 'king-addons') . '</strong> ' .
1231 esc_html__('Tokens are the basic unit of text that the AI processes. As a rough guide:', 'king-addons') . '</p>';
1232 echo '<p>• ' . esc_html__('1 token ≈ 4 characters or 0.75 words in English', 'king-addons') . '</p>';
1233 echo '<p>• ' . esc_html__('A typical paragraph might use around 50-100 tokens', 'king-addons') . '</p>';
1234 echo '<p>• ' . esc_html__('A full page of text (500 words) is approximately 750 tokens', 'king_addons') . '</p>';
1235 echo '<p>• ' . esc_html__('Recommended daily limit: 10,000 - 50,000 tokens for moderate use', 'king-addons') . '</p>';
1236 echo '</div>';
1237 }
1238
1239 /**
1240 * Default daily token limit if not explicitly set.
1241 *
1242 * @var int
1243 */
1244 private const DEFAULT_DAILY_TOKEN_LIMIT = 1000000;
1245
1246 /**
1247 * Gets the current daily token usage.
1248 *
1249 * @return int Number of tokens used today.
1250 */
1251 private function getAiDailyUsage(): int
1252 {
1253 $usage_data = get_option('king_addons_ai_daily_usage', ['date' => '', 'count' => 0]);
1254 $today = current_time('Y-m-d');
1255 if (!isset($usage_data['date']) || $usage_data['date'] !== $today) {
1256 return 0;
1257 }
1258 return intval($usage_data['count']);
1259 }
1260
1261 /**
1262 * Increments the daily token usage count.
1263 *
1264 * @param int $tokens Number of tokens to add.
1265 * @return void
1266 */
1267 public function incrementAiDailyUsage(int $tokens): void
1268 {
1269 $today = current_time('Y-m-d');
1270 $usage_data = get_option('king_addons_ai_daily_usage', ['date' => '', 'count' => 0]);
1271 if (!isset($usage_data['date']) || $usage_data['date'] !== $today) {
1272 $usage_data = [
1273 'date' => $today,
1274 'count' => 0,
1275 ];
1276 }
1277 $usage_data['count'] = intval($usage_data['count']) + $tokens;
1278 update_option('king_addons_ai_daily_usage', $usage_data, false);
1279 }
1280
1281 /**
1282 * Renders Usage Statistics section.
1283 *
1284 * @return void
1285 */
1286 public function renderAiStatsSection(): void
1287 {
1288 $usage_data = get_option('king_addons_ai_daily_usage', ['date' => '', 'count' => 0]);
1289 $today = current_time('Y-m-d');
1290 $used = (isset($usage_data['date']) && $usage_data['date'] === $today) ? intval($usage_data['count']) : 0;
1291
1292 $options = get_option('king_addons_ai_options', []);
1293 $limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1294
1295 if ($limit > 0) {
1296 $limit_display = number_format_i18n($limit);
1297 $remaining = max(0, $limit - $used);
1298 $remaining_display = number_format_i18n($remaining);
1299
1300 $usage_percentage = ($limit > 0) ? min(100, round(($used / $limit) * 100)) : 0;
1301
1302 echo '<div class="king-addons-ai-usage-stats">';
1303 echo '<table class="form-table">';
1304 echo '<tr>';
1305 echo '<th>' . esc_html__('Tokens Used Today', 'king-addons') . '</th>';
1306 echo '<td><strong>' . esc_html(number_format_i18n($used)) . '</strong></td>';
1307 echo '</tr>';
1308 echo '<tr>';
1309 echo '<th>' . esc_html__('Daily Limit', 'king-addons') . '</th>';
1310 echo '<td>' . esc_html($limit_display) . '</td>';
1311 echo '</tr>';
1312 echo '<tr>';
1313 echo '<th>' . esc_html__('Remaining', 'king-addons') . '</th>';
1314 echo '<td>' . esc_html($remaining_display) . '</td>';
1315 echo '</tr>';
1316 echo '</table>';
1317
1318 // Add progress bar
1319 echo '<div class="king-addons-ai-usage-bar-container" style="background-color: #f0f0f0; height: 20px; border-radius: 10px; margin: 15px 0; overflow: hidden;">';
1320 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>';
1321 echo '</div>';
1322 echo '<p class="description">' . esc_html(sprintf(__('Usage: %d%%', 'king-addons'), $usage_percentage)) . '</p>';
1323 echo '</div>';
1324 } else {
1325 echo '<p>' . esc_html__('No daily token limit is set. All requests will be processed.', 'king-addons') . '</p>';
1326 echo '<p><strong>' . esc_html__('Tokens used today:', 'king-addons') . ' ' . esc_html(number_format_i18n($used)) . '</strong></p>';
1327 }
1328 }
1329
1330 /**
1331 * AJAX handler to check token usage limits.
1332 *
1333 * @return void
1334 */
1335 public function handleAiCheckTokens(): void
1336 {
1337 check_ajax_referer('king_addons_ai_generate_nonce', 'nonce');
1338 if (!current_user_can('manage_options')) {
1339 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1340 }
1341
1342 $options = get_option('king_addons_ai_options', []);
1343 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1344 $daily_used = $this->getAiDailyUsage();
1345 // Check if API key and model are set
1346 $api_key = $options['openai_api_key'] ?? '';
1347 $model = $options['openai_model'] ?? '';
1348 $api_key_valid = !empty($api_key) && !empty($model);
1349
1350 wp_send_json_success([
1351 'daily_used' => $daily_used,
1352 'daily_limit' => $daily_limit,
1353 'limit_reached' => ($daily_limit > 0 && $daily_used >= $daily_limit),
1354 'api_key_valid' => $api_key_valid,
1355 ]);
1356 }
1357
1358 /**
1359 * AJAX handler to check image generation limits.
1360 *
1361 * @return void
1362 */
1363 public function handleAiImageCheckLimits(): void
1364 {
1365 check_ajax_referer('king_addons_ai_generate_image_nonce', 'nonce');
1366 if (! current_user_can('manage_options')) {
1367 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1368 }
1369
1370 $options = get_option('king_addons_ai_options', []);
1371 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1372 $daily_used = $this->getAiDailyUsage();
1373 // Check if API key and model are set
1374 $api_key = $options['openai_api_key'] ?? '';
1375 $model = $options['openai_model'] ?? '';
1376 $api_key_valid = !empty($api_key) && !empty($model);
1377
1378 wp_send_json_success([
1379 'daily_used' => $daily_used,
1380 'daily_limit' => $daily_limit,
1381 'limit_reached' => ($daily_limit > 0 && $daily_used >= $daily_limit),
1382 'api_key_valid' => $api_key_valid,
1383 ]);
1384
1385 }
1386
1387
1388 /**
1389 * Renders the Editor Integration section description.
1390 *
1391 * @return void
1392 */
1393 public function renderAiEditorSection(): void
1394 {
1395 echo '<p>' . esc_html__('Control the integration of AI features in the Elementor editor.', 'king-addons') . '</p>';
1396 }
1397
1398 /**
1399 * Renders description for Alt Text Settings section.
1400 *
1401 * @return void
1402 */
1403 public function renderAiAltTextSection(): void
1404 {
1405 echo '<p>' . esc_html__('Configure automatic alt text generation for images in Media Library.', 'king-addons') . '</p>';
1406 }
1407
1408 /**
1409 * Renders the Enable AI Buttons checkbox field.
1410 *
1411 * @return void
1412 */
1413 public function renderAiEnableButtonsField(): void
1414 {
1415 $options = get_option('king_addons_ai_options', []);
1416 // Default to true if option has never been saved, otherwise use saved value
1417 $enabled = array_key_exists('enable_ai_buttons', $options) ? (bool) $options['enable_ai_buttons'] : true;
1418 printf(
1419 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_buttons]" value="1" %s /> %s</label>',
1420 checked($enabled, true, false),
1421 esc_html__('Enable AI Text Editing Buttons in Elementor Editor', 'king-addons')
1422 );
1423 }
1424
1425 /**
1426 * Renders the Enable AI Image Generation checkbox field.
1427 *
1428 * @return void
1429 */
1430 public function renderAiImageGenerationField(): void
1431 {
1432 $options = get_option('king_addons_ai_options', []);
1433 // Default to true if option has never been saved, otherwise use saved value
1434 $enabled = array_key_exists('enable_ai_image_generation_button', $options) ? (bool) $options['enable_ai_image_generation_button'] : true;
1435 printf(
1436 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_image_generation_button]" value="1" %s /> %s</label>',
1437 checked($enabled, true, false),
1438 esc_html__('Enable AI Image Generation Button in Elementor Editor', 'king-addons')
1439 );
1440 }
1441
1442 /**
1443 * Renders the Enable AI Alt Text Button checkbox field.
1444 *
1445 * @return void
1446 */
1447 public function renderAiAltTextButtonField(): void
1448 {
1449 $options = get_option('king_addons_ai_options', []);
1450 // Default to true if option has never been saved, otherwise use saved value
1451 $enabled = array_key_exists('enable_ai_alt_text_button', $options) ? (bool) $options['enable_ai_alt_text_button'] : true;
1452 printf(
1453 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_alt_text_button]" value="1" %s /> %s</label>',
1454 checked($enabled, true, false),
1455 esc_html__('Enable AI Alt Text Generation Button in Media Library', 'king-addons')
1456 );
1457 echo '<p class="description">' . esc_html__('Show "Generate" button in Media Library to manually create alt text for images using AI.', 'king-addons') . '</p>';
1458 }
1459
1460 /**
1461 * Renders the Enable AI Alt Text Auto Generation checkbox field.
1462 *
1463 * @return void
1464 */
1465 public function renderAiAltTextAutoGenerationField(): void
1466 {
1467 $options = get_option('king_addons_ai_options', []);
1468 $is_pro = !king_addons_freemius()->can_use_premium_code();
1469 // Default to false if option has never been saved, otherwise use saved value
1470 $enabled = array_key_exists('enable_ai_alt_text_auto_generation', $options) ? (bool) $options['enable_ai_alt_text_auto_generation'] : false;
1471 printf(
1472 '<label><input type="checkbox"' . ($is_pro ? ' disabled' : '') . ' name="king_addons_ai_options[enable_ai_alt_text_auto_generation]" value="1" %s /> %s</label>',
1473 checked($enabled, true, false),
1474 esc_html__('Automatically Generate Alt Text for New Images' . ($is_pro ? ' (PRO feature)' : ''), 'king-addons')
1475 );
1476 echo '<p class="description">' . esc_html__('Automatically generate alt text when new images are uploaded to Media Library. Great for SEO.', 'king-addons') . '</p>';
1477 }
1478
1479
1480 /**
1481 * Renders the AI Alt Text Generation Interval field.
1482 *
1483 * @return void
1484 */
1485 public function renderAiAltTextIntervalField(): void
1486 {
1487 $options = get_option('king_addons_ai_options', []);
1488 $interval = isset($options['ai_alt_text_generation_interval']) ? (int) $options['ai_alt_text_generation_interval'] : 60;
1489 printf(
1490 '<input type="number" name="king_addons_ai_options[ai_alt_text_generation_interval]" value="%d" min="10" max="3600" placeholder="60" />',
1491 $interval
1492 );
1493 echo '<p class="description">' . esc_html__('How often (in seconds) the system should process alt text generation queue. Recommended: 60 seconds to avoid OpenAI API rate limits. Lower values may cause API errors during high usage periods. Range: 10-3600 seconds.', 'king-addons') . '</p>';
1494 }
1495
1496 /**
1497 * Renders the image model selection dropdown field.
1498 *
1499 * @return void
1500 */
1501 public function renderAiImageModelField(): void
1502 {
1503 $options = get_option('king_addons_ai_options', []);
1504 $selected = $options['openai_image_model'] ?? 'dall-e-3';
1505 $models = [
1506 'dall-e-3' => esc_html__('DALL·E 3', 'king-addons'),
1507 'gpt-image-1' => esc_html__('GPT Image 1', 'king-addons'),
1508 ];
1509 printf(
1510 '<select name="king_addons_ai_options[openai_image_model]" %s>',
1511 ''
1512 );
1513 foreach ($models as $id => $label) {
1514 printf(
1515 '<option value="%s" %s>%s</option>',
1516 esc_attr($id),
1517 selected($selected, $id, false),
1518 esc_html($label)
1519 );
1520 }
1521 echo '</select>';
1522 echo '<p class="description">';
1523 printf(
1524 /* translators: %1$s: URL to OpenAI Organization Settings */
1525 wp_kses(
1526 __( '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' ),
1527 [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [] ] ]
1528 ),
1529 esc_url( 'https://platform.openai.com/settings/organization/general' )
1530 );
1531 echo '</p>';
1532 }
1533
1534 /**
1535 * AJAX handler to generate images using OpenAI.
1536 *
1537 * @return void
1538 */
1539 public function handleAiGenerateImage(): void
1540 {
1541 // Verify nonce and permissions
1542 check_ajax_referer('king_addons_ai_generate_image_nonce', 'nonce');
1543 if (! current_user_can('manage_options')) {
1544 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1545 }
1546
1547 // Gather input parameters
1548 $prompt = isset($_POST['prompt']) ? sanitize_textarea_field(wp_unslash($_POST['prompt'])) : '';
1549 $quality = isset($_POST['quality']) ? sanitize_text_field(wp_unslash($_POST['quality'])) : '';
1550 $size = isset($_POST['size']) ? sanitize_text_field(wp_unslash($_POST['size'])) : '';
1551 // Model from frontend selector
1552 $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : 'dall-e-3';
1553
1554 $options = get_option('king_addons_ai_options', []);
1555 $api_key = $options['openai_api_key'] ?? '';
1556 if (empty($api_key)) {
1557 wp_send_json_error(['message' => esc_html__('OpenAI API key is not set.', 'king-addons')], 400);
1558 }
1559 if (empty($prompt)) {
1560 wp_send_json_error(['message' => esc_html__('Please provide an image prompt.', 'king-addons')], 400);
1561 }
1562
1563 // Build request body based on selected model
1564 $body = [
1565 'model' => $model,
1566 'prompt' => $prompt,
1567 'size' => $size,
1568 ];
1569 if ($model === 'dall-e-3') {
1570 // DALL·E 3 parameters
1571 $body['n'] = 1;
1572 $body['quality'] = ($quality === 'hd') ? 'hd' : 'standard';
1573 } elseif ($model === 'gpt-image-1') {
1574 // GPT Image 1 parameters
1575 // Only include background when transparent is requested
1576 if ( ! empty($_POST['background']) && 'transparent' === sanitize_text_field(wp_unslash($_POST['background'])) ) {
1577 $body['background'] = 'transparent';
1578 }
1579 $body['quality'] = in_array($quality, ['low','medium','high','auto'], true)
1580 ? $quality
1581 : 'auto';
1582 }
1583
1584 add_action('http_api_curl', function ($handle, $r) {
1585 // increase connect timeout to 60s, and total timeout to 5m
1586 curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 60);
1587 curl_setopt($handle, CURLOPT_DNS_CACHE_TIMEOUT, 300);
1588 curl_setopt($handle, CURLOPT_TIMEOUT, 300);
1589 }, 10, 2);
1590
1591 // Call OpenAI Image Generations API
1592 $response = wp_remote_post(
1593 'https://api.openai.com/v1/images/generations',
1594 [
1595 'headers' => [
1596 'Authorization' => 'Bearer ' . $api_key,
1597 'Content-Type' => 'application/json',
1598 ],
1599 'body' => wp_json_encode($body),
1600 'timeout' => 300,
1601 ]
1602 );
1603 if (is_wp_error($response)) {
1604 wp_send_json_error(['message' => $response->get_error_message()], 500);
1605 }
1606
1607 $image_url = '';
1608
1609 if ($model === 'gpt-image-1') {
1610 // Grab and decode the base64
1611
1612 $data = json_decode(wp_remote_retrieve_body($response), true);
1613
1614 $image_base64 = $data['data'][0]['b64_json'];
1615
1616 $bytes = base64_decode($image_base64);
1617 if (! $bytes) {
1618 wp_send_json_error(['message' => 'Invalid image data from API.'], 500);
1619 }
1620
1621 // Create a temp file and write it
1622 $tmp = wp_tempnam('gpt-image-1.png');
1623 if (! $tmp || ! file_put_contents($tmp, $bytes)) {
1624 wp_send_json_error(['message' => 'Failed to write temp image file.'], 500);
1625 }
1626
1627 // Prepare for sideload
1628 $file = [
1629 'name' => substr(sanitize_file_name($prompt), 0, 100) . '.png',
1630 'tmp_name' => $tmp,
1631 ];
1632
1633 // Make sure these are loaded
1634 require_once ABSPATH . 'wp-admin/includes/image.php';
1635 require_once ABSPATH . 'wp-admin/includes/file.php';
1636 require_once ABSPATH . 'wp-admin/includes/media.php';
1637
1638 // Sideload into the Media Library
1639 $attach_id = media_handle_sideload($file, 0, $prompt);
1640 if (is_wp_error($attach_id)) {
1641 wp_send_json_error(['message' => $attach_id->get_error_message()], 500);
1642 }
1643
1644 $url = wp_get_attachment_url($attach_id);
1645 wp_send_json_success(['attachment_id' => $attach_id, 'url' => $url]);
1646 } else {
1647
1648
1649 $code = wp_remote_retrieve_response_code($response);
1650 $data = json_decode(wp_remote_retrieve_body($response), true);
1651 if ($code !== 200 || empty($data['data'][0]['url'])) {
1652 $error_msg = $data['error']['message'] ?? esc_html__('AI image generation error.', 'king-addons');
1653 wp_send_json_error(['message' => $error_msg], 500);
1654 }
1655
1656 // Sideload image into media library
1657 require_once ABSPATH . 'wp-admin/includes/image.php';
1658 require_once ABSPATH . 'wp-admin/includes/file.php';
1659 require_once ABSPATH . 'wp-admin/includes/media.php';
1660
1661 if ($model === 'dall-e-3') {
1662 $image_url = esc_url_raw($data['data'][0]['url']);
1663 }
1664
1665 $attachment_id = media_sideload_image($image_url, 0, $prompt, 'id');
1666
1667 if (is_wp_error($attachment_id)) {
1668 wp_send_json_error(['message' => $attachment_id->get_error_message()], 500);
1669 }
1670 $attachment_url = wp_get_attachment_url($attachment_id);
1671
1672 // Respond with attachment details
1673 wp_send_json_success([
1674 'attachment_id' => $attachment_id,
1675 'url' => $attachment_url,
1676 ]);
1677 }
1678 }
1679
1680 /**
1681 * Renders the Image Detail Level dropdown for Alt Text Settings.
1682 *
1683 * @return void
1684 */
1685 public function renderAiAltTextImageDetailLevelField(): void
1686 {
1687 $options = get_option('king_addons_ai_options', []);
1688 $selected = $options['ai_alt_text_image_detail_level'] ?? 'low';
1689 echo '<select name="king_addons_ai_options[ai_alt_text_image_detail_level]">';
1690 echo '<option value="low"' . selected($selected, 'low', false) . '>' . esc_html__('Low', 'king-addons') . '</option>';
1691 echo '<option value="high"' . selected($selected, 'high', false) . '>' . esc_html__('High', 'king-addons') . '</option>';
1692 echo '</select>';
1693 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>';
1694 }
1695
1696 /**
1697 * Renders the Translation Settings section description.
1698 *
1699 * @return void
1700 */
1701 public function renderAiTranslationSection(): void
1702 {
1703 echo '<p>' . esc_html__('Configure AI Page Translator settings for Elementor editor.', 'king-addons') . '</p>';
1704 }
1705
1706 /**
1707 * Renders the Enable AI Page Translator checkbox field.
1708 *
1709 * @return void
1710 */
1711 public function renderAiPageTranslatorField(): void
1712 {
1713 $options = get_option('king_addons_ai_options', []);
1714 // Default to true if option has never been saved, otherwise use saved value
1715 $enabled = array_key_exists('enable_ai_page_translator', $options) ? (bool) $options['enable_ai_page_translator'] : true;
1716 printf(
1717 '<label><input type="checkbox" name="king_addons_ai_options[enable_ai_page_translator]" value="1" %s /> %s</label>',
1718 checked($enabled, true, false),
1719 esc_html__('Show AI Page Translator button in Elementor editor toolbar', 'king-addons')
1720 );
1721 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>';
1722 }
1723
1724 /**
1725 * AJAX handler to translate text using OpenAI.
1726 *
1727 * @return void
1728 */
1729 public function handleAiTranslateText(): void
1730 {
1731 check_ajax_referer('king_addons_ai_generate_nonce', 'nonce');
1732 if (!current_user_can('manage_options')) {
1733 wp_send_json_error(['message' => esc_html__('Permission denied.', 'king-addons')], 403);
1734 }
1735
1736 $text = isset($_POST['text']) ? sanitize_textarea_field(wp_unslash($_POST['text'])) : '';
1737 $from_lang = isset($_POST['from_lang']) ? sanitize_text_field(wp_unslash($_POST['from_lang'])) : 'auto';
1738 $to_lang = isset($_POST['to_lang']) ? sanitize_text_field(wp_unslash($_POST['to_lang'])) : 'en';
1739
1740 $options = get_option('king_addons_ai_options', []);
1741 $api_key = $options['openai_api_key'] ?? '';
1742 $model = $options['openai_model'] ?? '';
1743
1744 if (empty($api_key) || empty($model)) {
1745 wp_send_json_error(['message' => esc_html__('API key or model not set.', 'king-addons')], 400);
1746 }
1747
1748 if (empty($text)) {
1749 wp_send_json_error(['message' => esc_html__('No text provided for translation.', 'king-addons')], 400);
1750 }
1751
1752 // Check daily token limit
1753 $daily_limit = isset($options['daily_token_limit']) ? intval($options['daily_token_limit']) : self::DEFAULT_DAILY_TOKEN_LIMIT;
1754 $current_usage = $this->getAiDailyUsage();
1755
1756 if ($daily_limit > 0 && $current_usage >= $daily_limit) {
1757 wp_send_json_error([
1758 'message' => esc_html__('Daily token limit reached. Please try again tomorrow or increase the limit in AI Settings.', 'king-addons')
1759 ], 429);
1760 }
1761
1762 // Prepare translation prompt
1763 $from_lang_name = ($from_lang === 'auto') ? 'auto-detected language' : $this->getLanguageName($from_lang);
1764 $to_lang_name = $this->getLanguageName($to_lang);
1765
1766 // Enhanced system message for better custom language and prompt handling
1767 $system_message = 'You are a professional translator with expertise in languages, dialects, writing styles, and custom translation approaches. You can handle:
1768
1769 1. Standard languages (English, Spanish, etc.)
1770 2. Fictional/constructed languages (Klingon, Dothraki, Elvish, etc.)
1771 3. Historical language variants (Old English, Latin, etc.)
1772 4. Writing styles and tones (formal, casual, academic, business, etc.)
1773 5. Special communication styles (pirate speak, baby talk, technical jargon, etc.)
1774
1775 When translating:
1776 - Maintain the original meaning, tone, and formatting
1777 - Preserve HTML tags exactly as they appear
1778 - For custom languages, apply consistent linguistic rules
1779 - For style prompts, adapt the tone and vocabulary appropriately
1780 - Only return the translated/adapted text without explanations
1781
1782 If the target is a style rather than a language, transform the text to match that style while keeping the same language.';
1783
1784 // Enhanced user message with better context for custom languages and prompts
1785 if ($from_lang === 'auto') {
1786 $user_message = "Transform the following text to {$to_lang_name}:\n\n{$text}";
1787 } else {
1788 // Check if it looks like a style prompt rather than a language
1789 $is_style_prompt = $this->isStylePrompt($to_lang_name);
1790
1791 if ($is_style_prompt) {
1792 $user_message = "Transform the following text from {$from_lang_name} using this style/approach: {$to_lang_name}:\n\n{$text}";
1793 } else {
1794 $user_message = "Translate the following text from {$from_lang_name} to {$to_lang_name}:\n\n{$text}";
1795 }
1796 }
1797
1798 $messages = [
1799 ['role' => 'system', 'content' => $system_message],
1800 ['role' => 'user', 'content' => $user_message]
1801 ];
1802
1803 $response = wp_remote_post(
1804 'https://api.openai.com/v1/chat/completions',
1805 [
1806 'headers' => [
1807 'Authorization' => 'Bearer ' . $api_key,
1808 'Content-Type' => 'application/json',
1809 ],
1810 'body' => wp_json_encode([
1811 'model' => $model,
1812 'messages' => $messages,
1813 'max_tokens' => 1000,
1814 'temperature' => 0.3, // Lower temperature for more consistent translations
1815 ]),
1816 'timeout' => 30,
1817 ]
1818 );
1819
1820 if (is_wp_error($response)) {
1821 wp_send_json_error(['message' => $response->get_error_message()], 500);
1822 }
1823
1824 $code = wp_remote_retrieve_response_code($response);
1825 $data = json_decode(wp_remote_retrieve_body($response), true);
1826
1827 if ($code !== 200 || empty($data['choices'][0]['message']['content'])) {
1828 $error_msg = $data['error']['message'] ?? esc_html__('AI translation error.', 'king-addons');
1829 wp_send_json_error(['message' => $error_msg], 500);
1830 }
1831
1832 $translated_text = trim($data['choices'][0]['message']['content']);
1833
1834 // Update token usage statistics if present in the response
1835 if (isset($data['usage']['total_tokens'])) {
1836 $this->incrementAiDailyUsage(intval($data['usage']['total_tokens']));
1837 }
1838
1839 wp_send_json_success([
1840 'translated_text' => $translated_text,
1841 'usage' => [
1842 'tokens_used' => $data['usage']['total_tokens'] ?? 0,
1843 'daily_used' => $this->getAiDailyUsage(),
1844 'daily_limit' => $daily_limit,
1845 ]
1846 ]);
1847 }
1848
1849 /**
1850 * Get language name by code
1851 *
1852 * @param string $code Language code
1853 * @return string Language name
1854 */
1855 private function getLanguageName(string $code): string
1856 {
1857 $languages = [
1858 'en' => 'English',
1859 'es' => 'Spanish',
1860 'fr' => 'French',
1861 'de' => 'German',
1862 'it' => 'Italian',
1863 'pt' => 'Portuguese',
1864 'ru' => 'Russian',
1865 'ja' => 'Japanese',
1866 'ko' => 'Korean',
1867 'zh' => 'Chinese',
1868 'ar' => 'Arabic',
1869 'hi' => 'Hindi',
1870 'nl' => 'Dutch',
1871 'pl' => 'Polish',
1872 'tr' => 'Turkish',
1873 'uk' => 'Ukrainian',
1874 'cs' => 'Czech',
1875 'sv' => 'Swedish',
1876 'no' => 'Norwegian',
1877 'da' => 'Danish',
1878 'fi' => 'Finnish'
1879 ];
1880
1881 return $languages[$code] ?? $code;
1882 }
1883
1884 /**
1885 * Check if the given text appears to be a style prompt rather than a language
1886 *
1887 * @param string $text The text to check
1888 * @return bool True if it looks like a style prompt
1889 */
1890 private function isStylePrompt(string $text): bool
1891 {
1892 $text_lower = strtolower($text);
1893
1894 // Common style/tone indicators
1895 $style_indicators = [
1896 'formal', 'casual', 'professional', 'business', 'academic', 'technical',
1897 'friendly', 'serious', 'humorous', 'dramatic', 'poetic', 'simple',
1898 'complex', 'detailed', 'brief', 'conversational', 'literary',
1899 'scientific', 'medical', 'legal', 'marketing', 'sales',
1900 'tone', 'style', 'manner', 'approach', 'way', 'voice',
1901 'pirate', 'shakespeare', 'baby', 'child', 'elderly',
1902 'slang', 'jargon', 'dialect', 'accent'
1903 ];
1904
1905 // Check if any style indicators are present
1906 foreach ($style_indicators as $indicator) {
1907 if (strpos($text_lower, $indicator) !== false) {
1908 return true;
1909 }
1910 }
1911
1912 // Check if it contains descriptive phrases
1913 $descriptive_patterns = [
1914 'for ', 'like ', 'as if ', 'in the style of', 'in a ', 'with a ',
1915 'using ', 'speaking ', 'written ', 'sound like', 'talk like'
1916 ];
1917
1918 foreach ($descriptive_patterns as $pattern) {
1919 if (strpos($text_lower, $pattern) !== false) {
1920 return true;
1921 }
1922 }
1923
1924 return false;
1925 }
1926 }
1927