@automattic/jetpack-ai-client 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0] - 2023-12-20
9
+ ### Changed
10
+ - AI Client: improved usability with new block positioning, prompt and suggestion action buttons. [#34383]
11
+ - Updated package dependencies. [#34696]
12
+
8
13
  ## [0.2.1] - 2023-12-03
9
14
  ### Changed
10
15
  - Updated the prompt shadow for a better sense of depth. [#34362]
@@ -171,6 +176,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
171
176
  - Updated package dependencies. [#31659]
172
177
  - Updated package dependencies. [#31785]
173
178
 
179
+ [0.3.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.2.1...v0.3.0
174
180
  [0.2.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.2.0...v0.2.1
175
181
  [0.2.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.1.16...v0.2.0
176
182
  [0.1.16]: https://github.com/Automattic/jetpack-ai-client/compare/v0.1.15...v0.1.16
@@ -7,14 +7,7 @@ import './style.scss';
7
7
  * Types
8
8
  */
9
9
  import type { RequestingStateProp } from '../../types';
10
- /**
11
- * AI Control component.
12
- *
13
- * @param {AIControlProps} props - Component props.
14
- * @param {React.MutableRefObject} ref - Ref to the component.
15
- * @returns {React.ReactElement} Rendered component.
16
- */
17
- export declare function AIControl({ disabled, value, placeholder, showAccept, acceptLabel, showButtonLabels, isTransparent, state, showClearButton, showGuideLine, onChange, onSend, onStop, onAccept, }: {
10
+ type AiControlProps = {
18
11
  disabled?: boolean;
19
12
  value: string;
20
13
  placeholder?: string;
@@ -23,27 +16,22 @@ export declare function AIControl({ disabled, value, placeholder, showAccept, ac
23
16
  showButtonLabels?: boolean;
24
17
  isTransparent?: boolean;
25
18
  state?: RequestingStateProp;
26
- showClearButton?: boolean;
27
- showGuideLine?: boolean;
28
- onChange?: (newValue: string) => void;
29
- onSend?: (currentValue: string) => void;
30
- onStop?: () => void;
31
- onAccept?: () => void;
32
- }, ref: React.MutableRefObject<null>): React.ReactElement;
33
- declare const _default: React.ForwardRefExoticComponent<{
34
- disabled?: boolean;
35
- value: string;
36
- placeholder?: string;
37
- showAccept?: boolean;
38
- acceptLabel?: string;
39
- showButtonLabels?: boolean;
40
- isTransparent?: boolean;
41
- state?: "init" | "requesting" | "suggesting" | "done" | "error";
42
- showClearButton?: boolean;
43
19
  showGuideLine?: boolean;
20
+ customFooter?: React.ReactElement;
44
21
  onChange?: (newValue: string) => void;
45
22
  onSend?: (currentValue: string) => void;
46
23
  onStop?: () => void;
47
24
  onAccept?: () => void;
48
- } & React.RefAttributes<null>>;
25
+ onDiscard?: () => void;
26
+ showRemove?: boolean;
27
+ };
28
+ /**
29
+ * AI Control component.
30
+ *
31
+ * @param {AiControlProps} props - Component props.
32
+ * @param {React.MutableRefObject} ref - Ref to the component.
33
+ * @returns {React.ReactElement} Rendered component.
34
+ */
35
+ export declare function AIControl({ disabled, value, placeholder, showAccept, acceptLabel, showButtonLabels, isTransparent, state, showGuideLine, customFooter, onChange, onSend, onStop, onAccept, onDiscard, showRemove, }: AiControlProps, ref: React.MutableRefObject<null>): React.ReactElement;
36
+ declare const _default: React.ForwardRefExoticComponent<AiControlProps & React.RefAttributes<null>>;
49
37
  export default _default;
@@ -1,14 +1,15 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * External dependencies
4
4
  */
5
5
  import { PlainText } from '@wordpress/block-editor';
6
- import { Button } from '@wordpress/components';
6
+ import { Button, ButtonGroup } from '@wordpress/components';
7
7
  import { useKeyboardShortcut } from '@wordpress/compose';
8
- import { forwardRef, useImperativeHandle, useRef } from '@wordpress/element';
8
+ import { forwardRef, useImperativeHandle, useRef, useEffect, useCallback, } from '@wordpress/element';
9
9
  import { __ } from '@wordpress/i18n';
10
- import { Icon, closeSmall, check, arrowUp } from '@wordpress/icons';
10
+ import { Icon, closeSmall, check, arrowUp, trash, reusableBlock } from '@wordpress/icons';
11
11
  import classNames from 'classnames';
12
+ import React from 'react';
12
13
  /**
13
14
  * Internal dependencies
14
15
  */
@@ -20,14 +21,51 @@ const noop = () => { };
20
21
  /**
21
22
  * AI Control component.
22
23
  *
23
- * @param {AIControlProps} props - Component props.
24
+ * @param {AiControlProps} props - Component props.
24
25
  * @param {React.MutableRefObject} ref - Ref to the component.
25
26
  * @returns {React.ReactElement} Rendered component.
26
27
  */
27
- export function AIControl({ disabled = false, value = '', placeholder = '', showAccept = false, acceptLabel = __('Accept', 'jetpack-ai-client'), showButtonLabels = true, isTransparent = false, state = 'init', showClearButton = true, showGuideLine = false, onChange = noop, onSend = noop, onStop = noop, onAccept = noop, }, ref // eslint-disable-line @typescript-eslint/ban-types
28
+ 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, }, ref // eslint-disable-line @typescript-eslint/ban-types
28
29
  ) {
29
30
  const promptUserInputRef = useRef(null);
30
31
  const loading = state === 'requesting' || state === 'suggesting';
32
+ const [editRequest, setEditRequest] = React.useState(false);
33
+ const [lastValue, setLastValue] = React.useState(value || null);
34
+ useEffect(() => {
35
+ if (editRequest) {
36
+ promptUserInputRef?.current?.focus();
37
+ }
38
+ if (!editRequest && lastValue !== null && value !== lastValue) {
39
+ onChange?.(lastValue);
40
+ }
41
+ }, [editRequest, lastValue, value]);
42
+ const sendRequest = useCallback(() => {
43
+ setLastValue(value);
44
+ setEditRequest(false);
45
+ onSend?.(value);
46
+ }, [value]);
47
+ const changeHandler = useCallback((newValue) => {
48
+ onChange?.(newValue);
49
+ if (state === 'init') {
50
+ return;
51
+ }
52
+ if (!lastValue) {
53
+ // here we're coming from a one-click action
54
+ setEditRequest(newValue.length > 0);
55
+ }
56
+ else {
57
+ // here we're coming from an edit action
58
+ setEditRequest(newValue !== lastValue);
59
+ }
60
+ }, [lastValue, state]);
61
+ const discardHandler = useCallback(() => {
62
+ onDiscard?.();
63
+ onAccept?.();
64
+ }, []);
65
+ const cancelEdit = useCallback(() => {
66
+ onChange(lastValue || '');
67
+ setEditRequest(false);
68
+ }, [lastValue]);
31
69
  // Pass the ref to forwardRef.
32
70
  useImperativeHandle(ref, () => promptUserInputRef.current);
33
71
  useKeyboardShortcut('mod+enter', () => {
@@ -39,15 +77,12 @@ export function AIControl({ disabled = false, value = '', placeholder = '', show
39
77
  });
40
78
  useKeyboardShortcut('enter', e => {
41
79
  e.preventDefault();
42
- onSend?.(value);
80
+ sendRequest();
43
81
  }, {
44
82
  target: promptUserInputRef,
45
83
  });
46
- const actionButtonClasses = classNames('jetpack-components-ai-control__controls-prompt_button', {
47
- 'has-label': showButtonLabels,
48
- });
49
- return (_jsxs("div", { className: "jetpack-components-ai-control__container", children: [_jsxs("div", { className: classNames('jetpack-components-ai-control__wrapper', {
50
- 'is-transparent': isTransparent,
51
- }), children: [_jsx(AiStatusIndicator, { state: state }), _jsx("div", { className: "jetpack-components-ai-control__input-wrapper", children: _jsx(PlainText, { value: value, onChange: onChange, placeholder: placeholder, className: "jetpack-components-ai-control__input", disabled: loading || disabled, ref: promptUserInputRef }) }), value?.length > 0 && showClearButton && (_jsx(Button, { icon: closeSmall, className: "jetpack-components-ai-control__clear", onClick: () => onChange('') })), _jsx("div", { className: "jetpack-components-ai-control__controls-prompt_button_wrapper", children: !loading ? (_jsxs(Button, { className: actionButtonClasses, onClick: () => onSend?.(value), isSmall: true, disabled: !value?.length || disabled, label: __('Send request', 'jetpack-ai-client'), children: [_jsx(Icon, { icon: arrowUp }), showButtonLabels && __('Send', 'jetpack-ai-client')] })) : (_jsxs(Button, { className: actionButtonClasses, onClick: onStop, isSmall: true, label: __('Stop request', 'jetpack-ai-client'), children: [_jsx(Icon, { icon: closeSmall }), showButtonLabels && __('Stop (ESC)', 'jetpack-ai-client')] })) }), showAccept && (_jsx("div", { className: "jetpack-components-ai-control__controls-prompt_button_wrapper", children: _jsxs(Button, { className: actionButtonClasses, onClick: onAccept, isSmall: true, label: acceptLabel, children: [_jsx(Icon, { icon: check }), showButtonLabels && acceptLabel] }) }))] }), showGuideLine && _jsx(GuidelineMessage, {})] }));
84
+ return (_jsx("div", { className: "jetpack-components-ai-control__container-wrapper", children: _jsxs("div", { className: "jetpack-components-ai-control__container", children: [_jsxs("div", { className: classNames('jetpack-components-ai-control__wrapper', {
85
+ 'is-transparent': isTransparent,
86
+ }), children: [_jsx(AiStatusIndicator, { state: state }), _jsx("div", { className: "jetpack-components-ai-control__input-wrapper", children: _jsx(PlainText, { value: value, onChange: changeHandler, placeholder: placeholder, className: "jetpack-components-ai-control__input", disabled: loading || disabled, ref: promptUserInputRef }) }), (!showAccept || editRequest) && (_jsx("div", { className: "jetpack-components-ai-control__controls-prompt_button_wrapper", children: !loading ? (_jsxs(_Fragment, { children: [editRequest && (_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: cancelEdit, variant: "secondary", label: __('Cancel', 'jetpack-ai-client'), children: showButtonLabels ? (__('Cancel', 'jetpack-ai-client')) : (_jsx(Icon, { icon: closeSmall })) })), showRemove && !editRequest && !value?.length && onDiscard && (_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: discardHandler, variant: "secondary", label: __('Cancel', 'jetpack-ai-client'), children: showButtonLabels ? (__('Cancel', 'jetpack-ai-client')) : (_jsx(Icon, { icon: closeSmall })) })), value?.length > 0 && (_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: sendRequest, variant: "primary", disabled: !value?.length || disabled, label: __('Send request', 'jetpack-ai-client'), children: showButtonLabels ? (__('Generate', 'jetpack-ai-client')) : (_jsx(Icon, { icon: arrowUp })) }))] })) : (_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: onStop, variant: "secondary", label: __('Stop request', 'jetpack-ai-client'), children: showButtonLabels ? (__('Stop', 'jetpack-ai-client')) : (_jsx(Icon, { icon: closeSmall })) })) })), showAccept && !editRequest && (_jsxs("div", { className: "jetpack-components-ai-control__controls-prompt_button_wrapper", children: [(value?.length > 0 || lastValue === null) && (_jsxs(ButtonGroup, { children: [_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", label: __('Discard', 'jetpack-ai-client'), onClick: discardHandler, tooltipPosition: "top", children: _jsx(Icon, { icon: trash }) }), _jsx(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, children: _jsx(Icon, { icon: reusableBlock }) })] })), _jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: onAccept, variant: "primary", label: acceptLabel, children: showButtonLabels ? acceptLabel : _jsx(Icon, { icon: check }) })] }))] }), showGuideLine && !loading && !editRequest && (customFooter || _jsx(GuidelineMessage, {}))] }) }));
52
87
  }
53
88
  export default forwardRef(AIControl);
@@ -12,4 +12,4 @@ export type AiStatusIndicatorProps = {
12
12
  * @param {AiStatusIndicatorProps} props - component props.
13
13
  * @returns {React.ReactElement} - rendered component.
14
14
  */
15
- export default function AiStatusIndicator({ state, size, }: AiStatusIndicatorProps): React.ReactElement;
15
+ export default function AiStatusIndicator({ state }: AiStatusIndicatorProps): React.ReactElement;
@@ -2,12 +2,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
3
  * External dependencies
4
4
  */
5
- import { Icon } from '@wordpress/components';
5
+ import { Spinner } from '@wordpress/components';
6
6
  import classNames from 'classnames';
7
- /*
8
- * Internal dependencies
9
- */
10
- import { aiAssistantIcon } from '../../icons';
11
7
  import './style.scss';
12
8
  /**
13
9
  * AiStatusIndicator component.
@@ -15,8 +11,8 @@ import './style.scss';
15
11
  * @param {AiStatusIndicatorProps} props - component props.
16
12
  * @returns {React.ReactElement} - rendered component.
17
13
  */
18
- export default function AiStatusIndicator({ state, size = 24, }) {
14
+ export default function AiStatusIndicator({ state }) {
19
15
  return (_jsx("div", { className: classNames('jetpack-ai-status-indicator__icon-wrapper', {
20
16
  [`is-${state}`]: true,
21
- }), children: _jsx(Icon, { icon: aiAssistantIcon, size: size }) }));
17
+ }), children: _jsx(Spinner, {}) }));
22
18
  }
@@ -1,4 +1,4 @@
1
1
  export { default as AIControl } from './ai-control';
2
2
  export { default as AiStatusIndicator } from './ai-status-indicator';
3
3
  export { default as AudioDurationDisplay } from './audio-duration-display';
4
- export { GuidelineMessage } from './ai-control/message';
4
+ export { GuidelineMessage, default as FooterMessage } from './ai-control/message';
@@ -1,4 +1,4 @@
1
1
  export { default as AIControl } from './ai-control';
2
2
  export { default as AiStatusIndicator } from './ai-status-indicator';
3
3
  export { default as AudioDurationDisplay } from './audio-duration-display';
4
- export { GuidelineMessage } from './ai-control/message';
4
+ export { GuidelineMessage, default as FooterMessage } from './ai-control/message';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@automattic/jetpack-ai-client",
4
- "version": "0.2.1",
4
+ "version": "0.3.0",
5
5
  "description": "A JS client for consuming Jetpack AI services",
6
6
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/ai-client/#readme",
7
7
  "bugs": {
@@ -22,9 +22,9 @@
22
22
  },
23
23
  "type": "module",
24
24
  "devDependencies": {
25
- "@storybook/addon-actions": "7.4.6",
26
- "@storybook/blocks": "7.4.6",
27
- "@storybook/react": "7.4.6",
25
+ "@storybook/addon-actions": "7.6.5",
26
+ "@storybook/blocks": "7.6.5",
27
+ "@storybook/react": "7.6.5",
28
28
  "jest": "^29.6.2",
29
29
  "jest-environment-jsdom": "29.7.0",
30
30
  "typescript": "5.0.4"
@@ -39,8 +39,8 @@
39
39
  "types": "./build/index.d.ts",
40
40
  "dependencies": {
41
41
  "@automattic/jetpack-base-styles": "^0.6.14",
42
- "@automattic/jetpack-connection": "^0.30.9",
43
- "@automattic/jetpack-shared-extension-utils": "^0.13.3",
42
+ "@automattic/jetpack-connection": "^0.30.11",
43
+ "@automattic/jetpack-shared-extension-utils": "^0.13.5",
44
44
  "@microsoft/fetch-event-source": "2.0.1",
45
45
  "@types/react": "18.2.33",
46
46
  "@wordpress/api-fetch": "6.44.0",
@@ -11,6 +11,8 @@
11
11
  - `isTransparent` (**boolean**) (Optional): Controls the opacity of the component. Default value is `false`.
12
12
  - `state` (**RequestingStateProp**) (Optional): Determines the state of the component. Default value is `'init'`.
13
13
  - `showClearButton` (**boolean**) (Optional): Determines if the clear button is shown when the input has a value. Default value is `true`.
14
+ - `showGuideLine`: (**boolean**) (Optional): Whether to show the usual AI guidelines at the bottom of the input.
15
+ - `customFooter`: (**ReactElement**) (Optional): if provided together with `showGuideLine` it will be rendered at the bottom of the input.
14
16
  - `onChange` (**Function**) (Optional): Handler for input change. Default action is no operation.
15
17
  - `onSend` (**Function**) (Optional): Handler to send a request. Default action is no operation.
16
18
  - `onStop` (**Function**) (Optional): Handler to stop a request. Default action is no operation.
@@ -19,6 +21,8 @@
19
21
  #### Example Usage
20
22
 
21
23
  ```jsx
24
+ import { AIControl, FooterMessage } from '@automattic/jetpack-ai-client';
25
+
22
26
  <AIControl
23
27
  value="Type here"
24
28
  placeholder="Placeholder text"
@@ -26,5 +30,7 @@
26
30
  onSend={ handleSend }
27
31
  onStop={ handleStop }
28
32
  onAccept={ handleAccept }
33
+ showGuideLine={ true }
34
+ customFooter={ <FooterMessage severity="info">Remember AI suggestions can be inaccurate</FooterMessage> }
29
35
  />
30
- ```
36
+ ```
@@ -2,11 +2,17 @@
2
2
  * External dependencies
3
3
  */
4
4
  import { PlainText } from '@wordpress/block-editor';
5
- import { Button } from '@wordpress/components';
5
+ import { Button, ButtonGroup } from '@wordpress/components';
6
6
  import { useKeyboardShortcut } from '@wordpress/compose';
7
- import { forwardRef, useImperativeHandle, useRef } from '@wordpress/element';
7
+ import {
8
+ forwardRef,
9
+ useImperativeHandle,
10
+ useRef,
11
+ useEffect,
12
+ useCallback,
13
+ } from '@wordpress/element';
8
14
  import { __ } from '@wordpress/i18n';
9
- import { Icon, closeSmall, check, arrowUp } from '@wordpress/icons';
15
+ import { Icon, closeSmall, check, arrowUp, trash, reusableBlock } from '@wordpress/icons';
10
16
  import classNames from 'classnames';
11
17
  import React from 'react';
12
18
  /**
@@ -19,7 +25,7 @@ import { GuidelineMessage } from './message';
19
25
  * Types
20
26
  */
21
27
  import type { RequestingStateProp } from '../../types';
22
- type AIControlProps = {
28
+ type AiControlProps = {
23
29
  disabled?: boolean;
24
30
  value: string;
25
31
  placeholder?: string;
@@ -28,12 +34,14 @@ type AIControlProps = {
28
34
  showButtonLabels?: boolean;
29
35
  isTransparent?: boolean;
30
36
  state?: RequestingStateProp;
31
- showClearButton?: boolean;
32
37
  showGuideLine?: boolean;
38
+ customFooter?: React.ReactElement;
33
39
  onChange?: ( newValue: string ) => void;
34
40
  onSend?: ( currentValue: string ) => void;
35
41
  onStop?: () => void;
36
42
  onAccept?: () => void;
43
+ onDiscard?: () => void;
44
+ showRemove?: boolean;
37
45
  };
38
46
 
39
47
  // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -42,7 +50,7 @@ const noop = () => {};
42
50
  /**
43
51
  * AI Control component.
44
52
  *
45
- * @param {AIControlProps} props - Component props.
53
+ * @param {AiControlProps} props - Component props.
46
54
  * @param {React.MutableRefObject} ref - Ref to the component.
47
55
  * @returns {React.ReactElement} Rendered component.
48
56
  */
@@ -56,32 +64,65 @@ export function AIControl(
56
64
  showButtonLabels = true,
57
65
  isTransparent = false,
58
66
  state = 'init',
59
- showClearButton = true,
60
67
  showGuideLine = false,
68
+ customFooter = null,
61
69
  onChange = noop,
62
70
  onSend = noop,
63
71
  onStop = noop,
64
72
  onAccept = noop,
65
- }: {
66
- disabled?: boolean;
67
- value: string;
68
- placeholder?: string;
69
- showAccept?: boolean;
70
- acceptLabel?: string;
71
- showButtonLabels?: boolean;
72
- isTransparent?: boolean;
73
- state?: RequestingStateProp;
74
- showClearButton?: boolean;
75
- showGuideLine?: boolean;
76
- onChange?: ( newValue: string ) => void;
77
- onSend?: ( currentValue: string ) => void;
78
- onStop?: () => void;
79
- onAccept?: () => void;
80
- },
73
+ onDiscard = null,
74
+ showRemove = false,
75
+ }: AiControlProps,
81
76
  ref: React.MutableRefObject< null > // eslint-disable-line @typescript-eslint/ban-types
82
77
  ): React.ReactElement {
83
78
  const promptUserInputRef = useRef( null );
84
79
  const loading = state === 'requesting' || state === 'suggesting';
80
+ const [ editRequest, setEditRequest ] = React.useState( false );
81
+ const [ lastValue, setLastValue ] = React.useState( value || null );
82
+
83
+ useEffect( () => {
84
+ if ( editRequest ) {
85
+ promptUserInputRef?.current?.focus();
86
+ }
87
+
88
+ if ( ! editRequest && lastValue !== null && value !== lastValue ) {
89
+ onChange?.( lastValue );
90
+ }
91
+ }, [ editRequest, lastValue, value ] );
92
+
93
+ const sendRequest = useCallback( () => {
94
+ setLastValue( value );
95
+ setEditRequest( false );
96
+ onSend?.( value );
97
+ }, [ value ] );
98
+
99
+ const changeHandler = useCallback(
100
+ ( newValue: string ) => {
101
+ onChange?.( newValue );
102
+ if ( state === 'init' ) {
103
+ return;
104
+ }
105
+
106
+ if ( ! lastValue ) {
107
+ // here we're coming from a one-click action
108
+ setEditRequest( newValue.length > 0 );
109
+ } else {
110
+ // here we're coming from an edit action
111
+ setEditRequest( newValue !== lastValue );
112
+ }
113
+ },
114
+ [ lastValue, state ]
115
+ );
116
+
117
+ const discardHandler = useCallback( () => {
118
+ onDiscard?.();
119
+ onAccept?.();
120
+ }, [] );
121
+
122
+ const cancelEdit = useCallback( () => {
123
+ onChange( lastValue || '' );
124
+ setEditRequest( false );
125
+ }, [ lastValue ] );
85
126
 
86
127
  // Pass the ref to forwardRef.
87
128
  useImperativeHandle( ref, () => promptUserInputRef.current );
@@ -102,85 +143,137 @@ export function AIControl(
102
143
  'enter',
103
144
  e => {
104
145
  e.preventDefault();
105
- onSend?.( value );
146
+ sendRequest();
106
147
  },
107
148
  {
108
149
  target: promptUserInputRef,
109
150
  }
110
151
  );
111
152
 
112
- const actionButtonClasses = classNames( 'jetpack-components-ai-control__controls-prompt_button', {
113
- 'has-label': showButtonLabels,
114
- } );
115
-
116
153
  return (
117
- <div className="jetpack-components-ai-control__container">
118
- <div
119
- className={ classNames( 'jetpack-components-ai-control__wrapper', {
120
- 'is-transparent': isTransparent,
121
- } ) }
122
- >
123
- <AiStatusIndicator state={ state } />
124
-
125
- <div className="jetpack-components-ai-control__input-wrapper">
126
- <PlainText
127
- value={ value }
128
- onChange={ onChange }
129
- placeholder={ placeholder }
130
- className="jetpack-components-ai-control__input"
131
- disabled={ loading || disabled }
132
- ref={ promptUserInputRef }
133
- />
134
- </div>
154
+ <div className="jetpack-components-ai-control__container-wrapper">
155
+ <div className="jetpack-components-ai-control__container">
156
+ <div
157
+ className={ classNames( 'jetpack-components-ai-control__wrapper', {
158
+ 'is-transparent': isTransparent,
159
+ } ) }
160
+ >
161
+ <AiStatusIndicator state={ state } />
162
+
163
+ <div className="jetpack-components-ai-control__input-wrapper">
164
+ <PlainText
165
+ value={ value }
166
+ onChange={ changeHandler }
167
+ placeholder={ placeholder }
168
+ className="jetpack-components-ai-control__input"
169
+ disabled={ loading || disabled }
170
+ ref={ promptUserInputRef }
171
+ />
172
+ </div>
173
+
174
+ { ( ! showAccept || editRequest ) && (
175
+ <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
176
+ { ! loading ? (
177
+ <>
178
+ { editRequest && (
179
+ <Button
180
+ className="jetpack-components-ai-control__controls-prompt_button"
181
+ onClick={ cancelEdit }
182
+ variant="secondary"
183
+ label={ __( 'Cancel', 'jetpack-ai-client' ) }
184
+ >
185
+ { showButtonLabels ? (
186
+ __( 'Cancel', 'jetpack-ai-client' )
187
+ ) : (
188
+ <Icon icon={ closeSmall } />
189
+ ) }
190
+ </Button>
191
+ ) }
135
192
 
136
- { value?.length > 0 && showClearButton && (
137
- <Button
138
- icon={ closeSmall }
139
- className="jetpack-components-ai-control__clear"
140
- onClick={ () => onChange( '' ) }
141
- />
142
- ) }
143
-
144
- <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
145
- { ! loading ? (
146
- <Button
147
- className={ actionButtonClasses }
148
- onClick={ () => onSend?.( value ) }
149
- isSmall={ true }
150
- disabled={ ! value?.length || disabled }
151
- label={ __( 'Send request', 'jetpack-ai-client' ) }
152
- >
153
- <Icon icon={ arrowUp } />
154
- { showButtonLabels && __( 'Send', 'jetpack-ai-client' ) }
155
- </Button>
156
- ) : (
157
- <Button
158
- className={ actionButtonClasses }
159
- onClick={ onStop }
160
- isSmall={ true }
161
- label={ __( 'Stop request', 'jetpack-ai-client' ) }
162
- >
163
- <Icon icon={ closeSmall } />
164
- { showButtonLabels && __( 'Stop (ESC)', 'jetpack-ai-client' ) }
165
- </Button>
193
+ { showRemove && ! editRequest && ! value?.length && onDiscard && (
194
+ <Button
195
+ className="jetpack-components-ai-control__controls-prompt_button"
196
+ onClick={ discardHandler }
197
+ variant="secondary"
198
+ label={ __( 'Cancel', 'jetpack-ai-client' ) }
199
+ >
200
+ { showButtonLabels ? (
201
+ __( 'Cancel', 'jetpack-ai-client' )
202
+ ) : (
203
+ <Icon icon={ closeSmall } />
204
+ ) }
205
+ </Button>
206
+ ) }
207
+
208
+ { value?.length > 0 && (
209
+ <Button
210
+ className="jetpack-components-ai-control__controls-prompt_button"
211
+ onClick={ sendRequest }
212
+ variant="primary"
213
+ disabled={ ! value?.length || disabled }
214
+ label={ __( 'Send request', 'jetpack-ai-client' ) }
215
+ >
216
+ { showButtonLabels ? (
217
+ __( 'Generate', 'jetpack-ai-client' )
218
+ ) : (
219
+ <Icon icon={ arrowUp } />
220
+ ) }
221
+ </Button>
222
+ ) }
223
+ </>
224
+ ) : (
225
+ <Button
226
+ className="jetpack-components-ai-control__controls-prompt_button"
227
+ onClick={ onStop }
228
+ variant="secondary"
229
+ label={ __( 'Stop request', 'jetpack-ai-client' ) }
230
+ >
231
+ { showButtonLabels ? (
232
+ __( 'Stop', 'jetpack-ai-client' )
233
+ ) : (
234
+ <Icon icon={ closeSmall } />
235
+ ) }
236
+ </Button>
237
+ ) }
238
+ </div>
166
239
  ) }
167
- </div>
168
240
 
169
- { showAccept && (
170
- <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
171
- <Button
172
- className={ actionButtonClasses }
173
- onClick={ onAccept }
174
- isSmall={ true }
175
- label={ acceptLabel }
176
- >
177
- <Icon icon={ check } />
178
- { showButtonLabels && acceptLabel }
179
- </Button>
180
- </div>
181
- ) }
241
+ { showAccept && ! editRequest && (
242
+ <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
243
+ { ( value?.length > 0 || lastValue === null ) && (
244
+ <ButtonGroup>
245
+ <Button
246
+ className="jetpack-components-ai-control__controls-prompt_button"
247
+ label={ __( 'Discard', 'jetpack-ai-client' ) }
248
+ onClick={ discardHandler }
249
+ tooltipPosition="top"
250
+ >
251
+ <Icon icon={ trash } />
252
+ </Button>
253
+ <Button
254
+ className="jetpack-components-ai-control__controls-prompt_button"
255
+ label={ __( 'Regenerate', 'jetpack-ai-client' ) }
256
+ onClick={ () => onSend?.( value ) }
257
+ tooltipPosition="top"
258
+ disabled={ ! value?.length || value === null || disabled }
259
+ >
260
+ <Icon icon={ reusableBlock } />
261
+ </Button>
262
+ </ButtonGroup>
263
+ ) }
264
+ <Button
265
+ className="jetpack-components-ai-control__controls-prompt_button"
266
+ onClick={ onAccept }
267
+ variant="primary"
268
+ label={ acceptLabel }
269
+ >
270
+ { showButtonLabels ? acceptLabel : <Icon icon={ check } /> }
271
+ </Button>
272
+ </div>
273
+ ) }
274
+ </div>
275
+ { showGuideLine && ! loading && ! editRequest && ( customFooter || <GuidelineMessage /> ) }
182
276
  </div>
183
- { showGuideLine && <GuidelineMessage /> }
184
277
  </div>
185
278
  );
186
279
  }
@@ -2,20 +2,27 @@
2
2
 
3
3
  // AI CONTROL
4
4
 
5
+ .jetpack-components-ai-control__container-wrapper {
6
+ position: sticky;
7
+ bottom: 16px;
8
+ }
9
+
5
10
  .jetpack-components-ai-control__container {
6
11
  color: var( --jp-gray-80 );
7
12
  background-color: var( --jp-white );
8
13
  box-shadow: 0px 12px 15px 0px rgba(0, 0, 0, 0.12), 0px 3px 9px 0px rgba(0, 0, 0, 0.12), 0px 1px 3px 0px rgba(0, 0, 0, 0.15);
9
14
  font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
10
- width: 100%;
15
+ border-radius: 6px;
16
+ border: 1px solid var(--gutenberg-gray-400, #CCC);
11
17
  }
12
18
 
13
19
  .jetpack-components-ai-control__wrapper {
14
20
  display: flex;
15
- padding: 12px 14px;
16
- gap: 8px;
17
- align-items: center;
21
+ padding: 8px 8px 8px var(--grid-unit-15, 12px);
22
+ align-items: flex-end;
18
23
  box-sizing: border-box;
24
+ border-radius: 6px 6px 0 0;
25
+ gap: 6px;
19
26
 
20
27
  &.is-transparent {
21
28
  opacity: 0.4;
@@ -23,10 +30,11 @@
23
30
  }
24
31
 
25
32
  .jetpack-components-ai-control__input-wrapper {
26
- position: relative;
27
33
  display: flex;
28
34
  flex-grow: 1;
29
- width: 100%;
35
+ align-self: center;
36
+ min-height: 36px; // need compat height with buttons wrapper
37
+ align-items: center;
30
38
 
31
39
  textarea.jetpack-components-ai-control__input {
32
40
  background-color: var( --jp-white );
@@ -63,67 +71,28 @@
63
71
  }
64
72
  }
65
73
 
66
- .jetpack-components-ai-control__clear.components-button.has-icon {
67
- width: 24px;
68
- min-width: 24px;
69
- height: 24px;
70
- padding: 0;
71
- border-radius: 50%;
72
- background-color: black;
73
- opacity: 0.2;
74
- color: white;
75
-
76
- &:hover {
77
- opacity: 0.4;
78
- }
79
- }
80
-
81
- .jetpack-components-ai-controlton__icon {
82
- flex-shrink: 0;
83
- display: flex;
84
- align-items: center;
85
- justify-content: center;
86
-
87
- .input-icon {
88
- // Brand green regardless of theme
89
- color: var( --jp-green-40 );
90
- }
91
-
92
- .input-spinner {
93
- margin: 0;
94
- width: 24px;
95
- height: 24px;
96
-
97
- path {
98
- color: var( --jp-green-40 );
99
- }
100
- }
101
- }
102
-
103
74
  .jetpack-components-ai-control__controls-prompt_button_wrapper {
104
75
  text-transform: uppercase;
105
76
  font-size: 11px;
106
77
  font-weight: 600;
107
- line-height: 16px;
78
+ line-height: 1em;
108
79
  user-select: none;
109
80
  white-space: nowrap;
110
81
  display: flex;
111
82
  align-items: center;
83
+ gap: 8px;
112
84
 
113
85
  .components-button.is-small:not(.has-label) {
114
86
  padding: 0;
115
87
  }
116
- }
117
88
 
118
- .jetpack-components-ai-control__controls-prompt_button {
119
- color: var( --jp-gray-80 );
120
- text-transform: uppercase;
121
-
122
- &:hover,
123
- &:active {
124
- color: var( --wp-components-color-accent, var( --wp-admin-theme-color ) );
89
+ .components-button-group .components-button {
90
+ box-shadow: none;
91
+ padding: 6px 8px;
125
92
  }
93
+ }
126
94
 
95
+ .jetpack-components-ai-control__controls-prompt_button {
127
96
  &:disabled {
128
97
  opacity: 0.6;
129
98
  cursor: not-allowed;
@@ -134,24 +103,26 @@
134
103
 
135
104
  .jetpack-ai-assistant__message {
136
105
  display: flex;
137
- line-height: 28px;
138
- font-size: 12px;
139
- align-self: center;
140
- align-items: center;
141
- background-color: var( --jp-white-off );
142
- padding: 0 12px;
143
-
144
- > svg {
145
- fill: var( --jp-gray-40 );
146
- }
147
-
148
- .jetpack-ai-assistant__message-content {
149
- flex-grow: 2;
150
- margin: 0 8px;
151
- color: var( --jp-gray-70 );
152
-
153
- .components-external-link {
154
- color: var( --jp-gray-50 );
155
- }
156
- }
106
+ line-height: 28px;
107
+ font-size: 12px;
108
+ align-self: center;
109
+ align-items: center;
110
+ background-color: var( --jp-white-off );
111
+ padding: 0 12px;
112
+ border-radius: 0 0 6px 6px;
113
+
114
+ > svg {
115
+ fill: var( --jp-gray-40 );
116
+ }
117
+
118
+ .jetpack-ai-assistant__message-content {
119
+ flex-grow: 2;
120
+ margin: 0 8px;
121
+ color: var( --jp-gray-50 );
122
+ line-height: 1.4em;
123
+
124
+ .components-external-link {
125
+ color: var( --jp-gray-50 );
126
+ }
127
+ }
157
128
  }
@@ -1,12 +1,8 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { Icon } from '@wordpress/components';
4
+ import { Spinner } from '@wordpress/components';
5
5
  import classNames from 'classnames';
6
- /*
7
- * Internal dependencies
8
- */
9
- import { aiAssistantIcon } from '../../icons';
10
6
  /*
11
7
  * Types
12
8
  */
@@ -27,17 +23,14 @@ export type AiStatusIndicatorProps = {
27
23
  * @param {AiStatusIndicatorProps} props - component props.
28
24
  * @returns {React.ReactElement} - rendered component.
29
25
  */
30
- export default function AiStatusIndicator( {
31
- state,
32
- size = 24,
33
- }: AiStatusIndicatorProps ): React.ReactElement {
26
+ export default function AiStatusIndicator( { state }: AiStatusIndicatorProps ): React.ReactElement {
34
27
  return (
35
28
  <div
36
29
  className={ classNames( 'jetpack-ai-status-indicator__icon-wrapper', {
37
30
  [ `is-${ state }` ]: true,
38
31
  } ) }
39
32
  >
40
- <Icon icon={ aiAssistantIcon } size={ size } />
33
+ <Spinner />
41
34
  </div>
42
35
  );
43
36
  }
@@ -1,52 +1,19 @@
1
1
  @import '@automattic/jetpack-base-styles/style';
2
2
 
3
3
  .jetpack-ai-status-indicator__icon-wrapper {
4
- color: var( --jp-green-60 );
5
- height: 24px;
6
-
7
- &.is-init {
8
- color: var( --jp-green-60 );
9
- }
10
-
11
- &.is-requesting {
12
- color: var( --jp-green-40 );
13
- animation: fade 800ms linear infinite;
14
- }
15
-
16
- &.is-suggesting {
17
- color: var( --jp-green-30 );
18
- .ai-assistant-icon > path {
19
- animation: fadingSpark 300ms ease-in-out alternate infinite;
20
-
21
- &.spark-first {
22
- animation-delay: 0.2s;
23
- }
24
-
25
- &.spark-second {
26
- animation-delay: 0.6s;
27
- }
28
- }
29
- }
30
-
31
- &.is-done {
32
- color: var( --jp-green-40 );
33
- }
34
-
35
- &.is-error {
36
- color: var( --jp-yellow-30 );
4
+ transition: opacity 0.25s ease-in-out, width 0.25s;
5
+ width: 0;
6
+ opacity: 0;
7
+ align-self: baseline;
8
+
9
+ > svg {
10
+ height: 24px;
11
+ width: 24px;
12
+ margin: 6px 0 0;
37
13
  }
38
- }
39
-
40
14
 
41
- @keyframes fadingSpark {
42
- to {
43
- opacity: 0.3;
15
+ &.is-requesting, &.is-suggesting {
16
+ opacity: 1;
17
+ width: 24px;
44
18
  }
45
19
  }
46
-
47
- @keyframes fade {
48
- 0% { opacity: 1; }
49
- 50% { opacity: 0.4; }
50
- 100% { opacity: 1; }
51
- }
52
-
@@ -1,4 +1,4 @@
1
1
  export { default as AIControl } from './ai-control';
2
2
  export { default as AiStatusIndicator } from './ai-status-indicator';
3
3
  export { default as AudioDurationDisplay } from './audio-duration-display';
4
- export { GuidelineMessage } from './ai-control/message';
4
+ export { GuidelineMessage, default as FooterMessage } from './ai-control/message';