@automattic/jetpack-ai-client 0.12.4 → 0.13.1
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 +18 -0
- package/build/components/ai-control/ai-control.d.ts +3 -1
- package/build/components/ai-control/ai-control.js +2 -2
- package/build/components/ai-control/extension-ai-control.d.ts +6 -3
- package/build/components/ai-control/extension-ai-control.js +8 -5
- package/build/components/message/index.d.ts +1 -1
- package/build/components/message/index.js +1 -1
- package/build/hooks/use-image-generator/index.d.ts +2 -1
- package/build/hooks/use-image-generator/index.js +68 -15
- package/package.json +18 -18
- package/src/components/ai-control/ai-control.tsx +8 -1
- package/src/components/ai-control/extension-ai-control.tsx +21 -6
- package/src/components/message/index.tsx +2 -2
- package/src/hooks/use-image-generator/index.ts +72 -15
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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.13.1] - 2024-05-13
|
|
9
|
+
### Added
|
|
10
|
+
- AI Client: Add className to AI Control component. [#37322]
|
|
11
|
+
- AI Client: Add "try again" prop on Extension AI Control. [#37250]
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- AI Client: Add event to upgrade handler function of Extension AI Control. [#37224]
|
|
15
|
+
|
|
16
|
+
## [0.13.0] - 2024-05-06
|
|
17
|
+
### Added
|
|
18
|
+
- AI Client: Add wrapper ref to AI Control. [#37145]
|
|
19
|
+
- AI Featured Image: Support custom user prompt on the image generation. [#37086]
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Updated package dependencies. [#37147] [#37148] [#37160]
|
|
23
|
+
|
|
8
24
|
## [0.12.4] - 2024-04-29
|
|
9
25
|
### Added
|
|
10
26
|
- AI Client: Export ExtensionAIControl. [#37087]
|
|
@@ -298,6 +314,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
298
314
|
- Updated package dependencies. [#31659]
|
|
299
315
|
- Updated package dependencies. [#31785]
|
|
300
316
|
|
|
317
|
+
[0.13.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.13.0...v0.13.1
|
|
318
|
+
[0.13.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.12.4...v0.13.0
|
|
301
319
|
[0.12.4]: https://github.com/Automattic/jetpack-ai-client/compare/v0.12.3...v0.12.4
|
|
302
320
|
[0.12.3]: https://github.com/Automattic/jetpack-ai-client/compare/v0.12.2...v0.12.3
|
|
303
321
|
[0.12.2]: https://github.com/Automattic/jetpack-ai-client/compare/v0.12.1...v0.12.2
|
|
@@ -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;
|
|
@@ -17,6 +18,7 @@ type AIControlProps = {
|
|
|
17
18
|
actions?: ReactElement;
|
|
18
19
|
message?: ReactElement;
|
|
19
20
|
promptUserInputRef?: React.MutableRefObject<HTMLInputElement>;
|
|
21
|
+
wrapperRef?: React.MutableRefObject<HTMLDivElement | null>;
|
|
20
22
|
};
|
|
21
23
|
/**
|
|
22
24
|
* Base AIControl component. Contains the main structure of the control component and slots for banner, error, actions and message.
|
|
@@ -24,5 +26,5 @@ type AIControlProps = {
|
|
|
24
26
|
* @param {AIControlProps} props - Component props
|
|
25
27
|
* @returns {ReactElement} Rendered component
|
|
26
28
|
*/
|
|
27
|
-
export default function AIControl({ disabled, value, placeholder, isTransparent, state, onChange, banner, error, actions, message, promptUserInputRef, }: AIControlProps): ReactElement;
|
|
29
|
+
export default function AIControl({ className, disabled, value, placeholder, isTransparent, state, onChange, banner, error, actions, message, promptUserInputRef, wrapperRef, }: AIControlProps): ReactElement;
|
|
28
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, }) {
|
|
19
|
-
return (_jsxs("div", { className:
|
|
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
|
}
|
|
@@ -4,8 +4,9 @@ import './style.scss';
|
|
|
4
4
|
* Types
|
|
5
5
|
*/
|
|
6
6
|
import type { RequestingStateProp } from '../../types.js';
|
|
7
|
-
import type { ReactElement } from 'react';
|
|
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;
|
|
@@ -16,12 +17,14 @@ type ExtensionAIControlProps = {
|
|
|
16
17
|
error?: string;
|
|
17
18
|
requestsRemaining?: number;
|
|
18
19
|
showUpgradeMessage?: boolean;
|
|
20
|
+
wrapperRef?: React.MutableRefObject<HTMLDivElement | null>;
|
|
19
21
|
onChange?: (newValue: string) => void;
|
|
20
22
|
onSend?: (currentValue: string) => void;
|
|
21
23
|
onStop?: () => void;
|
|
22
24
|
onClose?: () => void;
|
|
23
25
|
onUndo?: () => void;
|
|
24
|
-
onUpgrade?: () => void;
|
|
26
|
+
onUpgrade?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
27
|
+
onTryAgain?: () => void;
|
|
25
28
|
};
|
|
26
29
|
/**
|
|
27
30
|
* ExtensionAIControl component. Used by the AI Assistant inline extensions, adding logic and components to the base AIControl component.
|
|
@@ -30,6 +33,6 @@ type ExtensionAIControlProps = {
|
|
|
30
33
|
* @param {React.MutableRefObject} ref - Ref to the component
|
|
31
34
|
* @returns {ReactElement} Rendered component
|
|
32
35
|
*/
|
|
33
|
-
export declare function ExtensionAIControl({ disabled, value, placeholder, showButtonLabels, isTransparent, state, showGuideLine, error, requestsRemaining, showUpgradeMessage, 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;
|
|
34
37
|
declare const _default: React.ForwardRefExoticComponent<ExtensionAIControlProps & React.RefAttributes<HTMLInputElement>>;
|
|
35
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, 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();
|
|
@@ -73,7 +76,7 @@ export function ExtensionAIControl({ disabled = false, value = '', placeholder =
|
|
|
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
78
|
if (error) {
|
|
76
|
-
message = _jsx(ErrorMessage, { error: error, onTryAgainClick:
|
|
79
|
+
message = _jsx(ErrorMessage, { error: error, onTryAgainClick: tryAgainHandler });
|
|
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 }));
|
|
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);
|
|
@@ -21,7 +21,7 @@ export type MessageProps = {
|
|
|
21
21
|
};
|
|
22
22
|
export type UpgradeMessageProps = {
|
|
23
23
|
requestsRemaining: number;
|
|
24
|
-
onUpgradeClick: () => void;
|
|
24
|
+
onUpgradeClick: (event?: React.MouseEvent<HTMLButtonElement>) => void;
|
|
25
25
|
};
|
|
26
26
|
export type ErrorMessageProps = {
|
|
27
27
|
error?: string;
|
|
@@ -65,5 +65,5 @@ export function ErrorMessage({ error, onTryAgainClick }) {
|
|
|
65
65
|
const errorMessage = error || __('Something went wrong', 'jetpack-ai-client');
|
|
66
66
|
return (_jsxs(Message, { severity: MESSAGE_SEVERITY_ERROR, children: [_jsx("span", { children: sprintf(
|
|
67
67
|
// translators: %1$d: A dynamic error message
|
|
68
|
-
__('Error: %1$s
|
|
68
|
+
__('Error: %1$s', 'jetpack-ai-client'), errorMessage) }), _jsx(Button, { variant: "link", onClick: onTryAgainClick, children: __('Try Again', 'jetpack-ai-client') })] }));
|
|
69
69
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
declare const useImageGenerator: () => {
|
|
2
|
-
generateImage: ({ feature, postContent, responseFormat, }: {
|
|
2
|
+
generateImage: ({ feature, postContent, responseFormat, userPrompt, }: {
|
|
3
3
|
feature: string;
|
|
4
4
|
postContent: string;
|
|
5
5
|
responseFormat?: 'url' | 'b64_json';
|
|
6
|
+
userPrompt?: string;
|
|
6
7
|
}) => Promise<{
|
|
7
8
|
data: {
|
|
8
9
|
[key: string]: string;
|
|
@@ -7,20 +7,57 @@ import debugFactory from 'debug';
|
|
|
7
7
|
*/
|
|
8
8
|
import requestJwt from '../../jwt/index.js';
|
|
9
9
|
const debug = debugFactory('ai-client:use-image-generator');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Cut the post content on a given lenght so the total length of the prompt is not longer than 4000 characters.
|
|
12
|
+
* @param {string} content - the content to be truncated
|
|
13
|
+
* @param {number} currentPromptLength - the length of the prompt already in use
|
|
14
|
+
* @returns {string} a truncated version of the content respecting the prompt length limit
|
|
15
|
+
*/
|
|
16
|
+
const truncateContent = (content, currentPromptLength) => {
|
|
17
|
+
const maxLength = 4000;
|
|
18
|
+
const remainingLength = maxLength - currentPromptLength;
|
|
19
|
+
// 6 is the length of the ellipsis and the space before it
|
|
20
|
+
return content.length > remainingLength
|
|
21
|
+
? content.substring(0, remainingLength - 6) + ` [...]`
|
|
22
|
+
: content;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Create the prompt string based on the provided context.
|
|
26
|
+
* @param {string} postContent - the content of the post
|
|
27
|
+
* @param {string} userPrompt - the user prompt for the image generation, if provided. Max length is 1000 characters, will be truncated.
|
|
28
|
+
* @returns {string} the prompt string
|
|
29
|
+
*/
|
|
30
|
+
const getImageGenerationPrompt = (postContent, userPrompt) => {
|
|
31
|
+
/**
|
|
32
|
+
* If the user provide some custom prompt for the image generation,
|
|
33
|
+
* we will use it, add the post content as additional context and
|
|
34
|
+
* provide some guardrails for the generation.
|
|
35
|
+
*/
|
|
36
|
+
if (userPrompt) {
|
|
37
|
+
const imageGenerationPrompt = `I need a cover image for a blog post based on this user prompt:
|
|
38
|
+
|
|
39
|
+
${userPrompt.length > 1000 ? userPrompt.substring(0, 1000) : userPrompt}
|
|
40
|
+
|
|
41
|
+
Before creating the image, identify the main topic of the user prompt and relate it to the post content.
|
|
42
|
+
Do not represent the whole content in one image, keep it simple and just represent one single idea.
|
|
43
|
+
Do not add details, detailed explanations or highlights from the content, just represent the main idea as if it was a photograph.
|
|
44
|
+
Do not use collages or compositions with multiple elements or scenes. Stick to one single scene. Do not compose unrealistic scenes.
|
|
45
|
+
If the content describes facts, objects or concepts from the real world, represent them on a realistic style and do not make unreal compositions.
|
|
46
|
+
If the content is more abstract, use a more abstract style to represent the main idea.
|
|
47
|
+
Make sure the light and the style are visually appealing.
|
|
48
|
+
Do not add text to the image.
|
|
49
|
+
|
|
50
|
+
For additional context, this is the post content:
|
|
51
|
+
|
|
52
|
+
`;
|
|
53
|
+
// truncating the content so the whole prompt is not longer than 4000 characters, the model limit.
|
|
54
|
+
return imageGenerationPrompt + truncateContent(postContent, imageGenerationPrompt.length);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* When the user does not provide a custom prompt, we will use the
|
|
58
|
+
* standard one, based solely on the post content.
|
|
59
|
+
*/
|
|
60
|
+
const imageGenerationPrompt = `I need a cover image for a blog post.
|
|
24
61
|
Before creating the image, identify the main topic of the content and only represent it.
|
|
25
62
|
Do not represent the whole content in one image, keep it simple and just represent one single idea.
|
|
26
63
|
Do not add details, detailed explanations or highlights from the content, just represent the main idea as if it was a photograph.
|
|
@@ -32,7 +69,23 @@ Do not add text to the image.
|
|
|
32
69
|
|
|
33
70
|
This is the post content:
|
|
34
71
|
|
|
35
|
-
|
|
72
|
+
`;
|
|
73
|
+
// truncating the content so the whole prompt is not longer than 4000 characters, the model limit.
|
|
74
|
+
return imageGenerationPrompt + truncateContent(postContent, imageGenerationPrompt.length);
|
|
75
|
+
};
|
|
76
|
+
const useImageGenerator = () => {
|
|
77
|
+
const generateImage = async function ({ feature, postContent, responseFormat = 'url', userPrompt, }) {
|
|
78
|
+
let token = '';
|
|
79
|
+
try {
|
|
80
|
+
token = (await requestJwt()).token;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
debug('Error getting token: %o', error);
|
|
84
|
+
return Promise.reject(error);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
debug('Generating image');
|
|
88
|
+
const imageGenerationPrompt = getImageGenerationPrompt(postContent, userPrompt);
|
|
36
89
|
const URL = 'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-image';
|
|
37
90
|
const body = {
|
|
38
91
|
prompt: imageGenerationPrompt,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"private": false,
|
|
3
3
|
"name": "@automattic/jetpack-ai-client",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.1",
|
|
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": {
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
},
|
|
24
24
|
"type": "module",
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@storybook/addon-actions": "8.0.
|
|
27
|
-
"@storybook/blocks": "8.0.
|
|
28
|
-
"@storybook/preview-api": "8.0.
|
|
29
|
-
"@storybook/react": "8.0.
|
|
30
|
-
"@types/markdown-it": "14.0.
|
|
26
|
+
"@storybook/addon-actions": "8.0.9",
|
|
27
|
+
"@storybook/blocks": "8.0.9",
|
|
28
|
+
"@storybook/preview-api": "8.0.9",
|
|
29
|
+
"@storybook/react": "8.0.9",
|
|
30
|
+
"@types/markdown-it": "14.0.1",
|
|
31
31
|
"@types/turndown": "5.0.4",
|
|
32
32
|
"jest": "^29.6.2",
|
|
33
33
|
"jest-environment-jsdom": "29.7.0",
|
|
@@ -42,19 +42,19 @@
|
|
|
42
42
|
"main": "./build/index.js",
|
|
43
43
|
"types": "./build/index.d.ts",
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@automattic/jetpack-base-styles": "^0.6.
|
|
46
|
-
"@automattic/jetpack-connection": "^0.33.
|
|
47
|
-
"@automattic/jetpack-shared-extension-utils": "^0.14.
|
|
45
|
+
"@automattic/jetpack-base-styles": "^0.6.24",
|
|
46
|
+
"@automattic/jetpack-connection": "^0.33.10",
|
|
47
|
+
"@automattic/jetpack-shared-extension-utils": "^0.14.12",
|
|
48
48
|
"@microsoft/fetch-event-source": "2.0.1",
|
|
49
|
-
"@types/react": "18.
|
|
50
|
-
"@wordpress/api-fetch": "6.
|
|
51
|
-
"@wordpress/block-editor": "12.
|
|
52
|
-
"@wordpress/components": "27.
|
|
53
|
-
"@wordpress/compose": "6.
|
|
54
|
-
"@wordpress/data": "9.
|
|
55
|
-
"@wordpress/element": "5.
|
|
56
|
-
"@wordpress/i18n": "4.
|
|
57
|
-
"@wordpress/icons": "9.
|
|
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",
|
|
58
58
|
"classnames": "2.3.2",
|
|
59
59
|
"debug": "4.3.4",
|
|
60
60
|
"markdown-it": "14.0.0",
|
|
@@ -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;
|
|
@@ -27,6 +28,7 @@ type AIControlProps = {
|
|
|
27
28
|
actions?: ReactElement;
|
|
28
29
|
message?: ReactElement;
|
|
29
30
|
promptUserInputRef?: React.MutableRefObject< HTMLInputElement >;
|
|
31
|
+
wrapperRef?: React.MutableRefObject< HTMLDivElement | null >;
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -36,6 +38,7 @@ type AIControlProps = {
|
|
|
36
38
|
* @returns {ReactElement} Rendered component
|
|
37
39
|
*/
|
|
38
40
|
export default function AIControl( {
|
|
41
|
+
className,
|
|
39
42
|
disabled = false,
|
|
40
43
|
value = '',
|
|
41
44
|
placeholder = '',
|
|
@@ -47,9 +50,13 @@ export default function AIControl( {
|
|
|
47
50
|
actions = null,
|
|
48
51
|
message = null,
|
|
49
52
|
promptUserInputRef = null,
|
|
53
|
+
wrapperRef = null,
|
|
50
54
|
}: AIControlProps ): ReactElement {
|
|
51
55
|
return (
|
|
52
|
-
<div
|
|
56
|
+
<div
|
|
57
|
+
className={ classNames( 'jetpack-components-ai-control__container-wrapper', className ) }
|
|
58
|
+
ref={ wrapperRef }
|
|
59
|
+
>
|
|
53
60
|
{ error }
|
|
54
61
|
<div className="jetpack-components-ai-control__container">
|
|
55
62
|
{ banner }
|
|
@@ -17,9 +17,10 @@ import './style.scss';
|
|
|
17
17
|
* Types
|
|
18
18
|
*/
|
|
19
19
|
import type { RequestingStateProp } from '../../types.js';
|
|
20
|
-
import type { ReactElement } from 'react';
|
|
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;
|
|
@@ -30,12 +31,14 @@ type ExtensionAIControlProps = {
|
|
|
30
31
|
error?: string;
|
|
31
32
|
requestsRemaining?: number;
|
|
32
33
|
showUpgradeMessage?: boolean;
|
|
34
|
+
wrapperRef?: React.MutableRefObject< HTMLDivElement | null >;
|
|
33
35
|
onChange?: ( newValue: string ) => void;
|
|
34
36
|
onSend?: ( currentValue: string ) => void;
|
|
35
37
|
onStop?: () => void;
|
|
36
38
|
onClose?: () => void;
|
|
37
39
|
onUndo?: () => void;
|
|
38
|
-
onUpgrade?: () => void;
|
|
40
|
+
onUpgrade?: ( event: MouseEvent< HTMLButtonElement > ) => void;
|
|
41
|
+
onTryAgain?: () => void;
|
|
39
42
|
};
|
|
40
43
|
|
|
41
44
|
/**
|
|
@@ -47,6 +50,7 @@ type ExtensionAIControlProps = {
|
|
|
47
50
|
*/
|
|
48
51
|
export function ExtensionAIControl(
|
|
49
52
|
{
|
|
53
|
+
className,
|
|
50
54
|
disabled = false,
|
|
51
55
|
value = '',
|
|
52
56
|
placeholder = '',
|
|
@@ -57,12 +61,14 @@ export function ExtensionAIControl(
|
|
|
57
61
|
error,
|
|
58
62
|
requestsRemaining,
|
|
59
63
|
showUpgradeMessage = false,
|
|
64
|
+
wrapperRef,
|
|
60
65
|
onChange,
|
|
61
66
|
onSend,
|
|
62
67
|
onStop,
|
|
63
68
|
onClose,
|
|
64
69
|
onUndo,
|
|
65
70
|
onUpgrade,
|
|
71
|
+
onTryAgain,
|
|
66
72
|
}: ExtensionAIControlProps,
|
|
67
73
|
ref: React.MutableRefObject< HTMLInputElement >
|
|
68
74
|
): ReactElement {
|
|
@@ -116,9 +122,16 @@ export function ExtensionAIControl(
|
|
|
116
122
|
onUndo?.();
|
|
117
123
|
}, [ onUndo ] );
|
|
118
124
|
|
|
119
|
-
const upgradeHandler = useCallback(
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
const upgradeHandler = useCallback(
|
|
126
|
+
( event: MouseEvent< HTMLButtonElement > ) => {
|
|
127
|
+
onUpgrade?.( event );
|
|
128
|
+
},
|
|
129
|
+
[ onUpgrade ]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const tryAgainHandler = useCallback( () => {
|
|
133
|
+
onTryAgain?.();
|
|
134
|
+
}, [ onTryAgain ] );
|
|
122
135
|
|
|
123
136
|
useKeyboardShortcut(
|
|
124
137
|
'enter',
|
|
@@ -190,7 +203,7 @@ export function ExtensionAIControl(
|
|
|
190
203
|
|
|
191
204
|
let message = null;
|
|
192
205
|
if ( error ) {
|
|
193
|
-
message = <ErrorMessage error={ error } onTryAgainClick={
|
|
206
|
+
message = <ErrorMessage error={ error } onTryAgainClick={ tryAgainHandler } />;
|
|
194
207
|
} else if ( showUpgradeMessage ) {
|
|
195
208
|
message = (
|
|
196
209
|
<UpgradeMessage requestsRemaining={ requestsRemaining } onUpgradeClick={ upgradeHandler } />
|
|
@@ -201,6 +214,7 @@ export function ExtensionAIControl(
|
|
|
201
214
|
|
|
202
215
|
return (
|
|
203
216
|
<AIControl
|
|
217
|
+
className={ className }
|
|
204
218
|
disabled={ disabled || loading }
|
|
205
219
|
value={ value }
|
|
206
220
|
placeholder={ placeholder }
|
|
@@ -210,6 +224,7 @@ export function ExtensionAIControl(
|
|
|
210
224
|
actions={ actions }
|
|
211
225
|
message={ message }
|
|
212
226
|
promptUserInputRef={ promptUserInputRef }
|
|
227
|
+
wrapperRef={ wrapperRef }
|
|
213
228
|
/>
|
|
214
229
|
);
|
|
215
230
|
}
|
|
@@ -39,7 +39,7 @@ export type MessageProps = {
|
|
|
39
39
|
|
|
40
40
|
export type UpgradeMessageProps = {
|
|
41
41
|
requestsRemaining: number;
|
|
42
|
-
onUpgradeClick: () => void;
|
|
42
|
+
onUpgradeClick: ( event?: React.MouseEvent< HTMLButtonElement > ) => void;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
export type ErrorMessageProps = {
|
|
@@ -145,7 +145,7 @@ export function ErrorMessage( { error, onTryAgainClick }: ErrorMessageProps ): R
|
|
|
145
145
|
<span>
|
|
146
146
|
{ sprintf(
|
|
147
147
|
// translators: %1$d: A dynamic error message
|
|
148
|
-
__( 'Error: %1$s
|
|
148
|
+
__( 'Error: %1$s', 'jetpack-ai-client' ),
|
|
149
149
|
errorMessage
|
|
150
150
|
) }
|
|
151
151
|
</span>
|
|
@@ -9,15 +9,86 @@ import requestJwt from '../../jwt/index.js';
|
|
|
9
9
|
|
|
10
10
|
const debug = debugFactory( 'ai-client:use-image-generator' );
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Cut the post content on a given lenght so the total length of the prompt is not longer than 4000 characters.
|
|
14
|
+
* @param {string} content - the content to be truncated
|
|
15
|
+
* @param {number} currentPromptLength - the length of the prompt already in use
|
|
16
|
+
* @returns {string} a truncated version of the content respecting the prompt length limit
|
|
17
|
+
*/
|
|
18
|
+
const truncateContent = ( content: string, currentPromptLength: number ): string => {
|
|
19
|
+
const maxLength = 4000;
|
|
20
|
+
const remainingLength = maxLength - currentPromptLength;
|
|
21
|
+
// 6 is the length of the ellipsis and the space before it
|
|
22
|
+
return content.length > remainingLength
|
|
23
|
+
? content.substring( 0, remainingLength - 6 ) + ` [...]`
|
|
24
|
+
: content;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create the prompt string based on the provided context.
|
|
29
|
+
* @param {string} postContent - the content of the post
|
|
30
|
+
* @param {string} userPrompt - the user prompt for the image generation, if provided. Max length is 1000 characters, will be truncated.
|
|
31
|
+
* @returns {string} the prompt string
|
|
32
|
+
*/
|
|
33
|
+
const getImageGenerationPrompt = ( postContent: string, userPrompt?: string ): string => {
|
|
34
|
+
/**
|
|
35
|
+
* If the user provide some custom prompt for the image generation,
|
|
36
|
+
* we will use it, add the post content as additional context and
|
|
37
|
+
* provide some guardrails for the generation.
|
|
38
|
+
*/
|
|
39
|
+
if ( userPrompt ) {
|
|
40
|
+
const imageGenerationPrompt = `I need a cover image for a blog post based on this user prompt:
|
|
41
|
+
|
|
42
|
+
${ userPrompt.length > 1000 ? userPrompt.substring( 0, 1000 ) : userPrompt }
|
|
43
|
+
|
|
44
|
+
Before creating the image, identify the main topic of the user prompt and relate it to the post content.
|
|
45
|
+
Do not represent the whole content in one image, keep it simple and just represent one single idea.
|
|
46
|
+
Do not add details, detailed explanations or highlights from the content, just represent the main idea as if it was a photograph.
|
|
47
|
+
Do not use collages or compositions with multiple elements or scenes. Stick to one single scene. Do not compose unrealistic scenes.
|
|
48
|
+
If the content describes facts, objects or concepts from the real world, represent them on a realistic style and do not make unreal compositions.
|
|
49
|
+
If the content is more abstract, use a more abstract style to represent the main idea.
|
|
50
|
+
Make sure the light and the style are visually appealing.
|
|
51
|
+
Do not add text to the image.
|
|
52
|
+
|
|
53
|
+
For additional context, this is the post content:
|
|
54
|
+
|
|
55
|
+
`;
|
|
56
|
+
// truncating the content so the whole prompt is not longer than 4000 characters, the model limit.
|
|
57
|
+
return imageGenerationPrompt + truncateContent( postContent, imageGenerationPrompt.length );
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* When the user does not provide a custom prompt, we will use the
|
|
62
|
+
* standard one, based solely on the post content.
|
|
63
|
+
*/
|
|
64
|
+
const imageGenerationPrompt = `I need a cover image for a blog post.
|
|
65
|
+
Before creating the image, identify the main topic of the content and only represent it.
|
|
66
|
+
Do not represent the whole content in one image, keep it simple and just represent one single idea.
|
|
67
|
+
Do not add details, detailed explanations or highlights from the content, just represent the main idea as if it was a photograph.
|
|
68
|
+
Do not use collages or compositions with multiple elements or scenes. Stick to one single scene. Do not compose unrealistic scenes.
|
|
69
|
+
If the content describes facts, objects or concepts from the real world, represent them on a realistic style and do not make unreal compositions.
|
|
70
|
+
If the content is more abstract, use a more abstract style to represent the main idea.
|
|
71
|
+
Make sure the light and the style are visually appealing.
|
|
72
|
+
Do not add text to the image.
|
|
73
|
+
|
|
74
|
+
This is the post content:
|
|
75
|
+
|
|
76
|
+
`;
|
|
77
|
+
// truncating the content so the whole prompt is not longer than 4000 characters, the model limit.
|
|
78
|
+
return imageGenerationPrompt + truncateContent( postContent, imageGenerationPrompt.length );
|
|
79
|
+
};
|
|
80
|
+
|
|
12
81
|
const useImageGenerator = () => {
|
|
13
82
|
const generateImage = async function ( {
|
|
14
83
|
feature,
|
|
15
84
|
postContent,
|
|
16
85
|
responseFormat = 'url',
|
|
86
|
+
userPrompt,
|
|
17
87
|
}: {
|
|
18
88
|
feature: string;
|
|
19
89
|
postContent: string;
|
|
20
90
|
responseFormat?: 'url' | 'b64_json';
|
|
91
|
+
userPrompt?: string;
|
|
21
92
|
} ): Promise< { data: Array< { [ key: string ]: string } > } > {
|
|
22
93
|
let token = '';
|
|
23
94
|
|
|
@@ -31,21 +102,7 @@ const useImageGenerator = () => {
|
|
|
31
102
|
try {
|
|
32
103
|
debug( 'Generating image' );
|
|
33
104
|
|
|
34
|
-
|
|
35
|
-
const imageGenerationPrompt =
|
|
36
|
-
`I need a cover image for a blog post.
|
|
37
|
-
Before creating the image, identify the main topic of the content and only represent it.
|
|
38
|
-
Do not represent the whole content in one image, keep it simple and just represent one single idea.
|
|
39
|
-
Do not add details, detailed explanations or highlights from the content, just represent the main idea as if it was a photograph.
|
|
40
|
-
Do not use collages or compositions with multiple elements or scenes. Stick to one single scene. Do not compose unrealistic scenes.
|
|
41
|
-
If the content describes facts, objects or concepts from the real world, represent them on a realistic style and do not make unreal compositions.
|
|
42
|
-
If the content is more abstract, use a more abstract style to represent the main idea.
|
|
43
|
-
Make sure the light and the style are visually appealing.
|
|
44
|
-
Do not add text to the image.
|
|
45
|
-
|
|
46
|
-
This is the post content:
|
|
47
|
-
|
|
48
|
-
` + ( postContent.length > 3000 ? postContent.substring( 0, 3000 ) + ` [...]` : postContent ); // truncating the content so the whole prompt is not longer than 4000 characters, the model limit.
|
|
105
|
+
const imageGenerationPrompt = getImageGenerationPrompt( postContent, userPrompt );
|
|
49
106
|
|
|
50
107
|
const URL = 'https://public-api.wordpress.com/wpcom/v2/jetpack-ai-image';
|
|
51
108
|
|