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

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