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

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