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

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

1,163 lines 30.2 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\Core\Base\Module as BaseModule;
5 use Elementor\Core\Common\Modules\Connect\Module as ConnectModule;
6 use Elementor\Core\Experiments\Manager as Experiments_Manager;
7 use Elementor\Core\Utils\Collection;
8 use Elementor\Modules\Ai\Connect\Ai;
9 use Elementor\Plugin;
10 use Elementor\User;
11 use Elementor\Utils;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit; // Exit if accessed directly.
15 }
16
17 class Module extends BaseModule {
18 const HISTORY_TYPE_ALL = 'all';
19 const HISTORY_TYPE_TEXT = 'text';
20 const HISTORY_TYPE_CODE = 'code';
21 const HISTORY_TYPE_IMAGE = 'images';
22 const HISTORY_TYPE_BLOCK = 'blocks';
23 const VALID_HISTORY_TYPES = [
24 self::HISTORY_TYPE_ALL,
25 self::HISTORY_TYPE_TEXT,
26 self::HISTORY_TYPE_CODE,
27 self::HISTORY_TYPE_IMAGE,
28 self::HISTORY_TYPE_BLOCK,
29 ];
30
31 const LAYOUT_EXPERIMENT = 'ai-layout';
32
33 public function get_name() {
34 return 'ai';
35 }
36
37 public function __construct() {
38 parent::__construct();
39
40 if ( is_admin() ) {
41 ( new Preferences() )->register();
42 }
43
44 if ( ! Preferences::is_ai_enabled( get_current_user_id() ) ) {
45 return;
46 }
47
48 $this->register_layout_experiment();
49
50 add_action( 'elementor/connect/apps/register', function ( ConnectModule $connect_module ) {
51 $connect_module->register_app( 'ai', Ai::get_class_name() );
52 } );
53
54 add_action( 'elementor/ajax/register_actions', function( $ajax ) {
55 $handlers = [
56 'ai_get_user_information' => [ $this, 'ajax_ai_get_user_information' ],
57 'ai_get_remote_config' => [ $this, 'ajax_ai_get_remote_config' ],
58 'ai_get_remote_frontend_config' => [ $this, 'ajax_ai_get_remote_frontend_config' ],
59 'ai_get_completion_text' => [ $this, 'ajax_ai_get_completion_text' ],
60 'ai_get_excerpt' => [ $this, 'ajax_ai_get_excerpt' ],
61 'ai_get_featured_image' => [ $this, 'ajax_ai_get_featured_image' ],
62 'ai_get_edit_text' => [ $this, 'ajax_ai_get_edit_text' ],
63 'ai_get_custom_code' => [ $this, 'ajax_ai_get_custom_code' ],
64 'ai_get_custom_css' => [ $this, 'ajax_ai_get_custom_css' ],
65 'ai_set_get_started' => [ $this, 'ajax_ai_set_get_started' ],
66 'ai_set_status_feedback' => [ $this, 'ajax_ai_set_status_feedback' ],
67 'ai_get_image_prompt_enhancer' => [ $this, 'ajax_ai_get_image_prompt_enhancer' ],
68 'ai_get_text_to_image' => [ $this, 'ajax_ai_get_text_to_image' ],
69 'ai_get_image_to_image' => [ $this, 'ajax_ai_get_image_to_image' ],
70 'ai_get_image_to_image_mask' => [ $this, 'ajax_ai_get_image_to_image_mask' ],
71 'ai_get_image_to_image_mask_cleanup' => [ $this, 'ajax_ai_get_image_to_image_mask_cleanup' ],
72 'ai_get_image_to_image_outpainting' => [ $this, 'ajax_ai_get_image_to_image_outpainting' ],
73 'ai_get_image_to_image_upscale' => [ $this, 'ajax_ai_get_image_to_image_upscale' ],
74 'ai_get_image_to_image_remove_background' => [ $this, 'ajax_ai_get_image_to_image_remove_background' ],
75 'ai_get_image_to_image_replace_background' => [ $this, 'ajax_ai_get_image_to_image_replace_background' ],
76 'ai_upload_image' => [ $this, 'ajax_ai_upload_image' ],
77 'ai_generate_layout' => [ $this, 'ajax_ai_generate_layout' ],
78 'ai_get_layout_prompt_enhancer' => [ $this, 'ajax_ai_get_layout_prompt_enhancer' ],
79 'ai_get_history' => [ $this, 'ajax_ai_get_history' ],
80 'ai_delete_history_item' => [ $this, 'ajax_ai_delete_history_item' ],
81 'ai_toggle_favorite_history_item' => [ $this, 'ajax_ai_toggle_favorite_history_item' ],
82 ];
83
84 foreach ( $handlers as $tag => $callback ) {
85 $ajax->register_ajax_action( $tag, $callback );
86 }
87 } );
88
89 add_action( 'elementor/editor/before_enqueue_scripts', function() {
90 $this->enqueue_main_script();
91
92 if ( $this->is_layout_active() ) {
93 $this->enqueue_layout_script();
94 }
95 } );
96
97 add_action( 'elementor/editor/after_enqueue_styles', function() {
98 wp_enqueue_style(
99 'elementor-ai-editor',
100 $this->get_css_assets_url( 'modules/ai/editor' ),
101 [],
102 ELEMENTOR_VERSION
103 );
104 } );
105
106 add_action( 'elementor/preview/enqueue_styles', function() {
107 if ( $this->is_layout_active() ) {
108 wp_enqueue_style(
109 'elementor-ai-layout-preview',
110 $this->get_css_assets_url( 'modules/ai/layout-preview' ),
111 [],
112 ELEMENTOR_VERSION
113 );
114 }
115 } );
116
117 if ( is_admin() ) {
118 add_action( 'wp_enqueue_media', [ $this, 'enqueue_ai_media_library' ] );
119 }
120
121 add_action( 'enqueue_block_editor_assets', function() {
122 wp_enqueue_script( 'elementor-ai-gutenberg',
123 $this->get_js_assets_url( 'ai-gutenberg' ),
124 [
125 'jquery',
126 'elementor-v2-ui',
127 'elementor-v2-icons',
128 'wp-blocks',
129 'wp-element',
130 'wp-editor',
131 'wp-data',
132 'wp-components',
133 'wp-compose',
134 'wp-i18n',
135 'wp-hooks',
136 'elementor-ai-media-library',
137 ],
138 ELEMENTOR_VERSION, true );
139
140 wp_localize_script(
141 'elementor-ai-gutenberg',
142 'ElementorAiConfig',
143 [
144 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
145 'connect_url' => $this->get_ai_connect_url(),
146 ]
147 );
148
149 wp_set_script_translations( 'elementor-ai-gutenberg', 'elementor' );
150 });
151
152 add_filter( 'elementor/document/save/data', function ( $data ) {
153 if ( $this->is_layout_active() ) {
154 return $this->remove_temporary_containers( $data );
155 }
156
157 return $data;
158 } );
159 }
160
161 private function register_layout_experiment() {
162 Plugin::$instance->experiments->add_feature( [
163 'name' => static::LAYOUT_EXPERIMENT,
164 'title' => esc_html__( 'Build with AI', 'elementor' ),
165 'description' => esc_html__( 'Tap into the potential of AI to easily create and customize containers to your specifications, right within Elementor. This feature comes packed with handy AI tools, including generation, variations, and URL references.', 'elementor' ),
166 'default' => Experiments_Manager::STATE_ACTIVE,
167 'release_status' => Experiments_Manager::RELEASE_STATUS_STABLE,
168 'dependencies' => [
169 'container',
170 ],
171 ] );
172 }
173
174 public function enqueue_ai_media_library() {
175 wp_enqueue_script( 'elementor-ai-media-library',
176 $this->get_js_assets_url( 'ai-media-library' ),
177 [
178 'jquery',
179 'elementor-v2-ui',
180 'elementor-v2-icons',
181 'media-grid',
182 ],
183 ELEMENTOR_VERSION,
184 true
185 );
186
187 wp_localize_script(
188 'elementor-ai-media-library',
189 'ElementorAiConfig',
190 [
191 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
192 'connect_url' => $this->get_ai_connect_url(),
193 ]
194 );
195
196 wp_set_script_translations( 'elementor-ai-media-library', 'elementor' );
197 }
198
199 private function enqueue_main_script() {
200 wp_enqueue_script(
201 'elementor-ai',
202 $this->get_js_assets_url( 'ai' ),
203 [
204 'react',
205 'react-dom',
206 'backbone-marionette',
207 'elementor-web-cli',
208 'wp-date',
209 'elementor-common',
210 'elementor-editor-modules',
211 'elementor-editor-document',
212 'elementor-v2-ui',
213 'elementor-v2-icons',
214 ],
215 ELEMENTOR_VERSION,
216 true
217 );
218
219 $config = [
220 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
221 'connect_url' => $this->get_ai_connect_url(),
222 ];
223
224 if ( $this->get_ai_app()->is_connected() ) {
225 // Use a cached version, don't call the API on every editor load.
226 $config['usage'] = $this->get_ai_app()->get_cached_usage();
227 }
228
229 wp_localize_script(
230 'elementor-ai',
231 'ElementorAiConfig',
232 $config
233 );
234
235 wp_set_script_translations( 'elementor-ai', 'elementor' );
236 }
237
238 private function enqueue_layout_script() {
239 wp_enqueue_script(
240 'elementor-ai-layout',
241 $this->get_js_assets_url( 'ai-layout' ),
242 [
243 'react',
244 'react-dom',
245 'backbone-marionette',
246 'elementor-common',
247 'elementor-web-cli',
248 'elementor-editor-modules',
249 'elementor-ai',
250 'elementor-v2-ui',
251 'elementor-v2-icons',
252 ],
253 ELEMENTOR_VERSION,
254 true
255 );
256
257 wp_set_script_translations( 'elementor-ai-layout', 'elementor' );
258 }
259
260 private function is_layout_active() {
261 return Plugin::$instance->experiments->is_feature_active( self::LAYOUT_EXPERIMENT );
262 }
263
264 private function remove_temporary_containers( $data ) {
265 if ( empty( $data['elements'] ) ) {
266 return $data;
267 }
268
269 // If for some reason the document has been saved during an AI Layout session,
270 // ensure that the temporary containers are removed from the data.
271 $data['elements'] = array_filter( $data['elements'], function( $element ) {
272 $is_preview_container = strpos( $element['id'], 'e-ai-preview-container' ) === 0;
273 $is_screenshot_container = strpos( $element['id'], 'e-ai-screenshot-container' ) === 0;
274
275 return ! $is_preview_container && ! $is_screenshot_container;
276 } );
277
278 return $data;
279 }
280
281 private function get_ai_connect_url() {
282 $app = $this->get_ai_app();
283
284 return $app->get_admin_url( 'authorize', [
285 'utm_source' => 'ai-popup',
286 'utm_campaign' => 'connect-account',
287 'utm_medium' => 'wp-dash',
288 'source' => 'generic',
289 ] );
290 }
291
292 public function ajax_ai_get_user_information( $data ) {
293 $app = $this->get_ai_app();
294
295 if ( ! $app->is_connected() ) {
296 return [
297 'is_connected' => false,
298 'connect_url' => $this->get_ai_connect_url(),
299 ];
300 }
301
302 $user_usage = wp_parse_args( $app->get_usage(), [
303 'hasAiSubscription' => false,
304 'usedQuota' => 0,
305 'quota' => 100,
306 ] );
307
308 return [
309 'is_connected' => true,
310 'is_get_started' => User::get_introduction_meta( 'ai_get_started' ),
311 'usage' => $user_usage,
312 ];
313 }
314
315 public function ajax_ai_get_remote_config() {
316 $app = $this->get_ai_app();
317
318 if ( ! $app->is_connected() ) {
319 return [];
320 }
321
322 return $app->get_remote_config();
323 }
324
325 public function ajax_ai_get_remote_frontend_config( $data ) {
326 $callback = function () use ( $data ) {
327 return $this->get_ai_app()->get_remote_frontend_config( $data );
328 };
329
330 return Utils::get_cached_callback( $callback, 'ai_remote_frontend_config-' . get_current_user_id(), HOUR_IN_SECONDS );
331 }
332
333 public function verify_upload_permissions( $data ) {
334 $referer = wp_get_referer();
335
336 if ( str_contains( $referer, 'wp-admin/upload.php' ) && current_user_can( 'upload_files' ) ) {
337 return;
338 }
339 $this->verify_permissions( $data['editor_post_id'] );
340 }
341 private function verify_permissions( $editor_post_id ) {
342 $document = Plugin::$instance->documents->get( $editor_post_id );
343
344 if ( ! $document ) {
345 throw new \Exception( 'Document not found' );
346 }
347
348 if ( ! $document->is_editable_by_current_user() ) {
349 throw new \Exception( 'Access denied' );
350 }
351 }
352
353 public function ajax_ai_get_image_prompt_enhancer( $data ) {
354 $this->verify_upload_permissions( $data );
355
356 $app = $this->get_ai_app();
357
358 if ( empty( $data['prompt'] ) ) {
359 throw new \Exception( 'Missing prompt' );
360 }
361
362 if ( ! $app->is_connected() ) {
363 throw new \Exception( 'not_connected' );
364 }
365 $request_ids = $this->get_request_ids( $data['payload'] );
366
367 $result = $app->get_image_prompt_enhanced( $data['prompt'], [], $request_ids );
368 $this->throw_on_error( $result );
369
370 return [
371 'text' => $result['text'],
372 'response_id' => $result['responseId'],
373 'usage' => $result['usage'],
374 ];
375 }
376
377 public function ajax_ai_get_completion_text( $data ) {
378 $this->verify_permissions( $data['editor_post_id'] );
379
380 $app = $this->get_ai_app();
381
382 if ( empty( $data['payload']['prompt'] ) ) {
383 throw new \Exception( 'Missing prompt' );
384 }
385
386 if ( ! $app->is_connected() ) {
387 throw new \Exception( 'not_connected' );
388 }
389
390 $context = $this->get_request_context( $data );
391
392 $request_ids = $this->get_request_ids( $data['payload'] );
393
394 $result = $app->get_completion_text( $data['payload']['prompt'], $context, $request_ids );
395 $this->throw_on_error( $result );
396
397 return [
398 'text' => $result['text'],
399 'response_id' => $result['responseId'],
400 'usage' => $result['usage'],
401 ];
402 }
403
404
405 public function ajax_ai_get_excerpt( $data ): array {
406 $app = $this->get_ai_app();
407
408 if ( empty( $data['payload']['content'] ) ) {
409 throw new \Exception( 'Missing content' );
410 }
411
412 if ( ! $app->is_connected() ) {
413 throw new \Exception( 'Not connected' );
414 }
415
416 $context = $this->get_request_context( $data );
417
418 $request_ids = $this->get_request_ids( $data['payload'] );
419
420 $result = $app->get_excerpt( $data['payload']['content'], $context, $request_ids );
421 $this->throw_on_error( $result );
422
423 return [
424 'text' => $result['text'],
425 'response_id' => $result['responseId'],
426 'usage' => $result['usage'],
427 ];
428 }
429
430 public function ajax_ai_get_featured_image( $data ): array {
431 $this->verify_upload_permissions( $data );
432
433 if ( empty( $data['payload']['prompt'] ) ) {
434 throw new \Exception( 'Missing prompt' );
435 }
436
437 $app = $this->get_ai_app();
438
439 if ( ! $app->is_connected() ) {
440 throw new \Exception( 'not_connected' );
441 }
442
443 $context = $this->get_request_context( $data );
444 $request_ids = $this->get_request_ids( $data['payload'] );
445
446 $result = $app->get_featured_image( $data, $context, $request_ids );
447
448 $this->throw_on_error( $result );
449
450 return [
451 'images' => $result['images'],
452 'response_id' => $result['responseId'],
453 'usage' => $result['usage'],
454 ];
455 }
456
457 private function get_ai_app() : Ai {
458 return Plugin::$instance->common->get_component( 'connect' )->get_app( 'ai' );
459 }
460
461 private function get_request_context( $data ) {
462 if ( empty( $data['context'] ) ) {
463 return [];
464 }
465
466 return $data['context'];
467 }
468
469 private function get_request_ids( $data ) {
470 if ( empty( $data['requestIds'] ) ) {
471 return new \stdClass();
472 }
473
474 return $data['requestIds'];
475 }
476
477 public function ajax_ai_get_edit_text( $data ) {
478 $this->verify_permissions( $data['editor_post_id'] );
479
480 $app = $this->get_ai_app();
481
482 if ( empty( $data['payload']['input'] ) ) {
483 throw new \Exception( 'Missing input' );
484 }
485
486 if ( empty( $data['payload']['instruction'] ) ) {
487 throw new \Exception( 'Missing instruction' );
488 }
489
490 if ( ! $app->is_connected() ) {
491 throw new \Exception( 'not_connected' );
492 }
493
494 $context = $this->get_request_context( $data );
495
496 $request_ids = $this->get_request_ids( $data['payload'] );
497
498 $result = $app->get_edit_text( $data, $context, $request_ids );
499 $this->throw_on_error( $result );
500
501 return [
502 'text' => $result['text'],
503 'response_id' => $result['responseId'],
504 'usage' => $result['usage'],
505 ];
506 }
507
508 public function ajax_ai_get_custom_code( $data ) {
509 $app = $this->get_ai_app();
510
511 if ( empty( $data['payload']['prompt'] ) ) {
512 throw new \Exception( 'Missing prompt' );
513 }
514
515 if ( empty( $data['payload']['language'] ) ) {
516 throw new \Exception( 'Missing language' );
517 }
518
519 if ( ! $app->is_connected() ) {
520 throw new \Exception( 'not_connected' );
521 }
522
523 $context = $this->get_request_context( $data );
524
525 $request_ids = $this->get_request_ids( $data['payload'] );
526
527 $result = $app->get_custom_code( $data, $context, $request_ids );
528 $this->throw_on_error( $result );
529
530 return [
531 'text' => $result['text'],
532 'response_id' => $result['responseId'],
533 'usage' => $result['usage'],
534 ];
535 }
536
537 public function ajax_ai_get_custom_css( $data ) {
538 $this->verify_permissions( $data['editor_post_id'] );
539
540 $app = $this->get_ai_app();
541
542 if ( empty( $data['payload']['prompt'] ) ) {
543 throw new \Exception( 'Missing prompt' );
544 }
545
546 if ( empty( $data['payload']['html_markup'] ) ) {
547 $data['html_markup'] = '';
548 }
549
550 if ( empty( $data['payload']['element_id'] ) ) {
551 throw new \Exception( 'Missing element_id' );
552 }
553
554 if ( ! $app->is_connected() ) {
555 throw new \Exception( 'not_connected' );
556 }
557
558 $context = $this->get_request_context( $data );
559 $request_ids = $this->get_request_ids( $data['payload'] );
560
561 $result = $app->get_custom_css( $data, $context, $request_ids );
562 $this->throw_on_error( $result );
563
564 return [
565 'text' => $result['text'],
566 'response_id' => $result['responseId'],
567 'usage' => $result['usage'],
568 ];
569 }
570
571 public function ajax_ai_set_get_started( $data ) {
572 $app = $this->get_ai_app();
573
574 User::set_introduction_viewed( [
575 'introductionKey' => 'ai_get_started',
576 ] );
577
578 return $app->set_get_started();
579 }
580
581 public function ajax_ai_set_status_feedback( $data ) {
582 if ( empty( $data['response_id'] ) ) {
583 throw new \Exception( 'Missing response_id' );
584 }
585
586 $app = $this->get_ai_app();
587
588 if ( ! $app->is_connected() ) {
589 throw new \Exception( 'not_connected' );
590 }
591
592 $app->set_status_feedback( $data['response_id'] );
593
594 return [];
595 }
596
597 public function ajax_ai_get_text_to_image( $data ) {
598 $this->verify_upload_permissions( $data );
599
600 if ( empty( $data['payload']['prompt'] ) ) {
601 throw new \Exception( 'Missing prompt' );
602 }
603
604 $app = $this->get_ai_app();
605
606 if ( ! $app->is_connected() ) {
607 throw new \Exception( 'not_connected' );
608 }
609
610 $context = $this->get_request_context( $data );
611 $request_ids = $this->get_request_ids( $data['payload'] );
612
613 $result = $app->get_text_to_image( $data, $context, $request_ids );
614
615 $this->throw_on_error( $result );
616
617 return [
618 'images' => $result['images'],
619 'response_id' => $result['responseId'],
620 'usage' => $result['usage'],
621 ];
622 }
623
624 public function ajax_ai_get_image_to_image( $data ) {
625 $this->verify_upload_permissions( $data );
626
627 $app = $this->get_ai_app();
628
629 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
630 throw new \Exception( 'Missing Image' );
631 }
632
633 if ( empty( $data['payload']['settings'] ) ) {
634 throw new \Exception( 'Missing prompt settings' );
635 }
636
637 if ( ! $app->is_connected() ) {
638 throw new \Exception( 'not_connected' );
639 }
640
641 $context = $this->get_request_context( $data );
642 $request_ids = $this->get_request_ids( $data['payload'] );
643
644 $result = $app->get_image_to_image( [
645 'prompt' => $data['payload']['prompt'],
646 'promptSettings' => $data['payload']['settings'],
647 'attachment_id' => $data['payload']['image']['id'],
648 ], $context, $request_ids );
649
650 $this->throw_on_error( $result );
651
652 return [
653 'images' => $result['images'],
654 'response_id' => $result['responseId'],
655 'usage' => $result['usage'],
656 ];
657 }
658
659 public function ajax_ai_get_image_to_image_upscale( $data ) {
660 $this->verify_upload_permissions( $data );
661
662 $app = $this->get_ai_app();
663
664 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
665 throw new \Exception( 'Missing Image' );
666 }
667
668 if ( empty( $data['payload']['promptSettings'] ) ) {
669 throw new \Exception( 'Missing prompt settings' );
670 }
671
672 if ( ! $app->is_connected() ) {
673 throw new \Exception( 'not_connected' );
674 }
675
676 $context = $this->get_request_context( $data );
677 $request_ids = $this->get_request_ids( $data['payload'] );
678
679 $result = $app->get_image_to_image_upscale( [
680 'promptSettings' => $data['payload']['promptSettings'],
681 'attachment_id' => $data['payload']['image']['id'],
682 ], $context, $request_ids );
683
684 $this->throw_on_error( $result );
685
686 return [
687 'images' => $result['images'],
688 'response_id' => $result['responseId'],
689 'usage' => $result['usage'],
690 ];
691 }
692
693 public function ajax_ai_get_image_to_image_replace_background( $data ) {
694 $this->verify_upload_permissions( $data );
695
696 $app = $this->get_ai_app();
697
698 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
699 throw new \Exception( 'Missing Image' );
700 }
701
702 if ( empty( $data['payload']['prompt'] ) ) {
703 throw new \Exception( 'Prompt Missing' );
704 }
705
706 if ( ! $app->is_connected() ) {
707 throw new \Exception( 'not_connected' );
708 }
709
710 $context = $this->get_request_context( $data );
711 $request_ids = $this->get_request_ids( $data['payload'] );
712
713 $result = $app->get_image_to_image_replace_background( [
714 'attachment_id' => $data['payload']['image']['id'],
715 'prompt' => $data['payload']['prompt'],
716 ], $context, $request_ids );
717
718 $this->throw_on_error( $result );
719
720 return [
721 'images' => $result['images'],
722 'response_id' => $result['responseId'],
723 'usage' => $result['usage'],
724 ];
725 }
726
727 public function ajax_ai_get_image_to_image_remove_background( $data ) {
728 $this->verify_upload_permissions( $data );
729
730 $app = $this->get_ai_app();
731
732 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
733 throw new \Exception( 'Missing Image' );
734 }
735
736 if ( ! $app->is_connected() ) {
737 throw new \Exception( 'not_connected' );
738 }
739
740 $context = $this->get_request_context( $data );
741 $request_ids = $this->get_request_ids( $data['payload'] );
742 $result = $app->get_image_to_image_remove_background( [
743 'attachment_id' => $data['payload']['image']['id'],
744 ], $context, $request_ids );
745
746 $this->throw_on_error( $result );
747
748 return [
749 'images' => $result['images'],
750 'response_id' => $result['responseId'],
751 'usage' => $result['usage'],
752 ];
753 }
754
755 public function ajax_ai_get_image_to_image_mask( $data ) {
756 $this->verify_upload_permissions( $data );
757
758 $app = $this->get_ai_app();
759
760 if ( empty( $data['payload']['prompt'] ) ) {
761 throw new \Exception( 'Missing prompt' );
762 }
763
764 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
765 throw new \Exception( 'Missing Image' );
766 }
767
768 if ( empty( $data['payload']['settings'] ) ) {
769 throw new \Exception( 'Missing prompt settings' );
770 }
771
772 if ( ! $app->is_connected() ) {
773 throw new \Exception( 'not_connected' );
774 }
775
776 if ( empty( $data['payload']['mask'] ) ) {
777 throw new \Exception( 'Missing Mask' );
778 }
779
780 $context = $this->get_request_context( $data );
781 $request_ids = $this->get_request_ids( $data['payload'] );
782
783 $result = $app->get_image_to_image_mask( [
784 'prompt' => $data['payload']['prompt'],
785 'attachment_id' => $data['payload']['image']['id'],
786 'mask' => $data['payload']['mask'],
787 ], $context, $request_ids );
788
789 $this->throw_on_error( $result );
790
791 return [
792 'images' => $result['images'],
793 'response_id' => $result['responseId'],
794 'usage' => $result['usage'],
795 ];
796 }
797 public function ajax_ai_get_image_to_image_mask_cleanup( $data ) {
798 $this->verify_upload_permissions( $data );
799
800 $app = $this->get_ai_app();
801
802 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
803 throw new \Exception( 'Missing Image' );
804 }
805
806 if ( empty( $data['payload']['settings'] ) ) {
807 throw new \Exception( 'Missing prompt settings' );
808 }
809
810 if ( ! $app->is_connected() ) {
811 throw new \Exception( 'not_connected' );
812 }
813
814 if ( empty( $data['payload']['mask'] ) ) {
815 throw new \Exception( 'Missing Mask' );
816 }
817
818 $context = $this->get_request_context( $data );
819 $request_ids = $this->get_request_ids( $data['payload'] );
820
821 $result = $app->get_image_to_image_mask_cleanup( [
822 'attachment_id' => $data['payload']['image']['id'],
823 'mask' => $data['payload']['mask'],
824 ], $context, $request_ids );
825
826 $this->throw_on_error( $result );
827
828 return [
829 'images' => $result['images'],
830 'response_id' => $result['responseId'],
831 'usage' => $result['usage'],
832 ];
833 }
834
835 public function ajax_ai_get_image_to_image_outpainting( $data ) {
836 $this->verify_upload_permissions( $data );
837
838 $app = $this->get_ai_app();
839
840 if ( ! $app->is_connected() ) {
841 throw new \Exception( 'not_connected' );
842 }
843
844 if ( empty( $data['payload']['mask'] ) ) {
845 throw new \Exception( 'Missing Expended Image' );
846 }
847
848 $context = $this->get_request_context( $data );
849 $request_ids = $this->get_request_ids( $data['payload'] );
850 $result = $app->get_image_to_image_out_painting( [
851 'size' => $data['payload']['size'],
852 'position' => $data['payload']['position'],
853 'mask' => $data['payload']['mask'],
854 'image_base64' => $data['payload']['image_base64'],
855 ], $context, $request_ids );
856
857 $this->throw_on_error( $result );
858
859 return [
860 'images' => $result['images'],
861 'response_id' => $result['responseId'],
862 'usage' => $result['usage'],
863 ];
864 }
865
866 public function ajax_ai_upload_image( $data ) {
867 if ( empty( $data['image'] ) ) {
868 throw new \Exception( 'Missing image data' );
869 }
870
871 $image = $data['image'];
872
873 if ( empty( $image['image_url'] ) ) {
874 throw new \Exception( 'Missing image_url' );
875 }
876
877 $image_data = $this->upload_image( $image['image_url'], $data['prompt'], $data['editor_post_id'] );
878
879 if ( is_wp_error( $image_data ) ) {
880 throw new \Exception( $image_data->get_error_message() );
881 }
882
883 if ( ! empty( $image['use_gallery_image'] ) && ! empty( $image['id'] ) ) {
884 $app = $this->get_ai_app();
885 $app->set_used_gallery_image( $image['id'] );
886 }
887
888 return [
889 'image' => array_merge( $image_data, $data ),
890 ];
891 }
892
893 public function ajax_ai_generate_layout( $data ) {
894 $this->verify_permissions( $data['editor_post_id'] );
895
896 $app = $this->get_ai_app();
897
898 if ( empty( $data['prompt'] ) && empty( $data['attachments'] ) ) {
899 throw new \Exception( 'Missing prompt / attachments' );
900 }
901
902 if ( ! $app->is_connected() ) {
903 throw new \Exception( 'not_connected' );
904 }
905
906 $result = $app->generate_layout(
907 $data,
908 $this->prepare_generate_layout_context( $data )
909 );
910
911 if ( is_wp_error( $result ) ) {
912 $message = $result->get_error_message();
913
914 if ( is_array( $message ) ) {
915 $message = implode( ', ', $message );
916 throw new \Exception( $message );
917 }
918
919 $this->throw_on_error( $result );
920 }
921
922 $elements = $result['text']['elements'] ?? [];
923 $base_template_id = $result['baseTemplateId'] ?? null;
924 $template_type = $result['templateType'] ?? null;
925
926 if ( empty( $elements ) || ! is_array( $elements ) ) {
927 throw new \Exception( 'unknown_error' );
928 }
929
930 if ( 1 === count( $elements ) ) {
931 $template = $elements[0];
932 } else {
933 $template = [
934 'elType' => 'container',
935 'elements' => $elements,
936 'settings' => [
937 'content_width' => 'full',
938 'flex_gap' => [
939 'column' => '0',
940 'row' => '0',
941 'unit' => 'px',
942 ],
943 'padding' => [
944 'unit' => 'px',
945 'top' => '0',
946 'right' => '0',
947 'bottom' => '0',
948 'left' => '0',
949 'isLinked' => true,
950 ],
951 ],
952 ];
953 }
954
955 return [
956 'all' => [],
957 'text' => $template,
958 'response_id' => $result['responseId'],
959 'usage' => $result['usage'],
960 'base_template_id' => $base_template_id,
961 'template_type' => $template_type,
962 ];
963 }
964
965 public function ajax_ai_get_layout_prompt_enhancer( $data ) {
966 $this->verify_permissions( $data['editor_post_id'] );
967
968 $app = $this->get_ai_app();
969
970 if ( empty( $data['prompt'] ) ) {
971 throw new \Exception( 'Missing prompt' );
972 }
973
974 if ( ! $app->is_connected() ) {
975 throw new \Exception( 'not_connected' );
976 }
977
978 $result = $app->get_layout_prompt_enhanced(
979 $data['prompt'],
980 $data['enhance_type'],
981 $this->prepare_generate_layout_context( $data )
982 );
983
984 $this->throw_on_error( $result );
985
986 return [
987 'text' => $result['text'] ?? $data['prompt'],
988 'response_id' => $result['responseId'] ?? '',
989 'usage' => $result['usage'] ?? '',
990 ];
991 }
992
993 private function prepare_generate_layout_context( $data ) {
994 $request_context = $this->get_request_context( $data );
995 $kit = Plugin::$instance->kits_manager->get_active_kit();
996
997 if ( ! $kit ) {
998 return $request_context;
999 }
1000
1001 $kits_data = Collection::make( $kit->get_data()['settings'] ?? [] );
1002
1003 $colors = $kits_data
1004 ->filter( function ( $_, $key ) {
1005 return in_array( $key, [ 'system_colors', 'custom_colors' ], true );
1006 } )
1007 ->flatten()
1008 ->filter( function ( $val ) {
1009 return ! empty( $val['_id'] );
1010 } )
1011 ->map( function ( $val ) {
1012 return [
1013 'id' => $val['_id'],
1014 'label' => $val['title'] ?? null,
1015 'value' => $val['color'] ?? null,
1016 ];
1017 } );
1018
1019 $typography = $kits_data
1020 ->filter( function ( $_, $key ) {
1021 return in_array( $key, [ 'system_typography', 'custom_typography' ], true );
1022 } )
1023 ->flatten()
1024 ->filter( function ( $val ) {
1025 return ! empty( $val['_id'] );
1026 } )
1027 ->map( function ( $val ) {
1028 $font_size = null;
1029
1030 if ( isset(
1031 $val['typography_font_size']['unit'],
1032 $val['typography_font_size']['size']
1033 ) ) {
1034 $prop = $val['typography_font_size'];
1035
1036 $font_size = 'custom' === $prop['unit']
1037 ? $prop['size']
1038 : $prop['size'] . $prop['unit'];
1039 }
1040
1041 return [
1042 'id' => $val['_id'],
1043 'label' => $val['title'] ?? null,
1044 'value' => [
1045 'family' => $val['typography_font_family'] ?? null,
1046 'weight' => $val['typography_font_weight'] ?? null,
1047 'style' => $val['typography_font_style'] ?? null,
1048 'size' => $font_size,
1049 ],
1050 ];
1051 } );
1052
1053 $request_context['globals'] = [
1054 'colors' => $colors->all(),
1055 'typography' => $typography->all(),
1056 ];
1057
1058 return $request_context;
1059 }
1060
1061 private function upload_image( $image_url, $image_title, $parent_post_id = 0 ) {
1062 if ( ! current_user_can( 'upload_files' ) ) {
1063 throw new \Exception( 'Not Allowed to Upload images' );
1064 }
1065
1066 $attachment_id = media_sideload_image( $image_url, $parent_post_id, $image_title, 'id' );
1067
1068 if ( ! empty( $attachment_id['error'] ) ) {
1069 return new \WP_Error( 'upload_error', $attachment_id['error'] );
1070 }
1071
1072 return [
1073 'id' => $attachment_id,
1074 'url' => wp_get_attachment_image_url( $attachment_id, 'full' ),
1075 'alt' => $image_title,
1076 'source' => 'library',
1077 ];
1078 }
1079
1080 public function ajax_ai_get_history( $data ): array {
1081 $type = $data['type'] ?? self::HISTORY_TYPE_ALL;
1082
1083 if ( ! in_array( $type, self::VALID_HISTORY_TYPES, true ) ) {
1084 throw new \Exception( 'Invalid history type' );
1085 }
1086
1087 $page = sanitize_text_field( $data['page'] ?? 1 );
1088 $limit = sanitize_text_field( $data['limit'] ?? 10 );
1089
1090 $app = $this->get_ai_app();
1091
1092 if ( ! $app->is_connected() ) {
1093 throw new \Exception( 'not_connected' );
1094 }
1095
1096 $context = $this->get_request_context( $data );
1097
1098 $result = $app->get_history_by_type( $type, $page, $limit, $context );
1099
1100 if ( is_wp_error( $result ) ) {
1101 throw new \Exception( $result->get_error_message() );
1102 }
1103
1104 return $result;
1105 }
1106
1107 public function ajax_ai_delete_history_item( $data ): array {
1108 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
1109 throw new \Exception( 'Missing id parameter' );
1110 }
1111
1112 $app = $this->get_ai_app();
1113
1114 if ( ! $app->is_connected() ) {
1115 throw new \Exception( 'not_connected' );
1116 }
1117
1118 $context = $this->get_request_context( $data );
1119
1120 $result = $app->delete_history_item( $data['id'], $context );
1121
1122 if ( is_wp_error( $result ) ) {
1123 throw new \Exception( $result->get_error_message() );
1124 }
1125
1126 return [];
1127 }
1128
1129 public function ajax_ai_toggle_favorite_history_item( $data ): array {
1130 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
1131 throw new \Exception( 'Missing id parameter' );
1132 }
1133
1134 $app = $this->get_ai_app();
1135
1136 if ( ! $app->is_connected() ) {
1137 throw new \Exception( 'not_connected' );
1138 }
1139
1140 $context = $this->get_request_context( $data );
1141
1142 $result = $app->toggle_favorite_history_item( $data['id'], $context );
1143
1144 if ( is_wp_error( $result ) ) {
1145 throw new \Exception( $result->get_error_message() );
1146 }
1147
1148 return [];
1149 }
1150
1151 /**
1152 * @param mixed $result
1153 */
1154 private function throw_on_error( $result ): void {
1155 if ( is_wp_error( $result ) ) {
1156 wp_send_json_error( [
1157 'message' => $result->get_error_message(),
1158 'extra_data' => $result->get_error_data(),
1159 ] );
1160 }
1161 }
1162 }
1163