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

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