/* * External dependencies */ import { aiAssistantIcon } from '@automattic/jetpack-ai-client'; import { useAnalytics } from '@automattic/jetpack-shared-extension-utils'; import { getBlockContent } from '@wordpress/blocks'; import { MenuItem, MenuGroup, ToolbarButton, Dropdown, Notice } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { useState, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { post, postContent, postExcerpt, termDescription } from '@wordpress/icons'; import React from 'react'; /** * Internal dependencies */ import { getStoreBlockId } from '../../extensions/ai-assistant/with-ai-assistant'; import { PROMPT_TYPE_CHANGE_TONE, PROMPT_TYPE_CORRECT_SPELLING, PROMPT_TYPE_MAKE_LONGER, PROMPT_TYPE_SIMPLIFY, PROMPT_TYPE_SUMMARIZE, PROMPT_TYPE_CHANGE_LANGUAGE, } from '../../lib/prompt'; import { getRawTextFromHTML } from '../../lib/utils/block-content'; import { transformToAIAssistantBlock } from '../../transforms'; import { I18nMenuDropdown } from '../i18n-dropdown-control'; import { ToneDropdownMenu } from '../tone-dropdown-control'; import './style.scss'; /** * Types and constants */ import type { ExtendedBlockProp } from '../../extensions/ai-assistant'; import type { PromptTypeProp } from '../../lib/prompt'; import type { ToneProp } from '../tone-dropdown-control'; // Quick edits option: "Correct spelling and grammar" const QUICK_EDIT_KEY_CORRECT_SPELLING = 'correct-spelling' as const; // Quick edits option: "Simplify" const QUICK_EDIT_KEY_SIMPLIFY = 'simplify' as const; // Quick edits option: "Summarize" const QUICK_EDIT_KEY_SUMMARIZE = 'summarize' as const; // Quick edits option: "Make longer" const QUICK_EDIT_KEY_MAKE_LONGER = 'make-longer' as const; // Ask AI Assistant option export const KEY_ASK_AI_ASSISTANT = 'ask-ai-assistant' as const; const quickActionsList = [ { name: __( 'Correct spelling and grammar', 'jetpack' ), key: QUICK_EDIT_KEY_CORRECT_SPELLING, aiSuggestion: PROMPT_TYPE_CORRECT_SPELLING, icon: termDescription, }, { name: __( 'Simplify', 'jetpack' ), key: QUICK_EDIT_KEY_SIMPLIFY, aiSuggestion: PROMPT_TYPE_SIMPLIFY, icon: post, }, { name: __( 'Summarize', 'jetpack' ), key: QUICK_EDIT_KEY_SUMMARIZE, aiSuggestion: PROMPT_TYPE_SUMMARIZE, icon: postExcerpt, }, { name: __( 'Expand', 'jetpack' ), key: QUICK_EDIT_KEY_MAKE_LONGER, aiSuggestion: PROMPT_TYPE_MAKE_LONGER, icon: postContent, }, ]; export type AiAssistantDropdownOnChangeOptionsArgProps = { tone?: ToneProp; language?: string; }; type AiAssistantControlComponentProps = { /* * The block type. Required. */ blockType: ExtendedBlockProp; }; /** * Given a list of blocks, it returns their content as a string. * @param {Array} blocks - The list of blocks. * @returns {string} The content of the blocks as a string. */ export function getBlocksContent( blocks ) { return blocks .filter( block => block != null ) // Safeguard against null or undefined blocks .map( block => getBlockContent( block ) ) .join( '\n\n' ); } type AiAssistantDropdownContentProps = { onClose: () => void; blockType: ExtendedBlockProp; }; /** * The React content of the dropdown. * @param {AiAssistantDropdownContentProps} props - The props. * @returns {React.ReactNode} The React content of the dropdown. */ function AiAssistantDropdownContent( { onClose, blockType, }: AiAssistantDropdownContentProps ): React.ReactNode { // Set the state for the no content info. const [ noContent, setNoContent ] = useState( false ); /* * Let's disable the eslint rule for this line. * @todo: fix by using StoreDescriptor, or something similar */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const { getSelectedBlockClientIds, getBlocksByClientId } = useSelect( 'core/block-editor' ); const { removeBlocks, replaceBlock } = useDispatch( 'core/block-editor' ); // Store the current content in a local state useEffect( () => { const clientIds = getSelectedBlockClientIds(); const blocks = getBlocksByClientId( clientIds ); const content = getBlocksContent( blocks ); const rawContent = getRawTextFromHTML( content ); // Set no content condition to show the Notice info message. return setNoContent( ! rawContent.length ); }, [ getBlocksByClientId, getSelectedBlockClientIds ] ); const { tracks } = useAnalytics(); const requestSuggestion = ( promptType: PromptTypeProp, options: AiAssistantDropdownOnChangeOptionsArgProps = {} ) => { const clientIds = getSelectedBlockClientIds(); const blocks = getBlocksByClientId( clientIds ); const content = getBlocksContent( blocks ); onClose(); tracks.recordEvent( 'jetpack_editor_ai_assistant_extension_toolbar_button_click', { suggestion: promptType, block_type: blockType, } ); const [ firstBlock ] = blocks; const [ firstClientId, ...otherBlocksIds ] = clientIds; const extendedBlockAttributes = { ...( firstBlock?.attributes || {} ), // firstBlock.attributes should never be undefined, but still add a fallback content, }; const newAIAssistantBlock = transformToAIAssistantBlock( blockType, extendedBlockAttributes ); /* * Store in the local storage the client id * of the block that need to auto-trigger the AI Assistant request. * @todo: find a better way to update the content, * probably using a new store triggering an action. */ // Storage client Id, prompt type, and options. const storeObject = { clientId: firstClientId, type: promptType, options: { ...options, contentType: 'generated', fromExtension: true }, // When converted, the original content must be treated as generated }; localStorage.setItem( getStoreBlockId( newAIAssistantBlock.clientId ), JSON.stringify( storeObject ) ); /* * Replace the first block with the new AI Assistant block instance. * This block contains the original content, * even for multiple blocks selection. */ replaceBlock( firstClientId, newAIAssistantBlock ); // It removes the rest of the blocks in case there are more than one. removeBlocks( otherBlocksIds ); }; const replaceWithAiAssistantBlock = () => { const clientIds = getSelectedBlockClientIds(); const blocks = getBlocksByClientId( clientIds ); const content = getBlocksContent( blocks ); const [ firstClientId, ...otherBlocksIds ] = clientIds; const [ firstBlock ] = blocks; const extendedBlockAttributes = { ...( firstBlock?.attributes || {} ), // firstBlock.attributes should never be undefined, but still add a fallback content, }; replaceBlock( firstClientId, transformToAIAssistantBlock( blockType, extendedBlockAttributes ) ); removeBlocks( otherBlocksIds ); }; return ( <> { noContent && ( { __( 'Add content to activate the tools below', 'jetpack' ) } ) } { __( 'Ask AI Assistant', 'jetpack' ) } { quickActionsList.map( quickAction => ( { requestSuggestion( quickAction.aiSuggestion, {} ); } } disabled={ noContent } > { quickAction.name } ) ) } { requestSuggestion( PROMPT_TYPE_CHANGE_TONE, { tone } ); } } disabled={ noContent } /> { requestSuggestion( PROMPT_TYPE_CHANGE_LANGUAGE, { language } ); } } disabled={ noContent } /> > ); } export default function AiAssistantDropdown( { blockType }: AiAssistantControlComponentProps ) { return ( { return ( ); } } renderContent={ ( { onClose: onClose } ) => ( ) } /> ); }