From 33dcc73efcaadfd6ecf5e229779e2eb02ad897b5 Mon Sep 17 00:00:00 2001 From: Douglas Date: Wed, 17 Apr 2024 15:57:49 -0300 Subject: [PATCH 01/16] add ErrorMessage component and Message stories --- .../src/components/ai-control/index.tsx | 4 +- .../src/components/ai-control/message.tsx | 118 ------------- .../src/components/ai-control/style.scss | 36 ---- .../ai-client/src/components/index.ts | 3 +- .../src/components/message/index.tsx | 157 ++++++++++++++++++ .../message/stories/index.stories.tsx | 68 ++++++++ .../src/components/message/style.scss | 83 +++++++++ .../ai-client/src/icons/error-exclamation.tsx | 18 ++ 8 files changed, 330 insertions(+), 157 deletions(-) delete mode 100644 projects/js-packages/ai-client/src/components/ai-control/message.tsx create mode 100644 projects/js-packages/ai-client/src/components/message/index.tsx create mode 100644 projects/js-packages/ai-client/src/components/message/stories/index.stories.tsx create mode 100644 projects/js-packages/ai-client/src/components/message/style.scss create mode 100644 projects/js-packages/ai-client/src/icons/error-exclamation.tsx 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 0f99c97f84e92..f2688ba2ea5a0 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 @@ -13,9 +13,9 @@ import React, { forwardRef } from 'react'; /** * Internal dependencies */ -import './style.scss'; import AiStatusIndicator from '../ai-status-indicator/index.js'; -import { GuidelineMessage } from './message.js'; +import { GuidelineMessage } from '../message/index.js'; +import './style.scss'; /** * Types */ diff --git a/projects/js-packages/ai-client/src/components/ai-control/message.tsx b/projects/js-packages/ai-client/src/components/ai-control/message.tsx deleted file mode 100644 index cd291e58fbd11..0000000000000 --- a/projects/js-packages/ai-client/src/components/ai-control/message.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/** - * External dependencies - */ -import { ExternalLink, Button } from '@wordpress/components'; -import { createInterpolateElement } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; -import { - Icon, - warning, - info, - cancelCircleFilled as error, - check as success, -} from '@wordpress/icons'; -/** - * Types - */ -import type React from 'react'; - -import './style.scss'; - -export const MESSAGE_SEVERITY_WARNING = 'warning'; -export const MESSAGE_SEVERITY_ERROR = 'error'; -export const MESSAGE_SEVERITY_SUCCESS = 'success'; -export const MESSAGE_SEVERITY_INFO = 'info'; - -const messageSeverityTypes = [ - MESSAGE_SEVERITY_WARNING, - MESSAGE_SEVERITY_ERROR, - MESSAGE_SEVERITY_SUCCESS, - MESSAGE_SEVERITY_INFO, -] as const; - -export type MessageSeverityProp = ( typeof messageSeverityTypes )[ number ] | null; - -export type MessageProps = { - icon?: React.ReactNode; - children: React.ReactNode; - severity: MessageSeverityProp; -}; - -const messageIconsMap = { - [ MESSAGE_SEVERITY_WARNING ]: warning, - [ MESSAGE_SEVERITY_ERROR ]: error, - [ MESSAGE_SEVERITY_SUCCESS ]: success, - [ MESSAGE_SEVERITY_INFO ]: info, -}; - -/** - * React component to render a block message. - * - * @param {MessageProps} props - Component props. - * @returns {React.ReactElement } Banner component. - */ -export default function Message( { - severity = null, - icon = null, - children, -}: MessageProps ): React.ReactElement { - return ( -
- { ( severity || icon ) && } -
{ children }
-
- ); -} - -/** - * React component to render a guideline message. - * - * @returns {React.ReactElement } - Message component. - */ -export function GuidelineMessage(): React.ReactElement { - return ( - - { createInterpolateElement( - __( - 'AI-generated content could be inaccurate or biased. Learn more', - 'jetpack-ai-client' - ), - { - link: , - } - ) } - - ); -} - -/** - * React component to render a upgrade message. - * - * @param {number} requestsRemaining - Number of requests remaining. - * @returns {React.ReactElement } - Message component. - */ -export function UpgradeMessage( { - requestsRemaining, - onUpgradeClick, -}: { - requestsRemaining: number; - onUpgradeClick: () => void; -} ): React.ReactElement { - return ( - - { createInterpolateElement( - sprintf( - // translators: %1$d: number of requests remaining - __( - 'You have %1$d free requests remaining. Upgrade and avoid interruptions', - 'jetpack-ai-client' - ), - requestsRemaining - ), - { - link: + ) } + + ); +} + +/** + * React component to render a guideline message. + * + * @returns {React.ReactElement } - Message component. + */ +export function GuidelineMessage(): React.ReactElement { + return ( + + + { __( 'AI-generated content could be inaccurate or biased.', 'jetpack-ai-client' ) } + + + { __( 'Learn more', 'jetpack-ai-client' ) } + + + ); +} + +/** + * React component to render an upgrade message for free tier users + * + * @param {number} requestsRemaining - Number of requests remaining. + * @returns {React.ReactElement } - Message component. + */ +export function UpgradeMessage( { + requestsRemaining, + onUpgradeClick, +}: UpgradeMessageProps ): React.ReactElement { + return ( + + + { sprintf( + // translators: %1$d: number of requests remaining + __( 'You have %1$d free requests remaining.', 'jetpack-ai-client' ), + requestsRemaining + ) } + + + + ); +} + +/** + * React component to render an error message + * + * @param {number} requestsRemaining - Number of requests remaining. + * @returns {React.ReactElement } - Message component. + */ +export function ErrorMessage( { error, onTryAgainClick }: ErrorMessageProps ): React.ReactElement { + const errorMessage = error || __( 'Something went wrong', 'jetpack-ai-client' ); + + return ( + + + { sprintf( + // translators: %1$d: A dynamic error message + __( 'Error: %1$s.', 'jetpack-ai-client' ), + errorMessage + ) } + + + + ); +} diff --git a/projects/js-packages/ai-client/src/components/message/stories/index.stories.tsx b/projects/js-packages/ai-client/src/components/message/stories/index.stories.tsx new file mode 100644 index 0000000000000..bba54c0560fd1 --- /dev/null +++ b/projects/js-packages/ai-client/src/components/message/stories/index.stories.tsx @@ -0,0 +1,68 @@ +/** + * External dependencies + */ +import { action } from '@storybook/addon-actions'; +import React from 'react'; +/** + * Internal dependencies + */ +import Message, { GuidelineMessage, UpgradeMessage, ErrorMessage } from '../index.js'; + +export default { + title: 'JS Packages/AI Client/Message', + component: Message, + decorators: [ + Story => ( +
+ +
+ ), + ], +}; + +const DefaultTemplate = args => { + return ; +}; + +const DefaultArgs = { + children: Message, +}; + +export const Default = DefaultTemplate.bind( {} ); +Default.args = DefaultArgs; + +const GuidelineTemplate = args => { + return ; +}; + +const GuidelineArgs = {}; + +export const Guideline = GuidelineTemplate.bind( {} ); +Guideline.args = GuidelineArgs; + +const UpgradeTemplate = args => { + return ( + + ); +}; + +const UpgradeArgs = { + requestsRemaining: 10, +}; + +export const Upgrade = UpgradeTemplate.bind( {} ); +Upgrade.args = UpgradeArgs; + +const ErrorTemplate = args => { + return ; +}; + +const ErrorArgs = { + error: 'An error occurred', +}; + +export const Error = ErrorTemplate.bind( {} ); +Error.args = ErrorArgs; diff --git a/projects/js-packages/ai-client/src/components/message/style.scss b/projects/js-packages/ai-client/src/components/message/style.scss new file mode 100644 index 0000000000000..ded7c9e3e6519 --- /dev/null +++ b/projects/js-packages/ai-client/src/components/message/style.scss @@ -0,0 +1,83 @@ +@import '@automattic/jetpack-base-styles/root-variables'; + +.jetpack-ai-assistant__message { + display: flex; + line-height: 28px; + font-size: 12px; + align-self: center; + align-items: center; + padding: 0 12px; + border-radius: 4px; + min-height: 28px; + + > svg { + fill: var( --jp-gray-40 ); + flex-shrink: 0; + } + + .jetpack-ai-assistant__message-content { + flex-grow: 2; + margin: 0 8px; + line-height: 1.4em; + display: flex; + gap: 4px; + align-items: center; + + span { + padding: 5px 0; + } + + .components-external-link { + color: var( --jp-gray-50 ); + } + + // Force padding 0 in link buttons, since default Gutenberg version in WordPress doesn't use iframe and + // Buttons receive styles from edit-post-visual-editor. + .components-button.is-link { + padding: 0; + } + + .components-button.is-link, + .components-external-link { + flex-shrink: 0; + } + } +} + +.jetpack-ai-assistant__message-severity-info { + background-color: var( --jp-gray-0 ); + color: var( --jp-gray-50 ); +} + +.jetpack-ai-assistant__message-severity-warning { + background-color: #FEF8EE; + color: var( --jp-gray-100 ); +} + +.jetpack-ai-assistant__message-severity-error { + background-color: var( --jp-red-0 ); + color: var( --jp-gray-100 ); + + > svg { + fill: var( --jp-red-40, #E65054 ); + } +} + +.jetpack-ai-assistant__message-severity-success { + background-color: var( --jp-green-5 ); + color: var( --jp-gray-100 ); + + > svg { + fill: var( --jp-green-30 ); + } +} + +.jetpack-ai-assistant__message-sidebar { + display: flex; + padding: 0; + height: unset; + + > svg { + fill: var( --jp-gray-50 ); + } +} diff --git a/projects/js-packages/ai-client/src/icons/error-exclamation.tsx b/projects/js-packages/ai-client/src/icons/error-exclamation.tsx new file mode 100644 index 0000000000000..6e241d6722f73 --- /dev/null +++ b/projects/js-packages/ai-client/src/icons/error-exclamation.tsx @@ -0,0 +1,18 @@ +/** + * External dependencies + */ +import { SVG, Path } from '@wordpress/components'; + +const errorExclamation = ( + + + + + +); + +export default errorExclamation; From 8bbca4eba2c19a882281452b8b1878c1b894148b Mon Sep 17 00:00:00 2001 From: Douglas Date: Wed, 17 Apr 2024 18:16:36 -0300 Subject: [PATCH 02/16] fix custom footer condition --- projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js b/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js index d584fe05c7756..204e151693de0 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js @@ -49,6 +49,7 @@ export default function AIAssistantEdit( { attributes, setAttributes, clientId, requestsCount, requestsLimit, currentTier, + loading: loadingAiFeature, } = useAiFeature(); const requestsRemaining = Math.max( requestsLimit - requestsCount, 0 ); @@ -404,6 +405,7 @@ export default function AIAssistantEdit( { attributes, setAttributes, clientId, customFooter={ // Only show the upgrade message on each 5th request or if it's the first request - and only if the user is on the free plan ( requestsRemaining % 5 === 0 || requestsCount === 1 ) && + ! loadingAiFeature && // Don't show the upgrade message while the feature is loading planType === PLAN_TYPE_FREE ? ( Date: Wed, 17 Apr 2024 20:08:13 -0300 Subject: [PATCH 03/16] separate AIControl component into two for separate UI and logic concerns --- .../src/components/ai-control/ai-control.tsx | 82 +++++ .../ai-control/block-ai-control.tsx | 281 +++++++++++++++++ .../src/components/ai-control/index.tsx | 283 +----------------- .../ai-control/stories/index.stories.tsx | 67 +++-- .../ai-client/src/components/index.ts | 2 +- .../extensions/blocks/ai-assistant/edit.js | 8 +- .../components/ai-assistant-bar/index.tsx | 5 +- 7 files changed, 421 insertions(+), 307 deletions(-) create mode 100644 projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx create mode 100644 projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx diff --git a/projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx b/projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx new file mode 100644 index 0000000000000..32f47ef22bac3 --- /dev/null +++ b/projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx @@ -0,0 +1,82 @@ +/** + * External dependencies + */ +import { PlainText } from '@wordpress/block-editor'; +import classNames from 'classnames'; +import React from 'react'; +/** + * Internal dependencies + */ +import AiStatusIndicator from '../ai-status-indicator/index.js'; +import './style.scss'; +/** + * Types + */ +import type { RequestingStateProp } from '../../types.js'; +import type { ReactElement } from 'react'; + +type AIControlProps = { + disabled?: boolean; + value: string; + placeholder?: string; + isTransparent?: boolean; + state?: RequestingStateProp; + onChange?: ( newValue: string ) => void; + bannerComponent?: ReactElement; + errorComponent?: ReactElement; + actions?: ReactElement; + message?: ReactElement; + promptUserInputRef?: React.MutableRefObject< HTMLInputElement >; +}; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {}; + +/** + * Base AIControl component. Contains the main structure of the control component and slots for banner, error, actions and message. + * + * @param {AIControlProps} props - Component props + * @returns {ReactElement} Rendered component + */ +export default function AIControl( { + disabled = false, + value = '', + placeholder = '', + isTransparent = false, + state = 'init', + onChange = noop, + bannerComponent = null, + errorComponent = null, + actions = null, + message = null, + promptUserInputRef = null, +}: AIControlProps ): ReactElement { + return ( +
+ { errorComponent } +
+ { bannerComponent } +
+ + +
+ + </div> + { actions } + </div> + { message } + </div> + </div> + ); +} diff --git a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx new file mode 100644 index 0000000000000..016ade9109dac --- /dev/null +++ b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx @@ -0,0 +1,281 @@ +/** + * External dependencies + */ +import { Button, ButtonGroup } from '@wordpress/components'; +import { useKeyboardShortcut } from '@wordpress/compose'; +import { useImperativeHandle, useRef, useEffect, useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { + Icon, + closeSmall, + check, + arrowUp, + trash, + reusableBlock as regenerate, +} from '@wordpress/icons'; +import debugFactory from 'debug'; +import React, { forwardRef } from 'react'; +/** + * Internal dependencies + */ +import { GuidelineMessage } from '../message/index.js'; +import AIControl from './ai-control.js'; +import './style.scss'; +/** + * Types + */ +import type { RequestingStateProp } from '../../types.js'; +import type { ReactElement } from 'react'; + +type BlockAIControlProps = { + disabled?: boolean; + value: string; + placeholder?: string; + showAccept?: boolean; + acceptLabel?: string; + showButtonLabels?: boolean; + isTransparent?: boolean; + state?: RequestingStateProp; + showGuideLine?: boolean; + customFooter?: ReactElement; + onChange?: ( newValue: string ) => void; + onSend?: ( currentValue: string ) => void; + onStop?: () => void; + onAccept?: () => void; + onDiscard?: () => void; + showRemove?: boolean; + bannerComponent?: ReactElement; + errorComponent?: ReactElement; +}; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {}; + +const debug = debugFactory( 'jetpack-ai-client:ai-control' ); + +/** + * BlockAIControl component. Used by the AI Assistant block, adding logic and components to the base AIControl component. + * + * @param {BlockAIControlProps} props - Component props + * @param {React.MutableRefObject} ref - Ref to the component + * @returns {ReactElement} Rendered component + */ +export function BlockAIControl( + { + disabled = false, + value = '', + placeholder = '', + showAccept = false, + acceptLabel = __( 'Accept', 'jetpack-ai-client' ), + showButtonLabels = true, + isTransparent = false, + state = 'init', + showGuideLine = false, + customFooter = null, + onChange = noop, + onSend = noop, + onStop = noop, + onAccept = noop, + onDiscard = null, + showRemove = false, + bannerComponent = null, + errorComponent = null, + }: BlockAIControlProps, + ref: React.MutableRefObject< HTMLInputElement > +): ReactElement { + const loading = state === 'requesting' || state === 'suggesting'; + const [ editRequest, setEditRequest ] = React.useState( false ); + const [ lastValue, setLastValue ] = React.useState( value || null ); + const promptUserInputRef = useRef( null ); + + // Pass the ref to forwardRef. + useImperativeHandle( ref, () => promptUserInputRef.current ); + + useEffect( () => { + if ( editRequest ) { + promptUserInputRef?.current?.focus(); + } + }, [ editRequest ] ); + + const sendRequest = useCallback( () => { + setLastValue( value ); + setEditRequest( false ); + onSend?.( value ); + }, [ value ] ); + + const changeHandler = useCallback( + ( newValue: string ) => { + onChange?.( newValue ); + if ( state === 'init' ) { + return; + } + + if ( ! lastValue ) { + // here we're coming from a one-click action + setEditRequest( newValue.length > 0 ); + } else { + // here we're coming from an edit action + setEditRequest( newValue !== lastValue ); + } + }, + [ lastValue, state ] + ); + + const discardHandler = useCallback( () => { + onDiscard?.(); + }, [] ); + + const cancelEdit = useCallback( () => { + debug( 'cancelEdit, revert to last value', lastValue ); + onChange( lastValue || '' ); + setEditRequest( false ); + }, [ lastValue ] ); + + useKeyboardShortcut( + 'mod+enter', + () => { + if ( showAccept ) { + onAccept?.(); + } + }, + { + target: promptUserInputRef, + } + ); + + useKeyboardShortcut( + 'enter', + e => { + e.preventDefault(); + sendRequest(); + }, + { + target: promptUserInputRef, + } + ); + + const actions = ( + <> + { ( ! showAccept || editRequest ) && ( + <div className="jetpack-components-ai-control__controls-prompt_button_wrapper"> + { ! loading ? ( + <> + { editRequest && ( + <Button + className="jetpack-components-ai-control__controls-prompt_button" + onClick={ cancelEdit } + variant="secondary" + label={ __( 'Cancel', 'jetpack-ai-client' ) } + > + { showButtonLabels ? ( + __( 'Cancel', 'jetpack-ai-client' ) + ) : ( + <Icon icon={ closeSmall } /> + ) } + </Button> + ) } + + { showRemove && ! editRequest && ! value?.length && onDiscard && ( + <Button + className="jetpack-components-ai-control__controls-prompt_button" + onClick={ discardHandler } + variant="secondary" + label={ __( 'Cancel', 'jetpack-ai-client' ) } + > + { showButtonLabels ? ( + __( 'Cancel', 'jetpack-ai-client' ) + ) : ( + <Icon icon={ closeSmall } /> + ) } + </Button> + ) } + + { value?.length > 0 && ( + <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" + onClick={ onStop } + variant="secondary" + label={ __( 'Stop request', 'jetpack-ai-client' ) } + > + { showButtonLabels ? ( + __( 'Stop', 'jetpack-ai-client' ) + ) : ( + <Icon icon={ closeSmall } /> + ) } + </Button> + ) } + </div> + ) } + { showAccept && ! editRequest && ( + <div className="jetpack-components-ai-control__controls-prompt_button_wrapper"> + { ( value?.length > 0 || lastValue === null ) && ( + <ButtonGroup> + <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" + disabled={ ! value?.length || value === null || disabled } + > + <Icon icon={ regenerate } /> + </Button> + </ButtonGroup> + ) } + <Button + className="jetpack-components-ai-control__controls-prompt_button" + onClick={ onAccept } + variant="primary" + label={ acceptLabel } + > + { showButtonLabels ? acceptLabel : <Icon icon={ check } /> } + </Button> + </div> + ) } + </> + ); + + const message = + showGuideLine && ! loading && ! editRequest && ( customFooter || <GuidelineMessage /> ); + + return ( + <AIControl + disabled={ disabled || loading } + value={ value } + placeholder={ placeholder } + isTransparent={ isTransparent } + state={ state } + onChange={ changeHandler } + bannerComponent={ bannerComponent } + errorComponent={ errorComponent } + actions={ actions } + message={ message } + promptUserInputRef={ promptUserInputRef } + /> + ); +} + +export default forwardRef( BlockAIControl ); 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 f2688ba2ea5a0..846d6df745d29 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 @@ -1,281 +1,2 @@ -/** - * External dependencies - */ -import { PlainText } from '@wordpress/block-editor'; -import { Button, ButtonGroup } from '@wordpress/components'; -import { useKeyboardShortcut } from '@wordpress/compose'; -import { useImperativeHandle, useRef, useEffect, useCallback } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { Icon, closeSmall, check, arrowUp, trash, reusableBlock } from '@wordpress/icons'; -import classNames from 'classnames'; -import debugFactory from 'debug'; -import React, { forwardRef } from 'react'; -/** - * Internal dependencies - */ -import AiStatusIndicator from '../ai-status-indicator/index.js'; -import { GuidelineMessage } from '../message/index.js'; -import './style.scss'; -/** - * Types - */ -import type { RequestingStateProp } from '../../types.js'; -import type { ReactElement } from 'react'; -type AiControlProps = { - disabled?: boolean; - value: string; - placeholder?: string; - showAccept?: boolean; - acceptLabel?: string; - showButtonLabels?: boolean; - isTransparent?: boolean; - state?: RequestingStateProp; - showGuideLine?: boolean; - customFooter?: ReactElement; - onChange?: ( newValue: string ) => void; - onSend?: ( currentValue: string ) => void; - onStop?: () => void; - onAccept?: () => void; - onDiscard?: () => void; - showRemove?: boolean; - bannerComponent?: ReactElement; - errorComponent?: ReactElement; -}; - -// eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => {}; - -const debug = debugFactory( 'jetpack-ai-client:ai-control' ); - -/** - * AI Control component. - * - * @param {AiControlProps} props - Component props. - * @param {React.MutableRefObject} ref - Ref to the component. - * @returns {ReactElement} Rendered component. - */ -export function AIControl( - { - disabled = false, - value = '', - placeholder = '', - showAccept = false, - acceptLabel = __( 'Accept', 'jetpack-ai-client' ), - showButtonLabels = true, - isTransparent = false, - state = 'init', - showGuideLine = false, - customFooter = null, - onChange = noop, - onSend = noop, - onStop = noop, - onAccept = noop, - onDiscard = null, - showRemove = false, - bannerComponent = null, - errorComponent = null, - }: AiControlProps, - ref: React.MutableRefObject< HTMLInputElement > -): ReactElement { - const promptUserInputRef = useRef( null ); - const loading = state === 'requesting' || state === 'suggesting'; - const [ editRequest, setEditRequest ] = React.useState( false ); - const [ lastValue, setLastValue ] = React.useState( value || null ); - - useEffect( () => { - if ( editRequest ) { - promptUserInputRef?.current?.focus(); - } - }, [ editRequest ] ); - - const sendRequest = useCallback( () => { - setLastValue( value ); - setEditRequest( false ); - onSend?.( value ); - }, [ value ] ); - - const changeHandler = useCallback( - ( newValue: string ) => { - onChange?.( newValue ); - if ( state === 'init' ) { - return; - } - - if ( ! lastValue ) { - // here we're coming from a one-click action - setEditRequest( newValue.length > 0 ); - } else { - // here we're coming from an edit action - setEditRequest( newValue !== lastValue ); - } - }, - [ lastValue, state ] - ); - - const discardHandler = useCallback( () => { - onDiscard?.(); - }, [] ); - - const cancelEdit = useCallback( () => { - debug( 'cancelEdit, revert to last value', lastValue ); - onChange( lastValue || '' ); - setEditRequest( false ); - }, [ lastValue ] ); - - // Pass the ref to forwardRef. - useImperativeHandle( ref, () => promptUserInputRef.current ); - - useKeyboardShortcut( - 'mod+enter', - () => { - if ( showAccept ) { - onAccept?.(); - } - }, - { - target: promptUserInputRef, - } - ); - - useKeyboardShortcut( - 'enter', - e => { - e.preventDefault(); - sendRequest(); - }, - { - target: promptUserInputRef, - } - ); - - return ( - <div className="jetpack-components-ai-control__container-wrapper"> - { errorComponent } - <div className="jetpack-components-ai-control__container"> - { bannerComponent } - <div - className={ classNames( 'jetpack-components-ai-control__wrapper', { - 'is-transparent': isTransparent, - } ) } - > - <AiStatusIndicator state={ state } /> - - <div className="jetpack-components-ai-control__input-wrapper"> - <PlainText - value={ value } - onChange={ changeHandler } - placeholder={ placeholder } - className="jetpack-components-ai-control__input" - disabled={ loading || disabled } - ref={ promptUserInputRef } - /> - </div> - - { ( ! showAccept || editRequest ) && ( - <div className="jetpack-components-ai-control__controls-prompt_button_wrapper"> - { ! loading ? ( - <> - { editRequest && ( - <Button - className="jetpack-components-ai-control__controls-prompt_button" - onClick={ cancelEdit } - variant="secondary" - label={ __( 'Cancel', 'jetpack-ai-client' ) } - > - { showButtonLabels ? ( - __( 'Cancel', 'jetpack-ai-client' ) - ) : ( - <Icon icon={ closeSmall } /> - ) } - </Button> - ) } - - { showRemove && ! editRequest && ! value?.length && onDiscard && ( - <Button - className="jetpack-components-ai-control__controls-prompt_button" - onClick={ discardHandler } - variant="secondary" - label={ __( 'Cancel', 'jetpack-ai-client' ) } - > - { showButtonLabels ? ( - __( 'Cancel', 'jetpack-ai-client' ) - ) : ( - <Icon icon={ closeSmall } /> - ) } - </Button> - ) } - - { value?.length > 0 && ( - <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" - onClick={ onStop } - variant="secondary" - label={ __( 'Stop request', 'jetpack-ai-client' ) } - > - { showButtonLabels ? ( - __( 'Stop', 'jetpack-ai-client' ) - ) : ( - <Icon icon={ closeSmall } /> - ) } - </Button> - ) } - </div> - ) } - - { showAccept && ! editRequest && ( - <div className="jetpack-components-ai-control__controls-prompt_button_wrapper"> - { ( value?.length > 0 || lastValue === null ) && ( - <ButtonGroup> - <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" - disabled={ ! value?.length || value === null || disabled } - > - <Icon icon={ reusableBlock } /> - </Button> - </ButtonGroup> - ) } - <Button - className="jetpack-components-ai-control__controls-prompt_button" - onClick={ onAccept } - variant="primary" - label={ acceptLabel } - > - { showButtonLabels ? acceptLabel : <Icon icon={ check } /> } - </Button> - </div> - ) } - </div> - { showGuideLine && ! loading && ! editRequest && ( customFooter || <GuidelineMessage /> ) } - </div> - </div> - ); -} - -export default forwardRef( AIControl ); +export { default as AIControl } from './ai-control.js'; +export { default as BlockAIControl } from './block-ai-control.js'; diff --git a/projects/js-packages/ai-client/src/components/ai-control/stories/index.stories.tsx b/projects/js-packages/ai-client/src/components/ai-control/stories/index.stories.tsx index 7ff4237aedf06..dc8d8f03d3279 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/stories/index.stories.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/stories/index.stories.tsx @@ -2,23 +2,16 @@ * External dependencies */ import { action } from '@storybook/addon-actions'; +import { Button, Notice } from '@wordpress/components'; import { useState } from '@wordpress/element'; import React from 'react'; /** * Internal dependencies */ -import AIControl from '../index.js'; -/** - * Types - */ -import type { Meta } from '@storybook/react'; - -interface AIControlStoryMeta extends Meta< typeof AIControl > { - title?: string; - component?: React.ReactElement; -} +import { GuidelineMessage, ErrorMessage, UpgradeMessage } from '../../message/index.js'; +import { AIControl } from '../index.js'; -const meta: AIControlStoryMeta = { +export default { title: 'JS Packages/AI Client/AI Control', component: AIControl, decorators: [ @@ -35,13 +28,48 @@ const meta: AIControlStoryMeta = { }, options: [ 'init', 'requesting', 'suggesting', 'done', 'error' ], }, + message: { + control: { + type: 'select', + }, + options: { + None: null, + 'Guideline message': <GuidelineMessage />, + 'Error message': <ErrorMessage onTryAgainClick={ action( 'onTryAgainClick' ) } />, + 'Upgrade message': ( + <UpgradeMessage requestsRemaining={ 10 } onUpgradeClick={ action( 'onUpgradeClick' ) } /> + ), + }, + }, + actions: { + control: { + type: 'select', + }, + options: { + None: null, + 'Accept button': <Button>Accept</Button>, + }, + }, + errorComponent: { + control: { + type: 'select', + }, + options: { + None: null, + 'Error notice': ( + <Notice status="error" isDismissible={ true }> + Error message + </Notice> + ), + }, + }, }, parameters: { controls: { exclude: /on[A-Z].*/, }, }, -} as Meta< typeof AIControl >; +}; const Template = args => { const [ value, setValue ] = useState( '' ); @@ -55,19 +83,16 @@ const Template = args => { }; const DefaultArgs = { + placeholder: 'Placeholder', + disabled: false, isTransparent: false, - placeholder: '', state: 'init', - showButtonLabels: true, - showAccept: false, - acceptLabel: 'Accept', onChange: action( 'onChange' ), - onSend: action( 'onSend' ), - onStop: action( 'onStop' ), - onAccept: action( 'onAccept' ), + message: null, + bannerComponent: null, + errorComponent: null, + actions: null, }; export const Default = Template.bind( {} ); Default.args = DefaultArgs; - -export default meta; diff --git a/projects/js-packages/ai-client/src/components/index.ts b/projects/js-packages/ai-client/src/components/index.ts index 9fe6275ec09df..a8d290f28d05b 100644 --- a/projects/js-packages/ai-client/src/components/index.ts +++ b/projects/js-packages/ai-client/src/components/index.ts @@ -1,4 +1,4 @@ -export { default as AIControl } from './ai-control/index.js'; +export { AIControl, BlockAIControl } from './ai-control/index.js'; export { default as AiStatusIndicator } from './ai-status-indicator/index.js'; export { default as AudioDurationDisplay } from './audio-duration-display/index.js'; export { diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js b/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js index 204e151693de0..c3956c782dfa3 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js @@ -1,7 +1,11 @@ /** * External dependencies */ -import { AIControl, UpgradeMessage, renderHTMLFromMarkdown } from '@automattic/jetpack-ai-client'; +import { + BlockAIControl, + UpgradeMessage, + renderHTMLFromMarkdown, +} from '@automattic/jetpack-ai-client'; import { useAnalytics } from '@automattic/jetpack-shared-extension-utils'; import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; import { rawHandler } from '@wordpress/blocks'; @@ -383,7 +387,7 @@ export default function AIAssistantEdit( { attributes, setAttributes, clientId, isGeneratingTitle={ isGeneratingTitle } /> ) } - <AIControl + <BlockAIControl ref={ aiControlRef } disabled={ requireUpgrade || ! connected } value={ attributes.userPrompt } diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/jetpack-contact-form/components/ai-assistant-bar/index.tsx b/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/jetpack-contact-form/components/ai-assistant-bar/index.tsx index b18a83cb9f75c..39f3c084b2ac2 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/jetpack-contact-form/components/ai-assistant-bar/index.tsx +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/jetpack-contact-form/components/ai-assistant-bar/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { useAiContext, AIControl, ERROR_QUOTA_EXCEEDED } from '@automattic/jetpack-ai-client'; +import { useAiContext, BlockAIControl, ERROR_QUOTA_EXCEEDED } from '@automattic/jetpack-ai-client'; import { useAnalytics } from '@automattic/jetpack-shared-extension-utils'; import { serialize } from '@wordpress/blocks'; import { KeyboardShortcuts } from '@wordpress/components'; @@ -19,6 +19,7 @@ import { import { __ } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import classNames from 'classnames'; +import React from 'react'; /** * Internal dependencies */ @@ -268,7 +269,7 @@ export default function AiAssistantBar( { > { siteRequireUpgrade && <UpgradePrompt placement="jetpack-form-block" /> } { ! connected && <ConnectPrompt /> } - <AIControl + <BlockAIControl ref={ inputRef } disabled={ siteRequireUpgrade || ! connected } value={ inputValue } From e4e27a212af6eaed5041e1093f35090f2526dcc7 Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Wed, 17 Apr 2024 20:10:35 -0300 Subject: [PATCH 04/16] changelog --- .../ai-client/changelog/update-jetpack-ai-input-ui | 4 ++++ projects/plugins/jetpack/changelog/update-jetpack-ai-input-ui | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 projects/js-packages/ai-client/changelog/update-jetpack-ai-input-ui create mode 100644 projects/plugins/jetpack/changelog/update-jetpack-ai-input-ui diff --git a/projects/js-packages/ai-client/changelog/update-jetpack-ai-input-ui b/projects/js-packages/ai-client/changelog/update-jetpack-ai-input-ui new file mode 100644 index 0000000000000..e4e0ade17621f --- /dev/null +++ b/projects/js-packages/ai-client/changelog/update-jetpack-ai-input-ui @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +AI Client: Separate AIControl UI from block logic diff --git a/projects/plugins/jetpack/changelog/update-jetpack-ai-input-ui b/projects/plugins/jetpack/changelog/update-jetpack-ai-input-ui new file mode 100644 index 0000000000000..f70ba1b5bb34d --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-jetpack-ai-input-ui @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +AI Assistant: Update AIControl imports From 64f2557b0c4e983d0f83067587a01fa984a1b31a Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Thu, 18 Apr 2024 13:30:31 -0300 Subject: [PATCH 05/16] change useState import --- .../src/components/ai-control/block-ai-control.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx index 016ade9109dac..c56fe3f639bb4 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx @@ -3,7 +3,7 @@ */ import { Button, ButtonGroup } from '@wordpress/components'; import { useKeyboardShortcut } from '@wordpress/compose'; -import { useImperativeHandle, useRef, useEffect, useCallback } from '@wordpress/element'; +import { useImperativeHandle, useRef, useEffect, useCallback, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { Icon, @@ -84,8 +84,8 @@ export function BlockAIControl( ref: React.MutableRefObject< HTMLInputElement > ): ReactElement { const loading = state === 'requesting' || state === 'suggesting'; - const [ editRequest, setEditRequest ] = React.useState( false ); - const [ lastValue, setLastValue ] = React.useState( value || null ); + const [ editRequest, setEditRequest ] = useState( false ); + const [ lastValue, setLastValue ] = useState( value || null ); const promptUserInputRef = useRef( null ); // Pass the ref to forwardRef. From 8dc1b40e8e4e97fc991836e92462fdae1431fc29 Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Thu, 18 Apr 2024 14:09:18 -0300 Subject: [PATCH 06/16] fix story mapping --- .../components/ai-control/stories/index.stories.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/projects/js-packages/ai-client/src/components/ai-control/stories/index.stories.tsx b/projects/js-packages/ai-client/src/components/ai-control/stories/index.stories.tsx index dc8d8f03d3279..49094e94046af 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/stories/index.stories.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/stories/index.stories.tsx @@ -32,7 +32,8 @@ export default { control: { type: 'select', }, - options: { + options: [ 'None', 'Guideline message', 'Error message', 'Upgrade message' ], + mapping: { None: null, 'Guideline message': <GuidelineMessage />, 'Error message': <ErrorMessage onTryAgainClick={ action( 'onTryAgainClick' ) } />, @@ -45,7 +46,8 @@ export default { control: { type: 'select', }, - options: { + options: [ 'None', 'Accept button' ], + mapping: { None: null, 'Accept button': <Button>Accept</Button>, }, @@ -54,7 +56,8 @@ export default { control: { type: 'select', }, - options: { + options: [ 'None', 'Error notice' ], + mapping: { None: null, 'Error notice': ( <Notice status="error" isDismissible={ true }> @@ -71,7 +74,7 @@ export default { }, }; -const Template = args => { +const DefaultTemplate = args => { const [ value, setValue ] = useState( '' ); const handleChange = ( newValue: string ) => { @@ -94,5 +97,5 @@ const DefaultArgs = { actions: null, }; -export const Default = Template.bind( {} ); +export const Default = DefaultTemplate.bind( {} ); Default.args = DefaultArgs; From 299946daf8441eebcb15357471198b2517d6640b Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Thu, 18 Apr 2024 14:16:07 -0300 Subject: [PATCH 07/16] add BlockAIControl story --- ...dex.stories.tsx => ai-control.stories.tsx} | 0 .../stories/block-ai-control.stories.tsx | 125 ++++++++++++++++++ 2 files changed, 125 insertions(+) rename projects/js-packages/ai-client/src/components/ai-control/stories/{index.stories.tsx => ai-control.stories.tsx} (100%) create mode 100644 projects/js-packages/ai-client/src/components/ai-control/stories/block-ai-control.stories.tsx diff --git a/projects/js-packages/ai-client/src/components/ai-control/stories/index.stories.tsx b/projects/js-packages/ai-client/src/components/ai-control/stories/ai-control.stories.tsx similarity index 100% rename from projects/js-packages/ai-client/src/components/ai-control/stories/index.stories.tsx rename to projects/js-packages/ai-client/src/components/ai-control/stories/ai-control.stories.tsx diff --git a/projects/js-packages/ai-client/src/components/ai-control/stories/block-ai-control.stories.tsx b/projects/js-packages/ai-client/src/components/ai-control/stories/block-ai-control.stories.tsx new file mode 100644 index 0000000000000..f756c034de3e7 --- /dev/null +++ b/projects/js-packages/ai-client/src/components/ai-control/stories/block-ai-control.stories.tsx @@ -0,0 +1,125 @@ +/** + * External dependencies + */ +import { action } from '@storybook/addon-actions'; +import { Notice } from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import React from 'react'; +/** + * Internal dependencies + */ +import { GuidelineMessage, ErrorMessage, UpgradeMessage } from '../../message/index.js'; +import { BlockAIControl } from '../index.js'; + +export default { + title: 'JS Packages/AI Client/AI Control/Block AI Control', + component: BlockAIControl, + decorators: [ + Story => ( + <div style={ { backgroundColor: 'white' } }> + <Story /> + </div> + ), + ], + argTypes: { + state: { + control: { + type: 'select', + }, + options: [ 'init', 'requesting', 'suggesting', 'done', 'error' ], + }, + errorComponent: { + control: { + type: 'select', + }, + options: [ 'None', 'Error notice' ], + mapping: { + None: null, + 'Error notice': ( + <Notice status="error" isDismissible={ true }> + Error message + </Notice> + ), + }, + }, + customFooter: { + control: { + type: 'select', + }, + options: [ 'None', 'Guideline message', 'Error message', 'Upgrade message' ], + mapping: { + None: null, + 'Guideline message': <GuidelineMessage />, + 'Error message': <ErrorMessage onTryAgainClick={ action( 'onTryAgainClick' ) } />, + 'Upgrade message': ( + <UpgradeMessage requestsRemaining={ 10 } onUpgradeClick={ action( 'onUpgradeClick' ) } /> + ), + }, + }, + }, + parameters: { + controls: { + exclude: /on[A-Z].*/, + }, + }, +}; + +const DefaultTemplate = args => { + const [ value, setValue ] = useState( '' ); + + const handleChange = ( newValue: string ) => { + setValue( newValue ); + args?.onChange?.( newValue ); + }; + + const handleSend = () => { + args?.onSend?.( value ); + }; + + const handleStop = () => { + args?.onStop?.(); + }; + + const handleAccept = () => { + args?.onAccept?.(); + }; + + const handleDiscard = () => { + args?.onDiscard?.(); + }; + + return ( + <BlockAIControl + { ...args } + onChange={ handleChange } + onSend={ handleSend } + onStop={ handleStop } + onAccept={ handleAccept } + onDiscard={ handleDiscard } + value={ args?.value ?? value } + /> + ); +}; + +const DefaultArgs = { + placeholder: 'Placeholder', + acceptLabel: 'Accept', + showButtonLabels: true, + disabled: false, + isTransparent: false, + state: 'init', + showAccept: true, + showGuideLine: true, + customFooter: null, + onChange: action( 'onChange' ), + onSend: action( 'onSend' ), + onStop: action( 'onStop' ), + onAccept: action( 'onAccept' ), + onDiscard: action( 'onDiscard' ), + showRemove: false, + bannerComponent: null, + errorComponent: null, +}; + +export const Default = DefaultTemplate.bind( {} ); +Default.args = DefaultArgs; From d513a90aa8a27e142886f7d1f73d8247aa352662 Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Thu, 18 Apr 2024 17:36:33 -0300 Subject: [PATCH 08/16] add storybook preview api to ai client --- pnpm-lock.yaml | 3 +++ projects/js-packages/ai-client/package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f3cdb54bb358..516aec549b80d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,9 @@ importers: '@storybook/blocks': specifier: 8.0.6 version: 8.0.6(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': + specifier: 8.0.6 + version: 8.0.6 '@storybook/react': specifier: 8.0.6 version: 8.0.6(react-dom@18.2.0)(react@18.2.0)(typescript@5.0.4) diff --git a/projects/js-packages/ai-client/package.json b/projects/js-packages/ai-client/package.json index 5b2f30d96574c..6f6f2a23af325 100644 --- a/projects/js-packages/ai-client/package.json +++ b/projects/js-packages/ai-client/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@storybook/addon-actions": "8.0.6", "@storybook/blocks": "8.0.6", + "@storybook/preview-api": "8.0.6", "@storybook/react": "8.0.6", "@types/markdown-it": "14.0.0", "@types/turndown": "5.0.4", From fa4eda259f247fd2f0bb5d3a040b6078946f83b8 Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Thu, 18 Apr 2024 17:49:51 -0300 Subject: [PATCH 09/16] fix style --- .../ai-client/src/components/ai-control/style.scss | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 8898e81ff5947..f62d514fad148 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 @@ -70,12 +70,6 @@ } .jetpack-components-ai-control__controls-prompt_button_wrapper { - text-transform: uppercase; - font-size: 11px; - font-weight: 600; - line-height: 1em; - user-select: none; - white-space: nowrap; display: flex; align-items: center; gap: 8px; @@ -88,6 +82,10 @@ box-shadow: none; padding: 6px 8px; } + + .components-button-group { + display: flex; + } } .jetpack-components-ai-control__controls-prompt_button { From 432ed07f38ae19fe6019ef9fd03a1fa200416ecc Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Thu, 18 Apr 2024 17:50:15 -0300 Subject: [PATCH 10/16] rename send handler --- .../src/components/ai-control/block-ai-control.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx index c56fe3f639bb4..a09a2cb70eb89 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx @@ -97,7 +97,7 @@ export function BlockAIControl( } }, [ editRequest ] ); - const sendRequest = useCallback( () => { + const sendHandler = useCallback( () => { setLastValue( value ); setEditRequest( false ); onSend?.( value ); @@ -118,18 +118,18 @@ export function BlockAIControl( setEditRequest( newValue !== lastValue ); } }, - [ lastValue, state ] + [ onChange, lastValue, state ] ); const discardHandler = useCallback( () => { onDiscard?.(); - }, [] ); + }, [ onDiscard ] ); const cancelEdit = useCallback( () => { debug( 'cancelEdit, revert to last value', lastValue ); onChange( lastValue || '' ); setEditRequest( false ); - }, [ lastValue ] ); + }, [ onChange, lastValue ] ); useKeyboardShortcut( 'mod+enter', @@ -147,7 +147,7 @@ export function BlockAIControl( 'enter', e => { e.preventDefault(); - sendRequest(); + sendHandler(); }, { target: promptUserInputRef, @@ -193,7 +193,7 @@ export function BlockAIControl( { value?.length > 0 && ( <Button className="jetpack-components-ai-control__controls-prompt_button" - onClick={ sendRequest } + onClick={ sendHandler } variant="primary" disabled={ ! value?.length || disabled } label={ __( 'Send request', 'jetpack-ai-client' ) } From 77811b0a1c94c6a3b251c47762f36a14f86462dc Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Thu, 18 Apr 2024 17:50:54 -0300 Subject: [PATCH 11/16] update story with useArgs --- .../stories/block-ai-control.stories.tsx | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/projects/js-packages/ai-client/src/components/ai-control/stories/block-ai-control.stories.tsx b/projects/js-packages/ai-client/src/components/ai-control/stories/block-ai-control.stories.tsx index f756c034de3e7..2a46a0cb6598c 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/stories/block-ai-control.stories.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/stories/block-ai-control.stories.tsx @@ -2,8 +2,8 @@ * External dependencies */ import { action } from '@storybook/addon-actions'; +import { useArgs } from '@storybook/preview-api'; import { Notice } from '@wordpress/components'; -import { useState } from '@wordpress/element'; import React from 'react'; /** * Internal dependencies @@ -65,26 +65,39 @@ export default { }; const DefaultTemplate = args => { - const [ value, setValue ] = useState( '' ); + const [ { value }, updateArgs, resetArgs ] = useArgs(); const handleChange = ( newValue: string ) => { - setValue( newValue ); + updateArgs( { value: newValue, showAccept: false } ); args?.onChange?.( newValue ); }; const handleSend = () => { + updateArgs( { state: 'requesting', error: null, showAccept: false } ); + + setTimeout( () => { + updateArgs( { state: 'suggesting' } ); + + setTimeout( () => { + updateArgs( { state: 'done', showAccept: true } ); + }, 3000 ); + }, 1000 ); + args?.onSend?.( value ); }; const handleStop = () => { + updateArgs( { state: 'done', error: null, showAccept: true } ); args?.onStop?.(); }; const handleAccept = () => { + resetArgs(); args?.onAccept?.(); }; const handleDiscard = () => { + resetArgs(); args?.onDiscard?.(); }; @@ -102,13 +115,14 @@ const DefaultTemplate = args => { }; const DefaultArgs = { + value: '', placeholder: 'Placeholder', acceptLabel: 'Accept', showButtonLabels: true, disabled: false, isTransparent: false, state: 'init', - showAccept: true, + showAccept: false, showGuideLine: true, customFooter: null, onChange: action( 'onChange' ), From 884249b12019c6c1af7094fe86e18673ba23386a Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Thu, 18 Apr 2024 17:53:59 -0300 Subject: [PATCH 12/16] add ExtensionAIControl component --- .../ai-control/extension-ai-control.tsx | 220 ++++++++++++++++++ .../src/components/ai-control/index.tsx | 1 + .../stories/extension-ai-control.stories.tsx | 124 ++++++++++ 3 files changed, 345 insertions(+) create mode 100644 projects/js-packages/ai-client/src/components/ai-control/extension-ai-control.tsx create mode 100644 projects/js-packages/ai-client/src/components/ai-control/stories/extension-ai-control.stories.tsx diff --git a/projects/js-packages/ai-client/src/components/ai-control/extension-ai-control.tsx b/projects/js-packages/ai-client/src/components/ai-control/extension-ai-control.tsx new file mode 100644 index 0000000000000..cc5b2c3f95b9c --- /dev/null +++ b/projects/js-packages/ai-client/src/components/ai-control/extension-ai-control.tsx @@ -0,0 +1,220 @@ +/** + * External dependencies + */ +import { Button, ButtonGroup } from '@wordpress/components'; +import { useKeyboardShortcut } from '@wordpress/compose'; +import { useImperativeHandle, useRef, useEffect, useCallback, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { Icon, closeSmall, arrowUp, undo } from '@wordpress/icons'; +import React, { forwardRef } from 'react'; +/** + * Internal dependencies + */ +import { GuidelineMessage, ErrorMessage, UpgradeMessage } from '../message/index.js'; +import AIControl from './ai-control.js'; +import './style.scss'; +/** + * Types + */ +import type { RequestingStateProp } from '../../types.js'; +import type { ReactElement } from 'react'; + +type ExtensionAIControlProps = { + disabled?: boolean; + value: string; + placeholder?: string; + showButtonLabels?: boolean; + isTransparent?: boolean; + state?: RequestingStateProp; + showGuideLine?: boolean; + error?: string; + requestsRemaining?: number; + showUpgradeMessage?: boolean; + onChange?: ( newValue: string ) => void; + onSend?: ( currentValue: string ) => void; + onStop?: () => void; + onClose?: () => void; + onUndo?: () => void; + onUpgrade?: () => void; +}; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {}; + +/** + * ExtensionAIControl component. Used by the AI Assistant inline extensions, adding logic and components to the base AIControl component. + * + * @param {ExtensionAIControlProps} props - Component props + * @param {React.MutableRefObject} ref - Ref to the component + * @returns {ReactElement} Rendered component + */ +export function ExtensionAIControl( + { + disabled = false, + value = '', + placeholder = '', + showButtonLabels = true, + isTransparent = false, + state = 'init', + showGuideLine = false, + error, + requestsRemaining, + showUpgradeMessage = false, + onChange = noop, + onSend = noop, + onStop = noop, + onClose = noop, + onUndo = noop, + onUpgrade = noop, + }: ExtensionAIControlProps, + ref: React.MutableRefObject< HTMLInputElement > +): ReactElement { + const loading = state === 'requesting' || state === 'suggesting'; + const [ editRequest, setEditRequest ] = useState( false ); + const [ lastValue, setLastValue ] = useState( value || null ); + const promptUserInputRef = useRef( null ); + + // Pass the ref to forwardRef. + useImperativeHandle( ref, () => promptUserInputRef.current ); + + useEffect( () => { + if ( editRequest ) { + promptUserInputRef?.current?.focus(); + } + }, [ editRequest ] ); + + const sendHandler = useCallback( () => { + setLastValue( value ); + setEditRequest( false ); + onSend?.( value ); + }, [ onSend, value ] ); + + const changeHandler = useCallback( + ( newValue: string ) => { + onChange?.( newValue ); + if ( state === 'init' ) { + return; + } + + if ( ! lastValue ) { + // here we're coming from a one-click action + setEditRequest( newValue.length > 0 ); + } else { + // here we're coming from an edit action + setEditRequest( newValue !== lastValue ); + } + }, + [ onChange, lastValue, state ] + ); + + const stopHandler = useCallback( () => { + onStop?.(); + }, [ onStop ] ); + + const closeHandler = useCallback( () => { + onClose?.(); + }, [ onClose ] ); + + const undoHandler = useCallback( () => { + onUndo?.(); + }, [ onUndo ] ); + + const upgradeHandler = useCallback( () => { + onUpgrade?.(); + }, [ onUpgrade ] ); + + useKeyboardShortcut( + 'enter', + e => { + e.preventDefault(); + sendHandler(); + }, + { + target: promptUserInputRef, + } + ); + + const actions = ( + <> + { loading ? ( + <Button + className="jetpack-components-ai-control__controls-prompt_button" + onClick={ stopHandler } + variant="secondary" + label={ __( 'Stop request', 'jetpack-ai-client' ) } + > + { showButtonLabels ? __( 'Stop', 'jetpack-ai-client' ) : <Icon icon={ closeSmall } /> } + </Button> + ) : ( + <> + { value?.length > 0 && ( + <div className="jetpack-components-ai-control__controls-prompt_button_wrapper"> + <Button + className="jetpack-components-ai-control__controls-prompt_button" + onClick={ sendHandler } + variant="primary" + disabled={ ! value?.length || disabled } + label={ __( 'Send request', 'jetpack-ai-client' ) } + > + { showButtonLabels ? ( + __( 'Generate', 'jetpack-ai-client' ) + ) : ( + <Icon icon={ arrowUp } /> + ) } + </Button> + </div> + ) } + { value?.length <= 0 && state === 'done' && ( + <div className="jetpack-components-ai-control__controls-prompt_button_wrapper"> + <ButtonGroup> + <Button + className="jetpack-components-ai-control__controls-prompt_button" + label={ __( 'Undo', 'jetpack-ai-client' ) } + onClick={ undoHandler } + tooltipPosition="top" + > + <Icon icon={ undo } /> + </Button> + <Button + className="jetpack-components-ai-control__controls-prompt_button" + label={ __( 'Close', 'jetpack-ai-client' ) } + onClick={ closeHandler } + variant="tertiary" + > + { __( 'Close', 'jetpack-ai-client' ) } + </Button> + </ButtonGroup> + </div> + ) } + </> + ) } + </> + ); + + let message = null; + if ( error ) { + message = <ErrorMessage error={ error } onTryAgainClick={ sendHandler } />; + } else if ( showUpgradeMessage ) { + message = ( + <UpgradeMessage requestsRemaining={ requestsRemaining } onUpgradeClick={ upgradeHandler } /> + ); + } else if ( showGuideLine ) { + message = <GuidelineMessage />; + } + + return ( + <AIControl + disabled={ disabled || loading } + value={ value } + placeholder={ placeholder } + isTransparent={ isTransparent } + state={ state } + onChange={ changeHandler } + actions={ actions } + message={ message } + promptUserInputRef={ promptUserInputRef } + /> + ); +} + +export default forwardRef( ExtensionAIControl ); 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 846d6df745d29..74ac53f165e8a 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 @@ -1,2 +1,3 @@ export { default as AIControl } from './ai-control.js'; export { default as BlockAIControl } from './block-ai-control.js'; +export { default as ExtensionAIControl } from './extension-ai-control.js'; diff --git a/projects/js-packages/ai-client/src/components/ai-control/stories/extension-ai-control.stories.tsx b/projects/js-packages/ai-client/src/components/ai-control/stories/extension-ai-control.stories.tsx new file mode 100644 index 0000000000000..0a67bb6ad0369 --- /dev/null +++ b/projects/js-packages/ai-client/src/components/ai-control/stories/extension-ai-control.stories.tsx @@ -0,0 +1,124 @@ +/** + * External dependencies + */ +import { action } from '@storybook/addon-actions'; +import { useArgs } from '@storybook/preview-api'; +import React from 'react'; +/** + * Internal dependencies + */ +import { ExtensionAIControl } from '../index.js'; + +export default { + title: 'JS Packages/AI Client/AI Control/Extension AI Control', + component: ExtensionAIControl, + decorators: [ + Story => ( + <div style={ { backgroundColor: 'white' } }> + <Story /> + </div> + ), + ], + argTypes: { + state: { + control: { + type: 'select', + }, + options: [ 'init', 'requesting', 'suggesting', 'done', 'error' ], + }, + error: { + control: { + type: 'text', + }, + }, + requestsRemaining: { + control: { + type: 'number', + }, + }, + }, + parameters: { + controls: { + exclude: /on[A-Z].*/, + }, + }, +}; + +const DefaultTemplate = args => { + const [ { value }, updateArgs, resetArgs ] = useArgs(); + + const handleChange = ( newValue: string ) => { + updateArgs( { value: newValue } ); + args?.onChange?.( newValue ); + }; + + const handleSend = () => { + updateArgs( { state: 'requesting', error: null, value: '', placeholder: value } ); + + setTimeout( () => { + updateArgs( { state: 'suggesting' } ); + + setTimeout( () => { + updateArgs( { state: 'done' } ); + }, 3000 ); + }, 1000 ); + + args?.onSend?.( value ); + }; + + const handleStop = () => { + updateArgs( { state: 'done', error: null } ); + args?.onStop?.(); + }; + + const handleClose = () => { + resetArgs(); + args?.onClose?.(); + resetArgs(); + }; + + const handleUndo = () => { + resetArgs(); + args?.onUndo?.(); + resetArgs(); + }; + + const handleUpgrade = () => { + args?.onUpgrade?.(); + }; + + return ( + <ExtensionAIControl + { ...args } + onChange={ handleChange } + onSend={ handleSend } + onStop={ handleStop } + onClose={ handleClose } + onUndo={ handleUndo } + onUpgrade={ handleUpgrade } + value={ args?.value ?? value } + /> + ); +}; + +const DefaultArgs = { + value: '', + placeholder: 'Placeholder', + showButtonLabels: true, + disabled: false, + isTransparent: false, + state: 'init', + showGuideLine: false, + error: null, + requestsRemaining: null, + showUpgradeMessage: false, + onChange: action( 'onChange' ), + onSend: action( 'onSend' ), + onStop: action( 'onStop' ), + onClose: action( 'onClose' ), + onUndo: action( 'onUndo' ), + onUpgrade: action( 'onUpgrade' ), +}; + +export const Default = DefaultTemplate.bind( {} ); +Default.args = DefaultArgs; From a57857c221ed8de273195123a2765ecdb4d91f66 Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Fri, 19 Apr 2024 17:35:24 -0300 Subject: [PATCH 13/16] remove noop functions --- .../src/components/ai-control/ai-control.tsx | 5 +---- .../components/ai-control/block-ai-control.tsx | 15 ++++++--------- .../ai-control/extension-ai-control.tsx | 15 ++++++--------- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx b/projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx index 32f47ef22bac3..3ae346e0cd7bd 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx @@ -29,9 +29,6 @@ type AIControlProps = { promptUserInputRef?: React.MutableRefObject< HTMLInputElement >; }; -// eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => {}; - /** * Base AIControl component. Contains the main structure of the control component and slots for banner, error, actions and message. * @@ -44,7 +41,7 @@ export default function AIControl( { placeholder = '', isTransparent = false, state = 'init', - onChange = noop, + onChange, bannerComponent = null, errorComponent = null, actions = null, diff --git a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx index a09a2cb70eb89..5eb41f64a9b30 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx @@ -48,9 +48,6 @@ type BlockAIControlProps = { errorComponent?: ReactElement; }; -// eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => {}; - const debug = debugFactory( 'jetpack-ai-client:ai-control' ); /** @@ -72,11 +69,11 @@ export function BlockAIControl( state = 'init', showGuideLine = false, customFooter = null, - onChange = noop, - onSend = noop, - onStop = noop, - onAccept = noop, - onDiscard = null, + onChange, + onSend, + onStop, + onAccept, + onDiscard, showRemove = false, bannerComponent = null, errorComponent = null, @@ -127,7 +124,7 @@ export function BlockAIControl( const cancelEdit = useCallback( () => { debug( 'cancelEdit, revert to last value', lastValue ); - onChange( lastValue || '' ); + onChange?.( lastValue || '' ); setEditRequest( false ); }, [ onChange, lastValue ] ); diff --git a/projects/js-packages/ai-client/src/components/ai-control/extension-ai-control.tsx b/projects/js-packages/ai-client/src/components/ai-control/extension-ai-control.tsx index cc5b2c3f95b9c..3389871d052ed 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/extension-ai-control.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/extension-ai-control.tsx @@ -38,9 +38,6 @@ type ExtensionAIControlProps = { onUpgrade?: () => void; }; -// eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => {}; - /** * ExtensionAIControl component. Used by the AI Assistant inline extensions, adding logic and components to the base AIControl component. * @@ -60,12 +57,12 @@ export function ExtensionAIControl( error, requestsRemaining, showUpgradeMessage = false, - onChange = noop, - onSend = noop, - onStop = noop, - onClose = noop, - onUndo = noop, - onUpgrade = noop, + onChange, + onSend, + onStop, + onClose, + onUndo, + onUpgrade, }: ExtensionAIControlProps, ref: React.MutableRefObject< HTMLInputElement > ): ReactElement { From daf8df9b8c868acde85394e73659b7863af89a77 Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Fri, 19 Apr 2024 18:05:37 -0300 Subject: [PATCH 14/16] fix discard issue --- .../src/components/ai-control/block-ai-control.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx index 5eb41f64a9b30..edff85dea0163 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx @@ -115,18 +115,18 @@ export function BlockAIControl( setEditRequest( newValue !== lastValue ); } }, - [ onChange, lastValue, state ] + [ lastValue, state ] ); const discardHandler = useCallback( () => { onDiscard?.(); - }, [ onDiscard ] ); + }, [] ); const cancelEdit = useCallback( () => { debug( 'cancelEdit, revert to last value', lastValue ); onChange?.( lastValue || '' ); setEditRequest( false ); - }, [ onChange, lastValue ] ); + }, [ lastValue ] ); useKeyboardShortcut( 'mod+enter', From 1654e36762c626344956303e903450f398c313db Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Tue, 23 Apr 2024 10:14:25 -0300 Subject: [PATCH 15/16] rename bannerComponent and errorComponent --- .../src/components/ai-control/ai-control.tsx | 12 ++++++------ .../src/components/ai-control/block-ai-control.tsx | 12 ++++++------ .../ai-control/stories/ai-control.stories.tsx | 6 +++--- .../ai-control/stories/block-ai-control.stories.tsx | 6 +++--- .../jetpack/extensions/blocks/ai-assistant/edit.js | 4 ++-- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx b/projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx index 3ae346e0cd7bd..43f660420aa04 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx @@ -22,8 +22,8 @@ type AIControlProps = { isTransparent?: boolean; state?: RequestingStateProp; onChange?: ( newValue: string ) => void; - bannerComponent?: ReactElement; - errorComponent?: ReactElement; + banner?: ReactElement; + error?: ReactElement; actions?: ReactElement; message?: ReactElement; promptUserInputRef?: React.MutableRefObject< HTMLInputElement >; @@ -42,17 +42,17 @@ export default function AIControl( { isTransparent = false, state = 'init', onChange, - bannerComponent = null, - errorComponent = null, + banner = null, + error = null, actions = null, message = null, promptUserInputRef = null, }: AIControlProps ): ReactElement { return ( <div className="jetpack-components-ai-control__container-wrapper"> - { errorComponent } + { error } <div className="jetpack-components-ai-control__container"> - { bannerComponent } + { banner } <div className={ classNames( 'jetpack-components-ai-control__wrapper', { 'is-transparent': isTransparent, diff --git a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx index edff85dea0163..f45868e8ce369 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx @@ -44,8 +44,8 @@ type BlockAIControlProps = { onAccept?: () => void; onDiscard?: () => void; showRemove?: boolean; - bannerComponent?: ReactElement; - errorComponent?: ReactElement; + banner?: ReactElement; + error?: ReactElement; }; const debug = debugFactory( 'jetpack-ai-client:ai-control' ); @@ -75,8 +75,8 @@ export function BlockAIControl( onAccept, onDiscard, showRemove = false, - bannerComponent = null, - errorComponent = null, + banner = null, + error = null, }: BlockAIControlProps, ref: React.MutableRefObject< HTMLInputElement > ): ReactElement { @@ -266,8 +266,8 @@ export function BlockAIControl( isTransparent={ isTransparent } state={ state } onChange={ changeHandler } - bannerComponent={ bannerComponent } - errorComponent={ errorComponent } + banner={ banner } + error={ error } actions={ actions } message={ message } promptUserInputRef={ promptUserInputRef } diff --git a/projects/js-packages/ai-client/src/components/ai-control/stories/ai-control.stories.tsx b/projects/js-packages/ai-client/src/components/ai-control/stories/ai-control.stories.tsx index 49094e94046af..a38d9b51c6b4d 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/stories/ai-control.stories.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/stories/ai-control.stories.tsx @@ -52,7 +52,7 @@ export default { 'Accept button': <Button>Accept</Button>, }, }, - errorComponent: { + error: { control: { type: 'select', }, @@ -92,8 +92,8 @@ const DefaultArgs = { state: 'init', onChange: action( 'onChange' ), message: null, - bannerComponent: null, - errorComponent: null, + banner: null, + error: null, actions: null, }; diff --git a/projects/js-packages/ai-client/src/components/ai-control/stories/block-ai-control.stories.tsx b/projects/js-packages/ai-client/src/components/ai-control/stories/block-ai-control.stories.tsx index 2a46a0cb6598c..ad1dbb3c44cd7 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/stories/block-ai-control.stories.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/stories/block-ai-control.stories.tsx @@ -28,7 +28,7 @@ export default { }, options: [ 'init', 'requesting', 'suggesting', 'done', 'error' ], }, - errorComponent: { + error: { control: { type: 'select', }, @@ -131,8 +131,8 @@ const DefaultArgs = { onAccept: action( 'onAccept' ), onDiscard: action( 'onDiscard' ), showRemove: false, - bannerComponent: null, - errorComponent: null, + banner: null, + error: null, }; export const Default = DefaultTemplate.bind( {} ); diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js b/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js index c3956c782dfa3..fb547ef273fab 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js @@ -404,8 +404,8 @@ export default function AIAssistantEdit( { attributes, setAttributes, clientId, acceptLabel={ acceptLabel } showGuideLine={ contentIsLoaded } showRemove={ attributes?.content?.length > 0 } - bannerComponent={ banner } - errorComponent={ errorNotice } + banner={ banner } + error={ errorNotice } customFooter={ // Only show the upgrade message on each 5th request or if it's the first request - and only if the user is on the free plan ( requestsRemaining % 5 === 0 || requestsCount === 1 ) && From efaf39715d2bbaa01886cc2948fbf599759d45ea Mon Sep 17 00:00:00 2001 From: Douglas <douglas.henri@automattic.com> Date: Tue, 23 Apr 2024 10:15:17 -0300 Subject: [PATCH 16/16] rename debug call --- .../ai-client/src/components/ai-control/block-ai-control.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx index f45868e8ce369..c6d16298e033d 100644 --- a/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx +++ b/projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx @@ -48,7 +48,7 @@ type BlockAIControlProps = { error?: ReactElement; }; -const debug = debugFactory( 'jetpack-ai-client:ai-control' ); +const debug = debugFactory( 'jetpack-ai-client:block-ai-control' ); /** * BlockAIControl component. Used by the AI Assistant block, adding logic and components to the base AIControl component.