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

1,008 lines 26.5 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']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
512 throw new \Exception( 'Missing Image' );
513 }
514
515 if ( empty( $data['payload']['settings'] ) ) {
516 throw new \Exception( 'Missing prompt settings' );
517 }
518
519 if ( ! $app->is_connected() ) {
520 throw new \Exception( 'not_connected' );
521 }
522
523 $context = $this->get_request_context( $data );
524 $request_ids = $this->get_request_ids( $data['payload'] );
525
526 $result = $app->get_image_to_image( [
527 'prompt' => $data['payload']['prompt'],
528 'promptSettings' => $data['payload']['settings'],
529 'attachment_id' => $data['payload']['image']['id'],
530 ], $context, $request_ids );
531
532 $this->throw_on_error( $result );
533
534 return [
535 'images' => $result['images'],
536 'response_id' => $result['responseId'],
537 'usage' => $result['usage'],
538 ];
539 }
540
541 public function ajax_ai_get_image_to_image_upscale( $data ) {
542 $this->verify_permissions( $data['editor_post_id'] );
543
544 $app = $this->get_ai_app();
545
546 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
547 throw new \Exception( 'Missing Image' );
548 }
549
550 if ( empty( $data['payload']['promptSettings'] ) ) {
551 throw new \Exception( 'Missing prompt settings' );
552 }
553
554 if ( ! $app->is_connected() ) {
555 throw new \Exception( 'not_connected' );
556 }
557
558 $context = $this->get_request_context( $data );
559 $request_ids = $this->get_request_ids( $data['payload'] );
560
561 $result = $app->get_image_to_image_upscale( [
562 'promptSettings' => $data['payload']['promptSettings'],
563 'attachment_id' => $data['payload']['image']['id'],
564 ], $context, $request_ids );
565
566 $this->throw_on_error( $result );
567
568 return [
569 'images' => $result['images'],
570 'response_id' => $result['responseId'],
571 'usage' => $result['usage'],
572 ];
573 }
574
575 public function ajax_ai_get_image_to_image_replace_background( $data ) {
576 $this->verify_permissions( $data['editor_post_id'] );
577
578 $app = $this->get_ai_app();
579
580 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
581 throw new \Exception( 'Missing Image' );
582 }
583
584 if ( empty( $data['payload']['prompt'] ) ) {
585 throw new \Exception( 'Prompt Missing' );
586 }
587
588 if ( ! $app->is_connected() ) {
589 throw new \Exception( 'not_connected' );
590 }
591
592 $context = $this->get_request_context( $data );
593 $request_ids = $this->get_request_ids( $data['payload'] );
594
595 $result = $app->get_image_to_image_replace_background( [
596 'attachment_id' => $data['payload']['image']['id'],
597 'prompt' => $data['payload']['prompt'],
598 ], $context, $request_ids );
599
600 $this->throw_on_error( $result );
601
602 return [
603 'images' => $result['images'],
604 'response_id' => $result['responseId'],
605 'usage' => $result['usage'],
606 ];
607 }
608
609 public function ajax_ai_get_image_to_image_remove_background( $data ) {
610 $this->verify_permissions( $data['editor_post_id'] );
611
612 $app = $this->get_ai_app();
613
614 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
615 throw new \Exception( 'Missing Image' );
616 }
617
618 if ( ! $app->is_connected() ) {
619 throw new \Exception( 'not_connected' );
620 }
621
622 $context = $this->get_request_context( $data );
623 $request_ids = $this->get_request_ids( $data['payload'] );
624 $result = $app->get_image_to_image_remove_background( [
625 'attachment_id' => $data['payload']['image']['id'],
626 ], $context, $request_ids );
627
628 $this->throw_on_error( $result );
629
630 return [
631 'images' => $result['images'],
632 'response_id' => $result['responseId'],
633 'usage' => $result['usage'],
634 ];
635 }
636
637 public function ajax_ai_get_image_to_image_mask( $data ) {
638 $this->verify_permissions( $data['editor_post_id'] );
639
640 $app = $this->get_ai_app();
641
642 if ( empty( $data['payload']['prompt'] ) ) {
643 throw new \Exception( 'Missing prompt' );
644 }
645
646 if ( empty( $data['payload']['image'] ) || empty( $data['payload']['image']['id'] ) ) {
647 throw new \Exception( 'Missing Image' );
648 }
649
650 if ( empty( $data['payload']['settings'] ) ) {
651 throw new \Exception( 'Missing prompt settings' );
652 }
653
654 if ( ! $app->is_connected() ) {
655 throw new \Exception( 'not_connected' );
656 }
657
658 if ( empty( $data['payload']['mask'] ) ) {
659 throw new \Exception( 'Missing Mask' );
660 }
661
662 $context = $this->get_request_context( $data );
663 $request_ids = $this->get_request_ids( $data['payload'] );
664
665 $result = $app->get_image_to_image_mask( [
666 'prompt' => $data['payload']['prompt'],
667 'attachment_id' => $data['payload']['image']['id'],
668 'mask' => $data['payload']['mask'],
669 'image_base64' => $data['payload']['image_base64'],
670 ], $context, $request_ids );
671
672 $this->throw_on_error( $result );
673
674 return [
675 'images' => $result['images'],
676 'response_id' => $result['responseId'],
677 'usage' => $result['usage'],
678 ];
679 }
680 public function ajax_ai_get_image_to_image_outpainting( $data ) {
681 $this->verify_permissions( $data['editor_post_id'] );
682
683 $app = $this->get_ai_app();
684
685 if ( ! $app->is_connected() ) {
686 throw new \Exception( 'not_connected' );
687 }
688
689 if ( empty( $data['payload']['mask'] ) ) {
690 throw new \Exception( 'Missing Expended Image' );
691 }
692
693 $context = $this->get_request_context( $data );
694 $request_ids = $this->get_request_ids( $data['payload'] );
695 $result = $app->get_image_to_image_out_painting( [
696 'size' => $data['payload']['size'],
697 'position' => $data['payload']['position'],
698 'mask' => $data['payload']['mask'],
699 'image_base64' => $data['payload']['image_base64'],
700 ], $context, $request_ids );
701
702 $this->throw_on_error( $result );
703
704 return [
705 'images' => $result['images'],
706 'response_id' => $result['responseId'],
707 'usage' => $result['usage'],
708 ];
709 }
710
711 public function ajax_ai_upload_image( $data ) {
712 if ( empty( $data['image'] ) ) {
713 throw new \Exception( 'Missing image data' );
714 }
715
716 $image = $data['image'];
717
718 if ( empty( $image['image_url'] ) ) {
719 throw new \Exception( 'Missing image_url' );
720 }
721
722 $image_data = $this->upload_image( $image['image_url'], $data['prompt'], $data['editor_post_id'] );
723
724 if ( is_wp_error( $image_data ) ) {
725 throw new \Exception( $image_data->get_error_message() );
726 }
727
728 if ( ! empty( $image['use_gallery_image'] ) && ! empty( $image['id'] ) ) {
729 $app = $this->get_ai_app();
730 $app->set_used_gallery_image( $image['id'] );
731 }
732
733 return [
734 'image' => array_merge( $image_data, $data ),
735 ];
736 }
737
738 public function ajax_ai_generate_layout( $data ) {
739 $this->verify_permissions( $data['editor_post_id'] );
740
741 $app = $this->get_ai_app();
742
743 if ( empty( $data['prompt'] ) && empty( $data['attachments'] ) ) {
744 throw new \Exception( 'Missing prompt / attachments' );
745 }
746
747 if ( ! $app->is_connected() ) {
748 throw new \Exception( 'not_connected' );
749 }
750
751 $result = $app->generate_layout(
752 $data,
753 $this->prepare_generate_layout_context( $data )
754 );
755
756 if ( is_wp_error( $result ) ) {
757 $message = $result->get_error_message();
758
759 if ( is_array( $message ) ) {
760 $message = implode( ', ', $message );
761 throw new \Exception( $message );
762 }
763
764 $this->throw_on_error( $result );
765 }
766
767 $elements = $result['text']['elements'] ?? [];
768 $base_template_id = $result['baseTemplateId'] ?? null;
769 $template_type = $result['templateType'] ?? null;
770
771 if ( empty( $elements ) || ! is_array( $elements ) ) {
772 throw new \Exception( 'unknown_error' );
773 }
774
775 if ( 1 === count( $elements ) ) {
776 $template = $elements[0];
777 } else {
778 $template = [
779 'elType' => 'container',
780 'elements' => $elements,
781 'settings' => [
782 'content_width' => 'full',
783 'flex_gap' => [
784 'column' => '0',
785 'row' => '0',
786 'unit' => 'px',
787 ],
788 'padding' => [
789 'unit' => 'px',
790 'top' => '0',
791 'right' => '0',
792 'bottom' => '0',
793 'left' => '0',
794 'isLinked' => true,
795 ],
796 ],
797 ];
798 }
799
800 return [
801 'all' => [],
802 'text' => $template,
803 'response_id' => $result['responseId'],
804 'usage' => $result['usage'],
805 'base_template_id' => $base_template_id,
806 'template_type' => $template_type,
807 ];
808 }
809
810 public function ajax_ai_get_layout_prompt_enhancer( $data ) {
811 $this->verify_permissions( $data['editor_post_id'] );
812
813 $app = $this->get_ai_app();
814
815 if ( empty( $data['prompt'] ) ) {
816 throw new \Exception( 'Missing prompt' );
817 }
818
819 if ( ! $app->is_connected() ) {
820 throw new \Exception( 'not_connected' );
821 }
822
823 $result = $app->get_layout_prompt_enhanced(
824 $data['prompt'],
825 $data['enhance_type'],
826 $this->prepare_generate_layout_context( $data )
827 );
828
829 $this->throw_on_error( $result );
830
831 return [
832 'text' => $result['text'] ?? $data['prompt'],
833 'response_id' => $result['responseId'] ?? '',
834 'usage' => $result['usage'] ?? '',
835 ];
836 }
837
838 private function prepare_generate_layout_context( $data ) {
839 $request_context = $this->get_request_context( $data );
840 $kit = Plugin::$instance->kits_manager->get_active_kit();
841
842 if ( ! $kit ) {
843 return $request_context;
844 }
845
846 $kits_data = Collection::make( $kit->get_data()['settings'] ?? [] );
847
848 $colors = $kits_data
849 ->filter( function ( $_, $key ) {
850 return in_array( $key, [ 'system_colors', 'custom_colors' ], true );
851 } )
852 ->flatten()
853 ->filter( function ( $val ) {
854 return ! empty( $val['_id'] );
855 } )
856 ->map( function ( $val ) {
857 return [
858 'id' => $val['_id'],
859 'label' => $val['title'] ?? null,
860 'value' => $val['color'] ?? null,
861 ];
862 } );
863
864 $typography = $kits_data
865 ->filter( function ( $_, $key ) {
866 return in_array( $key, [ 'system_typography', 'custom_typography' ], true );
867 } )
868 ->flatten()
869 ->filter( function ( $val ) {
870 return ! empty( $val['_id'] );
871 } )
872 ->map( function ( $val ) {
873 $font_size = null;
874
875 if ( isset(
876 $val['typography_font_size']['unit'],
877 $val['typography_font_size']['size']
878 ) ) {
879 $prop = $val['typography_font_size'];
880
881 $font_size = 'custom' === $prop['unit']
882 ? $prop['size']
883 : $prop['size'] . $prop['unit'];
884 }
885
886 return [
887 'id' => $val['_id'],
888 'label' => $val['title'] ?? null,
889 'value' => [
890 'family' => $val['typography_font_family'] ?? null,
891 'weight' => $val['typography_font_weight'] ?? null,
892 'style' => $val['typography_font_style'] ?? null,
893 'size' => $font_size,
894 ],
895 ];
896 } );
897
898 $request_context['globals'] = [
899 'colors' => $colors->all(),
900 'typography' => $typography->all(),
901 ];
902
903 return $request_context;
904 }
905
906 private function upload_image( $image_url, $image_title, $parent_post_id = 0 ) {
907 if ( ! current_user_can( 'upload_files' ) ) {
908 throw new \Exception( 'Not Allowed to Upload images' );
909 }
910
911 $attachment_id = media_sideload_image( $image_url, $parent_post_id, $image_title, 'id' );
912
913 if ( ! empty( $attachment_id['error'] ) ) {
914 return new \WP_Error( 'upload_error', $attachment_id['error'] );
915 }
916
917 return [
918 'id' => $attachment_id,
919 'url' => wp_get_attachment_image_url( $attachment_id, 'full' ),
920 'alt' => $image_title,
921 'source' => 'library',
922 ];
923 }
924
925 public function ajax_ai_get_history( $data ): array {
926 $type = $data['type'] ?? self::HISTORY_TYPE_ALL;
927
928 if ( ! in_array( $type, self::VALID_HISTORY_TYPES, true ) ) {
929 throw new \Exception( 'Invalid history type' );
930 }
931
932 $page = sanitize_text_field( $data['page'] ?? 1 );
933 $limit = sanitize_text_field( $data['limit'] ?? 10 );
934
935 $app = $this->get_ai_app();
936
937 if ( ! $app->is_connected() ) {
938 throw new \Exception( 'not_connected' );
939 }
940
941 $context = $this->get_request_context( $data );
942
943 $result = $app->get_history_by_type( $type, $page, $limit, $context );
944
945 if ( is_wp_error( $result ) ) {
946 throw new \Exception( $result->get_error_message() );
947 }
948
949 return $result;
950 }
951
952 public function ajax_ai_delete_history_item( $data ): array {
953 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
954 throw new \Exception( 'Missing id parameter' );
955 }
956
957 $app = $this->get_ai_app();
958
959 if ( ! $app->is_connected() ) {
960 throw new \Exception( 'not_connected' );
961 }
962
963 $context = $this->get_request_context( $data );
964
965 $result = $app->delete_history_item( $data['id'], $context );
966
967 if ( is_wp_error( $result ) ) {
968 throw new \Exception( $result->get_error_message() );
969 }
970
971 return [];
972 }
973
974 public function ajax_ai_toggle_favorite_history_item( $data ): array {
975 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
976 throw new \Exception( 'Missing id parameter' );
977 }
978
979 $app = $this->get_ai_app();
980
981 if ( ! $app->is_connected() ) {
982 throw new \Exception( 'not_connected' );
983 }
984
985 $context = $this->get_request_context( $data );
986
987 $result = $app->toggle_favorite_history_item( $data['id'], $context );
988
989 if ( is_wp_error( $result ) ) {
990 throw new \Exception( $result->get_error_message() );
991 }
992
993 return [];
994 }
995
996 /**
997 * @param mixed $result
998 */
999 private function throw_on_error( $result ): void {
1000 if ( is_wp_error( $result ) ) {
1001 wp_send_json_error( [
1002 'message' => $result->get_error_message(),
1003 'extra_data' => $result->get_error_data(),
1004 ] );
1005 }
1006 }
1007 }
1008