diff --git a/projects/plugins/jetpack/_inc/lib/class-jetpack-ai-helper.php b/projects/plugins/jetpack/_inc/lib/class-jetpack-ai-helper.php new file mode 100644 index 000000000000..4b3dd796ed71 --- /dev/null +++ b/projects/plugins/jetpack/_inc/lib/class-jetpack-ai-helper.php @@ -0,0 +1,208 @@ + rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Get the name of the transient for image generation. Unique per prompt and allows for reuse of results for the same prompt across entire WPCOM. + * I expext "puppy" to always be from cache. + * + * @param string $prompt - Supplied prompt. + */ + public static function transient_name_for_image_generation( $prompt ) { + return 'jetpack_openai_image_' . md5( $prompt ); + } + + /** + * Get the name of the transient for text completion. Unique per user, but not per text. Serves more as a cooldown. + */ + public static function transient_name_for_completion() { + return 'jetpack_openai_completion_' . get_current_user_id(); // Cache for each user, so that other users dont get weird cached version from somebody else. + } + + /** + * Get text back from WordPress.com based off a starting text. + * + * @param string $content The content that's already been typed in the block. + * @return mixed + */ + public static function get_gpt_completion( $content ) { + $content = wp_strip_all_tags( $content ); + $cache = get_transient( self::transient_name_for_completion() ); + if ( $cache ) { + return $cache; + } + + if ( ( new Status() )->is_offline_mode() ) { + return new WP_Error( + 'dev_mode', + __( 'Jetpack AI is not available in offline mode.', 'jetpack' ) + ); + } + + $site_id = Manager::get_site_id(); + if ( is_wp_error( $site_id ) ) { + return $site_id; + } + + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { + if ( ! class_exists( 'OpenAI' ) ) { + \require_lib( 'openai' ); + } + + $result = ( new OpenAI( 'openai' ) )->request_gpt_completion( $content ); + if ( is_wp_error( $result ) ) { + return $result; + } + // In case of Jetpack we are setting a transient on the WPCOM and not the remote site. I think the 'get_current_user_id' may default for the connection owner at this point but we'll deal with this later. + set_transient( self::transient_name_for_completion(), $result, self::$text_completion_cooldown_seconds ); + return $result; + } + + $response = Client::wpcom_json_api_request_as_user( + sprintf( '/sites/%d/jetpack-ai/completions', $site_id ), + 2, + array( + 'method' => 'post', + 'headers' => array( 'content-type' => 'application/json' ), + ), + wp_json_encode( + array( + 'content' => $content, + ) + ), + 'wpcom' + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $data = json_decode( wp_remote_retrieve_body( $response ) ); + + if ( wp_remote_retrieve_response_code( $response ) >= 400 ) { + return new WP_Error( $data->code, $data->message, $data->data ); + } + set_transient( self::transient_name_for_completion(), $data, self::$text_completion_cooldown_seconds ); + + return $data; + } + + /** + * Get an array of image objects back from WordPress.com based off a prompt. + * + * @param string $prompt The prompt to generate images for. + * @return mixed + */ + public static function get_dalle_generation( $prompt ) { + $cache = get_transient( self::transient_name_for_image_generation( $prompt ) ); + if ( $cache ) { + return $cache; + } + + if ( ( new Status() )->is_offline_mode() ) { + return new WP_Error( + 'dev_mode', + __( 'Jetpack AI is not available in offline mode.', 'jetpack' ) + ); + } + + $site_id = Manager::get_site_id(); + if ( is_wp_error( $site_id ) ) { + return $site_id; + } + + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { + if ( ! class_exists( 'OpenAI' ) ) { + \require_lib( 'openai' ); + } + + $result = ( new OpenAI( 'openai' ) )->request_dalle_generation( $prompt ); + if ( is_wp_error( $result ) ) { + return $result; + } + set_transient( self::transient_name_for_image_generation( $prompt ), $result, self::$image_generation_cache_timeout ); + return $result; + } + + $response = Client::wpcom_json_api_request_as_user( + sprintf( '/sites/%d/jetpack-ai/images/generations', $site_id ), + 2, + array( + 'method' => 'post', + 'headers' => array( 'content-type' => 'application/json' ), + ), + wp_json_encode( + array( + 'prompt' => $prompt, + ) + ), + 'wpcom' + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $data = json_decode( wp_remote_retrieve_body( $response ) ); + + if ( wp_remote_retrieve_response_code( $response ) >= 400 ) { + return new WP_Error( $data->code, $data->message, $data->data ); + } + set_transient( self::transient_name_for_image_generation( $prompt ), $data, self::$image_generation_cache_timeout ); + + return $data; + } +} diff --git a/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-ai.php b/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-ai.php new file mode 100644 index 000000000000..53cc5e2da508 --- /dev/null +++ b/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-ai.php @@ -0,0 +1,103 @@ +is_wpcom = false; + + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { + $this->is_wpcom = true; + $this->wpcom_is_wpcom_only_endpoint = true; + } elseif ( ! ( new Automattic\Jetpack\Status\Host() )->is_woa_site() ) { + // If this is not an atomic site, we want to bail and not even load the endpoint for now. + return; + } + + if ( ! class_exists( 'Jetpack_AI_Helper' ) ) { + require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-ai-helper.php'; + } + + add_action( 'rest_api_init', array( $this, 'register_routes' ) ); + } + + /** + * Register routes. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + $this->rest_base . '/completions', + array( + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'request_gpt_completion' ), + 'permission_callback' => array( 'Jetpack_AI_Helper', 'get_status_permission_check' ), + ), + 'args' => array( + 'content' => array( 'required' => true ), + 'token' => array( 'required' => false ), + ), + ) + ); + register_rest_route( + $this->namespace, + $this->rest_base . '/images/generations', + array( + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'request_dalle_generation' ), + 'permission_callback' => array( 'Jetpack_AI_Helper', 'get_status_permission_check' ), + ), + 'args' => array( + 'prompt' => array( 'required' => true ), + 'token' => array( 'required' => false ), + ), + ) + ); + } + + /** + * Get completions for a given text. + * + * @param WP_REST_Request $request The request. + */ + public function request_gpt_completion( $request ) { + return Jetpack_AI_Helper::get_gpt_completion( $request['content'] ); + } + + /** + * Get image generations for a given prompt. + * + * @param WP_REST_Request $request The request. + */ + public function request_dalle_generation( $request ) { + return Jetpack_AI_Helper::get_dalle_generation( $request['prompt'] ); + } +} + +wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_AI' ); diff --git a/projects/plugins/jetpack/changelog/add-ai-blocks b/projects/plugins/jetpack/changelog/add-ai-blocks new file mode 100644 index 000000000000..11e3867cf3a9 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-ai-blocks @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +Introduce AI-powered blocks for WordPress.com customers. diff --git a/projects/plugins/jetpack/extensions/blocks/ai-image/ai-image.php b/projects/plugins/jetpack/extensions/blocks/ai-image/ai-image.php new file mode 100644 index 000000000000..862b13d55b5f --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-image/ai-image.php @@ -0,0 +1,53 @@ +is_woa_site() ) { + Blocks::jetpack_register_block( + BLOCK_NAME, + array( 'render_callback' => __NAMESPACE__ . '\load_assets' ) + ); + } +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Jetpack AI image block registration/dependency declaration. + * + * @param array $attr Array containing the Jetpack AI image block attributes. + * @param string $content String containing the Jetpack AI image block content. + * + * @return string + */ +function load_assets( $attr, $content ) { + /* + * Enqueue necessary scripts and styles. + */ + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + + return sprintf( + '
%2$s
', + esc_attr( Jetpack_Gutenberg::block_classes( FEATURE_NAME, $attr ) ), + $content + ); +} diff --git a/projects/plugins/jetpack/extensions/blocks/ai-image/attributes.js b/projects/plugins/jetpack/extensions/blocks/ai-image/attributes.js new file mode 100644 index 000000000000..5f26a8a08855 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-image/attributes.js @@ -0,0 +1,10 @@ +export default { + content: { + type: 'string', + source: 'text', + }, + requestedPrompt: { + type: 'string', + default: false, + }, +}; diff --git a/projects/plugins/jetpack/extensions/blocks/ai-image/edit.js b/projects/plugins/jetpack/extensions/blocks/ai-image/edit.js new file mode 100644 index 000000000000..cd526e1c2930 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-image/edit.js @@ -0,0 +1,201 @@ +import apiFetch from '@wordpress/api-fetch'; +import { useBlockProps, store as blockEditorStore } from '@wordpress/block-editor'; +import { createBlock } from '@wordpress/blocks'; +import { + Button, + Placeholder, + TextareaControl, + Flex, + FlexBlock, + FlexItem, + Spinner, +} from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +function getImagesFromOpenAI( + prompt, + setAttributes, + setLoadingImages, + setResultImages, + setErrorMessage +) { + setLoadingImages( true ); + setAttributes( { requestedPrompt: prompt } ); // This will prevent double submitting. + + apiFetch( { + path: '/wpcom/v2/jetpack-ai/images/generations', + method: 'POST', + data: { + prompt, + }, + } ) + .then( res => { + setLoadingImages( false ); + if ( res.error && res.error.message ) { + setErrorMessage( res.error.message ); + return; + } + const images = res.data.map( image => { + return 'data:image/png;base64,' + image.b64_json; + } ); + setResultImages( images ); + } ) + .catch( () => { + setErrorMessage( + __( + 'Whoops, we have encountered an error. AI is like really, really hard and this is an experimental feature. Please try again later.', + 'jetpack' + ) + ); + setLoadingImages( false ); + } ); +} + +/*eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ +export default function Edit( { attributes, setAttributes, clientId } ) { + const [ loadingImages, setLoadingImages ] = useState( false ); + const [ resultImages, setResultImages ] = useState( [] ); + const [ prompt, setPrompt ] = useState( '' ); + const { replaceBlock } = useDispatch( blockEditorStore ); + const [ errorMessage, setErrorMessage ] = useState( '' ); + + const { mediaUpload } = useSelect( select => { + const { getSettings } = select( blockEditorStore ); + const settings = getSettings(); + return { + mediaUpload: settings.mediaUpload, + }; + }, [] ); + + const submit = () => + getImagesFromOpenAI( + prompt, + setAttributes, + setLoadingImages, + setResultImages, + setErrorMessage + ); + + return ( +
+ { ! loadingImages && errorMessage && ( + { errorMessage }
] } + > + + + + + + + + ) } + { ! errorMessage && ! attributes.requestedPrompt && ( + +
+ + +
+
+ ) } + { ! errorMessage && ! loadingImages && resultImages.length > 0 && ( + +
+
+ { attributes.requestedPrompt } +
+
+ { __( 'Please choose your image', 'jetpack' ) } +
+ + { resultImages.map( image => ( + + { + if ( loadingImages ) { + return; + } + setLoadingImages( true ); + // First convert image to a proper blob file + const resp = await fetch( image ); + const blob = await resp.blob(); + const file = new File( [ blob ], 'jetpack_ai_image.png', { + type: 'image/png', + } ); + // Actually upload the image + mediaUpload( { + filesList: [ file ], + onFileChange: ( [ img ] ) => { + if ( ! img.id ) { + // Without this image gets uploaded twice + return; + } + replaceBlock( + clientId, + createBlock( 'core/image', { + url: img.url, + caption: attributes.requestedPrompt, + alt: attributes.requestedPrompt, + } ) + ); + }, + allowedTypes: [ 'image' ], + onError: message => { + // eslint-disable-next-line no-console + console.error( message ); + setLoadingImages( false ); + }, + } ); + } } + /> + + ) ) } + +
+
+ ) } + { ! errorMessage && attributes.content && ! loadingImages && ( + +
+
{ attributes.content }
+
+
+ ) } + { ! errorMessage && loadingImages && ( + +
+ +
+
+ ) } + + ); +} diff --git a/projects/plugins/jetpack/extensions/blocks/ai-image/editor.js b/projects/plugins/jetpack/extensions/blocks/ai-image/editor.js new file mode 100644 index 000000000000..d7ec194d817a --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-image/editor.js @@ -0,0 +1,4 @@ +import registerJetpackBlock from '../../shared/register-jetpack-block'; +import { name, settings } from '.'; + +registerJetpackBlock( name, settings ); diff --git a/projects/plugins/jetpack/extensions/blocks/ai-image/editor.scss b/projects/plugins/jetpack/extensions/blocks/ai-image/editor.scss new file mode 100644 index 000000000000..634918e488d5 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-image/editor.scss @@ -0,0 +1,15 @@ +/** + * Editor styles for Jetpack AI Image + */ + +.wp-block-ai-image-image { + cursor: pointer; + width: 128px; + height:128px; + margin: 12px; +} +.wp-block-ai-image-image:hover { + width: 146px; + height:146px; + margin: 0px; +} diff --git a/projects/plugins/jetpack/extensions/blocks/ai-image/index.js b/projects/plugins/jetpack/extensions/blocks/ai-image/index.js new file mode 100644 index 000000000000..406349c58f22 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-image/index.js @@ -0,0 +1,72 @@ +import { useBlockProps } from '@wordpress/block-editor'; +import { Fragment } from '@wordpress/element'; +import { __, _x } from '@wordpress/i18n'; +import { getIconColor } from '../../shared/block-icons'; +import attributes from './attributes'; +import edit from './edit'; + +/** + * Style dependencies + */ +import './editor.scss'; + +export const name = 'ai-image'; +export const title = __( 'AI Image (Experimental)', 'jetpack' ); +export const settings = { + apiVersion: 2, + title, + description: ( + +

+ { __( + 'Automatically generate an illustration for your post, powered by AI magic. We are experimenting with this feature and can tweak or remove it at any point.', + 'jetpack' + ) } +

+
+ ), + icon: { + src: 'welcome-write-blog', + foreground: getIconColor(), + }, + category: 'media', + keywords: [ + _x( 'AI', 'block search term', 'jetpack' ), + _x( 'DALLe', 'block search term', 'jetpack' ), + _x( 'Diffusion', 'block search term', 'jetpack' ), + ], + supports: { + // Support for block's alignment (left, center, right, wide, full). When true, it adds block controls to change block’s alignment. + align: false /* if set to true, the 'align' option below can be used*/, + // Pick which alignment options to display. + /*align: [ 'left', 'right', 'full' ],*/ + // Support for wide alignment, that requires additional support in themes. + alignWide: false, + // When true, a new field in the block sidebar allows to define an id for the block and a button to copy the direct link. + anchor: false, + // When true, a new field in the block sidebar allows to define a custom className for the block’s wrapper. + customClassName: false, + // When false, Gutenberg won't add a class like .wp-block-your-block-name to the root element of your saved markup + className: true, + // Setting this to false suppress the ability to edit a block’s markup individually. We often set this to false in Jetpack blocks. + html: false, + // Passing false hides this block in Gutenberg's visual inserter. + /*inserter: true,*/ + // When false, user will only be able to insert the block once per post. + multiple: true, + // When false, the block won't be available to be converted into a reusable block. + reusable: false, + }, + edit, + /* @TODO Write the block editor output */ + save: args => { + const blockProps = useBlockProps.save(); + return
{ args.attributes.content }
; + }, + attributes, + example: { + attributes: { + // @TODO: Add default values for block attributes, for generating the block preview. + }, + }, +}; diff --git a/projects/plugins/jetpack/extensions/blocks/ai-paragraph/ai-paragraph.php b/projects/plugins/jetpack/extensions/blocks/ai-paragraph/ai-paragraph.php new file mode 100644 index 000000000000..d00b634f384a --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-paragraph/ai-paragraph.php @@ -0,0 +1,54 @@ +is_woa_site() ) { + Blocks::jetpack_register_block( + BLOCK_NAME, + array( 'render_callback' => __NAMESPACE__ . '\load_assets' ) + ); + } +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Jetpack AI Paragraph block registration/dependency declaration. + * + * @param array $attr Array containing the Jetpack AI Paragraph block attributes. + * @param string $content String containing the Jetpack AI Paragraph block content. + * + * @return string + */ +function load_assets( $attr, $content ) { + /* + * Enqueue necessary scripts and styles. + */ + Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME ); + + return sprintf( + '
%2$s
', + esc_attr( Jetpack_Gutenberg::block_classes( FEATURE_NAME, $attr ) ), + $content + ); +} diff --git a/projects/plugins/jetpack/extensions/blocks/ai-paragraph/attributes.js b/projects/plugins/jetpack/extensions/blocks/ai-paragraph/attributes.js new file mode 100644 index 000000000000..4bee9fddade4 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-paragraph/attributes.js @@ -0,0 +1,10 @@ +export default { + content: { + type: 'string', + source: 'text', + }, + requestedPrompt: { + type: 'boolean', + default: false, + }, +}; diff --git a/projects/plugins/jetpack/extensions/blocks/ai-paragraph/edit.js b/projects/plugins/jetpack/extensions/blocks/ai-paragraph/edit.js new file mode 100644 index 000000000000..7eb3ee20bae3 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-paragraph/edit.js @@ -0,0 +1,131 @@ +import './editor.scss'; + +import apiFetch from '@wordpress/api-fetch'; +import { useBlockProps } from '@wordpress/block-editor'; +import { Placeholder, Button, Spinner } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { useState, RawHTML, useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +function formatPromptToOpenAI( editor ) { + const index = editor.getBlockInsertionPoint().index - 1; + const allBlocksBefore = editor.getBlocks().slice( 0, index ); + return allBlocksBefore + .filter( function ( block ) { + return block && block.attributes && block.attributes.content; + } ) + .map( function ( block ) { + return block.attributes.content.replaceAll( '
', '\n\n' ); + } ) + .join( '\n\n' ); +} + +function getSuggestionFromOpenAI( + setAttributes, + formattedPrompt, + setLoadingCompletion, + setErrorMessage +) { + if ( formattedPrompt.length < 120 ) { + setErrorMessage( + __( + 'Please write a little bit more. Jetpack AI needs at least 120 characters to make the gears spin.', + 'jetpack' + ) + ); + return; + } + setErrorMessage( '' ); + // We only take the last 240 chars into account, otherwise the prompt gets too long and because we have a 110 tokens limit, there is no place for response. + formattedPrompt = formattedPrompt.slice( -240 ); + const data = { content: formattedPrompt }; + setLoadingCompletion( true ); + setAttributes( { requestedPrompt: true } ); // This will prevent double submitting. + apiFetch( { + path: '/wpcom/v2/jetpack-ai/completions', + method: 'POST', + data: data, + } ) + .then( res => { + setLoadingCompletion( false ); + const content = res.prompts[ 0 ].text; + // This is to animate text input. I think this will give an idea of a "better" AI. + // At this point this is an established pattern. + const tokens = content.split( ' ' ); + for ( let i = 0; i < tokens.length; i++ ) { + const output = tokens.slice( 0, i ).join( ' ' ); + setTimeout( () => setAttributes( { content: output } ), 50 * i ); + } + setTimeout( () => setAttributes( { content: content } ), 50 * tokens.length ); + } ) + .catch( () => { + setErrorMessage( + __( + 'Whoops, we have encountered an error. AI is like really, really hard and this is an experimental feature. Please try again later.', + 'jetpack' + ) + ); + setLoadingCompletion( false ); + } ); +} + +export default function Edit( { attributes, setAttributes } ) { + const [ loadingCompletion, setLoadingCompletion ] = useState( false ); + const [ errorMessage, setErrorMessage ] = useState( '' ); + + const formattedPrompt = useSelect( select => { + return formatPromptToOpenAI( select( 'core/block-editor' ) ); + }, [] ); + + //useEffect hook is called only once when block is first rendered. + useEffect( () => { + //Theoretically useEffect would ensure we only fire this once, but I don't want to fire it when we get data to edit either. + if ( ! attributes.content && ! attributes.requestedPrompt ) { + getSuggestionFromOpenAI( + setAttributes, + formattedPrompt, + setLoadingCompletion, + setErrorMessage + ); + } + }, [ attributes, formattedPrompt, setAttributes ] ); + + return ( +
+ { errorMessage && ( + + + + ) } + { attributes.content && ! loadingCompletion && ( +
+
+ { attributes.content.trim().replaceAll( '\n', '
' ) }
+
+
+ ) } + { loadingCompletion && ( +
+ +
+ ) } +
+ ); +} diff --git a/projects/plugins/jetpack/extensions/blocks/ai-paragraph/editor.js b/projects/plugins/jetpack/extensions/blocks/ai-paragraph/editor.js new file mode 100644 index 000000000000..d7ec194d817a --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-paragraph/editor.js @@ -0,0 +1,4 @@ +import registerJetpackBlock from '../../shared/register-jetpack-block'; +import { name, settings } from '.'; + +registerJetpackBlock( name, settings ); diff --git a/projects/plugins/jetpack/extensions/blocks/ai-paragraph/editor.scss b/projects/plugins/jetpack/extensions/blocks/ai-paragraph/editor.scss new file mode 100644 index 000000000000..bf76d66d77de --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-paragraph/editor.scss @@ -0,0 +1,18 @@ +/** + * Editor styles for Jetpack AI Paragraph + */ + +.wp-block-ai-generate-suggestion { + padding: 0; +} + +.wp-block-ai-generate-suggestion .content {} + +.wp-block-ai-generate-suggestion .components-text-control__input { + margin: 10px; + width:80%; +} + +.wp-block-ai-generate-suggestion .components-button.is-primary { + margin: 10px; +} diff --git a/projects/plugins/jetpack/extensions/blocks/ai-paragraph/index.js b/projects/plugins/jetpack/extensions/blocks/ai-paragraph/index.js new file mode 100644 index 000000000000..5a4d5b69ade7 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-paragraph/index.js @@ -0,0 +1,85 @@ +import { useBlockProps } from '@wordpress/block-editor'; +import { createBlock } from '@wordpress/blocks'; +import { Fragment } from '@wordpress/element'; +import { __, _x } from '@wordpress/i18n'; +import { getIconColor } from '../../shared/block-icons'; +import attributes from './attributes'; +import edit from './edit'; + +/** + * Style dependencies + */ +import './editor.scss'; + +export const name = 'ai-paragraph'; +export const title = __( 'AI Paragraph (Experimental)', 'jetpack' ); +export const settings = { + apiVersion: 2, + title, + description: ( + +

+ { __( + 'Automatically generate new paragraphs using your existing content, powered by AI magic. We are experimenting with this feature and can tweak or remove it at any point.', + 'jetpack' + ) } +

+
+ ), + icon: { + src: 'welcome-write-blog', + foreground: getIconColor(), + }, + category: 'text', + keywords: [ + _x( 'AI', 'block search term', 'jetpack' ), + _x( 'GPT', 'block search term', 'jetpack' ), + ], + supports: { + // Support for block's alignment (left, center, right, wide, full). When true, it adds block controls to change block’s alignment. + align: true /* if set to true, the 'align' option below can be used*/, + // Pick which alignment options to display. + /*align: [ 'left', 'right', 'full' ],*/ + // Support for wide alignment, that requires additional support in themes. + alignWide: true, + // When true, a new field in the block sidebar allows to define an id for the block and a button to copy the direct link. + anchor: true, + // When true, a new field in the block sidebar allows to define a custom className for the block’s wrapper. + customClassName: true, + // When false, Gutenberg won't add a class like .wp-block-your-block-name to the root element of your saved markup + className: true, + // Setting this to false suppress the ability to edit a block’s markup individually. We often set this to false in Jetpack blocks. + html: true, + // Passing false hides this block in Gutenberg's visual inserter. + /*inserter: true,*/ + // When false, user will only be able to insert the block once per post. + multiple: true, + // When false, the block won't be available to be converted into a reusable block. + reusable: false, + }, + edit, + save: attrs => { + const blockProps = useBlockProps.save(); + return
{ attrs.attributes.content }
; + }, + attributes, + transforms: { + to: [ + { + type: 'block', + blocks: [ 'core/paragraph' ], + transform: ( { content } ) => { + return createBlock( 'core/paragraph', { + content, + } ); + }, + }, + ], + }, + example: { + attributes: { + requestedPrompt: true, + content: __( "I'm afraid I can't do that, Dave.", 'jetpack' ), + }, + }, +}; diff --git a/projects/plugins/jetpack/extensions/index.json b/projects/plugins/jetpack/extensions/index.json index 122fd3301d79..1f88a00ec913 100644 --- a/projects/plugins/jetpack/extensions/index.json +++ b/projects/plugins/jetpack/extensions/index.json @@ -45,6 +45,8 @@ "payment-buttons" ], "beta": [ + "ai-image", + "ai-paragraph", "amazon", "google-docs-embed", "launchpad-save-modal",