-
Notifications
You must be signed in to change notification settings - Fork 801
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
AI Client: Separate AIControl UI from block logic and update messages (…
…#36967) * add ErrorMessage component and Message stories * fix custom footer condition * separate AIControl component into two for separate UI and logic concerns * changelog * change useState import * fix story mapping * add BlockAIControl story * add storybook preview api to ai client * fix style * rename send handler * update story with useArgs * add ExtensionAIControl component * remove noop functions * fix discard issue * rename bannerComponent and errorComponent * rename debug call
- Loading branch information
Showing
21 changed files
with
1,299 additions
and
522 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
4 changes: 4 additions & 0 deletions
4
projects/js-packages/ai-client/changelog/update-jetpack-ai-input-ui
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Significance: patch | ||
Type: changed | ||
|
||
AI Client: Separate AIControl UI from block logic |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
projects/js-packages/ai-client/src/components/ai-control/ai-control.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/** | ||
* 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; | ||
banner?: ReactElement; | ||
error?: ReactElement; | ||
actions?: ReactElement; | ||
message?: ReactElement; | ||
promptUserInputRef?: React.MutableRefObject< HTMLInputElement >; | ||
}; | ||
|
||
/** | ||
* 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, | ||
banner = null, | ||
error = null, | ||
actions = null, | ||
message = null, | ||
promptUserInputRef = null, | ||
}: AIControlProps ): ReactElement { | ||
return ( | ||
<div className="jetpack-components-ai-control__container-wrapper"> | ||
{ error } | ||
<div className="jetpack-components-ai-control__container"> | ||
{ banner } | ||
<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={ onChange } | ||
placeholder={ placeholder } | ||
className="jetpack-components-ai-control__input" | ||
disabled={ disabled } | ||
ref={ promptUserInputRef } | ||
/> | ||
</div> | ||
{ actions } | ||
</div> | ||
{ message } | ||
</div> | ||
</div> | ||
); | ||
} |
278 changes: 278 additions & 0 deletions
278
projects/js-packages/ai-client/src/components/ai-control/block-ai-control.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,278 @@ | ||
/** | ||
* 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, | ||
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; | ||
banner?: ReactElement; | ||
error?: ReactElement; | ||
}; | ||
|
||
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. | ||
* | ||
* @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, | ||
onSend, | ||
onStop, | ||
onAccept, | ||
onDiscard, | ||
showRemove = false, | ||
banner = null, | ||
error = null, | ||
}: BlockAIControlProps, | ||
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 ); | ||
}, [ 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(); | ||
sendHandler(); | ||
}, | ||
{ | ||
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={ sendHandler } | ||
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 } | ||
banner={ banner } | ||
error={ error } | ||
actions={ actions } | ||
message={ message } | ||
promptUserInputRef={ promptUserInputRef } | ||
/> | ||
); | ||
} | ||
|
||
export default forwardRef( BlockAIControl ); |
Oops, something went wrong.