@automattic/jetpack-ai-client 0.12.2 → 0.12.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.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { PlainText } from '@wordpress/block-editor';
5
+ import classNames from 'classnames';
6
+ import React from 'react';
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import AiStatusIndicator from '../ai-status-indicator/index.js';
11
+ import './style.scss';
12
+ /**
13
+ * Types
14
+ */
15
+ import type { RequestingStateProp } from '../../types.js';
16
+ import type { ReactElement } from 'react';
17
+
18
+ type AIControlProps = {
19
+ disabled?: boolean;
20
+ value: string;
21
+ placeholder?: string;
22
+ isTransparent?: boolean;
23
+ state?: RequestingStateProp;
24
+ onChange?: ( newValue: string ) => void;
25
+ banner?: ReactElement;
26
+ error?: ReactElement;
27
+ actions?: ReactElement;
28
+ message?: ReactElement;
29
+ promptUserInputRef?: React.MutableRefObject< HTMLInputElement >;
30
+ };
31
+
32
+ /**
33
+ * Base AIControl component. Contains the main structure of the control component and slots for banner, error, actions and message.
34
+ *
35
+ * @param {AIControlProps} props - Component props
36
+ * @returns {ReactElement} Rendered component
37
+ */
38
+ export default function AIControl( {
39
+ disabled = false,
40
+ value = '',
41
+ placeholder = '',
42
+ isTransparent = false,
43
+ state = 'init',
44
+ onChange,
45
+ banner = null,
46
+ error = null,
47
+ actions = null,
48
+ message = null,
49
+ promptUserInputRef = null,
50
+ }: AIControlProps ): ReactElement {
51
+ return (
52
+ <div className="jetpack-components-ai-control__container-wrapper">
53
+ { error }
54
+ <div className="jetpack-components-ai-control__container">
55
+ { banner }
56
+ <div
57
+ className={ classNames( 'jetpack-components-ai-control__wrapper', {
58
+ 'is-transparent': isTransparent,
59
+ } ) }
60
+ >
61
+ <AiStatusIndicator state={ state } />
62
+
63
+ <div className="jetpack-components-ai-control__input-wrapper">
64
+ <PlainText
65
+ value={ value }
66
+ onChange={ onChange }
67
+ placeholder={ placeholder }
68
+ className="jetpack-components-ai-control__input"
69
+ disabled={ disabled }
70
+ ref={ promptUserInputRef }
71
+ />
72
+ </div>
73
+ { actions }
74
+ </div>
75
+ { message }
76
+ </div>
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { Button, ButtonGroup } from '@wordpress/components';
5
+ import { useKeyboardShortcut } from '@wordpress/compose';
6
+ import { useImperativeHandle, useRef, useEffect, useCallback, useState } from '@wordpress/element';
7
+ import { __ } from '@wordpress/i18n';
8
+ import {
9
+ Icon,
10
+ closeSmall,
11
+ check,
12
+ arrowUp,
13
+ trash,
14
+ reusableBlock as regenerate,
15
+ } from '@wordpress/icons';
16
+ import debugFactory from 'debug';
17
+ import React, { forwardRef } from 'react';
18
+ /**
19
+ * Internal dependencies
20
+ */
21
+ import { GuidelineMessage } from '../message/index.js';
22
+ import AIControl from './ai-control.js';
23
+ import './style.scss';
24
+ /**
25
+ * Types
26
+ */
27
+ import type { RequestingStateProp } from '../../types.js';
28
+ import type { ReactElement } from 'react';
29
+
30
+ type BlockAIControlProps = {
31
+ disabled?: boolean;
32
+ value: string;
33
+ placeholder?: string;
34
+ showAccept?: boolean;
35
+ acceptLabel?: string;
36
+ showButtonLabels?: boolean;
37
+ isTransparent?: boolean;
38
+ state?: RequestingStateProp;
39
+ showGuideLine?: boolean;
40
+ customFooter?: ReactElement;
41
+ onChange?: ( newValue: string ) => void;
42
+ onSend?: ( currentValue: string ) => void;
43
+ onStop?: () => void;
44
+ onAccept?: () => void;
45
+ onDiscard?: () => void;
46
+ showRemove?: boolean;
47
+ banner?: ReactElement;
48
+ error?: ReactElement;
49
+ };
50
+
51
+ const debug = debugFactory( 'jetpack-ai-client:block-ai-control' );
52
+
53
+ /**
54
+ * BlockAIControl component. Used by the AI Assistant block, adding logic and components to the base AIControl component.
55
+ *
56
+ * @param {BlockAIControlProps} props - Component props
57
+ * @param {React.MutableRefObject} ref - Ref to the component
58
+ * @returns {ReactElement} Rendered component
59
+ */
60
+ export function BlockAIControl(
61
+ {
62
+ disabled = false,
63
+ value = '',
64
+ placeholder = '',
65
+ showAccept = false,
66
+ acceptLabel = __( 'Accept', 'jetpack-ai-client' ),
67
+ showButtonLabels = true,
68
+ isTransparent = false,
69
+ state = 'init',
70
+ showGuideLine = false,
71
+ customFooter = null,
72
+ onChange,
73
+ onSend,
74
+ onStop,
75
+ onAccept,
76
+ onDiscard,
77
+ showRemove = false,
78
+ banner = null,
79
+ error = null,
80
+ }: BlockAIControlProps,
81
+ ref: React.MutableRefObject< HTMLInputElement >
82
+ ): ReactElement {
83
+ const loading = state === 'requesting' || state === 'suggesting';
84
+ const [ editRequest, setEditRequest ] = useState( false );
85
+ const [ lastValue, setLastValue ] = useState( value || null );
86
+ const promptUserInputRef = useRef( null );
87
+
88
+ // Pass the ref to forwardRef.
89
+ useImperativeHandle( ref, () => promptUserInputRef.current );
90
+
91
+ useEffect( () => {
92
+ if ( editRequest ) {
93
+ promptUserInputRef?.current?.focus();
94
+ }
95
+ }, [ editRequest ] );
96
+
97
+ const sendHandler = useCallback( () => {
98
+ setLastValue( value );
99
+ setEditRequest( false );
100
+ onSend?.( value );
101
+ }, [ value ] );
102
+
103
+ const changeHandler = useCallback(
104
+ ( newValue: string ) => {
105
+ onChange?.( newValue );
106
+ if ( state === 'init' ) {
107
+ return;
108
+ }
109
+
110
+ if ( ! lastValue ) {
111
+ // here we're coming from a one-click action
112
+ setEditRequest( newValue.length > 0 );
113
+ } else {
114
+ // here we're coming from an edit action
115
+ setEditRequest( newValue !== lastValue );
116
+ }
117
+ },
118
+ [ lastValue, state ]
119
+ );
120
+
121
+ const discardHandler = useCallback( () => {
122
+ onDiscard?.();
123
+ }, [] );
124
+
125
+ const cancelEdit = useCallback( () => {
126
+ debug( 'cancelEdit, revert to last value', lastValue );
127
+ onChange?.( lastValue || '' );
128
+ setEditRequest( false );
129
+ }, [ lastValue ] );
130
+
131
+ useKeyboardShortcut(
132
+ 'mod+enter',
133
+ () => {
134
+ if ( showAccept ) {
135
+ onAccept?.();
136
+ }
137
+ },
138
+ {
139
+ target: promptUserInputRef,
140
+ }
141
+ );
142
+
143
+ useKeyboardShortcut(
144
+ 'enter',
145
+ e => {
146
+ e.preventDefault();
147
+ sendHandler();
148
+ },
149
+ {
150
+ target: promptUserInputRef,
151
+ }
152
+ );
153
+
154
+ const actions = (
155
+ <>
156
+ { ( ! showAccept || editRequest ) && (
157
+ <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
158
+ { ! loading ? (
159
+ <>
160
+ { editRequest && (
161
+ <Button
162
+ className="jetpack-components-ai-control__controls-prompt_button"
163
+ onClick={ cancelEdit }
164
+ variant="secondary"
165
+ label={ __( 'Cancel', 'jetpack-ai-client' ) }
166
+ >
167
+ { showButtonLabels ? (
168
+ __( 'Cancel', 'jetpack-ai-client' )
169
+ ) : (
170
+ <Icon icon={ closeSmall } />
171
+ ) }
172
+ </Button>
173
+ ) }
174
+
175
+ { showRemove && ! editRequest && ! value?.length && onDiscard && (
176
+ <Button
177
+ className="jetpack-components-ai-control__controls-prompt_button"
178
+ onClick={ discardHandler }
179
+ variant="secondary"
180
+ label={ __( 'Cancel', 'jetpack-ai-client' ) }
181
+ >
182
+ { showButtonLabels ? (
183
+ __( 'Cancel', 'jetpack-ai-client' )
184
+ ) : (
185
+ <Icon icon={ closeSmall } />
186
+ ) }
187
+ </Button>
188
+ ) }
189
+
190
+ { value?.length > 0 && (
191
+ <Button
192
+ className="jetpack-components-ai-control__controls-prompt_button"
193
+ onClick={ sendHandler }
194
+ variant="primary"
195
+ disabled={ ! value?.length || disabled }
196
+ label={ __( 'Send request', 'jetpack-ai-client' ) }
197
+ >
198
+ { showButtonLabels ? (
199
+ __( 'Generate', 'jetpack-ai-client' )
200
+ ) : (
201
+ <Icon icon={ arrowUp } />
202
+ ) }
203
+ </Button>
204
+ ) }
205
+ </>
206
+ ) : (
207
+ <Button
208
+ className="jetpack-components-ai-control__controls-prompt_button"
209
+ onClick={ onStop }
210
+ variant="secondary"
211
+ label={ __( 'Stop request', 'jetpack-ai-client' ) }
212
+ >
213
+ { showButtonLabels ? (
214
+ __( 'Stop', 'jetpack-ai-client' )
215
+ ) : (
216
+ <Icon icon={ closeSmall } />
217
+ ) }
218
+ </Button>
219
+ ) }
220
+ </div>
221
+ ) }
222
+ { showAccept && ! editRequest && (
223
+ <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
224
+ { ( value?.length > 0 || lastValue === null ) && (
225
+ <ButtonGroup>
226
+ <Button
227
+ className="jetpack-components-ai-control__controls-prompt_button"
228
+ label={ __( 'Discard', 'jetpack-ai-client' ) }
229
+ onClick={ discardHandler }
230
+ tooltipPosition="top"
231
+ >
232
+ <Icon icon={ trash } />
233
+ </Button>
234
+ <Button
235
+ className="jetpack-components-ai-control__controls-prompt_button"
236
+ label={ __( 'Regenerate', 'jetpack-ai-client' ) }
237
+ onClick={ () => onSend?.( value ) }
238
+ tooltipPosition="top"
239
+ disabled={ ! value?.length || value === null || disabled }
240
+ >
241
+ <Icon icon={ regenerate } />
242
+ </Button>
243
+ </ButtonGroup>
244
+ ) }
245
+ <Button
246
+ className="jetpack-components-ai-control__controls-prompt_button"
247
+ onClick={ onAccept }
248
+ variant="primary"
249
+ label={ acceptLabel }
250
+ >
251
+ { showButtonLabels ? acceptLabel : <Icon icon={ check } /> }
252
+ </Button>
253
+ </div>
254
+ ) }
255
+ </>
256
+ );
257
+
258
+ const message =
259
+ showGuideLine && ! loading && ! editRequest && ( customFooter || <GuidelineMessage /> );
260
+
261
+ return (
262
+ <AIControl
263
+ disabled={ disabled || loading }
264
+ value={ value }
265
+ placeholder={ placeholder }
266
+ isTransparent={ isTransparent }
267
+ state={ state }
268
+ onChange={ changeHandler }
269
+ banner={ banner }
270
+ error={ error }
271
+ actions={ actions }
272
+ message={ message }
273
+ promptUserInputRef={ promptUserInputRef }
274
+ />
275
+ );
276
+ }
277
+
278
+ export default forwardRef( BlockAIControl );
@@ -0,0 +1,217 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { Button, ButtonGroup } from '@wordpress/components';
5
+ import { useKeyboardShortcut } from '@wordpress/compose';
6
+ import { useImperativeHandle, useRef, useEffect, useCallback, useState } from '@wordpress/element';
7
+ import { __ } from '@wordpress/i18n';
8
+ import { Icon, closeSmall, arrowUp, undo } from '@wordpress/icons';
9
+ import React, { forwardRef } from 'react';
10
+ /**
11
+ * Internal dependencies
12
+ */
13
+ import { GuidelineMessage, ErrorMessage, UpgradeMessage } from '../message/index.js';
14
+ import AIControl from './ai-control.js';
15
+ import './style.scss';
16
+ /**
17
+ * Types
18
+ */
19
+ import type { RequestingStateProp } from '../../types.js';
20
+ import type { ReactElement } from 'react';
21
+
22
+ type ExtensionAIControlProps = {
23
+ disabled?: boolean;
24
+ value: string;
25
+ placeholder?: string;
26
+ showButtonLabels?: boolean;
27
+ isTransparent?: boolean;
28
+ state?: RequestingStateProp;
29
+ showGuideLine?: boolean;
30
+ error?: string;
31
+ requestsRemaining?: number;
32
+ showUpgradeMessage?: boolean;
33
+ onChange?: ( newValue: string ) => void;
34
+ onSend?: ( currentValue: string ) => void;
35
+ onStop?: () => void;
36
+ onClose?: () => void;
37
+ onUndo?: () => void;
38
+ onUpgrade?: () => void;
39
+ };
40
+
41
+ /**
42
+ * ExtensionAIControl component. Used by the AI Assistant inline extensions, adding logic and components to the base AIControl component.
43
+ *
44
+ * @param {ExtensionAIControlProps} props - Component props
45
+ * @param {React.MutableRefObject} ref - Ref to the component
46
+ * @returns {ReactElement} Rendered component
47
+ */
48
+ export function ExtensionAIControl(
49
+ {
50
+ disabled = false,
51
+ value = '',
52
+ placeholder = '',
53
+ showButtonLabels = true,
54
+ isTransparent = false,
55
+ state = 'init',
56
+ showGuideLine = false,
57
+ error,
58
+ requestsRemaining,
59
+ showUpgradeMessage = false,
60
+ onChange,
61
+ onSend,
62
+ onStop,
63
+ onClose,
64
+ onUndo,
65
+ onUpgrade,
66
+ }: ExtensionAIControlProps,
67
+ ref: React.MutableRefObject< HTMLInputElement >
68
+ ): ReactElement {
69
+ const loading = state === 'requesting' || state === 'suggesting';
70
+ const [ editRequest, setEditRequest ] = useState( false );
71
+ const [ lastValue, setLastValue ] = useState( value || null );
72
+ const promptUserInputRef = useRef( null );
73
+
74
+ // Pass the ref to forwardRef.
75
+ useImperativeHandle( ref, () => promptUserInputRef.current );
76
+
77
+ useEffect( () => {
78
+ if ( editRequest ) {
79
+ promptUserInputRef?.current?.focus();
80
+ }
81
+ }, [ editRequest ] );
82
+
83
+ const sendHandler = useCallback( () => {
84
+ setLastValue( value );
85
+ setEditRequest( false );
86
+ onSend?.( value );
87
+ }, [ onSend, value ] );
88
+
89
+ const changeHandler = useCallback(
90
+ ( newValue: string ) => {
91
+ onChange?.( newValue );
92
+ if ( state === 'init' ) {
93
+ return;
94
+ }
95
+
96
+ if ( ! lastValue ) {
97
+ // here we're coming from a one-click action
98
+ setEditRequest( newValue.length > 0 );
99
+ } else {
100
+ // here we're coming from an edit action
101
+ setEditRequest( newValue !== lastValue );
102
+ }
103
+ },
104
+ [ onChange, lastValue, state ]
105
+ );
106
+
107
+ const stopHandler = useCallback( () => {
108
+ onStop?.();
109
+ }, [ onStop ] );
110
+
111
+ const closeHandler = useCallback( () => {
112
+ onClose?.();
113
+ }, [ onClose ] );
114
+
115
+ const undoHandler = useCallback( () => {
116
+ onUndo?.();
117
+ }, [ onUndo ] );
118
+
119
+ const upgradeHandler = useCallback( () => {
120
+ onUpgrade?.();
121
+ }, [ onUpgrade ] );
122
+
123
+ useKeyboardShortcut(
124
+ 'enter',
125
+ e => {
126
+ e.preventDefault();
127
+ sendHandler();
128
+ },
129
+ {
130
+ target: promptUserInputRef,
131
+ }
132
+ );
133
+
134
+ const actions = (
135
+ <>
136
+ { loading ? (
137
+ <Button
138
+ className="jetpack-components-ai-control__controls-prompt_button"
139
+ onClick={ stopHandler }
140
+ variant="secondary"
141
+ label={ __( 'Stop request', 'jetpack-ai-client' ) }
142
+ >
143
+ { showButtonLabels ? __( 'Stop', 'jetpack-ai-client' ) : <Icon icon={ closeSmall } /> }
144
+ </Button>
145
+ ) : (
146
+ <>
147
+ { value?.length > 0 && (
148
+ <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
149
+ <Button
150
+ className="jetpack-components-ai-control__controls-prompt_button"
151
+ onClick={ sendHandler }
152
+ variant="primary"
153
+ disabled={ ! value?.length || disabled }
154
+ label={ __( 'Send request', 'jetpack-ai-client' ) }
155
+ >
156
+ { showButtonLabels ? (
157
+ __( 'Generate', 'jetpack-ai-client' )
158
+ ) : (
159
+ <Icon icon={ arrowUp } />
160
+ ) }
161
+ </Button>
162
+ </div>
163
+ ) }
164
+ { value?.length <= 0 && state === 'done' && (
165
+ <div className="jetpack-components-ai-control__controls-prompt_button_wrapper">
166
+ <ButtonGroup>
167
+ <Button
168
+ className="jetpack-components-ai-control__controls-prompt_button"
169
+ label={ __( 'Undo', 'jetpack-ai-client' ) }
170
+ onClick={ undoHandler }
171
+ tooltipPosition="top"
172
+ >
173
+ <Icon icon={ undo } />
174
+ </Button>
175
+ <Button
176
+ className="jetpack-components-ai-control__controls-prompt_button"
177
+ label={ __( 'Close', 'jetpack-ai-client' ) }
178
+ onClick={ closeHandler }
179
+ variant="tertiary"
180
+ >
181
+ { __( 'Close', 'jetpack-ai-client' ) }
182
+ </Button>
183
+ </ButtonGroup>
184
+ </div>
185
+ ) }
186
+ </>
187
+ ) }
188
+ </>
189
+ );
190
+
191
+ let message = null;
192
+ if ( error ) {
193
+ message = <ErrorMessage error={ error } onTryAgainClick={ sendHandler } />;
194
+ } else if ( showUpgradeMessage ) {
195
+ message = (
196
+ <UpgradeMessage requestsRemaining={ requestsRemaining } onUpgradeClick={ upgradeHandler } />
197
+ );
198
+ } else if ( showGuideLine ) {
199
+ message = <GuidelineMessage />;
200
+ }
201
+
202
+ return (
203
+ <AIControl
204
+ disabled={ disabled || loading }
205
+ value={ value }
206
+ placeholder={ placeholder }
207
+ isTransparent={ isTransparent }
208
+ state={ state }
209
+ onChange={ changeHandler }
210
+ actions={ actions }
211
+ message={ message }
212
+ promptUserInputRef={ promptUserInputRef }
213
+ />
214
+ );
215
+ }
216
+
217
+ export default forwardRef( ExtensionAIControl );