Skip to content

Commit

Permalink
feat: add AI content toolbar
Browse files Browse the repository at this point in the history
  • Loading branch information
Soare-Robert-Daniel committed Aug 9, 2023
1 parent df44baf commit 52f9440
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 13 deletions.
9 changes: 2 additions & 7 deletions inc/server/class-prompt-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,7 @@ public function get_prompts( $request ) {
if ( $request->get_param( 'name' ) !== null ) {
$prompts = false !== $prompts ? $prompts : $response['prompts'];
// Prompt can be filtered by name. By filtering by name, we can get only the prompt we need and save some bandwidth.
$response['prompts'] = array_filter(
$prompts,
function ( $prompt ) use ( $request ) {
return $prompt['otter_name'] === $request->get_param( 'name' );
}
);
$response['prompts'] = $prompts; // TODO: temporary change. The original did not give an array as JSON response.

if ( empty( $response['prompts'] ) ) {
$response['prompts'] = array();
Expand Down Expand Up @@ -146,7 +141,7 @@ public function retrieve_prompts_from_server() {
'license_id' => apply_filters( 'product_otter_license_key', 'free' ),
'cache' => gmdate( 'u' ),
),
'https://api.themeisle.com/templates-cloud/otter-prompts'
'http://localhost:3000/prompts' // TODO: change to https://api.themeisle.com/otter/prompts when it is ready.
);

$response = '';
Expand Down
8 changes: 4 additions & 4 deletions src/blocks/components/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type PromptPlaceholderProps = {
actionButtons?: ( props: {status?: string}) => ReactNode
};

export const apiKeyName = 'themeisle_open_ai_api_key';
export const openAiAPIKeyName = 'themeisle_open_ai_api_key';

const PromptBlockEditor = (
props: {
Expand Down Expand Up @@ -159,9 +159,9 @@ const PromptPlaceholder = ( props: PromptPlaceholderProps ) => {
}

if ( 'loaded' === status ) {
if ( getOption( apiKeyName ) ) {
if ( getOption( openAiAPIKeyName ) ) {
setApiKeyStatus( 'present' );
setApiKey( getOption( apiKeyName ) );
setApiKey( getOption( openAiAPIKeyName ) );
} else {
setApiKeyStatus( 'missing' );
}
Expand Down Expand Up @@ -297,7 +297,7 @@ const PromptPlaceholder = ( props: PromptPlaceholderProps ) => {
return;
}

updateOption( apiKeyName, apiKey.slice(), __( 'Open AI API Key saved.', 'otter-blocks' ), 'o-api-key', () => {
updateOption( openAiAPIKeyName, apiKey.slice(), __( 'Open AI API Key saved.', 'otter-blocks' ), 'o-api-key', () => {
setApiKey( '' );
});
setApiKeyStatus( 'checking' );
Expand Down
9 changes: 8 additions & 1 deletion src/blocks/helpers/block-utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,8 @@ export function insertBlockBelow( clientId, block ) {
} = select( 'core/block-editor' );

const {
insertBlock
insertBlock,
insertBlocks
} = dispatch( 'core/block-editor' );

const rootClientId = getBlockRootClientId( clientId );
Expand All @@ -503,5 +504,11 @@ export function insertBlockBelow( clientId, block ) {
}

const index = getBlockIndex( clientId, rootClientId );

// If the block is an array of blocks, insert them all.
if ( Array.isArray( block ) ) {
return insertBlocks( block, index + 1, rootClientId );
}

insertBlock( block, index + 1, rootClientId );
}
18 changes: 17 additions & 1 deletion src/blocks/helpers/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export type PromptData = {
function_call: {
[key: string]: string
}
}
} & Record<string, string>

export type PromptsData = PromptData[]

Expand Down Expand Up @@ -243,3 +243,19 @@ export function retrieveEmbeddedPrompt( promptName ?: string ) {
method: 'GET'
});
}

export function injectActionIntoPrompt( embeddedPrompt: PromptData, actionPrompt: string ): PromptData {
return {
...embeddedPrompt,
messages: embeddedPrompt.messages.map( ( message ) => {
if ( 'user' === message.role && message.content.includes( '{ACTION}' ) ) {
return {
role: 'user',
content: message.content.replace( '{ACTION}', actionPrompt )
};
}

return message;
})
} as PromptData;
}
7 changes: 7 additions & 0 deletions src/blocks/plugins/ai-content/editor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.o-menu-item-header {
font-family: monospace;
padding-left: 8px;
font-size: 13px;
color: #828282;
text-transform: uppercase;
}
259 changes: 259 additions & 0 deletions src/blocks/plugins/ai-content/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
/**
* WordPress dependencies.
*/
import { __ } from '@wordpress/i18n';

// @ts-ignore
import {
__experimentalToolsPanel as ToolsPanel,
__experimentalToolsPanelItem as ToolsPanelItem, DropdownMenu, MenuGroup, MenuItem, Toolbar, Spinner
} from '@wordpress/components';

import { createHigherOrderComponent } from '@wordpress/compose';

import { Fragment, useEffect, useState } from '@wordpress/element';

import {
addFilter,
applyFilters
} from '@wordpress/hooks';

import { useDispatch, useSelect } from '@wordpress/data';
import { rawHandler, serialize } from '@wordpress/blocks';
import { store as blockEditorStore } from '@wordpress/block-editor';

/**
* Internal dependencies.
*/
import { BlockControls } from '@wordpress/block-editor';
import { aiGeneration } from '../../helpers/icons';
import './editor.scss';
import { PromptsData, injectActionIntoPrompt, retrieveEmbeddedPrompt, sendPromptToOpenAI } from '../../helpers/prompt';
import useSettings from '../../helpers/use-settings';
import { openAiAPIKeyName } from '../../components/prompt';
import { insertBlockBelow } from '../../helpers/block-utility';

const isValidBlock = ( blockName: string|undefined ) => {
if ( ! blockName ) {
return false;
}

return [
'core/paragraph'
].some( ( name ) => name === blockName );
};

const extractContent = ( props ) => {
if ( 'core/paragraph' === props.name ) {
return props.attributes.content;
}

return '';
};

let embeddedPromptsCache: PromptsData|null = null;

const withConditions = createHigherOrderComponent( BlockEdit => {
return props => {
const [ embeddedPrompts, setEmbeddedPrompts ] = useState<PromptsData|null>( null );
const [ getOption, updateOption, status ] = useSettings();
const [ apiKey, setApiKey ] = useState<string | null>( null );
const [ isProcessing, setIsProcessing ] = useState<Record<string, boolean>>({});
const [ displayError, setDisplayError ] = useState<string|undefined>( undefined );

// Get the create notice function from the hooks api.
const { createNotice } = useDispatch( 'core/notices' );


useEffect( () => {
const getEmbeddedPrompt = async() => {
retrieveEmbeddedPrompt( 'textTransformation' ).then( ( promptServer ) => {
setEmbeddedPrompts( promptServer.prompts );
embeddedPromptsCache = promptServer.prompts;
});
};

if ( ! embeddedPromptsCache ) {
console.count( 'getEmbeddedPrompt' ); // TODO: remove after prototyping
getEmbeddedPrompt();
}
}, []);

useEffect( () => {
if ( 'loading' === status ) {
return;
}

if ( 'loaded' === status && ! apiKey ) {
if ( getOption( openAiAPIKeyName ) ) {
console.log( getOption( openAiAPIKeyName ) );
setApiKey( getOption( openAiAPIKeyName ) );
}
}
}, [ status, getOption ]);

useEffect( () => {
if ( ! displayError ) {
return;
}

createNotice(
'error',
displayError,
{
type: 'snackbar',
isDismissible: true
}
);

setDisplayError( undefined );
}, [ displayError ]);

const generateContent = ( content: string, actionKey: string, callback: Function = () =>{}) => {

if ( ! content ) {
setDisplayError( __( 'No content detected in selected block.', 'otter-blocks' ) );
return;
}

const embeddedPrompt = embeddedPromptsCache?.find( ( prompt ) => 'textTransformation' === prompt.otter_name );

if ( ! embeddedPrompt ) {
setDisplayError( __( 'Something when wrong retrieving the prompts.', 'otter-blocks' ) );
return;
}

const action: undefined | string = embeddedPrompt?.[actionKey];

if ( ! action ) {
setDisplayError( __( 'The action is not longer available.', 'otter-blocks' ) );
return;
}

if ( ! apiKey ) {
setDisplayError( __( 'No Open API key detected. Please add your key.', 'otter-blocks' ) );
return;
}

setIsProcessing( prevState => ({ ...prevState, [ actionKey ]: true }) );
sendPromptToOpenAI(
content,
apiKey,
injectActionIntoPrompt(
embeddedPrompt,
action
)
).then( ( response ) => {
if ( response.error ) {
setDisplayError( response.error );
return;
}
console.log( response );

console.log( response?.choices?.[0]?.message.content );
const blockContentRaw = response?.choices?.[0]?.message.content;

if ( ! blockContentRaw ) {
return;
}

const newBlocks = rawHandler({
HTML: blockContentRaw
});

insertBlockBelow( props.clientId, newBlocks );

setIsProcessing( prevState => ({ ...prevState, [ actionKey ]: false }) );
callback?.();
}).catch( ( error ) => {
setDisplayError( error.message );
setIsProcessing( prevState => ({ ...prevState, [ actionKey ]: false }) );
});
};

return (
<Fragment>
<BlockEdit { ...props } />

{ isValidBlock( props.name ) && props.isSelected && (
<BlockControls>
<Toolbar>
<DropdownMenu
icon={aiGeneration}
label={ __( 'Otter AI Content' ) }
>
{
({ onClose }) => (
<Fragment>
<MenuGroup>
<span className="o-menu-item-header">{__( 'Writing', 'otter-blocks' )}</span>
<MenuItem onClick={ () => {
generateContent( extractContent( props ), 'otter_action_generate_title', onClose );
} }>
{ __( 'Generate a title', 'otter-blocks' ) }
{ isProcessing?.['otter_action_generate_title'] && <Spinner /> }
</MenuItem>
<MenuItem onClick={ () => {
generateContent( extractContent( props ), 'otter_action_continue_writing', onClose );
} }>
{ __( 'Continue writing', 'otter-blocks' ) }
{ isProcessing?.['otter_action_continue_writing'] && <Spinner /> }
</MenuItem>
<MenuItem onClick={ () => {
generateContent( extractContent( props ), 'otter_action_summarize', onClose );
} }>
{ __( 'Summarize text', 'otter-blocks' ) }
{ isProcessing?.['otter_action_summarize'] && <Spinner /> }
</MenuItem>
<MenuItem onClick={ () => {
generateContent( extractContent( props ), 'otter_action_make_shorter', onClose );
} }>
{ __( 'Make shorter', 'otter-blocks' ) }
{ isProcessing?.['otter_action_make_shorter'] && <Spinner /> }
</MenuItem>
<MenuItem onClick={ () => {
generateContent( extractContent( props ), 'otter_action_make_longer', onClose );
} }>
{ __( 'Make longer', 'otter-blocks' ) }
{ isProcessing?.['otter_action_make_longer'] && <Spinner /> }
</MenuItem>
</MenuGroup>
<MenuGroup>
<span className="o-menu-item-header">{__( 'Tone', 'otter-blocks' )}</span>
<MenuItem onClick={ () => {
generateContent( extractContent( props ), 'otter_action_tone_professional', onClose );
} }>
{ __( 'Professional', 'otter-blocks' ) }
{ isProcessing?.['otter_action_tone_professional'] && <Spinner /> }
</MenuItem>
<MenuItem onClick={ () => {
generateContent( extractContent( props ), 'otter_action_tone_friendly', onClose );
} }>
{ __( 'Friendly', 'otter-blocks' ) }
{ isProcessing?.['otter_action_tone_friendly'] && <Spinner /> }
</MenuItem>
<MenuItem onClick={ () => {
generateContent( extractContent( props ), 'otter_action_tone_humorous', onClose );
} }>
{ __( 'Humorous', 'otter-blocks' ) }
{ isProcessing?.['otter_action_tone_humorous'] && <Spinner /> }
</MenuItem>
</MenuGroup>
<MenuGroup>
<MenuItem onClick={ onClose }>
{ __( 'Discard changes', 'otter-blocks' ) }
</MenuItem>
</MenuGroup>
</Fragment>
)
}
</DropdownMenu>
</Toolbar>
</BlockControls>
) }
</Fragment>
);
};
}, 'withConditions' );

addFilter( 'editor.BlockEdit', 'themeisle-gutenberg/otter-ai-content-toolbar', withConditions );
1 change: 1 addition & 0 deletions src/blocks/plugins/registerPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import './feedback/index.js';
import './otter-tools-inspector/index';
import './live-search/index.js';
import './upsell-block/index.js';
import './ai-content/index.tsx';

const icon = <Icon icon={ otterIcon } />;

Expand Down

0 comments on commit 52f9440

Please sign in to comment.