Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: enhancement

AI Paragraph block now is aware of the rest of editor canvas and can work without any input
82 changes: 70 additions & 12 deletions projects/plugins/jetpack/extensions/blocks/ai-paragraph/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ 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';
import { sprintf, __ } from '@wordpress/i18n';

function formatPromptToOpenAI( editor ) {
function formatEditorCanvasContentForCompletion( editor ) {
const index = editor.getBlockInsertionPoint().index - 1;
const allBlocksBefore = editor.getBlocks().slice( 0, index );
return allBlocksBefore
Expand All @@ -26,18 +26,23 @@ function getSuggestionFromOpenAI(
setLoadingCompletion,
setErrorMessage
) {
if ( formattedPrompt.length < 120 ) {
const needsAtLeast = 36;
if ( formattedPrompt.length < needsAtLeast ) {
setErrorMessage(
__(
'Please write a little bit more. Jetpack AI needs at least 120 characters to make the gears spin.',
'jetpack'
sprintf(
/** translators: First placeholder is a number of more characters we need, second is the total number we need */
__(
'AI needs you to write %1$d more characters above this block. When inserted into an empty post with a title (or additionally categories selected), AI Paragraph will generate a new post for you. If you insert the AI Paragraph block following other text, it will attemp to auto-complete, but it needs at least %2$d characters of input.',
'jetpack'
),
needsAtLeast - formattedPrompt.length,
needsAtLeast
)
);
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.
Expand Down Expand Up @@ -69,13 +74,66 @@ function getSuggestionFromOpenAI(
} );
}

/**
* Gather data available in Gutenberg and prepare the best prompt we can come up with.
*
* @param {Function} select - function returning Gutenberg data store.
* @returns {string} - prompt ready to pipe to OpenAI.
*/
function preparePromptBasedOnEditorState( select ) {
const prompt = formatEditorCanvasContentForCompletion( select( 'core/block-editor' ) );

// If a user started typing something, we will just create a completion.
if ( prompt.length > 0 ) {
// 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.
return prompt.slice( -240 );
}

// Let's grab post data so that we can do something smart.
const currentPost = select( 'core/editor' ).getCurrentPost();
if ( ! currentPost ) {
return '';
}
Comment on lines +93 to +96
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me think we may want to gate the block so it doesn't show in the site or widget editor, where there is no post data?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is an easy flag than yes, we could prevent it being there. Do you know how to get that?

But I hope to make it more aware of where it is and behave accordingly, as we can also detect the current post type etc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no easy solution at the moment I'm afraid, short of checking if the post and post types are defined.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When no post is available it will just default to the auto-complete behaviour so that seems fine for now


// If there is no title, there is not much we can do.
if ( ! currentPost.title ) {
return '';
}

// We are filtering out the default "Uncategorized"
const categories = currentPost.categories.filter( catId => catId !== 1 );

// User did not set any categories, we are going to base the suggestions off a title.
if ( ! categories.length ) {
/** translators: This will be a prompt to OpenAI to generate a post based on the post title */
return sprintf( __( 'Write content of a post titled "%s"', 'jetpack' ), currentPost.title );
}

// We are grabbing more data from WP.
const categoryObjects = categories.map( categoryId =>
select( 'core' ).getEntityRecord( 'taxonomy', 'category', categoryId )
);
// We want to wait until all category names are loaded. This will return empty string (aka loading state) until all objects are truthy.
if ( categoryObjects.filter( obj => ! obj || ! obj.name ).length ) {
return '';
}

const categoryNames = categoryObjects.map( ( { name } ) => name ).join( ', ' );

return sprintf(
/** translators: This will be a prompt to OpenAI to generate a post based on the comma-seperated category names and the post title */
__( 'Write content of a post with categories "%1$s" titled "%2$s"', 'jetpack' ),
categoryNames,
currentPost.title
);
}

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' ) );
}, [] );
// Here is where we craft the prompt.
const formattedPrompt = useSelect( preparePromptBasedOnEditorState, [] );

//useEffect hook is called only once when block is first rendered.
useEffect( () => {
Expand All @@ -88,7 +146,7 @@ export default function Edit( { attributes, setAttributes } ) {
setErrorMessage
);
}
}, [ attributes, formattedPrompt, setAttributes ] );
}, [ setAttributes, attributes ] ); // eslint-disable-line react-hooks/exhaustive-deps

return (
<div { ...useBlockProps() }>
Expand Down