@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.
- package/CHANGELOG.md +34 -0
- package/README.md +18 -96
- package/index.ts +5 -0
- package/package.json +12 -9
- package/src/ask-question/index.ts +0 -4
- package/src/components/ai-control/Readme.md +29 -0
- package/src/components/ai-control/index.tsx +101 -60
- package/src/components/ai-control/message.tsx +86 -0
- package/src/components/ai-control/stories/index.stories.tsx +27 -1
- package/src/components/ai-control/style.scss +58 -12
- package/src/components/ai-status-indicator/Readme.md +11 -0
- package/src/components/ai-status-indicator/index.tsx +43 -0
- package/src/components/ai-status-indicator/stories/index.mdx +25 -0
- package/src/components/ai-status-indicator/stories/index.stories.tsx +84 -0
- package/src/components/ai-status-indicator/style.scss +50 -0
- package/src/data-flow/Readme.md +2 -0
- package/src/data-flow/context.tsx +4 -2
- package/src/data-flow/use-ai-context.ts +18 -3
- package/src/hooks/use-ai-suggestions/Readme.md +1 -1
- package/src/hooks/use-ai-suggestions/index.ts +15 -11
- package/src/suggestions-event-source/index.ts +48 -2
- package/src/types.ts +29 -1
|
@@ -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
|
-
|
|
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
|
+
|
package/src/data-flow/Readme.md
CHANGED
|
@@ -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 {
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
}
|