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

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

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