@automattic/jetpack-ai-client 0.12.0 → 0.12.2

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,14 @@ 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.12.2] - 2024-04-22
9
+ ### Added
10
+ - AI Client: Add Markdown and HTML conversions. [#36906]
11
+
12
+ ## [0.12.1] - 2024-04-15
13
+ ### Added
14
+ - AI Client: Add callbacks, initial requesting state and change error handling. [#36869]
15
+
8
16
  ## [0.12.0] - 2024-04-08
9
17
  ### Added
10
18
  - Add error rejection in image generation. [#36709]
@@ -282,6 +290,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
282
290
  - Updated package dependencies. [#31659]
283
291
  - Updated package dependencies. [#31785]
284
292
 
293
+ [0.12.2]: https://github.com/Automattic/jetpack-ai-client/compare/v0.12.1...v0.12.2
294
+ [0.12.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.12.0...v0.12.1
285
295
  [0.12.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.11.0...v0.12.0
286
296
  [0.11.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.10.1...v0.11.0
287
297
  [0.10.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.10.0...v0.10.1
@@ -17,9 +17,12 @@ type useAiSuggestionsOptions = {
17
17
  * AskQuestion options.
18
18
  */
19
19
  askQuestionOptions?: AskQuestionOptionsArgProps;
20
+ initialRequestingState?: RequestingStateProp;
20
21
  onSuggestion?: (suggestion: string) => void;
21
22
  onDone?: (content: string) => void;
23
+ onStop?: () => void;
22
24
  onError?: (error: RequestingErrorProps) => void;
25
+ onAllErrors?: (error: RequestingErrorProps) => void;
23
26
  };
24
27
  type useAiSuggestionsProps = {
25
28
  suggestion: string;
@@ -29,6 +32,7 @@ type useAiSuggestionsProps = {
29
32
  request: (prompt: PromptProp, options?: AskQuestionOptionsArgProps) => Promise<void>;
30
33
  reset: () => void;
31
34
  stopSuggestion: () => void;
35
+ handleErrorQuotaExceededError: () => void;
32
36
  };
33
37
  /**
34
38
  * Get the error data for a given error code.
@@ -44,5 +48,5 @@ export declare function getErrorData(errorCode: SuggestionErrorCode): Requesting
44
48
  * @param {useAiSuggestionsOptions} options - The options for the hook.
45
49
  * @returns {useAiSuggestionsProps} The props for the hook.
46
50
  */
47
- export default function useAiSuggestions({ prompt, autoRequest, askQuestionOptions, onSuggestion, onDone, onError, }?: useAiSuggestionsOptions): useAiSuggestionsProps;
51
+ export default function useAiSuggestions({ prompt, autoRequest, askQuestionOptions, initialRequestingState, onSuggestion, onDone, onStop, onError, onAllErrors, }?: useAiSuggestionsOptions): useAiSuggestionsProps;
48
52
  export {};
@@ -3,13 +3,11 @@
3
3
  */
4
4
  import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
5
5
  import { __ } from '@wordpress/i18n';
6
- import debugFactory from 'debug';
7
6
  /**
8
7
  * Internal dependencies
9
8
  */
10
9
  import askQuestion from '../../ask-question/index.js';
11
- import { ERROR_MODERATION, ERROR_NETWORK, ERROR_QUOTA_EXCEEDED, ERROR_SERVICE_UNAVAILABLE, ERROR_UNCLEAR_PROMPT, } from '../../types.js';
12
- const debug = debugFactory('jetpack-ai-client:use-suggestion');
10
+ import { ERROR_CONTEXT_TOO_LARGE, ERROR_MODERATION, ERROR_NETWORK, ERROR_QUOTA_EXCEEDED, ERROR_SERVICE_UNAVAILABLE, ERROR_UNCLEAR_PROMPT, ERROR_RESPONSE, } from '../../types.js';
13
11
  /**
14
12
  * Get the error data for a given error code.
15
13
  *
@@ -42,6 +40,12 @@ export function getErrorData(errorCode) {
42
40
  message: __('This request has been flagged by our moderation system. Please try to rephrase it and try again.', 'jetpack-ai-client'),
43
41
  severity: 'info',
44
42
  };
43
+ case ERROR_CONTEXT_TOO_LARGE:
44
+ return {
45
+ code: ERROR_CONTEXT_TOO_LARGE,
46
+ message: __('The content is too large to be processed all at once. Please try to shorten it or divide it into smaller parts.', 'jetpack-ai-client'),
47
+ severity: 'info',
48
+ };
45
49
  case ERROR_NETWORK:
46
50
  default:
47
51
  return {
@@ -58,8 +62,8 @@ export function getErrorData(errorCode) {
58
62
  * @param {useAiSuggestionsOptions} options - The options for the hook.
59
63
  * @returns {useAiSuggestionsProps} The props for the hook.
60
64
  */
61
- export default function useAiSuggestions({ prompt, autoRequest = false, askQuestionOptions = {}, onSuggestion, onDone, onError, } = {}) {
62
- const [requestingState, setRequestingState] = useState('init');
65
+ export default function useAiSuggestions({ prompt, autoRequest = false, askQuestionOptions = {}, initialRequestingState = 'init', onSuggestion, onDone, onStop, onError, onAllErrors, } = {}) {
66
+ const [requestingState, setRequestingState] = useState(initialRequestingState);
63
67
  const [suggestion, setSuggestion] = useState('');
64
68
  const [error, setError] = useState();
65
69
  // Store the event source in a ref, so we can handle it if needed.
@@ -81,9 +85,13 @@ export default function useAiSuggestions({ prompt, autoRequest = false, askQuest
81
85
  * @returns {void}
82
86
  */
83
87
  const handleDone = useCallback((event) => {
88
+ closeEventSource();
84
89
  onDone?.(event?.detail);
85
90
  setRequestingState('done');
86
91
  }, [onDone]);
92
+ const handleAnyError = useCallback((event) => {
93
+ onAllErrors?.(event?.detail);
94
+ }, [onAllErrors]);
87
95
  const handleError = useCallback((errorCode) => {
88
96
  eventSourceRef?.current?.close();
89
97
  setRequestingState('error');
@@ -103,35 +111,26 @@ export default function useAiSuggestions({ prompt, autoRequest = false, askQuest
103
111
  * @returns {Promise<void>} The promise.
104
112
  */
105
113
  const request = useCallback(async (promptArg, options = { ...askQuestionOptions }) => {
106
- if (Array.isArray(promptArg) && promptArg?.length) {
107
- promptArg.forEach(({ role, content: promptContent }, i) => debug('(%s/%s) %o\n%s', i + 1, promptArg.length, `[${role}]`, promptContent));
108
- }
109
- else {
110
- debug('%o', promptArg);
111
- }
114
+ // Clear any error.
115
+ setError(undefined);
112
116
  // Set the request status.
113
117
  setRequestingState('requesting');
114
- try {
115
- eventSourceRef.current = await askQuestion(promptArg, options);
116
- if (!eventSourceRef?.current) {
117
- return;
118
- }
119
- // Alias
120
- const eventSource = eventSourceRef.current;
121
- // Set the request status.
122
- setRequestingState('suggesting');
123
- eventSource.addEventListener('suggestion', handleSuggestion);
124
- eventSource.addEventListener(ERROR_QUOTA_EXCEEDED, handleErrorQuotaExceededError);
125
- eventSource.addEventListener(ERROR_UNCLEAR_PROMPT, handleUnclearPromptError);
126
- eventSource.addEventListener(ERROR_SERVICE_UNAVAILABLE, handleServiceUnavailableError);
127
- eventSource.addEventListener(ERROR_MODERATION, handleModerationError);
128
- eventSource.addEventListener(ERROR_NETWORK, handleNetworkError);
129
- eventSource.addEventListener('done', handleDone);
130
- }
131
- catch (e) {
132
- // eslint-disable-next-line no-console
133
- console.error(e);
118
+ eventSourceRef.current = await askQuestion(promptArg, options);
119
+ if (!eventSourceRef?.current) {
120
+ return;
134
121
  }
122
+ // Alias
123
+ const eventSource = eventSourceRef.current;
124
+ // Set the request status.
125
+ setRequestingState('suggesting');
126
+ eventSource.addEventListener('suggestion', handleSuggestion);
127
+ eventSource.addEventListener(ERROR_QUOTA_EXCEEDED, handleErrorQuotaExceededError);
128
+ eventSource.addEventListener(ERROR_UNCLEAR_PROMPT, handleUnclearPromptError);
129
+ eventSource.addEventListener(ERROR_SERVICE_UNAVAILABLE, handleServiceUnavailableError);
130
+ eventSource.addEventListener(ERROR_MODERATION, handleModerationError);
131
+ eventSource.addEventListener(ERROR_NETWORK, handleNetworkError);
132
+ eventSource.addEventListener(ERROR_RESPONSE, handleAnyError);
133
+ eventSource.addEventListener('done', handleDone);
135
134
  }, [
136
135
  handleDone,
137
136
  handleErrorQuotaExceededError,
@@ -152,11 +151,11 @@ export default function useAiSuggestions({ prompt, autoRequest = false, askQuest
152
151
  setError(undefined);
153
152
  }, []);
154
153
  /**
155
- * Stop suggestion handler.
154
+ * Close the event source connection.
156
155
  *
157
156
  * @returns {void}
158
157
  */
159
- const stopSuggestion = useCallback(() => {
158
+ const closeEventSource = useCallback(() => {
160
159
  if (!eventSourceRef?.current) {
161
160
  return;
162
161
  }
@@ -172,8 +171,6 @@ export default function useAiSuggestions({ prompt, autoRequest = false, askQuest
172
171
  eventSource.removeEventListener(ERROR_MODERATION, handleModerationError);
173
172
  eventSource.removeEventListener(ERROR_NETWORK, handleNetworkError);
174
173
  eventSource.removeEventListener('done', handleDone);
175
- // Set requesting state to done since the suggestion stopped.
176
- setRequestingState('done');
177
174
  }, [
178
175
  eventSourceRef,
179
176
  handleSuggestion,
@@ -184,6 +181,16 @@ export default function useAiSuggestions({ prompt, autoRequest = false, askQuest
184
181
  handleNetworkError,
185
182
  handleDone,
186
183
  ]);
184
+ /**
185
+ * Stop suggestion handler.
186
+ *
187
+ * @returns {void}
188
+ */
189
+ const stopSuggestion = useCallback(() => {
190
+ closeEventSource();
191
+ onStop?.();
192
+ setRequestingState('done');
193
+ }, [onStop]);
187
194
  // Request suggestions automatically when ready.
188
195
  useEffect(() => {
189
196
  // Check if there is a prompt to request.
@@ -208,6 +215,8 @@ export default function useAiSuggestions({ prompt, autoRequest = false, askQuest
208
215
  request,
209
216
  stopSuggestion,
210
217
  reset,
218
+ // Error handlers
219
+ handleErrorQuotaExceededError,
211
220
  // SuggestionsEventSource
212
221
  eventSource: eventSourceRef.current,
213
222
  };
@@ -39,7 +39,6 @@ export default function useTranscriptionPostProcessing({ feature, onReady, onErr
39
39
  onError?.(errorData.message);
40
40
  }, [setPostProcessingError, onError]);
41
41
  const { request, stopSuggestion } = useAiSuggestions({
42
- autoRequest: false,
43
42
  onSuggestion: handleOnSuggestion,
44
43
  onDone: handleOnDone,
45
44
  onError: handleOnError,
package/build/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { default as requestJwt } from './jwt/index.js';
2
2
  export { default as SuggestionsEventSource } from './suggestions-event-source/index.js';
3
3
  export { default as askQuestion } from './ask-question/index.js';
4
4
  export { default as transcribeAudio } from './audio-transcription/index.js';
5
- export { default as useAiSuggestions } from './hooks/use-ai-suggestions/index.js';
5
+ export { default as useAiSuggestions, getErrorData } from './hooks/use-ai-suggestions/index.js';
6
6
  export { default as useMediaRecording } from './hooks/use-media-recording/index.js';
7
7
  export { default as useAudioTranscription } from './hooks/use-audio-transcription/index.js';
8
8
  export { default as useTranscriptionPostProcessing } from './hooks/use-transcription-post-processing/index.js';
@@ -12,3 +12,4 @@ export * from './icons/index.js';
12
12
  export * from './components/index.js';
13
13
  export * from './data-flow/index.js';
14
14
  export * from './types.js';
15
+ export * from './libs/index.js';
package/build/index.js CHANGED
@@ -8,7 +8,7 @@ export { default as transcribeAudio } from './audio-transcription/index.js';
8
8
  /*
9
9
  * Hooks
10
10
  */
11
- export { default as useAiSuggestions } from './hooks/use-ai-suggestions/index.js';
11
+ export { default as useAiSuggestions, getErrorData } from './hooks/use-ai-suggestions/index.js';
12
12
  export { default as useMediaRecording } from './hooks/use-media-recording/index.js';
13
13
  export { default as useAudioTranscription } from './hooks/use-audio-transcription/index.js';
14
14
  export { default as useTranscriptionPostProcessing } from './hooks/use-transcription-post-processing/index.js';
@@ -30,3 +30,7 @@ export * from './data-flow/index.js';
30
30
  * Types
31
31
  */
32
32
  export * from './types.js';
33
+ /*
34
+ * Libs
35
+ */
36
+ export * from './libs/index.js';
@@ -0,0 +1 @@
1
+ export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML, } from './markdown/index.js';
@@ -0,0 +1 @@
1
+ export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML, } from './markdown/index.js';
@@ -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
+ }
@@ -49,7 +49,6 @@ export default class SuggestionsEventSource extends EventTarget {
49
49
  checkForUnclearPrompt(): void;
50
50
  close(): void;
51
51
  processEvent(e: EventSourceMessage): void;
52
- processConnectionError(response: any): void;
53
52
  processErrorEvent(e: any): void;
54
53
  }
55
54
  export {};
@@ -128,7 +128,9 @@ export default class SuggestionsEventSource extends EventTarget {
128
128
  if (response.status >= 400 &&
129
129
  response.status <= 500 &&
130
130
  ![413, 422, 429].includes(response.status)) {
131
- this.processConnectionError(response);
131
+ debug('Connection error: %o', response);
132
+ errorCode = ERROR_NETWORK;
133
+ this.dispatchEvent(new CustomEvent(ERROR_NETWORK, { detail: response }));
132
134
  }
133
135
  /*
134
136
  * error code 503
@@ -264,13 +266,6 @@ export default class SuggestionsEventSource extends EventTarget {
264
266
  this.dispatchEvent(new CustomEvent('functionCallChunk', { detail: this.fullFunctionCall }));
265
267
  }
266
268
  }
267
- processConnectionError(response) {
268
- debug('Connection error: %o', response);
269
- this.dispatchEvent(new CustomEvent(ERROR_NETWORK, { detail: response }));
270
- this.dispatchEvent(new CustomEvent(ERROR_RESPONSE, {
271
- detail: getErrorData(ERROR_NETWORK),
272
- }));
273
- }
274
269
  processErrorEvent(e) {
275
270
  debug('onerror: %o', e);
276
271
  // Dispatch a generic network error event
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.0",
4
+ "version": "0.12.2",
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": {
@@ -26,6 +26,8 @@
26
26
  "@storybook/addon-actions": "8.0.6",
27
27
  "@storybook/blocks": "8.0.6",
28
28
  "@storybook/react": "8.0.6",
29
+ "@types/markdown-it": "14.0.0",
30
+ "@types/turndown": "5.0.4",
29
31
  "jest": "^29.6.2",
30
32
  "jest-environment-jsdom": "29.7.0",
31
33
  "typescript": "5.0.4"
@@ -39,9 +41,9 @@
39
41
  "main": "./build/index.js",
40
42
  "types": "./build/index.d.ts",
41
43
  "dependencies": {
42
- "@automattic/jetpack-base-styles": "^0.6.21",
43
- "@automattic/jetpack-connection": "^0.33.7",
44
- "@automattic/jetpack-shared-extension-utils": "^0.14.9",
44
+ "@automattic/jetpack-base-styles": "^0.6.22",
45
+ "@automattic/jetpack-connection": "^0.33.8",
46
+ "@automattic/jetpack-shared-extension-utils": "^0.14.10",
45
47
  "@microsoft/fetch-event-source": "2.0.1",
46
48
  "@types/react": "18.2.74",
47
49
  "@wordpress/api-fetch": "6.52.0",
@@ -54,7 +56,9 @@
54
56
  "@wordpress/icons": "9.46.0",
55
57
  "classnames": "2.3.2",
56
58
  "debug": "4.3.4",
59
+ "markdown-it": "14.0.0",
57
60
  "react": "18.2.0",
58
- "react-dom": "18.2.0"
61
+ "react-dom": "18.2.0",
62
+ "turndown": "7.1.2"
59
63
  }
60
64
  }
@@ -3,17 +3,18 @@
3
3
  */
4
4
  import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
5
5
  import { __ } from '@wordpress/i18n';
6
- import debugFactory from 'debug';
7
6
  /**
8
7
  * Internal dependencies
9
8
  */
10
9
  import askQuestion from '../../ask-question/index.js';
11
10
  import {
11
+ ERROR_CONTEXT_TOO_LARGE,
12
12
  ERROR_MODERATION,
13
13
  ERROR_NETWORK,
14
14
  ERROR_QUOTA_EXCEEDED,
15
15
  ERROR_SERVICE_UNAVAILABLE,
16
16
  ERROR_UNCLEAR_PROMPT,
17
+ ERROR_RESPONSE,
17
18
  } from '../../types.js';
18
19
  /**
19
20
  * Types & constants
@@ -56,6 +57,11 @@ type useAiSuggestionsOptions = {
56
57
  */
57
58
  askQuestionOptions?: AskQuestionOptionsArgProps;
58
59
 
60
+ /*
61
+ * Initial requesting state.
62
+ */
63
+ initialRequestingState?: RequestingStateProp;
64
+
59
65
  /*
60
66
  * onSuggestion callback.
61
67
  */
@@ -66,10 +72,20 @@ type useAiSuggestionsOptions = {
66
72
  */
67
73
  onDone?: ( content: string ) => void;
68
74
 
75
+ /*
76
+ * onStop callback.
77
+ */
78
+ onStop?: () => void;
79
+
69
80
  /*
70
81
  * onError callback.
71
82
  */
72
83
  onError?: ( error: RequestingErrorProps ) => void;
84
+
85
+ /*
86
+ * Error callback common for all errors.
87
+ */
88
+ onAllErrors?: ( error: RequestingErrorProps ) => void;
73
89
  };
74
90
 
75
91
  type useAiSuggestionsProps = {
@@ -107,9 +123,12 @@ type useAiSuggestionsProps = {
107
123
  * The handler to stop a suggestion.
108
124
  */
109
125
  stopSuggestion: () => void;
110
- };
111
126
 
112
- const debug = debugFactory( 'jetpack-ai-client:use-suggestion' );
127
+ /*
128
+ * The handler to handle the quota exceeded error.
129
+ */
130
+ handleErrorQuotaExceededError: () => void;
131
+ };
113
132
 
114
133
  /**
115
134
  * Get the error data for a given error code.
@@ -149,6 +168,15 @@ export function getErrorData( errorCode: SuggestionErrorCode ): RequestingErrorP
149
168
  ),
150
169
  severity: 'info',
151
170
  };
171
+ case ERROR_CONTEXT_TOO_LARGE:
172
+ return {
173
+ code: ERROR_CONTEXT_TOO_LARGE,
174
+ message: __(
175
+ 'The content is too large to be processed all at once. Please try to shorten it or divide it into smaller parts.',
176
+ 'jetpack-ai-client'
177
+ ),
178
+ severity: 'info',
179
+ };
152
180
  case ERROR_NETWORK:
153
181
  default:
154
182
  return {
@@ -173,11 +201,15 @@ export default function useAiSuggestions( {
173
201
  prompt,
174
202
  autoRequest = false,
175
203
  askQuestionOptions = {},
204
+ initialRequestingState = 'init',
176
205
  onSuggestion,
177
206
  onDone,
207
+ onStop,
178
208
  onError,
209
+ onAllErrors,
179
210
  }: useAiSuggestionsOptions = {} ): useAiSuggestionsProps {
180
- const [ requestingState, setRequestingState ] = useState< RequestingStateProp >( 'init' );
211
+ const [ requestingState, setRequestingState ] =
212
+ useState< RequestingStateProp >( initialRequestingState );
181
213
  const [ suggestion, setSuggestion ] = useState< string >( '' );
182
214
  const [ error, setError ] = useState< RequestingErrorProps >();
183
215
 
@@ -206,12 +238,20 @@ export default function useAiSuggestions( {
206
238
  */
207
239
  const handleDone = useCallback(
208
240
  ( event: CustomEvent ) => {
241
+ closeEventSource();
209
242
  onDone?.( event?.detail );
210
243
  setRequestingState( 'done' );
211
244
  },
212
245
  [ onDone ]
213
246
  );
214
247
 
248
+ const handleAnyError = useCallback(
249
+ ( event: CustomEvent ) => {
250
+ onAllErrors?.( event?.detail );
251
+ },
252
+ [ onAllErrors ]
253
+ );
254
+
215
255
  const handleError = useCallback(
216
256
  ( errorCode: SuggestionErrorCode ) => {
217
257
  eventSourceRef?.current?.close();
@@ -250,43 +290,34 @@ export default function useAiSuggestions( {
250
290
  promptArg: PromptProp,
251
291
  options: AskQuestionOptionsArgProps = { ...askQuestionOptions }
252
292
  ) => {
253
- if ( Array.isArray( promptArg ) && promptArg?.length ) {
254
- promptArg.forEach( ( { role, content: promptContent }, i ) =>
255
- debug( '(%s/%s) %o\n%s', i + 1, promptArg.length, `[${ role }]`, promptContent )
256
- );
257
- } else {
258
- debug( '%o', promptArg );
259
- }
293
+ // Clear any error.
294
+ setError( undefined );
260
295
 
261
296
  // Set the request status.
262
297
  setRequestingState( 'requesting' );
263
298
 
264
- try {
265
- eventSourceRef.current = await askQuestion( promptArg, options );
299
+ eventSourceRef.current = await askQuestion( promptArg, options );
266
300
 
267
- if ( ! eventSourceRef?.current ) {
268
- return;
269
- }
301
+ if ( ! eventSourceRef?.current ) {
302
+ return;
303
+ }
270
304
 
271
- // Alias
272
- const eventSource = eventSourceRef.current;
305
+ // Alias
306
+ const eventSource = eventSourceRef.current;
273
307
 
274
- // Set the request status.
275
- setRequestingState( 'suggesting' );
308
+ // Set the request status.
309
+ setRequestingState( 'suggesting' );
276
310
 
277
- eventSource.addEventListener( 'suggestion', handleSuggestion );
311
+ eventSource.addEventListener( 'suggestion', handleSuggestion );
278
312
 
279
- eventSource.addEventListener( ERROR_QUOTA_EXCEEDED, handleErrorQuotaExceededError );
280
- eventSource.addEventListener( ERROR_UNCLEAR_PROMPT, handleUnclearPromptError );
281
- eventSource.addEventListener( ERROR_SERVICE_UNAVAILABLE, handleServiceUnavailableError );
282
- eventSource.addEventListener( ERROR_MODERATION, handleModerationError );
283
- eventSource.addEventListener( ERROR_NETWORK, handleNetworkError );
313
+ eventSource.addEventListener( ERROR_QUOTA_EXCEEDED, handleErrorQuotaExceededError );
314
+ eventSource.addEventListener( ERROR_UNCLEAR_PROMPT, handleUnclearPromptError );
315
+ eventSource.addEventListener( ERROR_SERVICE_UNAVAILABLE, handleServiceUnavailableError );
316
+ eventSource.addEventListener( ERROR_MODERATION, handleModerationError );
317
+ eventSource.addEventListener( ERROR_NETWORK, handleNetworkError );
318
+ eventSource.addEventListener( ERROR_RESPONSE, handleAnyError );
284
319
 
285
- eventSource.addEventListener( 'done', handleDone );
286
- } catch ( e ) {
287
- // eslint-disable-next-line no-console
288
- console.error( e );
289
- }
320
+ eventSource.addEventListener( 'done', handleDone );
290
321
  },
291
322
  [
292
323
  handleDone,
@@ -311,11 +342,11 @@ export default function useAiSuggestions( {
311
342
  }, [] );
312
343
 
313
344
  /**
314
- * Stop suggestion handler.
345
+ * Close the event source connection.
315
346
  *
316
347
  * @returns {void}
317
348
  */
318
- const stopSuggestion = useCallback( () => {
349
+ const closeEventSource = useCallback( () => {
319
350
  if ( ! eventSourceRef?.current ) {
320
351
  return;
321
352
  }
@@ -336,9 +367,6 @@ export default function useAiSuggestions( {
336
367
  eventSource.removeEventListener( ERROR_NETWORK, handleNetworkError );
337
368
 
338
369
  eventSource.removeEventListener( 'done', handleDone );
339
-
340
- // Set requesting state to done since the suggestion stopped.
341
- setRequestingState( 'done' );
342
370
  }, [
343
371
  eventSourceRef,
344
372
  handleSuggestion,
@@ -350,6 +378,17 @@ export default function useAiSuggestions( {
350
378
  handleDone,
351
379
  ] );
352
380
 
381
+ /**
382
+ * Stop suggestion handler.
383
+ *
384
+ * @returns {void}
385
+ */
386
+ const stopSuggestion = useCallback( () => {
387
+ closeEventSource();
388
+ onStop?.();
389
+ setRequestingState( 'done' );
390
+ }, [ onStop ] );
391
+
353
392
  // Request suggestions automatically when ready.
354
393
  useEffect( () => {
355
394
  // Check if there is a prompt to request.
@@ -379,6 +418,9 @@ export default function useAiSuggestions( {
379
418
  stopSuggestion,
380
419
  reset,
381
420
 
421
+ // Error handlers
422
+ handleErrorQuotaExceededError,
423
+
382
424
  // SuggestionsEventSource
383
425
  eventSource: eventSourceRef.current,
384
426
  };
@@ -83,7 +83,6 @@ export default function useTranscriptionPostProcessing( {
83
83
  );
84
84
 
85
85
  const { request, stopSuggestion } = useAiSuggestions( {
86
- autoRequest: false,
87
86
  onSuggestion: handleOnSuggestion,
88
87
  onDone: handleOnDone,
89
88
  onError: handleOnError,
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@ export { default as transcribeAudio } from './audio-transcription/index.js';
9
9
  /*
10
10
  * Hooks
11
11
  */
12
- export { default as useAiSuggestions } from './hooks/use-ai-suggestions/index.js';
12
+ export { default as useAiSuggestions, getErrorData } from './hooks/use-ai-suggestions/index.js';
13
13
  export { default as useMediaRecording } from './hooks/use-media-recording/index.js';
14
14
  export { default as useAudioTranscription } from './hooks/use-audio-transcription/index.js';
15
15
  export { default as useTranscriptionPostProcessing } from './hooks/use-transcription-post-processing/index.js';
@@ -35,3 +35,8 @@ export * from './data-flow/index.js';
35
35
  * Types
36
36
  */
37
37
  export * from './types.js';
38
+
39
+ /*
40
+ * Libs
41
+ */
42
+ export * from './libs/index.js';
@@ -0,0 +1,6 @@
1
+ export {
2
+ MarkdownToHTML,
3
+ HTMLToMarkdown,
4
+ renderHTMLFromMarkdown,
5
+ renderMarkdownFromHTML,
6
+ } from './markdown/index.js';
@@ -0,0 +1,74 @@
1
+ # Markdown converters
2
+
3
+ Typescript functions and classes to convert Markdown to and from HTML.
4
+
5
+ ## HTML to Markdown
6
+
7
+ The HTML to Markdown conversion uses the [Turndown](https://github.com/mixmark-io/turndown) library and supports Turndown's options and rules.
8
+
9
+ Example:
10
+ ```typescript
11
+ /**
12
+ * External dependencies
13
+ */
14
+ import { renderMarkdownFromHTML } from '@automattic/jetpack-ai-client';
15
+
16
+ const htmlContent = '<strong>Hello world</strong>';
17
+ const markdownContent = renderMarkdownFromHTML( { content: htmlContent } );
18
+ // **Hello world**
19
+ ```
20
+
21
+ To use custom options and rules:
22
+ ```typescript
23
+ /**
24
+ * External dependencies
25
+ */
26
+ import { HTMLToMarkdown } from '@automattic/jetpack-ai-client';
27
+
28
+ const htmlContent = '<strong>Hello world</strong>';
29
+ const options = { headingStyle: 'setext' };
30
+ const rules = {
31
+ customStrong: {
32
+ filter: [ 'strong' ],
33
+ replacement: function( content: string ) {
34
+ return '***' + content + '***';
35
+ }
36
+ }
37
+ };
38
+ const renderer = new HTMLToMarkdown( options, rules );
39
+ const markdownContent = renderer.render( { content: htmlContent } );
40
+ // ***Hello world***
41
+ ```
42
+
43
+ ## Markdown to HTML
44
+
45
+ The Markdown to HTML conversion uses the [markdown-it](https://github.com/markdown-it/markdown-it) library and supports markdown-it's options. It also adds access to common fixes.
46
+
47
+ Example:
48
+ ```typescript
49
+ /**
50
+ * External dependencies
51
+ */
52
+ import { renderHTMLFromMarkdown } from '@automattic/jetpack-ai-client';
53
+
54
+ const markdownContent = '**Hello world**';
55
+ const htmlContent = renderHTMLFromMarkdown( { content: markdownContent, rules: 'all' } ); // 'all' is a default value
56
+ // <p><strong>Hello world</strong></p>\n
57
+ ```
58
+
59
+ To use custom options and fixes:
60
+ ```typescript
61
+ /**
62
+ * External dependencies
63
+ */
64
+ import { MarkdownToHTML } from '@automattic/jetpack-ai-client';
65
+
66
+ const markdownContent = '**Hello world**';
67
+ const options = { breaks: 'false' };
68
+ const rules = [ 'list' ];
69
+ const renderer = new MarkdownToHTML( options );
70
+ const htmlContent = renderer.render( { content: markdownContent, rules } );
71
+ // <p><strong>Hello world</strong></p>\n
72
+ ```
73
+
74
+ Currently `rules` only supports `'all'` and `['list']`. Further specific fixes can be added when necessary.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import TurndownService from 'turndown';
5
+ /**
6
+ * Types
7
+ */
8
+ import type { Options, Rule } from 'turndown';
9
+
10
+ const defaultTurndownOptions: Options = { emDelimiter: '_', headingStyle: 'atx' };
11
+ const defaultTurndownRules: { [ key: string ]: Rule } = {
12
+ strikethrough: {
13
+ filter: [ 'del', 's' ],
14
+ replacement: function ( content: string ) {
15
+ return '~~' + content + '~~';
16
+ },
17
+ },
18
+ };
19
+
20
+ export default class HTMLToMarkdown {
21
+ turndownService: TurndownService;
22
+
23
+ constructor(
24
+ options: Options = defaultTurndownOptions,
25
+ rules: { [ key: string ]: Rule } = defaultTurndownRules
26
+ ) {
27
+ this.turndownService = new TurndownService( options );
28
+ for ( const rule in rules ) {
29
+ this.turndownService.addRule( rule, rules[ rule ] );
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Renders HTML from Markdown content with specified processing rules.
35
+ * @param {object} options - The options to use when rendering the Markdown content
36
+ * @param {string} options.content - The HTML content to render
37
+ * @returns {string} The rendered Markdown content
38
+ */
39
+ render( { content }: { content: string } ): string {
40
+ return this.turndownService.turndown( content );
41
+ }
42
+ }
@@ -0,0 +1,28 @@
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
+
11
+ const defaultMarkdownConverter = new MarkdownToHTML();
12
+ const defaultHTMLConverter = new HTMLToMarkdown();
13
+
14
+ const renderHTMLFromMarkdown = ( {
15
+ content,
16
+ rules = 'all',
17
+ }: {
18
+ content: string;
19
+ rules?: Array< HTMLFix > | 'all';
20
+ } ) => {
21
+ return defaultMarkdownConverter.render( { content, rules } );
22
+ };
23
+
24
+ const renderMarkdownFromHTML = ( { content }: { content: string } ) => {
25
+ return defaultHTMLConverter.render( { content } );
26
+ };
27
+
28
+ export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import MarkdownIt from 'markdown-it';
5
+ /**
6
+ * Types
7
+ */
8
+ import type { Options } from 'markdown-it';
9
+
10
+ export type Fix = 'list';
11
+ type Fixes = {
12
+ [ key in Fix ]: ( content: string ) => string;
13
+ };
14
+
15
+ const fixes: Fixes = {
16
+ list: ( content: string ) => {
17
+ // Fix list indentation
18
+ return content.replace( /<li>\s+<p>/g, '<li>' ).replace( /<\/p>\s+<\/li>/g, '</li>' );
19
+ },
20
+ };
21
+
22
+ const defaultMarkdownItOptions: Options = {
23
+ breaks: true,
24
+ };
25
+
26
+ export default class MarkdownToHTML {
27
+ markdownConverter: MarkdownIt;
28
+
29
+ constructor( options: Options = defaultMarkdownItOptions ) {
30
+ this.markdownConverter = new MarkdownIt( options );
31
+ }
32
+
33
+ /**
34
+ * Renders HTML from Markdown content with specified processing rules.
35
+ * @param {object} options - The options to use when rendering the HTML content
36
+ * @param {string} options.content - The Markdown content to render
37
+ * @param {string} options.rules - The rules to apply to the rendered content
38
+ * @returns {string} The rendered HTML content
39
+ */
40
+ render( { content, rules = 'all' }: { content: string; rules: Array< Fix > | 'all' } ): string {
41
+ const rendered = this.markdownConverter.render( content );
42
+ const rulesToApply = rules === 'all' ? Object.keys( fixes ) : rules;
43
+
44
+ return rulesToApply.reduce( ( renderedContent, rule ) => {
45
+ return fixes[ rule ]( renderedContent );
46
+ }, rendered );
47
+ }
48
+ }
@@ -197,7 +197,9 @@ export default class SuggestionsEventSource extends EventTarget {
197
197
  response.status <= 500 &&
198
198
  ! [ 413, 422, 429 ].includes( response.status )
199
199
  ) {
200
- this.processConnectionError( response );
200
+ debug( 'Connection error: %o', response );
201
+ errorCode = ERROR_NETWORK;
202
+ this.dispatchEvent( new CustomEvent( ERROR_NETWORK, { detail: response } ) );
201
203
  }
202
204
 
203
205
  /*
@@ -358,16 +360,6 @@ export default class SuggestionsEventSource extends EventTarget {
358
360
  }
359
361
  }
360
362
 
361
- processConnectionError( response ) {
362
- debug( 'Connection error: %o', response );
363
- this.dispatchEvent( new CustomEvent( ERROR_NETWORK, { detail: response } ) );
364
- this.dispatchEvent(
365
- new CustomEvent( ERROR_RESPONSE, {
366
- detail: getErrorData( ERROR_NETWORK ),
367
- } )
368
- );
369
- }
370
-
371
363
  processErrorEvent( e ) {
372
364
  debug( 'onerror: %o', e );
373
365
 
package/src/types.ts CHANGED
@@ -93,6 +93,17 @@ export type { RecordingState } from './hooks/use-media-recording/index.js';
93
93
  */
94
94
  export type CancelablePromise< T = void > = Promise< T > & { canceled?: boolean };
95
95
 
96
+ export type Block = {
97
+ attributes?: {
98
+ [ key: string ]: unknown;
99
+ };
100
+ clientId?: string;
101
+ innerBlocks?: Block[];
102
+ isValid?: boolean;
103
+ name?: string;
104
+ originalContent?: string;
105
+ };
106
+
96
107
  /*
97
108
  * Transcription types
98
109
  */