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 ) => (