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

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