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

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