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

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