diff --git a/projects/plugins/jetpack/changelog/update-jetpack-ai-inline-extensions-input-state b/projects/plugins/jetpack/changelog/update-jetpack-ai-inline-extensions-input-state new file mode 100644 index 0000000000000..00b95c548c131 --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-jetpack-ai-inline-extensions-input-state @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +AI Assistant: Link toolbar actions to requests on inline extensions diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/components/ai-assistant-toolbar-dropdown/dropdown-content.tsx b/projects/plugins/jetpack/extensions/blocks/ai-assistant/components/ai-assistant-toolbar-dropdown/dropdown-content.tsx index 88c5ad2927f4a..f17b9ebee6c8b 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/components/ai-assistant-toolbar-dropdown/dropdown-content.tsx +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/components/ai-assistant-toolbar-dropdown/dropdown-content.tsx @@ -18,8 +18,9 @@ import { PROMPT_TYPE_CHANGE_LANGUAGE, PROMPT_TYPE_USER_PROMPT, } from '../../lib/prompt'; -import { I18nMenuDropdown } from '../i18n-dropdown-control'; -import { ToneDropdownMenu } from '../tone-dropdown-control'; +import { capitalize } from '../../lib/utils/capitalize'; +import { I18nMenuDropdown, TRANSLATE_LABEL } from '../i18n-dropdown-control'; +import { TONE_LABEL, ToneDropdownMenu } from '../tone-dropdown-control'; import './style.scss'; /** * Types and constants @@ -112,7 +113,8 @@ export type AiAssistantDropdownOnChangeOptionsArgProps = { export type OnRequestSuggestion = ( promptType: PromptTypeProp, - options?: AiAssistantDropdownOnChangeOptionsArgProps + options?: AiAssistantDropdownOnChangeOptionsArgProps, + humanText?: string ) => void; type AiAssistantToolbarDropdownContentProps = { @@ -162,7 +164,11 @@ export default function AiAssistantToolbarDropdownContent( { iconPosition="left" key={ `key-${ quickAction.key }` } onClick={ () => { - onRequestSuggestion( quickAction.aiSuggestion, { ...( quickAction.options ?? {} ) } ); + onRequestSuggestion( + quickAction.aiSuggestion, + { ...( quickAction.options ?? {} ) }, + quickAction.name + ); } } disabled={ disabled } > @@ -172,14 +178,22 @@ export default function AiAssistantToolbarDropdownContent( { { - onRequestSuggestion( PROMPT_TYPE_CHANGE_TONE, { tone } ); + onRequestSuggestion( + PROMPT_TYPE_CHANGE_TONE, + { tone }, + `${ TONE_LABEL }: ${ capitalize( tone ) }` + ); } } disabled={ disabled } /> { - onRequestSuggestion( PROMPT_TYPE_CHANGE_LANGUAGE, { language } ); + onChange={ ( language, name ) => { + onRequestSuggestion( + PROMPT_TYPE_CHANGE_LANGUAGE, + { language }, + `${ TRANSLATE_LABEL }: ${ name }` + ); } } disabled={ disabled } /> diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/components/i18n-dropdown-control/index.tsx b/projects/plugins/jetpack/extensions/blocks/ai-assistant/components/i18n-dropdown-control/index.tsx index 147f51b92ecc1..8307184f2eddb 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/components/i18n-dropdown-control/index.tsx +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/components/i18n-dropdown-control/index.tsx @@ -13,6 +13,7 @@ import { import { __ } from '@wordpress/i18n'; import { Icon, chevronRight } from '@wordpress/icons'; import { globe } from '@wordpress/icons'; +import React from 'react'; /* * Internal dependencies */ @@ -38,7 +39,7 @@ export type LanguageProp = ( typeof LANGUAGE_LIST )[ number ]; type LanguageDropdownControlProps = { value?: LanguageProp; - onChange: ( value: string ) => void; + onChange: ( value: string, name?: string ) => void; label?: string; disabled?: boolean; }; @@ -46,7 +47,7 @@ type LanguageDropdownControlProps = { const defaultLanguageLocale = window?.Jetpack_Editor_Initial_State?.siteLocale || navigator?.language; -const defaultLabel = __( 'Translate', 'jetpack' ); +export const TRANSLATE_LABEL = __( 'Translate', 'jetpack' ); export const defaultLanguage = ( defaultLanguageLocale?.split( '-' )[ 0 ] || 'en' ) as LanguageProp; @@ -89,16 +90,6 @@ export const LANGUAGE_MAP = { ko: { label: __( 'Korean', 'jetpack' ), }, - - id: { - label: __( 'Indonesian', 'jetpack' ), - }, - tl: { - label: __( 'Filipino', 'jetpack' ), - }, - vi: { - label: __( 'Vietnamese', 'jetpack' ), - }, }; export const I18nMenuGroup = ( { @@ -117,7 +108,12 @@ export const I18nMenuGroup = ( { return ( onChange( language + ' (' + LANGUAGE_MAP[ language ].label + ')' ) } + onClick={ () => + onChange( + language + ' (' + LANGUAGE_MAP[ language ].label + ')', + LANGUAGE_MAP[ language ].label + ) + } isSelected={ value === language } > { LANGUAGE_MAP[ language ].label } @@ -130,7 +126,7 @@ export const I18nMenuGroup = ( { export default function I18nDropdownControl( { value = defaultLanguage, - label = defaultLabel, + label = TRANSLATE_LABEL, onChange, disabled = false, }: LanguageDropdownControlProps ) { @@ -164,7 +160,7 @@ export default function I18nDropdownControl( { export function I18nMenuDropdown( { value = defaultLanguage, - label = defaultLabel, + label = TRANSLATE_LABEL, onChange, disabled = false, }: Pick< LanguageDropdownControlProps, 'label' | 'onChange' | 'value' | 'disabled' > & { @@ -187,8 +183,8 @@ export function I18nMenuDropdown( { > { ( { onClose } ) => ( { - onChange( newLanguage ); + onChange={ ( ...args ) => { + onChange( ...args ); onClose(); } } value={ value } diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/components/tone-dropdown-control/index.tsx b/projects/plugins/jetpack/extensions/blocks/ai-assistant/components/tone-dropdown-control/index.tsx index e45244050f254..cf88111776f0c 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/components/tone-dropdown-control/index.tsx +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/components/tone-dropdown-control/index.tsx @@ -14,6 +14,7 @@ import { } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { chevronRight } from '@wordpress/icons'; +import React from 'react'; /** * Internal dependencies */ @@ -23,27 +24,19 @@ const PROMPT_TONES_LIST = [ 'formal', 'informal', 'optimistic', - // 'pessimistic', 'humorous', 'serious', 'skeptical', 'empathetic', - // 'enthusiastic', - // 'neutral', 'confident', - // 'curious', - // 'respectful', 'passionate', - // 'cautious', 'provocative', - // 'inspirational', - // 'satirical', - // 'dramatic', - // 'mysterious', ] as const; export const DEFAULT_PROMPT_TONE = 'formal'; +export const TONE_LABEL = __( 'Change tone', 'jetpack' ); + export const PROMPT_TONES_MAP = { formal: { label: __( 'Formal', 'jetpack' ), @@ -57,10 +50,6 @@ export const PROMPT_TONES_MAP = { label: __( 'Optimistic', 'jetpack' ), emoji: '๐Ÿ˜ƒ', }, - // pessimistic: { - // label: __( 'Pessimistic', 'jetpack' ), - // emoji: 'โ˜น๏ธ', - // }, humorous: { label: __( 'Humorous', 'jetpack' ), emoji: '๐Ÿ˜‚', @@ -77,54 +66,18 @@ export const PROMPT_TONES_MAP = { label: __( 'Empathetic', 'jetpack' ), emoji: '๐Ÿ’—', }, - // enthusiastic: { - // label: __( 'Enthusiastic', 'jetpack' ), - // emoji: '๐Ÿคฉ', - // }, - // neutral: { - // label: __( 'Neutral', 'jetpack' ), - // emoji: '๐Ÿ˜ถ', - // }, confident: { label: __( 'Confident', 'jetpack' ), emoji: '๐Ÿ˜Ž', }, - // curious: { - // label: __( 'Curious', 'jetpack' ), - // emoji: '๐Ÿง', - // }, - // respectful: { - // label: __( 'Respectful', 'jetpack' ), - // emoji: '๐Ÿ™', - // }, passionate: { label: __( 'Passionate', 'jetpack' ), emoji: 'โค๏ธ', }, - // cautious: { - // label: __( 'Cautious', 'jetpack' ), - // emoji: '๐Ÿšง', - // }, provocative: { label: __( 'Provocative', 'jetpack' ), emoji: '๐Ÿ”ฅ', }, - // inspirational: { - // label: __( 'Inspirational', 'jetpack' ), - // emoji: 'โœจ', - // }, - // satirical: { - // label: __( 'Satirical', 'jetpack' ), - // emoji: '๐Ÿƒ', - // }, - // dramatic: { - // label: __( 'Dramatic', 'jetpack' ), - // emoji: '๐ŸŽญ', - // }, - // mysterious: { - // label: __( 'Mysterious', 'jetpack' ), - // emoji: '๐Ÿ”ฎ', - // }, }; export type ToneProp = ( typeof PROMPT_TONES_LIST )[ number ]; @@ -153,7 +106,7 @@ const ToneMenuGroup = ( { value, onChange }: ToneToolbarDropdownMenuProps ) => ( ); export function ToneDropdownMenu( { - label = __( 'Change tone', 'jetpack' ), + label = TONE_LABEL, value = DEFAULT_PROMPT_TONE, onChange, disabled = false, @@ -194,7 +147,6 @@ export default function ToneToolbarDropdownMenu( { onChange, disabled = false, }: ToneToolbarDropdownMenuProps ) { - const label = __( 'Change tone', 'jetpack' ); const { tracks } = useAnalytics(); const toggleHandler = isOpen => { @@ -204,7 +156,7 @@ export default function ToneToolbarDropdownMenu( { }; return disabled ? ( - + @@ -212,7 +164,7 @@ export default function ToneToolbarDropdownMenu( { ) : ( ; + action?: string; request: ( question: string ) => void; stopSuggestion?: () => void; close?: () => void; @@ -52,8 +55,21 @@ export default function AiAssistantInput( { throw new Error( 'Function not implemented.' ); } + // Clear the input value on reset and when the request is done. + useEffect( () => { + if ( [ 'init', 'done' ].includes( requestingState ) ) { + setValue( '' ); + } + }, [ requestingState ] ); + + // Set the value to the quick action text once it changes. + useEffect( () => { + setValue( action || '' ); + }, [ action ] ); + return ( { - onRequestSuggestion?.( promptType, options ); + const handleRequestSuggestion: OnRequestSuggestion = ( ...args ) => { + onRequestSuggestion?.( ...args ); onClose?.(); }; diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/inline-extensions/heading/index.tsx b/projects/plugins/jetpack/extensions/blocks/ai-assistant/inline-extensions/heading/index.tsx index 68bd951729293..c0191fcfe8661 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/inline-extensions/heading/index.tsx +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/inline-extensions/heading/index.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { renderMarkdownFromHTML, renderHTMLFromMarkdown } from '@automattic/jetpack-ai-client'; -import { rawHandler } from '@wordpress/blocks'; +import { rawHandler, getBlockContent } from '@wordpress/blocks'; import { select, dispatch } from '@wordpress/data'; /** * Types @@ -10,7 +10,7 @@ import { select, dispatch } from '@wordpress/data'; import type { BlockEditorSelect, IBlockHandler } from '../types'; import type { Block } from '@automattic/jetpack-ai-client'; -export function getContent( html ) { +export function getMarkdown( html ) { return renderMarkdownFromHTML( { content: html } ); } @@ -26,6 +26,10 @@ export class HeadingHandler implements IBlockHandler { this.block = getBlock( clientId ); } + public getContent() { + return getMarkdown( getBlockContent( this.block ) ); + } + public onSuggestion( suggestion: string ): void { // Adjust suggestion if it does not start with a hash. if ( ! suggestion.startsWith( '#' ) ) { diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/inline-extensions/types.ts b/projects/plugins/jetpack/extensions/blocks/ai-assistant/inline-extensions/types.ts index a5800c8e936f3..75645e9647ce9 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/inline-extensions/types.ts +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/inline-extensions/types.ts @@ -7,6 +7,7 @@ export type OnSuggestion = ( suggestion: string ) => void; export interface IBlockHandler { onSuggestion: OnSuggestion; + getContent: () => string; } export type BlockEditorSelect = { diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/inline-extensions/with-ai-extension.tsx b/projects/plugins/jetpack/extensions/blocks/ai-assistant/inline-extensions/with-ai-extension.tsx index a56e504aa1f14..2e00607d22414 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/inline-extensions/with-ai-extension.tsx +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/inline-extensions/with-ai-extension.tsx @@ -17,6 +17,7 @@ import React from 'react'; * Internal dependencies */ import { EXTENDED_INLINE_BLOCKS } from '../extensions/ai-assistant'; +import { BuildPromptOptionsProps, buildPromptForExtensions } from '../lib/prompt'; import { blockHandler } from './block-handler'; import AiAssistantInput from './components/ai-assistant-input'; import AiAssistantExtensionToolbarDropdown from './components/ai-assistant-toolbar-dropdown'; @@ -24,8 +25,12 @@ import { isPossibleToExtendBlock } from './lib/is-possible-to-extend-block'; /* * Types */ -import type { OnRequestSuggestion } from '../components/ai-assistant-toolbar-dropdown/dropdown-content'; +import type { + AiAssistantDropdownOnChangeOptionsArgProps, + OnRequestSuggestion, +} from '../components/ai-assistant-toolbar-dropdown/dropdown-content'; import type { ExtendedInlineBlockProp } from '../extensions/ai-assistant'; +import type { PromptTypeProp } from '../lib/prompt'; const debug = debugFactory( 'jetpack-ai-assistant:extensions:with-ai-extension' ); @@ -36,7 +41,7 @@ const blockEditWithAiComponents = createHigherOrderComponent( BlockEdit => { const controlRef: React.MutableRefObject< HTMLDivElement | null > = useRef( null ); const controlObserver = useRef< ResizeObserver | null >( null ); const blockStyle = useRef< string >( '' ); - const [ block, setBlock ] = useState< HTMLElement | null >( null ); + const [ action, setAction ] = useState< string >( '' ); // Only extend the allowed block types. const possibleToExtendBlock = isPossibleToExtendBlock( { @@ -68,9 +73,16 @@ const blockEditWithAiComponents = createHigherOrderComponent( BlockEdit => { ); // Data and functions with block-specific implementations. - const { onSuggestion } = blockHandler( blockName, clientId ); - - const { request, stopSuggestion, requestingState, error, suggestion } = useAiSuggestions( { + const { onSuggestion, getContent } = blockHandler( blockName, clientId ); + + const { + request, + stopSuggestion, + requestingState, + error, + suggestion, + reset: resetSuggestions, + } = useAiSuggestions( { onSuggestion, onDone, onError, @@ -80,22 +92,11 @@ const blockEditWithAiComponents = createHigherOrderComponent( BlockEdit => { }, } ); - // Close the AI Control if the block is deselected. - useEffect( () => { - if ( ! isSelected ) { - setShowAiControl( false ); - // TODO: reset all extension data. - } - }, [ isSelected ] ); - const { id } = useBlockProps(); useEffect( () => { - // Keep the block reference. - setBlock( document.getElementById( id ) ); - }, [ id ] ); + const block = document.getElementById( id ); - useEffect( () => { if ( ! block ) { return; } @@ -123,7 +124,7 @@ const blockEditWithAiComponents = createHigherOrderComponent( BlockEdit => { controlObserver.current.disconnect(); controlObserver.current = null; } - }, [ block, clientId, controlObserver, id, showAiControl ] ); + }, [ clientId, controlObserver, id, showAiControl ] ); // Only extend the target block. if ( ! possibleToExtendBlock ) { @@ -139,16 +140,66 @@ const blockEditWithAiComponents = createHigherOrderComponent( BlockEdit => { setShowAiControl( true ); }; - const onRequestSuggestion: OnRequestSuggestion = ( promptType, options ) => { + const getRequestMessages = ( { + promptType, + options, + userPrompt, + }: { + promptType: PromptTypeProp; + options?: AiAssistantDropdownOnChangeOptionsArgProps; + userPrompt?: string; + } ) => { + const blockContent = getContent(); + + const promptOptions: BuildPromptOptionsProps = { + tone: options?.tone, + language: options?.language, + fromExtension: true, + }; + + return buildPromptForExtensions( { + blockContent, + options: promptOptions, + type: promptType, + userPrompt, + } ); + }; + + const onRequestSuggestion: OnRequestSuggestion = ( promptType, options, humanText ) => { setShowAiControl( true ); - // TODO: handle the promptType and options to request the suggestion. + + if ( humanText ) { + setAction( humanText ); + } + + const messages = getRequestMessages( { promptType, options } ); + debug( 'onRequestSuggestion', promptType, options ); + + request( messages ); }; - const onClose = () => { + const onClose = useCallback( () => { setShowAiControl( false ); + resetSuggestions(); + setAction( '' ); + }, [ resetSuggestions ] ); + + const onUserRequest = ( userPrompt: string ) => { + const promptType = 'userPrompt'; + const options = {}; + const messages = getRequestMessages( { promptType, options, userPrompt } ); + + request( messages ); }; + // Close the AI Control if the block is deselected. + useEffect( () => { + if ( ! isSelected ) { + onClose(); + } + }, [ isSelected, onClose ] ); + const onUndo = () => { // TODO: handle the undo action. debug( 'onUndo' ); @@ -166,7 +217,8 @@ const blockEditWithAiComponents = createHigherOrderComponent( BlockEdit => { requestingError={ error } suggestion={ suggestion } wrapperRef={ controlRef } - request={ request } + action={ action } + request={ onUserRequest } stopSuggestion={ stopSuggestion } close={ onClose } undo={ onUndo } diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/prompt/backend-prompt.ts b/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/prompt/backend-prompt.ts index 20266d0c9f082..96f8b76d08aee 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/prompt/backend-prompt.ts +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/prompt/backend-prompt.ts @@ -50,7 +50,7 @@ export function buildInitialMessageForBackendPrompt( promptType: PromptTypeProp * @param {string} relevantContent - The relevant content. * @returns {PromptItemProps} The initial message. */ -function buildRelevantContentMessageForBackendPrompt( +export function buildRelevantContentMessageForBackendPrompt( isContentGenerated?: boolean, relevantContent?: string | null ): PromptItemProps | null { @@ -172,7 +172,7 @@ function getSubject( * @param {BuildPromptProps} options - The prompt options. * @returns {object} The context. */ -function buildMessageContextForUserPrompt( { +export function buildMessageContextForUserPrompt( { options, type, userPrompt, diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/prompt/index.ts b/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/prompt/index.ts index a966f4f6f622b..fdaba3a24908e 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/prompt/index.ts +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/prompt/index.ts @@ -4,7 +4,9 @@ import { ToneProp } from '../../components/tone-dropdown-control'; import { buildInitialMessageForBackendPrompt, + buildMessageContextForUserPrompt, buildMessagesForBackendPrompt, + buildRelevantContentMessageForBackendPrompt, } from './backend-prompt'; /** * Types & consts @@ -108,7 +110,7 @@ export type BuildPromptOptionsProps = { }; export type BuildPromptProps = { - generatedContent: string; + generatedContent?: string; allPostContent?: string; postContentAbove?: string; currentPostTitle?: string; @@ -153,3 +155,44 @@ export function buildPromptForBlock( { return [ initialMessage, ...userMessages ]; } + +export type BuildExtensionPromptProps = { + blockContent: string; + options: BuildPromptOptionsProps; + type: PromptTypeProp; + userPrompt?: string; +}; + +/** + * Builds a prompt based on the type of prompt. + * Meant for use by the extensions. + * + * @param {BuildPromptProps} options - The prompt options. + * @returns {Array< PromptItemProps >} The prompt. + * @throws {Error} If the type is not recognized. + */ +export function buildPromptForExtensions( { + blockContent, + options, + type, + userPrompt, +}: BuildExtensionPromptProps ): Array< PromptItemProps > { + const messages = [ buildInitialMessageForBackendPrompt( type ) ]; + + const relevantContentMessage = buildRelevantContentMessageForBackendPrompt( false, blockContent ); + + if ( relevantContentMessage ) { + messages.push( relevantContentMessage ); + } + + messages.push( { + role: 'jetpack-ai' as const, + context: buildMessageContextForUserPrompt( { + options, + type, + userPrompt, + } ), + } ); + + return messages; +} diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/utils/capitalize.ts b/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/utils/capitalize.ts new file mode 100644 index 0000000000000..d6bcdb9145fd8 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/utils/capitalize.ts @@ -0,0 +1,7 @@ +export function capitalize( text: string ) { + if ( ! text || typeof text !== 'string' ) { + return ''; + } + + return text.charAt( 0 ).toUpperCase() + text.slice( 1 ); +}