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

1,550 lines 42.0 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 'ai_get_image_to_image_isolate_objects' => [ $this, 'ajax_ai_get_product_image_unification' ],
86 ];
87
88 foreach ( $handlers as $tag => $callback ) {
89 $ajax->register_ajax_action( $tag, $callback );
90 }
91 } );
92
93 add_action( 'elementor/editor/before_enqueue_scripts', function() {
94 $this->enqueue_main_script();
95 $this->enqueue_layout_script();
96 } );
97
98 add_action( 'elementor/editor/after_enqueue_styles', function() {
99 wp_enqueue_style(
100 'elementor-ai-editor',
101 $this->get_css_assets_url( 'modules/ai/editor' ),
102 [],
103 ELEMENTOR_VERSION
104 );
105 } );
106
107 add_action( 'elementor/preview/enqueue_styles', function() {
108 wp_enqueue_style(
109 'elementor-ai-layout-preview',
110 $this->get_css_assets_url( 'modules/ai/layout-preview' ),
111 [],
112 ELEMENTOR_VERSION
113 );
114 } );
115
116 if ( is_admin() ) {
117 add_action( 'wp_enqueue_media', [ $this, 'enqueue_ai_media_library' ] );
118 add_action( 'admin_head', [ $this, 'enqueue_ai_media_library_upload_screen' ] );
119
120 if ( current_user_can( 'edit_products' ) || current_user_can( 'publish_products' ) ) {
121 add_action( 'admin_init', [ $this, 'enqueue_ai_products_page_scripts' ] );
122 add_action( 'current_screen', [ $this, 'enqueue_ai_single_product_page_scripts' ] );
123 add_action( 'wp_ajax_elementor-ai-get-product-images', [ $this, 'get_product_images_ajax' ] );
124 add_action( 'wp_ajax_elementor-ai-set-product-images', [ $this, 'set_product_images_ajax' ] );
125 Product_Image_Unification_Intro::add_hooks();
126 }
127 }
128
129 add_action( 'enqueue_block_editor_assets', function() {
130 wp_enqueue_script( 'elementor-ai-gutenberg',
131 $this->get_js_assets_url( 'ai-gutenberg' ),
132 [
133 'jquery',
134 'elementor-v2-ui',
135 'elementor-v2-icons',
136 'wp-blocks',
137 'wp-element',
138 'wp-editor',
139 'wp-data',
140 'wp-components',
141 'wp-compose',
142 'wp-i18n',
143 'wp-hooks',
144 'elementor-ai-media-library',
145 ],
146 ELEMENTOR_VERSION, true );
147
148 wp_localize_script(
149 'elementor-ai-gutenberg',
150 'ElementorAiConfig',
151 [
152 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
153 'connect_url' => $this->get_ai_connect_url(),
154 ]
155 );
156
157 wp_set_script_translations( 'elementor-ai-gutenberg', 'elementor' );
158 });
159
160 add_filter( 'elementor/document/save/data', function ( $data ) {
161 return $this->remove_temporary_containers( $data );
162 } );
163
164 add_action( 'elementor/element/common/section_effects/after_section_start', [ $this, 'register_ai_motion_effect_control' ], 10, 1 );
165 add_action( 'elementor/element/container/section_effects/after_section_start', [ $this, 'register_ai_motion_effect_control' ], 10, 1 );
166 add_action( 'elementor/element/common/_section_transform/after_section_end', [ $this, 'register_ai_hover_effect_control' ], 10, 1 );
167 add_action( 'elementor/element/container/_section_transform/after_section_end', [ $this, 'register_ai_hover_effect_control' ], 10, 1 );
168 }
169
170 public function is_ai_enabled() {
171 if ( ! Plugin::$instance->experiments->is_feature_active( 'container' ) ) {
172 return false;
173 }
174
175 return Preferences::is_ai_enabled( get_current_user_id() );
176 }
177
178 public function handle_kit_install( $imported_data ) {
179 if ( ! $this->is_ai_enabled() ) {
180 return;
181 }
182
183 if ( ! isset( $imported_data['status'] ) || 'success' !== $imported_data['status'] ) {
184 return;
185 }
186
187 if ( ! isset( $imported_data['runner'] ) || 'site-settings' !== $imported_data['runner'] ) {
188 return;
189 }
190
191 if ( ! isset( $imported_data['configData']['lastImportedSession']['instance_data']['site_settings']['settings']['ai'] ) ) {
192 return;
193 }
194
195 $is_connected = $this->get_ai_app()->is_connected() && User::get_introduction_meta( 'ai_get_started' );
196
197 if ( ! $is_connected ) {
198 return;
199 }
200
201 $last_imported_session = $imported_data['configData']['lastImportedSession'];
202 $imported_ai_data = $last_imported_session['instance_data']['site_settings']['settings']['ai'];
203
204 $this->get_ai_app()->send_event( [
205 'name' => 'kit_installed',
206 'data' => $imported_ai_data,
207 'client' => [
208 'name' => 'elementor',
209 'version' => ELEMENTOR_VERSION,
210 'session_id' => $last_imported_session['session_id'],
211 ],
212 ] );
213 }
214
215 public function register_ai_hover_effect_control( Element_Base $element ) {
216 if ( ! $element->get_controls( 'ai_hover_animation' ) ) {
217 $element->add_control(
218 'ai_hover_animation',
219 [
220 'tabs_wrapper' => '_tabs_positioning',
221 'inner_tab' => '_tab_positioning_hover',
222 'label' => esc_html__( 'Animate With AI', 'elementor' ),
223 'type' => Controls_Manager::RAW_HTML,
224 'raw' => '
225 <style>
226 .elementor-control-ai_hover_animation .elementor-control-content {
227 display: flex;
228 flex-direction: row;
229 justify-content: space-between;
230 align-items: center;
231 }
232 .elementor-control-ai_hover_animation .elementor-control-raw-html {
233 display: none;
234 }
235 </style>',
236 'render_type' => 'none',
237 'ai' => [
238 'active' => true,
239 'type' => 'hover_animation',
240 ],
241 ],
242 [
243 'position' => [
244 'of' => '_transform_rotate_popover_hover',
245 'type' => 'control',
246 'at' => 'before',
247 ],
248 ]
249 );
250 }
251 }
252 public function register_ai_motion_effect_control( $element ) {
253 if ( Utils::has_pro() && ! $element->get_controls( 'ai_animation' ) ) {
254 $element->add_control(
255 'ai_animation',
256 [
257 'label' => esc_html__( 'Animate With AI', 'elementor' ),
258 'type' => Controls_Manager::RAW_HTML,
259 'raw' => '
260 <style>
261 .elementor-control-ai_animation .elementor-control-content {
262 display: flex;
263 flex-direction: row;
264 justify-content: space-between;
265 align-items: center;
266 }
267 .elementor-control-ai_animation .elementor-control-raw-html {
268 display: none;
269 }
270 </style>',
271 'render_type' => 'none',
272 'ai' => [
273 'active' => true,
274 'type' => 'animation',
275 ],
276 ]
277 );
278 }
279 }
280
281 private function get_current_screen() {
282 $is_wc = class_exists( 'WooCommerce' ) && post_type_exists( 'product' );
283 if ( ! $is_wc ) {
284 return 'other';
285 }
286
287 $is_products_page = isset( $_GET['post_type'] ) && 'product' === $_GET['post_type'];
288 if ( $is_products_page ) {
289 return 'wc-products';
290 }
291
292 $screen = get_current_screen();
293 $is_single_product_page = isset( $screen->post_type ) && ( 'product' === $screen->post_type && 'post' === $screen->base );
294 if ( $is_single_product_page ) {
295 return 'wc-single-product';
296 }
297
298 return 'other';
299 }
300
301 public function enqueue_ai_products_page_scripts() {
302 if ( 'wc-products' !== $this->get_current_screen() ) {
303 return;
304 }
305
306 $this->add_wc_scripts();
307 }
308
309 public function enqueue_ai_single_product_page_scripts() {
310 if ( 'wc-single-product' !== $this->get_current_screen() ) {
311 return;
312 }
313
314 $this->add_wc_scripts();
315 }
316
317 private function add_products_bulk_action( $bulk_actions ) {
318 $bulk_actions['elementor-ai-unify-product-images'] = __( 'Unify with Elementor AI', 'elementor' );
319 return $bulk_actions;
320 }
321
322 public function get_product_images_ajax() {
323 check_ajax_referer( 'elementor-ai-unify-product-images_nonce', 'nonce' );
324
325 $post_ids = isset( $_POST['post_ids'] ) ? array_map( 'intval', $_POST['post_ids'] ) : [];
326 $is_galley_only = isset( $_POST['is_galley_only'] ) && sanitize_text_field( wp_unslash( $_POST['is_galley_only'] ) );
327
328 $image_ids = [];
329
330 foreach ( $post_ids as $post_id ) {
331 if ( $is_galley_only ) {
332 $product = wc_get_product( $post_id );
333 $gallery_image_ids = $product->get_gallery_image_ids();
334 foreach ( $gallery_image_ids as $image_id ) {
335 $image_ids[] = [
336 'productId' => $post_id,
337 'id' => $image_id,
338 'image_url' => wp_get_attachment_url( $image_id ),
339 ];
340 }
341 continue;
342 }
343
344 $image_id = get_post_thumbnail_id( $post_id );
345
346 if ( ! $image_id ) {
347 $product = wc_get_product( $post_id );
348 $gallery_image_ids = $product->get_gallery_image_ids();
349 if ( ! empty( $gallery_image_ids ) ) {
350 $image_id = $gallery_image_ids[0];
351 }
352 }
353
354 $image_ids[] = [
355 'productId' => $post_id,
356 'id' => $image_id ? $image_id : 'No Image',
357 'image_url' => $image_id ? wp_get_attachment_url( $image_id ) : 'No Image',
358 ];
359 }
360
361 wp_send_json_success( [ 'product_images' => array_slice( $image_ids, 0, 10 ) ] );
362
363 wp_die();
364 }
365
366 private function get_attachment_id_by_url( $url ) {
367 $attachments = get_posts( [
368 'post_type' => 'attachment',
369 'meta_query' => [
370 [
371 'key' => '_wp_attached_file',
372 'value' => basename( $url ),
373 'compare' => 'LIKE',
374 ],
375 ],
376 'fields' => 'ids',
377 'numberposts' => 1,
378 ] );
379
380 return ! empty( $attachments ) ? $attachments[0] : null;
381 }
382
383 public function set_product_images_ajax() {
384 check_ajax_referer( 'elementor-ai-unify-product-images_nonce', 'nonce' );
385
386 $product_id = isset( $_POST['productId'] ) ? sanitize_text_field( wp_unslash( $_POST['productId'] ) ) : '';
387 $image_url = isset( $_POST['image_url'] ) ? sanitize_text_field( wp_unslash( $_POST['image_url'] ) ) : '';
388 $image_to_add = isset( $_POST['image_to_add'] ) ? intval( wp_unslash( $_POST['image_to_add'] ) ) : null;
389 $image_to_remove = isset( $_POST['image_to_remove'] ) ? intval( wp_unslash( $_POST['image_to_remove'] ) ) : null;
390 $is_product_gallery = isset( $_POST['is_product_gallery'] ) && sanitize_text_field( wp_unslash( $_POST['is_product_gallery'] ) ) === 'true';
391
392 if ( ! $product_id || ! $image_url ) {
393 throw new \Exception( 'Product ID and Image URL are required' );
394 }
395
396 $product = wc_get_product( $product_id );
397 if ( ! $product ) {
398 throw new \Exception( 'Product not found' );
399 }
400
401 $attachment_id = $this->get_attachment_id_by_url( $image_url );
402 if ( is_wp_error( $attachment_id ) ) {
403 throw new \Exception( 'Image upload failed' );
404 }
405
406 if ( $is_product_gallery ) {
407 $this->update_product_gallery( $product, $image_to_remove, $image_to_add );
408 } else {
409 $product->set_image_id( $attachment_id );
410 $product->save();
411 }
412
413 wp_send_json_success( [
414 'message' => __( 'Image added successfully', 'elementor' ),
415 ] );
416 }
417
418 public function enqueue_ai_media_library_upload_screen() {
419 $screen = get_current_screen();
420 if ( ! $screen || 'upload' !== $screen->id ) {
421 return;
422 }
423
424 $this->enqueue_ai_media_library();
425 }
426
427 public function enqueue_ai_media_library() {
428 wp_enqueue_script( 'elementor-ai-media-library',
429 $this->get_js_assets_url( 'ai-media-library' ),
430 [
431 'jquery',
432 'elementor-v2-ui',
433 'elementor-v2-icons',
434 'media-grid',
435 ],
436 ELEMENTOR_VERSION,
437 true
438 );
439
440 wp_localize_script(
441 'elementor-ai-media-library',
442 'ElementorAiConfig',
443 [
444 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
445 'connect_url' => $this->get_ai_connect_url(),
446 ]
447 );
448
449 wp_set_script_translations( 'elementor-ai-media-library', 'elementor' );
450 }
451
452 private function enqueue_main_script() {
453 wp_enqueue_script(
454 'elementor-ai',
455 $this->get_js_assets_url( 'ai' ),
456 [
457 'react',
458 'react-dom',
459 'backbone-marionette',
460 'elementor-web-cli',
461 'wp-date',
462 'elementor-common',
463 'elementor-editor-modules',
464 'elementor-editor-document',
465 'elementor-v2-ui',
466 'elementor-v2-icons',
467 ],
468 ELEMENTOR_VERSION,
469 true
470 );
471
472 $config = [
473 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
474 'connect_url' => $this->get_ai_connect_url(),
475 ];
476
477 wp_localize_script(
478 'elementor-ai',
479 'ElementorAiConfig',
480 $config
481 );
482
483 wp_set_script_translations( 'elementor-ai', 'elementor' );
484 }
485
486 private function enqueue_layout_script() {
487 wp_enqueue_script(
488 'elementor-ai-layout',
489 $this->get_js_assets_url( 'ai-layout' ),
490 [
491 'react',
492 'react-dom',
493 'backbone-marionette',
494 'elementor-common',
495 'elementor-web-cli',
496 'elementor-editor-modules',
497 'elementor-ai',
498 'elementor-v2-ui',
499 'elementor-v2-icons',
500 ],
501 ELEMENTOR_VERSION,
502 true
503 );
504
505 wp_set_script_translations( 'elementor-ai-layout', 'elementor' );
506 }
507
508 private function remove_temporary_containers( $data ) {
509 if ( empty( $data['elements'] ) || ! is_array( $data['elements'] ) ) {
510 return $data;
511 }
512
513 // If for some reason the document has been saved during an AI Layout session,
514 // ensure that the temporary containers are removed from the data.
515 $data['elements'] = array_filter( $data['elements'], function( $element ) {
516 $is_preview_container = strpos( $element['id'], 'e-ai-preview-container' ) === 0;
517 $is_screenshot_container = strpos( $element['id'], 'e-ai-screenshot-container' ) === 0;
518
519 return ! $is_preview_container && ! $is_screenshot_container;
520 } );
521
522 return $data;
523 }
524
525 private function get_ai_connect_url() {
526 $app = $this->get_ai_app();
527
528 return $app->get_admin_url( 'authorize', [
529 'utm_source' => 'ai-popup',
530 'utm_campaign' => 'connect-account',
531 'utm_medium' => 'wp-dash',
532 'source' => 'generic',
533 ] );
534 }
535
536 public function ajax_ai_get_user_information( $data ) {
537 $app = $this->get_ai_app();
538
539 if ( ! $app->is_connected() ) {
540 return [
541 'is_connected' => false,
542 'connect_url' => $this->get_ai_connect_url(),
543 ];
544 }
545
546 $user_usage = wp_parse_args( $app->get_usage(), [
547 'hasAiSubscription' => false,
548 'usedQuota' => 0,
549 'quota' => 100,
550 ] );
551
552 return [
553 'is_connected' => true,
554 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
555 'usage' => $user_usage,
556 ];
557 }
558
559 public function ajax_ai_get_remote_config() {
560 $app = $this->get_ai_app();
561
562 if ( ! $app->is_connected() ) {
563 return [];
564 }
565
566 return $app->get_remote_config();
567 }
568
569 public function ajax_ai_get_remote_frontend_config( $data ) {
570 $callback = function () use ( $data ) {
571 return $this->get_ai_app()->get_remote_frontend_config( $data );
572 };
573
574 return Utils::get_cached_callback( $callback, 'ai_remote_frontend_config-' . get_current_user_id(), HOUR_IN_SECONDS );
575 }
576
577 public function verify_upload_permissions( $data ) {
578 $referer = wp_get_referer();
579
580 if ( str_contains( $referer, 'wp-admin/upload.php' ) && current_user_can( 'upload_files' ) ) {
581 return;
582 }
583 $this->verify_permissions( $data['editor_post_id'] );
584 }
585
586 private function verify_permissions( $editor_post_id ) {
587 $document = Plugin::$instance->documents->get( $editor_post_id );
588
589 if ( ! $document ) {
590 throw new \Exception( 'Document not found' );
591 }
592
593 if ( $document->is_built_with_elementor() ) {
594 if ( ! $document->is_editable_by_current_user() ) {
595 throw new \Exception( 'Access denied' );
596 }
597 } elseif ( ! current_user_can( 'edit_post', $editor_post_id ) ) {
598 throw new \Exception( 'Access denied' );
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 ( empty( $data['payload']['mask'] ) ) {
1022 throw new \Exception( 'Missing Mask' );
1023 }
1024
1025 $context = $this->get_request_context( $data );
1026 $request_ids = $this->get_request_ids( $data['payload'] );
1027
1028 $result = $app->get_image_to_image_mask( [
1029 'prompt' => $data['payload']['prompt'],
1030 'attachment_id' => $data['payload']['image']['id'],
1031 'mask' => $data['payload']['mask'],
1032 ], $context, $request_ids );
1033
1034 $this->throw_on_error( $result );
1035
1036 return [
1037 'images' => $result['images'],
1038 'response_id' => $result['responseId'],
1039 'usage' => $result['usage'],
1040 ];
1041 }
1042 public function ajax_ai_get_image_to_image_mask_cleanup( $data ) {
1043 $this->verify_upload_permissions( $data );
1044
1045 $app = $this->get_ai_app();
1046
1047 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
1048 throw new \Exception( 'Missing Image' );
1049 }
1050
1051 if ( empty( $data['payload']['settings'] ) ) {
1052 throw new \Exception( 'Missing prompt settings' );
1053 }
1054
1055 if ( ! $app->is_connected() ) {
1056 throw new \Exception( 'not_connected' );
1057 }
1058
1059 if ( empty( $data['payload']['mask'] ) ) {
1060 throw new \Exception( 'Missing Mask' );
1061 }
1062
1063 $context = $this->get_request_context( $data );
1064 $request_ids = $this->get_request_ids( $data['payload'] );
1065
1066 $result = $app->get_image_to_image_mask_cleanup( [
1067 'attachment_id' => $data['payload']['image']['id'],
1068 'mask' => $data['payload']['mask'],
1069 ], $context, $request_ids );
1070
1071 $this->throw_on_error( $result );
1072
1073 return [
1074 'images' => $result['images'],
1075 'response_id' => $result['responseId'],
1076 'usage' => $result['usage'],
1077 ];
1078 }
1079
1080 public function ajax_ai_get_image_to_image_outpainting( $data ) {
1081 $this->verify_upload_permissions( $data );
1082
1083 $app = $this->get_ai_app();
1084
1085 if ( ! $app->is_connected() ) {
1086 throw new \Exception( 'not_connected' );
1087 }
1088
1089 if ( empty( $data['payload']['mask'] ) ) {
1090 throw new \Exception( 'Missing Expended Image' );
1091 }
1092
1093 $context = $this->get_request_context( $data );
1094 $request_ids = $this->get_request_ids( $data['payload'] );
1095 $result = $app->get_image_to_image_out_painting( [
1096 'size' => $data['payload']['size'],
1097 'position' => $data['payload']['position'],
1098 'mask' => $data['payload']['mask'],
1099 'image_base64' => $data['payload']['image_base64'],
1100 ], $context, $request_ids );
1101
1102 $this->throw_on_error( $result );
1103
1104 return [
1105 'images' => $result['images'],
1106 'response_id' => $result['responseId'],
1107 'usage' => $result['usage'],
1108 ];
1109 }
1110
1111 public function ajax_ai_upload_image( $data ) {
1112 if ( empty( $data['image'] ) ) {
1113 throw new \Exception( 'Missing image data' );
1114 }
1115
1116 $image = $data['image'];
1117
1118 if ( empty( $image['image_url'] ) ) {
1119 throw new \Exception( 'Missing image_url' );
1120 }
1121
1122 $image_data = $this->upload_image( $image['image_url'], $data['prompt'], $data['editor_post_id'] );
1123
1124 if ( is_wp_error( $image_data ) ) {
1125 throw new \Exception( esc_html( $image_data->get_error_message() ) );
1126 }
1127
1128 if ( ! empty( $image['use_gallery_image'] ) && ! empty( $image['id'] ) ) {
1129 $app = $this->get_ai_app();
1130 $app->set_used_gallery_image( $image['id'] );
1131 }
1132
1133 return [
1134 'image' => array_merge( $image_data, $data ),
1135 ];
1136 }
1137
1138 public function ajax_ai_generate_layout( $data ) {
1139 $this->verify_permissions( $data['editor_post_id'] );
1140
1141 $app = $this->get_ai_app();
1142
1143 if ( empty( $data['prompt'] ) && empty( $data['attachments'] ) ) {
1144 throw new \Exception( 'Missing prompt / attachments' );
1145 }
1146
1147 if ( ! $app->is_connected() ) {
1148 throw new \Exception( 'not_connected' );
1149 }
1150
1151 $result = $app->generate_layout(
1152 $data,
1153 $this->prepare_generate_layout_context( $data )
1154 );
1155
1156 if ( is_wp_error( $result ) ) {
1157 $message = $result->get_error_message();
1158
1159 if ( is_array( $message ) ) {
1160 $message = implode( ', ', $message );
1161 throw new \Exception( esc_html( $message ) );
1162 }
1163
1164 $this->throw_on_error( $result );
1165 }
1166
1167 $elements = $result['text']['elements'] ?? [];
1168 $base_template_id = $result['baseTemplateId'] ?? null;
1169 $template_type = $result['templateType'] ?? null;
1170
1171 if ( empty( $elements ) || ! is_array( $elements ) ) {
1172 throw new \Exception( 'unknown_error' );
1173 }
1174
1175 if ( 1 === count( $elements ) ) {
1176 $template = $elements[0];
1177 } else {
1178 $template = [
1179 'elType' => 'container',
1180 'elements' => $elements,
1181 'settings' => [
1182 'content_width' => 'full',
1183 'flex_gap' => [
1184 'column' => '0',
1185 'row' => '0',
1186 'unit' => 'px',
1187 ],
1188 'padding' => [
1189 'unit' => 'px',
1190 'top' => '0',
1191 'right' => '0',
1192 'bottom' => '0',
1193 'left' => '0',
1194 'isLinked' => true,
1195 ],
1196 ],
1197 ];
1198 }
1199
1200 return [
1201 'all' => [],
1202 'text' => $template,
1203 'response_id' => $result['responseId'],
1204 'usage' => $result['usage'],
1205 'base_template_id' => $base_template_id,
1206 'template_type' => $template_type,
1207 ];
1208 }
1209
1210 public function ajax_ai_get_layout_prompt_enhancer( $data ) {
1211 $this->verify_permissions( $data['editor_post_id'] );
1212
1213 $app = $this->get_ai_app();
1214
1215 if ( empty( $data['prompt'] ) ) {
1216 throw new \Exception( 'Missing prompt' );
1217 }
1218
1219 if ( ! $app->is_connected() ) {
1220 throw new \Exception( 'not_connected' );
1221 }
1222
1223 $result = $app->get_layout_prompt_enhanced(
1224 $data['prompt'],
1225 $data['enhance_type'],
1226 $this->prepare_generate_layout_context( $data )
1227 );
1228
1229 $this->throw_on_error( $result );
1230
1231 return [
1232 'text' => $result['text'] ?? $data['prompt'],
1233 'response_id' => $result['responseId'] ?? '',
1234 'usage' => $result['usage'] ?? '',
1235 ];
1236 }
1237
1238 private function prepare_generate_layout_context( $data ) {
1239 $request_context = $this->get_request_context( $data );
1240 $kit = Plugin::$instance->kits_manager->get_active_kit();
1241
1242 if ( ! $kit ) {
1243 return $request_context;
1244 }
1245
1246 $kits_data = Collection::make( $kit->get_data()['settings'] ?? [] );
1247
1248 $colors = $kits_data
1249 ->filter( function ( $_, $key ) {
1250 return in_array( $key, [ 'system_colors', 'custom_colors' ], true );
1251 } )
1252 ->flatten()
1253 ->filter( function ( $val ) {
1254 return ! empty( $val['_id'] );
1255 } )
1256 ->map( function ( $val ) {
1257 return [
1258 'id' => $val['_id'],
1259 'label' => $val['title'] ?? null,
1260 'value' => $val['color'] ?? null,
1261 ];
1262 } );
1263
1264 $typography = $kits_data
1265 ->filter( function ( $_, $key ) {
1266 return in_array( $key, [ 'system_typography', 'custom_typography' ], true );
1267 } )
1268 ->flatten()
1269 ->filter( function ( $val ) {
1270 return ! empty( $val['_id'] );
1271 } )
1272 ->map( function ( $val ) {
1273 $font_size = null;
1274
1275 if ( isset(
1276 $val['typography_font_size']['unit'],
1277 $val['typography_font_size']['size']
1278 ) ) {
1279 $prop = $val['typography_font_size'];
1280
1281 $font_size = 'custom' === $prop['unit']
1282 ? $prop['size']
1283 : $prop['size'] . $prop['unit'];
1284 }
1285
1286 return [
1287 'id' => $val['_id'],
1288 'label' => $val['title'] ?? null,
1289 'value' => [
1290 'family' => $val['typography_font_family'] ?? null,
1291 'weight' => $val['typography_font_weight'] ?? null,
1292 'style' => $val['typography_font_style'] ?? null,
1293 'size' => $font_size,
1294 ],
1295 ];
1296 } );
1297
1298 $request_context['globals'] = [
1299 'colors' => $colors->all(),
1300 'typography' => $typography->all(),
1301 ];
1302
1303 return $request_context;
1304 }
1305
1306 private function upload_image( $image_url, $image_title, $parent_post_id = 0 ) {
1307 if ( ! current_user_can( 'upload_files' ) ) {
1308 throw new \Exception( 'Not Allowed to Upload images' );
1309 }
1310
1311 $uploads_manager = new \Elementor\Core\Files\Uploads_Manager();
1312 if ( $uploads_manager::are_unfiltered_uploads_enabled() ) {
1313 Plugin::$instance->uploads_manager->set_elementor_upload_state( true );
1314 add_filter( 'wp_handle_sideload_prefilter', [ Plugin::$instance->uploads_manager, 'handle_elementor_upload' ] );
1315 add_filter( 'image_sideload_extensions', function( $extensions ) {
1316 $extensions[] = 'svg';
1317 return $extensions;
1318 });
1319 }
1320
1321 $attachment_id = media_sideload_image( $image_url, $parent_post_id, $image_title, 'id' );
1322
1323 if ( is_wp_error( $attachment_id ) ) {
1324 return new \WP_Error( 'upload_error', $attachment_id->get_error_message() );
1325 }
1326
1327 if ( ! empty( $attachment_id['error'] ) ) {
1328 return new \WP_Error( 'upload_error', $attachment_id['error'] );
1329 }
1330
1331 return [
1332 'id' => $attachment_id,
1333 'url' => esc_url( wp_get_attachment_image_url( $attachment_id, 'full' ) ),
1334 'alt' => esc_attr( $image_title ),
1335 'source' => 'library',
1336 ];
1337 }
1338
1339 public function ajax_ai_get_history( $data ): array {
1340 $type = $data['type'] ?? self::HISTORY_TYPE_ALL;
1341
1342 if ( ! in_array( $type, self::VALID_HISTORY_TYPES, true ) ) {
1343 throw new \Exception( 'Invalid history type' );
1344 }
1345
1346 $page = sanitize_text_field( $data['page'] ?? 1 );
1347 $limit = sanitize_text_field( $data['limit'] ?? 10 );
1348
1349 $app = $this->get_ai_app();
1350
1351 if ( ! $app->is_connected() ) {
1352 throw new \Exception( 'not_connected' );
1353 }
1354
1355 $context = $this->get_request_context( $data );
1356
1357 $result = $app->get_history_by_type( $type, $page, $limit, $context );
1358
1359 if ( is_wp_error( $result ) ) {
1360 throw new \Exception( esc_html( $result->get_error_message() ) );
1361 }
1362
1363 return $result;
1364 }
1365
1366 public function ajax_ai_delete_history_item( $data ): array {
1367 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
1368 throw new \Exception( 'Missing id parameter' );
1369 }
1370
1371 $app = $this->get_ai_app();
1372
1373 if ( ! $app->is_connected() ) {
1374 throw new \Exception( 'not_connected' );
1375 }
1376
1377 $context = $this->get_request_context( $data );
1378
1379 $result = $app->delete_history_item( $data['id'], $context );
1380
1381 if ( is_wp_error( $result ) ) {
1382 throw new \Exception( esc_html( $result->get_error_message() ) );
1383 }
1384
1385 return [];
1386 }
1387
1388 public function ajax_ai_toggle_favorite_history_item( $data ): array {
1389 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
1390 throw new \Exception( 'Missing id parameter' );
1391 }
1392
1393 $app = $this->get_ai_app();
1394
1395 if ( ! $app->is_connected() ) {
1396 throw new \Exception( 'not_connected' );
1397 }
1398
1399 $context = $this->get_request_context( $data );
1400
1401 $result = $app->toggle_favorite_history_item( $data['id'], $context );
1402
1403 if ( is_wp_error( $result ) ) {
1404 throw new \Exception( esc_html( $result->get_error_message() ) );
1405 }
1406
1407 return [];
1408 }
1409
1410 public function ajax_ai_get_product_image_unification( $data ): array {
1411 if ( ! empty( $data['payload']['postId'] ) ) {
1412 $data['editor_post_id'] = $data['payload']['postId'];
1413 }
1414 $this->verify_upload_permissions( $data );
1415
1416 $app = $this->get_ai_app();
1417
1418 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
1419 throw new \Exception( 'Missing Image' );
1420 }
1421
1422 if ( empty( $data['payload']['settings'] ) ) {
1423 throw new \Exception( 'Missing prompt settings' );
1424 }
1425
1426 if ( ! $app->is_connected() ) {
1427 throw new \Exception( 'not_connected' );
1428 }
1429
1430 $context = $this->get_request_context( $data );
1431 $request_ids = $this->get_request_ids( $data['payload'] );
1432
1433 $result = $app->get_unify_product_images( [
1434 'promptSettings' => $data['payload']['settings'],
1435 'attachment_id' => $data['payload']['image']['id'],
1436 'featureIdentifier' => $data['payload']['featureIdentifier'] ?? '',
1437 ], $context, $request_ids );
1438
1439 $this->throw_on_error( $result );
1440
1441 return [
1442 'images' => $result['images'],
1443 'response_id' => $result['responseId'],
1444 'usage' => $result['usage'],
1445 ];
1446 }
1447
1448 public function ajax_ai_get_animation( $data ): array {
1449 $this->verify_upload_permissions( $data );
1450
1451 $app = $this->get_ai_app();
1452
1453 if ( empty( $data['payload']['prompt'] ) ) {
1454 throw new \Exception( 'Missing prompt' );
1455 }
1456
1457 if ( empty( $data['payload']['motionEffectType'] ) ) {
1458 throw new \Exception( 'Missing animation type' );
1459 }
1460
1461 if ( ! $app->is_connected() ) {
1462 throw new \Exception( 'not_connected' );
1463 }
1464
1465 $context = $this->get_request_context( $data );
1466
1467 $request_ids = $this->get_request_ids( $data['payload'] );
1468
1469 $result = $app->get_animation( $data, $context, $request_ids );
1470 $this->throw_on_error( $result );
1471
1472 return [
1473 'text' => $result['text'],
1474 'response_id' => $result['responseId'],
1475 'usage' => $result['usage'],
1476 ];
1477 }
1478
1479 /**
1480 * @param mixed $result
1481 */
1482 private function throw_on_error( $result ): void {
1483 if ( is_wp_error( $result ) ) {
1484 wp_send_json_error( [
1485 'message' => esc_html( $result->get_error_message() ),
1486 'extra_data' => $result->get_error_data(),
1487 ] );
1488 }
1489 }
1490
1491 /**
1492 * @return void
1493 */
1494 public function add_wc_scripts(): void {
1495 wp_enqueue_script( 'elementor-ai-unify-product-images',
1496 $this->get_js_assets_url( 'ai-unify-product-images' ),
1497 [
1498 'jquery',
1499 'elementor-v2-ui',
1500 'elementor-v2-icons',
1501 'wp-components',
1502 'elementor-common',
1503 ],
1504 ELEMENTOR_VERSION,
1505 true
1506 );
1507
1508 wp_localize_script(
1509 'elementor-ai-unify-product-images',
1510 'UnifyProductImagesConfig',
1511 [
1512 'get_product_images_url' => admin_url( 'admin-ajax.php' ),
1513 'set_product_images_url' => admin_url( 'admin-ajax.php' ),
1514 'nonce' => wp_create_nonce( 'elementor-ai-unify-product-images_nonce' ),
1515 'placeholder' => ELEMENTOR_ASSETS_URL . 'images/app/ai/product-image-unification-example.gif?' . ELEMENTOR_VERSION,
1516 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
1517 'connect_url' => $this->get_ai_connect_url(),
1518 ]
1519 );
1520
1521 add_filter( 'bulk_actions-edit-product', function ( $data ) {
1522 return $this->add_products_bulk_action( $data );
1523 });
1524
1525 wp_set_script_translations( 'elementor-ai-unify-product-images', 'elementor' );
1526 }
1527
1528 /**
1529 * @param $product
1530 * @param int|null $image_to_remove
1531 * @param int|null $image_to_add
1532 * @return void
1533 */
1534 private function update_product_gallery( $product, ?int $image_to_remove, ?int $image_to_add ): void {
1535 $gallery_image_ids = $product->get_gallery_image_ids();
1536
1537 $index = array_search( $image_to_remove, $gallery_image_ids, true );
1538 if ( false !== $index ) {
1539 unset( $gallery_image_ids[ $index ] );
1540 }
1541
1542 if ( ! in_array( $image_to_add, $gallery_image_ids, true ) ) {
1543 $gallery_image_ids[] = $image_to_add;
1544 }
1545
1546 $product->set_gallery_image_ids( $gallery_image_ids );
1547 $product->save();
1548 }
1549 }
1550