@automattic/jetpack-ai-client 0.1.1 → 0.1.3

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.
@@ -1,23 +1,33 @@
1
+ // AI CONTROL
2
+
1
3
  .jetpack-components-ai-control__container {
2
4
  color: var( --jp-gray-80 );
3
5
  background-color: var( --jp-white );
4
6
  box-shadow: var( --wp--preset--color--cyan-bluish-gray ) 0px 0px 0px 1px, var( --wp--preset--color--cyan-bluish-gray ) 0px 0px 8px;
5
7
  font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
8
+ width: 100%;
6
9
  }
7
10
 
8
11
  .jetpack-components-ai-control__wrapper {
9
12
  display: flex;
10
13
  padding: 12px 14px;
11
14
  gap: 8px;
15
+ align-items: center;
12
16
 
13
17
  &.is-opaque {
14
18
  opacity: 0.4;
15
19
  }
20
+ }
21
+
22
+ .jetpack-components-ai-control__input-wrapper {
23
+ position: relative;
24
+ display: flex;
25
+ flex-grow: 1;
26
+ width: 100%;
16
27
 
17
28
  textarea.jetpack-components-ai-control__input {
18
29
  width: 100%;
19
- min-height: 20px;
20
- flex-grow: 1;
30
+ min-height: 20px;
21
31
  border-radius: 2px;
22
32
  padding: 6px 8px;
23
33
 
@@ -49,6 +59,21 @@
49
59
  }
50
60
  }
51
61
 
62
+ .jetpack-components-ai-control__clear.components-button.has-icon {
63
+ width: 24px;
64
+ min-width: 24px;
65
+ height: 24px;
66
+ padding: 0;
67
+ border-radius: 50%;
68
+ background-color: black;
69
+ opacity: 0.2;
70
+ color: white;
71
+
72
+ &:hover {
73
+ opacity: 0.4;
74
+ }
75
+ }
76
+
52
77
  .jetpack-components-ai-controlton__icon {
53
78
  flex-shrink: 0;
54
79
  display: flex;
@@ -71,11 +96,6 @@
71
96
  }
72
97
  }
73
98
 
74
- .jetpack-components-ai-control__controls {
75
- display: flex;
76
- align-items: center;
77
- }
78
-
79
99
  .jetpack-components-ai-control__controls-prompt_button_wrapper {
80
100
  text-transform: uppercase;
81
101
  font-size: 11px;
@@ -85,16 +105,16 @@
85
105
  white-space: nowrap;
86
106
  display: flex;
87
107
  align-items: center;
108
+
109
+ .components-button.is-small:not(.has-label) {
110
+ padding: 0;
111
+ }
88
112
  }
89
113
 
90
114
  .jetpack-components-ai-control__controls-prompt_button {
91
115
  color: var( --jp-gray-80 );
92
116
  text-transform: uppercase;
93
117
 
94
- > SVG {
95
- margin-right: 4px;
96
- }
97
-
98
118
  &:hover,
99
119
  &:active {
100
120
  color: var( --wp-components-color-accent, var( --wp-admin-theme-color ) );
@@ -104,4 +124,30 @@
104
124
  opacity: 0.6;
105
125
  cursor: not-allowed;
106
126
  }
107
- }
127
+ }
128
+
129
+ // MESSAGE
130
+
131
+ .jetpack-ai-assistant__message {
132
+ display: flex;
133
+ line-height: 28px;
134
+ font-size: 12px;
135
+ align-self: center;
136
+ align-items: center;
137
+ background-color: var( --jp-white-off );
138
+ padding: 0 12px;
139
+
140
+ > svg {
141
+ fill: var( --jp-gray-40 );
142
+ }
143
+
144
+ .jetpack-ai-assistant__message-content {
145
+ flex-grow: 2;
146
+ margin: 0 8px;
147
+ color: var( --jp-gray-70 );
148
+
149
+ .components-external-link {
150
+ color: var( --jp-gray-50 );
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,11 @@
1
+ ### AiStatusIndicator
2
+
3
+ #### Properties
4
+
5
+ - `state` (**RequestingStateProp**) (Optional): Determines the appearance of the icon.
6
+ - `size` (**AiStatusIndicatorIconSize** 24 | 32 | 48 | 64 pixels) (Optional): Defines the size of the icon. Default value is `24`.
7
+
8
+ #### Example Usage
9
+ ```jsx
10
+ <AiStatusIndicator state="requesting" size={ 32 } />
11
+ ```
@@ -0,0 +1,43 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { Icon } from '@wordpress/components';
5
+ import classNames from 'classnames';
6
+ /*
7
+ * Internal dependencies
8
+ */
9
+ import { aiAssistantIcon } from '../../icons';
10
+ /*
11
+ * Types
12
+ */
13
+ import type { RequestingStateProp } from '../../types';
14
+ export type AiStatusIndicatorIconSize = 24 | 32 | 48 | 64;
15
+ import type React from 'react';
16
+
17
+ import './style.scss';
18
+
19
+ export type AiStatusIndicatorProps = {
20
+ state?: RequestingStateProp;
21
+ size?: AiStatusIndicatorIconSize;
22
+ };
23
+
24
+ /**
25
+ * AiStatusIndicator component.
26
+ *
27
+ * @param {AiStatusIndicatorProps} props - component props.
28
+ * @returns {React.ReactElement} - rendered component.
29
+ */
30
+ export default function AiStatusIndicator( {
31
+ state,
32
+ size = 24,
33
+ }: AiStatusIndicatorProps ): React.ReactElement {
34
+ return (
35
+ <div
36
+ className={ classNames( 'jetpack-ai-status-indicator__icon-wrapper', {
37
+ [ `is-${ state }` ]: true,
38
+ } ) }
39
+ >
40
+ <Icon icon={ aiAssistantIcon } size={ size } />
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,25 @@
1
+ import { Meta, Story, Canvas } from '@storybook/blocks';
2
+ import AiStatusIndicator from '../index';
3
+ import * as AiStatusIndicatorStory from './index.stories';
4
+
5
+ <Meta of={AiStatusIndicatorStory} />
6
+
7
+ # AiStatusIndicator
8
+
9
+ ## Requesting states
10
+
11
+ ### Init
12
+ <Story id="js-packages-ai-client-aistatusindicator--init" />
13
+
14
+ ### Requesting
15
+ <Story id="js-packages-ai-client-aistatusindicator--requesting" />
16
+
17
+ ### Suggesting
18
+ <Story id="js-packages-ai-client-aistatusindicator--suggesting" />
19
+
20
+ ### Done
21
+ <Story id="js-packages-ai-client-aistatusindicator--done" />
22
+
23
+ ### Error
24
+ <Story id="js-packages-ai-client-aistatusindicator--error" />
25
+
@@ -0,0 +1,84 @@
1
+ /*
2
+ * External Dependencies
3
+ */
4
+ import React from 'react';
5
+ /*
6
+ * Internal Dependencies
7
+ */
8
+ import AiStatusIndicator, { AiStatusIndicatorProps } from '..';
9
+ import { REQUESTING_STATES } from '../../../types';
10
+
11
+ type AiStatusIndicatoryStoryProps = AiStatusIndicatorProps & {
12
+ icon: string;
13
+ children?: React.ReactNode;
14
+ };
15
+
16
+ export default {
17
+ title: 'JS Packages/AI Client/AiStatusIndicator',
18
+ component: AiStatusIndicator,
19
+ argTypes: {
20
+ state: {
21
+ control: {
22
+ type: 'select',
23
+ },
24
+ options: REQUESTING_STATES,
25
+ },
26
+ size: {
27
+ control: {
28
+ type: 'select',
29
+ },
30
+ options: [ 24, 32, 48, 64 ],
31
+ },
32
+
33
+ action: {
34
+ table: {
35
+ disable: true,
36
+ },
37
+ },
38
+ },
39
+ };
40
+
41
+ const DefaultTemplate = ( args: AiStatusIndicatoryStoryProps ) => {
42
+ const props: AiStatusIndicatorProps = {
43
+ state: args.state,
44
+ size: args.size,
45
+ };
46
+
47
+ return <AiStatusIndicator { ...props } />;
48
+ };
49
+
50
+ export const _default = DefaultTemplate.bind( {} );
51
+ _default.args = {
52
+ state: 'init',
53
+ size: 24,
54
+ };
55
+
56
+ export const Init = DefaultTemplate.bind( {} );
57
+ Init.args = {
58
+ state: 'init',
59
+ size: 48,
60
+ };
61
+
62
+ export const Requesting = DefaultTemplate.bind( {} );
63
+ Requesting.args = {
64
+ state: 'requesting',
65
+ size: 48,
66
+ };
67
+
68
+ export const Suggesting = DefaultTemplate.bind( {} );
69
+ Suggesting.args = {
70
+ state: 'suggesting',
71
+ size: 48,
72
+ };
73
+
74
+ export const Error = DefaultTemplate.bind( {} );
75
+ Error.args = {
76
+ state: 'error',
77
+ size: 48,
78
+ };
79
+
80
+ export const Done = DefaultTemplate.bind( {} );
81
+ Done.args = {
82
+ state: 'done',
83
+ size: 48,
84
+ };
@@ -0,0 +1,50 @@
1
+ .jetpack-ai-status-indicator__icon-wrapper {
2
+ color: var( --jp-green-60 );
3
+ height: 24px;
4
+
5
+ &.is-init {
6
+ color: var( --jp-green-60 );
7
+ }
8
+
9
+ &.is-requesting {
10
+ color: var( --jp-green-40 );
11
+ animation: fade 800ms linear infinite;
12
+ }
13
+
14
+ &.is-suggesting {
15
+ color: var( --jp-green-30 );
16
+ .ai-assistant-icon > path {
17
+ animation: fadingSpark 300ms ease-in-out alternate infinite;
18
+
19
+ &.spark-first {
20
+ animation-delay: 0.2s;
21
+ }
22
+
23
+ &.spark-second {
24
+ animation-delay: 0.6s;
25
+ }
26
+ }
27
+ }
28
+
29
+ &.is-done {
30
+ color: var( --jp-green-40 );
31
+ }
32
+
33
+ &.is-error {
34
+ color: var( --jp-yellow-30 );
35
+ }
36
+ }
37
+
38
+
39
+ @keyframes fadingSpark {
40
+ to {
41
+ opacity: 0.3;
42
+ }
43
+ }
44
+
45
+ @keyframes fade {
46
+ 0% { opacity: 1; }
47
+ 50% { opacity: 0.4; }
48
+ 100% { opacity: 1; }
49
+ }
50
+
@@ -1,6 +1,8 @@
1
1
 
2
2
  # AI Assistant Data Flow
3
3
 
4
+ Data Flow is a seamless and robust implementation designed to manage the state and functionality of an AI Assistant within a React application. By leveraging a React context, Higher Order Components (HOCs), and custom hooks, this implementation streamlines the interaction with the AI, handles suggestions, manages error states, and efficiently controls request functionality.
5
+
4
6
  ```jsx
5
7
  import { withAiAssistantData, useAiContext } from '@automattic/jetpack-ai-client';
6
8
 
@@ -7,8 +7,10 @@ import React from 'react';
7
7
  * Types & Constants
8
8
  */
9
9
  import SuggestionsEventSource from '../suggestions-event-source';
10
- import type { RequestingStateProp, RequestingErrorProps } from '../hooks/use-ai-suggestions';
10
+ import type { AskQuestionOptionsArgProps } from '../ask-question';
11
+ import type { RequestingErrorProps } from '../hooks/use-ai-suggestions';
11
12
  import type { PromptProp } from '../types';
13
+ import type { RequestingStateProp } from '../types';
12
14
 
13
15
  export type AiDataContextProps = {
14
16
  /*
@@ -29,7 +31,7 @@ export type AiDataContextProps = {
29
31
  /*
30
32
  * Request suggestion function
31
33
  */
32
- requestSuggestion: ( prompt: PromptProp ) => void;
34
+ requestSuggestion: ( prompt: PromptProp, options?: AskQuestionOptionsArgProps ) => void;
33
35
 
34
36
  /*
35
37
  * The Suggestions Event Source instance
@@ -5,6 +5,7 @@ import { useCallback, useContext, useEffect } from '@wordpress/element';
5
5
  /**
6
6
  * Internal dependencies
7
7
  */
8
+ import { ERROR_RESPONSE, RequestingErrorProps } from '../types';
8
9
  import { AiDataContext } from '.';
9
10
  /**
10
11
  * Types & constants
@@ -12,7 +13,7 @@ import { AiDataContext } from '.';
12
13
  import type { AiDataContextProps } from './context';
13
14
  import type { AskQuestionOptionsArgProps } from '../ask-question';
14
15
 
15
- type useAiContextOptions = {
16
+ export type UseAiContextOptions = {
16
17
  /*
17
18
  * Ask question options.
18
19
  */
@@ -27,6 +28,11 @@ type useAiContextOptions = {
27
28
  * onSuggestion callback.
28
29
  */
29
30
  onSuggestion?: ( suggestion: string ) => void;
31
+
32
+ /*
33
+ * onError callback.
34
+ */
35
+ onError?: ( error: RequestingErrorProps ) => void;
30
36
  };
31
37
 
32
38
  /**
@@ -34,13 +40,14 @@ type useAiContextOptions = {
34
40
  * the AI Assistant data (from context),
35
41
  * and to subscribe to the request events (onDone, onSuggestion).
36
42
  *
37
- * @param {useAiContextOptions} options - the hook options.
43
+ * @param {UseAiContextOptions} options - the hook options.
38
44
  * @returns {AiDataContextProps} the AI Assistant data context.
39
45
  */
40
46
  export default function useAiContext( {
41
47
  onDone,
42
48
  onSuggestion,
43
- }: useAiContextOptions = {} ): AiDataContextProps {
49
+ onError,
50
+ }: UseAiContextOptions = {} ): AiDataContextProps {
44
51
  const context = useContext( AiDataContext );
45
52
  const { eventSource } = context;
46
53
 
@@ -49,6 +56,9 @@ export default function useAiContext( {
49
56
  ( event: CustomEvent ) => onSuggestion?.( event?.detail ),
50
57
  [ onSuggestion ]
51
58
  );
59
+ const error = useCallback( ( event: CustomEvent ) => {
60
+ onError?.( event?.detail );
61
+ }, [] );
52
62
 
53
63
  useEffect( () => {
54
64
  if ( ! eventSource ) {
@@ -63,9 +73,14 @@ export default function useAiContext( {
63
73
  eventSource.addEventListener( 'suggestion', suggestion );
64
74
  }
65
75
 
76
+ if ( onError ) {
77
+ eventSource.addEventListener( ERROR_RESPONSE, error );
78
+ }
79
+
66
80
  return () => {
67
81
  eventSource.removeEventListener( 'done', done );
68
82
  eventSource.removeEventListener( 'suggestion', suggestion );
83
+ eventSource.removeEventListener( ERROR_RESPONSE, error );
69
84
  };
70
85
  }, [ eventSource ] );
71
86
 
@@ -35,7 +35,7 @@ An object with the following properties:
35
35
  - `error: RequestingErrorProps | undefined`: An error object if an error occurs.
36
36
  - `requestingState: RequestingStateProp`: The state of the request.
37
37
  - `eventSource: SuggestionsEventSource | undefined`: The event source of the request.
38
- - `request: ( prompt: Array< PromptItemProps > ) => Promise< void >`: The request handler.
38
+ - `request: ( prompt: Array< PromptItemProps >, options: AskQuestionOptionsArgProps ) => Promise< void >`: The request handler.
39
39
 
40
40
  ### `PromptItemProps`
41
41
 
@@ -21,6 +21,7 @@ import {
21
21
  import type { AskQuestionOptionsArgProps } from '../../ask-question';
22
22
  import type SuggestionsEventSource from '../../suggestions-event-source';
23
23
  import type { PromptProp, SuggestionErrorCode } from '../../types';
24
+ import type { RequestingStateProp } from '../../types';
24
25
 
25
26
  export type RequestingErrorProps = {
26
27
  /*
@@ -71,8 +72,6 @@ type useAiSuggestionsOptions = {
71
72
  onError?: ( error: RequestingErrorProps ) => void;
72
73
  };
73
74
 
74
- export type RequestingStateProp = 'init' | 'requesting' | 'suggesting' | 'done' | 'error';
75
-
76
75
  type useAiSuggestionsProps = {
77
76
  /*
78
77
  * The suggestion.
@@ -97,7 +96,7 @@ type useAiSuggestionsProps = {
97
96
  /*
98
97
  * The request handler.
99
98
  */
100
- request: ( prompt: PromptProp ) => Promise< void >;
99
+ request: ( prompt: PromptProp, options?: AskQuestionOptionsArgProps ) => Promise< void >;
101
100
  };
102
101
 
103
102
  const debug = debugFactory( 'jetpack-ai-client:use-suggestion' );
@@ -108,7 +107,7 @@ const debug = debugFactory( 'jetpack-ai-client:use-suggestion' );
108
107
  * @param {SuggestionErrorCode} errorCode - The error code.
109
108
  * @returns {RequestingErrorProps} The error data.
110
109
  */
111
- function getErrorData( errorCode: SuggestionErrorCode ): RequestingErrorProps {
110
+ export function getErrorData( errorCode: SuggestionErrorCode ): RequestingErrorProps {
112
111
  switch ( errorCode ) {
113
112
  case ERROR_QUOTA_EXCEEDED:
114
113
  return {
@@ -235,15 +234,20 @@ export default function useAiSuggestions( {
235
234
 
236
235
  const handleModerationError = useCallback( () => handleError( ERROR_MODERATION ), [] );
237
236
 
238
- const handleNetwotkError = useCallback( () => handleError( ERROR_NETWORK ), [] );
237
+ const handleNetworkError = useCallback( () => handleError( ERROR_NETWORK ), [] );
239
238
 
240
239
  /**
241
240
  * Request handler.
242
241
  *
242
+ * @param {PromptProp} promptArg - The messages array of the prompt.
243
+ * @param {AskQuestionOptionsArgProps} options - The options for the askQuestion request. Uses the hook's askQuestionOptions by default.
243
244
  * @returns {Promise<void>} The promise.
244
245
  */
245
246
  const request = useCallback(
246
- async ( promptArg: PromptProp ) => {
247
+ async (
248
+ promptArg: PromptProp,
249
+ options: AskQuestionOptionsArgProps = { ...askQuestionOptions }
250
+ ) => {
247
251
  if ( Array.isArray( promptArg ) && promptArg?.length ) {
248
252
  promptArg.forEach( ( { role, content: promptContent }, i ) =>
249
253
  debug( '(%s/%s) %o\n%s', i + 1, promptArg.length, `[${ role }]`, promptContent )
@@ -256,7 +260,7 @@ export default function useAiSuggestions( {
256
260
  setRequestingState( 'requesting' );
257
261
 
258
262
  try {
259
- eventSourceRef.current = await askQuestion( promptArg, askQuestionOptions );
263
+ eventSourceRef.current = await askQuestion( promptArg, options );
260
264
 
261
265
  if ( ! eventSourceRef?.current ) {
262
266
  return;
@@ -274,7 +278,7 @@ export default function useAiSuggestions( {
274
278
  eventSource.addEventListener( ERROR_UNCLEAR_PROMPT, handleUnclearPromptError );
275
279
  eventSource.addEventListener( ERROR_SERVICE_UNAVAILABLE, handleServiceUnavailableError );
276
280
  eventSource.addEventListener( ERROR_MODERATION, handleModerationError );
277
- eventSource.addEventListener( ERROR_NETWORK, handleNetwotkError );
281
+ eventSource.addEventListener( ERROR_NETWORK, handleNetworkError );
278
282
 
279
283
  eventSource.addEventListener( 'done', handleDone );
280
284
  } catch ( e ) {
@@ -288,7 +292,7 @@ export default function useAiSuggestions( {
288
292
  handleUnclearPromptError,
289
293
  handleServiceUnavailableError,
290
294
  handleModerationError,
291
- handleNetwotkError,
295
+ handleNetworkError,
292
296
  handleSuggestion,
293
297
  ]
294
298
  );
@@ -302,7 +306,7 @@ export default function useAiSuggestions( {
302
306
 
303
307
  // Trigger the request.
304
308
  if ( autoRequest ) {
305
- request( prompt );
309
+ request( prompt, askQuestionOptions );
306
310
  }
307
311
 
308
312
  return () => {
@@ -323,7 +327,7 @@ export default function useAiSuggestions( {
323
327
  eventSource.removeEventListener( ERROR_UNCLEAR_PROMPT, handleUnclearPromptError );
324
328
  eventSource.removeEventListener( ERROR_SERVICE_UNAVAILABLE, handleServiceUnavailableError );
325
329
  eventSource.removeEventListener( ERROR_MODERATION, handleModerationError );
326
- eventSource.removeEventListener( ERROR_NETWORK, handleNetwotkError );
330
+ eventSource.removeEventListener( ERROR_NETWORK, handleNetworkError );
327
331
 
328
332
  eventSource.removeEventListener( 'done', handleDone );
329
333
  };
@@ -3,6 +3,11 @@
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
  */
@@ -10,15 +15,16 @@ import {
10
15
  ERROR_MODERATION,
11
16
  ERROR_NETWORK,
12
17
  ERROR_QUOTA_EXCEEDED,
18
+ ERROR_RESPONSE,
13
19
  ERROR_SERVICE_UNAVAILABLE,
14
20
  ERROR_UNCLEAR_PROMPT,
15
21
  } from '../types';
16
- import type { PromptMessagesProp, PromptProp } from '../types';
22
+ import type { PromptMessagesProp, PromptProp, SuggestionErrorCode } from '../types';
17
23
 
18
24
  type SuggestionsEventSourceConstructorArgs = {
19
25
  url?: string;
20
26
  question: PromptProp;
21
- token: string;
27
+ token?: string;
22
28
  options?: {
23
29
  postId?: number;
24
30
  feature?: 'ai-assistant-experimental' | string | undefined;
@@ -79,6 +85,18 @@ export default class SuggestionsEventSource extends EventTarget {
79
85
  token,
80
86
  options = {},
81
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
+
82
100
  const bodyData: {
83
101
  post_id?: number;
84
102
  messages?: PromptMessagesProp;
@@ -150,6 +168,9 @@ export default class SuggestionsEventSource extends EventTarget {
150
168
  if ( response.ok ) {
151
169
  return;
152
170
  }
171
+
172
+ let errorCode: SuggestionErrorCode;
173
+
153
174
  if (
154
175
  response.status >= 400 &&
155
176
  response.status <= 500 &&
@@ -163,6 +184,7 @@ export default class SuggestionsEventSource extends EventTarget {
163
184
  * service unavailable
164
185
  */
165
186
  if ( response.status === 503 ) {
187
+ errorCode = ERROR_SERVICE_UNAVAILABLE;
166
188
  this.dispatchEvent( new CustomEvent( ERROR_SERVICE_UNAVAILABLE ) );
167
189
  }
168
190
 
@@ -171,6 +193,7 @@ export default class SuggestionsEventSource extends EventTarget {
171
193
  * you exceeded your current quota please check your plan and billing details
172
194
  */
173
195
  if ( response.status === 429 ) {
196
+ errorCode = ERROR_QUOTA_EXCEEDED;
174
197
  this.dispatchEvent( new CustomEvent( ERROR_QUOTA_EXCEEDED ) );
175
198
  }
176
199
 
@@ -179,9 +202,17 @@ export default class SuggestionsEventSource extends EventTarget {
179
202
  * request flagged by moderation system
180
203
  */
181
204
  if ( response.status === 422 ) {
205
+ errorCode = ERROR_MODERATION;
182
206
  this.dispatchEvent( new CustomEvent( ERROR_MODERATION ) );
183
207
  }
184
208
 
209
+ // Always dispatch a global ERROR_RESPONSE event
210
+ this.dispatchEvent(
211
+ new CustomEvent( ERROR_RESPONSE, {
212
+ detail: getErrorData( errorCode ),
213
+ } )
214
+ );
215
+
185
216
  throw new Error();
186
217
  },
187
218
 
@@ -204,6 +235,11 @@ export default class SuggestionsEventSource extends EventTarget {
204
235
  if ( replacedMessage.startsWith( 'JETPACK_AI_ERROR' ) ) {
205
236
  // The unclear prompt marker was found, so we dispatch an error event
206
237
  this.dispatchEvent( new CustomEvent( ERROR_UNCLEAR_PROMPT ) );
238
+ this.dispatchEvent(
239
+ new CustomEvent( ERROR_RESPONSE, {
240
+ detail: getErrorData( ERROR_UNCLEAR_PROMPT ),
241
+ } )
242
+ );
207
243
  } else if ( 'JETPACK_AI_ERROR'.startsWith( replacedMessage ) ) {
208
244
  // Partial unclear prompt marker was found, so we wait for more data and print a debug message without dispatching an event
209
245
  debug( this.fullMessage );
@@ -276,6 +312,11 @@ export default class SuggestionsEventSource extends EventTarget {
276
312
  processConnectionError( response ) {
277
313
  debug( 'Connection error: %o', response );
278
314
  this.dispatchEvent( new CustomEvent( ERROR_NETWORK, { detail: response } ) );
315
+ this.dispatchEvent(
316
+ new CustomEvent( ERROR_RESPONSE, {
317
+ detail: getErrorData( ERROR_NETWORK ),
318
+ } )
319
+ );
279
320
  }
280
321
 
281
322
  processErrorEvent( e ) {
@@ -283,5 +324,10 @@ export default class SuggestionsEventSource extends EventTarget {
283
324
 
284
325
  // Dispatch a generic network error event
285
326
  this.dispatchEvent( new CustomEvent( ERROR_NETWORK, { detail: e } ) );
327
+ this.dispatchEvent(
328
+ new CustomEvent( ERROR_RESPONSE, {
329
+ detail: getErrorData( ERROR_NETWORK ),
330
+ } )
331
+ );
286
332
  }
287
333
  }