@automattic/jetpack-ai-client 0.1.2 → 0.1.4

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,15 +5,43 @@ 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.1.4] - 2023-08-14
9
+ ### Added
10
+ - AI Client: Add border-box in AIControl. [#32419]
11
+ - AI Client: Export AiStatusIndicator. [#32397]
12
+ - AI Client: Import base styles in the AI status indicator component. [#32396]
13
+ - AI Control: Forward ref to consumer. [#32400]
14
+ - AI Control: Import jetpack-base-styles. [#32376]
15
+
16
+ ### Changed
17
+ - AI Client: Expose stopSuggestion function on useAiSuggestions hook so the consumer can stop a suggestion in the middle. [#32382]
18
+
19
+ ### Removed
20
+ - AI Client: Remove redundant switch case [#32405]
21
+
22
+ ## [0.1.3] - 2023-08-09
23
+ ### Added
24
+ - AI Client: Introduce disabled prop in AI Control. [#32326]
25
+ - AI Control: Add guideline message. [#32358]
26
+
27
+ ### Changed
28
+ - AI Client: handle token fetching errors by dispatching an event from the SuggestionsEventSource class. [#32350]
29
+ - AI Client: tweak layout and styles to make AI Control mobile friendly. [#32362]
30
+ - AI Control: clean up props. [#32360]
31
+ - Updated package dependencies. [#32166]
32
+
33
+ ### Fixed
34
+ - AI Client: fix TS type definition issue [#32330]
35
+
8
36
  ## [0.1.2] - 2023-08-07
9
37
  ### Added
10
- - AI Assistant: add options parameter to request function on useAiSuggestions hook [#32198]
38
+ - AI Assistant: Add options parameter to request function on useAiSuggestions hook [#32198]
11
39
  - AI Client: add @wordpress/compose dependency [#32228]
12
- - AI Client: add clear button in AI Control component [#32274]
13
- - AI Client: add keyboard shortcut to AIControl [#32239]
40
+ - AI Client: Add clear button in AI Control component [#32274]
41
+ - AI Client: Add keyboard shortcut to AIControl [#32239]
14
42
  - AI Client: add onError() response support [#32223]
15
- - AI Client: export types [#32209]
16
- - AI Client: start supporting request options on requestSuggestion callback. [#32303]
43
+ - AI Client: Export types [#32209]
44
+ - AI Client: Start supporting request options on requestSuggestion callback. [#32303]
17
45
  - AI Control: introduce AiStatusIndicator component [#32258]
18
46
 
19
47
  ### Changed
@@ -54,5 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
54
82
  - Updated package dependencies. [#31659]
55
83
  - Updated package dependencies. [#31785]
56
84
 
85
+ [0.1.4]: https://github.com/Automattic/jetpack-ai-client/compare/v0.1.3...v0.1.4
86
+ [0.1.3]: https://github.com/Automattic/jetpack-ai-client/compare/v0.1.2...v0.1.3
57
87
  [0.1.2]: https://github.com/Automattic/jetpack-ai-client/compare/v0.1.1...v0.1.2
58
88
  [0.1.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.1.0...v0.1.1
package/index.ts CHANGED
@@ -18,7 +18,7 @@ export * from './src/icons';
18
18
  /*
19
19
  * Components
20
20
  */
21
- export { default as AIControl } from './src/components/ai-control';
21
+ export * from './src/components';
22
22
 
23
23
  /*
24
24
  * Contexts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@automattic/jetpack-ai-client",
4
- "version": "0.1.2",
4
+ "version": "0.1.4",
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": {
@@ -32,17 +32,18 @@
32
32
  ".": "./index.ts"
33
33
  },
34
34
  "dependencies": {
35
+ "@automattic/jetpack-base-styles": "^0.6.5",
35
36
  "@automattic/jetpack-connection": "workspace:*",
36
- "@automattic/jetpack-shared-extension-utils": "^0.11.0",
37
+ "@automattic/jetpack-shared-extension-utils": "^0.11.1",
37
38
  "@microsoft/fetch-event-source": "2.0.1",
38
- "@wordpress/api-fetch": "6.34.0",
39
- "@wordpress/block-editor": "12.5.0",
40
- "@wordpress/components": "25.3.0",
41
- "@wordpress/compose": "6.14.0",
42
- "@wordpress/data": "9.7.0",
43
- "@wordpress/element": "5.14.0",
44
- "@wordpress/i18n": "4.37.0",
45
- "@wordpress/icons": "9.28.0",
39
+ "@wordpress/api-fetch": "6.35.0",
40
+ "@wordpress/block-editor": "12.6.0",
41
+ "@wordpress/components": "25.4.0",
42
+ "@wordpress/compose": "6.15.0",
43
+ "@wordpress/data": "9.8.0",
44
+ "@wordpress/element": "5.15.0",
45
+ "@wordpress/i18n": "4.38.0",
46
+ "@wordpress/icons": "9.29.0",
46
47
  "classnames": "2.3.2",
47
48
  "debug": "4.3.4",
48
49
  "react": "18.2.0",
@@ -2,7 +2,6 @@
2
2
  * External dependencies
3
3
  */
4
4
  import debugFactory from 'debug';
5
- import requestJwt from '../jwt';
6
5
  import SuggestionsEventSource from '../suggestions-event-source';
7
6
  /*
8
7
  * Types & constants
@@ -62,11 +61,8 @@ export default async function askQuestion(
62
61
  ): Promise< SuggestionsEventSource > {
63
62
  debug( 'Asking question: %o. options: %o', question, { postId, fromCache, feature, functions } );
64
63
 
65
- const { token } = await requestJwt();
66
-
67
64
  return new SuggestionsEventSource( {
68
65
  question,
69
- token,
70
66
  options: { postId, feature, fromCache, functions },
71
67
  } );
72
68
  }
@@ -2,14 +2,14 @@
2
2
 
3
3
  #### Properties
4
4
 
5
- - `loading` (**boolean**) (Optional): Determines the loading state. Default value is `false`.
5
+ - `disabled` (**boolean**) (Optional): Disables the ai control. Default value is `false`.
6
6
  - `value` (**string**): Current input value. Default value is `''`.
7
7
  - `placeholder` (**string**) (Optional): Placeholder text for the input field. Default value is `''`.
8
8
  - `showAccept` (**boolean**) (Optional): Determines if the accept button is shown. Default value is `false`.
9
9
  - `acceptLabel` (**string**) (Optional): Label text for the accept button. Default value is `'Accept'`.
10
10
  - `showButtonsLabel` (**boolean**) (Optional): Determines if button labels are shown. Default value is `true`.
11
11
  - `isOpaque` (**boolean**) (Optional): Controls the opacity of the component. Default value is `false`.
12
- - `requestingState` (**RequestingStateProp**) (Optional): Determines the state of the request. Default value is `'init'`.
12
+ - `state` (**RequestingStateProp**) (Optional): Determines the state of the component. Default value is `'init'`.
13
13
  - `onChange` (**Function**) (Optional): Handler for input change. Default action is no operation.
14
14
  - `onSend` (**Function**) (Optional): Handler to send a request. Default action is no operation.
15
15
  - `onStop` (**Function**) (Optional): Handler to stop a request. Default action is no operation.
@@ -4,15 +4,17 @@
4
4
  import { PlainText } from '@wordpress/block-editor';
5
5
  import { Button } from '@wordpress/components';
6
6
  import { useKeyboardShortcut } from '@wordpress/compose';
7
- import { useRef } from '@wordpress/element';
7
+ import { forwardRef, useImperativeHandle, useRef } from '@wordpress/element';
8
8
  import { __ } from '@wordpress/i18n';
9
9
  import { Icon, closeSmall, check, arrowUp } from '@wordpress/icons';
10
10
  import classNames from 'classnames';
11
+ import React from 'react';
11
12
  /**
12
13
  * Internal dependencies
13
14
  */
14
15
  import './style.scss';
15
16
  import AiStatusIndicator from '../ai-status-indicator';
17
+ import { GuidelineMessage } from './message';
16
18
  /**
17
19
  * Types
18
20
  */
@@ -25,48 +27,57 @@ const noop = () => {};
25
27
  * AI Control component.
26
28
  *
27
29
  * @param {object} props - component props
28
- * @param {boolean} props.loading - loading state
30
+ * @param {boolean} props.disabled - is disabled
29
31
  * @param {string} props.value - input value
30
32
  * @param {string} props.placeholder - input placeholder
31
33
  * @param {boolean} props.showAccept - show accept button
32
34
  * @param {string} props.acceptLabel - accept button label
33
35
  * @param {boolean} props.showButtonsLabel - show buttons label
34
36
  * @param {boolean} props.isOpaque - is opaque
37
+ * @param {string} props.state - requesting state
35
38
  * @param {Function} props.onChange - input change handler
36
39
  * @param {Function} props.onSend - send request handler
37
40
  * @param {Function} props.onStop - stop request handler
38
41
  * @param {Function} props.onAccept - accept handler
39
- * @param {string} props.requestingState - requesting state
42
+ * @param {object} ref - Auto injected ref from react
40
43
  * @returns {object} - AI Control component
41
44
  */
42
- export default function AIControl( {
43
- loading = false,
44
- value = '',
45
- placeholder = '',
46
- showAccept = false,
47
- acceptLabel = __( 'Accept', 'jetpack-ai-client' ),
48
- showButtonsLabel = true,
49
- isOpaque = false,
50
- requestingState = 'init',
51
- onChange = noop,
52
- onSend = noop,
53
- onStop = noop,
54
- onAccept = noop,
55
- }: {
56
- loading?: boolean;
57
- value: string;
58
- placeholder?: string;
59
- showAccept?: boolean;
60
- acceptLabel?: string;
61
- showButtonsLabel?: boolean;
62
- isOpaque?: boolean;
63
- requestingState?: RequestingStateProp;
64
- onChange?: ( newValue: string ) => void;
65
- onSend?: ( currentValue: string ) => void;
66
- onStop?: () => void;
67
- onAccept?: () => void;
68
- } ) {
45
+ export function AIControl(
46
+ {
47
+ disabled = false,
48
+ value = '',
49
+ placeholder = '',
50
+ showAccept = false,
51
+ acceptLabel = __( 'Accept', 'jetpack-ai-client' ),
52
+ showButtonsLabel = true,
53
+ isOpaque = false,
54
+ state = 'init',
55
+ onChange = noop,
56
+ onSend = noop,
57
+ onStop = noop,
58
+ onAccept = noop,
59
+ }: {
60
+ disabled?: boolean;
61
+ value: string;
62
+ placeholder?: string;
63
+ showAccept?: boolean;
64
+ acceptLabel?: string;
65
+ showButtonsLabel?: boolean;
66
+ isOpaque?: boolean;
67
+ state?: RequestingStateProp;
68
+ onChange?: ( newValue: string ) => void;
69
+ onSend?: ( currentValue: string ) => void;
70
+ onStop?: () => void;
71
+ onAccept?: () => void;
72
+ },
73
+ ref
74
+ ) {
69
75
  const promptUserInputRef = useRef( null );
76
+ const loading = state === 'requesting' || state === 'suggesting';
77
+ const showGuideLine = ! ( loading || disabled || value?.length || isOpaque );
78
+
79
+ // Pass the ref to forwardRef.
80
+ useImperativeHandle( ref, () => promptUserInputRef.current );
70
81
 
71
82
  useKeyboardShortcut(
72
83
  'mod+enter',
@@ -91,6 +102,10 @@ export default function AIControl( {
91
102
  }
92
103
  );
93
104
 
105
+ const actionButtonClasses = classNames( 'jetpack-components-ai-control__controls-prompt_button', {
106
+ 'has-label': showButtonsLabel,
107
+ } );
108
+
94
109
  return (
95
110
  <div className="jetpack-components-ai-control__container">
96
111
  <div
@@ -98,7 +113,7 @@ export default function AIControl( {
98
113
  'is-opaque': isOpaque,
99
114
  } ) }
100
115
  >
101
- <AiStatusIndicator state={ requestingState } />
116
+ <AiStatusIndicator state={ state } />
102
117
 
103
118
  <div className="jetpack-components-ai-control__input-wrapper">
104
119
  <PlainText
@@ -106,60 +121,61 @@ export default function AIControl( {
106
121
  onChange={ onChange }
107
122
  placeholder={ placeholder }
108
123
  className="jetpack-components-ai-control__input"
109
- disabled={ loading }
124
+ disabled={ loading || disabled }
110
125
  ref={ promptUserInputRef }
111
126
  />
127
+ </div>
128
+
129
+ { value?.length > 0 && (
130
+ <Button
131
+ icon={ closeSmall }
132
+ className="jetpack-components-ai-control__clear"
133
+ onClick={ () => onChange( '' ) }
134
+ />
135
+ ) }
112
136
 
113
- { value?.length > 0 && (
114
- <Icon
115
- icon={ closeSmall }
116
- className="jetpack-components-ai-control__clear"
117
- onClick={ () => onChange( '' ) }
118
- />
137
+ <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
138
+ { ! loading ? (
139
+ <Button
140
+ className={ actionButtonClasses }
141
+ onClick={ () => onSend( value ) }
142
+ isSmall={ true }
143
+ disabled={ ! value?.length || disabled }
144
+ label={ __( 'Send request', 'jetpack-ai-client' ) }
145
+ >
146
+ <Icon icon={ arrowUp } />
147
+ { showButtonsLabel && __( 'Send', 'jetpack-ai-client' ) }
148
+ </Button>
149
+ ) : (
150
+ <Button
151
+ className={ actionButtonClasses }
152
+ onClick={ onStop }
153
+ isSmall={ true }
154
+ label={ __( 'Stop request', 'jetpack-ai-client' ) }
155
+ >
156
+ <Icon icon={ closeSmall } />
157
+ { showButtonsLabel && __( 'Stop', 'jetpack-ai-client' ) }
158
+ </Button>
119
159
  ) }
120
160
  </div>
121
161
 
122
- <div className="jetpack-components-ai-control__controls">
162
+ { showAccept && (
123
163
  <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
124
- { ! loading ? (
125
- <Button
126
- className="jetpack-components-ai-control__controls-prompt_button"
127
- onClick={ () => onSend( value ) }
128
- isSmall={ true }
129
- disabled={ ! value?.length }
130
- label={ __( 'Send request', 'jetpack-ai-client' ) }
131
- >
132
- <Icon icon={ arrowUp } />
133
- { showButtonsLabel && __( 'Send', 'jetpack-ai-client' ) }
134
- </Button>
135
- ) : (
136
- <Button
137
- className="jetpack-components-ai-control__controls-prompt_button"
138
- onClick={ onStop }
139
- isSmall={ true }
140
- label={ __( 'Stop request', 'jetpack-ai-client' ) }
141
- >
142
- <Icon icon={ closeSmall } />
143
- { showButtonsLabel && __( 'Stop', 'jetpack-ai-client' ) }
144
- </Button>
145
- ) }
164
+ <Button
165
+ className={ actionButtonClasses }
166
+ onClick={ onAccept }
167
+ isSmall={ true }
168
+ label={ acceptLabel }
169
+ >
170
+ <Icon icon={ check } />
171
+ { showButtonsLabel && acceptLabel }
172
+ </Button>
146
173
  </div>
147
-
148
- { showAccept && (
149
- <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
150
- <Button
151
- className="jetpack-components-ai-control__controls-prompt_button"
152
- onClick={ onAccept }
153
- isSmall={ true }
154
- label={ acceptLabel }
155
- >
156
- <Icon icon={ check } />
157
- { acceptLabel }
158
- </Button>
159
- </div>
160
- ) }
161
- </div>
174
+ ) }
162
175
  </div>
176
+ { showGuideLine && <GuidelineMessage /> }
163
177
  </div>
164
178
  );
165
179
  }
180
+
181
+ export default forwardRef( AIControl );
@@ -0,0 +1,86 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { ExternalLink } from '@wordpress/components';
5
+ import { createInterpolateElement } from '@wordpress/element';
6
+ import { __ } from '@wordpress/i18n';
7
+ import {
8
+ Icon,
9
+ warning,
10
+ info,
11
+ cancelCircleFilled as error,
12
+ check as success,
13
+ } from '@wordpress/icons';
14
+ /**
15
+ * Types
16
+ */
17
+ import type React from 'react';
18
+
19
+ import './style.scss';
20
+
21
+ export const MESSAGE_SEVERITY_WARNING = 'warning';
22
+ export const MESSAGE_SEVERITY_ERROR = 'error';
23
+ export const MESSAGE_SEVERITY_SUCCESS = 'success';
24
+ export const MESSAGE_SEVERITY_INFO = 'info';
25
+
26
+ const messageSeverityTypes = [
27
+ MESSAGE_SEVERITY_WARNING,
28
+ MESSAGE_SEVERITY_ERROR,
29
+ MESSAGE_SEVERITY_SUCCESS,
30
+ MESSAGE_SEVERITY_INFO,
31
+ ] as const;
32
+
33
+ export type MessageSeverityProp = ( typeof messageSeverityTypes )[ number ] | null;
34
+
35
+ export type MessageProps = {
36
+ icon?: React.ReactNode;
37
+ children: React.ReactNode;
38
+ severity: MessageSeverityProp;
39
+ };
40
+
41
+ const messageIconsMap = {
42
+ [ MESSAGE_SEVERITY_WARNING ]: warning,
43
+ [ MESSAGE_SEVERITY_ERROR ]: error,
44
+ [ MESSAGE_SEVERITY_SUCCESS ]: success,
45
+ [ MESSAGE_SEVERITY_INFO ]: info,
46
+ };
47
+
48
+ /**
49
+ * React component to render a block message.
50
+ *
51
+ * @param {MessageProps} props - Component props.
52
+ * @returns {React.ReactElement } Banner component.
53
+ */
54
+ export default function Message( {
55
+ severity = null,
56
+ icon = null,
57
+ children,
58
+ }: MessageProps ): React.ReactElement {
59
+ return (
60
+ <div className="jetpack-ai-assistant__message">
61
+ { ( severity || icon ) && <Icon icon={ messageIconsMap[ severity ] || icon } /> }
62
+ <div className="jetpack-ai-assistant__message-content">{ children }</div>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ /**
68
+ * React component to render a guideline message.
69
+ *
70
+ * @returns {React.ReactElement } - Message component.
71
+ */
72
+ export function GuidelineMessage(): React.ReactElement {
73
+ return (
74
+ <Message severity={ MESSAGE_SEVERITY_INFO }>
75
+ { createInterpolateElement(
76
+ __(
77
+ 'AI-generated content could be inaccurate or biased. <link>Learn more</link>',
78
+ 'jetpack-ai-client'
79
+ ),
80
+ {
81
+ link: <ExternalLink href="https://automattic.com/ai-guidelines" />,
82
+ }
83
+ ) }
84
+ </Message>
85
+ );
86
+ }
@@ -1,3 +1,7 @@
1
+ @import '@automattic/jetpack-base-styles/root-variables';
2
+
3
+ // AI CONTROL
4
+
1
5
  .jetpack-components-ai-control__container {
2
6
  color: var( --jp-gray-80 );
3
7
  background-color: var( --jp-white );
@@ -11,6 +15,7 @@
11
15
  padding: 12px 14px;
12
16
  gap: 8px;
13
17
  align-items: center;
18
+ box-sizing: border-box;
14
19
 
15
20
  &.is-opaque {
16
21
  opacity: 0.4;
@@ -23,26 +28,11 @@
23
28
  flex-grow: 1;
24
29
  width: 100%;
25
30
 
26
- .jetpack-components-ai-control__clear {
27
- position: absolute;
28
- right: 8px;
29
- top: calc( 50% - 12px );
30
- border-radius: 50%;
31
- background-color: black;
32
- opacity: 0.2;
33
- fill: white;
34
- cursor: pointer;
35
-
36
- &:hover {
37
- opacity: 0.4;
38
- }
39
- }
40
-
41
31
  textarea.jetpack-components-ai-control__input {
42
32
  width: 100%;
43
33
  min-height: 20px;
44
34
  border-radius: 2px;
45
- padding: 6px 38px 6px 8px;
35
+ padding: 6px 8px;
46
36
 
47
37
  resize: none !important;
48
38
  border: none;
@@ -72,6 +62,21 @@
72
62
  }
73
63
  }
74
64
 
65
+ .jetpack-components-ai-control__clear.components-button.has-icon {
66
+ width: 24px;
67
+ min-width: 24px;
68
+ height: 24px;
69
+ padding: 0;
70
+ border-radius: 50%;
71
+ background-color: black;
72
+ opacity: 0.2;
73
+ color: white;
74
+
75
+ &:hover {
76
+ opacity: 0.4;
77
+ }
78
+ }
79
+
75
80
  .jetpack-components-ai-controlton__icon {
76
81
  flex-shrink: 0;
77
82
  display: flex;
@@ -94,11 +99,6 @@
94
99
  }
95
100
  }
96
101
 
97
- .jetpack-components-ai-control__controls {
98
- display: flex;
99
- align-items: center;
100
- }
101
-
102
102
  .jetpack-components-ai-control__controls-prompt_button_wrapper {
103
103
  text-transform: uppercase;
104
104
  font-size: 11px;
@@ -108,16 +108,16 @@
108
108
  white-space: nowrap;
109
109
  display: flex;
110
110
  align-items: center;
111
+
112
+ .components-button.is-small:not(.has-label) {
113
+ padding: 0;
114
+ }
111
115
  }
112
116
 
113
117
  .jetpack-components-ai-control__controls-prompt_button {
114
118
  color: var( --jp-gray-80 );
115
119
  text-transform: uppercase;
116
120
 
117
- > SVG {
118
- margin-right: 4px;
119
- }
120
-
121
121
  &:hover,
122
122
  &:active {
123
123
  color: var( --wp-components-color-accent, var( --wp-admin-theme-color ) );
@@ -127,4 +127,30 @@
127
127
  opacity: 0.6;
128
128
  cursor: not-allowed;
129
129
  }
130
- }
130
+ }
131
+
132
+ // MESSAGE
133
+
134
+ .jetpack-ai-assistant__message {
135
+ display: flex;
136
+ line-height: 28px;
137
+ font-size: 12px;
138
+ align-self: center;
139
+ align-items: center;
140
+ background-color: var( --jp-white-off );
141
+ padding: 0 12px;
142
+
143
+ > svg {
144
+ fill: var( --jp-gray-40 );
145
+ }
146
+
147
+ .jetpack-ai-assistant__message-content {
148
+ flex-grow: 2;
149
+ margin: 0 8px;
150
+ color: var( --jp-gray-70 );
151
+
152
+ .components-external-link {
153
+ color: var( --jp-gray-50 );
154
+ }
155
+ }
156
+ }
@@ -1,3 +1,5 @@
1
+ @import '@automattic/jetpack-base-styles/style';
2
+
1
3
  .jetpack-ai-status-indicator__icon-wrapper {
2
4
  color: var( --jp-green-60 );
3
5
  height: 24px;
@@ -0,0 +1,2 @@
1
+ export { default as AIControl } from './ai-control';
2
+ export { default as AiStatusIndicator } from './ai-status-indicator';
@@ -33,6 +33,11 @@ export type AiDataContextProps = {
33
33
  */
34
34
  requestSuggestion: ( prompt: PromptProp, options?: AskQuestionOptionsArgProps ) => void;
35
35
 
36
+ /*
37
+ * Stop suggestion function
38
+ */
39
+ stopSuggestion: () => void;
40
+
36
41
  /*
37
42
  * The Suggestions Event Source instance
38
43
  */
@@ -5,7 +5,7 @@ import { useCallback, useContext, useEffect } from '@wordpress/element';
5
5
  /**
6
6
  * Internal dependencies
7
7
  */
8
- import { ERROR_RESPONSE } from '../types';
8
+ import { ERROR_RESPONSE, RequestingErrorProps } from '../types';
9
9
  import { AiDataContext } from '.';
10
10
  /**
11
11
  * Types & constants
@@ -32,7 +32,7 @@ export type UseAiContextOptions = {
32
32
  /*
33
33
  * onError callback.
34
34
  */
35
- onError?: ( error: Error ) => void;
35
+ onError?: ( error: RequestingErrorProps ) => void;
36
36
  };
37
37
 
38
38
  /**
@@ -25,6 +25,7 @@ const withAiDataProvider = createHigherOrderComponent( ( WrappedComponent: React
25
25
  error: requestingError,
26
26
  requestingState,
27
27
  request: requestSuggestion,
28
+ stopSuggestion,
28
29
  eventSource,
29
30
  } = useAiSuggestions();
30
31
 
@@ -37,8 +38,16 @@ const withAiDataProvider = createHigherOrderComponent( ( WrappedComponent: React
37
38
  eventSource,
38
39
 
39
40
  requestSuggestion,
41
+ stopSuggestion,
40
42
  } ),
41
- [ suggestion, requestingError, requestingState, eventSource, requestSuggestion ]
43
+ [
44
+ suggestion,
45
+ requestingError,
46
+ requestingState,
47
+ eventSource,
48
+ requestSuggestion,
49
+ stopSuggestion,
50
+ ]
42
51
  );
43
52
 
44
53
  return (
@@ -97,6 +97,11 @@ type useAiSuggestionsProps = {
97
97
  * The request handler.
98
98
  */
99
99
  request: ( prompt: PromptProp, options?: AskQuestionOptionsArgProps ) => Promise< void >;
100
+
101
+ /*
102
+ * The handler to stop a suggestion.
103
+ */
104
+ stopSuggestion: () => void;
100
105
  };
101
106
 
102
107
  const debug = debugFactory( 'jetpack-ai-client:use-suggestion' );
@@ -140,14 +145,6 @@ export function getErrorData( errorCode: SuggestionErrorCode ): RequestingErrorP
140
145
  severity: 'info',
141
146
  };
142
147
  case ERROR_NETWORK:
143
- return {
144
- code: ERROR_NETWORK,
145
- message: __(
146
- 'It was not possible to process your request. Mind trying again?',
147
- 'jetpack-ai-client'
148
- ),
149
- severity: 'info',
150
- };
151
148
  default:
152
149
  return {
153
150
  code: ERROR_NETWORK,
@@ -234,7 +231,7 @@ export default function useAiSuggestions( {
234
231
 
235
232
  const handleModerationError = useCallback( () => handleError( ERROR_MODERATION ), [] );
236
233
 
237
- const handleNetwotkError = useCallback( () => handleError( ERROR_NETWORK ), [] );
234
+ const handleNetworkError = useCallback( () => handleError( ERROR_NETWORK ), [] );
238
235
 
239
236
  /**
240
237
  * Request handler.
@@ -278,7 +275,7 @@ export default function useAiSuggestions( {
278
275
  eventSource.addEventListener( ERROR_UNCLEAR_PROMPT, handleUnclearPromptError );
279
276
  eventSource.addEventListener( ERROR_SERVICE_UNAVAILABLE, handleServiceUnavailableError );
280
277
  eventSource.addEventListener( ERROR_MODERATION, handleModerationError );
281
- eventSource.addEventListener( ERROR_NETWORK, handleNetwotkError );
278
+ eventSource.addEventListener( ERROR_NETWORK, handleNetworkError );
282
279
 
283
280
  eventSource.addEventListener( 'done', handleDone );
284
281
  } catch ( e ) {
@@ -292,11 +289,51 @@ export default function useAiSuggestions( {
292
289
  handleUnclearPromptError,
293
290
  handleServiceUnavailableError,
294
291
  handleModerationError,
295
- handleNetwotkError,
292
+ handleNetworkError,
296
293
  handleSuggestion,
297
294
  ]
298
295
  );
299
296
 
297
+ /**
298
+ * Stop suggestion handler.
299
+ *
300
+ * @returns {void}
301
+ */
302
+ const stopSuggestion = useCallback( () => {
303
+ if ( ! eventSourceRef?.current ) {
304
+ return;
305
+ }
306
+
307
+ // Alias
308
+ const eventSource = eventSourceRef?.current;
309
+
310
+ // Close the connection.
311
+ eventSource.close();
312
+
313
+ // Clean up the event listeners.
314
+ eventSource.removeEventListener( 'suggestion', handleSuggestion );
315
+
316
+ eventSource.removeEventListener( ERROR_QUOTA_EXCEEDED, handleErrorQuotaExceededError );
317
+ eventSource.removeEventListener( ERROR_UNCLEAR_PROMPT, handleUnclearPromptError );
318
+ eventSource.removeEventListener( ERROR_SERVICE_UNAVAILABLE, handleServiceUnavailableError );
319
+ eventSource.removeEventListener( ERROR_MODERATION, handleModerationError );
320
+ eventSource.removeEventListener( ERROR_NETWORK, handleNetworkError );
321
+
322
+ eventSource.removeEventListener( 'done', handleDone );
323
+
324
+ // Set requesting state to done since the suggestion stopped.
325
+ setRequestingState( 'done' );
326
+ }, [
327
+ eventSourceRef,
328
+ handleSuggestion,
329
+ handleErrorQuotaExceededError,
330
+ handleUnclearPromptError,
331
+ handleServiceUnavailableError,
332
+ handleModerationError,
333
+ handleNetworkError,
334
+ handleDone,
335
+ ] );
336
+
300
337
  // Request suggestions automatically when ready.
301
338
  useEffect( () => {
302
339
  // Check if there is a prompt to request.
@@ -310,38 +347,10 @@ export default function useAiSuggestions( {
310
347
  }
311
348
 
312
349
  return () => {
313
- if ( ! eventSourceRef?.current ) {
314
- return;
315
- }
316
-
317
- // Alias
318
- const eventSource = eventSourceRef.current;
319
-
320
- // Close the connection.
321
- eventSource.close();
322
-
323
- // Clean up the event listeners.
324
- eventSource.removeEventListener( 'suggestion', handleSuggestion );
325
-
326
- eventSource.removeEventListener( ERROR_QUOTA_EXCEEDED, handleErrorQuotaExceededError );
327
- eventSource.removeEventListener( ERROR_UNCLEAR_PROMPT, handleUnclearPromptError );
328
- eventSource.removeEventListener( ERROR_SERVICE_UNAVAILABLE, handleServiceUnavailableError );
329
- eventSource.removeEventListener( ERROR_MODERATION, handleModerationError );
330
- eventSource.removeEventListener( ERROR_NETWORK, handleNetwotkError );
331
-
332
- eventSource.removeEventListener( 'done', handleDone );
350
+ // Stop the suggestion if the component unmounts.
351
+ stopSuggestion();
333
352
  };
334
- }, [
335
- autoRequest,
336
- handleDone,
337
- handleErrorQuotaExceededError,
338
- handleModerationError,
339
- handleServiceUnavailableError,
340
- handleSuggestion,
341
- handleUnclearPromptError,
342
- prompt,
343
- request,
344
- ] );
353
+ }, [ autoRequest, prompt, request, stopSuggestion ] );
345
354
 
346
355
  return {
347
356
  // Data
@@ -349,8 +358,9 @@ export default function useAiSuggestions( {
349
358
  error,
350
359
  requestingState,
351
360
 
352
- // Request handler
361
+ // Request/stop handlers
353
362
  request,
363
+ stopSuggestion,
354
364
 
355
365
  // SuggestionsEventSource
356
366
  eventSource: eventSourceRef.current,
@@ -3,10 +3,14 @@
3
3
  */
4
4
  import { EventSourceMessage, fetchEventSource } from '@microsoft/fetch-event-source';
5
5
  import debugFactory from 'debug';
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { getErrorData } from '../hooks/use-ai-suggestions';
10
+ import requestJwt from '../jwt';
6
11
  /*
7
12
  * Types & constants
8
13
  */
9
- import { getErrorData } from '../hooks/use-ai-suggestions';
10
14
  import {
11
15
  ERROR_MODERATION,
12
16
  ERROR_NETWORK,
@@ -20,7 +24,7 @@ import type { PromptMessagesProp, PromptProp, SuggestionErrorCode } from '../typ
20
24
  type SuggestionsEventSourceConstructorArgs = {
21
25
  url?: string;
22
26
  question: PromptProp;
23
- token: string;
27
+ token?: string;
24
28
  options?: {
25
29
  postId?: number;
26
30
  feature?: 'ai-assistant-experimental' | string | undefined;
@@ -81,6 +85,18 @@ export default class SuggestionsEventSource extends EventTarget {
81
85
  token,
82
86
  options = {},
83
87
  }: SuggestionsEventSourceConstructorArgs ) {
88
+ // If the token is not provided, try to get one
89
+ if ( ! token ) {
90
+ try {
91
+ debug( 'Token was not provided, requesting one...' );
92
+ token = ( await requestJwt() ).token;
93
+ } catch ( err ) {
94
+ this.processErrorEvent( err );
95
+
96
+ return;
97
+ }
98
+ }
99
+
84
100
  const bodyData: {
85
101
  post_id?: number;
86
102
  messages?: PromptMessagesProp;