@automattic/jetpack-ai-client 0.14.0 → 0.14.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 +12 -0
- package/build/components/ai-control/extension-ai-control.d.ts +2 -1
- package/build/components/ai-control/extension-ai-control.js +3 -3
- package/build/components/message/index.d.ts +4 -2
- package/build/components/message/index.js +4 -4
- package/build/hooks/use-image-generator/index.d.ts +10 -10
- package/build/hooks/use-image-generator/index.js +31 -67
- package/build/libs/markdown/html-to-markdown.d.ts +11 -3
- package/build/libs/markdown/html-to-markdown.js +19 -5
- package/build/libs/markdown/index.d.ts +3 -2
- package/build/libs/markdown/index.js +2 -2
- package/build/libs/markdown/markdown-to-html.d.ts +9 -7
- package/build/libs/markdown/markdown-to-html.js +48 -10
- package/package.json +2 -2
- package/src/components/ai-control/extension-ai-control.tsx +8 -1
- package/src/components/message/index.tsx +17 -3
- package/src/hooks/use-image-generator/index.ts +55 -90
- package/src/libs/markdown/README.md +4 -3
- package/src/libs/markdown/html-to-markdown.ts +40 -9
- package/src/libs/markdown/index.ts +5 -3
- package/src/libs/markdown/markdown-to-html.ts +68 -13
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ 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.14.2] - 2024-06-03
|
|
9
|
+
### Added
|
|
10
|
+
- AI Client: Add list-related fixes on MarkdownToHTML conversion. [#37564]
|
|
11
|
+
- Jetpack AI: Support upgrade links on the AI Control that will open on a new tab. [#37629]
|
|
12
|
+
|
|
13
|
+
## [0.14.1] - 2024-05-27
|
|
14
|
+
### Changed
|
|
15
|
+
- AI Client: Add paragraph tweaks to Markdown conversion libs. [#37461]
|
|
16
|
+
- AI Featured Image: add type info. [#37474]
|
|
17
|
+
|
|
8
18
|
## [0.14.0] - 2024-05-20
|
|
9
19
|
### Added
|
|
10
20
|
- AI Client: Expose HTML render rules type. [#37386]
|
|
@@ -323,6 +333,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
323
333
|
- Updated package dependencies. [#31659]
|
|
324
334
|
- Updated package dependencies. [#31785]
|
|
325
335
|
|
|
336
|
+
[0.14.2]: https://github.com/Automattic/jetpack-ai-client/compare/v0.14.1...v0.14.2
|
|
337
|
+
[0.14.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.14.0...v0.14.1
|
|
326
338
|
[0.14.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.13.1...v0.14.0
|
|
327
339
|
[0.13.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.13.0...v0.13.1
|
|
328
340
|
[0.13.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.12.4...v0.13.0
|
|
@@ -17,6 +17,7 @@ type ExtensionAIControlProps = {
|
|
|
17
17
|
error?: RequestingErrorProps;
|
|
18
18
|
requestsRemaining?: number;
|
|
19
19
|
showUpgradeMessage?: boolean;
|
|
20
|
+
upgradeUrl?: string;
|
|
20
21
|
wrapperRef?: React.MutableRefObject<HTMLDivElement | null>;
|
|
21
22
|
onChange?: (newValue: string) => void;
|
|
22
23
|
onSend?: (currentValue: string) => void;
|
|
@@ -33,6 +34,6 @@ type ExtensionAIControlProps = {
|
|
|
33
34
|
* @param {React.MutableRefObject} ref - Ref to the component
|
|
34
35
|
* @returns {ReactElement} Rendered component
|
|
35
36
|
*/
|
|
36
|
-
export declare function ExtensionAIControl({ className, disabled, value, placeholder, showButtonLabels, isTransparent, state, showGuideLine, error, requestsRemaining, showUpgradeMessage, wrapperRef, onChange, onSend, onStop, onClose, onUndo, onUpgrade, onTryAgain, }: ExtensionAIControlProps, ref: React.MutableRefObject<HTMLInputElement>): ReactElement;
|
|
37
|
+
export declare function ExtensionAIControl({ className, disabled, value, placeholder, showButtonLabels, isTransparent, state, showGuideLine, error, requestsRemaining, showUpgradeMessage, upgradeUrl, wrapperRef, onChange, onSend, onStop, onClose, onUndo, onUpgrade, onTryAgain, }: ExtensionAIControlProps, ref: React.MutableRefObject<HTMLInputElement>): ReactElement;
|
|
37
38
|
declare const _default: React.ForwardRefExoticComponent<ExtensionAIControlProps & React.RefAttributes<HTMLInputElement>>;
|
|
38
39
|
export default _default;
|
|
@@ -21,7 +21,7 @@ import './style.scss';
|
|
|
21
21
|
* @param {React.MutableRefObject} ref - Ref to the component
|
|
22
22
|
* @returns {ReactElement} Rendered component
|
|
23
23
|
*/
|
|
24
|
-
export function ExtensionAIControl({ className, disabled = false, value = '', placeholder = '', showButtonLabels = true, isTransparent = false, state = 'init', showGuideLine = false, error, requestsRemaining, showUpgradeMessage = false, wrapperRef, onChange, onSend, onStop, onClose, onUndo, onUpgrade, onTryAgain, }, ref) {
|
|
24
|
+
export function ExtensionAIControl({ className, disabled = false, value = '', placeholder = '', showButtonLabels = true, isTransparent = false, state = 'init', showGuideLine = false, error, requestsRemaining, showUpgradeMessage = false, upgradeUrl, wrapperRef, onChange, onSend, onStop, onClose, onUndo, onUpgrade, onTryAgain, }, ref) {
|
|
25
25
|
const loading = state === 'requesting' || state === 'suggesting';
|
|
26
26
|
const [editRequest, setEditRequest] = useState(false);
|
|
27
27
|
const [lastValue, setLastValue] = useState(value || null);
|
|
@@ -76,10 +76,10 @@ export function ExtensionAIControl({ className, disabled = false, value = '', pl
|
|
|
76
76
|
const actions = (_jsx(_Fragment, { children: loading ? (_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: stopHandler, variant: "secondary", label: __('Stop request', 'jetpack-ai-client'), children: showButtonLabels ? __('Stop', 'jetpack-ai-client') : _jsx(Icon, { icon: closeSmall }) })) : (_jsxs(_Fragment, { children: [value?.length > 0 && (_jsx("div", { className: "jetpack-components-ai-control__controls-prompt_button_wrapper", children: _jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", onClick: sendHandler, variant: "primary", disabled: !value?.length || disabled, label: __('Send request', 'jetpack-ai-client'), children: showButtonLabels ? (__('Generate', 'jetpack-ai-client')) : (_jsx(Icon, { icon: arrowUp })) }) })), value?.length <= 0 && state === 'done' && (_jsx("div", { className: "jetpack-components-ai-control__controls-prompt_button_wrapper", children: _jsxs(ButtonGroup, { children: [_jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", label: __('Undo', 'jetpack-ai-client'), onClick: undoHandler, tooltipPosition: "top", children: _jsx(Icon, { icon: undo }) }), _jsx(Button, { className: "jetpack-components-ai-control__controls-prompt_button", label: __('Close', 'jetpack-ai-client'), onClick: closeHandler, variant: "tertiary", children: __('Close', 'jetpack-ai-client') })] }) }))] })) }));
|
|
77
77
|
let message = null;
|
|
78
78
|
if (error?.message) {
|
|
79
|
-
message = (_jsx(ErrorMessage, { error: error.message, code: error.code, onTryAgainClick: tryAgainHandler, onUpgradeClick: upgradeHandler }));
|
|
79
|
+
message = (_jsx(ErrorMessage, { error: error.message, code: error.code, onTryAgainClick: tryAgainHandler, onUpgradeClick: upgradeHandler, upgradeUrl: upgradeUrl }));
|
|
80
80
|
}
|
|
81
81
|
else if (showUpgradeMessage) {
|
|
82
|
-
message = (_jsx(UpgradeMessage, { requestsRemaining: requestsRemaining, onUpgradeClick: upgradeHandler }));
|
|
82
|
+
message = (_jsx(UpgradeMessage, { requestsRemaining: requestsRemaining, onUpgradeClick: upgradeHandler, upgradeUrl: upgradeUrl }));
|
|
83
83
|
}
|
|
84
84
|
else if (showGuideLine) {
|
|
85
85
|
message = _jsx(GuidelineMessage, {});
|
|
@@ -25,12 +25,14 @@ export type UpgradeMessageProps = {
|
|
|
25
25
|
requestsRemaining: number;
|
|
26
26
|
severity?: MessageSeverityProp;
|
|
27
27
|
onUpgradeClick: OnUpgradeClick;
|
|
28
|
+
upgradeUrl?: string;
|
|
28
29
|
};
|
|
29
30
|
export type ErrorMessageProps = {
|
|
30
31
|
error?: string;
|
|
31
32
|
code?: SuggestionErrorCode;
|
|
32
33
|
onTryAgainClick: () => void;
|
|
33
34
|
onUpgradeClick: OnUpgradeClick;
|
|
35
|
+
upgradeUrl?: string;
|
|
34
36
|
};
|
|
35
37
|
/**
|
|
36
38
|
* React component to render a block message.
|
|
@@ -51,12 +53,12 @@ export declare function GuidelineMessage(): React.ReactElement;
|
|
|
51
53
|
* @param {number} requestsRemaining - Number of requests remaining.
|
|
52
54
|
* @returns {React.ReactElement } - Message component.
|
|
53
55
|
*/
|
|
54
|
-
export declare function UpgradeMessage({ requestsRemaining, severity, onUpgradeClick, }: UpgradeMessageProps): React.ReactElement;
|
|
56
|
+
export declare function UpgradeMessage({ requestsRemaining, severity, onUpgradeClick, upgradeUrl, }: UpgradeMessageProps): React.ReactElement;
|
|
55
57
|
/**
|
|
56
58
|
* React component to render an error message
|
|
57
59
|
*
|
|
58
60
|
* @param {number} requestsRemaining - Number of requests remaining.
|
|
59
61
|
* @returns {React.ReactElement } - Message component.
|
|
60
62
|
*/
|
|
61
|
-
export declare function ErrorMessage({ error, code, onTryAgainClick, onUpgradeClick, }: ErrorMessageProps): React.ReactElement;
|
|
63
|
+
export declare function ErrorMessage({ error, code, onTryAgainClick, onUpgradeClick, upgradeUrl, }: ErrorMessageProps): React.ReactElement;
|
|
62
64
|
export {};
|
|
@@ -51,14 +51,14 @@ export function GuidelineMessage() {
|
|
|
51
51
|
* @param {number} requestsRemaining - Number of requests remaining.
|
|
52
52
|
* @returns {React.ReactElement } - Message component.
|
|
53
53
|
*/
|
|
54
|
-
export function UpgradeMessage({ requestsRemaining, severity, onUpgradeClick, }) {
|
|
54
|
+
export function UpgradeMessage({ requestsRemaining, severity, onUpgradeClick, upgradeUrl, }) {
|
|
55
55
|
let messageSeverity = severity;
|
|
56
56
|
if (messageSeverity == null) {
|
|
57
57
|
messageSeverity = requestsRemaining > 0 ? MESSAGE_SEVERITY_INFO : MESSAGE_SEVERITY_WARNING;
|
|
58
58
|
}
|
|
59
59
|
return (_jsxs(Message, { severity: messageSeverity, children: [_jsx("span", { children: sprintf(
|
|
60
60
|
// translators: %1$d: number of requests remaining
|
|
61
|
-
__('You have %1$d
|
|
61
|
+
__('You have %1$d requests remaining.', 'jetpack-ai-client'), requestsRemaining) }), _jsx(Button, { variant: "link", onClick: onUpgradeClick, href: upgradeUrl, target: upgradeUrl ? '_blank' : null, children: __('Upgrade now', 'jetpack-ai-client') })] }));
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* React component to render an error message
|
|
@@ -66,9 +66,9 @@ export function UpgradeMessage({ requestsRemaining, severity, onUpgradeClick, })
|
|
|
66
66
|
* @param {number} requestsRemaining - Number of requests remaining.
|
|
67
67
|
* @returns {React.ReactElement } - Message component.
|
|
68
68
|
*/
|
|
69
|
-
export function ErrorMessage({ error, code, onTryAgainClick, onUpgradeClick, }) {
|
|
69
|
+
export function ErrorMessage({ error, code, onTryAgainClick, onUpgradeClick, upgradeUrl, }) {
|
|
70
70
|
const errorMessage = error || __('Something went wrong', 'jetpack-ai-client');
|
|
71
71
|
return (_jsxs(Message, { severity: MESSAGE_SEVERITY_ERROR, children: [_jsx("span", { children: sprintf(
|
|
72
72
|
// translators: %1$d: A dynamic error message
|
|
73
|
-
__('Error: %1$s', 'jetpack-ai-client'), errorMessage) }), code === ERROR_QUOTA_EXCEEDED ? (_jsx(Button, { variant: "link", onClick: onUpgradeClick, children: __('Upgrade now', 'jetpack-ai-client') })) : (_jsx(Button, { variant: "link", onClick: onTryAgainClick, children: __('Try again', 'jetpack-ai-client') }))] }));
|
|
73
|
+
__('Error: %1$s', 'jetpack-ai-client'), errorMessage) }), code === ERROR_QUOTA_EXCEEDED ? (_jsx(Button, { variant: "link", onClick: onUpgradeClick, href: upgradeUrl, target: upgradeUrl ? '_blank' : null, children: __('Upgrade now', 'jetpack-ai-client') })) : (_jsx(Button, { variant: "link", onClick: onTryAgainClick, children: __('Try again', 'jetpack-ai-client') }))] }));
|
|
74
74
|
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The type of the response from the image generation API.
|
|
3
|
+
*/
|
|
4
|
+
type ImageGenerationResponse = {
|
|
5
|
+
data: Array<{
|
|
6
|
+
[key: string]: string;
|
|
7
|
+
}>;
|
|
8
|
+
};
|
|
1
9
|
declare const useImageGenerator: () => {
|
|
2
10
|
generateImage: ({ feature, postContent, responseFormat, userPrompt, }: {
|
|
3
11
|
feature: string;
|
|
4
12
|
postContent: string;
|
|
5
13
|
responseFormat?: 'url' | 'b64_json';
|
|
6
14
|
userPrompt?: string;
|
|
7
|
-
}) => Promise<
|
|
8
|
-
data: {
|
|
9
|
-
[key: string]: string;
|
|
10
|
-
}[];
|
|
11
|
-
}>;
|
|
15
|
+
}) => Promise<ImageGenerationResponse>;
|
|
12
16
|
generateImageWithStableDiffusion: ({ feature, postContent, userPrompt, }: {
|
|
13
17
|
feature: string;
|
|
14
18
|
postContent: string;
|
|
15
19
|
userPrompt?: string;
|
|
16
|
-
}) => Promise<
|
|
17
|
-
data: {
|
|
18
|
-
[key: string]: string;
|
|
19
|
-
}[];
|
|
20
|
-
}>;
|
|
20
|
+
}) => Promise<ImageGenerationResponse>;
|
|
21
21
|
};
|
|
22
22
|
export default useImageGenerator;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
|
-
import { __ } from '@wordpress/i18n';
|
|
5
4
|
import debugFactory from 'debug';
|
|
6
5
|
/**
|
|
7
6
|
* Internal dependencies
|
|
@@ -130,101 +129,66 @@ const getStableDiffusionImageGenerationPrompt = async (postContent, userPrompt,
|
|
|
130
129
|
return data.choices?.[0]?.message?.content;
|
|
131
130
|
};
|
|
132
131
|
const useImageGenerator = () => {
|
|
133
|
-
const
|
|
134
|
-
let token =
|
|
132
|
+
const executeImageGeneration = async function (parameters) {
|
|
133
|
+
let token = '';
|
|
135
134
|
try {
|
|
136
|
-
token = await requestJwt();
|
|
135
|
+
token = (await requestJwt()).token;
|
|
137
136
|
}
|
|
138
137
|
catch (error) {
|
|
139
138
|
debug('Error getting token: %o', error);
|
|
140
139
|
return Promise.reject(error);
|
|
141
140
|
}
|
|
142
141
|
try {
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
style: 'photographic',
|
|
148
|
-
token: token.token,
|
|
149
|
-
width: 1024,
|
|
150
|
-
height: 768,
|
|
142
|
+
const URL = 'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-image';
|
|
143
|
+
const headers = {
|
|
144
|
+
Authorization: `Bearer ${token}`,
|
|
145
|
+
'Content-Type': 'application/json',
|
|
151
146
|
};
|
|
152
|
-
const
|
|
147
|
+
const data = await fetch(URL, {
|
|
153
148
|
method: 'POST',
|
|
154
|
-
headers
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
debug('Error generating image: %o', response);
|
|
161
|
-
return Promise.reject({
|
|
162
|
-
data: {
|
|
163
|
-
status: response.status,
|
|
164
|
-
},
|
|
165
|
-
message: __('Error generating image. Please try again later.', 'jetpack-ai-client'),
|
|
166
|
-
});
|
|
149
|
+
headers,
|
|
150
|
+
body: JSON.stringify(parameters),
|
|
151
|
+
}).then(response => response.json());
|
|
152
|
+
if (data?.data?.status && data?.data?.status > 200) {
|
|
153
|
+
debug('Error generating image: %o', data);
|
|
154
|
+
return Promise.reject(data);
|
|
167
155
|
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Convert the blob to base64 to keep the same format as the Dalle API.
|
|
171
|
-
*/
|
|
172
|
-
const base64 = await new Promise((resolve, reject) => {
|
|
173
|
-
const reader = new FileReader();
|
|
174
|
-
reader.onloadend = () => {
|
|
175
|
-
const base64data = reader.result;
|
|
176
|
-
return resolve(base64data.replace(/^data:image\/(png|jpg);base64,/, ''));
|
|
177
|
-
};
|
|
178
|
-
reader.onerror = reject;
|
|
179
|
-
reader.readAsDataURL(blob);
|
|
180
|
-
});
|
|
181
|
-
// Return the Dalle API format
|
|
182
|
-
return {
|
|
183
|
-
data: [
|
|
184
|
-
{
|
|
185
|
-
b64_json: base64,
|
|
186
|
-
revised_prompt: prompt,
|
|
187
|
-
},
|
|
188
|
-
],
|
|
189
|
-
};
|
|
156
|
+
return data;
|
|
190
157
|
}
|
|
191
158
|
catch (error) {
|
|
192
159
|
debug('Error generating image: %o', error);
|
|
193
160
|
return Promise.reject(error);
|
|
194
161
|
}
|
|
195
162
|
};
|
|
196
|
-
const
|
|
197
|
-
let token = '';
|
|
163
|
+
const generateImageWithStableDiffusion = async function ({ feature, postContent, userPrompt, }) {
|
|
198
164
|
try {
|
|
199
|
-
|
|
165
|
+
debug('Generating image with Stable Diffusion');
|
|
166
|
+
const prompt = await getStableDiffusionImageGenerationPrompt(postContent, userPrompt, feature);
|
|
167
|
+
const parameters = {
|
|
168
|
+
prompt,
|
|
169
|
+
feature,
|
|
170
|
+
model: 'stable-diffusion',
|
|
171
|
+
style: 'photographic',
|
|
172
|
+
};
|
|
173
|
+
const data = await executeImageGeneration(parameters);
|
|
174
|
+
return data;
|
|
200
175
|
}
|
|
201
176
|
catch (error) {
|
|
202
|
-
debug('Error
|
|
177
|
+
debug('Error generating image: %o', error);
|
|
203
178
|
return Promise.reject(error);
|
|
204
179
|
}
|
|
180
|
+
};
|
|
181
|
+
const generateImage = async function ({ feature, postContent, responseFormat = 'url', userPrompt, }) {
|
|
205
182
|
try {
|
|
206
183
|
debug('Generating image');
|
|
207
184
|
const imageGenerationPrompt = getDalleImageGenerationPrompt(postContent, userPrompt);
|
|
208
|
-
const
|
|
209
|
-
const body = {
|
|
185
|
+
const parameters = {
|
|
210
186
|
prompt: imageGenerationPrompt,
|
|
211
187
|
response_format: responseFormat,
|
|
212
188
|
feature,
|
|
213
189
|
size: '1792x1024',
|
|
214
190
|
};
|
|
215
|
-
const
|
|
216
|
-
Authorization: `Bearer ${token}`,
|
|
217
|
-
'Content-Type': 'application/json',
|
|
218
|
-
};
|
|
219
|
-
const data = await fetch(URL, {
|
|
220
|
-
method: 'POST',
|
|
221
|
-
headers,
|
|
222
|
-
body: JSON.stringify(body),
|
|
223
|
-
}).then(response => response.json());
|
|
224
|
-
if (data?.data?.status && data?.data?.status > 200) {
|
|
225
|
-
debug('Error generating image: %o', data);
|
|
226
|
-
return Promise.reject(data);
|
|
227
|
-
}
|
|
191
|
+
const data = await executeImageGeneration(parameters);
|
|
228
192
|
return data;
|
|
229
193
|
}
|
|
230
194
|
catch (error) {
|
|
@@ -5,11 +5,19 @@ import TurndownService from 'turndown';
|
|
|
5
5
|
/**
|
|
6
6
|
* Types
|
|
7
7
|
*/
|
|
8
|
-
import type { Options, Rule } from 'turndown';
|
|
8
|
+
import type { Options, Rule, Filter } from 'turndown';
|
|
9
|
+
export type Fix = 'paragraph';
|
|
9
10
|
export default class HTMLToMarkdown {
|
|
10
11
|
turndownService: TurndownService;
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
fixes: Fix[];
|
|
13
|
+
constructor({ options, rules, keep, remove, fixes, }?: {
|
|
14
|
+
options?: Options;
|
|
15
|
+
rules?: {
|
|
16
|
+
[key: string]: Rule;
|
|
17
|
+
};
|
|
18
|
+
keep?: Filter;
|
|
19
|
+
remove?: Filter;
|
|
20
|
+
fixes?: Fix[];
|
|
13
21
|
});
|
|
14
22
|
/**
|
|
15
23
|
* Renders HTML from Markdown content with specified processing rules.
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
4
|
import TurndownService from 'turndown';
|
|
5
|
+
const fixesList = {
|
|
6
|
+
paragraph: (content) => {
|
|
7
|
+
// Keep <br> tags to prevent paragraphs from being split
|
|
8
|
+
return content.replaceAll('\n', '<br />');
|
|
9
|
+
},
|
|
10
|
+
};
|
|
5
11
|
const defaultTurndownOptions = { emDelimiter: '_', headingStyle: 'atx' };
|
|
6
12
|
const defaultTurndownRules = {
|
|
7
13
|
strikethrough: {
|
|
@@ -13,10 +19,15 @@ const defaultTurndownRules = {
|
|
|
13
19
|
};
|
|
14
20
|
export default class HTMLToMarkdown {
|
|
15
21
|
turndownService;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
fixes;
|
|
23
|
+
constructor({ options = {}, rules = {}, keep = [], remove = [], fixes = [], } = {}) {
|
|
24
|
+
this.fixes = fixes;
|
|
25
|
+
this.turndownService = new TurndownService({ ...defaultTurndownOptions, ...options });
|
|
26
|
+
this.turndownService.keep(keep);
|
|
27
|
+
this.turndownService.remove(remove);
|
|
28
|
+
const allRules = { ...defaultTurndownRules, ...rules };
|
|
29
|
+
for (const rule in allRules) {
|
|
30
|
+
this.turndownService.addRule(rule, allRules[rule]);
|
|
20
31
|
}
|
|
21
32
|
}
|
|
22
33
|
/**
|
|
@@ -26,6 +37,9 @@ export default class HTMLToMarkdown {
|
|
|
26
37
|
* @returns {string} The rendered Markdown content
|
|
27
38
|
*/
|
|
28
39
|
render({ content }) {
|
|
29
|
-
|
|
40
|
+
const rendered = this.turndownService.turndown(content);
|
|
41
|
+
return this.fixes.reduce((renderedContent, fix) => {
|
|
42
|
+
return fixesList[fix](renderedContent);
|
|
43
|
+
}, rendered);
|
|
30
44
|
}
|
|
31
45
|
}
|
|
@@ -7,10 +7,11 @@ import MarkdownToHTML from './markdown-to-html.js';
|
|
|
7
7
|
* Types
|
|
8
8
|
*/
|
|
9
9
|
import type { Fix as HTMLFix } from './markdown-to-html.js';
|
|
10
|
-
export type RenderHTMLRules =
|
|
11
|
-
declare const renderHTMLFromMarkdown: ({ content, rules, }: {
|
|
10
|
+
export type RenderHTMLRules = Array<HTMLFix>;
|
|
11
|
+
declare const renderHTMLFromMarkdown: ({ content, rules, extension, }: {
|
|
12
12
|
content: string;
|
|
13
13
|
rules?: RenderHTMLRules;
|
|
14
|
+
extension?: boolean;
|
|
14
15
|
}) => string;
|
|
15
16
|
declare const renderMarkdownFromHTML: ({ content }: {
|
|
16
17
|
content: string;
|
|
@@ -5,8 +5,8 @@ import HTMLToMarkdown from './html-to-markdown.js';
|
|
|
5
5
|
import MarkdownToHTML from './markdown-to-html.js';
|
|
6
6
|
const defaultMarkdownConverter = new MarkdownToHTML();
|
|
7
7
|
const defaultHTMLConverter = new HTMLToMarkdown();
|
|
8
|
-
const renderHTMLFromMarkdown = ({ content, rules
|
|
9
|
-
return defaultMarkdownConverter.render({ content, rules });
|
|
8
|
+
const renderHTMLFromMarkdown = ({ content, rules, extension, }) => {
|
|
9
|
+
return defaultMarkdownConverter.render({ content, rules, extension });
|
|
10
10
|
};
|
|
11
11
|
const renderMarkdownFromHTML = ({ content }) => {
|
|
12
12
|
return defaultHTMLConverter.render({ content });
|
|
@@ -6,19 +6,21 @@ import MarkdownIt from 'markdown-it';
|
|
|
6
6
|
* Types
|
|
7
7
|
*/
|
|
8
8
|
import type { Options } from 'markdown-it';
|
|
9
|
-
export type Fix = 'list';
|
|
9
|
+
export type Fix = 'list' | 'paragraph' | 'listItem';
|
|
10
10
|
export default class MarkdownToHTML {
|
|
11
11
|
markdownConverter: MarkdownIt;
|
|
12
12
|
constructor(options?: Options);
|
|
13
13
|
/**
|
|
14
14
|
* Renders HTML from Markdown content with specified processing rules.
|
|
15
|
-
* @param {object} options
|
|
16
|
-
* @param {string} options.content
|
|
17
|
-
* @param {string} options.rules
|
|
18
|
-
* @
|
|
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
|
+
* @param {boolean} options.extension - Whether to apply the extension-specific rules
|
|
19
|
+
* @returns {string} The rendered HTML content
|
|
19
20
|
*/
|
|
20
|
-
render({ content, rules }: {
|
|
21
|
+
render({ content, rules, extension, }: {
|
|
21
22
|
content: string;
|
|
22
|
-
rules: Array<Fix
|
|
23
|
+
rules: Array<Fix>;
|
|
24
|
+
extension?: boolean;
|
|
23
25
|
}): string;
|
|
24
26
|
}
|
|
@@ -2,15 +2,53 @@
|
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
4
|
import MarkdownIt from 'markdown-it';
|
|
5
|
+
const addListComments = (content) => {
|
|
6
|
+
return (content
|
|
7
|
+
// First remove any existing Gutenberg comments to avoid duplicates
|
|
8
|
+
.replaceAll('<!-- wp:list-item -->', '')
|
|
9
|
+
.replaceAll('<!-- /wp:list-item -->', '')
|
|
10
|
+
.replaceAll('<!-- wp:list -->', '')
|
|
11
|
+
.replaceAll('<!-- /wp:list -->', '')
|
|
12
|
+
// Add Gutenberg comments to <li> tags
|
|
13
|
+
.replaceAll('<li>', '<!-- wp:list-item --><li>')
|
|
14
|
+
.replaceAll('</li>', '</li><!-- /wp:list-item -->')
|
|
15
|
+
// Add Gutenberg comments to <ol> tags
|
|
16
|
+
.replaceAll('<ol>', '<!-- wp:list {"ordered":true} --><ol>')
|
|
17
|
+
.replaceAll('</ol>', '</ol><!-- /wp:list -->')
|
|
18
|
+
// Add Gutenberg comments to <ul> tags
|
|
19
|
+
.replaceAll('<ul>', '<!-- wp:list --><ul>')
|
|
20
|
+
.replaceAll('</ul>', '</ul><!-- /wp:list -->'));
|
|
21
|
+
};
|
|
5
22
|
const fixes = {
|
|
6
|
-
list: (content) => {
|
|
23
|
+
list: (content, extension = false) => {
|
|
7
24
|
// Fix list indentation
|
|
8
|
-
|
|
25
|
+
const fixedIndentation = content
|
|
26
|
+
.replace(/<li>\s+<p>/g, '<li>')
|
|
27
|
+
.replace(/<\/p>\s+<\/li>/g, '</li>');
|
|
28
|
+
return extension ? addListComments(fixedIndentation) : fixedIndentation;
|
|
29
|
+
},
|
|
30
|
+
listItem: (content, extension = false) => {
|
|
31
|
+
if (!extension) {
|
|
32
|
+
return content;
|
|
33
|
+
}
|
|
34
|
+
return addListComments(content
|
|
35
|
+
// Remove wrapping <ul> or <ol> tag
|
|
36
|
+
.replace(/^<[ou]l>\s*/g, '')
|
|
37
|
+
.replace(/\s*<\/[ou]l>\s*$/g, ''));
|
|
38
|
+
},
|
|
39
|
+
paragraph: (content, extension = false) => {
|
|
40
|
+
if (!extension) {
|
|
41
|
+
return content;
|
|
42
|
+
}
|
|
43
|
+
// Fix encoding of <br /> tags
|
|
44
|
+
return content.replaceAll(/\s*<br \/>\s*/g, '<br />');
|
|
9
45
|
},
|
|
10
46
|
};
|
|
11
47
|
const defaultMarkdownItOptions = {
|
|
12
48
|
breaks: true,
|
|
13
49
|
};
|
|
50
|
+
// The rules used by the AI Assistant block
|
|
51
|
+
const assistantBlockRules = ['list'];
|
|
14
52
|
export default class MarkdownToHTML {
|
|
15
53
|
markdownConverter;
|
|
16
54
|
constructor(options = defaultMarkdownItOptions) {
|
|
@@ -18,16 +56,16 @@ export default class MarkdownToHTML {
|
|
|
18
56
|
}
|
|
19
57
|
/**
|
|
20
58
|
* Renders HTML from Markdown content with specified processing rules.
|
|
21
|
-
* @param {object} options
|
|
22
|
-
* @param {string} options.content
|
|
23
|
-
* @param {string} options.rules
|
|
24
|
-
* @
|
|
59
|
+
* @param {object} options - The options to use when rendering the HTML content
|
|
60
|
+
* @param {string} options.content - The Markdown content to render
|
|
61
|
+
* @param {string} options.rules - The rules to apply to the rendered content
|
|
62
|
+
* @param {boolean} options.extension - Whether to apply the extension-specific rules
|
|
63
|
+
* @returns {string} The rendered HTML content
|
|
25
64
|
*/
|
|
26
|
-
render({ content, rules =
|
|
65
|
+
render({ content, rules = assistantBlockRules, extension = false, }) {
|
|
27
66
|
const rendered = this.markdownConverter.render(content);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return fixes[rule](renderedContent);
|
|
67
|
+
return rules.reduce((renderedContent, rule) => {
|
|
68
|
+
return fixes[rule](renderedContent, extension);
|
|
31
69
|
}, rendered);
|
|
32
70
|
}
|
|
33
71
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"private": false,
|
|
3
3
|
"name": "@automattic/jetpack-ai-client",
|
|
4
|
-
"version": "0.14.
|
|
4
|
+
"version": "0.14.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": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"types": "./build/index.d.ts",
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@automattic/jetpack-base-styles": "^0.6.25",
|
|
46
|
-
"@automattic/jetpack-connection": "^0.33.
|
|
46
|
+
"@automattic/jetpack-connection": "^0.33.12",
|
|
47
47
|
"@automattic/jetpack-shared-extension-utils": "^0.14.13",
|
|
48
48
|
"@microsoft/fetch-event-source": "2.0.1",
|
|
49
49
|
"@types/react": "18.3.1",
|
|
@@ -31,6 +31,7 @@ type ExtensionAIControlProps = {
|
|
|
31
31
|
error?: RequestingErrorProps;
|
|
32
32
|
requestsRemaining?: number;
|
|
33
33
|
showUpgradeMessage?: boolean;
|
|
34
|
+
upgradeUrl?: string;
|
|
34
35
|
wrapperRef?: React.MutableRefObject< HTMLDivElement | null >;
|
|
35
36
|
onChange?: ( newValue: string ) => void;
|
|
36
37
|
onSend?: ( currentValue: string ) => void;
|
|
@@ -61,6 +62,7 @@ export function ExtensionAIControl(
|
|
|
61
62
|
error,
|
|
62
63
|
requestsRemaining,
|
|
63
64
|
showUpgradeMessage = false,
|
|
65
|
+
upgradeUrl,
|
|
64
66
|
wrapperRef,
|
|
65
67
|
onChange,
|
|
66
68
|
onSend,
|
|
@@ -210,11 +212,16 @@ export function ExtensionAIControl(
|
|
|
210
212
|
code={ error.code }
|
|
211
213
|
onTryAgainClick={ tryAgainHandler }
|
|
212
214
|
onUpgradeClick={ upgradeHandler }
|
|
215
|
+
upgradeUrl={ upgradeUrl }
|
|
213
216
|
/>
|
|
214
217
|
);
|
|
215
218
|
} else if ( showUpgradeMessage ) {
|
|
216
219
|
message = (
|
|
217
|
-
<UpgradeMessage
|
|
220
|
+
<UpgradeMessage
|
|
221
|
+
requestsRemaining={ requestsRemaining }
|
|
222
|
+
onUpgradeClick={ upgradeHandler }
|
|
223
|
+
upgradeUrl={ upgradeUrl }
|
|
224
|
+
/>
|
|
218
225
|
);
|
|
219
226
|
} else if ( showGuideLine ) {
|
|
220
227
|
message = <GuidelineMessage />;
|
|
@@ -45,6 +45,7 @@ export type UpgradeMessageProps = {
|
|
|
45
45
|
requestsRemaining: number;
|
|
46
46
|
severity?: MessageSeverityProp;
|
|
47
47
|
onUpgradeClick: OnUpgradeClick;
|
|
48
|
+
upgradeUrl?: string;
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
export type ErrorMessageProps = {
|
|
@@ -52,6 +53,7 @@ export type ErrorMessageProps = {
|
|
|
52
53
|
code?: SuggestionErrorCode;
|
|
53
54
|
onTryAgainClick: () => void;
|
|
54
55
|
onUpgradeClick: OnUpgradeClick;
|
|
56
|
+
upgradeUrl?: string;
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
const messageIconsMap = {
|
|
@@ -122,6 +124,7 @@ export function UpgradeMessage( {
|
|
|
122
124
|
requestsRemaining,
|
|
123
125
|
severity,
|
|
124
126
|
onUpgradeClick,
|
|
127
|
+
upgradeUrl,
|
|
125
128
|
}: UpgradeMessageProps ): React.ReactElement {
|
|
126
129
|
let messageSeverity = severity;
|
|
127
130
|
|
|
@@ -134,11 +137,16 @@ export function UpgradeMessage( {
|
|
|
134
137
|
<span>
|
|
135
138
|
{ sprintf(
|
|
136
139
|
// translators: %1$d: number of requests remaining
|
|
137
|
-
__( 'You have %1$d
|
|
140
|
+
__( 'You have %1$d requests remaining.', 'jetpack-ai-client' ),
|
|
138
141
|
requestsRemaining
|
|
139
142
|
) }
|
|
140
143
|
</span>
|
|
141
|
-
<Button
|
|
144
|
+
<Button
|
|
145
|
+
variant="link"
|
|
146
|
+
onClick={ onUpgradeClick }
|
|
147
|
+
href={ upgradeUrl }
|
|
148
|
+
target={ upgradeUrl ? '_blank' : null }
|
|
149
|
+
>
|
|
142
150
|
{ __( 'Upgrade now', 'jetpack-ai-client' ) }
|
|
143
151
|
</Button>
|
|
144
152
|
</Message>
|
|
@@ -156,6 +164,7 @@ export function ErrorMessage( {
|
|
|
156
164
|
code,
|
|
157
165
|
onTryAgainClick,
|
|
158
166
|
onUpgradeClick,
|
|
167
|
+
upgradeUrl,
|
|
159
168
|
}: ErrorMessageProps ): React.ReactElement {
|
|
160
169
|
const errorMessage = error || __( 'Something went wrong', 'jetpack-ai-client' );
|
|
161
170
|
|
|
@@ -169,7 +178,12 @@ export function ErrorMessage( {
|
|
|
169
178
|
) }
|
|
170
179
|
</span>
|
|
171
180
|
{ code === ERROR_QUOTA_EXCEEDED ? (
|
|
172
|
-
<Button
|
|
181
|
+
<Button
|
|
182
|
+
variant="link"
|
|
183
|
+
onClick={ onUpgradeClick }
|
|
184
|
+
href={ upgradeUrl }
|
|
185
|
+
target={ upgradeUrl ? '_blank' : null }
|
|
186
|
+
>
|
|
173
187
|
{ __( 'Upgrade now', 'jetpack-ai-client' ) }
|
|
174
188
|
</Button>
|
|
175
189
|
) : (
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
|
-
import { __ } from '@wordpress/i18n';
|
|
5
4
|
import debugFactory from 'debug';
|
|
6
5
|
/**
|
|
7
6
|
* Internal dependencies
|
|
@@ -11,6 +10,13 @@ import requestJwt from '../../jwt/index.js';
|
|
|
11
10
|
|
|
12
11
|
const debug = debugFactory( 'ai-client:use-image-generator' );
|
|
13
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The type of the response from the image generation API.
|
|
15
|
+
*/
|
|
16
|
+
type ImageGenerationResponse = {
|
|
17
|
+
data: Array< { [ key: string ]: string } >;
|
|
18
|
+
};
|
|
19
|
+
|
|
14
20
|
/**
|
|
15
21
|
* Cut the post content on a given lenght so the total length of the prompt is not longer than 4000 characters.
|
|
16
22
|
* @param {string} content - the content to be truncated
|
|
@@ -148,6 +154,44 @@ const getStableDiffusionImageGenerationPrompt = async (
|
|
|
148
154
|
};
|
|
149
155
|
|
|
150
156
|
const useImageGenerator = () => {
|
|
157
|
+
const executeImageGeneration = async function ( parameters: {
|
|
158
|
+
[ key: string ]: string;
|
|
159
|
+
} ): Promise< ImageGenerationResponse > {
|
|
160
|
+
let token = '';
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
token = ( await requestJwt() ).token;
|
|
164
|
+
} catch ( error ) {
|
|
165
|
+
debug( 'Error getting token: %o', error );
|
|
166
|
+
return Promise.reject( error );
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const URL = 'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-image';
|
|
171
|
+
|
|
172
|
+
const headers = {
|
|
173
|
+
Authorization: `Bearer ${ token }`,
|
|
174
|
+
'Content-Type': 'application/json',
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const data = await fetch( URL, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers,
|
|
180
|
+
body: JSON.stringify( parameters ),
|
|
181
|
+
} ).then( response => response.json() );
|
|
182
|
+
|
|
183
|
+
if ( data?.data?.status && data?.data?.status > 200 ) {
|
|
184
|
+
debug( 'Error generating image: %o', data );
|
|
185
|
+
return Promise.reject( data );
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return data as ImageGenerationResponse;
|
|
189
|
+
} catch ( error ) {
|
|
190
|
+
debug( 'Error generating image: %o', error );
|
|
191
|
+
return Promise.reject( error );
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
151
195
|
const generateImageWithStableDiffusion = async function ( {
|
|
152
196
|
feature,
|
|
153
197
|
postContent,
|
|
@@ -156,16 +200,7 @@ const useImageGenerator = () => {
|
|
|
156
200
|
feature: string;
|
|
157
201
|
postContent: string;
|
|
158
202
|
userPrompt?: string;
|
|
159
|
-
} ): Promise<
|
|
160
|
-
let token = null;
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
token = await requestJwt();
|
|
164
|
-
} catch ( error ) {
|
|
165
|
-
debug( 'Error getting token: %o', error );
|
|
166
|
-
return Promise.reject( error );
|
|
167
|
-
}
|
|
168
|
-
|
|
203
|
+
} ): Promise< ImageGenerationResponse > {
|
|
169
204
|
try {
|
|
170
205
|
debug( 'Generating image with Stable Diffusion' );
|
|
171
206
|
|
|
@@ -175,59 +210,15 @@ const useImageGenerator = () => {
|
|
|
175
210
|
feature
|
|
176
211
|
);
|
|
177
212
|
|
|
178
|
-
const
|
|
213
|
+
const parameters = {
|
|
179
214
|
prompt,
|
|
215
|
+
feature,
|
|
216
|
+
model: 'stable-diffusion',
|
|
180
217
|
style: 'photographic',
|
|
181
|
-
token: token.token,
|
|
182
|
-
width: 1024,
|
|
183
|
-
height: 768,
|
|
184
218
|
};
|
|
185
219
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
{
|
|
189
|
-
method: 'POST',
|
|
190
|
-
headers: {
|
|
191
|
-
'Content-Type': 'application/json',
|
|
192
|
-
},
|
|
193
|
-
body: JSON.stringify( data ),
|
|
194
|
-
}
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
if ( ! response?.ok ) {
|
|
198
|
-
debug( 'Error generating image: %o', response );
|
|
199
|
-
return Promise.reject( {
|
|
200
|
-
data: {
|
|
201
|
-
status: response.status,
|
|
202
|
-
},
|
|
203
|
-
message: __( 'Error generating image. Please try again later.', 'jetpack-ai-client' ),
|
|
204
|
-
} );
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const blob = await response.blob();
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Convert the blob to base64 to keep the same format as the Dalle API.
|
|
211
|
-
*/
|
|
212
|
-
const base64 = await new Promise( ( resolve, reject ) => {
|
|
213
|
-
const reader = new FileReader();
|
|
214
|
-
reader.onloadend = () => {
|
|
215
|
-
const base64data = reader.result as string;
|
|
216
|
-
return resolve( base64data.replace( /^data:image\/(png|jpg);base64,/, '' ) );
|
|
217
|
-
};
|
|
218
|
-
reader.onerror = reject;
|
|
219
|
-
reader.readAsDataURL( blob );
|
|
220
|
-
} );
|
|
221
|
-
|
|
222
|
-
// Return the Dalle API format
|
|
223
|
-
return {
|
|
224
|
-
data: [
|
|
225
|
-
{
|
|
226
|
-
b64_json: base64 as string,
|
|
227
|
-
revised_prompt: prompt,
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
};
|
|
220
|
+
const data: ImageGenerationResponse = await executeImageGeneration( parameters );
|
|
221
|
+
return data;
|
|
231
222
|
} catch ( error ) {
|
|
232
223
|
debug( 'Error generating image: %o', error );
|
|
233
224
|
return Promise.reject( error );
|
|
@@ -244,47 +235,21 @@ const useImageGenerator = () => {
|
|
|
244
235
|
postContent: string;
|
|
245
236
|
responseFormat?: 'url' | 'b64_json';
|
|
246
237
|
userPrompt?: string;
|
|
247
|
-
} ): Promise<
|
|
248
|
-
let token = '';
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
token = ( await requestJwt() ).token;
|
|
252
|
-
} catch ( error ) {
|
|
253
|
-
debug( 'Error getting token: %o', error );
|
|
254
|
-
return Promise.reject( error );
|
|
255
|
-
}
|
|
256
|
-
|
|
238
|
+
} ): Promise< ImageGenerationResponse > {
|
|
257
239
|
try {
|
|
258
240
|
debug( 'Generating image' );
|
|
259
241
|
|
|
260
242
|
const imageGenerationPrompt = getDalleImageGenerationPrompt( postContent, userPrompt );
|
|
261
243
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
const body = {
|
|
244
|
+
const parameters = {
|
|
265
245
|
prompt: imageGenerationPrompt,
|
|
266
246
|
response_format: responseFormat,
|
|
267
247
|
feature,
|
|
268
248
|
size: '1792x1024',
|
|
269
249
|
};
|
|
270
250
|
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
'Content-Type': 'application/json',
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
const data = await fetch( URL, {
|
|
277
|
-
method: 'POST',
|
|
278
|
-
headers,
|
|
279
|
-
body: JSON.stringify( body ),
|
|
280
|
-
} ).then( response => response.json() );
|
|
281
|
-
|
|
282
|
-
if ( data?.data?.status && data?.data?.status > 200 ) {
|
|
283
|
-
debug( 'Error generating image: %o', data );
|
|
284
|
-
return Promise.reject( data );
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return data as { data: { [ key: string ]: string }[] };
|
|
251
|
+
const data: ImageGenerationResponse = await executeImageGeneration( parameters );
|
|
252
|
+
return data;
|
|
288
253
|
} catch ( error ) {
|
|
289
254
|
debug( 'Error generating image: %o', error );
|
|
290
255
|
return Promise.reject( error );
|
|
@@ -35,7 +35,7 @@ const rules = {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
|
-
const renderer = new HTMLToMarkdown( options, rules );
|
|
38
|
+
const renderer = new HTMLToMarkdown( { options, rules } );
|
|
39
39
|
const markdownContent = renderer.render( { content: htmlContent } );
|
|
40
40
|
// ***Hello world***
|
|
41
41
|
```
|
|
@@ -52,7 +52,7 @@ Example:
|
|
|
52
52
|
import { renderHTMLFromMarkdown } from '@automattic/jetpack-ai-client';
|
|
53
53
|
|
|
54
54
|
const markdownContent = '**Hello world**';
|
|
55
|
-
const htmlContent = renderHTMLFromMarkdown( { content: markdownContent, rules: '
|
|
55
|
+
const htmlContent = renderHTMLFromMarkdown( { content: markdownContent, rules: [ 'list' ], extension: false } ); // [ 'list' ] and false are default values
|
|
56
56
|
// <p><strong>Hello world</strong></p>\n
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -71,4 +71,5 @@ const htmlContent = renderer.render( { content: markdownContent, rules } );
|
|
|
71
71
|
// <p><strong>Hello world</strong></p>\n
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
The `rules` array supports the following values: `'list'`, `'paragraph'` and `'list-item'`. Further specific fixes can be added when necessary.
|
|
75
|
+
Due to different implementations between the Jetpack AI Assistant block and its extensions, the `extension` flag is used to apply context-specific fixes.
|
|
@@ -5,7 +5,19 @@ import TurndownService from 'turndown';
|
|
|
5
5
|
/**
|
|
6
6
|
* Types
|
|
7
7
|
*/
|
|
8
|
-
import type { Options, Rule } from 'turndown';
|
|
8
|
+
import type { Options, Rule, Filter } from 'turndown';
|
|
9
|
+
|
|
10
|
+
export type Fix = 'paragraph';
|
|
11
|
+
type Fixes = {
|
|
12
|
+
[ key in Fix ]: ( content: string ) => string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const fixesList: Fixes = {
|
|
16
|
+
paragraph: ( content: string ) => {
|
|
17
|
+
// Keep <br> tags to prevent paragraphs from being split
|
|
18
|
+
return content.replaceAll( '\n', '<br />' );
|
|
19
|
+
},
|
|
20
|
+
};
|
|
9
21
|
|
|
10
22
|
const defaultTurndownOptions: Options = { emDelimiter: '_', headingStyle: 'atx' };
|
|
11
23
|
const defaultTurndownRules: { [ key: string ]: Rule } = {
|
|
@@ -19,14 +31,29 @@ const defaultTurndownRules: { [ key: string ]: Rule } = {
|
|
|
19
31
|
|
|
20
32
|
export default class HTMLToMarkdown {
|
|
21
33
|
turndownService: TurndownService;
|
|
34
|
+
fixes: Fix[];
|
|
35
|
+
|
|
36
|
+
constructor( {
|
|
37
|
+
options = {},
|
|
38
|
+
rules = {},
|
|
39
|
+
keep = [],
|
|
40
|
+
remove = [],
|
|
41
|
+
fixes = [],
|
|
42
|
+
}: {
|
|
43
|
+
options?: Options;
|
|
44
|
+
rules?: { [ key: string ]: Rule };
|
|
45
|
+
keep?: Filter;
|
|
46
|
+
remove?: Filter;
|
|
47
|
+
fixes?: Fix[];
|
|
48
|
+
} = {} ) {
|
|
49
|
+
this.fixes = fixes;
|
|
50
|
+
this.turndownService = new TurndownService( { ...defaultTurndownOptions, ...options } );
|
|
51
|
+
this.turndownService.keep( keep );
|
|
52
|
+
this.turndownService.remove( remove );
|
|
22
53
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
) {
|
|
27
|
-
this.turndownService = new TurndownService( options );
|
|
28
|
-
for ( const rule in rules ) {
|
|
29
|
-
this.turndownService.addRule( rule, rules[ rule ] );
|
|
54
|
+
const allRules = { ...defaultTurndownRules, ...rules };
|
|
55
|
+
for ( const rule in allRules ) {
|
|
56
|
+
this.turndownService.addRule( rule, allRules[ rule ] );
|
|
30
57
|
}
|
|
31
58
|
}
|
|
32
59
|
|
|
@@ -37,6 +64,10 @@ export default class HTMLToMarkdown {
|
|
|
37
64
|
* @returns {string} The rendered Markdown content
|
|
38
65
|
*/
|
|
39
66
|
render( { content }: { content: string } ): string {
|
|
40
|
-
|
|
67
|
+
const rendered = this.turndownService.turndown( content );
|
|
68
|
+
|
|
69
|
+
return this.fixes.reduce( ( renderedContent, fix ) => {
|
|
70
|
+
return fixesList[ fix ]( renderedContent );
|
|
71
|
+
}, rendered );
|
|
41
72
|
}
|
|
42
73
|
}
|
|
@@ -11,16 +11,18 @@ import type { Fix as HTMLFix } from './markdown-to-html.js';
|
|
|
11
11
|
const defaultMarkdownConverter = new MarkdownToHTML();
|
|
12
12
|
const defaultHTMLConverter = new HTMLToMarkdown();
|
|
13
13
|
|
|
14
|
-
export type RenderHTMLRules =
|
|
14
|
+
export type RenderHTMLRules = Array< HTMLFix >;
|
|
15
15
|
|
|
16
16
|
const renderHTMLFromMarkdown = ( {
|
|
17
17
|
content,
|
|
18
|
-
rules
|
|
18
|
+
rules,
|
|
19
|
+
extension,
|
|
19
20
|
}: {
|
|
20
21
|
content: string;
|
|
21
22
|
rules?: RenderHTMLRules;
|
|
23
|
+
extension?: boolean;
|
|
22
24
|
} ) => {
|
|
23
|
-
return defaultMarkdownConverter.render( { content, rules } );
|
|
25
|
+
return defaultMarkdownConverter.render( { content, rules, extension } );
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
const renderMarkdownFromHTML = ( { content }: { content: string } ) => {
|
|
@@ -7,15 +7,59 @@ import MarkdownIt from 'markdown-it';
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Options } from 'markdown-it';
|
|
9
9
|
|
|
10
|
-
export type Fix = 'list';
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
export type Fix = 'list' | 'paragraph' | 'listItem';
|
|
11
|
+
|
|
12
|
+
const addListComments = ( content: string ) => {
|
|
13
|
+
return (
|
|
14
|
+
content
|
|
15
|
+
// First remove any existing Gutenberg comments to avoid duplicates
|
|
16
|
+
.replaceAll( '<!-- wp:list-item -->', '' )
|
|
17
|
+
.replaceAll( '<!-- /wp:list-item -->', '' )
|
|
18
|
+
.replaceAll( '<!-- wp:list -->', '' )
|
|
19
|
+
.replaceAll( '<!-- /wp:list -->', '' )
|
|
20
|
+
// Add Gutenberg comments to <li> tags
|
|
21
|
+
.replaceAll( '<li>', '<!-- wp:list-item --><li>' )
|
|
22
|
+
.replaceAll( '</li>', '</li><!-- /wp:list-item -->' )
|
|
23
|
+
// Add Gutenberg comments to <ol> tags
|
|
24
|
+
.replaceAll( '<ol>', '<!-- wp:list {"ordered":true} --><ol>' )
|
|
25
|
+
.replaceAll( '</ol>', '</ol><!-- /wp:list -->' )
|
|
26
|
+
// Add Gutenberg comments to <ul> tags
|
|
27
|
+
.replaceAll( '<ul>', '<!-- wp:list --><ul>' )
|
|
28
|
+
.replaceAll( '</ul>', '</ul><!-- /wp:list -->' )
|
|
29
|
+
);
|
|
13
30
|
};
|
|
14
31
|
|
|
32
|
+
type Fixes = {
|
|
33
|
+
[ key in Fix ]: ( content: string, extension?: boolean ) => string;
|
|
34
|
+
};
|
|
15
35
|
const fixes: Fixes = {
|
|
16
|
-
list: ( content: string ) => {
|
|
36
|
+
list: ( content: string, extension = false ) => {
|
|
17
37
|
// Fix list indentation
|
|
18
|
-
|
|
38
|
+
const fixedIndentation = content
|
|
39
|
+
.replace( /<li>\s+<p>/g, '<li>' )
|
|
40
|
+
.replace( /<\/p>\s+<\/li>/g, '</li>' );
|
|
41
|
+
|
|
42
|
+
return extension ? addListComments( fixedIndentation ) : fixedIndentation;
|
|
43
|
+
},
|
|
44
|
+
listItem: ( content: string, extension = false ) => {
|
|
45
|
+
if ( ! extension ) {
|
|
46
|
+
return content;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return addListComments(
|
|
50
|
+
content
|
|
51
|
+
// Remove wrapping <ul> or <ol> tag
|
|
52
|
+
.replace( /^<[ou]l>\s*/g, '' )
|
|
53
|
+
.replace( /\s*<\/[ou]l>\s*$/g, '' )
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
paragraph: ( content: string, extension = false ) => {
|
|
57
|
+
if ( ! extension ) {
|
|
58
|
+
return content;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fix encoding of <br /> tags
|
|
62
|
+
return content.replaceAll( /\s*<br \/>\s*/g, '<br />' );
|
|
19
63
|
},
|
|
20
64
|
};
|
|
21
65
|
|
|
@@ -23,6 +67,9 @@ const defaultMarkdownItOptions: Options = {
|
|
|
23
67
|
breaks: true,
|
|
24
68
|
};
|
|
25
69
|
|
|
70
|
+
// The rules used by the AI Assistant block
|
|
71
|
+
const assistantBlockRules: Array< Fix > = [ 'list' ];
|
|
72
|
+
|
|
26
73
|
export default class MarkdownToHTML {
|
|
27
74
|
markdownConverter: MarkdownIt;
|
|
28
75
|
|
|
@@ -32,17 +79,25 @@ export default class MarkdownToHTML {
|
|
|
32
79
|
|
|
33
80
|
/**
|
|
34
81
|
* Renders HTML from Markdown content with specified processing rules.
|
|
35
|
-
* @param {object} options
|
|
36
|
-
* @param {string} options.content
|
|
37
|
-
* @param {string} options.rules
|
|
38
|
-
* @
|
|
82
|
+
* @param {object} options - The options to use when rendering the HTML content
|
|
83
|
+
* @param {string} options.content - The Markdown content to render
|
|
84
|
+
* @param {string} options.rules - The rules to apply to the rendered content
|
|
85
|
+
* @param {boolean} options.extension - Whether to apply the extension-specific rules
|
|
86
|
+
* @returns {string} The rendered HTML content
|
|
39
87
|
*/
|
|
40
|
-
render( {
|
|
88
|
+
render( {
|
|
89
|
+
content,
|
|
90
|
+
rules = assistantBlockRules,
|
|
91
|
+
extension = false,
|
|
92
|
+
}: {
|
|
93
|
+
content: string;
|
|
94
|
+
rules: Array< Fix >;
|
|
95
|
+
extension?: boolean;
|
|
96
|
+
} ): string {
|
|
41
97
|
const rendered = this.markdownConverter.render( content );
|
|
42
|
-
const rulesToApply = rules === 'all' ? Object.keys( fixes ) : rules;
|
|
43
98
|
|
|
44
|
-
return
|
|
45
|
-
return fixes[ rule ]( renderedContent );
|
|
99
|
+
return rules.reduce( ( renderedContent, rule ) => {
|
|
100
|
+
return fixes[ rule ]( renderedContent, extension );
|
|
46
101
|
}, rendered );
|
|
47
102
|
}
|
|
48
103
|
}
|