PluginProbe
Elementor Website Builder – more than just a page builder / 3.27.0-dev2
Elementor Website Builder – more than just a page builder v3.27.0-dev2
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 3.27.0-dev2, at modules/ai/module.php

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