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

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