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

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