@automattic/jetpack-ai-client 0.13.0 → 0.14.0

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,23 @@ 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.0] - 2024-05-20
9
+ ### Added
10
+ - AI Client: Expose HTML render rules type. [#37386]
11
+ - AI Featured Image: Support Stable Diffusion image generation. [#37413]
12
+
13
+ ### Changed
14
+ - AI Client: Change default behavior of Message components [#37365]
15
+ - Updated package dependencies. [#37379] [#37380]
16
+
17
+ ## [0.13.1] - 2024-05-13
18
+ ### Added
19
+ - AI Client: Add className to AI Control component. [#37322]
20
+ - AI Client: Add "try again" prop on Extension AI Control. [#37250]
21
+
22
+ ### Changed
23
+ - AI Client: Add event to upgrade handler function of Extension AI Control. [#37224]
24
+
8
25
  ## [0.13.0] - 2024-05-06
9
26
  ### Added
10
27
  - AI Client: Add wrapper ref to AI Control. [#37145]
@@ -306,6 +323,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
306
323
  - Updated package dependencies. [#31659]
307
324
  - Updated package dependencies. [#31785]
308
325
 
326
+ [0.14.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.13.1...v0.14.0
327
+ [0.13.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.13.0...v0.13.1
309
328
  [0.13.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.12.4...v0.13.0
310
329
  [0.12.4]: https://github.com/Automattic/jetpack-ai-client/compare/v0.12.3...v0.12.4
311
330
  [0.12.3]: https://github.com/Automattic/jetpack-ai-client/compare/v0.12.2...v0.12.3
@@ -0,0 +1,30 @@
1
+ import { AskQuestionOptionsArgProps } from './index.js';
2
+ import type { PromptProp } from '../types.js';
3
+ /**
4
+ * The response data from the AI assistant when doing a sync, not-streamed question.
5
+ */
6
+ export type ResponseData = {
7
+ choices: Array<{
8
+ message: {
9
+ content: string;
10
+ };
11
+ }>;
12
+ };
13
+ /**
14
+ * A function that asks a question without streaming.
15
+ *
16
+ * @param {PromptProp} question - The question to ask. It can be a simple string or an array of PromptMessageItemProps objects.
17
+ * @param {AskQuestionOptionsArgProps} options - An optional object for additional configuration: postId, feature, model.
18
+ * @returns {Promise<ResponseData>} - A promise that resolves to an instance of the ResponseData
19
+ * @example
20
+ * const question = "What is the meaning of life?";
21
+ * const options = {
22
+ * feature: 'ai-featured-image',
23
+ * model: 'gpt-4-turbo'
24
+ * }
25
+ * askQuestionSync( question, options ).then( responseData => {
26
+ * // access the choices array on the response data
27
+ * const content = responseData.choices[ 0 ].message.content;
28
+ * } );
29
+ */
30
+ export default function askQuestionSync(question: PromptProp, { postId, feature, model }?: AskQuestionOptionsArgProps): Promise<ResponseData>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import debugFactory from 'debug';
5
+ /*
6
+ * Types & constants
7
+ */
8
+ import requestJwt from '../jwt/index.js';
9
+ const debug = debugFactory('jetpack-ai-client:ask-question-sync');
10
+ /**
11
+ * A function that asks a question without streaming.
12
+ *
13
+ * @param {PromptProp} question - The question to ask. It can be a simple string or an array of PromptMessageItemProps objects.
14
+ * @param {AskQuestionOptionsArgProps} options - An optional object for additional configuration: postId, feature, model.
15
+ * @returns {Promise<ResponseData>} - A promise that resolves to an instance of the ResponseData
16
+ * @example
17
+ * const question = "What is the meaning of life?";
18
+ * const options = {
19
+ * feature: 'ai-featured-image',
20
+ * model: 'gpt-4-turbo'
21
+ * }
22
+ * askQuestionSync( question, options ).then( responseData => {
23
+ * // access the choices array on the response data
24
+ * const content = responseData.choices[ 0 ].message.content;
25
+ * } );
26
+ */
27
+ export default async function askQuestionSync(question, { postId = null, feature, model } = {}) {
28
+ debug('Asking question with no streaming: %o. options: %o', question, {
29
+ postId,
30
+ feature,
31
+ model,
32
+ });
33
+ /**
34
+ * The URL to the AI assistant query endpoint.
35
+ */
36
+ const URL = 'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-query';
37
+ let token = null;
38
+ try {
39
+ token = (await requestJwt()).token;
40
+ }
41
+ catch (error) {
42
+ debug('Error getting token: %o', error);
43
+ return Promise.reject(error);
44
+ }
45
+ const body = {
46
+ question: question,
47
+ stream: false,
48
+ postId,
49
+ feature,
50
+ model,
51
+ };
52
+ const headers = {
53
+ Authorization: `Bearer ${token}`,
54
+ 'Content-Type': 'application/json',
55
+ };
56
+ const data = await fetch(URL, {
57
+ method: 'POST',
58
+ headers,
59
+ body: JSON.stringify(body),
60
+ }).then(response => response.json());
61
+ if (data?.data?.status && data?.data?.status > 200) {
62
+ debug('Error generating prompt: %o', data);
63
+ return Promise.reject(data);
64
+ }
65
+ return data;
66
+ }
@@ -6,6 +6,7 @@ import './style.scss';
6
6
  import type { RequestingStateProp } from '../../types.js';
7
7
  import type { ReactElement } from 'react';
8
8
  type AIControlProps = {
9
+ className?: string;
9
10
  disabled?: boolean;
10
11
  value: string;
11
12
  placeholder?: string;
@@ -25,5 +26,5 @@ type AIControlProps = {
25
26
  * @param {AIControlProps} props - Component props
26
27
  * @returns {ReactElement} Rendered component
27
28
  */
28
- export default function AIControl({ disabled, value, placeholder, isTransparent, state, onChange, banner, error, actions, message, promptUserInputRef, wrapperRef, }: AIControlProps): ReactElement;
29
+ export default function AIControl({ className, disabled, value, placeholder, isTransparent, state, onChange, banner, error, actions, message, promptUserInputRef, wrapperRef, }: AIControlProps): ReactElement;
29
30
  export {};
@@ -15,8 +15,8 @@ import './style.scss';
15
15
  * @param {AIControlProps} props - Component props
16
16
  * @returns {ReactElement} Rendered component
17
17
  */
18
- export default function AIControl({ disabled = false, value = '', placeholder = '', isTransparent = false, state = 'init', onChange, banner = null, error = null, actions = null, message = null, promptUserInputRef = null, wrapperRef = null, }) {
19
- return (_jsxs("div", { className: "jetpack-components-ai-control__container-wrapper", ref: wrapperRef, children: [error, _jsxs("div", { className: "jetpack-components-ai-control__container", children: [banner, _jsxs("div", { className: classNames('jetpack-components-ai-control__wrapper', {
18
+ export default function AIControl({ className, disabled = false, value = '', placeholder = '', isTransparent = false, state = 'init', onChange, banner = null, error = null, actions = null, message = null, promptUserInputRef = null, wrapperRef = null, }) {
19
+ return (_jsxs("div", { className: classNames('jetpack-components-ai-control__container-wrapper', className), ref: wrapperRef, children: [error, _jsxs("div", { className: "jetpack-components-ai-control__container", children: [banner, _jsxs("div", { className: classNames('jetpack-components-ai-control__wrapper', {
20
20
  'is-transparent': isTransparent,
21
21
  }), children: [_jsx(AiStatusIndicator, { state: state }), _jsx("div", { className: "jetpack-components-ai-control__input-wrapper", children: _jsx(PlainText, { value: value, onChange: onChange, placeholder: placeholder, className: "jetpack-components-ai-control__input", disabled: disabled, ref: promptUserInputRef }) }), actions] }), message] })] }));
22
22
  }
@@ -3,9 +3,10 @@ import './style.scss';
3
3
  /**
4
4
  * Types
5
5
  */
6
- import type { RequestingStateProp } from '../../types.js';
7
- import type { ReactElement } from 'react';
6
+ import type { RequestingErrorProps, RequestingStateProp } from '../../types.js';
7
+ import type { ReactElement, MouseEvent } from 'react';
8
8
  type ExtensionAIControlProps = {
9
+ className?: string;
9
10
  disabled?: boolean;
10
11
  value: string;
11
12
  placeholder?: string;
@@ -13,7 +14,7 @@ type ExtensionAIControlProps = {
13
14
  isTransparent?: boolean;
14
15
  state?: RequestingStateProp;
15
16
  showGuideLine?: boolean;
16
- error?: string;
17
+ error?: RequestingErrorProps;
17
18
  requestsRemaining?: number;
18
19
  showUpgradeMessage?: boolean;
19
20
  wrapperRef?: React.MutableRefObject<HTMLDivElement | null>;
@@ -22,7 +23,8 @@ type ExtensionAIControlProps = {
22
23
  onStop?: () => void;
23
24
  onClose?: () => void;
24
25
  onUndo?: () => void;
25
- onUpgrade?: () => void;
26
+ onUpgrade?: (event: MouseEvent<HTMLButtonElement>) => void;
27
+ onTryAgain?: () => void;
26
28
  };
27
29
  /**
28
30
  * ExtensionAIControl component. Used by the AI Assistant inline extensions, adding logic and components to the base AIControl component.
@@ -31,6 +33,6 @@ type ExtensionAIControlProps = {
31
33
  * @param {React.MutableRefObject} ref - Ref to the component
32
34
  * @returns {ReactElement} Rendered component
33
35
  */
34
- export declare function ExtensionAIControl({ disabled, value, placeholder, showButtonLabels, isTransparent, state, showGuideLine, error, requestsRemaining, showUpgradeMessage, wrapperRef, onChange, onSend, onStop, onClose, onUndo, onUpgrade, }: ExtensionAIControlProps, ref: React.MutableRefObject<HTMLInputElement>): ReactElement;
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;
35
37
  declare const _default: React.ForwardRefExoticComponent<ExtensionAIControlProps & React.RefAttributes<HTMLInputElement>>;
36
38
  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({ disabled = false, value = '', placeholder = '', showButtonLabels = true, isTransparent = false, state = 'init', showGuideLine = false, error, requestsRemaining, showUpgradeMessage = false, wrapperRef, onChange, onSend, onStop, onClose, onUndo, onUpgrade, }, ref) {
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) {
25
25
  const loading = state === 'requesting' || state === 'suggesting';
26
26
  const [editRequest, setEditRequest] = useState(false);
27
27
  const [lastValue, setLastValue] = useState(value || null);
@@ -61,9 +61,12 @@ export function ExtensionAIControl({ disabled = false, value = '', placeholder =
61
61
  const undoHandler = useCallback(() => {
62
62
  onUndo?.();
63
63
  }, [onUndo]);
64
- const upgradeHandler = useCallback(() => {
65
- onUpgrade?.();
64
+ const upgradeHandler = useCallback((event) => {
65
+ onUpgrade?.(event);
66
66
  }, [onUpgrade]);
67
+ const tryAgainHandler = useCallback(() => {
68
+ onTryAgain?.();
69
+ }, [onTryAgain]);
67
70
  useKeyboardShortcut('enter', e => {
68
71
  e.preventDefault();
69
72
  sendHandler();
@@ -72,8 +75,8 @@ export function ExtensionAIControl({ disabled = false, value = '', placeholder =
72
75
  });
73
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') })] }) }))] })) }));
74
77
  let message = null;
75
- if (error) {
76
- message = _jsx(ErrorMessage, { error: error, onTryAgainClick: sendHandler });
78
+ if (error?.message) {
79
+ message = (_jsx(ErrorMessage, { error: error.message, code: error.code, onTryAgainClick: tryAgainHandler, onUpgradeClick: upgradeHandler }));
77
80
  }
78
81
  else if (showUpgradeMessage) {
79
82
  message = (_jsx(UpgradeMessage, { requestsRemaining: requestsRemaining, onUpgradeClick: upgradeHandler }));
@@ -81,6 +84,6 @@ export function ExtensionAIControl({ disabled = false, value = '', placeholder =
81
84
  else if (showGuideLine) {
82
85
  message = _jsx(GuidelineMessage, {});
83
86
  }
84
- return (_jsx(AIControl, { disabled: disabled || loading, value: value, placeholder: placeholder, isTransparent: isTransparent, state: state, onChange: changeHandler, actions: actions, message: message, promptUserInputRef: promptUserInputRef, wrapperRef: wrapperRef }));
87
+ return (_jsx(AIControl, { className: className, disabled: disabled || loading, value: value, placeholder: placeholder, isTransparent: isTransparent, state: state, onChange: changeHandler, actions: actions, message: message, promptUserInputRef: promptUserInputRef, wrapperRef: wrapperRef }));
85
88
  }
86
89
  export default forwardRef(ExtensionAIControl);
@@ -5,6 +5,7 @@ import './style.scss';
5
5
  /**
6
6
  * Types
7
7
  */
8
+ import type { SuggestionErrorCode } from '../../types.js';
8
9
  import type React from 'react';
9
10
  export declare const MESSAGE_SEVERITY_WARNING = "warning";
10
11
  export declare const MESSAGE_SEVERITY_ERROR = "error";
@@ -19,13 +20,17 @@ export type MessageProps = {
19
20
  onSidebarIconClick?: () => void;
20
21
  children: React.ReactNode;
21
22
  };
23
+ export type OnUpgradeClick = (event?: React.MouseEvent<HTMLButtonElement>) => void;
22
24
  export type UpgradeMessageProps = {
23
25
  requestsRemaining: number;
24
- onUpgradeClick: () => void;
26
+ severity?: MessageSeverityProp;
27
+ onUpgradeClick: OnUpgradeClick;
25
28
  };
26
29
  export type ErrorMessageProps = {
27
30
  error?: string;
31
+ code?: SuggestionErrorCode;
28
32
  onTryAgainClick: () => void;
33
+ onUpgradeClick: OnUpgradeClick;
29
34
  };
30
35
  /**
31
36
  * React component to render a block message.
@@ -46,12 +51,12 @@ export declare function GuidelineMessage(): React.ReactElement;
46
51
  * @param {number} requestsRemaining - Number of requests remaining.
47
52
  * @returns {React.ReactElement } - Message component.
48
53
  */
49
- export declare function UpgradeMessage({ requestsRemaining, onUpgradeClick, }: UpgradeMessageProps): React.ReactElement;
54
+ export declare function UpgradeMessage({ requestsRemaining, severity, onUpgradeClick, }: UpgradeMessageProps): React.ReactElement;
50
55
  /**
51
56
  * React component to render an error message
52
57
  *
53
58
  * @param {number} requestsRemaining - Number of requests remaining.
54
59
  * @returns {React.ReactElement } - Message component.
55
60
  */
56
- export declare function ErrorMessage({ error, onTryAgainClick }: ErrorMessageProps): React.ReactElement;
61
+ export declare function ErrorMessage({ error, code, onTryAgainClick, onUpgradeClick, }: ErrorMessageProps): React.ReactElement;
57
62
  export {};
@@ -11,6 +11,7 @@ import classNames from 'classnames';
11
11
  */
12
12
  import './style.scss';
13
13
  import errorExclamation from '../../icons/error-exclamation.js';
14
+ import { ERROR_QUOTA_EXCEEDED } from '../../types.js';
14
15
  export const MESSAGE_SEVERITY_WARNING = 'warning';
15
16
  export const MESSAGE_SEVERITY_ERROR = 'error';
16
17
  export const MESSAGE_SEVERITY_SUCCESS = 'success';
@@ -50,8 +51,12 @@ export function GuidelineMessage() {
50
51
  * @param {number} requestsRemaining - Number of requests remaining.
51
52
  * @returns {React.ReactElement } - Message component.
52
53
  */
53
- export function UpgradeMessage({ requestsRemaining, onUpgradeClick, }) {
54
- return (_jsxs(Message, { severity: MESSAGE_SEVERITY_WARNING, children: [_jsx("span", { children: sprintf(
54
+ export function UpgradeMessage({ requestsRemaining, severity, onUpgradeClick, }) {
55
+ let messageSeverity = severity;
56
+ if (messageSeverity == null) {
57
+ messageSeverity = requestsRemaining > 0 ? MESSAGE_SEVERITY_INFO : MESSAGE_SEVERITY_WARNING;
58
+ }
59
+ return (_jsxs(Message, { severity: messageSeverity, children: [_jsx("span", { children: sprintf(
55
60
  // translators: %1$d: number of requests remaining
56
61
  __('You have %1$d free requests remaining.', 'jetpack-ai-client'), requestsRemaining) }), _jsx(Button, { variant: "link", onClick: onUpgradeClick, children: __('Upgrade now', 'jetpack-ai-client') })] }));
57
62
  }
@@ -61,9 +66,9 @@ export function UpgradeMessage({ requestsRemaining, onUpgradeClick, }) {
61
66
  * @param {number} requestsRemaining - Number of requests remaining.
62
67
  * @returns {React.ReactElement } - Message component.
63
68
  */
64
- export function ErrorMessage({ error, onTryAgainClick }) {
69
+ export function ErrorMessage({ error, code, onTryAgainClick, onUpgradeClick, }) {
65
70
  const errorMessage = error || __('Something went wrong', 'jetpack-ai-client');
66
71
  return (_jsxs(Message, { severity: MESSAGE_SEVERITY_ERROR, children: [_jsx("span", { children: sprintf(
67
72
  // translators: %1$d: A dynamic error message
68
- __('Error: %1$s.', 'jetpack-ai-client'), errorMessage) }), _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, children: __('Upgrade now', 'jetpack-ai-client') })) : (_jsx(Button, { variant: "link", onClick: onTryAgainClick, children: __('Try again', 'jetpack-ai-client') }))] }));
69
74
  }
@@ -9,5 +9,14 @@ declare const useImageGenerator: () => {
9
9
  [key: string]: string;
10
10
  }[];
11
11
  }>;
12
+ generateImageWithStableDiffusion: ({ feature, postContent, userPrompt, }: {
13
+ feature: string;
14
+ postContent: string;
15
+ userPrompt?: string;
16
+ }) => Promise<{
17
+ data: {
18
+ [key: string]: string;
19
+ }[];
20
+ }>;
12
21
  };
13
22
  export default useImageGenerator;
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
+ import { __ } from '@wordpress/i18n';
4
5
  import debugFactory from 'debug';
5
6
  /**
6
7
  * Internal dependencies
7
8
  */
9
+ import askQuestionSync from '../../ask-question/sync.js';
8
10
  import requestJwt from '../../jwt/index.js';
9
11
  const debug = debugFactory('ai-client:use-image-generator');
10
12
  /**
@@ -27,7 +29,7 @@ const truncateContent = (content, currentPromptLength) => {
27
29
  * @param {string} userPrompt - the user prompt for the image generation, if provided. Max length is 1000 characters, will be truncated.
28
30
  * @returns {string} the prompt string
29
31
  */
30
- const getImageGenerationPrompt = (postContent, userPrompt) => {
32
+ const getDalleImageGenerationPrompt = (postContent, userPrompt) => {
31
33
  /**
32
34
  * If the user provide some custom prompt for the image generation,
33
35
  * we will use it, add the post content as additional context and
@@ -73,7 +75,124 @@ This is the post content:
73
75
  // truncating the content so the whole prompt is not longer than 4000 characters, the model limit.
74
76
  return imageGenerationPrompt + truncateContent(postContent, imageGenerationPrompt.length);
75
77
  };
78
+ /**
79
+ * Create the Stable Diffusion pre-processing prompt based on the provided context.
80
+ * @param {string} postContent - the content of the post.
81
+ * @param {string} userPrompt - the user prompt for the image generation, if provided. Max length is 1000 characters, will be truncated.
82
+ * @returns {string} the prompt string to be fed to the AI Assistant model.
83
+ */
84
+ const getStableDiffusionPreProcessingPrompt = (postContent, userPrompt) => {
85
+ /**
86
+ * If the user provide some custom prompt for the image generation,
87
+ * we will use it and add the post content as additional context.
88
+ */
89
+ if (userPrompt) {
90
+ const preProcessingPrompt = `I need a Stable Diffusion prompt to generate a featured image for a blog post based on this user-provided image description:
91
+
92
+ ${userPrompt.length > 1000 ? userPrompt.substring(0, 1000) : userPrompt}
93
+
94
+ The image should be a photo. Make sure you highlight the main suject of the image description, and include brief details about the light and style of the image.
95
+ Include a request to use high resolution and produce a highly detailed image, with sharp focus.
96
+ Return just the prompt, without comments.
97
+
98
+ For additional context, this is the post content:
99
+
100
+ `;
101
+ // truncating the content so the whole prompt is not longer than 4000 characters, the model limit.
102
+ return preProcessingPrompt + truncateContent(postContent, preProcessingPrompt.length);
103
+ }
104
+ /**
105
+ * When the user does not provide a custom prompt, we will use the
106
+ * standard one, based solely on the post content.
107
+ */
108
+ const preProcessingPrompt = `I need a Stable Diffusion prompt to generate a featured image for a blog post with the following content.
109
+ The image should be a photo. Make sure you highlight the main suject of the content, and include brief details about the light and style of the image.
110
+ Include a request to use high resolution and produce a highly detailed image, with sharp focus.
111
+ Return just the prompt, without comments. The content is:
112
+
113
+ `;
114
+ // truncating the content so the whole prompt is not longer than 4000 characters, the model limit.
115
+ return preProcessingPrompt + truncateContent(postContent, preProcessingPrompt.length);
116
+ };
117
+ /**
118
+ * Uses the Jetpack AI query endpoint to produce a prompt for the stable diffusion model.
119
+ * @param {string} postContent - the content of the post.
120
+ * @param {string} userPrompt - the user prompt for the image generation, if provided. Max length is 1000 characters, will be truncated
121
+ * @param {string} feature - the feature to be used for the image generation.
122
+ * @returns {string} the prompt string to be used on stable diffusion image generation.
123
+ */
124
+ const getStableDiffusionImageGenerationPrompt = async (postContent, userPrompt, feature) => {
125
+ const prompt = getStableDiffusionPreProcessingPrompt(postContent, userPrompt);
126
+ /**
127
+ * Request the prompt on the AI Assistant endpoint
128
+ */
129
+ const data = await askQuestionSync(prompt, { feature });
130
+ return data.choices?.[0]?.message?.content;
131
+ };
76
132
  const useImageGenerator = () => {
133
+ const generateImageWithStableDiffusion = async function ({ feature, postContent, userPrompt, }) {
134
+ let token = null;
135
+ try {
136
+ token = await requestJwt();
137
+ }
138
+ catch (error) {
139
+ debug('Error getting token: %o', error);
140
+ return Promise.reject(error);
141
+ }
142
+ try {
143
+ debug('Generating image with Stable Diffusion');
144
+ const prompt = await getStableDiffusionImageGenerationPrompt(postContent, userPrompt, feature);
145
+ const data = {
146
+ prompt,
147
+ style: 'photographic',
148
+ token: token.token,
149
+ width: 1024,
150
+ height: 768,
151
+ };
152
+ const response = await fetch(`https://public-api.wordpress.com/wpcom/v2/sites/${token.blogId}/ai-image`, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ },
157
+ body: JSON.stringify(data),
158
+ });
159
+ if (!response?.ok) {
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
+ });
167
+ }
168
+ const blob = await response.blob();
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
+ };
190
+ }
191
+ catch (error) {
192
+ debug('Error generating image: %o', error);
193
+ return Promise.reject(error);
194
+ }
195
+ };
77
196
  const generateImage = async function ({ feature, postContent, responseFormat = 'url', userPrompt, }) {
78
197
  let token = '';
79
198
  try {
@@ -85,7 +204,7 @@ const useImageGenerator = () => {
85
204
  }
86
205
  try {
87
206
  debug('Generating image');
88
- const imageGenerationPrompt = getImageGenerationPrompt(postContent, userPrompt);
207
+ const imageGenerationPrompt = getDalleImageGenerationPrompt(postContent, userPrompt);
89
208
  const URL = 'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-image';
90
209
  const body = {
91
210
  prompt: imageGenerationPrompt,
@@ -115,6 +234,7 @@ const useImageGenerator = () => {
115
234
  };
116
235
  return {
117
236
  generateImage,
237
+ generateImageWithStableDiffusion,
118
238
  };
119
239
  };
120
240
  export default useImageGenerator;
@@ -1 +1,2 @@
1
1
  export { MarkdownToHTML, HTMLToMarkdown, renderHTMLFromMarkdown, renderMarkdownFromHTML, } from './markdown/index.js';
2
+ export type { RenderHTMLRules } from './markdown/index.js';
@@ -7,9 +7,10 @@ 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 = 'all' | Array<HTMLFix>;
10
11
  declare const renderHTMLFromMarkdown: ({ content, rules, }: {
11
12
  content: string;
12
- rules?: Array<HTMLFix> | 'all';
13
+ rules?: RenderHTMLRules;
13
14
  }) => string;
14
15
  declare const renderMarkdownFromHTML: ({ content }: {
15
16
  content: string;
package/build/types.d.ts CHANGED
@@ -39,3 +39,4 @@ export type Block = {
39
39
  originalContent?: string;
40
40
  };
41
41
  export type TranscriptionState = RecordingState | 'validating' | 'processing' | 'error';
42
+ export type { RenderHTMLRules } from './libs/index.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@automattic/jetpack-ai-client",
4
- "version": "0.13.0",
4
+ "version": "0.14.0",
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": {
@@ -42,24 +42,24 @@
42
42
  "main": "./build/index.js",
43
43
  "types": "./build/index.d.ts",
44
44
  "dependencies": {
45
- "@automattic/jetpack-base-styles": "^0.6.24",
46
- "@automattic/jetpack-connection": "^0.33.9",
47
- "@automattic/jetpack-shared-extension-utils": "^0.14.11",
45
+ "@automattic/jetpack-base-styles": "^0.6.25",
46
+ "@automattic/jetpack-connection": "^0.33.11",
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",
50
- "@wordpress/api-fetch": "6.53.0",
51
- "@wordpress/block-editor": "12.24.0",
52
- "@wordpress/components": "27.4.0",
53
- "@wordpress/compose": "6.33.0",
54
- "@wordpress/data": "9.26.0",
55
- "@wordpress/element": "5.33.0",
56
- "@wordpress/i18n": "4.56.0",
57
- "@wordpress/icons": "9.47.0",
50
+ "@wordpress/api-fetch": "6.54.0",
51
+ "@wordpress/block-editor": "12.25.0",
52
+ "@wordpress/components": "27.5.0",
53
+ "@wordpress/compose": "6.34.0",
54
+ "@wordpress/data": "9.27.0",
55
+ "@wordpress/element": "5.34.0",
56
+ "@wordpress/i18n": "4.57.0",
57
+ "@wordpress/icons": "9.48.0",
58
58
  "classnames": "2.3.2",
59
59
  "debug": "4.3.4",
60
60
  "markdown-it": "14.0.0",
61
- "react": "18.2.0",
62
- "react-dom": "18.2.0",
61
+ "react": "18.3.1",
62
+ "react-dom": "18.3.1",
63
63
  "turndown": "7.1.2"
64
64
  }
65
65
  }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import debugFactory from 'debug';
5
+ /*
6
+ * Types & constants
7
+ */
8
+ import requestJwt from '../jwt/index.js';
9
+ import { AskQuestionOptionsArgProps } from './index.js';
10
+ import type { PromptProp } from '../types.js';
11
+
12
+ /**
13
+ * The response data from the AI assistant when doing a sync, not-streamed question.
14
+ */
15
+ export type ResponseData = {
16
+ choices: Array< {
17
+ message: {
18
+ content: string;
19
+ };
20
+ } >;
21
+ };
22
+
23
+ const debug = debugFactory( 'jetpack-ai-client:ask-question-sync' );
24
+
25
+ /**
26
+ * A function that asks a question without streaming.
27
+ *
28
+ * @param {PromptProp} question - The question to ask. It can be a simple string or an array of PromptMessageItemProps objects.
29
+ * @param {AskQuestionOptionsArgProps} options - An optional object for additional configuration: postId, feature, model.
30
+ * @returns {Promise<ResponseData>} - A promise that resolves to an instance of the ResponseData
31
+ * @example
32
+ * const question = "What is the meaning of life?";
33
+ * const options = {
34
+ * feature: 'ai-featured-image',
35
+ * model: 'gpt-4-turbo'
36
+ * }
37
+ * askQuestionSync( question, options ).then( responseData => {
38
+ * // access the choices array on the response data
39
+ * const content = responseData.choices[ 0 ].message.content;
40
+ * } );
41
+ */
42
+ export default async function askQuestionSync(
43
+ question: PromptProp,
44
+ { postId = null, feature, model }: AskQuestionOptionsArgProps = {}
45
+ ): Promise< ResponseData > {
46
+ debug( 'Asking question with no streaming: %o. options: %o', question, {
47
+ postId,
48
+ feature,
49
+ model,
50
+ } );
51
+
52
+ /**
53
+ * The URL to the AI assistant query endpoint.
54
+ */
55
+ const URL = 'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-query';
56
+
57
+ let token = null;
58
+
59
+ try {
60
+ token = ( await requestJwt() ).token;
61
+ } catch ( error ) {
62
+ debug( 'Error getting token: %o', error );
63
+ return Promise.reject( error );
64
+ }
65
+
66
+ const body = {
67
+ question: question,
68
+ stream: false,
69
+ postId,
70
+ feature,
71
+ model,
72
+ };
73
+
74
+ const headers = {
75
+ Authorization: `Bearer ${ token }`,
76
+ 'Content-Type': 'application/json',
77
+ };
78
+
79
+ const data = await fetch( URL, {
80
+ method: 'POST',
81
+ headers,
82
+ body: JSON.stringify( body ),
83
+ } ).then( response => response.json() );
84
+
85
+ if ( data?.data?.status && data?.data?.status > 200 ) {
86
+ debug( 'Error generating prompt: %o', data );
87
+ return Promise.reject( data );
88
+ }
89
+
90
+ return data as ResponseData;
91
+ }
@@ -16,6 +16,7 @@ import type { RequestingStateProp } from '../../types.js';
16
16
  import type { ReactElement } from 'react';
17
17
 
18
18
  type AIControlProps = {
19
+ className?: string;
19
20
  disabled?: boolean;
20
21
  value: string;
21
22
  placeholder?: string;
@@ -37,6 +38,7 @@ type AIControlProps = {
37
38
  * @returns {ReactElement} Rendered component
38
39
  */
39
40
  export default function AIControl( {
41
+ className,
40
42
  disabled = false,
41
43
  value = '',
42
44
  placeholder = '',
@@ -51,7 +53,10 @@ export default function AIControl( {
51
53
  wrapperRef = null,
52
54
  }: AIControlProps ): ReactElement {
53
55
  return (
54
- <div className="jetpack-components-ai-control__container-wrapper" ref={ wrapperRef }>
56
+ <div
57
+ className={ classNames( 'jetpack-components-ai-control__container-wrapper', className ) }
58
+ ref={ wrapperRef }
59
+ >
55
60
  { error }
56
61
  <div className="jetpack-components-ai-control__container">
57
62
  { banner }
@@ -16,10 +16,11 @@ import './style.scss';
16
16
  /**
17
17
  * Types
18
18
  */
19
- import type { RequestingStateProp } from '../../types.js';
20
- import type { ReactElement } from 'react';
19
+ import type { RequestingErrorProps, RequestingStateProp } from '../../types.js';
20
+ import type { ReactElement, MouseEvent } from 'react';
21
21
 
22
22
  type ExtensionAIControlProps = {
23
+ className?: string;
23
24
  disabled?: boolean;
24
25
  value: string;
25
26
  placeholder?: string;
@@ -27,7 +28,7 @@ type ExtensionAIControlProps = {
27
28
  isTransparent?: boolean;
28
29
  state?: RequestingStateProp;
29
30
  showGuideLine?: boolean;
30
- error?: string;
31
+ error?: RequestingErrorProps;
31
32
  requestsRemaining?: number;
32
33
  showUpgradeMessage?: boolean;
33
34
  wrapperRef?: React.MutableRefObject< HTMLDivElement | null >;
@@ -36,7 +37,8 @@ type ExtensionAIControlProps = {
36
37
  onStop?: () => void;
37
38
  onClose?: () => void;
38
39
  onUndo?: () => void;
39
- onUpgrade?: () => void;
40
+ onUpgrade?: ( event: MouseEvent< HTMLButtonElement > ) => void;
41
+ onTryAgain?: () => void;
40
42
  };
41
43
 
42
44
  /**
@@ -48,6 +50,7 @@ type ExtensionAIControlProps = {
48
50
  */
49
51
  export function ExtensionAIControl(
50
52
  {
53
+ className,
51
54
  disabled = false,
52
55
  value = '',
53
56
  placeholder = '',
@@ -65,6 +68,7 @@ export function ExtensionAIControl(
65
68
  onClose,
66
69
  onUndo,
67
70
  onUpgrade,
71
+ onTryAgain,
68
72
  }: ExtensionAIControlProps,
69
73
  ref: React.MutableRefObject< HTMLInputElement >
70
74
  ): ReactElement {
@@ -118,9 +122,16 @@ export function ExtensionAIControl(
118
122
  onUndo?.();
119
123
  }, [ onUndo ] );
120
124
 
121
- const upgradeHandler = useCallback( () => {
122
- onUpgrade?.();
123
- }, [ onUpgrade ] );
125
+ const upgradeHandler = useCallback(
126
+ ( event: MouseEvent< HTMLButtonElement > ) => {
127
+ onUpgrade?.( event );
128
+ },
129
+ [ onUpgrade ]
130
+ );
131
+
132
+ const tryAgainHandler = useCallback( () => {
133
+ onTryAgain?.();
134
+ }, [ onTryAgain ] );
124
135
 
125
136
  useKeyboardShortcut(
126
137
  'enter',
@@ -191,8 +202,16 @@ export function ExtensionAIControl(
191
202
  );
192
203
 
193
204
  let message = null;
194
- if ( error ) {
195
- message = <ErrorMessage error={ error } onTryAgainClick={ sendHandler } />;
205
+
206
+ if ( error?.message ) {
207
+ message = (
208
+ <ErrorMessage
209
+ error={ error.message }
210
+ code={ error.code }
211
+ onTryAgainClick={ tryAgainHandler }
212
+ onUpgradeClick={ upgradeHandler }
213
+ />
214
+ );
196
215
  } else if ( showUpgradeMessage ) {
197
216
  message = (
198
217
  <UpgradeMessage requestsRemaining={ requestsRemaining } onUpgradeClick={ upgradeHandler } />
@@ -203,6 +222,7 @@ export function ExtensionAIControl(
203
222
 
204
223
  return (
205
224
  <AIControl
225
+ className={ className }
206
226
  disabled={ disabled || loading }
207
227
  value={ value }
208
228
  placeholder={ placeholder }
@@ -10,9 +10,11 @@ import classNames from 'classnames';
10
10
  */
11
11
  import './style.scss';
12
12
  import errorExclamation from '../../icons/error-exclamation.js';
13
+ import { ERROR_QUOTA_EXCEEDED } from '../../types.js';
13
14
  /**
14
15
  * Types
15
16
  */
17
+ import type { SuggestionErrorCode } from '../../types.js';
16
18
  import type React from 'react';
17
19
 
18
20
  export const MESSAGE_SEVERITY_WARNING = 'warning';
@@ -37,14 +39,19 @@ export type MessageProps = {
37
39
  children: React.ReactNode;
38
40
  };
39
41
 
42
+ export type OnUpgradeClick = ( event?: React.MouseEvent< HTMLButtonElement > ) => void;
43
+
40
44
  export type UpgradeMessageProps = {
41
45
  requestsRemaining: number;
42
- onUpgradeClick: () => void;
46
+ severity?: MessageSeverityProp;
47
+ onUpgradeClick: OnUpgradeClick;
43
48
  };
44
49
 
45
50
  export type ErrorMessageProps = {
46
51
  error?: string;
52
+ code?: SuggestionErrorCode;
47
53
  onTryAgainClick: () => void;
54
+ onUpgradeClick: OnUpgradeClick;
48
55
  };
49
56
 
50
57
  const messageIconsMap = {
@@ -113,10 +120,17 @@ export function GuidelineMessage(): React.ReactElement {
113
120
  */
114
121
  export function UpgradeMessage( {
115
122
  requestsRemaining,
123
+ severity,
116
124
  onUpgradeClick,
117
125
  }: UpgradeMessageProps ): React.ReactElement {
126
+ let messageSeverity = severity;
127
+
128
+ if ( messageSeverity == null ) {
129
+ messageSeverity = requestsRemaining > 0 ? MESSAGE_SEVERITY_INFO : MESSAGE_SEVERITY_WARNING;
130
+ }
131
+
118
132
  return (
119
- <Message severity={ MESSAGE_SEVERITY_WARNING }>
133
+ <Message severity={ messageSeverity }>
120
134
  <span>
121
135
  { sprintf(
122
136
  // translators: %1$d: number of requests remaining
@@ -137,7 +151,12 @@ export function UpgradeMessage( {
137
151
  * @param {number} requestsRemaining - Number of requests remaining.
138
152
  * @returns {React.ReactElement } - Message component.
139
153
  */
140
- export function ErrorMessage( { error, onTryAgainClick }: ErrorMessageProps ): React.ReactElement {
154
+ export function ErrorMessage( {
155
+ error,
156
+ code,
157
+ onTryAgainClick,
158
+ onUpgradeClick,
159
+ }: ErrorMessageProps ): React.ReactElement {
141
160
  const errorMessage = error || __( 'Something went wrong', 'jetpack-ai-client' );
142
161
 
143
162
  return (
@@ -145,13 +164,19 @@ export function ErrorMessage( { error, onTryAgainClick }: ErrorMessageProps ): R
145
164
  <span>
146
165
  { sprintf(
147
166
  // translators: %1$d: A dynamic error message
148
- __( 'Error: %1$s.', 'jetpack-ai-client' ),
167
+ __( 'Error: %1$s', 'jetpack-ai-client' ),
149
168
  errorMessage
150
169
  ) }
151
170
  </span>
152
- <Button variant="link" onClick={ onTryAgainClick }>
153
- { __( 'Try Again', 'jetpack-ai-client' ) }
154
- </Button>
171
+ { code === ERROR_QUOTA_EXCEEDED ? (
172
+ <Button variant="link" onClick={ onUpgradeClick }>
173
+ { __( 'Upgrade now', 'jetpack-ai-client' ) }
174
+ </Button>
175
+ ) : (
176
+ <Button variant="link" onClick={ onTryAgainClick }>
177
+ { __( 'Try again', 'jetpack-ai-client' ) }
178
+ </Button>
179
+ ) }
155
180
  </Message>
156
181
  );
157
182
  }
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
+ import { __ } from '@wordpress/i18n';
4
5
  import debugFactory from 'debug';
5
6
  /**
6
7
  * Internal dependencies
7
8
  */
9
+ import askQuestionSync from '../../ask-question/sync.js';
8
10
  import requestJwt from '../../jwt/index.js';
9
11
 
10
12
  const debug = debugFactory( 'ai-client:use-image-generator' );
@@ -30,7 +32,7 @@ const truncateContent = ( content: string, currentPromptLength: number ): string
30
32
  * @param {string} userPrompt - the user prompt for the image generation, if provided. Max length is 1000 characters, will be truncated.
31
33
  * @returns {string} the prompt string
32
34
  */
33
- const getImageGenerationPrompt = ( postContent: string, userPrompt?: string ): string => {
35
+ const getDalleImageGenerationPrompt = ( postContent: string, userPrompt?: string ): string => {
34
36
  /**
35
37
  * If the user provide some custom prompt for the image generation,
36
38
  * we will use it, add the post content as additional context and
@@ -78,7 +80,160 @@ This is the post content:
78
80
  return imageGenerationPrompt + truncateContent( postContent, imageGenerationPrompt.length );
79
81
  };
80
82
 
83
+ /**
84
+ * Create the Stable Diffusion pre-processing prompt based on the provided context.
85
+ * @param {string} postContent - the content of the post.
86
+ * @param {string} userPrompt - the user prompt for the image generation, if provided. Max length is 1000 characters, will be truncated.
87
+ * @returns {string} the prompt string to be fed to the AI Assistant model.
88
+ */
89
+ const getStableDiffusionPreProcessingPrompt = (
90
+ postContent: string,
91
+ userPrompt?: string
92
+ ): string => {
93
+ /**
94
+ * If the user provide some custom prompt for the image generation,
95
+ * we will use it and add the post content as additional context.
96
+ */
97
+ if ( userPrompt ) {
98
+ const preProcessingPrompt = `I need a Stable Diffusion prompt to generate a featured image for a blog post based on this user-provided image description:
99
+
100
+ ${ userPrompt.length > 1000 ? userPrompt.substring( 0, 1000 ) : userPrompt }
101
+
102
+ The image should be a photo. Make sure you highlight the main suject of the image description, and include brief details about the light and style of the image.
103
+ Include a request to use high resolution and produce a highly detailed image, with sharp focus.
104
+ Return just the prompt, without comments.
105
+
106
+ For additional context, this is the post content:
107
+
108
+ `;
109
+ // truncating the content so the whole prompt is not longer than 4000 characters, the model limit.
110
+ return preProcessingPrompt + truncateContent( postContent, preProcessingPrompt.length );
111
+ }
112
+
113
+ /**
114
+ * When the user does not provide a custom prompt, we will use the
115
+ * standard one, based solely on the post content.
116
+ */
117
+ const preProcessingPrompt = `I need a Stable Diffusion prompt to generate a featured image for a blog post with the following content.
118
+ The image should be a photo. Make sure you highlight the main suject of the content, and include brief details about the light and style of the image.
119
+ Include a request to use high resolution and produce a highly detailed image, with sharp focus.
120
+ Return just the prompt, without comments. The content is:
121
+
122
+ `;
123
+
124
+ // truncating the content so the whole prompt is not longer than 4000 characters, the model limit.
125
+ return preProcessingPrompt + truncateContent( postContent, preProcessingPrompt.length );
126
+ };
127
+
128
+ /**
129
+ * Uses the Jetpack AI query endpoint to produce a prompt for the stable diffusion model.
130
+ * @param {string} postContent - the content of the post.
131
+ * @param {string} userPrompt - the user prompt for the image generation, if provided. Max length is 1000 characters, will be truncated
132
+ * @param {string} feature - the feature to be used for the image generation.
133
+ * @returns {string} the prompt string to be used on stable diffusion image generation.
134
+ */
135
+ const getStableDiffusionImageGenerationPrompt = async (
136
+ postContent: string,
137
+ userPrompt?: string,
138
+ feature?: string
139
+ ): Promise< string > => {
140
+ const prompt = getStableDiffusionPreProcessingPrompt( postContent, userPrompt );
141
+
142
+ /**
143
+ * Request the prompt on the AI Assistant endpoint
144
+ */
145
+ const data = await askQuestionSync( prompt, { feature } );
146
+
147
+ return data.choices?.[ 0 ]?.message?.content;
148
+ };
149
+
81
150
  const useImageGenerator = () => {
151
+ const generateImageWithStableDiffusion = async function ( {
152
+ feature,
153
+ postContent,
154
+ userPrompt,
155
+ }: {
156
+ feature: string;
157
+ postContent: string;
158
+ userPrompt?: string;
159
+ } ): Promise< { data: Array< { [ key: string ]: string } > } > {
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
+
169
+ try {
170
+ debug( 'Generating image with Stable Diffusion' );
171
+
172
+ const prompt = await getStableDiffusionImageGenerationPrompt(
173
+ postContent,
174
+ userPrompt,
175
+ feature
176
+ );
177
+
178
+ const data = {
179
+ prompt,
180
+ style: 'photographic',
181
+ token: token.token,
182
+ width: 1024,
183
+ height: 768,
184
+ };
185
+
186
+ const response = await fetch(
187
+ `https://public-api.wordpress.com/wpcom/v2/sites/${ token.blogId }/ai-image`,
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
+ };
231
+ } catch ( error ) {
232
+ debug( 'Error generating image: %o', error );
233
+ return Promise.reject( error );
234
+ }
235
+ };
236
+
82
237
  const generateImage = async function ( {
83
238
  feature,
84
239
  postContent,
@@ -102,7 +257,7 @@ const useImageGenerator = () => {
102
257
  try {
103
258
  debug( 'Generating image' );
104
259
 
105
- const imageGenerationPrompt = getImageGenerationPrompt( postContent, userPrompt );
260
+ const imageGenerationPrompt = getDalleImageGenerationPrompt( postContent, userPrompt );
106
261
 
107
262
  const URL = 'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-image';
108
263
 
@@ -138,6 +293,7 @@ const useImageGenerator = () => {
138
293
 
139
294
  return {
140
295
  generateImage,
296
+ generateImageWithStableDiffusion,
141
297
  };
142
298
  };
143
299
 
package/src/libs/index.ts CHANGED
@@ -4,3 +4,5 @@ export {
4
4
  renderHTMLFromMarkdown,
5
5
  renderMarkdownFromHTML,
6
6
  } from './markdown/index.js';
7
+
8
+ export type { RenderHTMLRules } from './markdown/index.js';
@@ -11,12 +11,14 @@ 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 = 'all' | Array< HTMLFix >;
15
+
14
16
  const renderHTMLFromMarkdown = ( {
15
17
  content,
16
18
  rules = 'all',
17
19
  }: {
18
20
  content: string;
19
- rules?: Array< HTMLFix > | 'all';
21
+ rules?: RenderHTMLRules;
20
22
  } ) => {
21
23
  return defaultMarkdownConverter.render( { content, rules } );
22
24
  };
package/src/types.ts CHANGED
@@ -108,3 +108,8 @@ export type Block = {
108
108
  * Transcription types
109
109
  */
110
110
  export type TranscriptionState = RecordingState | 'validating' | 'processing' | 'error';
111
+
112
+ /*
113
+ * Lib types
114
+ */
115
+ export type { RenderHTMLRules } from './libs/index.js';