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

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