PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.3
Elementor Website Builder – more than just a page builder v4.3.3
4.3.3 4.3.2 4.3.1 4.3.0 4.3.0-beta3 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 All 456 releases
elementor / modules / ai / module.php

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

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