diff --git a/.eslintignore b/.eslintignore
index fbba0c29c99..208b7a688ed 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,5 +1,5 @@
assets/lib/**/*.js
-assets/js/*.js
+assets/js/
**/*.min.js
**/node_modules/**
**/vendor/**
diff --git a/assets/dev/js/editor/controls/base-data.js b/assets/dev/js/editor/controls/base-data.js
index e0e805859e2..bf6edd5a4c8 100644
--- a/assets/dev/js/editor/controls/base-data.js
+++ b/assets/dev/js/editor/controls/base-data.js
@@ -197,7 +197,7 @@ ControlBaseDataView = ControlBaseView.extend( {
/**
* Get the responsive parent view if exists.
*
- * @return {ControlBaseDataView|null} responsive parent view if exists
+ * @return {ControlBaseDataView|undefined} responsive parent view if exists
*/
getResponsiveParentView() {
const parent = this.model.get( 'parent' );
diff --git a/assets/dev/js/editor/elements/views/base.js b/assets/dev/js/editor/elements/views/base.js
index 728c844ca6e..1b014c3f2dd 100644
--- a/assets/dev/js/editor/elements/views/base.js
+++ b/assets/dev/js/editor/elements/views/base.js
@@ -212,8 +212,8 @@ BaseElementView = BaseContainer.extend( {
*
* This filter allows adding new context menu groups to elements.
*
- * @param array customGroups - An array of group objects.
- * @param string elementType - The current element type.
+ * @param array customGroups - An array of group objects.
+ * @param string elementType - The current element type.
*/
customGroups = elementor.hooks.applyFilters( 'elements/context-menu/groups', customGroups, this.options.model.get( 'elType' ) );
@@ -296,7 +296,7 @@ BaseElementView = BaseContainer.extend( {
*
* @since 3.5.0
*
- * @param array editButtons An array of buttons.
+ * @param array editButtons An array of buttons.
*/
editButtons = elementor.hooks.applyFilters( `elements/edit-buttons`, editButtons );
@@ -309,7 +309,7 @@ BaseElementView = BaseContainer.extend( {
*
* @since 3.5.0
*
- * @param array editButtons An array of buttons.
+ * @param array editButtons An array of buttons.
*/
editButtons = elementor.hooks.applyFilters( `elements/edit-buttons/${ elementType }`, editButtons );
}
diff --git a/assets/dev/js/editor/utils/helpers.js b/assets/dev/js/editor/utils/helpers.js
index 64641958d58..bff5395601a 100644
--- a/assets/dev/js/editor/utils/helpers.js
+++ b/assets/dev/js/editor/utils/helpers.js
@@ -167,7 +167,7 @@ module.exports = {
* @param {*} attributes - default {} - attributes to attach to rendered html tag
* @param {string} tag - default i - html tag to render
* @param {*} returnType - default value - retrun type
- * @return {string|boolean|*} result
+ * @return {string|undefined|*} result
*/
renderIcon( view, icon, attributes = {}, tag = 'i', returnType = 'value' ) {
if ( ! icon || ! icon.library ) {
diff --git a/assets/dev/js/editor/utils/localized-value-store.js b/assets/dev/js/editor/utils/localized-value-store.js
index 63fbe2c86af..98049285169 100644
--- a/assets/dev/js/editor/utils/localized-value-store.js
+++ b/assets/dev/js/editor/utils/localized-value-store.js
@@ -8,7 +8,7 @@ export default class LocalizedValueStore {
* English values will be returned as is
* Paste will return an empty value
*
- * @param event - the incoming event
+ * @param event - the incoming event
* @return string
*/
diff --git a/core/common/modules/finder/categories/general.php b/core/common/modules/finder/categories/general.php
index 054a1c3fa87..0bd48df145a 100644
--- a/core/common/modules/finder/categories/general.php
+++ b/core/common/modules/finder/categories/general.php
@@ -46,7 +46,7 @@ public function get_id() {
public function get_category_items( array $options = [] ) {
return [
'saved-templates' => [
- 'title' => esc_html_x( 'Saved Templates', 'Template Library', 'elementor' ),
+ 'title' => esc_html__( 'Saved Templates', 'elementor' ),
'icon' => 'library-save',
'url' => Source_Local::get_admin_url(),
'keywords' => [ 'template', 'section', 'page', 'library' ],
diff --git a/includes/controls/groups/flex-container.php b/includes/controls/groups/flex-container.php
index 3b3a8e97ef2..02255fae037 100644
--- a/includes/controls/groups/flex-container.php
+++ b/includes/controls/groups/flex-container.php
@@ -94,15 +94,15 @@ protected function init_fields() {
'default' => '',
'options' => [
'flex-start' => [
- 'title' => esc_html_x( 'Start', 'Flex Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Start', 'elementor' ),
'icon' => 'eicon-flex eicon-justify-start-h',
],
'center' => [
- 'title' => esc_html_x( 'Center', 'Flex Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Center', 'elementor' ),
'icon' => 'eicon-flex eicon-justify-center-h',
],
'flex-end' => [
- 'title' => esc_html_x( 'End', 'Flex Container Control', 'elementor' ),
+ 'title' => esc_html__( 'End', 'elementor' ),
'icon' => 'eicon-flex eicon-justify-end-h',
],
'space-between' => [
@@ -130,15 +130,15 @@ protected function init_fields() {
'default' => '',
'options' => [
'flex-start' => [
- 'title' => esc_html_x( 'Start', 'Flex Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Start', 'elementor' ),
'icon' => 'eicon-flex eicon-align-start-v',
],
'center' => [
- 'title' => esc_html_x( 'Center', 'Flex Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Center', 'elementor' ),
'icon' => 'eicon-flex eicon-align-center-v',
],
'flex-end' => [
- 'title' => esc_html_x( 'End', 'Flex Container Control', 'elementor' ),
+ 'title' => esc_html__( 'End', 'elementor' ),
'icon' => 'eicon-flex eicon-align-end-v',
],
'stretch' => [
@@ -180,15 +180,15 @@ protected function init_fields() {
];
$fields['wrap'] = [
- 'label' => esc_html_x( 'Wrap', 'Flex Container Control', 'elementor' ),
+ 'label' => esc_html__( 'Wrap', 'elementor' ),
'type' => Controls_Manager::CHOOSE,
'options' => [
'nowrap' => [
- 'title' => esc_html_x( 'No Wrap', 'Flex Container Control', 'elementor' ),
+ 'title' => esc_html__( 'No Wrap', 'elementor' ),
'icon' => 'eicon-flex eicon-nowrap',
],
'wrap' => [
- 'title' => esc_html_x( 'Wrap', 'Flex Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Wrap', 'elementor' ),
'icon' => 'eicon-flex eicon-wrap',
],
],
diff --git a/includes/controls/groups/flex-item.php b/includes/controls/groups/flex-item.php
index b12a24c5200..ddb138a4912 100644
--- a/includes/controls/groups/flex-item.php
+++ b/includes/controls/groups/flex-item.php
@@ -61,15 +61,15 @@ protected function init_fields() {
'type' => Controls_Manager::CHOOSE,
'options' => [
'flex-start' => [
- 'title' => esc_html_x( 'Start', 'Flex Item Control', 'elementor' ),
+ 'title' => esc_html__( 'Start', 'elementor' ),
'icon' => 'eicon-flex eicon-align-start-v',
],
'center' => [
- 'title' => esc_html_x( 'Center', 'Flex Item Control', 'elementor' ),
+ 'title' => esc_html__( 'Center', 'elementor' ),
'icon' => 'eicon-flex eicon-align-center-v',
],
'flex-end' => [
- 'title' => esc_html_x( 'End', 'Flex Item Control', 'elementor' ),
+ 'title' => esc_html__( 'End', 'elementor' ),
'icon' => 'eicon-flex eicon-align-end-v',
],
'stretch' => [
@@ -91,11 +91,11 @@ protected function init_fields() {
'default' => '',
'options' => [
'start' => [
- 'title' => esc_html_x( 'Start', 'Flex Item Control', 'elementor' ),
+ 'title' => esc_html__( 'Start', 'elementor' ),
'icon' => 'eicon-flex eicon-order-start',
],
'end' => [
- 'title' => esc_html_x( 'End', 'Flex Item Control', 'elementor' ),
+ 'title' => esc_html__( 'End', 'elementor' ),
'icon' => 'eicon-flex eicon-order-end',
],
'custom' => [
diff --git a/includes/controls/groups/grid-container.php b/includes/controls/groups/grid-container.php
index 9024cc901e7..0e1719a5d0c 100644
--- a/includes/controls/groups/grid-container.php
+++ b/includes/controls/groups/grid-container.php
@@ -132,15 +132,15 @@ protected function init_fields() {
'type' => Controls_Manager::CHOOSE,
'options' => [
'start' => [
- 'title' => esc_html_x( 'Start', 'Grid Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Start', 'elementor' ),
'icon' => 'eicon-align-' . $icon_start . '-h',
],
'center' => [
- 'title' => esc_html_x( 'Center', 'Grid Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Center', 'elementor' ),
'icon' => 'eicon-align-center-h',
],
'end' => [
- 'title' => esc_html_x( 'End', 'Grid Container Control', 'elementor' ),
+ 'title' => esc_html__( 'End', 'elementor' ),
'icon' => 'eicon-align-' . $icon_end . '-h',
],
'stretch' => [
@@ -160,15 +160,15 @@ protected function init_fields() {
'type' => Controls_Manager::CHOOSE,
'options' => [
'start' => [
- 'title' => esc_html_x( 'Start', 'Grid Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Start', 'elementor' ),
'icon' => 'eicon-align-start-v',
],
'center' => [
- 'title' => esc_html_x( 'Center', 'Grid Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Center', 'elementor' ),
'icon' => 'eicon-align-center-v',
],
'end' => [
- 'title' => esc_html_x( 'End', 'Grid Container Control', 'elementor' ),
+ 'title' => esc_html__( 'End', 'elementor' ),
'icon' => 'eicon-align-end-v',
],
'stretch' => [
@@ -189,15 +189,15 @@ protected function init_fields() {
'default' => '',
'options' => [
'start' => [
- 'title' => esc_html_x( 'Start', 'Grid Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Start', 'elementor' ),
'icon' => 'eicon-justify-start-h',
],
'center' => [
- 'title' => esc_html_x( 'Middle', 'Grid Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Middle', 'elementor' ),
'icon' => 'eicon-justify-center-h',
],
'end' => [
- 'title' => esc_html_x( 'End', 'Grid Container Control', 'elementor' ),
+ 'title' => esc_html__( 'End', 'elementor' ),
'icon' => 'eicon-justify-end-h',
],
'space-between' => [
@@ -229,15 +229,15 @@ protected function init_fields() {
'default' => '',
'options' => [
'start' => [
- 'title' => esc_html_x( 'Start', 'Grid Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Start', 'elementor' ),
'icon' => 'eicon-justify-start-v',
],
'center' => [
- 'title' => esc_html_x( 'Middle', 'Grid Container Control', 'elementor' ),
+ 'title' => esc_html__( 'Middle', 'elementor' ),
'icon' => 'eicon-justify-center-v',
],
'end' => [
- 'title' => esc_html_x( 'End', 'Grid Container Control', 'elementor' ),
+ 'title' => esc_html__( 'End', 'elementor' ),
'icon' => 'eicon-justify-end-v',
],
'space-between' => [
diff --git a/includes/controls/groups/typography.php b/includes/controls/groups/typography.php
index 4c3ca03270a..50c74aff1d6 100644
--- a/includes/controls/groups/typography.php
+++ b/includes/controls/groups/typography.php
@@ -143,8 +143,8 @@ protected function init_fields() {
'800' => '800 ' . esc_html_x( '(Extra Bold)', 'Typography Control', 'elementor' ),
'900' => '900 ' . esc_html_x( '(Black)', 'Typography Control', 'elementor' ),
'' => esc_html__( 'Default', 'elementor' ),
- 'normal' => esc_html_x( 'Normal', 'Typography Control', 'elementor' ),
- 'bold' => esc_html_x( 'Bold', 'Typography Control', 'elementor' ),
+ 'normal' => esc_html__( 'Normal', 'elementor' ),
+ 'bold' => esc_html__( 'Bold', 'elementor' ),
],
];
@@ -157,7 +157,7 @@ protected function init_fields() {
'uppercase' => esc_html_x( 'Uppercase', 'Typography Control', 'elementor' ),
'lowercase' => esc_html_x( 'Lowercase', 'Typography Control', 'elementor' ),
'capitalize' => esc_html_x( 'Capitalize', 'Typography Control', 'elementor' ),
- 'none' => esc_html_x( 'Normal', 'Typography Control', 'elementor' ),
+ 'none' => esc_html__( 'Normal', 'elementor' ),
],
];
@@ -167,7 +167,7 @@ protected function init_fields() {
'default' => '',
'options' => [
'' => esc_html__( 'Default', 'elementor' ),
- 'normal' => esc_html_x( 'Normal', 'Typography Control', 'elementor' ),
+ 'normal' => esc_html__( 'Normal', 'elementor' ),
'italic' => esc_html_x( 'Italic', 'Typography Control', 'elementor' ),
'oblique' => esc_html_x( 'Oblique', 'Typography Control', 'elementor' ),
],
@@ -182,7 +182,7 @@ protected function init_fields() {
'underline' => esc_html_x( 'Underline', 'Typography Control', 'elementor' ),
'overline' => esc_html_x( 'Overline', 'Typography Control', 'elementor' ),
'line-through' => esc_html_x( 'Line Through', 'Typography Control', 'elementor' ),
- 'none' => esc_html_x( 'None', 'Typography Control', 'elementor' ),
+ 'none' => esc_html__( 'None', 'elementor' ),
],
];
diff --git a/includes/controls/select2.php b/includes/controls/select2.php
index b0f07648952..73b0b63db6f 100644
--- a/includes/controls/select2.php
+++ b/includes/controls/select2.php
@@ -81,7 +81,7 @@ public function content_template() {
var selected = ( -1 !== value.indexOf( option_value ) ) ? 'selected' : '';
}
#>
-
+
<# } ); #>
diff --git a/includes/elements/container.php b/includes/elements/container.php
index 802ea53f140..991245755ed 100644
--- a/includes/elements/container.php
+++ b/includes/elements/container.php
@@ -352,7 +352,7 @@ protected function get_flex_control_options( $is_container_grid_active ) {
'selector' => '{{WRAPPER}}',
'fields_options' => [
'gap' => [
- 'label' => esc_html_x( 'Gaps', 'Flex Container Control', 'elementor' ),
+ 'label' => esc_html__( 'Gaps', 'elementor' ),
'device_args' => [
Breakpoints_Manager::BREAKPOINT_KEY_DESKTOP => [
// Use the default gap from the kit as a placeholder.
diff --git a/includes/template-library/sources/local.php b/includes/template-library/sources/local.php
index 8882607e5f2..6c8438adc7a 100644
--- a/includes/template-library/sources/local.php
+++ b/includes/template-library/sources/local.php
@@ -233,16 +233,16 @@ public function register_data() {
$labels = [
'name' => $name,
'singular_name' => esc_html_x( 'Template', 'Template Library', 'elementor' ),
- 'add_new' => esc_html_x( 'Add New', 'Template Library', 'elementor' ),
- 'add_new_item' => esc_html_x( 'Add New Template', 'Template Library', 'elementor' ),
- 'edit_item' => esc_html_x( 'Edit Template', 'Template Library', 'elementor' ),
- 'new_item' => esc_html_x( 'New Template', 'Template Library', 'elementor' ),
- 'all_items' => esc_html_x( 'All Templates', 'Template Library', 'elementor' ),
- 'view_item' => esc_html_x( 'View Template', 'Template Library', 'elementor' ),
- 'search_items' => esc_html_x( 'Search Template', 'Template Library', 'elementor' ),
- 'not_found' => esc_html_x( 'No Templates found', 'Template Library', 'elementor' ),
- 'not_found_in_trash' => esc_html_x( 'No Templates found in Trash', 'Template Library', 'elementor' ),
- 'parent_item_colon' => '',
+ 'add_new' => esc_html__( 'Add New Template', 'elementor' ),
+ 'add_new_item' => esc_html__( 'Add New Template', 'elementor' ),
+ 'edit_item' => esc_html__( 'Edit Template', 'elementor' ),
+ 'new_item' => esc_html__( 'New Template', 'elementor' ),
+ 'all_items' => esc_html__( 'All Templates', 'elementor' ),
+ 'view_item' => esc_html__( 'View Template', 'elementor' ),
+ 'search_items' => esc_html__( 'Search Template', 'elementor' ),
+ 'not_found' => esc_html__( 'No Templates found', 'elementor' ),
+ 'not_found_in_trash' => esc_html__( 'No Templates found in Trash', 'elementor' ),
+ 'parent_item_colon' => esc_html__( 'Parent Template:', 'elementor' ),
'menu_name' => esc_html_x( 'Templates', 'Template Library', 'elementor' ),
];
diff --git a/modules/ai/assets/js/editor/ai-layout-behavior.js b/modules/ai/assets/js/editor/ai-layout-behavior.js
index 547c68f478a..878522a444a 100644
--- a/modules/ai/assets/js/editor/ai-layout-behavior.js
+++ b/modules/ai/assets/js/editor/ai-layout-behavior.js
@@ -26,6 +26,12 @@ export default class AiLayoutBehavior extends Marionette.Behavior {
renderLayoutApp( {
at: this.view.getOption( 'at' ),
onInsert: this.onInsert.bind( this ),
+ onRenderApp: ( args ) => {
+ args.previewContainer.init();
+ },
+ onGenerate: ( args ) => {
+ args.previewContainer.reset();
+ },
} );
}
diff --git a/modules/ai/assets/js/editor/api/index.js b/modules/ai/assets/js/editor/api/index.js
index 132403a0a76..cbc78e9071b 100644
--- a/modules/ai/assets/js/editor/api/index.js
+++ b/modules/ai/assets/js/editor/api/index.js
@@ -54,7 +54,20 @@ export const getImagePromptEnhanced = ( prompt ) => request( 'ai_get_image_promp
export const uploadImage = ( image ) => request( 'ai_upload_image', { ...image } );
-export const generateLayout = ( prompt, variationType, prevGeneratedIds, signal ) => request( 'ai_generate_layout', { prompt, variationType, prevGeneratedIds }, true, signal );
+/**
+ * @typedef {Object} AttachmentPropType - See ./types/attachment.js
+ * @typedef {Object} requestBody
+ * @property {string} prompt - Prompt to generate the layout from.
+ * @property {0|1|2} [variationType] - Type of the layout to generate (actually it's a position).
+ * @property {string[]} [prevGeneratedIds] - Previously generated ids for exclusion on regeneration.
+ * @property {AttachmentPropType[]} [attachments] - Attachments to use for the generation. currently only `json` type is supported - a container JSON to generate variations from.
+ */
+
+/**
+ * @param {requestBody} requestBody
+ * @param {AbortSignal} [signal]
+ */
+export const generateLayout = ( requestBody, signal ) => request( 'ai_generate_layout', requestBody, true, signal );
export const getLayoutPromptEnhanced = ( prompt ) => request( 'ai_get_layout_prompt_enhancer', { prompt } );
diff --git a/modules/ai/assets/js/editor/layout-app.js b/modules/ai/assets/js/editor/layout-app.js
index 6a17f8a39e8..a9332c25f85 100644
--- a/modules/ai/assets/js/editor/layout-app.js
+++ b/modules/ai/assets/js/editor/layout-app.js
@@ -1,10 +1,14 @@
import { ThemeProvider, DirectionProvider } from '@elementor/ui';
import PropTypes from 'prop-types';
import LayoutContent from './layout-content';
+import { AttachmentPropType, AttachmentsTypesPropType } from './types/attachment';
+import { ConfigProvider } from './pages/form-layout/context/config';
const LayoutApp = ( {
isRTL,
colorScheme,
+ attachmentsTypes,
+ attachments,
onClose,
onConnect,
onData,
@@ -15,14 +19,19 @@ const LayoutApp = ( {
return (
-
+ >
+
+
);
@@ -31,6 +40,8 @@ const LayoutApp = ( {
LayoutApp.propTypes = {
colorScheme: PropTypes.oneOf( [ 'auto', 'light', 'dark' ] ),
isRTL: PropTypes.bool,
+ attachmentsTypes: AttachmentsTypesPropType,
+ attachments: PropTypes.arrayOf( AttachmentPropType ),
onClose: PropTypes.func.isRequired,
onConnect: PropTypes.func.isRequired,
onData: PropTypes.func.isRequired,
diff --git a/modules/ai/assets/js/editor/layout-content.js b/modules/ai/assets/js/editor/layout-content.js
index bb0d6c96bce..12294c04496 100644
--- a/modules/ai/assets/js/editor/layout-content.js
+++ b/modules/ai/assets/js/editor/layout-content.js
@@ -10,10 +10,13 @@ import { Alert } from '@elementor/ui';
import { __ } from '@wordpress/i18n';
import PropTypes from 'prop-types';
import useIntroduction from './hooks/use-introduction';
+import { AttachmentPropType } from './types/attachment';
+import { useConfig } from './pages/form-layout/context/config';
-const LayoutContent = ( { onClose, onConnect, onData, onInsert, onSelect, onGenerate } ) => {
+const LayoutContent = ( props ) => {
const { isLoading, isConnected, isGetStarted, connectUrl, fetchData, hasSubscription, credits, usagePercentage } = useUserInfo();
const { isViewed, markAsViewed } = useIntroduction( 'e-ai-builder-coming-soon-info' );
+ const { onClose, onConnect } = useConfig();
if ( isLoading ) {
return (
@@ -61,12 +64,7 @@ const LayoutContent = ( { onClose, onConnect, onData, onInsert, onSelect, onGene
return (
,
} }
@@ -85,12 +83,7 @@ const LayoutContent = ( { onClose, onConnect, onData, onInsert, onSelect, onGene
};
LayoutContent.propTypes = {
- onClose: PropTypes.func.isRequired,
- onConnect: PropTypes.func.isRequired,
- onData: PropTypes.func.isRequired,
- onInsert: PropTypes.func.isRequired,
- onSelect: PropTypes.func.isRequired,
- onGenerate: PropTypes.func.isRequired,
+ attachments: PropTypes.arrayOf( AttachmentPropType ),
};
export default LayoutContent;
diff --git a/modules/ai/assets/js/editor/layout-module.js b/modules/ai/assets/js/editor/layout-module.js
index 1bf00158668..7d586bdf424 100644
--- a/modules/ai/assets/js/editor/layout-module.js
+++ b/modules/ai/assets/js/editor/layout-module.js
@@ -1,8 +1,12 @@
import AiLayoutBehavior from './ai-layout-behavior';
+import { importToEditor, renderLayoutApp } from './utils/editor-integration';
+import { __ } from '@wordpress/i18n';
export default class Module extends elementorModules.editor.utils.Module {
onElementorInit() {
elementor.hooks.addFilter( 'views/add-section/behaviors', this.registerAiLayoutBehavior );
+
+ elementor.hooks.addFilter( 'elements/container/contextMenuGroups', this.registerVariationsContextMenu );
}
registerAiLayoutBehavior( behaviors ) {
@@ -12,6 +16,52 @@ export default class Module extends elementorModules.editor.utils.Module {
return behaviors;
}
+
+ registerVariationsContextMenu = ( groups, currentElement ) => {
+ const saveGroup = groups.find( ( group ) => 'save' === group.name );
+
+ if ( ! saveGroup ) {
+ return groups;
+ }
+
+ // Add on top of save group actions
+ saveGroup.actions.unshift( {
+ name: 'ai',
+ icon: 'eicon-ai',
+ title: __( 'Generate AI Variations', 'elementor' ),
+ callback: async () => {
+ const container = currentElement.getContainer();
+ const json = container.model.toJSON( { remove: [ 'default' ] } );
+ const attachments = [ {
+ type: 'json',
+ previewHTML: '',
+ content: json,
+ label: container.model.get( 'title' ),
+ } ];
+
+ renderLayoutApp( {
+ at: container.view._index,
+ attachments,
+ onSelect: () => {
+ container.view.$el.hide();
+ },
+ onClose: () => {
+ container.view.$el.show();
+ },
+ onInsert: ( template ) => {
+ importToEditor( {
+ at: container.view._index,
+ template,
+ historyTitle: __( 'AI Variation', 'elementor' ),
+ replace: true,
+ } );
+ },
+ } );
+ },
+ } );
+
+ return groups;
+ };
}
new Module();
diff --git a/modules/ai/assets/js/editor/pages/form-layout/components/attachments.js b/modules/ai/assets/js/editor/pages/form-layout/components/attachments.js
new file mode 100644
index 00000000000..86631cf4a25
--- /dev/null
+++ b/modules/ai/assets/js/editor/pages/form-layout/components/attachments.js
@@ -0,0 +1,23 @@
+import AttachmentJson from './attachments/attachment-json';
+import PropTypes from 'prop-types';
+import { AttachmentPropType } from '../../../types/attachment';
+
+const ATTACHMENT_TYPE_JSON = 'json';
+
+const Attachments = ( props ) => {
+ const type = props.attachments[ 0 ]?.type;
+
+ switch ( type ) {
+ case ATTACHMENT_TYPE_JSON:
+ return ;
+ }
+
+ return null;
+};
+
+Attachments.propTypes = {
+ attachments: PropTypes.arrayOf( AttachmentPropType ).isRequired,
+ disabled: PropTypes.bool,
+};
+
+export default Attachments;
diff --git a/modules/ai/assets/js/editor/pages/form-layout/components/attachments/attachment-json.js b/modules/ai/assets/js/editor/pages/form-layout/components/attachments/attachment-json.js
new file mode 100644
index 00000000000..e9ec6fb3b82
--- /dev/null
+++ b/modules/ai/assets/js/editor/pages/form-layout/components/attachments/attachment-json.js
@@ -0,0 +1,37 @@
+import { Thumbnail } from './thumbnail';
+import PropTypes from 'prop-types';
+import { Skeleton } from '@elementor/ui';
+import { AttachmentPropType } from '../../../../types/attachment';
+
+export const AttachmentJson = ( props ) => {
+ const attachment = props.attachments?.find( ( item ) => 'json' === item.type );
+
+ if ( ! attachment ) {
+ return null;
+ }
+
+ if ( ! attachment.previewHTML ) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+AttachmentJson.propTypes = {
+ attachments: PropTypes.arrayOf( AttachmentPropType ).isRequired,
+ disabled: PropTypes.bool,
+};
+
+export default AttachmentJson;
diff --git a/modules/ai/assets/js/editor/pages/form-layout/components/attachments/thumbnail.js b/modules/ai/assets/js/editor/pages/form-layout/components/attachments/thumbnail.js
new file mode 100644
index 00000000000..31f056150e8
--- /dev/null
+++ b/modules/ai/assets/js/editor/pages/form-layout/components/attachments/thumbnail.js
@@ -0,0 +1,64 @@
+import { useEffect, useRef } from 'react';
+import { Box } from '@elementor/ui';
+import PropTypes from 'prop-types';
+
+const THUMBNAIL_SIZE = 64;
+
+export const Thumbnail = ( props ) => {
+ const previewRef = useRef( null );
+
+ useEffect( () => {
+ if ( previewRef.current ) {
+ const previewRoot = previewRef.current.firstElementChild;
+ const width = previewRoot?.offsetWidth || THUMBNAIL_SIZE;
+ const height = previewRoot?.offsetHeight || THUMBNAIL_SIZE;
+
+ const scaleFactor = Math.max( height, width );
+ const scale = THUMBNAIL_SIZE / scaleFactor;
+
+ previewRef.current.style.transform = `scale(${ scale })`;
+ }
+ }, [] );
+
+ return (
+
+
+
+ );
+};
+
+Thumbnail.propTypes = {
+ html: PropTypes.string.isRequired,
+ disabled: PropTypes.bool,
+};
diff --git a/modules/ai/assets/js/editor/pages/form-layout/components/prompt-form.js b/modules/ai/assets/js/editor/pages/form-layout/components/prompt-form.js
index 4b828623a77..5c5fc30555b 100644
--- a/modules/ai/assets/js/editor/pages/form-layout/components/prompt-form.js
+++ b/modules/ai/assets/js/editor/pages/form-layout/components/prompt-form.js
@@ -8,6 +8,9 @@ import GenerateSubmit from '../../form-media/components/generate-submit';
import ArrowLeftIcon from '../../../icons/arrow-left-icon';
import EditIcon from '../../../icons/edit-icon';
import usePromptEnhancer from '../../../hooks/use-prompt-enhancer';
+import Attachments from './attachments';
+import { useConfig } from '../context/config';
+import { AttachmentPropType } from '../../../types/attachment';
const PROMPT_SUGGESTIONS = Object.freeze( [
{ text: __( 'A services section with a list layout, icons, and corresponding service descriptions for', 'elementor' ) },
@@ -63,12 +66,25 @@ const GenerateButton = ( props ) => (
);
-const PromptForm = forwardRef( ( { isActive, isLoading, showActions = false, onSubmit, onBack, onEdit }, ref ) => {
+const PromptForm = forwardRef( ( {
+ attachments,
+ isActive,
+ isLoading,
+ showActions = false,
+ onSubmit,
+ onBack,
+ onEdit,
+}, ref ) => {
const [ prompt, setPrompt ] = useState( '' );
const { isEnhancing, enhance } = usePromptEnhancer( prompt, 'layout' );
const previousPrompt = useRef( '' );
+ const { attachmentsTypes } = useConfig();
- const isInteractionsDisabled = isEnhancing || isLoading || ! isActive || '' === prompt;
+ const isInteractionsDisabled = isEnhancing || isLoading || ! isActive || ( '' === prompt && ! attachments.length );
+
+ const attachmentsType = attachments[ 0 ]?.type || '';
+ const attachmentsConfig = attachmentsTypes[ attachmentsType ];
+ const promptSuggestions = attachmentsConfig?.promptSuggestions || PROMPT_SUGGESTIONS;
const handleBack = () => {
setPrompt( previousPrompt.current );
@@ -81,15 +97,15 @@ const PromptForm = forwardRef( ( { isActive, isLoading, showActions = false, onS
};
return (
- onSubmit( e, prompt ) }
+ direction="row"
sx={ { p: 2 } }
- display="flex"
alignItems="center"
gap={ 1 }
>
-
+
{
showActions && (
isActive ? (
@@ -100,11 +116,16 @@ const PromptForm = forwardRef( ( { isActive, isLoading, showActions = false, onS
)
}
+
+
onSubmit( e, prompt ) }
- options={ PROMPT_SUGGESTIONS }
+ options={ promptSuggestions }
getOptionLabel={ ( option ) => option.text ? option.text + '...' : prompt }
onChange={ ( _, selectedValue ) => setPrompt( selectedValue.text + ' ' ) }
renderInput={ ( params ) => (
@@ -126,7 +147,7 @@ const PromptForm = forwardRef( ( { isActive, isLoading, showActions = false, onS
/>
-
+
);
} );
@@ -137,6 +158,7 @@ PromptForm.propTypes = {
onSubmit: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
+ attachments: PropTypes.arrayOf( AttachmentPropType ),
};
export default PromptForm;
diff --git a/modules/ai/assets/js/editor/pages/form-layout/context/config.js b/modules/ai/assets/js/editor/pages/form-layout/context/config.js
new file mode 100644
index 00000000000..9853106b63e
--- /dev/null
+++ b/modules/ai/assets/js/editor/pages/form-layout/context/config.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const ConfigContext = React.createContext( {} );
+
+export const useConfig = () => React.useContext( ConfigContext );
+
+export const ConfigProvider = ( props ) => {
+ return (
+
+ { props.children }
+
+ );
+};
+
+ConfigProvider.propTypes = {
+ children: PropTypes.node.isRequired,
+ attachmentsTypes: PropTypes.object.isRequired,
+ onClose: PropTypes.func.isRequired,
+ onConnect: PropTypes.func.isRequired,
+ onData: PropTypes.func.isRequired,
+ onInsert: PropTypes.func.isRequired,
+ onSelect: PropTypes.func.isRequired,
+ onGenerate: PropTypes.func.isRequired,
+};
+
+export default ConfigContext;
diff --git a/modules/ai/assets/js/editor/pages/form-layout/hooks/use-layout-prompt.js b/modules/ai/assets/js/editor/pages/form-layout/hooks/use-layout-prompt.js
index 365c97f1b00..d38ff124cd0 100644
--- a/modules/ai/assets/js/editor/pages/form-layout/hooks/use-layout-prompt.js
+++ b/modules/ai/assets/js/editor/pages/form-layout/hooks/use-layout-prompt.js
@@ -2,7 +2,11 @@ import { generateLayout } from '../../../api';
import usePrompt from '../../../hooks/use-prompt';
const useLayoutPrompt = ( type, initialValue ) => {
- return usePrompt( ( prompt, prevGeneratedIds, signal ) => generateLayout( prompt, type, prevGeneratedIds, signal ), initialValue );
+ return usePrompt( ( requestBody, signal ) => {
+ requestBody.variationType = type;
+
+ return generateLayout( requestBody, signal );
+ }, initialValue );
};
export default useLayoutPrompt;
diff --git a/modules/ai/assets/js/editor/pages/form-layout/hooks/use-screenshot.js b/modules/ai/assets/js/editor/pages/form-layout/hooks/use-screenshot.js
index 7f78334b36e..438e2e0fdeb 100644
--- a/modules/ai/assets/js/editor/pages/form-layout/hooks/use-screenshot.js
+++ b/modules/ai/assets/js/editor/pages/form-layout/hooks/use-screenshot.js
@@ -9,11 +9,11 @@ const useScreenshot = ( type, onData ) => {
const layoutData = useLayoutPrompt( type, null );
- const generate = ( prompt, prevGeneratedIds, signal ) => {
+ const generate = ( requestBody, signal ) => {
setIsLoading( true );
setError( ERROR_INITIAL_VALUE );
- return layoutData.send( prompt, prevGeneratedIds, signal )
+ return layoutData.send( requestBody, signal )
.then( async ( data ) => {
const createdScreenshot = await onData( data.result );
diff --git a/modules/ai/assets/js/editor/pages/form-layout/hooks/use-screenshots.js b/modules/ai/assets/js/editor/pages/form-layout/hooks/use-screenshots.js
index 0a9f497151e..9fc78b0dbf7 100644
--- a/modules/ai/assets/js/editor/pages/form-layout/hooks/use-screenshots.js
+++ b/modules/ai/assets/js/editor/pages/form-layout/hooks/use-screenshots.js
@@ -21,7 +21,7 @@ const useScreenshots = ( { onData } ) => {
const abort = () => abortController.current?.abort();
- const createScreenshots = async ( prompt ) => {
+ const createScreenshots = async ( prompt, attachments ) => {
abortController.current = new AbortController();
const onGenerate = ( screenshot ) => {
@@ -53,7 +53,20 @@ const useScreenshots = ( { onData } ) => {
const promises = screenshotsData.map( ( { generate } ) => {
const prevGeneratedIds = screenshots.map( ( screenshot ) => screenshot.baseTemplateId );
- return generate( prompt, prevGeneratedIds, abortController.current.signal )
+ const requestBody = {
+ prompt,
+ prevGeneratedIds,
+ attachments: attachments.map( ( { type, content, label } ) => {
+ // Send only the data that is needed for the generation.
+ return {
+ type,
+ content,
+ label,
+ };
+ } ),
+ };
+
+ return generate( requestBody, abortController.current.signal )
.then( onGenerate )
.catch( onError );
} );
@@ -72,20 +85,20 @@ const useScreenshots = ( { onData } ) => {
}
};
- const generate = ( prompt ) => {
+ const generate = ( prompt, attachments ) => {
const placeholders = Array( screenshotsGroupCount ).fill( PENDING_VALUE );
setScreenshots( placeholders );
- createScreenshots( prompt );
+ createScreenshots( prompt, attachments );
};
- const regenerate = ( prompt ) => {
+ const regenerate = ( prompt, attachments ) => {
const placeholders = Array( screenshotsGroupCount ).fill( PENDING_VALUE );
setScreenshots( ( prev ) => [ ...prev, ...placeholders ] );
- createScreenshots( prompt );
+ createScreenshots( prompt, attachments );
};
return {
diff --git a/modules/ai/assets/js/editor/pages/form-layout/index.js b/modules/ai/assets/js/editor/pages/form-layout/index.js
index b978963833c..7201407ef34 100644
--- a/modules/ai/assets/js/editor/pages/form-layout/index.js
+++ b/modules/ai/assets/js/editor/pages/form-layout/index.js
@@ -1,7 +1,7 @@
-import { useState, useRef, useEffect } from 'react';
+import { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { __ } from '@wordpress/i18n';
-import { Box, Divider, Button, Pagination, IconButton, Collapse, Tooltip, withDirection } from '@elementor/ui';
+import { Box, Button, Collapse, Divider, IconButton, Pagination, Tooltip, withDirection } from '@elementor/ui';
import PromptErrorMessage from '../../components/prompt-error-message';
import UnsavedChangesAlert from './components/unsaved-changes-alert';
import LayoutDialog from './components/layout-dialog';
@@ -12,10 +12,20 @@ import useScreenshots from './hooks/use-screenshots';
import useSlider from './hooks/use-slider';
import MinimizeDiagonalIcon from '../../icons/minimize-diagonal-icon';
import ExpandDiagonalIcon from '../../icons/expand-diagonal-icon';
+import { useConfig } from './context/config';
+import { AttachmentPropType } from '../../types/attachment';
const DirectionalMinimizeDiagonalIcon = withDirection( MinimizeDiagonalIcon );
const DirectionalExpandDiagonalIcon = withDirection( ExpandDiagonalIcon );
+/**
+ * @typedef {Object} Attachment
+ * @property {('json')} type - The type of the attachment, currently only `json` is supported.
+ * @property {string} previewHTML - HTML content as a string, representing a preview.
+ * @property {string} content - Actual content of the attachment as a string.
+ * @property {string} label - Label for the attachment.
+ */
+
const RegenerateButton = ( props ) => (