@automattic/jetpack-ai-client 0.12.1 → 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.
Files changed (45) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/build/components/ai-control/ai-control.d.ts +28 -0
  3. package/build/components/ai-control/ai-control.js +22 -0
  4. package/build/components/ai-control/block-ai-control.d.ts +37 -0
  5. package/build/components/ai-control/block-ai-control.js +82 -0
  6. package/build/components/ai-control/extension-ai-control.d.ts +35 -0
  7. package/build/components/ai-control/extension-ai-control.js +86 -0
  8. package/build/components/ai-control/index.d.ts +3 -40
  9. package/build/components/ai-control/index.js +3 -86
  10. package/build/components/index.d.ts +2 -2
  11. package/build/components/index.js +2 -2
  12. package/build/components/{ai-control/message.d.ts → message/index.d.ts} +25 -8
  13. package/build/components/message/index.js +69 -0
  14. package/build/icons/error-exclamation.d.ts +2 -0
  15. package/build/icons/error-exclamation.js +7 -0
  16. package/build/index.d.ts +1 -0
  17. package/build/index.js +4 -0
  18. package/build/libs/index.d.ts +1 -0
  19. package/build/libs/index.js +1 -0
  20. package/build/libs/markdown/html-to-markdown.d.ts +23 -0
  21. package/build/libs/markdown/html-to-markdown.js +31 -0
  22. package/build/libs/markdown/index.d.ts +17 -0
  23. package/build/libs/markdown/index.js +14 -0
  24. package/build/libs/markdown/markdown-to-html.d.ts +24 -0
  25. package/build/libs/markdown/markdown-to-html.js +33 -0
  26. package/build/types.d.ts +10 -0
  27. package/package.json +8 -3
  28. package/src/components/ai-control/ai-control.tsx +79 -0
  29. package/src/components/ai-control/block-ai-control.tsx +278 -0
  30. package/src/components/ai-control/extension-ai-control.tsx +217 -0
  31. package/src/components/ai-control/index.tsx +3 -281
  32. package/src/components/ai-control/style.scss +4 -42
  33. package/src/components/index.ts +3 -2
  34. package/src/components/message/index.tsx +157 -0
  35. package/src/components/message/style.scss +83 -0
  36. package/src/icons/error-exclamation.tsx +18 -0
  37. package/src/index.ts +5 -0
  38. package/src/libs/index.ts +6 -0
  39. package/src/libs/markdown/README.md +74 -0
  40. package/src/libs/markdown/html-to-markdown.ts +42 -0
  41. package/src/libs/markdown/index.ts +28 -0
  42. package/src/libs/markdown/markdown-to-html.ts +48 -0
  43. package/src/types.ts +11 -0
  44. package/build/components/ai-control/message.js +0 -57
  45. package/src/components/ai-control/message.tsx +0 -118
@@ -0,0 +1,23 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import TurndownService from 'turndown';
5
+ /**
6
+ * Types
7
+ */
8
+ import type { Options, Rule } from 'turndown';
9
+ export default class HTMLToMarkdown {
10
+ turndownService: TurndownService;
11
+ constructor(options?: Options, rules?: {
12
+ [key: string]: Rule;
13
+ });
14
+ /**
15
+ * Renders HTML from Markdown content with specified processing rules.
16
+ * @param {object} options - The options to use when rendering the Markdown content
17
+ * @param {string} options.content - The HTML content to render
18
+ * @returns {string} The rendered Markdown content
19
+ */
20
+ render({ content }: {
21
+ content: string;
22
+ }): string;
23
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import TurndownService from 'turndown';
5
+ const defaultTurndownOptions = { emDelimiter: '_', headingStyle: 'atx' };
6
+ const defaultTurndownRules = {
7
+ strikethrough: {
8
+ filter: ['del', 's'],
9
+ replacement: function (content) {
10
+ return '~~' + content + '~~';
11
+ },
12
+ },
13
+ };
14
+ export default class HTMLToMarkdown {
15
+ turndownService;
16
+ constructor(options = defaultTurndownOptions, rules = defaultTurndownRules) {
17
+ this.turndownService = new TurndownService(options);
18
+ for (const rule in rules) {
19
+ this.turndownService.addRule(rule, rules[rule]);
20
+ }
21
+ }
22
+ /**
23
+ * Renders HTML from Markdown content with specified processing rules.
24
+ * @param {object} options - The options to use when rendering the Markdown content
25
+ * @param {string} options.content - The HTML content to render
26
+ * @returns {string} The rendered Markdown content
27
+ */
28
+ render({ content }) {
29
+ return this.turndownService.turndown(content);
30
+ }
31
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import HTMLToMarkdown from './html-to-markdown.js';
5
+ import MarkdownToHTML from './markdown-to-html.js';
6
+ /**
7
+ * Types
8
+ */
9
+ import type { Fix as HTMLFix } from './markdown-to-html.js';
10
+ declare const renderHTMLFromMarkdown: ({ content, rules, }: {
11
+ content: string;
12
+ rules?: Array<HTMLFix> | 'all';
13
+ }) => string;
14
+ declare const renderMarkdownFromHTML: ({ content }: {
15
+ content: string;
16
+ }) => string;
17
+ export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import HTMLToMarkdown from './html-to-markdown.js';
5
+ import MarkdownToHTML from './markdown-to-html.js';
6
+ const defaultMarkdownConverter = new MarkdownToHTML();
7
+ const defaultHTMLConverter = new HTMLToMarkdown();
8
+ const renderHTMLFromMarkdown = ({ content, rules = 'all', }) => {
9
+ return defaultMarkdownConverter.render({ content, rules });
10
+ };
11
+ const renderMarkdownFromHTML = ({ content }) => {
12
+ return defaultHTMLConverter.render({ content });
13
+ };
14
+ export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import MarkdownIt from 'markdown-it';
5
+ /**
6
+ * Types
7
+ */
8
+ import type { Options } from 'markdown-it';
9
+ export type Fix = 'list';
10
+ export default class MarkdownToHTML {
11
+ markdownConverter: MarkdownIt;
12
+ constructor(options?: Options);
13
+ /**
14
+ * Renders HTML from Markdown content with specified processing rules.
15
+ * @param {object} options - The options to use when rendering the HTML content
16
+ * @param {string} options.content - The Markdown content to render
17
+ * @param {string} options.rules - The rules to apply to the rendered content
18
+ * @returns {string} The rendered HTML content
19
+ */
20
+ render({ content, rules }: {
21
+ content: string;
22
+ rules: Array<Fix> | 'all';
23
+ }): string;
24
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import MarkdownIt from 'markdown-it';
5
+ const fixes = {
6
+ list: (content) => {
7
+ // Fix list indentation
8
+ return content.replace(/<li>\s+<p>/g, '<li>').replace(/<\/p>\s+<\/li>/g, '</li>');
9
+ },
10
+ };
11
+ const defaultMarkdownItOptions = {
12
+ breaks: true,
13
+ };
14
+ export default class MarkdownToHTML {
15
+ markdownConverter;
16
+ constructor(options = defaultMarkdownItOptions) {
17
+ this.markdownConverter = new MarkdownIt(options);
18
+ }
19
+ /**
20
+ * Renders HTML from Markdown content with specified processing rules.
21
+ * @param {object} options - The options to use when rendering the HTML content
22
+ * @param {string} options.content - The Markdown content to render
23
+ * @param {string} options.rules - The rules to apply to the rendered content
24
+ * @returns {string} The rendered HTML content
25
+ */
26
+ render({ content, rules = 'all' }) {
27
+ const rendered = this.markdownConverter.render(content);
28
+ const rulesToApply = rules === 'all' ? Object.keys(fixes) : rules;
29
+ return rulesToApply.reduce((renderedContent, rule) => {
30
+ return fixes[rule](renderedContent);
31
+ }, rendered);
32
+ }
33
+ }
package/build/types.d.ts CHANGED
@@ -28,4 +28,14 @@ export type { RecordingState } from './hooks/use-media-recording/index.js';
28
28
  export type CancelablePromise<T = void> = Promise<T> & {
29
29
  canceled?: boolean;
30
30
  };
31
+ export type Block = {
32
+ attributes?: {
33
+ [key: string]: unknown;
34
+ };
35
+ clientId?: string;
36
+ innerBlocks?: Block[];
37
+ isValid?: boolean;
38
+ name?: string;
39
+ originalContent?: string;
40
+ };
31
41
  export type TranscriptionState = RecordingState | 'validating' | 'processing' | 'error';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@automattic/jetpack-ai-client",
4
- "version": "0.12.1",
4
+ "version": "0.12.3",
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": {
@@ -25,7 +25,10 @@
25
25
  "devDependencies": {
26
26
  "@storybook/addon-actions": "8.0.6",
27
27
  "@storybook/blocks": "8.0.6",
28
+ "@storybook/preview-api": "8.0.6",
28
29
  "@storybook/react": "8.0.6",
30
+ "@types/markdown-it": "14.0.0",
31
+ "@types/turndown": "5.0.4",
29
32
  "jest": "^29.6.2",
30
33
  "jest-environment-jsdom": "29.7.0",
31
34
  "typescript": "5.0.4"
@@ -39,7 +42,7 @@
39
42
  "main": "./build/index.js",
40
43
  "types": "./build/index.d.ts",
41
44
  "dependencies": {
42
- "@automattic/jetpack-base-styles": "^0.6.22",
45
+ "@automattic/jetpack-base-styles": "^0.6.23",
43
46
  "@automattic/jetpack-connection": "^0.33.8",
44
47
  "@automattic/jetpack-shared-extension-utils": "^0.14.10",
45
48
  "@microsoft/fetch-event-source": "2.0.1",
@@ -54,7 +57,9 @@
54
57
  "@wordpress/icons": "9.46.0",
55
58
  "classnames": "2.3.2",
56
59
  "debug": "4.3.4",
60
+ "markdown-it": "14.0.0",
57
61
  "react": "18.2.0",
58
- "react-dom": "18.2.0"
62
+ "react-dom": "18.2.0",
63
+ "turndown": "7.1.2"
59
64
  }
60
65
  }
@@ -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 );