@automattic/jetpack-ai-client 0.1.0 → 0.1.1

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,24 @@ 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.1] - 2023-08-01
9
+ ### Added
10
+ - Add AI Client icon components [#32079]
11
+ - AI Assistant: add function calling feature. [#32161]
12
+ - AI Client: add AI Assistant data context. [#32129]
13
+ - AI Client: add useAiContext() react hook. [#32145]
14
+ - AI Client: add useAiSuggestions() react custom hook. [#32022]
15
+ - AI Client: introduce AI Control component. [#32163]
16
+ - AI Client: introduce withAiDataProvider HOC. [#32142]
17
+
18
+ ### Changed
19
+ - AI Client: add Icon suffix to icon components. [#32173]
20
+ - AI Client: handle properly passing the post_id parameter to endpoint. [#32104]
21
+ - AI Client: replace using CSS modules with the regular way. [#32171]
22
+
23
+ ### Removed
24
+ - AI Client: remove unused image library [#32127]
25
+
8
26
  ## 0.1.0 - 2023-07-25
9
27
  ### Added
10
28
  - Add Jetpack AI Client [#30855]
@@ -17,3 +35,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
35
  - Updated package dependencies. [#31468]
18
36
  - Updated package dependencies. [#31659]
19
37
  - Updated package dependencies. [#31785]
38
+
39
+ [0.1.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.1.0...v0.1.1
package/index.ts CHANGED
@@ -1,4 +1,26 @@
1
+ /*
2
+ * Core library exports
3
+ */
1
4
  export { default as requestJwt } from './src/jwt';
2
5
  export { default as SuggestionsEventSource } from './src/suggestions-event-source';
3
6
  export { default as askQuestion } from './src/ask-question';
4
- export * from './src/index.js';
7
+
8
+ /*
9
+ * Hooks
10
+ */
11
+ export { default as useAiSuggestions } from './src/hooks/use-ai-suggestions';
12
+
13
+ /*
14
+ * Components: Icons
15
+ */
16
+ export * from './src/icons';
17
+
18
+ /*
19
+ * Components
20
+ */
21
+ export { default as AIControl } from './src/components/ai-control';
22
+
23
+ /*
24
+ * Contexts
25
+ */
26
+ export * from './src/data-flow';
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.0",
4
+ "version": "0.1.1",
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": {
@@ -21,7 +21,8 @@
21
21
  },
22
22
  "type": "module",
23
23
  "devDependencies": {
24
- "jest": "*",
24
+ "@storybook/react": "7.1.0",
25
+ "jest": "^29.6.2",
25
26
  "jest-environment-jsdom": "29.5.0",
26
27
  "typescript": "5.0.4"
27
28
  },
@@ -33,6 +34,15 @@
33
34
  "@automattic/jetpack-shared-extension-utils": "^0.10.9",
34
35
  "@microsoft/fetch-event-source": "2.0.1",
35
36
  "@wordpress/api-fetch": "6.34.0",
36
- "debug": "4.3.4"
37
+ "@wordpress/block-editor": "12.5.0",
38
+ "@wordpress/components": "25.3.0",
39
+ "@wordpress/data": "9.7.0",
40
+ "@wordpress/element": "5.14.0",
41
+ "@wordpress/i18n": "4.37.0",
42
+ "@wordpress/icons": "9.28.0",
43
+ "classnames": "2.3.2",
44
+ "debug": "4.3.4",
45
+ "react": "18.2.0",
46
+ "react-dom": "18.2.0"
37
47
  }
38
48
  }
@@ -11,7 +11,7 @@ function askQuestion(
11
11
  ): Promise< SuggestionsEventSource >
12
12
  ```
13
13
 
14
- ## Parameters
14
+ <h2 id="ask-question-parameters">Parameters</h2>
15
15
 
16
16
  - `question` (**string** | **PromptItemProps[]**):
17
17
  - The question to ask.
@@ -7,9 +7,9 @@ import SuggestionsEventSource from '../suggestions-event-source';
7
7
  /*
8
8
  * Types & constants
9
9
  */
10
- import type { PromptItemProps } from '../types';
10
+ import type { PromptProp } from '../types';
11
11
 
12
- type AskQuestionOptionsArgProps = {
12
+ export type AskQuestionOptionsArgProps = {
13
13
  /*
14
14
  * ID of the post where the question is asked.
15
15
  */
@@ -24,6 +24,16 @@ type AskQuestionOptionsArgProps = {
24
24
  * Allows to use a specific AI assistant feature. Default value is undefined.
25
25
  */
26
26
  feature?: 'ai-assistant-experimental' | string | undefined;
27
+
28
+ /*
29
+ * Allows the use of function calling. Default value is undefined.
30
+ */
31
+ functions?: Array< {
32
+ name?: string;
33
+ arguments?: string;
34
+ // eslint-disable-next-line @typescript-eslint/ban-types
35
+ implementation?: Function;
36
+ } >;
27
37
  };
28
38
 
29
39
  const debug = debugFactory( 'jetpack-ai-client:ask-question' );
@@ -32,7 +42,7 @@ const debug = debugFactory( 'jetpack-ai-client:ask-question' );
32
42
  * An asynchronous function that asks a question
33
43
  * and returns an event source with suggestions.
34
44
  *
35
- * @param {string|PromptItemProps[]} question - The question to ask. It can be a simple string or an array of PromptItemProps objects.
45
+ * @param {PromptProp} question - The question to ask. It can be a simple string or an array of PromptMessageItemProps objects.
36
46
  * @param {AskQuestionOptionsArgProps} options - An optional object for additional configuration:
37
47
  * @returns {Promise<SuggestionsEventSource>} A promise that resolves to an instance of the SuggestionsEventSource
38
48
  * @example
@@ -47,12 +57,16 @@ const debug = debugFactory( 'jetpack-ai-client:ask-question' );
47
57
  * } );
48
58
  */
49
59
  export default async function askQuestion(
50
- question: string | PromptItemProps[],
51
- { postId = null, fromCache = false, feature }: AskQuestionOptionsArgProps = {}
60
+ question: PromptProp,
61
+ { postId = null, fromCache = false, feature, functions }: AskQuestionOptionsArgProps = {}
52
62
  ): Promise< SuggestionsEventSource > {
53
- debug( 'Asking question: %o. options: %o', question, { postId, fromCache, feature } );
63
+ debug( 'Asking question: %o. options: %o', question, { postId, fromCache, feature, functions } );
54
64
 
55
65
  const { token } = await requestJwt();
56
66
 
57
- return new SuggestionsEventSource( { question, token, options: { postId, feature, fromCache } } );
67
+ return new SuggestionsEventSource( {
68
+ question,
69
+ token,
70
+ options: { postId, feature, fromCache, functions },
71
+ } );
58
72
  }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { PlainText } from '@wordpress/block-editor';
5
+ import { Button, Spinner } from '@wordpress/components';
6
+ import { useRef } from '@wordpress/element';
7
+ import { __ } from '@wordpress/i18n';
8
+ import { Icon, closeSmall, check, arrowUp } from '@wordpress/icons';
9
+ import classNames from 'classnames';
10
+ /**
11
+ * Internal dependencies
12
+ */
13
+ import { aiAssistantIcon } from '../../icons';
14
+ import './style.scss';
15
+
16
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
17
+ const noop = () => {};
18
+
19
+ /**
20
+ * AI Control component.
21
+ *
22
+ * @param {object} props - component props
23
+ * @param {boolean} props.loading - loading state
24
+ * @param {string} props.value - input value
25
+ * @param {string} props.placeholder - input placeholder
26
+ * @param {boolean} props.showAccept - show accept button
27
+ * @param {string} props.acceptLabel - accept button label
28
+ * @param {boolean} props.showButtonsLabel - show buttons label
29
+ * @param {boolean} props.isOpaque - is opaque
30
+ * @param {Function} props.onChange - input change handler
31
+ * @param {Function} props.onSend - send request handler
32
+ * @param {Function} props.onStop - stop request handler
33
+ * @param {Function} props.onAccept - accept handler
34
+ * @returns {object} - AI Control component
35
+ */
36
+ export default function AIControl( {
37
+ loading = false,
38
+ value = '',
39
+ placeholder = '',
40
+ showAccept = false,
41
+ acceptLabel = __( 'Accept', 'jetpack-ai-client' ),
42
+ showButtonsLabel = true,
43
+ isOpaque = false,
44
+ onChange = noop,
45
+ onSend = noop,
46
+ onStop = noop,
47
+ onAccept = noop,
48
+ }: {
49
+ loading?: boolean;
50
+ value: string;
51
+ placeholder?: string;
52
+ showAccept?: boolean;
53
+ acceptLabel?: string;
54
+ showButtonsLabel?: boolean;
55
+ isOpaque?: boolean;
56
+ onChange: ( newValue: string ) => void;
57
+ onSend: ( currentValue: string ) => void;
58
+ onStop: () => void;
59
+ onAccept: () => void;
60
+ } ) {
61
+ const promptUserInputRef = useRef( null );
62
+
63
+ return (
64
+ <div className="jetpack-components-ai-control__container">
65
+ <div
66
+ className={ classNames( 'jetpack-components-ai-control__wrapper', {
67
+ 'is-opaque': isOpaque,
68
+ } ) }
69
+ >
70
+ <div className="jetpack-components-ai-controlton__icon">
71
+ { loading ? (
72
+ <Spinner className="input-spinner" />
73
+ ) : (
74
+ <Icon className="input-icon" icon={ aiAssistantIcon } size={ 24 } />
75
+ ) }
76
+ </div>
77
+
78
+ <PlainText
79
+ value={ value }
80
+ onChange={ onChange }
81
+ placeholder={ placeholder }
82
+ className="jetpack-components-ai-control__input"
83
+ disabled={ loading }
84
+ ref={ promptUserInputRef }
85
+ />
86
+
87
+ <div className="jetpack-components-ai-control__controls">
88
+ <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
89
+ { ! loading ? (
90
+ <Button
91
+ className="jetpack-components-ai-control__controls-prompt_button"
92
+ onClick={ () => onSend( value ) }
93
+ isSmall={ true }
94
+ disabled={ value?.length }
95
+ label={ __( 'Send request', 'jetpack-ai-client' ) }
96
+ >
97
+ <Icon icon={ arrowUp } />
98
+ { showButtonsLabel && __( 'Send', 'jetpack-ai-client' ) }
99
+ </Button>
100
+ ) : (
101
+ <Button
102
+ className="jetpack-components-ai-control__controls-prompt_button"
103
+ onClick={ onStop }
104
+ isSmall={ true }
105
+ label={ __( 'Stop request', 'jetpack-ai-client' ) }
106
+ >
107
+ <Icon icon={ closeSmall } />
108
+ { showButtonsLabel && __( 'Stop', 'jetpack-ai-client' ) }
109
+ </Button>
110
+ ) }
111
+ </div>
112
+
113
+ { showAccept && (
114
+ <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
115
+ <Button
116
+ className="jetpack-components-ai-control__controls-prompt_button"
117
+ onClick={ onAccept }
118
+ isSmall={ true }
119
+ label={ acceptLabel }
120
+ >
121
+ <Icon icon={ check } />
122
+ { acceptLabel }
123
+ </Button>
124
+ </div>
125
+ ) }
126
+ </div>
127
+ </div>
128
+ </div>
129
+ );
130
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { useState } from '@wordpress/element';
5
+ import React from 'react';
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import AIControl from '../index';
10
+ /**
11
+ * Types
12
+ */
13
+ import type { Meta } from '@storybook/react';
14
+
15
+ export default {
16
+ title: 'JS Packages/AI Client/AI Control',
17
+ component: AIControl,
18
+ parameters: {},
19
+ } as Meta< typeof AIControl >;
20
+
21
+ const Template = args => {
22
+ const [ value, setValue ] = useState( '' );
23
+
24
+ const handleChange = ( newValue: string ) => {
25
+ setValue( newValue );
26
+ args?.onChange?.( newValue );
27
+ };
28
+
29
+ return <AIControl { ...args } onChange={ handleChange } value={ args?.value ?? value } />;
30
+ };
31
+
32
+ const DefaultArgs = {
33
+ loading: false,
34
+ isOpaque: false,
35
+ placeholder: '',
36
+ showButtonsLabel: true,
37
+ showAccept: false,
38
+ acceptLabel: 'Accept',
39
+ };
40
+
41
+ export const Default = Template.bind( {} );
42
+ Default.args = DefaultArgs;
@@ -0,0 +1,107 @@
1
+ .jetpack-components-ai-control__container {
2
+ color: var( --jp-gray-80 );
3
+ background-color: var( --jp-white );
4
+ box-shadow: var( --wp--preset--color--cyan-bluish-gray ) 0px 0px 0px 1px, var( --wp--preset--color--cyan-bluish-gray ) 0px 0px 8px;
5
+ font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
6
+ }
7
+
8
+ .jetpack-components-ai-control__wrapper {
9
+ display: flex;
10
+ padding: 12px 14px;
11
+ gap: 8px;
12
+
13
+ &.is-opaque {
14
+ opacity: 0.4;
15
+ }
16
+
17
+ textarea.jetpack-components-ai-control__input {
18
+ width: 100%;
19
+ min-height: 20px;
20
+ flex-grow: 1;
21
+ border-radius: 2px;
22
+ padding: 6px 8px;
23
+
24
+ resize: none !important;
25
+ border: none;
26
+ box-shadow: none;
27
+ color: var( --jp-gray-80 );
28
+
29
+ font-size: 16px;
30
+ font-weight: 400;
31
+ line-height: 20px;
32
+
33
+ transition: box-shadow 0.1s linear 0s;
34
+
35
+ &:focus {
36
+ box-shadow: none;
37
+ color: var( --jp-gray-80 );
38
+ }
39
+
40
+ @media (min-width: 600px) {
41
+ font-size: 13px;
42
+ }
43
+
44
+ &::placeholder {
45
+ text-overflow: ellipsis;
46
+ white-space: nowrap;
47
+ opacity: 0.75;
48
+ }
49
+ }
50
+ }
51
+
52
+ .jetpack-components-ai-controlton__icon {
53
+ flex-shrink: 0;
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+
58
+ .input-icon {
59
+ // Brand green regardless of theme
60
+ color: var( --jp-green-40 );
61
+ }
62
+
63
+ .input-spinner {
64
+ margin: 0;
65
+ width: 24px;
66
+ height: 24px;
67
+
68
+ path {
69
+ color: var( --jp-green-40 );
70
+ }
71
+ }
72
+ }
73
+
74
+ .jetpack-components-ai-control__controls {
75
+ display: flex;
76
+ align-items: center;
77
+ }
78
+
79
+ .jetpack-components-ai-control__controls-prompt_button_wrapper {
80
+ text-transform: uppercase;
81
+ font-size: 11px;
82
+ font-weight: 600;
83
+ line-height: 16px;
84
+ user-select: none;
85
+ white-space: nowrap;
86
+ display: flex;
87
+ align-items: center;
88
+ }
89
+
90
+ .jetpack-components-ai-control__controls-prompt_button {
91
+ color: var( --jp-gray-80 );
92
+ text-transform: uppercase;
93
+
94
+ > SVG {
95
+ margin-right: 4px;
96
+ }
97
+
98
+ &:hover,
99
+ &:active {
100
+ color: var( --wp-components-color-accent, var( --wp-admin-theme-color ) );
101
+ }
102
+
103
+ &:disabled {
104
+ opacity: 0.6;
105
+ cursor: not-allowed;
106
+ }
107
+ }
@@ -0,0 +1,113 @@
1
+
2
+ # AI Assistant Data Flow
3
+
4
+ ```jsx
5
+ import { withAiAssistantData, useAiContext } from '@automattic/jetpack-ai-client';
6
+
7
+ const MyComponent = () => {
8
+ const { suggestion, requestingState, requestSuggestion } = useAiContext( {
9
+ onDone: content => console.log( `Content is done: ${ content }.` );
10
+ } );
11
+
12
+ return (
13
+ <>
14
+ <div>{ suggestions }</div>
15
+ <button
16
+ onClick={ () => requestSuggestion( 'How to make a cake' ) }
17
+ disabled={ requestingState === 'suggesting' }
18
+ >
19
+ Request
20
+ </button>
21
+ <>
22
+ )
23
+ };
24
+
25
+ // Ensure to provide the data context to `MyComponent`.
26
+ export default withAiAssistantData( MyComponent );
27
+
28
+ ```
29
+
30
+ ## In-depth Analysis
31
+
32
+ * [AI Data Context](#ai-assistant-content)
33
+ * [withAiDataProvider HOC](#with-ai-data-provider)
34
+ * [useAiContext Hook](#use-ai-context)
35
+
36
+ <h2 id="ai-assistant-content">Ai Data Context</h2>
37
+
38
+ The Ai Data Context is a React context implementation for managing the state and functionality of an AI Assistant. It manages the suggestion values, error states, and request functionality.
39
+
40
+
41
+ ### Usage
42
+
43
+ Import the Ai Data Context and Provider into your component:
44
+
45
+ ```javascript
46
+ import { AiDataContext, AiDataContextProvider } from '@automattic/jetpack-ai-client';
47
+ ```
48
+
49
+ Use the Provider in your component's render method to wrap the children components:
50
+
51
+ ```es6
52
+ <AiDataContextProvider value={ value }>
53
+ { children }
54
+ </AiDataContextProvider>
55
+ ```
56
+
57
+ You can access the context values in your child components using the `useContext` hook:
58
+
59
+ ```javascript
60
+ const aiContext = React.useContext( AiDataContext );
61
+ ```
62
+
63
+ ### Context Values
64
+
65
+ The Ai Data Context has the following values:
66
+
67
+ #### `suggestion`
68
+ The suggestion value from the AI.
69
+
70
+ #### `requestingError`
71
+ The error object returned from the AI suggestion request. It contains the following properties:
72
+ - `code`: A code referring to the type of error. The possible error codes are `ERROR_SERVICE_UNAVAILABLE`, `ERROR_QUOTA_EXCEEDED`, `ERROR_MODERATION`, `ERROR_NETWORK`, `ERROR_UNCLEAR_PROMPT`.
73
+ - `message`: A user-friendly error message.
74
+ - `severity`: The severity of the error. It can either be 'info' or 'error'.
75
+
76
+ #### `requestingState`
77
+ The current state of the suggestion request. It can be one of the following:
78
+ - `init`: The initial state before a request is made.
79
+ - `requesting`: The state when a request is being made.
80
+ - `suggesting`: The state when the AI is generating a suggestion.
81
+ - `done`: The state when a suggestion has been received.
82
+ - `error`: The state when an error has occurred during the request.
83
+
84
+ #### `requestSuggestion`
85
+ A function to request a suggestion from the AI. The function takes a prompt parameter which can be an object of `PromptMessagesProp` or a string.
86
+
87
+ <h2 id="with-ai-data-provider">withAiDataProvider HOC</h2>
88
+
89
+ Higher Order Component (HOC) that wraps a given component and provides it with the AI Assistant Data context. This HOC is instrumental in the data flow of the AI Assistant functionality and helps manage the interaction with the AI Assistant's communication layer.
90
+
91
+ <h2 id="use-ai-context">useAiContext Hook</h2>
92
+
93
+ The `useAiContext` hook provides a convenient way to access the
94
+ Ai Data Context and subscribe to the `done` and `suggestion` events emitted by SuggestionsEventSource.
95
+
96
+ ```es6
97
+ const { suggestion } = useAiContext( {
98
+ onDone: content => console.log( content ),
99
+ onSuggestion: suggestion => console.log( suggestion ),
100
+ } );
101
+
102
+ ```
103
+
104
+ _Before using the hook, ensure the data is provided by the [Ai Data Context](#ai-assistant-content). [withAiDataProvider HOC](#with-ai-data-provider) is usually the best option_
105
+
106
+ Optional options object:
107
+
108
+ - `onDone`: A callback function to be called when a request completes. The callback receives the request result as a parameter.
109
+ - `onSuggestion`: A callback function to be called when a new suggestion is received. The callback receives the suggestion as a parameter.
110
+
111
+ These callbacks will be invoked with the detail of the corresponding event emitted by SuggestionsEventSource.
112
+
113
+ When called, the hook returns the Ai Data Context.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { createContext } from '@wordpress/element';
5
+ import React from 'react';
6
+ /**
7
+ * Types & Constants
8
+ */
9
+ import SuggestionsEventSource from '../suggestions-event-source';
10
+ import type { RequestingStateProp, RequestingErrorProps } from '../hooks/use-ai-suggestions';
11
+ import type { PromptProp } from '../types';
12
+
13
+ export type AiDataContextProps = {
14
+ /*
15
+ * Suggestion value
16
+ */
17
+ suggestion: string;
18
+
19
+ /*
20
+ * Suggestion error
21
+ */
22
+ requestingError: RequestingErrorProps;
23
+
24
+ /*
25
+ * Requesting state
26
+ */
27
+ requestingState: RequestingStateProp;
28
+
29
+ /*
30
+ * Request suggestion function
31
+ */
32
+ requestSuggestion: ( prompt: PromptProp ) => void;
33
+
34
+ /*
35
+ * The Suggestions Event Source instance
36
+ */
37
+ eventSource: SuggestionsEventSource | null;
38
+ };
39
+
40
+ type AiDataContextProviderProps = {
41
+ /*
42
+ * Data to provide to the context
43
+ */
44
+ value: AiDataContextProps;
45
+
46
+ /*
47
+ * Children
48
+ */
49
+ children: React.ReactNode;
50
+ };
51
+
52
+ /**
53
+ * AI Data Context
54
+ *
55
+ * @returns {AiDataContextProps} Context.
56
+ */
57
+ export const AiDataContext = createContext( {} as AiDataContextProps );
58
+
59
+ /**
60
+ * AI Data Context Provider
61
+ *
62
+ * @param {AiDataContextProviderProps} props - Component props.
63
+ * @returns {React.ReactNode} Context provider.
64
+ * @example
65
+ * <AiDataContextProvider value={ value }>
66
+ * { children }
67
+ * </AiDataContextProvider>
68
+ */
69
+ export const AiDataContextProvider = ( {
70
+ value,
71
+ children,
72
+ }: AiDataContextProviderProps ): React.ReactNode => (
73
+ <AiDataContext.Provider value={ value } children={ children } />
74
+ );
@@ -0,0 +1,3 @@
1
+ export { AiDataContext, AiDataContextProvider } from './context';
2
+ export { default as withAiDataProvider } from './with-ai-assistant-data';
3
+ export { default as useAiContext } from './use-ai-context';