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

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