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

1,010 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 'promptSettings' => $data['payload']['settings'],
668 'attachment_id' => $data['payload']['image']['id'],
669 'mask' => $data['payload']['mask'],
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 ( empty( $data['payload']['prompt'] ) ) {
686 throw new \Exception( 'Missing prompt' );
687 }
688
689 if ( ! $app->is_connected() ) {
690 throw new \Exception( 'not_connected' );
691 }
692
693 if ( empty( $data['payload']['mask'] ) ) {
694 throw new \Exception( 'Missing Expended Image' );
695 }
696
697 $context = $this->get_request_context( $data );
698 $request_ids = $this->get_request_ids( $data['payload'] );
699 $result = $app->get_image_to_image_out_painting( [
700 'prompt' => $data['payload']['prompt'],
701 'mask' => $data['payload']['mask'],
702 ], $context, $request_ids );
703
704 $this->throw_on_error( $result );
705
706 return [
707 'images' => $result['images'],
708 'response_id' => $result['responseId'],
709 'usage' => $result['usage'],
710 ];
711 }
712
713 public function ajax_ai_upload_image( $data ) {
714 if ( empty( $data['image'] ) ) {
715 throw new \Exception( 'Missing image data' );
716 }
717
718 $image = $data['image'];
719
720 if ( empty( $image['image_url'] ) ) {
721 throw new \Exception( 'Missing image_url' );
722 }
723
724 $image_data = $this->upload_image( $image['image_url'], $data['prompt'], $data['editor_post_id'] );
725
726 if ( is_wp_error( $image_data ) ) {
727 throw new \Exception( $image_data->get_error_message() );
728 }
729
730 if ( ! empty( $image['use_gallery_image'] ) && ! empty( $image['id'] ) ) {
731 $app = $this->get_ai_app();
732 $app->set_used_gallery_image( $image['id'] );
733 }
734
735 return [
736 'image' => array_merge( $image_data, $data ),
737 ];
738 }
739
740 public function ajax_ai_generate_layout( $data ) {
741 $this->verify_permissions( $data['editor_post_id'] );
742
743 $app = $this->get_ai_app();
744
745 if ( empty( $data['prompt'] ) && empty( $data['attachments'] ) ) {
746 throw new \Exception( 'Missing prompt / attachments' );
747 }
748
749 if ( ! $app->is_connected() ) {
750 throw new \Exception( 'not_connected' );
751 }
752
753 $result = $app->generate_layout(
754 $data,
755 $this->prepare_generate_layout_context( $data )
756 );
757
758 if ( is_wp_error( $result ) ) {
759 $message = $result->get_error_message();
760
761 if ( is_array( $message ) ) {
762 $message = implode( ', ', $message );
763 throw new \Exception( $message );
764 }
765
766 $this->throw_on_error( $result );
767 }
768
769 $elements = $result['text']['elements'] ?? [];
770 $base_template_id = $result['baseTemplateId'] ?? null;
771 $template_type = $result['templateType'] ?? null;
772
773 if ( empty( $elements ) || ! is_array( $elements ) ) {
774 throw new \Exception( 'unknown_error' );
775 }
776
777 if ( 1 === count( $elements ) ) {
778 $template = $elements[0];
779 } else {
780 $template = [
781 'elType' => 'container',
782 'elements' => $elements,
783 'settings' => [
784 'content_width' => 'full',
785 'flex_gap' => [
786 'column' => '0',
787 'row' => '0',
788 'unit' => 'px',
789 ],
790 'padding' => [
791 'unit' => 'px',
792 'top' => '0',
793 'right' => '0',
794 'bottom' => '0',
795 'left' => '0',
796 'isLinked' => true,
797 ],
798 ],
799 ];
800 }
801
802 return [
803 'all' => [],
804 'text' => $template,
805 'response_id' => $result['responseId'],
806 'usage' => $result['usage'],
807 'base_template_id' => $base_template_id,
808 'template_type' => $template_type,
809 ];
810 }
811
812 public function ajax_ai_get_layout_prompt_enhancer( $data ) {
813 $this->verify_permissions( $data['editor_post_id'] );
814
815 $app = $this->get_ai_app();
816
817 if ( empty( $data['prompt'] ) ) {
818 throw new \Exception( 'Missing prompt' );
819 }
820
821 if ( ! $app->is_connected() ) {
822 throw new \Exception( 'not_connected' );
823 }
824
825 $result = $app->get_layout_prompt_enhanced(
826 $data['prompt'],
827 $data['enhance_type'],
828 $this->prepare_generate_layout_context( $data )
829 );
830
831 $this->throw_on_error( $result );
832
833 return [
834 'text' => $result['text'] ?? $data['prompt'],
835 'response_id' => $result['responseId'] ?? '',
836 'usage' => $result['usage'] ?? '',
837 ];
838 }
839
840 private function prepare_generate_layout_context( $data ) {
841 $request_context = $this->get_request_context( $data );
842 $kit = Plugin::$instance->kits_manager->get_active_kit();
843
844 if ( ! $kit ) {
845 return $request_context;
846 }
847
848 $kits_data = Collection::make( $kit->get_data()['settings'] ?? [] );
849
850 $colors = $kits_data
851 ->filter( function ( $_, $key ) {
852 return in_array( $key, [ 'system_colors', 'custom_colors' ], true );
853 } )
854 ->flatten()
855 ->filter( function ( $val ) {
856 return ! empty( $val['_id'] );
857 } )
858 ->map( function ( $val ) {
859 return [
860 'id' => $val['_id'],
861 'label' => $val['title'] ?? null,
862 'value' => $val['color'] ?? null,
863 ];
864 } );
865
866 $typography = $kits_data
867 ->filter( function ( $_, $key ) {
868 return in_array( $key, [ 'system_typography', 'custom_typography' ], true );
869 } )
870 ->flatten()
871 ->filter( function ( $val ) {
872 return ! empty( $val['_id'] );
873 } )
874 ->map( function ( $val ) {
875 $font_size = null;
876
877 if ( isset(
878 $val['typography_font_size']['unit'],
879 $val['typography_font_size']['size']
880 ) ) {
881 $prop = $val['typography_font_size'];
882
883 $font_size = 'custom' === $prop['unit']
884 ? $prop['size']
885 : $prop['size'] . $prop['unit'];
886 }
887
888 return [
889 'id' => $val['_id'],
890 'label' => $val['title'] ?? null,
891 'value' => [
892 'family' => $val['typography_font_family'] ?? null,
893 'weight' => $val['typography_font_weight'] ?? null,
894 'style' => $val['typography_font_style'] ?? null,
895 'size' => $font_size,
896 ],
897 ];
898 } );
899
900 $request_context['globals'] = [
901 'colors' => $colors->all(),
902 'typography' => $typography->all(),
903 ];
904
905 return $request_context;
906 }
907
908 private function upload_image( $image_url, $image_title, $parent_post_id = 0 ) {
909 if ( ! current_user_can( 'upload_files' ) ) {
910 throw new \Exception( 'Not Allowed to Upload images' );
911 }
912
913 $attachment_id = media_sideload_image( $image_url, $parent_post_id, $image_title, 'id' );
914
915 if ( ! empty( $attachment_id['error'] ) ) {
916 return new \WP_Error( 'upload_error', $attachment_id['error'] );
917 }
918
919 return [
920 'id' => $attachment_id,
921 'url' => wp_get_attachment_image_url( $attachment_id, 'full' ),
922 'alt' => $image_title,
923 'source' => 'library',
924 ];
925 }
926
927 public function ajax_ai_get_history( $data ): array {
928 $type = $data['type'] ?? self::HISTORY_TYPE_ALL;
929
930 if ( ! in_array( $type, self::VALID_HISTORY_TYPES, true ) ) {
931 throw new \Exception( 'Invalid history type' );
932 }
933
934 $page = sanitize_text_field( $data['page'] ?? 1 );
935 $limit = sanitize_text_field( $data['limit'] ?? 10 );
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->get_history_by_type( $type, $page, $limit, $context );
946
947 if ( is_wp_error( $result ) ) {
948 throw new \Exception( $result->get_error_message() );
949 }
950
951 return $result;
952 }
953
954 public function ajax_ai_delete_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->delete_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 public function ajax_ai_toggle_favorite_history_item( $data ): array {
977 if ( empty( $data['id'] ) || ! wp_is_uuid( $data['id'] ) ) {
978 throw new \Exception( 'Missing id parameter' );
979 }
980
981 $app = $this->get_ai_app();
982
983 if ( ! $app->is_connected() ) {
984 throw new \Exception( 'not_connected' );
985 }
986
987 $context = $this->get_request_context( $data );
988
989 $result = $app->toggle_favorite_history_item( $data['id'], $context );
990
991 if ( is_wp_error( $result ) ) {
992 throw new \Exception( $result->get_error_message() );
993 }
994
995 return [];
996 }
997
998 /**
999 * @param mixed $result
1000 */
1001 private function throw_on_error( $result ): void {
1002 if ( is_wp_error( $result ) ) {
1003 wp_send_json_error( [
1004 'message' => $result->get_error_message(),
1005 'extra_data' => $result->get_error_data(),
1006 ] );
1007 }
1008 }
1009 }
1010