@automattic/jetpack-ai-client 0.22.0 → 0.24.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.
Files changed (28) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/build/ai-client/src/index.d.ts +1 -0
  3. package/build/ai-client/src/index.js +1 -0
  4. package/build/ai-client/src/logo-generator/components/feature-fetch-failure-screen.d.ts +1 -1
  5. package/build/ai-client/src/logo-generator/components/feature-fetch-failure-screen.js +2 -1
  6. package/build/ai-client/src/logo-generator/components/generator-modal.js +23 -10
  7. package/build/ai-client/src/logo-generator/components/prompt.d.ts +9 -0
  8. package/build/ai-client/src/logo-generator/components/prompt.js +32 -27
  9. package/build/ai-client/src/logo-generator/hooks/use-logo-generator.js +1 -1
  10. package/build/ai-client/src/logo-generator/index.d.ts +1 -0
  11. package/build/ai-client/src/logo-generator/index.js +1 -0
  12. package/build/ai-client/src/logo-generator/store/actions.js +3 -0
  13. package/build/ai-client/src/logo-generator/store/reducer.d.ts +32 -3
  14. package/build/ai-client/src/logo-generator/store/reducer.js +10 -0
  15. package/build/ai-client/src/logo-generator/store/types.d.ts +1 -0
  16. package/build/ai-client/src/logo-generator/types.d.ts +1 -1
  17. package/package.json +1 -1
  18. package/src/index.ts +1 -0
  19. package/src/logo-generator/components/feature-fetch-failure-screen.tsx +14 -5
  20. package/src/logo-generator/components/generator-modal.tsx +27 -10
  21. package/src/logo-generator/components/prompt.scss +1 -1
  22. package/src/logo-generator/components/prompt.tsx +87 -57
  23. package/src/logo-generator/hooks/use-logo-generator.ts +1 -1
  24. package/src/logo-generator/index.ts +1 -0
  25. package/src/logo-generator/store/actions.ts +4 -0
  26. package/src/logo-generator/store/reducer.ts +10 -0
  27. package/src/logo-generator/store/types.ts +1 -0
  28. package/src/logo-generator/types.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ 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.24.0] - 2024-10-29
9
+ ### Added
10
+ - AI Client: export image generator hook constants [#39917]
11
+
12
+ ## [0.23.0] - 2024-10-28
13
+ ### Changed
14
+ - AI Client: Decouple prompt input as component and export it for reusability. [#39864]
15
+ - AI Client: Make reload handler prop optional. [#39848]
16
+
17
+ ### Fixed
18
+ - AI Client: Fix initial state being mapped even when fetch fails. [#39846]
19
+
8
20
  ## [0.22.0] - 2024-10-21
9
21
  ### Changed
10
22
  - AI Client: Add types for AI assistant feature payload data branch featuresControl. [#39826]
@@ -439,6 +451,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
439
451
  - Updated package dependencies. [#31659]
440
452
  - Updated package dependencies. [#31785]
441
453
 
454
+ [0.24.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.23.0...v0.24.0
455
+ [0.23.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.22.0...v0.23.0
442
456
  [0.22.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.21.0...v0.22.0
443
457
  [0.21.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.20.1...v0.21.0
444
458
  [0.20.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.20.0...v0.20.1
@@ -9,6 +9,7 @@ export { default as useAudioTranscription } from './hooks/use-audio-transcriptio
9
9
  export { default as useTranscriptionPostProcessing } from './hooks/use-transcription-post-processing/index.js';
10
10
  export { default as useAudioValidation } from './hooks/use-audio-validation/index.js';
11
11
  export { default as useImageGenerator } from './hooks/use-image-generator/index.js';
12
+ export * from './hooks/use-image-generator/constants.js';
12
13
  export * from './icons/index.js';
13
14
  export * from './components/index.js';
14
15
  export * from './data-flow/index.js';
@@ -15,6 +15,7 @@ export { default as useAudioTranscription } from './hooks/use-audio-transcriptio
15
15
  export { default as useTranscriptionPostProcessing } from './hooks/use-transcription-post-processing/index.js';
16
16
  export { default as useAudioValidation } from './hooks/use-audio-validation/index.js';
17
17
  export { default as useImageGenerator } from './hooks/use-image-generator/index.js';
18
+ export * from './hooks/use-image-generator/constants.js';
18
19
  /*
19
20
  * Components: Icons
20
21
  */
@@ -4,5 +4,5 @@
4
4
  import type React from 'react';
5
5
  export declare const FeatureFetchFailureScreen: React.FC<{
6
6
  onCancel: () => void;
7
- onRetry: () => void;
7
+ onRetry?: () => void;
8
8
  }>;
@@ -6,5 +6,6 @@ import { Button } from '@wordpress/components';
6
6
  import { __ } from '@wordpress/i18n';
7
7
  export const FeatureFetchFailureScreen = ({ onCancel, onRetry }) => {
8
8
  const errorMessage = __('We are sorry. There was an error loading your Jetpack AI plan data. Please, try again.', 'jetpack-ai-client');
9
- return (_jsxs("div", { className: "jetpack-ai-logo-generator-modal__notice-message-wrapper", children: [_jsx("div", { className: "jetpack-ai-logo-generator-modal__notice-message", children: _jsx("span", { className: "jetpack-ai-logo-generator-modal__loading-message", children: errorMessage }) }), _jsxs("div", { className: "jetpack-ai-logo-generator-modal__notice-actions", children: [_jsx(Button, { variant: "tertiary", onClick: onCancel, children: __('Cancel', 'jetpack-ai-client') }), _jsx(Button, { variant: "primary", onClick: onRetry, children: __('Try again', 'jetpack-ai-client') })] })] }));
9
+ const errorMessageWithoutRetry = __('We are sorry. There was an error loading your Jetpack AI plan data. Please, reload the page and try again.', 'jetpack-ai-client');
10
+ return (_jsxs("div", { className: "jetpack-ai-logo-generator-modal__notice-message-wrapper", children: [_jsx("div", { className: "jetpack-ai-logo-generator-modal__notice-message", children: _jsx("span", { className: "jetpack-ai-logo-generator-modal__loading-message", children: onRetry ? errorMessage : errorMessageWithoutRetry }) }), _jsxs("div", { className: "jetpack-ai-logo-generator-modal__notice-actions", children: [_jsx(Button, { variant: "tertiary", onClick: onCancel, children: __('Cancel', 'jetpack-ai-client') }), onRetry && (_jsx(Button, { variant: "primary", onClick: onRetry, children: __('Try again', 'jetpack-ai-client') }))] })] }));
10
11
  };
@@ -29,7 +29,7 @@ import { UpgradeScreen } from './upgrade-screen.js';
29
29
  import { VisitSiteBanner } from './visit-site-banner.js';
30
30
  import './generator-modal.scss';
31
31
  const debug = debugFactory('jetpack-ai-calypso:generator-modal');
32
- export const GeneratorModal = ({ isOpen, onClose, onApplyLogo, onReload, siteDetails, context, placement, }) => {
32
+ export const GeneratorModal = ({ isOpen, onClose, onApplyLogo, onReload = null, siteDetails, context, placement, }) => {
33
33
  const { tracks } = useAnalytics();
34
34
  const { recordEvent: recordTracksEvent } = tracks;
35
35
  const { setSiteDetails, fetchAiAssistantFeature, loadLogoHistory, setIsLoadingHistory } = useDispatch(STORE_NAME);
@@ -41,7 +41,7 @@ export const GeneratorModal = ({ isOpen, onClose, onApplyLogo, onReload, siteDet
41
41
  const [needsFeature, setNeedsFeature] = useState(false);
42
42
  const [needsMoreRequests, setNeedsMoreRequests] = useState(false);
43
43
  const { selectedLogo, getAiAssistantFeature, generateFirstPrompt, generateLogo, setContext, tierPlansEnabled, site, requireUpgrade, } = useLogoGenerator();
44
- const { featureFetchError, firstLogoPromptFetchError, clearErrors } = useRequestErrors();
44
+ const { featureFetchError, setFeatureFetchError, firstLogoPromptFetchError, clearErrors } = useRequestErrors();
45
45
  const siteId = siteDetails?.ID;
46
46
  const [logoAccepted, setLogoAccepted] = useState(false);
47
47
  const { nextTierCheckoutURL: upgradeURL } = useCheckout();
@@ -70,6 +70,13 @@ export const GeneratorModal = ({ isOpen, onClose, onApplyLogo, onReload, siteDet
70
70
  */
71
71
  const initializeModal = useCallback(async () => {
72
72
  try {
73
+ if (!siteId) {
74
+ throw new Error('Site ID is missing');
75
+ }
76
+ if (!feature?.featuresControl?.['logo-generator']?.enabled) {
77
+ setFeatureFetchError('Failed to fetch feature data');
78
+ throw new Error('Failed to fetch feature data');
79
+ }
73
80
  const hasHistory = !isLogoHistoryEmpty(String(siteId));
74
81
  const logoCost = feature?.costs?.['jetpack-ai-logo-generator']?.logo ?? DEFAULT_LOGO_COST;
75
82
  const promptCreationCost = 1;
@@ -86,9 +93,9 @@ export const GeneratorModal = ({ isOpen, onClose, onApplyLogo, onReload, siteDet
86
93
  ? currentLimit - currentUsage < logoCost + promptCreationCost
87
94
  : currentLimit < currentUsage);
88
95
  // If the site requires an upgrade, show the upgrade screen immediately.
89
- setNeedsFeature(currentLimit === 0);
96
+ setNeedsFeature(currentValue === 0);
90
97
  setNeedsMoreRequests(siteNeedsMoreRequests);
91
- if (currentLimit === 0 || siteNeedsMoreRequests) {
98
+ if (currentValue === 0 || siteNeedsMoreRequests) {
92
99
  setLoadingState(null);
93
100
  return;
94
101
  }
@@ -135,6 +142,7 @@ export const GeneratorModal = ({ isOpen, onClose, onApplyLogo, onReload, siteDet
135
142
  isLogoHistoryEmpty,
136
143
  siteId,
137
144
  requireUpgrade,
145
+ setFeatureFetchError,
138
146
  ]);
139
147
  const handleModalOpen = useCallback(async () => {
140
148
  setContext(context);
@@ -153,6 +161,14 @@ export const GeneratorModal = ({ isOpen, onClose, onApplyLogo, onReload, siteDet
153
161
  setIsLoadingHistory(false);
154
162
  recordTracksEvent(EVENT_MODAL_CLOSE, { context, placement });
155
163
  };
164
+ const handleReload = useCallback(() => {
165
+ if (!onReload) {
166
+ return;
167
+ }
168
+ closeModal();
169
+ requestedFeatureData.current = false;
170
+ onReload();
171
+ }, [onReload, closeModal]);
156
172
  const handleApplyLogo = (mediaId) => {
157
173
  setLogoAccepted(true);
158
174
  onApplyLogo?.(mediaId);
@@ -177,7 +193,7 @@ export const GeneratorModal = ({ isOpen, onClose, onApplyLogo, onReload, siteDet
177
193
  // Handles modal opening logic
178
194
  useEffect(() => {
179
195
  // While the modal is not open, the siteId is not set, or the feature data is not available, do nothing.
180
- if (!isOpen || !siteId || !feature?.costs) {
196
+ if (!isOpen) {
181
197
  return;
182
198
  }
183
199
  // Prevent multiple calls of the handleModalOpen function
@@ -185,16 +201,13 @@ export const GeneratorModal = ({ isOpen, onClose, onApplyLogo, onReload, siteDet
185
201
  needsToHandleModalOpen.current = false;
186
202
  handleModalOpen();
187
203
  }
188
- }, [isOpen, siteId, handleModalOpen, feature]);
204
+ }, [isOpen, handleModalOpen]);
189
205
  let body;
190
206
  if (loadingState) {
191
207
  body = _jsx(FirstLoadScreen, { state: loadingState });
192
208
  }
193
209
  else if (featureFetchError || firstLogoPromptFetchError) {
194
- body = (_jsx(FeatureFetchFailureScreen, { onCancel: closeModal, onRetry: () => {
195
- closeModal();
196
- onReload?.();
197
- } }));
210
+ body = (_jsx(FeatureFetchFailureScreen, { onCancel: closeModal, onRetry: onReload ? handleReload : null }));
198
211
  }
199
212
  else if (needsFeature || needsMoreRequests) {
200
213
  body = (_jsx(UpgradeScreen, { onCancel: closeModal, upgradeURL: upgradeURL, reason: needsFeature ? 'feature' : 'requests' }));
@@ -1,6 +1,15 @@
1
+ import { Dispatch, SetStateAction } from 'react';
1
2
  import './prompt.scss';
2
3
  type PromptProps = {
3
4
  initialPrompt?: string;
4
5
  };
6
+ export declare const AiModalPromptInput: ({ prompt, setPrompt, disabled, generateHandler, placeholder, buttonLabel, }: {
7
+ prompt: string;
8
+ setPrompt: Dispatch<SetStateAction<string>>;
9
+ disabled: boolean;
10
+ generateHandler: () => void;
11
+ placeholder?: string;
12
+ buttonLabel?: string;
13
+ }) => import("react/jsx-runtime").JSX.Element;
5
14
  export declare const Prompt: ({ initialPrompt }: PromptProps) => import("react/jsx-runtime").JSX.Element;
6
15
  export {};
@@ -21,6 +21,36 @@ import { FairUsageNotice } from './fair-usage-notice.js';
21
21
  import { UpgradeNudge } from './upgrade-nudge.js';
22
22
  import './prompt.scss';
23
23
  const debug = debugFactory('jetpack-ai-calypso:prompt-box');
24
+ export const AiModalPromptInput = ({ prompt = '', setPrompt = () => { }, disabled = false, generateHandler = () => { }, placeholder = '', buttonLabel = '', }) => {
25
+ const inputRef = useRef(null);
26
+ const hasPrompt = prompt?.length >= MINIMUM_PROMPT_LENGTH;
27
+ const onPromptInput = (event) => {
28
+ setPrompt(event.target.textContent || '');
29
+ };
30
+ const onPromptPaste = (event) => {
31
+ event.preventDefault();
32
+ const selection = event.currentTarget.ownerDocument.getSelection();
33
+ if (!selection || !selection.rangeCount) {
34
+ return;
35
+ }
36
+ // Paste plain text only
37
+ const text = event.clipboardData.getData('text/plain');
38
+ selection.deleteFromDocument();
39
+ const range = selection.getRangeAt(0);
40
+ range.insertNode(document.createTextNode(text));
41
+ selection.collapseToEnd();
42
+ setPrompt(inputRef.current?.textContent || '');
43
+ };
44
+ const onKeyDown = (event) => {
45
+ if (event.key === 'Enter') {
46
+ event.preventDefault();
47
+ generateHandler();
48
+ }
49
+ };
50
+ return (_jsxs("div", { className: "jetpack-ai-logo-generator__prompt-query", children: [_jsx("div", { role: "textbox", tabIndex: 0, ref: inputRef, contentEditable: !disabled,
51
+ // The content editable div is expected to be updated by the enhance prompt, so warnings are suppressed
52
+ suppressContentEditableWarning: true, className: "prompt-query__input", onInput: onPromptInput, onPaste: onPromptPaste, onKeyDown: onKeyDown, "data-placeholder": placeholder }), _jsx(Button, { variant: "primary", className: "jetpack-ai-logo-generator__prompt-submit", onClick: generateHandler, disabled: disabled || !hasPrompt, children: buttonLabel || __('Generate', 'jetpack-ai-client') })] }));
53
+ };
24
54
  export const Prompt = ({ initialPrompt = '' }) => {
25
55
  const { tracks } = useAnalytics();
26
56
  const { recordEvent: recordTracksEvent } = tracks;
@@ -70,7 +100,7 @@ export const Prompt = ({ initialPrompt = '' }) => {
70
100
  }
71
101
  }, [prompt]);
72
102
  useEffect(() => {
73
- if (imageStyles.length > 0) {
103
+ if (imageStyles && imageStyles.length > 0) {
74
104
  // Sort styles to have "None" and "Auto" first
75
105
  setStyles([
76
106
  imageStyles.find(({ value }) => value === IMAGE_STYLE_NONE),
@@ -103,23 +133,6 @@ export const Prompt = ({ initialPrompt = '' }) => {
103
133
  generateLogo({ prompt, style });
104
134
  }
105
135
  }, [context, generateLogo, prompt, style]);
106
- const onPromptInput = (event) => {
107
- setPrompt(event.target.textContent || '');
108
- };
109
- const onPromptPaste = (event) => {
110
- event.preventDefault();
111
- const selection = event.currentTarget.ownerDocument.getSelection();
112
- if (!selection || !selection.rangeCount) {
113
- return;
114
- }
115
- // Paste plain text only
116
- const text = event.clipboardData.getData('text/plain');
117
- selection.deleteFromDocument();
118
- const range = selection.getRangeAt(0);
119
- range.insertNode(document.createTextNode(text));
120
- selection.collapseToEnd();
121
- setPrompt(inputRef.current?.textContent || '');
122
- };
123
136
  const onUpgradeClick = () => {
124
137
  recordTracksEvent(EVENT_UPGRADE, { context, placement: EVENT_PLACEMENT_INPUT_FOOTER });
125
138
  };
@@ -128,15 +141,7 @@ export const Prompt = ({ initialPrompt = '' }) => {
128
141
  setStyle(imageStyle);
129
142
  recordTracksEvent(EVENT_SWITCH_STYLE, { context, style: imageStyle });
130
143
  }, [context, setStyle, recordTracksEvent]);
131
- const onKeyDown = (event) => {
132
- if (event.key === 'Enter') {
133
- event.preventDefault();
134
- onGenerate();
135
- }
136
- };
137
- return (_jsxs("div", { className: "jetpack-ai-logo-generator__prompt", children: [_jsxs("div", { className: "jetpack-ai-logo-generator__prompt-header", children: [_jsx("div", { className: "jetpack-ai-logo-generator__prompt-label", children: __('Describe your site:', 'jetpack-ai-client') }), _jsx("div", { className: "jetpack-ai-logo-generator__prompt-actions", children: _jsxs(Button, { variant: "link", disabled: isBusy || requireUpgrade || !hasPrompt, onClick: onEnhance, children: [_jsx(AiIcon, {}), enhanceButtonLabel] }) }), showStyleSelector && (_jsx(SelectControl, { __nextHasNoMarginBottom: true, value: style, options: styles, onChange: updateStyle, disabled: isBusy || requireUpgrade }))] }), _jsxs("div", { className: "jetpack-ai-logo-generator__prompt-query", children: [_jsx("div", { role: "textbox", tabIndex: 0, ref: inputRef, contentEditable: !isBusy && !requireUpgrade,
138
- // The content editable div is expected to be updated by the enhance prompt, so warnings are suppressed
139
- suppressContentEditableWarning: true, className: "prompt-query__input", onInput: onPromptInput, onPaste: onPromptPaste, onKeyDown: onKeyDown, "data-placeholder": __('Describe your site or simply ask for a logo specifying some details about it', 'jetpack-ai-client') }), _jsx(Button, { variant: "primary", className: "jetpack-ai-logo-generator__prompt-submit", onClick: onGenerate, disabled: isBusy || requireUpgrade || !hasPrompt, children: __('Generate', 'jetpack-ai-client') })] }), _jsxs("div", { className: "jetpack-ai-logo-generator__prompt-footer", children: [!isUnlimited && !requireUpgrade && (_jsxs("div", { className: "jetpack-ai-logo-generator__prompt-requests", children: [_jsx("div", { children: sprintf(
144
+ return (_jsxs("div", { className: "jetpack-ai-logo-generator__prompt", children: [_jsxs("div", { className: "jetpack-ai-logo-generator__prompt-header", children: [_jsx("div", { className: "jetpack-ai-logo-generator__prompt-label", children: __('Describe your site:', 'jetpack-ai-client') }), _jsx("div", { className: "jetpack-ai-logo-generator__prompt-actions", children: _jsxs(Button, { variant: "link", disabled: isBusy || requireUpgrade || !hasPrompt, onClick: onEnhance, children: [_jsx(AiIcon, {}), enhanceButtonLabel] }) }), showStyleSelector && (_jsx(SelectControl, { __nextHasNoMarginBottom: true, value: style, options: styles, onChange: updateStyle, disabled: isBusy || requireUpgrade }))] }), _jsx(AiModalPromptInput, { prompt: prompt, setPrompt: setPrompt, generateHandler: onGenerate, disabled: isBusy || requireUpgrade, placeholder: __('Describe your site or simply ask for a logo specifying some details about it', 'jetpack-ai-client') }), _jsxs("div", { className: "jetpack-ai-logo-generator__prompt-footer", children: [!isUnlimited && !requireUpgrade && (_jsxs("div", { className: "jetpack-ai-logo-generator__prompt-requests", children: [_jsx("div", { children: sprintf(
140
145
  // translators: %u is the number of requests
141
146
  __('%u requests remaining.', 'jetpack-ai-client'), requestsRemaining) }), hasNextTier && (_jsxs(_Fragment, { children: ["\u00A0", _jsx(Button, { variant: "link", href: checkoutUrl, target: "_blank", onClick: onUpgradeClick, children: __('Upgrade', 'jetpack-ai-client') })] })), "\u00A0", _jsx(Tooltip, { text: __('Logo generation costs 10 requests; prompt enhancement costs 1 request each', 'jetpack-ai-client'), placement: "bottom", children: _jsx(Icon, { className: "prompt-footer__icon", icon: info }) })] })), requireUpgrade && tierPlansEnabled && _jsx(UpgradeNudge, {}), requireUpgrade && !tierPlansEnabled && _jsx(FairUsageNotice, {}), enhancePromptFetchError && (_jsx("div", { className: "jetpack-ai-logo-generator__prompt-error", children: __('Error enhancing prompt. Please try again.', 'jetpack-ai-client') })), logoFetchError && (_jsx("div", { className: "jetpack-ai-logo-generator__prompt-error", children: __('Error generating logo. Please try again.', 'jetpack-ai-client') }))] })] }));
142
147
  };
@@ -45,7 +45,7 @@ const useLogoGenerator = () => {
45
45
  const aiAssistantFeatureData = getAiAssistantFeature(siteId);
46
46
  const logoGenerationCost = aiAssistantFeatureData?.costs?.['jetpack-ai-logo-generator']?.logo;
47
47
  const logoGeneratorControl = aiAssistantFeatureData?.featuresControl?.['logo-generator'];
48
- const imageStyles = logoGeneratorControl?.styles;
48
+ const imageStyles = logoGeneratorControl?.styles || [];
49
49
  const generateFirstPrompt = useCallback(async function () {
50
50
  setFirstLogoPromptFetchError(null);
51
51
  increaseAiAssistantRequestsCount();
@@ -1 +1,2 @@
1
1
  export * from './components/generator-modal.js';
2
+ export { AiModalPromptInput } from './components/prompt.js';
@@ -1 +1,2 @@
1
1
  export * from './components/generator-modal.js';
2
+ export { AiModalPromptInput } from './components/prompt.js';
@@ -56,6 +56,9 @@ const actions = {
56
56
  path: '/wpcom/v2/jetpack-ai/ai-assistant-feature',
57
57
  query: 'force=wpcom',
58
58
  });
59
+ if (response.data) {
60
+ throw new Error('Failed to fetch');
61
+ }
59
62
  // Store the feature in the store.
60
63
  dispatch(actions.storeAiAssistantFeature(mapAiFeatureResponseToAiFeatureProps(response)));
61
64
  }
@@ -214,6 +214,38 @@ export default function reducer(state: LogoGeneratorStateProp, action: {
214
214
  history: import("./types.js").Logo[];
215
215
  selectedLogoIndex: number;
216
216
  } | {
217
+ features: {
218
+ aiAssistantFeature: {
219
+ _meta: {
220
+ isRequesting: boolean;
221
+ asyncRequestCountdown: number;
222
+ asyncRequestTimerId: number;
223
+ isRequestingImage: boolean;
224
+ };
225
+ hasFeature: boolean;
226
+ isOverLimit: boolean;
227
+ requestsCount: number;
228
+ requestsLimit: number;
229
+ requireUpgrade: boolean;
230
+ errorMessage?: string;
231
+ errorCode?: string;
232
+ upgradeType: import("./types.js").UpgradeTypeProp;
233
+ currentTier?: import("./types.js").TierProp;
234
+ usagePeriod?: {
235
+ currentStart: string;
236
+ nextStart: string;
237
+ requestsCount: number;
238
+ };
239
+ nextTier?: import("./types.js").TierProp;
240
+ tierPlansEnabled?: boolean;
241
+ costs?: {
242
+ 'jetpack-ai-logo-generator': {
243
+ logo: number;
244
+ };
245
+ };
246
+ featuresControl?: import("./types.js").FeaturesControl;
247
+ };
248
+ };
217
249
  _meta: {
218
250
  featureFetchError: RequestError;
219
251
  isSavingLogoToLibrary?: boolean;
@@ -229,9 +261,6 @@ export default function reducer(state: LogoGeneratorStateProp, action: {
229
261
  isLoadingHistory?: boolean;
230
262
  };
231
263
  siteDetails?: SiteDetails | Record<string, never>;
232
- features: {
233
- aiAssistantFeature?: AiFeatureStateProps;
234
- };
235
264
  history: import("./types.js").Logo[];
236
265
  selectedLogoIndex: number;
237
266
  } | {
@@ -236,6 +236,16 @@ export default function reducer(state = INITIAL_STATE, action) {
236
236
  case ACTION_SET_FEATURE_FETCH_ERROR:
237
237
  return {
238
238
  ...state,
239
+ features: {
240
+ ...state.features,
241
+ aiAssistantFeature: {
242
+ ...state?.features?.aiAssistantFeature,
243
+ _meta: {
244
+ ...state?.features?.aiAssistantFeature?._meta,
245
+ isRequesting: false,
246
+ },
247
+ },
248
+ },
239
249
  _meta: {
240
250
  ...(state._meta ?? {}),
241
251
  featureFetchError: action.error,
@@ -172,6 +172,7 @@ export type AiAssistantFeatureEndpointResponseProps = {
172
172
  };
173
173
  };
174
174
  'features-control'?: FeaturesControl;
175
+ data?: string;
175
176
  };
176
177
  export type SaveLogo = (logo: Logo) => Promise<{
177
178
  mediaId: number;
@@ -14,7 +14,7 @@ export interface GeneratorModalProps {
14
14
  isOpen: boolean;
15
15
  onClose: () => void;
16
16
  onApplyLogo: (mediaId: number) => void;
17
- onReload: () => void;
17
+ onReload?: () => void;
18
18
  context: string;
19
19
  placement: string;
20
20
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@automattic/jetpack-ai-client",
4
- "version": "0.22.0",
4
+ "version": "0.24.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": {
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export { default as useAudioTranscription } from './hooks/use-audio-transcriptio
16
16
  export { default as useTranscriptionPostProcessing } from './hooks/use-transcription-post-processing/index.js';
17
17
  export { default as useAudioValidation } from './hooks/use-audio-validation/index.js';
18
18
  export { default as useImageGenerator } from './hooks/use-image-generator/index.js';
19
+ export * from './hooks/use-image-generator/constants.js';
19
20
 
20
21
  /*
21
22
  * Components: Icons
@@ -10,25 +10,34 @@ import type React from 'react';
10
10
 
11
11
  export const FeatureFetchFailureScreen: React.FC< {
12
12
  onCancel: () => void;
13
- onRetry: () => void;
13
+ onRetry?: () => void;
14
14
  } > = ( { onCancel, onRetry } ) => {
15
15
  const errorMessage = __(
16
16
  'We are sorry. There was an error loading your Jetpack AI plan data. Please, try again.',
17
17
  'jetpack-ai-client'
18
18
  );
19
19
 
20
+ const errorMessageWithoutRetry = __(
21
+ 'We are sorry. There was an error loading your Jetpack AI plan data. Please, reload the page and try again.',
22
+ 'jetpack-ai-client'
23
+ );
24
+
20
25
  return (
21
26
  <div className="jetpack-ai-logo-generator-modal__notice-message-wrapper">
22
27
  <div className="jetpack-ai-logo-generator-modal__notice-message">
23
- <span className="jetpack-ai-logo-generator-modal__loading-message">{ errorMessage }</span>
28
+ <span className="jetpack-ai-logo-generator-modal__loading-message">
29
+ { onRetry ? errorMessage : errorMessageWithoutRetry }
30
+ </span>
24
31
  </div>
25
32
  <div className="jetpack-ai-logo-generator-modal__notice-actions">
26
33
  <Button variant="tertiary" onClick={ onCancel }>
27
34
  { __( 'Cancel', 'jetpack-ai-client' ) }
28
35
  </Button>
29
- <Button variant="primary" onClick={ onRetry }>
30
- { __( 'Try again', 'jetpack-ai-client' ) }
31
- </Button>
36
+ { onRetry && (
37
+ <Button variant="primary" onClick={ onRetry }>
38
+ { __( 'Try again', 'jetpack-ai-client' ) }
39
+ </Button>
40
+ ) }
32
41
  </div>
33
42
  </div>
34
43
  );
@@ -45,7 +45,7 @@ export const GeneratorModal: React.FC< GeneratorModalProps > = ( {
45
45
  isOpen,
46
46
  onClose,
47
47
  onApplyLogo,
48
- onReload,
48
+ onReload = null,
49
49
  siteDetails,
50
50
  context,
51
51
  placement,
@@ -73,7 +73,8 @@ export const GeneratorModal: React.FC< GeneratorModalProps > = ( {
73
73
  site,
74
74
  requireUpgrade,
75
75
  } = useLogoGenerator();
76
- const { featureFetchError, firstLogoPromptFetchError, clearErrors } = useRequestErrors();
76
+ const { featureFetchError, setFeatureFetchError, firstLogoPromptFetchError, clearErrors } =
77
+ useRequestErrors();
77
78
  const siteId = siteDetails?.ID;
78
79
  const [ logoAccepted, setLogoAccepted ] = useState( false );
79
80
  const { nextTierCheckoutURL: upgradeURL } = useCheckout();
@@ -105,6 +106,15 @@ export const GeneratorModal: React.FC< GeneratorModalProps > = ( {
105
106
  */
106
107
  const initializeModal = useCallback( async () => {
107
108
  try {
109
+ if ( ! siteId ) {
110
+ throw new Error( 'Site ID is missing' );
111
+ }
112
+
113
+ if ( ! feature?.featuresControl?.[ 'logo-generator' ]?.enabled ) {
114
+ setFeatureFetchError( 'Failed to fetch feature data' );
115
+ throw new Error( 'Failed to fetch feature data' );
116
+ }
117
+
108
118
  const hasHistory = ! isLogoHistoryEmpty( String( siteId ) );
109
119
 
110
120
  const logoCost = feature?.costs?.[ 'jetpack-ai-logo-generator' ]?.logo ?? DEFAULT_LOGO_COST;
@@ -125,10 +135,10 @@ export const GeneratorModal: React.FC< GeneratorModalProps > = ( {
125
135
  : currentLimit < currentUsage );
126
136
 
127
137
  // If the site requires an upgrade, show the upgrade screen immediately.
128
- setNeedsFeature( currentLimit === 0 );
138
+ setNeedsFeature( currentValue === 0 );
129
139
  setNeedsMoreRequests( siteNeedsMoreRequests );
130
140
 
131
- if ( currentLimit === 0 || siteNeedsMoreRequests ) {
141
+ if ( currentValue === 0 || siteNeedsMoreRequests ) {
132
142
  setLoadingState( null );
133
143
  return;
134
144
  }
@@ -179,6 +189,7 @@ export const GeneratorModal: React.FC< GeneratorModalProps > = ( {
179
189
  isLogoHistoryEmpty,
180
190
  siteId,
181
191
  requireUpgrade,
192
+ setFeatureFetchError,
182
193
  ] );
183
194
 
184
195
  const handleModalOpen = useCallback( async () => {
@@ -201,6 +212,15 @@ export const GeneratorModal: React.FC< GeneratorModalProps > = ( {
201
212
  recordTracksEvent( EVENT_MODAL_CLOSE, { context, placement } );
202
213
  };
203
214
 
215
+ const handleReload = useCallback( () => {
216
+ if ( ! onReload ) {
217
+ return;
218
+ }
219
+ closeModal();
220
+ requestedFeatureData.current = false;
221
+ onReload();
222
+ }, [ onReload, closeModal ] );
223
+
204
224
  const handleApplyLogo = ( mediaId: number ) => {
205
225
  setLogoAccepted( true );
206
226
  onApplyLogo?.( mediaId );
@@ -229,7 +249,7 @@ export const GeneratorModal: React.FC< GeneratorModalProps > = ( {
229
249
  // Handles modal opening logic
230
250
  useEffect( () => {
231
251
  // While the modal is not open, the siteId is not set, or the feature data is not available, do nothing.
232
- if ( ! isOpen || ! siteId || ! feature?.costs ) {
252
+ if ( ! isOpen ) {
233
253
  return;
234
254
  }
235
255
 
@@ -238,7 +258,7 @@ export const GeneratorModal: React.FC< GeneratorModalProps > = ( {
238
258
  needsToHandleModalOpen.current = false;
239
259
  handleModalOpen();
240
260
  }
241
- }, [ isOpen, siteId, handleModalOpen, feature ] );
261
+ }, [ isOpen, handleModalOpen ] );
242
262
 
243
263
  let body: React.ReactNode;
244
264
 
@@ -248,10 +268,7 @@ export const GeneratorModal: React.FC< GeneratorModalProps > = ( {
248
268
  body = (
249
269
  <FeatureFetchFailureScreen
250
270
  onCancel={ closeModal }
251
- onRetry={ () => {
252
- closeModal();
253
- onReload?.();
254
- } }
271
+ onRetry={ onReload ? handleReload : null }
255
272
  />
256
273
  );
257
274
  } else if ( needsFeature || needsMoreRequests ) {
@@ -69,7 +69,7 @@
69
69
  color: var(--studio-gray-50, #646970);
70
70
  }
71
71
 
72
- &[data-placeholder]:empty:focus::before {
72
+ &[data-placeholder]:empty:focus::before:not([contentEditable="false"]) {
73
73
  content: "";
74
74
  }
75
75
  }
@@ -7,6 +7,7 @@ import { __, sprintf } from '@wordpress/i18n';
7
7
  import { Icon, info } from '@wordpress/icons';
8
8
  import debugFactory from 'debug';
9
9
  import { useCallback, useEffect, useState, useRef } from 'react';
10
+ import { Dispatch, SetStateAction } from 'react';
10
11
  /**
11
12
  * Internal dependencies
12
13
  */
@@ -37,6 +38,81 @@ type PromptProps = {
37
38
  initialPrompt?: string;
38
39
  };
39
40
 
41
+ export const AiModalPromptInput = ( {
42
+ prompt = '',
43
+ setPrompt = () => {},
44
+ disabled = false,
45
+ generateHandler = () => {},
46
+ placeholder = '',
47
+ buttonLabel = '',
48
+ }: {
49
+ prompt: string;
50
+ setPrompt: Dispatch< SetStateAction< string > >;
51
+ disabled: boolean;
52
+ generateHandler: () => void;
53
+ placeholder?: string;
54
+ buttonLabel?: string;
55
+ } ) => {
56
+ const inputRef = useRef< HTMLDivElement | null >( null );
57
+ const hasPrompt = prompt?.length >= MINIMUM_PROMPT_LENGTH;
58
+
59
+ const onPromptInput = ( event: React.ChangeEvent< HTMLInputElement > ) => {
60
+ setPrompt( event.target.textContent || '' );
61
+ };
62
+
63
+ const onPromptPaste = ( event: React.ClipboardEvent< HTMLInputElement > ) => {
64
+ event.preventDefault();
65
+
66
+ const selection = event.currentTarget.ownerDocument.getSelection();
67
+ if ( ! selection || ! selection.rangeCount ) {
68
+ return;
69
+ }
70
+
71
+ // Paste plain text only
72
+ const text = event.clipboardData.getData( 'text/plain' );
73
+
74
+ selection.deleteFromDocument();
75
+ const range = selection.getRangeAt( 0 );
76
+ range.insertNode( document.createTextNode( text ) );
77
+ selection.collapseToEnd();
78
+
79
+ setPrompt( inputRef.current?.textContent || '' );
80
+ };
81
+
82
+ const onKeyDown = ( event: React.KeyboardEvent ) => {
83
+ if ( event.key === 'Enter' ) {
84
+ event.preventDefault();
85
+ generateHandler();
86
+ }
87
+ };
88
+
89
+ return (
90
+ <div className="jetpack-ai-logo-generator__prompt-query">
91
+ <div
92
+ role="textbox"
93
+ tabIndex={ 0 }
94
+ ref={ inputRef }
95
+ contentEditable={ ! disabled }
96
+ // The content editable div is expected to be updated by the enhance prompt, so warnings are suppressed
97
+ suppressContentEditableWarning
98
+ className="prompt-query__input"
99
+ onInput={ onPromptInput }
100
+ onPaste={ onPromptPaste }
101
+ onKeyDown={ onKeyDown }
102
+ data-placeholder={ placeholder }
103
+ ></div>
104
+ <Button
105
+ variant="primary"
106
+ className="jetpack-ai-logo-generator__prompt-submit"
107
+ onClick={ generateHandler }
108
+ disabled={ disabled || ! hasPrompt }
109
+ >
110
+ { buttonLabel || __( 'Generate', 'jetpack-ai-client' ) }
111
+ </Button>
112
+ </div>
113
+ );
114
+ };
115
+
40
116
  export const Prompt = ( { initialPrompt = '' }: PromptProps ) => {
41
117
  const { tracks } = useAnalytics();
42
118
  const { recordEvent: recordTracksEvent } = tracks;
@@ -107,7 +183,7 @@ export const Prompt = ( { initialPrompt = '' }: PromptProps ) => {
107
183
  }, [ prompt ] );
108
184
 
109
185
  useEffect( () => {
110
- if ( imageStyles.length > 0 ) {
186
+ if ( imageStyles && imageStyles.length > 0 ) {
111
187
  // Sort styles to have "None" and "Auto" first
112
188
  setStyles(
113
189
  [
@@ -143,29 +219,6 @@ export const Prompt = ( { initialPrompt = '' }: PromptProps ) => {
143
219
  }
144
220
  }, [ context, generateLogo, prompt, style ] );
145
221
 
146
- const onPromptInput = ( event: React.ChangeEvent< HTMLInputElement > ) => {
147
- setPrompt( event.target.textContent || '' );
148
- };
149
-
150
- const onPromptPaste = ( event: React.ClipboardEvent< HTMLInputElement > ) => {
151
- event.preventDefault();
152
-
153
- const selection = event.currentTarget.ownerDocument.getSelection();
154
- if ( ! selection || ! selection.rangeCount ) {
155
- return;
156
- }
157
-
158
- // Paste plain text only
159
- const text = event.clipboardData.getData( 'text/plain' );
160
-
161
- selection.deleteFromDocument();
162
- const range = selection.getRangeAt( 0 );
163
- range.insertNode( document.createTextNode( text ) );
164
- selection.collapseToEnd();
165
-
166
- setPrompt( inputRef.current?.textContent || '' );
167
- };
168
-
169
222
  const onUpgradeClick = () => {
170
223
  recordTracksEvent( EVENT_UPGRADE, { context, placement: EVENT_PLACEMENT_INPUT_FOOTER } );
171
224
  };
@@ -179,13 +232,6 @@ export const Prompt = ( { initialPrompt = '' }: PromptProps ) => {
179
232
  [ context, setStyle, recordTracksEvent ]
180
233
  );
181
234
 
182
- const onKeyDown = ( event: React.KeyboardEvent ) => {
183
- if ( event.key === 'Enter' ) {
184
- event.preventDefault();
185
- onGenerate();
186
- }
187
- };
188
-
189
235
  return (
190
236
  <div className="jetpack-ai-logo-generator__prompt">
191
237
  <div className="jetpack-ai-logo-generator__prompt-header">
@@ -212,32 +258,16 @@ export const Prompt = ( { initialPrompt = '' }: PromptProps ) => {
212
258
  />
213
259
  ) }
214
260
  </div>
215
- <div className="jetpack-ai-logo-generator__prompt-query">
216
- <div
217
- role="textbox"
218
- tabIndex={ 0 }
219
- ref={ inputRef }
220
- contentEditable={ ! isBusy && ! requireUpgrade }
221
- // The content editable div is expected to be updated by the enhance prompt, so warnings are suppressed
222
- suppressContentEditableWarning
223
- className="prompt-query__input"
224
- onInput={ onPromptInput }
225
- onPaste={ onPromptPaste }
226
- onKeyDown={ onKeyDown }
227
- data-placeholder={ __(
228
- 'Describe your site or simply ask for a logo specifying some details about it',
229
- 'jetpack-ai-client'
230
- ) }
231
- ></div>
232
- <Button
233
- variant="primary"
234
- className="jetpack-ai-logo-generator__prompt-submit"
235
- onClick={ onGenerate }
236
- disabled={ isBusy || requireUpgrade || ! hasPrompt }
237
- >
238
- { __( 'Generate', 'jetpack-ai-client' ) }
239
- </Button>
240
- </div>
261
+ <AiModalPromptInput
262
+ prompt={ prompt }
263
+ setPrompt={ setPrompt }
264
+ generateHandler={ onGenerate }
265
+ disabled={ isBusy || requireUpgrade }
266
+ placeholder={ __(
267
+ 'Describe your site or simply ask for a logo specifying some details about it',
268
+ 'jetpack-ai-client'
269
+ ) }
270
+ />
241
271
  <div className="jetpack-ai-logo-generator__prompt-footer">
242
272
  { ! isUnlimited && ! requireUpgrade && (
243
273
  <div className="jetpack-ai-logo-generator__prompt-requests">
@@ -92,7 +92,7 @@ const useLogoGenerator = () => {
92
92
  const logoGeneratorControl = aiAssistantFeatureData?.featuresControl?.[
93
93
  'logo-generator'
94
94
  ] as LogoGeneratorFeatureControl;
95
- const imageStyles: Array< ImageStyleObject > = logoGeneratorControl?.styles;
95
+ const imageStyles: Array< ImageStyleObject > = logoGeneratorControl?.styles || [];
96
96
 
97
97
  const generateFirstPrompt = useCallback(
98
98
  async function (): Promise< string > {
@@ -1 +1,2 @@
1
1
  export * from './components/generator-modal.js';
2
+ export { AiModalPromptInput } from './components/prompt.js';
@@ -93,6 +93,10 @@ const actions = {
93
93
  query: 'force=wpcom',
94
94
  } );
95
95
 
96
+ if ( response.data ) {
97
+ throw new Error( 'Failed to fetch' );
98
+ }
99
+
96
100
  // Store the feature in the store.
97
101
  dispatch(
98
102
  actions.storeAiAssistantFeature( mapAiFeatureResponseToAiFeatureProps( response ) )
@@ -325,6 +325,16 @@ export default function reducer(
325
325
  case ACTION_SET_FEATURE_FETCH_ERROR:
326
326
  return {
327
327
  ...state,
328
+ features: {
329
+ ...state.features,
330
+ aiAssistantFeature: {
331
+ ...state?.features?.aiAssistantFeature,
332
+ _meta: {
333
+ ...state?.features?.aiAssistantFeature?._meta,
334
+ isRequesting: false,
335
+ },
336
+ },
337
+ },
328
338
  _meta: {
329
339
  ...( state._meta ?? {} ),
330
340
  featureFetchError: action.error,
@@ -220,6 +220,7 @@ export type AiAssistantFeatureEndpointResponseProps = {
220
220
  };
221
221
  };
222
222
  'features-control'?: FeaturesControl;
223
+ data?: string; // when WP responds with a 200 status code but it's the error wrap
223
224
  };
224
225
 
225
226
  export type SaveLogo = ( logo: Logo ) => Promise< { mediaId: number; mediaURL: string } >;
@@ -16,7 +16,7 @@ export interface GeneratorModalProps {
16
16
  isOpen: boolean;
17
17
  onClose: () => void;
18
18
  onApplyLogo: ( mediaId: number ) => void;
19
- onReload: () => void;
19
+ onReload?: () => void;
20
20
  context: string;
21
21
  placement: string;
22
22
  }