@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 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: "jetpack-components-ai-control__container-wrapper", 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
  }
@@ -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: sendHandler });
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.', 'jetpack-ai-client'), errorMessage) }), _jsx(Button, { variant: "link", onClick: onTryAgainClick, children: __('Try Again', 'jetpack-ai-client') })] }));
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
- const useImageGenerator = () => {
11
- const generateImage = async function ({ feature, postContent, responseFormat = 'url', }) {
12
- let token = '';
13
- try {
14
- token = (await requestJwt()).token;
15
- }
16
- catch (error) {
17
- debug('Error getting token: %o', error);
18
- return Promise.reject(error);
19
- }
20
- try {
21
- debug('Generating image');
22
- // TODO: fine tune the prompt as we move forward
23
- const imageGenerationPrompt = `I need a cover image for a blog post.
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
- ` + (postContent.length > 3000 ? postContent.substring(0, 3000) + ` [...]` : postContent); // truncating the content so the whole prompt is not longer than 4000 characters, the model limit.
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.12.4",
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.6",
27
- "@storybook/blocks": "8.0.6",
28
- "@storybook/preview-api": "8.0.6",
29
- "@storybook/react": "8.0.6",
30
- "@types/markdown-it": "14.0.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.23",
46
- "@automattic/jetpack-connection": "^0.33.8",
47
- "@automattic/jetpack-shared-extension-utils": "^0.14.10",
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.2.74",
50
- "@wordpress/api-fetch": "6.52.0",
51
- "@wordpress/block-editor": "12.23.0",
52
- "@wordpress/components": "27.3.0",
53
- "@wordpress/compose": "6.32.0",
54
- "@wordpress/data": "9.25.0",
55
- "@wordpress/element": "5.32.0",
56
- "@wordpress/i18n": "4.55.0",
57
- "@wordpress/icons": "9.46.0",
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 className="jetpack-components-ai-control__container-wrapper">
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
- onUpgrade?.();
121
- }, [ 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 ] );
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={ sendHandler } />;
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.', 'jetpack-ai-client' ),
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
- // TODO: fine tune the prompt as we move forward
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