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

1,012 lines 26.4 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 $this->throw_on_error( $result );
303
304 return [
305 'text' => $result['text'],
306 'response_id' => $result['responseId'],
307 'usage' => $result['usage'],
308 ];
309 }
310
311 public function ajax_ai_get_completion_text( $data ) {
312 $this->verify_permissions( $data['editor_post_id'] );
313
314 $app = $this->get_ai_app();
315
316 if ( empty( $data['payload']['prompt'] ) ) {
317 throw new \Exception( 'Missing prompt' );
318 }
319
320 if ( ! $app->is_connected() ) {
321 throw new \Exception( 'not_connected' );
322 }
323
324 $context = $this->get_request_context( $data );
325
326 $request_ids = $this->get_request_ids( $data['payload'] );
327
328 $result = $app->get_completion_text( $data['payload']['prompt'], $context, $request_ids );
329 $this->throw_on_error( $result );
330
331 return [
332 'text' => $result['text'],
333 'response_id' => $result['responseId'],
334 'usage' => $result['usage'],
335 ];
336 }
337
338 private function get_ai_app() : Ai {
339 return Plugin::$instance->common->get_component( 'connect' )->get_app( 'ai' );
340 }
341
342 private function get_request_context( $data ) {
343 if ( empty( $data['context'] ) ) {
344 return [];
345 }
346
347 return $data['context'];
348 }
349
350 private function get_request_ids( $data ) {
351 if ( empty( $data['requestIds'] ) ) {
352 return new \stdClass();
353 }
354
355 return $data['requestIds'];
356 }
357
358 public function ajax_ai_get_edit_text( $data ) {
359 $this->verify_permissions( $data['editor_post_id'] );
360
361 $app = $this->get_ai_app();
362
363 if ( empty( $data['payload']['input'] ) ) {
364 throw new \Exception( 'Missing input' );
365 }
366
367 if ( empty( $data['payload']['instruction'] ) ) {
368 throw new \Exception( 'Missing instruction' );
369 }
370
371 if ( ! $app->is_connected() ) {
372 throw new \Exception( 'not_connected' );
373 }
374
375 $context = $this->get_request_context( $data );
376
377 $request_ids = $this->get_request_ids( $data['payload'] );
378
379 $result = $app->get_edit_text( $data, $context, $request_ids );
380 $this->throw_on_error( $result );
381
382 return [
383 'text' => $result['text'],
384 'response_id' => $result['responseId'],
385 'usage' => $result['usage'],
386 ];
387 }
388
389 public function ajax_ai_get_custom_code( $data ) {
390 $app = $this->get_ai_app();
391
392 if ( empty( $data['payload']['prompt'] ) ) {
393 throw new \Exception( 'Missing prompt' );
394 }
395
396 if ( empty( $data['payload']['language'] ) ) {
397 throw new \Exception( 'Missing language' );
398 }
399
400 if ( ! $app->is_connected() ) {
401 throw new \Exception( 'not_connected' );
402 }
403
404 $context = $this->get_request_context( $data );
405
406 $request_ids = $this->get_request_ids( $data['payload'] );
407
408 $result = $app->get_custom_code( $data, $context, $request_ids );
409 $this->throw_on_error( $result );
410
411 return [
412 'text' => $result['text'],
413 'response_id' => $result['responseId'],
414 'usage' => $result['usage'],
415 ];
416 }
417
418 public function ajax_ai_get_custom_css( $data ) {
419 $this->verify_permissions( $data['editor_post_id'] );
420
421 $app = $this->get_ai_app();
422
423 if ( empty( $data['payload']['prompt'] ) ) {
424 throw new \Exception( 'Missing prompt' );
425 }
426
427 if ( empty( $data['payload']['html_markup'] ) ) {
428 $data['html_markup'] = '';
429 }
430
431 if ( empty( $data['payload']['element_id'] ) ) {
432 throw new \Exception( 'Missing element_id' );
433 }
434
435 if ( ! $app->is_connected() ) {
436 throw new \Exception( 'not_connected' );
437 }
438
439 $context = $this->get_request_context( $data );
440 $request_ids = $this->get_request_ids( $data['payload'] );
441
442 $result = $app->get_custom_css( $data, $context, $request_ids );
443 $this->throw_on_error( $result );
444
445 return [
446 'text' => $result['text'],
447 'response_id' => $result['responseId'],
448 'usage' => $result['usage'],
449 ];
450 }
451
452 public function ajax_ai_set_get_started( $data ) {
453 $app = $this->get_ai_app();
454
455 User::set_introduction_viewed( [
456 'introductionKey' => 'ai_get_started',
457 ] );
458
459 return $app->set_get_started();
460 }
461
462 public function ajax_ai_set_status_feedback( $data ) {
463 if ( empty( $data['response_id'] ) ) {
464 throw new \Exception( 'Missing response_id' );
465 }
466
467 $app = $this->get_ai_app();
468
469 if ( ! $app->is_connected() ) {
470 throw new \Exception( 'not_connected' );
471 }
472
473 $app->set_status_feedback( $data['response_id'] );
474
475 return [];
476 }
477
478 public function ajax_ai_get_text_to_image( $data ) {
479 $this->verify_permissions( $data['editor_post_id'] );
480
481 if ( empty( $data['payload']['prompt'] ) ) {
482 throw new \Exception( 'Missing prompt' );
483 }
484
485 $app = $this->get_ai_app();
486
487 if ( ! $app->is_connected() ) {
488 throw new \Exception( 'not_connected' );
489 }
490
491 $context = $this->get_request_context( $data );
492 $request_ids = $this->get_request_ids( $data['payload'] );
493
494 $result = $app->get_text_to_image( $data, $context, $request_ids );
495
496 $this->throw_on_error( $result );
497
498 return [
499 'images' => $result['images'],
500 'response_id' => $result['responseId'],
501 'usage' => $result['usage'],
502 ];
503 }
504
505 public function ajax_ai_get_image_to_image( $data ) {
506 $this->verify_permissions( $data['editor_post_id'] );
507
508 $app = $this->get_ai_app();
509
510 if ( empty( $data['payload']['prompt'] ) ) {
511 throw new \Exception( 'Missing prompt' );
512 }
513
514 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
515 throw new \Exception( 'Missing Image' );
516 }
517
518 if ( empty( $data['payload']['settings'] ) ) {
519 throw new \Exception( 'Missing prompt settings' );
520 }
521
522 if ( ! $app->is_connected() ) {
523 throw new \Exception( 'not_connected' );
524 }
525
526 $context = $this->get_request_context( $data );
527 $request_ids = $this->get_request_ids( $data['payload'] );
528
529 $result = $app->get_image_to_image( [
530 'prompt' => $data['payload']['prompt'],
531 'promptSettings' => $data['payload']['settings'],
532 'attachment_id' => $data['payload']['image']['id'],
533 ], $context, $request_ids );
534
535 $this->throw_on_error( $result );
536
537 return [
538 'images' => $result['images'],
539 'response_id' => $result['responseId'],
540 'usage' => $result['usage'],
541 ];
542 }
543
544 public function ajax_ai_get_image_to_image_upscale( $data ) {
545 $this->verify_permissions( $data['editor_post_id'] );
546
547 $app = $this->get_ai_app();
548
549 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
550 throw new \Exception( 'Missing Image' );
551 }
552
553 if ( empty( $data['payload']['promptSettings'] ) ) {
554 throw new \Exception( 'Missing prompt settings' );
555 }
556
557 if ( ! $app->is_connected() ) {
558 throw new \Exception( 'not_connected' );
559 }
560
561 $context = $this->get_request_context( $data );
562 $request_ids = $this->get_request_ids( $data['payload'] );
563
564 $result = $app->get_image_to_image_upscale( [
565 'promptSettings' => $data['payload']['promptSettings'],
566 'attachment_id' => $data['payload']['image']['id'],
567 ], $context, $request_ids );
568
569 $this->throw_on_error( $result );
570
571 return [
572 'images' => $result['images'],
573 'response_id' => $result['responseId'],
574 'usage' => $result['usage'],
575 ];
576 }
577
578 public function ajax_ai_get_image_to_image_replace_background( $data ) {
579 $this->verify_permissions( $data['editor_post_id'] );
580
581 $app = $this->get_ai_app();
582
583 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
584 throw new \Exception( 'Missing Image' );
585 }
586
587 if ( empty( $data['payload']['prompt'] ) ) {
588 throw new \Exception( 'Prompt Missing' );
589 }
590
591 if ( ! $app->is_connected() ) {
592 throw new \Exception( 'not_connected' );
593 }
594
595 $context = $this->get_request_context( $data );
596 $request_ids = $this->get_request_ids( $data['payload'] );
597
598 $result = $app->get_image_to_image_replace_background( [
599 'attachment_id' => $data['payload']['image']['id'],
600 'prompt' => $data['payload']['prompt'],
601 ], $context, $request_ids );
602
603 $this->throw_on_error( $result );
604
605 return [
606 'images' => $result['images'],
607 'response_id' => $result['responseId'],
608 'usage' => $result['usage'],
609 ];
610 }
611
612 public function ajax_ai_get_image_to_image_remove_background( $data ) {
613 $this->verify_permissions( $data['editor_post_id'] );
614
615 $app = $this->get_ai_app();
616
617 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
618 throw new \Exception( 'Missing Image' );
619 }
620
621 if ( ! $app->is_connected() ) {
622 throw new \Exception( 'not_connected' );
623 }
624
625 $context = $this->get_request_context( $data );
626 $request_ids = $this->get_request_ids( $data['payload'] );
627 $result = $app->get_image_to_image_remove_background( [
628 'attachment_id' => $data['payload']['image']['id'],
629 ], $context, $request_ids );
630
631 $this->throw_on_error( $result );
632
633 return [
634 'images' => $result['images'],
635 'response_id' => $result['responseId'],
636 'usage' => $result['usage'],
637 ];
638 }
639
640 public function ajax_ai_get_image_to_image_mask( $data ) {
641 $this->verify_permissions( $data['editor_post_id'] );
642
643 $app = $this->get_ai_app();
644
645 if ( empty( $data['payload']['prompt'] ) ) {
646 throw new \Exception( 'Missing prompt' );
647 }
648
649 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
650 throw new \Exception( 'Missing Image' );
651 }
652
653 if ( empty( $data['payload']['settings'] ) ) {
654 throw new \Exception( 'Missing prompt settings' );
655 }
656
657 if ( ! $app->is_connected() ) {
658 throw new \Exception( 'not_connected' );
659 }
660
661 if ( empty( $data['payload']['mask'] ) ) {
662 throw new \Exception( 'Missing Mask' );
663 }
664
665 $context = $this->get_request_context( $data );
666 $request_ids = $this->get_request_ids( $data['payload'] );
667
668 $result = $app->get_image_to_image_mask( [
669 'prompt' => $data['payload']['prompt'],
670 'promptSettings' => $data['payload']['settings'],
671 'attachment_id' => $data['payload']['image']['id'],
672 'mask' => $data['payload']['mask'],
673 ], $context, $request_ids );
674
675 $this->throw_on_error( $result );
676
677 return [
678 'images' => $result['images'],
679 'response_id' => $result['responseId'],
680 'usage' => $result['usage'],
681 ];
682 }
683 public function ajax_ai_get_image_to_image_outpainting( $data ) {
684 $this->verify_permissions( $data['editor_post_id'] );
685
686 $app = $this->get_ai_app();
687
688 if ( empty( $data['payload']['prompt'] ) ) {
689 throw new \Exception( 'Missing prompt' );
690 }
691
692 if ( ! $app->is_connected() ) {
693 throw new \Exception( 'not_connected' );
694 }
695
696 if ( empty( $data['payload']['mask'] ) ) {
697 throw new \Exception( 'Missing Expended Image' );
698 }
699
700 $context = $this->get_request_context( $data );
701 $request_ids = $this->get_request_ids( $data['payload'] );
702 $result = $app->get_image_to_image_out_painting( [
703 'prompt' => $data['payload']['prompt'],
704 'mask' => $data['payload']['mask'],
705 ], $context, $request_ids );
706
707 $this->throw_on_error( $result );
708
709 return [
710 'images' => $result['images'],
711 'response_id' => $result['responseId'],
712 'usage' => $result['usage'],
713 ];
714 }
715
716 public function ajax_ai_upload_image( $data ) {
717 if ( empty( $data['image'] ) ) {
718 throw new \Exception( 'Missing image data' );
719 }
720
721 $image = $data['image'];
722
723 if ( empty( $image['image_url'] ) ) {
724 throw new \Exception( 'Missing image_url' );
725 }
726
727 $image_data = $this->upload_image( $image['image_url'], $data['prompt'], $data['editor_post_id'] );
728
729 if ( is_wp_error( $image_data ) ) {
730 throw new \Exception( $image_data->get_error_message() );
731 }
732
733 if ( ! empty( $image['use_gallery_image'] ) && ! empty( $image['id'] ) ) {
734 $app = $this->get_ai_app();
735 $app->set_used_gallery_image( $image['id'] );
736 }
737
738 return [
739 'image' => array_merge( $image_data, $data ),
740 ];
741 }
742
743 public function ajax_ai_generate_layout( $data ) {
744 $this->verify_permissions( $data['editor_post_id'] );
745
746 $app = $this->get_ai_app();
747
748 if ( empty( $data['prompt'] ) && empty( $data['attachments'] ) ) {
749 throw new \Exception( 'Missing prompt / attachments' );
750 }
751
752 if ( ! $app->is_connected() ) {
753 throw new \Exception( 'not_connected' );
754 }
755
756 $result = $app->generate_layout(
757 $data,
758 $this->prepare_generate_layout_context()
759 );
760
761 if ( is_wp_error( $result ) ) {
762 $message = $result->get_error_message();
763
764 if ( is_array( $message ) ) {
765 $message = implode( ', ', $message );
766 throw new \Exception( $message );
767 }
768
769 $this->throw_on_error( $result );
770 }
771
772 $elements = $result['text']['elements'] ?? [];
773 $base_template_id = $result['baseTemplateId'] ?? null;
774 $template_type = $result['templateType'] ?? null;
775
776 if ( empty( $elements ) || ! is_array( $elements ) ) {
777 throw new \Exception( 'unknown_error' );
778 }
779
780 if ( 1 === count( $elements ) ) {
781 $template = $elements[0];
782 } else {
783 $template = [
784 'elType' => 'container',
785 'elements' => $elements,
786 'settings' => [
787 'content_width' => 'full',
788 'flex_gap' => [
789 'column' => '0',
790 'row' => '0',
791 'unit' => 'px',
792 ],
793 'padding' => [
794 'unit' => 'px',
795 'top' => '0',
796 'right' => '0',
797 'bottom' => '0',
798 'left' => '0',
799 'isLinked' => true,
800 ],
801 ],
802 ];
803 }
804
805 return [
806 'all' => [],
807 'text' => $template,
808 'response_id' => $result['responseId'],
809 'usage' => $result['usage'],
810 'base_template_id' => $base_template_id,
811 'template_type' => $template_type,
812 ];
813 }
814
815 public function ajax_ai_get_layout_prompt_enhancer( $data ) {
816 $this->verify_permissions( $data['editor_post_id'] );
817
818 $app = $this->get_ai_app();
819
820 if ( empty( $data['prompt'] ) ) {
821 throw new \Exception( 'Missing prompt' );
822 }
823
824 if ( ! $app->is_connected() ) {
825 throw new \Exception( 'not_connected' );
826 }
827
828 $result = $app->get_layout_prompt_enhanced(
829 $data['prompt'],
830 $data['enhance_type'],
831 $this->prepare_generate_layout_context()
832 );
833
834 $this->throw_on_error( $result );
835
836 return [
837 'text' => $result['text'] ?? $data['prompt'],
838 'response_id' => $result['responseId'] ?? '',
839 'usage' => $result['usage'] ?? '',
840 ];
841 }
842
843 private function prepare_generate_layout_context() {
844 $kit = Plugin::$instance->kits_manager->get_active_kit();
845
846 if ( ! $kit ) {
847 return [];
848 }
849
850 $kits_data = Collection::make( $kit->get_data()['settings'] ?? [] );
851
852 $colors = $kits_data
853 ->filter( function ( $_, $key ) {
854 return in_array( $key, [ 'system_colors', 'custom_colors' ], true );
855 } )
856 ->flatten()
857 ->filter( function ( $val ) {
858 return ! empty( $val['_id'] );
859 } )
860 ->map( function ( $val ) {
861 return [
862 'id' => $val['_id'],
863 'label' => $val['title'] ?? null,
864 'value' => $val['color'] ?? null,
865 ];
866 } );
867
868 $typography = $kits_data
869 ->filter( function ( $_, $key ) {
870 return in_array( $key, [ 'system_typography', 'custom_typography' ], true );
871 } )
872 ->flatten()
873 ->filter( function ( $val ) {
874 return ! empty( $val['_id'] );
875 } )
876 ->map( function ( $val ) {
877 $font_size = null;
878
879 if ( isset(
880 $val['typography_font_size']['unit'],
881 $val['typography_font_size']['size']
882 ) ) {
883 $prop = $val['typography_font_size'];
884
885 $font_size = 'custom' === $prop['unit']
886 ? $prop['size']
887 : $prop['size'] . $prop['unit'];
888 }
889
890 return [
891 'id' => $val['_id'],
892 'label' => $val['title'] ?? null,
893 'value' => [
894 'family' => $val['typography_font_family'] ?? null,
895 'weight' => $val['typography_font_weight'] ?? null,
896 'style' => $val['typography_font_style'] ?? null,
897 'size' => $font_size,
898 ],
899 ];
900 } );
901
902 return [
903 'globals' => [
904 'colors' => $colors->all(),
905 'typography' => $typography->all(),
906 ],
907 ];
908 }
909
910 private function upload_image( $image_url, $image_title, $parent_post_id = 0 ) {
911 if ( ! current_user_can( 'upload_files' ) ) {
912 throw new \Exception( 'Not Allowed to Upload images' );
913 }
914
915 $attachment_id = media_sideload_image( $image_url, $parent_post_id, $image_title, 'id' );
916
917 if ( ! empty( $attachment_id['error'] ) ) {
918 return new \WP_Error( 'upload_error', $attachment_id['error'] );
919 }
920
921 return [
922 'id' => $attachment_id,
923 'url' => wp_get_attachment_image_url( $attachment_id, 'full' ),
924 'alt' => $image_title,
925 'source' => 'library',
926 ];
927 }
928
929 public function ajax_ai_get_history( $data ): array {
930 $type = $data['type'] ?? self::HISTORY_TYPE_ALL;
931
932 if ( ! in_array( $type, self::VALID_HISTORY_TYPES, true ) ) {
933 throw new \Exception( 'Invalid history type' );
934 }
935
936 $page = sanitize_text_field( $data['page'] ?? 1 );
937 $limit = sanitize_text_field( $data['limit'] ?? 10 );
938
939 $app = $this->get_ai_app();
940
941 if ( ! $app->is_connected() ) {
942 throw new \Exception( 'not_connected' );
943 }
944
945 $context = $this->get_request_context( $data );
946
947 $result = $app->get_history_by_type( $type, $page, $limit, $context );
948
949 if ( is_wp_error( $result ) ) {
950 throw new \Exception( $result->get_error_message() );
951 }
952
953 return $result;
954 }
955
956 public function ajax_ai_delete_history_item( $data ): array {
957 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
958 throw new \Exception( 'Missing id parameter' );
959 }
960
961 $app = $this->get_ai_app();
962
963 if ( ! $app->is_connected() ) {
964 throw new \Exception( 'not_connected' );
965 }
966
967 $context = $this->get_request_context( $data );
968
969 $result = $app->delete_history_item( $data['id'], $context );
970
971 if ( is_wp_error( $result ) ) {
972 throw new \Exception( $result->get_error_message() );
973 }
974
975 return [];
976 }
977
978 public function ajax_ai_toggle_favorite_history_item( $data ): array {
979 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
980 throw new \Exception( 'Missing id parameter' );
981 }
982
983 $app = $this->get_ai_app();
984
985 if ( ! $app->is_connected() ) {
986 throw new \Exception( 'not_connected' );
987 }
988
989 $context = $this->get_request_context( $data );
990
991 $result = $app->toggle_favorite_history_item( $data['id'], $context );
992
993 if ( is_wp_error( $result ) ) {
994 throw new \Exception( $result->get_error_message() );
995 }
996
997 return [];
998 }
999
1000 /**
1001 * @param mixed $result
1002 */
1003 private function throw_on_error( $result ): void {
1004 if ( is_wp_error( $result ) ) {
1005 wp_send_json_error( [
1006 'message' => $result->get_error_message(),
1007 'extra_data' => $result->get_error_data(),
1008 ] );
1009 }
1010 }
1011 }
1012