diff --git a/projects/js-packages/ai-client/changelog/add-ai-suggestions-actions b/projects/js-packages/ai-client/changelog/add-ai-suggestions-actions new file mode 100644 index 0000000000000..34aa993ec4f86 --- /dev/null +++ b/projects/js-packages/ai-client/changelog/add-ai-suggestions-actions @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +AI Client: add internal state management to toggle editing last prompt. Add discard handler prop diff --git a/projects/js-packages/ai-client/src/components/ai-control/index.tsx b/projects/js-packages/ai-client/src/components/ai-control/index.tsx index 373a418b5e357..72c092df8bded 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/index.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/index.tsx @@ -4,9 +4,23 @@ import { PlainText } from '@wordpress/block-editor'; import { Button } from '@wordpress/components'; import { useKeyboardShortcut } from '@wordpress/compose'; -import { forwardRef, useImperativeHandle, useRef } from '@wordpress/element'; +import { + forwardRef, + useImperativeHandle, + useRef, + useEffect, + useCallback, +} from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { Icon, closeSmall, check, arrowUp } from '@wordpress/icons'; +import { + Icon, + closeSmall, + check, + arrowUp, + arrowLeft, + trash, + reusableBlock, +} from '@wordpress/icons'; import classNames from 'classnames'; import React from 'react'; /** @@ -60,6 +74,7 @@ export function AIControl( onSend = noop, onStop = noop, onAccept = noop, + onDiscard = noop, }: { disabled?: boolean; value: string; @@ -75,11 +90,43 @@ export function AIControl( onSend?: ( currentValue: string ) => void; onStop?: () => void; onAccept?: () => void; + onDiscard?: () => void; }, ref: React.MutableRefObject< null > // eslint-disable-line @typescript-eslint/ban-types ): React.ReactElement { const promptUserInputRef = useRef( null ); const loading = state === 'requesting' || state === 'suggesting'; + const [ editRequest, setEditRequest ] = React.useState( false ); + const [ lastValue, setLastValue ] = React.useState( '' ); + + useEffect( () => { + if ( editRequest ) { + promptUserInputRef?.current?.focus(); + } + + if ( ! editRequest && lastValue && value !== lastValue ) { + onChange?.( lastValue ); + } + }, [ editRequest, lastValue ] ); + + const sendRequest = useCallback( () => { + setLastValue( value ); + setEditRequest( false ); + onSend?.( value ); + }, [ value ] ); + + const changeHandler = useCallback( + ( newValue: string ) => { + onChange?.( newValue ); + setEditRequest( state !== 'init' && lastValue && lastValue !== newValue ); + }, + [ lastValue, state ] + ); + + const discardHandler = useCallback( () => { + onDiscard?.(); + onAccept?.(); + }, [] ); // Pass the ref to forwardRef. useImperativeHandle( ref, () => promptUserInputRef.current ); @@ -100,7 +147,7 @@ export function AIControl( 'enter', e => { e.preventDefault(); - onSend?.( value ); + sendRequest(); }, { target: promptUserInputRef, @@ -119,7 +166,7 @@ export function AIControl(
</div> - { ! showAccept && value?.length > 0 && ( + { ( ! showAccept || editRequest ) && value?.length > 0 && ( <div className="jetpack-components-ai-control__controls-prompt_button_wrapper"> { ! loading ? ( - <Button - className="jetpack-components-ai-control__controls-prompt_button" - onClick={ () => onSend?.( value ) } - variant="primary" - disabled={ ! value?.length || disabled } - label={ __( 'Send request', 'jetpack-ai-client' ) } - > - { showButtonLabels ? ( - __( 'Generate', 'jetpack-ai-client' ) - ) : ( - <Icon icon={ arrowUp } /> + <> + { editRequest && ( + <Button + className="jetpack-components-ai-control__controls-prompt_button" + onClick={ () => setEditRequest( false ) } + variant="tertiary" + label={ __( 'Cancel', 'jetpack-ai-client' ) } + > + { showButtonLabels ? ( + __( 'Cancel', 'jetpack-ai-client' ) + ) : ( + <Icon icon={ closeSmall } /> + ) } + </Button> ) } - </Button> + + <Button + className="jetpack-components-ai-control__controls-prompt_button" + onClick={ sendRequest } + variant="primary" + disabled={ ! value?.length || disabled } + label={ __( 'Send request', 'jetpack-ai-client' ) } + > + { showButtonLabels ? ( + __( 'Generate', 'jetpack-ai-client' ) + ) : ( + <Icon icon={ arrowUp } /> + ) } + </Button> + </> ) : ( <Button className="jetpack-components-ai-control__controls-prompt_button" @@ -160,8 +224,32 @@ export function AIControl( </div> ) } - { showAccept && ( + { showAccept && ! editRequest && value?.length > 0 && ( <div className="jetpack-components-ai-control__controls-prompt_button_wrapper"> + <Button + className="jetpack-components-ai-control__controls-prompt_button" + label={ __( 'Back to edit', 'jetpack-ai-client' ) } + onClick={ () => setEditRequest( true ) } + tooltipPosition="top" + > + <Icon icon={ arrowLeft } /> + </Button> + <Button + className="jetpack-components-ai-control__controls-prompt_button" + label={ __( 'Discard', 'jetpack-ai-client' ) } + onClick={ discardHandler } + tooltipPosition="top" + > + <Icon icon={ trash } /> + </Button> + <Button + className="jetpack-components-ai-control__controls-prompt_button" + label={ __( 'Regenerate', 'jetpack-ai-client' ) } + onClick={ () => onSend?.( value ) } + tooltipPosition="top" + > + <Icon icon={ reusableBlock } /> + </Button> <Button className="jetpack-components-ai-control__controls-prompt_button" onClick={ onAccept } diff --git a/projects/js-packages/ai-client/src/components/ai-control/style.scss b/projects/js-packages/ai-client/src/components/ai-control/style.scss index ef8c0c9994d1f..81c82d6b6b45a 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/style.scss +++ b/projects/js-packages/ai-client/src/components/ai-control/style.scss @@ -63,6 +63,21 @@ } } +.jetpack-components-ai-control__controls-prompt_button_wrapper { + text-transform: uppercase; + font-size: 11px; + font-weight: 600; + line-height: 16px; + user-select: none; + white-space: nowrap; + display: flex; + align-items: center; + + .components-button.is-small:not(.has-label) { + padding: 0; + } +} + .jetpack-components-ai-control__controls-prompt_button { &:disabled { opacity: 0.6; diff --git a/projects/plugins/jetpack/changelog/add-ai-suggestions-actions b/projects/plugins/jetpack/changelog/add-ai-suggestions-actions new file mode 100644 index 0000000000000..c5a381914b3fb --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-ai-suggestions-actions @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +Pass down tryAgain handler to AI Control diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js b/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js index 5f831e56dfee8..56c60bcb3af6d 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js @@ -335,7 +335,7 @@ export default function AIAssistantEdit( { attributes, setAttributes, clientId, * - Create blocks from HTML code */ const HTML = markdownConverter - .render( attributes.content ) + .render( attributes.content || '' ) // Fix list indentation .replace( /<li>\s+<p>/g, '<li>' ) .replace( /<\/p>\s+<\/li>/g, '</li>' ); @@ -362,7 +362,7 @@ export default function AIAssistantEdit( { attributes, setAttributes, clientId, const handleAcceptTitle = () => { if ( isInBlockEditor ) { - editPost( { title: attributes.content.trim() } ); + editPost( { title: attributes.content ? attributes.content.trim() : '' } ); removeBlock( clientId ); } else { handleAcceptContent(); @@ -568,10 +568,11 @@ export default function AIAssistantEdit( { attributes, setAttributes, clientId, onSend={ handleSend } onStop={ handleStopSuggestion } onAccept={ handleAccept } + onDiscard={ handleTryAgain } state={ requestingState } isTransparent={ requireUpgrade || ! connected } showButtonLabels={ ! isMobileViewport } - showAccept={ contentIsLoaded && ! isWaitingState } + showAccept={ requestingState !== 'init' && contentIsLoaded && ! isWaitingState } acceptLabel={ acceptLabel } showGuideLine={ contentIsLoaded } />