PluginProbe
Elementor Website Builder – more than just a page builder / 4.2.0
Elementor Website Builder – more than just a page builder v4.2.0
4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
elementor / modules / ai / module.php

module.php in Elementor Website Builder – more than just a page builder 4.2.0, at modules/ai/module.php

1,598 lines 43.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Modules\Ai;
3
4 use Elementor\Controls_Manager;
5 use Elementor\Core\Base\Module as BaseModule;
6 use Elementor\Core\Common\Modules\Connect\Module as ConnectModule;
7 use Elementor\Element_Base;
8 use Elementor\Modules\Ai\Feature_Intro\Product_Image_Unification_Intro;
9 use Elementor\Plugin;
10 use Elementor\Core\Utils\Collection;
11 use Elementor\Modules\Ai\Connect\Ai;
12 use Elementor\User;
13 use Elementor\Utils;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 class Module extends BaseModule {
20 const HISTORY_TYPE_ALL = 'all';
21 const HISTORY_TYPE_TEXT = 'text';
22 const HISTORY_TYPE_CODE = 'code';
23 const HISTORY_TYPE_IMAGE = 'images';
24 const HISTORY_TYPE_BLOCK = 'blocks';
25 const VALID_HISTORY_TYPES = [
26 self::HISTORY_TYPE_ALL,
27 self::HISTORY_TYPE_TEXT,
28 self::HISTORY_TYPE_CODE,
29 self::HISTORY_TYPE_IMAGE,
30 self::HISTORY_TYPE_BLOCK,
31 ];
32 const MIN_PAGES_FOR_CREATE_WITH_AI_BANNER = 10;
33
34 public function get_name() {
35 return 'ai';
36 }
37
38 public function __construct() {
39 parent::__construct();
40
41 ( new SitePlannerConnect\Module() );
42
43 if ( is_admin() ) {
44 ( new Preferences() )->register();
45 add_action( 'elementor/import-export/import-kit/runner/after-run', [ $this, 'handle_kit_install' ] );
46 }
47
48 if ( ! $this->is_ai_enabled() ) {
49 return;
50 }
51
52 add_filter( 'elementor/core/admin/homescreen', [ $this, 'add_create_with_ai_banner_to_homescreen' ] );
53
54 add_action( 'elementor/connect/apps/register', function ( ConnectModule $connect_module ) {
55 $connect_module->register_app( 'ai', Ai::get_class_name() );
56 } );
57
58 add_action( 'elementor/ajax/register_actions', function( $ajax ) {
59 $handlers = [
60 'ai_get_user_information' => [ $this, 'ajax_ai_get_user_information' ],
61 'ai_get_remote_config' => [ $this, 'ajax_ai_get_remote_config' ],
62 'ai_get_remote_frontend_config' => [ $this, 'ajax_ai_get_remote_frontend_config' ],
63 'ai_get_completion_text' => [ $this, 'ajax_ai_get_completion_text' ],
64 'ai_get_excerpt' => [ $this, 'ajax_ai_get_excerpt' ],
65 'ai_get_featured_image' => [ $this, 'ajax_ai_get_featured_image' ],
66 'ai_get_edit_text' => [ $this, 'ajax_ai_get_edit_text' ],
67 'ai_get_custom_code' => [ $this, 'ajax_ai_get_custom_code' ],
68 'ai_get_custom_css' => [ $this, 'ajax_ai_get_custom_css' ],
69 'ai_set_get_started' => [ $this, 'ajax_ai_set_get_started' ],
70 'ai_set_status_feedback' => [ $this, 'ajax_ai_set_status_feedback' ],
71 'ai_get_image_prompt_enhancer' => [ $this, 'ajax_ai_get_image_prompt_enhancer' ],
72 'ai_get_text_to_image' => [ $this, 'ajax_ai_get_text_to_image' ],
73 'ai_get_image_to_image' => [ $this, 'ajax_ai_get_image_to_image' ],
74 'ai_get_image_to_image_mask' => [ $this, 'ajax_ai_get_image_to_image_mask' ],
75 'ai_get_image_to_image_mask_cleanup' => [ $this, 'ajax_ai_get_image_to_image_mask_cleanup' ],
76 'ai_get_image_to_image_outpainting' => [ $this, 'ajax_ai_get_image_to_image_outpainting' ],
77 'ai_get_image_to_image_upscale' => [ $this, 'ajax_ai_get_image_to_image_upscale' ],
78 'ai_get_image_to_image_remove_background' => [ $this, 'ajax_ai_get_image_to_image_remove_background' ],
79 'ai_get_image_to_image_replace_background' => [ $this, 'ajax_ai_get_image_to_image_replace_background' ],
80 'ai_upload_image' => [ $this, 'ajax_ai_upload_image' ],
81 'ai_generate_layout' => [ $this, 'ajax_ai_generate_layout' ],
82 'ai_get_layout_prompt_enhancer' => [ $this, 'ajax_ai_get_layout_prompt_enhancer' ],
83 'ai_get_history' => [ $this, 'ajax_ai_get_history' ],
84 'ai_delete_history_item' => [ $this, 'ajax_ai_delete_history_item' ],
85 'ai_toggle_favorite_history_item' => [ $this, 'ajax_ai_toggle_favorite_history_item' ],
86 'ai_get_product_image_unification' => [ $this, 'ajax_ai_get_product_image_unification' ],
87 'ai_get_animation' => [ $this, 'ajax_ai_get_animation' ],
88 'ai_get_image_to_image_isolate_objects' => [ $this, 'ajax_ai_get_product_image_unification' ],
89 ];
90
91 foreach ( $handlers as $tag => $callback ) {
92 $ajax->register_ajax_action( $tag, $callback );
93 }
94 } );
95
96 add_action( 'elementor/editor/before_enqueue_scripts', function() {
97 $this->enqueue_main_script();
98 $this->enqueue_layout_script();
99 } );
100
101 add_action( 'elementor/editor/after_enqueue_styles', function() {
102 wp_enqueue_style(
103 'elementor-ai-editor',
104 $this->get_css_assets_url( 'modules/ai/editor' ),
105 [],
106 ELEMENTOR_VERSION
107 );
108 } );
109
110 add_action( 'elementor/preview/enqueue_styles', function() {
111 wp_enqueue_style(
112 'elementor-ai-layout-preview',
113 $this->get_css_assets_url( 'modules/ai/layout-preview' ),
114 [],
115 ELEMENTOR_VERSION
116 );
117 } );
118
119 if ( is_admin() ) {
120 add_action( 'wp_enqueue_media', [ $this, 'enqueue_ai_media_library' ] );
121 add_action( 'admin_head', [ $this, 'enqueue_ai_media_library_upload_screen' ] );
122
123 if ( current_user_can( 'edit_products' ) || current_user_can( 'publish_products' ) ) {
124 add_action( 'admin_init', [ $this, 'enqueue_ai_products_page_scripts' ] );
125 add_action( 'current_screen', [ $this, 'enqueue_ai_single_product_page_scripts' ] );
126 add_action( 'wp_ajax_elementor-ai-get-product-images', [ $this, 'get_product_images_ajax' ] );
127 add_action( 'wp_ajax_elementor-ai-set-product-images', [ $this, 'set_product_images_ajax' ] );
128 Product_Image_Unification_Intro::add_hooks();
129 }
130 }
131
132 add_action( 'enqueue_block_editor_assets', function() {
133 wp_enqueue_script( 'elementor-ai-gutenberg',
134 $this->get_js_assets_url( 'ai-gutenberg' ),
135 [
136 'jquery',
137 'elementor-v2-ui',
138 'elementor-v2-icons',
139 'wp-blocks',
140 'wp-element',
141 'wp-editor',
142 'wp-data',
143 'wp-components',
144 'wp-compose',
145 'wp-i18n',
146 'wp-hooks',
147 'elementor-ai-media-library',
148 ],
149 ELEMENTOR_VERSION, true );
150
151 wp_localize_script(
152 'elementor-ai-gutenberg',
153 'ElementorAiConfig',
154 [
155 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
156 'connect_url' => $this->get_ai_connect_url(),
157 ]
158 );
159
160 wp_set_script_translations( 'elementor-ai-gutenberg', 'elementor' );
161 });
162
163 add_filter( 'elementor/document/save/data', function ( $data ) {
164 return $this->remove_temporary_containers( $data );
165 } );
166
167 add_action( 'elementor/element/common/section_effects/after_section_start', [ $this, 'register_ai_motion_effect_control' ], 10, 1 );
168 add_action( 'elementor/element/container/section_effects/after_section_start', [ $this, 'register_ai_motion_effect_control' ], 10, 1 );
169 add_action( 'elementor/element/common/_section_transform/after_section_end', [ $this, 'register_ai_hover_effect_control' ], 10, 1 );
170 add_action( 'elementor/element/container/_section_transform/after_section_end', [ $this, 'register_ai_hover_effect_control' ], 10, 1 );
171 }
172
173 public function is_ai_enabled() {
174 if ( ! Plugin::$instance->experiments->is_feature_active( 'container' ) ) {
175 return false;
176 }
177
178 return Preferences::is_ai_enabled( get_current_user_id() );
179 }
180
181 public function handle_kit_install( $imported_data ) {
182 if ( ! $this->is_ai_enabled() ) {
183 return;
184 }
185
186 if ( ! isset( $imported_data['status'] ) || 'success' !== $imported_data['status'] ) {
187 return;
188 }
189
190 if ( ! isset( $imported_data['runner'] ) || 'site-settings' !== $imported_data['runner'] ) {
191 return;
192 }
193
194 if ( ! isset( $imported_data['configData']['lastImportedSession']['instance_data']['site_settings']['settings']['ai'] ) ) {
195 return;
196 }
197
198 $is_connected = $this->get_ai_app()->is_connected() && User::get_introduction_meta( 'ai_get_started' );
199
200 if ( ! $is_connected ) {
201 return;
202 }
203
204 $last_imported_session = $imported_data['configData']['lastImportedSession'];
205 $imported_ai_data = $last_imported_session['instance_data']['site_settings']['settings']['ai'];
206
207 $this->get_ai_app()->send_event( [
208 'name' => 'kit_installed',
209 'data' => $imported_ai_data,
210 'client' => [
211 'name' => 'elementor',
212 'version' => ELEMENTOR_VERSION,
213 'session_id' => $last_imported_session['session_id'],
214 ],
215 ] );
216 }
217
218 public function register_ai_hover_effect_control( Element_Base $element ) {
219 if ( ! $element->get_controls( 'ai_hover_animation' ) ) {
220 $element->add_control(
221 'ai_hover_animation',
222 [
223 'tabs_wrapper' => '_tabs_positioning',
224 'inner_tab' => '_tab_positioning_hover',
225 'label' => esc_html__( 'Animate With AI', 'elementor' ),
226 'type' => Controls_Manager::RAW_HTML,
227 'raw' => '
228 <style>
229 .elementor-control-ai_hover_animation .elementor-control-content {
230 display: flex;
231 flex-direction: row;
232 justify-content: space-between;
233 align-items: center;
234 }
235 .elementor-control-ai_hover_animation .elementor-control-raw-html {
236 display: none;
237 }
238 </style>',
239 'render_type' => 'none',
240 'ai' => [
241 'active' => true,
242 'type' => 'hover_animation',
243 ],
244 ],
245 [
246 'position' => [
247 'of' => '_transform_rotate_popover_hover',
248 'type' => 'control',
249 'at' => 'before',
250 ],
251 ]
252 );
253 }
254 }
255 public function register_ai_motion_effect_control( $element ) {
256 if ( Utils::has_pro() && ! $element->get_controls( 'ai_animation' ) ) {
257 $element->add_control(
258 'ai_animation',
259 [
260 'label' => esc_html__( 'Animate With AI', 'elementor' ),
261 'type' => Controls_Manager::RAW_HTML,
262 'raw' => '
263 <style>
264 .elementor-control-ai_animation .elementor-control-content {
265 display: flex;
266 flex-direction: row;
267 justify-content: space-between;
268 align-items: center;
269 }
270 .elementor-control-ai_animation .elementor-control-raw-html {
271 display: none;
272 }
273 </style>',
274 'render_type' => 'none',
275 'ai' => [
276 'active' => true,
277 'type' => 'animation',
278 ],
279 ]
280 );
281 }
282 }
283
284 private function get_current_screen() {
285 $is_wc = class_exists( 'WooCommerce' ) && post_type_exists( 'product' );
286 if ( ! $is_wc ) {
287 return 'other';
288 }
289
290 $is_products_page = isset( $_GET['post_type'] ) && 'product' === $_GET['post_type'];
291 if ( $is_products_page ) {
292 return 'wc-products';
293 }
294
295 $screen = get_current_screen();
296 $is_single_product_page = isset( $screen->post_type ) && ( 'product' === $screen->post_type && 'post' === $screen->base );
297 if ( $is_single_product_page ) {
298 return 'wc-single-product';
299 }
300
301 return 'other';
302 }
303
304 public function enqueue_ai_products_page_scripts() {
305 if ( 'wc-products' !== $this->get_current_screen() ) {
306 return;
307 }
308
309 $this->add_wc_scripts();
310 }
311
312 public function enqueue_ai_single_product_page_scripts() {
313 if ( 'wc-single-product' !== $this->get_current_screen() ) {
314 return;
315 }
316
317 $this->add_wc_scripts();
318 }
319
320 private function add_products_bulk_action( $bulk_actions ) {
321 $bulk_actions['elementor-ai-unify-product-images'] = __( 'Unify with Elementor AI', 'elementor' );
322 return $bulk_actions;
323 }
324
325 public function get_product_images_ajax() {
326 check_ajax_referer( 'elementor-ai-unify-product-images_nonce', 'nonce' );
327
328 $post_ids = isset( $_POST['post_ids'] ) ? array_map( 'intval', $_POST['post_ids'] ) : [];
329 $is_galley_only = isset( $_POST['is_galley_only'] ) && sanitize_text_field( wp_unslash( $_POST['is_galley_only'] ) );
330
331 $image_ids = [];
332
333 foreach ( $post_ids as $post_id ) {
334 if ( $is_galley_only ) {
335 $product = wc_get_product( $post_id );
336 $gallery_image_ids = $product->get_gallery_image_ids();
337 foreach ( $gallery_image_ids as $image_id ) {
338 $image_ids[] = [
339 'productId' => $post_id,
340 'id' => $image_id,
341 'image_url' => wp_get_attachment_url( $image_id ),
342 ];
343 }
344 continue;
345 }
346
347 $image_id = get_post_thumbnail_id( $post_id );
348
349 if ( ! $image_id ) {
350 $product = wc_get_product( $post_id );
351 $gallery_image_ids = $product->get_gallery_image_ids();
352 if ( ! empty( $gallery_image_ids ) ) {
353 $image_id = $gallery_image_ids[0];
354 }
355 }
356
357 $image_ids[] = [
358 'productId' => $post_id,
359 'id' => $image_id ? $image_id : 'No Image',
360 'image_url' => $image_id ? wp_get_attachment_url( $image_id ) : 'No Image',
361 ];
362 }
363
364 wp_send_json_success( [ 'product_images' => array_slice( $image_ids, 0, 10 ) ] );
365
366 wp_die();
367 }
368
369 private function get_attachment_id_by_url( $url ) {
370 $attachments = get_posts( [
371 'post_type' => 'attachment',
372 'meta_query' => [
373 [
374 'key' => '_wp_attached_file',
375 'value' => basename( $url ),
376 'compare' => 'LIKE',
377 ],
378 ],
379 'fields' => 'ids',
380 'numberposts' => 1,
381 ] );
382
383 return ! empty( $attachments ) ? $attachments[0] : null;
384 }
385
386 public function set_product_images_ajax() {
387 check_ajax_referer( 'elementor-ai-unify-product-images_nonce', 'nonce' );
388
389 $product_id = isset( $_POST['productId'] ) ? sanitize_text_field( wp_unslash( $_POST['productId'] ) ) : '';
390 $image_url = isset( $_POST['image_url'] ) ? sanitize_text_field( wp_unslash( $_POST['image_url'] ) ) : '';
391 $image_to_add = isset( $_POST['image_to_add'] ) ? intval( wp_unslash( $_POST['image_to_add'] ) ) : null;
392 $image_to_remove = isset( $_POST['image_to_remove'] ) ? intval( wp_unslash( $_POST['image_to_remove'] ) ) : null;
393 $is_product_gallery = isset( $_POST['is_product_gallery'] ) && sanitize_text_field( wp_unslash( $_POST['is_product_gallery'] ) ) === 'true';
394
395 if ( ! $product_id || ! $image_url ) {
396 throw new \Exception( 'Product ID and Image URL are required' );
397 }
398
399 $product = wc_get_product( $product_id );
400 if ( ! $product ) {
401 throw new \Exception( 'Product not found' );
402 }
403
404 $attachment_id = $this->get_attachment_id_by_url( $image_url );
405 if ( is_wp_error( $attachment_id ) ) {
406 throw new \Exception( 'Image upload failed' );
407 }
408
409 if ( $is_product_gallery ) {
410 $this->update_product_gallery( $product, $image_to_remove, $image_to_add );
411 } else {
412 $product->set_image_id( $attachment_id );
413 $product->save();
414 }
415
416 wp_send_json_success( [
417 'message' => __( 'Image added successfully', 'elementor' ),
418 ] );
419 }
420
421 public function enqueue_ai_media_library_upload_screen() {
422 $screen = get_current_screen();
423 if ( ! $screen || 'upload' !== $screen->id ) {
424 return;
425 }
426
427 $this->enqueue_ai_media_library();
428 }
429
430 public function enqueue_ai_media_library() {
431 wp_enqueue_script( 'elementor-ai-media-library',
432 $this->get_js_assets_url( 'ai-media-library' ),
433 [
434 'jquery',
435 'elementor-v2-ui',
436 'elementor-v2-icons',
437 'media-grid',
438 ],
439 ELEMENTOR_VERSION,
440 true
441 );
442
443 wp_localize_script(
444 'elementor-ai-media-library',
445 'ElementorAiConfig',
446 [
447 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
448 'connect_url' => $this->get_ai_connect_url(),
449 ]
450 );
451
452 wp_set_script_translations( 'elementor-ai-media-library', 'elementor' );
453 }
454
455 private function enqueue_main_script() {
456 wp_enqueue_script(
457 'elementor-ai',
458 $this->get_js_assets_url( 'ai' ),
459 [
460 'react',
461 'react-dom',
462 'backbone-marionette',
463 'elementor-web-cli',
464 'wp-date',
465 'elementor-common',
466 'elementor-editor-modules',
467 'elementor-editor-document',
468 'elementor-v2-ui',
469 'elementor-v2-icons',
470 ],
471 ELEMENTOR_VERSION,
472 true
473 );
474
475 $config = [
476 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
477 'connect_url' => $this->get_ai_connect_url(),
478 ];
479
480 wp_localize_script(
481 'elementor-ai',
482 'ElementorAiConfig',
483 $config
484 );
485
486 wp_set_script_translations( 'elementor-ai', 'elementor' );
487 }
488
489 private function enqueue_layout_script() {
490 wp_enqueue_script(
491 'elementor-ai-layout',
492 $this->get_js_assets_url( 'ai-layout' ),
493 [
494 'react',
495 'react-dom',
496 'backbone-marionette',
497 'elementor-common',
498 'elementor-web-cli',
499 'elementor-editor-modules',
500 'elementor-ai',
501 'elementor-v2-ui',
502 'elementor-v2-icons',
503 ],
504 ELEMENTOR_VERSION,
505 true
506 );
507
508 wp_set_script_translations( 'elementor-ai-layout', 'elementor' );
509 }
510
511 private function remove_temporary_containers( $data ) {
512 if ( empty( $data['elements'] ) || ! is_array( $data['elements'] ) ) {
513 return $data;
514 }
515
516 // If for some reason the document has been saved during an AI Layout session,
517 // ensure that the temporary containers are removed from the data.
518 $data['elements'] = array_filter( $data['elements'], function( $element ) {
519 $is_preview_container = strpos( $element['id'], 'e-ai-preview-container' ) === 0;
520 $is_screenshot_container = strpos( $element['id'], 'e-ai-screenshot-container' ) === 0;
521
522 return ! $is_preview_container && ! $is_screenshot_container;
523 } );
524
525 return $data;
526 }
527
528 private function get_ai_connect_url() {
529 $app = $this->get_ai_app();
530
531 return $app->get_admin_url( 'authorize', [
532 'utm_source' => 'ai-popup',
533 'utm_campaign' => 'connect-account',
534 'utm_medium' => 'wp-dash',
535 'source' => 'generic',
536 ] );
537 }
538
539 public function ajax_ai_get_user_information( $data ) {
540 $app = $this->get_ai_app();
541
542 if ( ! $app->is_connected() ) {
543 return [
544 'is_connected' => false,
545 'connect_url' => $this->get_ai_connect_url(),
546 ];
547 }
548
549 $user_usage = wp_parse_args( $app->get_usage(), [
550 'hasAiSubscription' => false,
551 'usedQuota' => 0,
552 'quota' => 100,
553 ] );
554
555 return [
556 'is_connected' => true,
557 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
558 'usage' => $user_usage,
559 ];
560 }
561
562 public function ajax_ai_get_remote_config() {
563 $app = $this->get_ai_app();
564
565 if ( ! $app->is_connected() ) {
566 return [];
567 }
568
569 return $app->get_remote_config();
570 }
571
572 public function ajax_ai_get_remote_frontend_config( $data ) {
573 $callback = function () use ( $data ) {
574 return $this->get_ai_app()->get_remote_frontend_config( $data );
575 };
576
577 return Utils::get_cached_callback( $callback, 'ai_remote_frontend_config-' . get_current_user_id(), HOUR_IN_SECONDS );
578 }
579
580 public function verify_upload_permissions( $data ) {
581 $referer = wp_get_referer();
582
583 if ( str_contains( $referer, 'wp-admin/upload.php' ) && current_user_can( 'upload_files' ) ) {
584 return;
585 }
586 $this->verify_permissions( $data['editor_post_id'] );
587 }
588
589 private function verify_permissions( $editor_post_id ) {
590 $document = Plugin::$instance->documents->get( $editor_post_id );
591
592 if ( ! $document ) {
593 throw new \Exception( 'Document not found' );
594 }
595
596 if ( $document->is_built_with_elementor() ) {
597 if ( ! $document->is_editable_by_current_user() ) {
598 throw new \Exception( 'Access denied' );
599 }
600 } elseif ( ! current_user_can( 'edit_post', $editor_post_id ) ) {
601 throw new \Exception( 'Access denied' );
602 }
603 }
604
605 public function ajax_ai_get_image_prompt_enhancer( $data ) {
606 $this->verify_upload_permissions( $data );
607
608 $app = $this->get_ai_app();
609
610 if ( empty( $data['prompt'] ) ) {
611 throw new \Exception( 'Missing prompt' );
612 }
613
614 if ( ! $app->is_connected() ) {
615 throw new \Exception( 'not_connected' );
616 }
617 $request_ids = $this->get_request_ids( $data['payload'] );
618
619 $result = $app->get_image_prompt_enhanced( $data['prompt'], [], $request_ids );
620 $this->throw_on_error( $result );
621
622 return [
623 'text' => $result['text'],
624 'response_id' => $result['responseId'],
625 'usage' => $result['usage'],
626 ];
627 }
628
629 public function ajax_ai_get_completion_text( $data ) {
630 $this->verify_permissions( $data['editor_post_id'] );
631
632 $app = $this->get_ai_app();
633
634 if ( empty( $data['payload']['prompt'] ) ) {
635 throw new \Exception( 'Missing prompt' );
636 }
637
638 if ( ! $app->is_connected() ) {
639 throw new \Exception( 'not_connected' );
640 }
641
642 $context = $this->get_request_context( $data );
643
644 $request_ids = $this->get_request_ids( $data['payload'] );
645
646 $result = $app->get_completion_text( $data['payload']['prompt'], $context, $request_ids );
647 $this->throw_on_error( $result );
648
649 return [
650 'text' => $result['text'],
651 'response_id' => $result['responseId'],
652 'usage' => $result['usage'],
653 ];
654 }
655
656
657 public function ajax_ai_get_excerpt( $data ): array {
658 $app = $this->get_ai_app();
659
660 if ( empty( $data['payload']['content'] ) ) {
661 throw new \Exception( 'Missing content' );
662 }
663
664 if ( ! $app->is_connected() ) {
665 throw new \Exception( 'Not connected' );
666 }
667
668 $context = $this->get_request_context( $data );
669
670 $request_ids = $this->get_request_ids( $data['payload'] );
671
672 $result = $app->get_excerpt( $data['payload']['content'], $context, $request_ids );
673 $this->throw_on_error( $result );
674
675 return [
676 'text' => $result['text'],
677 'response_id' => $result['responseId'],
678 'usage' => $result['usage'],
679 ];
680 }
681
682 public function ajax_ai_get_featured_image( $data ): array {
683 $this->verify_upload_permissions( $data );
684
685 if ( empty( $data['payload']['prompt'] ) ) {
686 throw new \Exception( 'Missing prompt' );
687 }
688
689 $app = $this->get_ai_app();
690
691 if ( ! $app->is_connected() ) {
692 throw new \Exception( 'not_connected' );
693 }
694
695 $context = $this->get_request_context( $data );
696 $request_ids = $this->get_request_ids( $data['payload'] );
697
698 $result = $app->get_featured_image( $data, $context, $request_ids );
699
700 $this->throw_on_error( $result );
701
702 return [
703 'images' => $result['images'],
704 'response_id' => $result['responseId'],
705 'usage' => $result['usage'],
706 ];
707 }
708
709 private function get_ai_app(): Ai {
710 return Plugin::$instance->common->get_component( 'connect' )->get_app( 'ai' );
711 }
712
713 private function get_request_context( $data ) {
714 if ( empty( $data['context'] ) ) {
715 return [];
716 }
717
718 return $data['context'];
719 }
720
721 private function get_request_ids( $data ) {
722 if ( empty( $data['requestIds'] ) ) {
723 return new \stdClass();
724 }
725
726 return $data['requestIds'];
727 }
728
729 public function ajax_ai_get_edit_text( $data ) {
730 $this->verify_permissions( $data['editor_post_id'] );
731
732 $app = $this->get_ai_app();
733
734 if ( empty( $data['payload']['input'] ) ) {
735 throw new \Exception( 'Missing input' );
736 }
737
738 if ( empty( $data['payload']['instruction'] ) ) {
739 throw new \Exception( 'Missing instruction' );
740 }
741
742 if ( ! $app->is_connected() ) {
743 throw new \Exception( 'not_connected' );
744 }
745
746 $context = $this->get_request_context( $data );
747
748 $request_ids = $this->get_request_ids( $data['payload'] );
749
750 $result = $app->get_edit_text( $data, $context, $request_ids );
751 $this->throw_on_error( $result );
752
753 return [
754 'text' => $result['text'],
755 'response_id' => $result['responseId'],
756 'usage' => $result['usage'],
757 ];
758 }
759
760 public function ajax_ai_get_custom_code( $data ) {
761 $app = $this->get_ai_app();
762
763 if ( empty( $data['payload']['prompt'] ) ) {
764 throw new \Exception( 'Missing prompt' );
765 }
766
767 if ( empty( $data['payload']['language'] ) ) {
768 throw new \Exception( 'Missing language' );
769 }
770
771 if ( ! $app->is_connected() ) {
772 throw new \Exception( 'not_connected' );
773 }
774
775 $context = $this->get_request_context( $data );
776
777 $request_ids = $this->get_request_ids( $data['payload'] );
778
779 $result = $app->get_custom_code( $data, $context, $request_ids );
780 $this->throw_on_error( $result );
781
782 return [
783 'text' => $result['text'],
784 'response_id' => $result['responseId'],
785 'usage' => $result['usage'],
786 ];
787 }
788
789 public function ajax_ai_get_custom_css( $data ) {
790 $this->verify_permissions( $data['editor_post_id'] );
791
792 $app = $this->get_ai_app();
793
794 if ( empty( $data['payload']['prompt'] ) ) {
795 throw new \Exception( 'Missing prompt' );
796 }
797
798 if ( empty( $data['payload']['html_markup'] ) ) {
799 $data['html_markup'] = '';
800 }
801
802 if ( empty( $data['payload']['element_id'] ) ) {
803 throw new \Exception( 'Missing element_id' );
804 }
805
806 if ( ! $app->is_connected() ) {
807 throw new \Exception( 'not_connected' );
808 }
809
810 $context = $this->get_request_context( $data );
811 $request_ids = $this->get_request_ids( $data['payload'] );
812
813 $result = $app->get_custom_css( $data, $context, $request_ids );
814 $this->throw_on_error( $result );
815
816 return [
817 'text' => $result['text'],
818 'response_id' => $result['responseId'],
819 'usage' => $result['usage'],
820 ];
821 }
822
823 public function ajax_ai_set_get_started( $data ) {
824 $app = $this->get_ai_app();
825
826 User::set_introduction_viewed( [
827 'introductionKey' => 'ai_get_started',
828 ] );
829
830 return $app->set_get_started();
831 }
832
833 public function ajax_ai_set_status_feedback( $data ) {
834 if ( empty( $data['response_id'] ) ) {
835 throw new \Exception( 'Missing response_id' );
836 }
837
838 $app = $this->get_ai_app();
839
840 if ( ! $app->is_connected() ) {
841 throw new \Exception( 'not_connected' );
842 }
843
844 $app->set_status_feedback( $data['response_id'] );
845
846 return [];
847 }
848
849 public function ajax_ai_get_text_to_image( $data ) {
850 $this->verify_upload_permissions( $data );
851
852 if ( empty( $data['payload']['prompt'] ) ) {
853 throw new \Exception( 'Missing prompt' );
854 }
855
856 $app = $this->get_ai_app();
857
858 if ( ! $app->is_connected() ) {
859 throw new \Exception( 'not_connected' );
860 }
861
862 $context = $this->get_request_context( $data );
863 $request_ids = $this->get_request_ids( $data['payload'] );
864
865 $result = $app->get_text_to_image( $data, $context, $request_ids );
866
867 $this->throw_on_error( $result );
868
869 return [
870 'images' => $result['images'],
871 'response_id' => $result['responseId'],
872 'usage' => $result['usage'],
873 ];
874 }
875
876 public function ajax_ai_get_image_to_image( $data ) {
877 $this->verify_upload_permissions( $data );
878
879 $app = $this->get_ai_app();
880
881 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
882 throw new \Exception( 'Missing Image' );
883 }
884
885 if ( empty( $data['payload']['settings'] ) ) {
886 throw new \Exception( 'Missing prompt settings' );
887 }
888
889 if ( ! $app->is_connected() ) {
890 throw new \Exception( 'not_connected' );
891 }
892
893 $context = $this->get_request_context( $data );
894 $request_ids = $this->get_request_ids( $data['payload'] );
895
896 $result = $app->get_image_to_image( [
897 'prompt' => $data['payload']['prompt'],
898 'promptSettings' => $data['payload']['settings'],
899 'attachment_id' => $data['payload']['image']['id'],
900 ], $context, $request_ids );
901
902 $this->throw_on_error( $result );
903
904 return [
905 'images' => $result['images'],
906 'response_id' => $result['responseId'],
907 'usage' => $result['usage'],
908 ];
909 }
910
911 public function ajax_ai_get_image_to_image_upscale( $data ) {
912 $this->verify_upload_permissions( $data );
913
914 $app = $this->get_ai_app();
915
916 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
917 throw new \Exception( 'Missing Image' );
918 }
919
920 if ( empty( $data['payload']['promptSettings'] ) ) {
921 throw new \Exception( 'Missing prompt settings' );
922 }
923
924 if ( ! $app->is_connected() ) {
925 throw new \Exception( 'not_connected' );
926 }
927
928 $context = $this->get_request_context( $data );
929 $request_ids = $this->get_request_ids( $data['payload'] );
930
931 $result = $app->get_image_to_image_upscale( [
932 'promptSettings' => $data['payload']['promptSettings'],
933 'attachment_id' => $data['payload']['image']['id'],
934 ], $context, $request_ids );
935
936 $this->throw_on_error( $result );
937
938 return [
939 'images' => $result['images'],
940 'response_id' => $result['responseId'],
941 'usage' => $result['usage'],
942 ];
943 }
944
945 public function ajax_ai_get_image_to_image_replace_background( $data ) {
946 $this->verify_upload_permissions( $data );
947
948 $app = $this->get_ai_app();
949
950 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
951 throw new \Exception( 'Missing Image' );
952 }
953
954 if ( empty( $data['payload']['prompt'] ) ) {
955 throw new \Exception( 'Prompt Missing' );
956 }
957
958 if ( ! $app->is_connected() ) {
959 throw new \Exception( 'not_connected' );
960 }
961
962 $context = $this->get_request_context( $data );
963 $request_ids = $this->get_request_ids( $data['payload'] );
964
965 $result = $app->get_image_to_image_replace_background( [
966 'attachment_id' => $data['payload']['image']['id'],
967 'prompt' => $data['payload']['prompt'],
968 ], $context, $request_ids );
969
970 $this->throw_on_error( $result );
971
972 return [
973 'images' => $result['images'],
974 'response_id' => $result['responseId'],
975 'usage' => $result['usage'],
976 ];
977 }
978
979 public function ajax_ai_get_image_to_image_remove_background( $data ) {
980 $this->verify_upload_permissions( $data );
981
982 $app = $this->get_ai_app();
983
984 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
985 throw new \Exception( 'Missing Image' );
986 }
987
988 if ( ! $app->is_connected() ) {
989 throw new \Exception( 'not_connected' );
990 }
991
992 $context = $this->get_request_context( $data );
993 $request_ids = $this->get_request_ids( $data['payload'] );
994 $result = $app->get_image_to_image_remove_background( [
995 'attachment_id' => $data['payload']['image']['id'],
996 ], $context, $request_ids );
997
998 $this->throw_on_error( $result );
999
1000 return [
1001 'images' => $result['images'],
1002 'response_id' => $result['responseId'],
1003 'usage' => $result['usage'],
1004 ];
1005 }
1006
1007 public function ajax_ai_get_image_to_image_mask( $data ) {
1008 $this->verify_upload_permissions( $data );
1009
1010 $app = $this->get_ai_app();
1011
1012 if ( empty( $data['payload']['prompt'] ) ) {
1013 throw new \Exception( 'Missing prompt' );
1014 }
1015
1016 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
1017 throw new \Exception( 'Missing Image' );
1018 }
1019
1020 if ( empty( $data['payload']['settings'] ) ) {
1021 throw new \Exception( 'Missing prompt settings' );
1022 }
1023
1024 if ( empty( $data['payload']['mask'] ) ) {
1025 throw new \Exception( 'Missing Mask' );
1026 }
1027
1028 $context = $this->get_request_context( $data );
1029 $request_ids = $this->get_request_ids( $data['payload'] );
1030
1031 $result = $app->get_image_to_image_mask( [
1032 'prompt' => $data['payload']['prompt'],
1033 'attachment_id' => $data['payload']['image']['id'],
1034 'mask' => $data['payload']['mask'],
1035 ], $context, $request_ids );
1036
1037 $this->throw_on_error( $result );
1038
1039 return [
1040 'images' => $result['images'],
1041 'response_id' => $result['responseId'],
1042 'usage' => $result['usage'],
1043 ];
1044 }
1045 public function ajax_ai_get_image_to_image_mask_cleanup( $data ) {
1046 $this->verify_upload_permissions( $data );
1047
1048 $app = $this->get_ai_app();
1049
1050 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
1051 throw new \Exception( 'Missing Image' );
1052 }
1053
1054 if ( empty( $data['payload']['settings'] ) ) {
1055 throw new \Exception( 'Missing prompt settings' );
1056 }
1057
1058 if ( ! $app->is_connected() ) {
1059 throw new \Exception( 'not_connected' );
1060 }
1061
1062 if ( empty( $data['payload']['mask'] ) ) {
1063 throw new \Exception( 'Missing Mask' );
1064 }
1065
1066 $context = $this->get_request_context( $data );
1067 $request_ids = $this->get_request_ids( $data['payload'] );
1068
1069 $result = $app->get_image_to_image_mask_cleanup( [
1070 'attachment_id' => $data['payload']['image']['id'],
1071 'mask' => $data['payload']['mask'],
1072 ], $context, $request_ids );
1073
1074 $this->throw_on_error( $result );
1075
1076 return [
1077 'images' => $result['images'],
1078 'response_id' => $result['responseId'],
1079 'usage' => $result['usage'],
1080 ];
1081 }
1082
1083 public function ajax_ai_get_image_to_image_outpainting( $data ) {
1084 $this->verify_upload_permissions( $data );
1085
1086 $app = $this->get_ai_app();
1087
1088 if ( ! $app->is_connected() ) {
1089 throw new \Exception( 'not_connected' );
1090 }
1091
1092 if ( empty( $data['payload']['mask'] ) ) {
1093 throw new \Exception( 'Missing Expended Image' );
1094 }
1095
1096 $context = $this->get_request_context( $data );
1097 $request_ids = $this->get_request_ids( $data['payload'] );
1098 $result = $app->get_image_to_image_out_painting( [
1099 'size' => $data['payload']['size'],
1100 'position' => $data['payload']['position'],
1101 'mask' => $data['payload']['mask'],
1102 'image_base64' => $data['payload']['image_base64'],
1103 ], $context, $request_ids );
1104
1105 $this->throw_on_error( $result );
1106
1107 return [
1108 'images' => $result['images'],
1109 'response_id' => $result['responseId'],
1110 'usage' => $result['usage'],
1111 ];
1112 }
1113
1114 public function ajax_ai_upload_image( $data ) {
1115 $this->verify_upload_permissions( $data );
1116
1117 if ( empty( $data['image'] ) ) {
1118 throw new \Exception( 'Missing image data' );
1119 }
1120
1121 $image = $data['image'];
1122
1123 if ( empty( $image['image_url'] ) ) {
1124 throw new \Exception( 'Missing image_url' );
1125 }
1126
1127 $image_data = $this->upload_image( $image['image_url'], $data['prompt'], $data['editor_post_id'] );
1128
1129 if ( is_wp_error( $image_data ) ) {
1130 throw new \Exception( esc_html( $image_data->get_error_message() ) );
1131 }
1132
1133 if ( ! empty( $image['use_gallery_image'] ) && ! empty( $image['id'] ) ) {
1134 $app = $this->get_ai_app();
1135 $app->set_used_gallery_image( $image['id'] );
1136 }
1137
1138 return [
1139 'image' => array_merge( $image_data, $data ),
1140 ];
1141 }
1142
1143 public function ajax_ai_generate_layout( $data ) {
1144 $this->verify_permissions( $data['editor_post_id'] );
1145
1146 $app = $this->get_ai_app();
1147
1148 if ( empty( $data['prompt'] ) && empty( $data['attachments'] ) ) {
1149 throw new \Exception( 'Missing prompt / attachments' );
1150 }
1151
1152 if ( ! $app->is_connected() ) {
1153 throw new \Exception( 'not_connected' );
1154 }
1155
1156 $result = $app->generate_layout(
1157 $data,
1158 $this->prepare_generate_layout_context( $data )
1159 );
1160
1161 if ( is_wp_error( $result ) ) {
1162 $message = $result->get_error_message();
1163
1164 if ( is_array( $message ) ) {
1165 $message = implode( ', ', $message );
1166 throw new \Exception( esc_html( $message ) );
1167 }
1168
1169 $this->throw_on_error( $result );
1170 }
1171
1172 $elements = $result['text']['elements'] ?? [];
1173 $base_template_id = $result['baseTemplateId'] ?? null;
1174 $template_type = $result['templateType'] ?? null;
1175
1176 if ( empty( $elements ) || ! is_array( $elements ) ) {
1177 throw new \Exception( 'unknown_error' );
1178 }
1179
1180 if ( 1 === count( $elements ) ) {
1181 $template = $elements[0];
1182 } else {
1183 $template = [
1184 'elType' => 'container',
1185 'elements' => $elements,
1186 'settings' => [
1187 'content_width' => 'full',
1188 'flex_gap' => [
1189 'column' => '0',
1190 'row' => '0',
1191 'unit' => 'px',
1192 ],
1193 'padding' => [
1194 'unit' => 'px',
1195 'top' => '0',
1196 'right' => '0',
1197 'bottom' => '0',
1198 'left' => '0',
1199 'isLinked' => true,
1200 ],
1201 ],
1202 ];
1203 }
1204
1205 return [
1206 'all' => [],
1207 'text' => $template,
1208 'response_id' => $result['responseId'],
1209 'usage' => $result['usage'],
1210 'base_template_id' => $base_template_id,
1211 'template_type' => $template_type,
1212 ];
1213 }
1214
1215 public function ajax_ai_get_layout_prompt_enhancer( $data ) {
1216 $this->verify_permissions( $data['editor_post_id'] );
1217
1218 $app = $this->get_ai_app();
1219
1220 if ( empty( $data['prompt'] ) ) {
1221 throw new \Exception( 'Missing prompt' );
1222 }
1223
1224 if ( ! $app->is_connected() ) {
1225 throw new \Exception( 'not_connected' );
1226 }
1227
1228 $result = $app->get_layout_prompt_enhanced(
1229 $data['prompt'],
1230 $data['enhance_type'],
1231 $this->prepare_generate_layout_context( $data )
1232 );
1233
1234 $this->throw_on_error( $result );
1235
1236 return [
1237 'text' => $result['text'] ?? $data['prompt'],
1238 'response_id' => $result['responseId'] ?? '',
1239 'usage' => $result['usage'] ?? '',
1240 ];
1241 }
1242
1243 private function prepare_generate_layout_context( $data ) {
1244 $request_context = $this->get_request_context( $data );
1245 $kit = Plugin::$instance->kits_manager->get_active_kit();
1246
1247 if ( ! $kit ) {
1248 return $request_context;
1249 }
1250
1251 $kits_data = Collection::make( $kit->get_data()['settings'] ?? [] );
1252
1253 $colors = $kits_data
1254 ->filter( function ( $_, $key ) {
1255 return in_array( $key, [ 'system_colors', 'custom_colors' ], true );
1256 } )
1257 ->flatten()
1258 ->filter( function ( $val ) {
1259 return ! empty( $val['_id'] );
1260 } )
1261 ->map( function ( $val ) {
1262 return [
1263 'id' => $val['_id'],
1264 'label' => $val['title'] ?? null,
1265 'value' => $val['color'] ?? null,
1266 ];
1267 } );
1268
1269 $typography = $kits_data
1270 ->filter( function ( $_, $key ) {
1271 return in_array( $key, [ 'system_typography', 'custom_typography' ], true );
1272 } )
1273 ->flatten()
1274 ->filter( function ( $val ) {
1275 return ! empty( $val['_id'] );
1276 } )
1277 ->map( function ( $val ) {
1278 $font_size = null;
1279
1280 if ( isset(
1281 $val['typography_font_size']['unit'],
1282 $val['typography_font_size']['size']
1283 ) ) {
1284 $prop = $val['typography_font_size'];
1285
1286 $font_size = 'custom' === $prop['unit']
1287 ? $prop['size']
1288 : $prop['size'] . $prop['unit'];
1289 }
1290
1291 return [
1292 'id' => $val['_id'],
1293 'label' => $val['title'] ?? null,
1294 'value' => [
1295 'family' => $val['typography_font_family'] ?? null,
1296 'weight' => $val['typography_font_weight'] ?? null,
1297 'style' => $val['typography_font_style'] ?? null,
1298 'size' => $font_size,
1299 ],
1300 ];
1301 } );
1302
1303 $request_context['globals'] = [
1304 'colors' => $colors->all(),
1305 'typography' => $typography->all(),
1306 ];
1307
1308 return $request_context;
1309 }
1310
1311 private function upload_image( $image_url, $image_title, $parent_post_id = 0 ) {
1312 if ( ! current_user_can( 'upload_files' ) ) {
1313 throw new \Exception( 'Not Allowed to Upload images' );
1314 }
1315
1316 $uploads_manager = new \Elementor\Core\Files\Uploads_Manager();
1317 if ( $uploads_manager::are_unfiltered_uploads_enabled() ) {
1318 Plugin::$instance->uploads_manager->set_elementor_upload_state( true );
1319 add_filter( 'wp_handle_sideload_prefilter', [ Plugin::$instance->uploads_manager, 'handle_elementor_upload' ] );
1320 add_filter( 'image_sideload_extensions', function( $extensions ) {
1321 $extensions[] = 'svg';
1322 return $extensions;
1323 });
1324 }
1325
1326 $attachment_id = media_sideload_image( $image_url, $parent_post_id, $image_title, 'id' );
1327
1328 if ( is_wp_error( $attachment_id ) ) {
1329 return new \WP_Error( 'upload_error', $attachment_id->get_error_message() );
1330 }
1331
1332 if ( ! empty( $attachment_id['error'] ) ) {
1333 return new \WP_Error( 'upload_error', $attachment_id['error'] );
1334 }
1335
1336 return [
1337 'id' => $attachment_id,
1338 'url' => esc_url( wp_get_attachment_image_url( $attachment_id, 'full' ) ),
1339 'alt' => esc_attr( $image_title ),
1340 'source' => 'library',
1341 ];
1342 }
1343
1344 public function ajax_ai_get_history( $data ): array {
1345 $type = $data['type'] ?? self::HISTORY_TYPE_ALL;
1346
1347 if ( ! in_array( $type, self::VALID_HISTORY_TYPES, true ) ) {
1348 throw new \Exception( 'Invalid history type' );
1349 }
1350
1351 $page = sanitize_text_field( $data['page'] ?? 1 );
1352 $limit = sanitize_text_field( $data['limit'] ?? 10 );
1353
1354 $app = $this->get_ai_app();
1355
1356 if ( ! $app->is_connected() ) {
1357 throw new \Exception( 'not_connected' );
1358 }
1359
1360 $context = $this->get_request_context( $data );
1361
1362 $result = $app->get_history_by_type( $type, $page, $limit, $context );
1363
1364 if ( is_wp_error( $result ) ) {
1365 throw new \Exception( esc_html( $result->get_error_message() ) );
1366 }
1367
1368 return $result;
1369 }
1370
1371 public function ajax_ai_delete_history_item( $data ): array {
1372 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
1373 throw new \Exception( 'Missing id parameter' );
1374 }
1375
1376 $app = $this->get_ai_app();
1377
1378 if ( ! $app->is_connected() ) {
1379 throw new \Exception( 'not_connected' );
1380 }
1381
1382 $context = $this->get_request_context( $data );
1383
1384 $result = $app->delete_history_item( $data['id'], $context );
1385
1386 if ( is_wp_error( $result ) ) {
1387 throw new \Exception( esc_html( $result->get_error_message() ) );
1388 }
1389
1390 return [];
1391 }
1392
1393 public function ajax_ai_toggle_favorite_history_item( $data ): array {
1394 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
1395 throw new \Exception( 'Missing id parameter' );
1396 }
1397
1398 $app = $this->get_ai_app();
1399
1400 if ( ! $app->is_connected() ) {
1401 throw new \Exception( 'not_connected' );
1402 }
1403
1404 $context = $this->get_request_context( $data );
1405
1406 $result = $app->toggle_favorite_history_item( $data['id'], $context );
1407
1408 if ( is_wp_error( $result ) ) {
1409 throw new \Exception( esc_html( $result->get_error_message() ) );
1410 }
1411
1412 return [];
1413 }
1414
1415 public function ajax_ai_get_product_image_unification( $data ): array {
1416 if ( ! empty( $data['payload']['postId'] ) ) {
1417 $data['editor_post_id'] = $data['payload']['postId'];
1418 }
1419 $this->verify_upload_permissions( $data );
1420
1421 $app = $this->get_ai_app();
1422
1423 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
1424 throw new \Exception( 'Missing Image' );
1425 }
1426
1427 if ( empty( $data['payload']['settings'] ) ) {
1428 throw new \Exception( 'Missing prompt settings' );
1429 }
1430
1431 if ( ! $app->is_connected() ) {
1432 throw new \Exception( 'not_connected' );
1433 }
1434
1435 $context = $this->get_request_context( $data );
1436 $request_ids = $this->get_request_ids( $data['payload'] );
1437
1438 $result = $app->get_unify_product_images( [
1439 'promptSettings' => $data['payload']['settings'],
1440 'attachment_id' => $data['payload']['image']['id'],
1441 'featureIdentifier' => $data['payload']['featureIdentifier'] ?? '',
1442 ], $context, $request_ids );
1443
1444 $this->throw_on_error( $result );
1445
1446 return [
1447 'images' => $result['images'],
1448 'response_id' => $result['responseId'],
1449 'usage' => $result['usage'],
1450 ];
1451 }
1452
1453 public function ajax_ai_get_animation( $data ): array {
1454 $this->verify_upload_permissions( $data );
1455
1456 $app = $this->get_ai_app();
1457
1458 if ( empty( $data['payload']['prompt'] ) ) {
1459 throw new \Exception( 'Missing prompt' );
1460 }
1461
1462 if ( empty( $data['payload']['motionEffectType'] ) ) {
1463 throw new \Exception( 'Missing animation type' );
1464 }
1465
1466 if ( ! $app->is_connected() ) {
1467 throw new \Exception( 'not_connected' );
1468 }
1469
1470 $context = $this->get_request_context( $data );
1471
1472 $request_ids = $this->get_request_ids( $data['payload'] );
1473
1474 $result = $app->get_animation( $data, $context, $request_ids );
1475 $this->throw_on_error( $result );
1476
1477 return [
1478 'text' => $result['text'],
1479 'response_id' => $result['responseId'],
1480 'usage' => $result['usage'],
1481 ];
1482 }
1483
1484 /**
1485 * @param mixed $result
1486 */
1487 private function throw_on_error( $result ): void {
1488 if ( is_wp_error( $result ) ) {
1489 wp_send_json_error( [
1490 'message' => esc_html( $result->get_error_message() ),
1491 'extra_data' => $result->get_error_data(),
1492 ] );
1493 }
1494 }
1495
1496 /**
1497 * @return void
1498 */
1499 public function add_wc_scripts(): void {
1500 wp_enqueue_script( 'elementor-ai-unify-product-images',
1501 $this->get_js_assets_url( 'ai-unify-product-images' ),
1502 [
1503 'jquery',
1504 'elementor-v2-ui',
1505 'elementor-v2-icons',
1506 'wp-components',
1507 'elementor-common',
1508 ],
1509 ELEMENTOR_VERSION,
1510 true
1511 );
1512
1513 wp_localize_script(
1514 'elementor-ai-unify-product-images',
1515 'UnifyProductImagesConfig',
1516 [
1517 'get_product_images_url' => admin_url( 'admin-ajax.php' ),
1518 'set_product_images_url' => admin_url( 'admin-ajax.php' ),
1519 'nonce' => wp_create_nonce( 'elementor-ai-unify-product-images_nonce' ),
1520 'placeholder' => ELEMENTOR_ASSETS_URL . 'images/app/ai/product-image-unification-example.gif?' . ELEMENTOR_VERSION,
1521 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
1522 'connect_url' => $this->get_ai_connect_url(),
1523 ]
1524 );
1525
1526 add_filter( 'bulk_actions-edit-product', function ( $data ) {
1527 return $this->add_products_bulk_action( $data );
1528 });
1529
1530 wp_set_script_translations( 'elementor-ai-unify-product-images', 'elementor' );
1531 }
1532
1533 /**
1534 * @param $product
1535 * @param int|null $image_to_remove
1536 * @param int|null $image_to_add
1537 * @return void
1538 */
1539 private function update_product_gallery( $product, ?int $image_to_remove, ?int $image_to_add ): void {
1540 $gallery_image_ids = $product->get_gallery_image_ids();
1541
1542 $index = array_search( $image_to_remove, $gallery_image_ids, true );
1543 if ( false !== $index ) {
1544 unset( $gallery_image_ids[ $index ] );
1545 }
1546
1547 if ( ! in_array( $image_to_add, $gallery_image_ids, true ) ) {
1548 $gallery_image_ids[] = $image_to_add;
1549 }
1550
1551 $product->set_gallery_image_ids( $gallery_image_ids );
1552 $product->save();
1553 }
1554
1555 private function should_display_create_with_ai_banner() {
1556 $elementor_pages = new \WP_Query( [
1557 'post_type' => 'page',
1558 'post_status' => 'publish',
1559 'fields' => 'ids',
1560 'posts_per_page' => self::MIN_PAGES_FOR_CREATE_WITH_AI_BANNER + 1,
1561 ] );
1562
1563 if ( $elementor_pages->post_count > self::MIN_PAGES_FOR_CREATE_WITH_AI_BANNER ) {
1564 return false;
1565 }
1566
1567 if ( Utils::is_custom_kit_applied() ) {
1568 return false;
1569 }
1570
1571 return true;
1572 }
1573
1574 private function get_create_with_ai_banner_data() {
1575 return [
1576 'title' => 'Create and launch your site faster with AI',
1577 'description' => 'Share your vision with our AI Chat and watch as it becomes a brief, sitemap, and wireframes in minutes:',
1578 'input_placeholder' => 'Start describing the site you want to create...',
1579 'button_title' => 'Create with AI',
1580 'button_cta_url' => 'http://planner.elementor.com/chat.html',
1581 'background_image' => ELEMENTOR_ASSETS_URL . 'images/app/ai/ai-site-creator-homepage-bg.svg',
1582 'utm_source' => 'editor-home',
1583 'utm_medium' => 'wp-dash',
1584 'utm_campaign' => 'generate-with-ai',
1585 ];
1586 }
1587
1588 public function add_create_with_ai_banner_to_homescreen( $home_screen_data ) {
1589 if ( $this->should_display_create_with_ai_banner() ) {
1590 $home_screen_data['create_with_ai'] = $this->get_create_with_ai_banner_data();
1591 } else {
1592 $home_screen_data['create_with_ai'] = null;
1593 }
1594
1595 return $home_screen_data;
1596 }
1597 }
1598