@automattic/jetpack-ai-client 0.25.1 → 0.25.3

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 (27) hide show
  1. package/CHANGELOG.md +17 -4
  2. package/build/ai-client/src/components/ai-feedback/index.d.ts +26 -0
  3. package/build/ai-client/src/components/ai-feedback/index.js +70 -0
  4. package/build/ai-client/src/components/index.d.ts +1 -0
  5. package/build/ai-client/src/components/index.js +1 -0
  6. package/build/ai-client/src/logo-generator/components/fair-usage-notice.d.ts +1 -1
  7. package/build/ai-client/src/logo-generator/components/fair-usage-notice.js +1 -1
  8. package/build/ai-client/src/logo-generator/components/logo-presenter.js +25 -1
  9. package/build/ai-client/src/logo-generator/hooks/use-fair-usage-notice-message.d.ts +2 -2
  10. package/build/ai-client/src/logo-generator/hooks/use-logo-generator.js +3 -0
  11. package/build/ai-client/src/logo-generator/lib/logo-storage.d.ts +9 -8
  12. package/build/ai-client/src/logo-generator/lib/logo-storage.js +12 -8
  13. package/build/ai-client/src/logo-generator/store/reducer.d.ts +2 -2
  14. package/build/ai-client/src/logo-generator/store/types.d.ts +2 -0
  15. package/build/ai-client/src/logo-generator/types.d.ts +1 -0
  16. package/package.json +14 -13
  17. package/src/components/ai-feedback/index.tsx +133 -0
  18. package/src/components/ai-feedback/style.scss +16 -0
  19. package/src/components/index.ts +1 -0
  20. package/src/logo-generator/components/fair-usage-notice.tsx +1 -5
  21. package/src/logo-generator/components/logo-presenter.tsx +41 -0
  22. package/src/logo-generator/hooks/use-fair-usage-notice-message.tsx +2 -2
  23. package/src/logo-generator/hooks/use-logo-generator.ts +4 -0
  24. package/src/logo-generator/lib/logo-storage.ts +18 -8
  25. package/src/logo-generator/store/reducer.ts +1 -6
  26. package/src/logo-generator/store/types.ts +2 -0
  27. package/src/logo-generator/types.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -5,22 +5,33 @@ 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.25.3] - 2024-12-23
9
+ ### Added
10
+ - Jetpack AI: Add thumbs up/down component to AI logo generator [#40610]
11
+
12
+ ## [0.25.2] - 2024-12-16
13
+ ### Changed
14
+ - Updated package dependencies. [#40564]
15
+
16
+ ### Fixed
17
+ - Fixed lints following ESLint rule changes for TS. [#40584]
18
+
8
19
  ## [0.25.1] - 2024-12-09
9
20
  ### Changed
10
- - AI Assistant: Add disclaimer to image generation modals [#40397]
21
+ - AI Assistant: Add disclaimer to image generation modals. [#40397]
11
22
  - Updated package dependencies. [#40363]
12
23
 
13
24
  ## [0.25.0] - 2024-11-25
14
25
  ### Added
15
- - AI Client: split disabled prop to allow disabling input and action button separately [#40210]
26
+ - AI Client: split disabled prop to allow disabling input and action button separately. [#40210]
16
27
 
17
28
  ### Changed
18
- - AI Client: fix prompt cursor to text when editable [#40247]
29
+ - AI Client: fix prompt cursor to text when editable. [#40247]
19
30
  - Updated package dependencies. [#40288]
20
31
 
21
32
  ## [0.24.3] - 2024-11-18
22
33
  ### Changed
23
- - AI Client: add effect on AiModalInputPrompt to update/set prompt on prop update [#40113]
34
+ - AI Client: add effect on AiModalInputPrompt to update/set prompt on prop update. [#40113]
24
35
 
25
36
  ## [0.24.2] - 2024-11-11
26
37
  ### Changed
@@ -476,6 +487,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
476
487
  - AI Client: stop using smart document visibility handling on the fetchEventSource library, so it does not restart the completion when changing tabs. [#32004]
477
488
  - Updated package dependencies. [#31468] [#31659] [#31785]
478
489
 
490
+ [0.25.3]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.2...v0.25.3
491
+ [0.25.2]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.1...v0.25.2
479
492
  [0.25.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.0...v0.25.1
480
493
  [0.25.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.24.3...v0.25.0
481
494
  [0.24.3]: https://github.com/Automattic/jetpack-ai-client/compare/v0.24.2...v0.24.3
@@ -0,0 +1,26 @@
1
+ import './style.scss';
2
+ /**
3
+ * Types
4
+ */
5
+ import type React from 'react';
6
+ type AiFeedbackThumbsProps = {
7
+ disabled?: boolean;
8
+ iconSize?: number;
9
+ ratedItem?: string;
10
+ feature?: string;
11
+ savedRatings?: Record<string, string>;
12
+ options?: {
13
+ mediaLibraryId?: number;
14
+ prompt?: string;
15
+ revisedPrompt?: string;
16
+ };
17
+ onRate?: (rating: string) => void;
18
+ };
19
+ /**
20
+ * AiFeedbackThumbs component.
21
+ *
22
+ * @param {AiFeedbackThumbsProps} props - component props.
23
+ * @return {React.ReactElement} - rendered component.
24
+ */
25
+ export default function AiFeedbackThumbs({ disabled, iconSize, ratedItem, feature, savedRatings, options, onRate, }: AiFeedbackThumbsProps): React.ReactElement;
26
+ export {};
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * External dependencies
4
+ */
5
+ import { useAnalytics, getJetpackExtensionAvailability, } from '@automattic/jetpack-shared-extension-utils';
6
+ import { Button, Tooltip } from '@wordpress/components';
7
+ import { useEffect, useState } from '@wordpress/element';
8
+ import { __ } from '@wordpress/i18n';
9
+ import { thumbsUp, thumbsDown } from '@wordpress/icons';
10
+ import clsx from 'clsx';
11
+ /*
12
+ * Internal dependencies
13
+ */
14
+ import './style.scss';
15
+ /**
16
+ * Get the availability of a feature.
17
+ *
18
+ * @param {string} feature - The feature to check availability for.
19
+ * @return {boolean} - Whether the feature is available.
20
+ */
21
+ function getFeatureAvailability(feature) {
22
+ return getJetpackExtensionAvailability(feature).available === true;
23
+ }
24
+ /**
25
+ * AiFeedbackThumbs component.
26
+ *
27
+ * @param {AiFeedbackThumbsProps} props - component props.
28
+ * @return {React.ReactElement} - rendered component.
29
+ */
30
+ export default function AiFeedbackThumbs({ disabled = false, iconSize = 24, ratedItem = '', feature = '', savedRatings = {}, options = {}, onRate, }) {
31
+ if (!getFeatureAvailability('ai-response-feedback')) {
32
+ return null;
33
+ }
34
+ const [itemsRated, setItemsRated] = useState({});
35
+ const { tracks } = useAnalytics();
36
+ useEffect(() => {
37
+ const newItemsRated = { ...savedRatings, ...itemsRated };
38
+ if (JSON.stringify(newItemsRated) !== JSON.stringify(itemsRated)) {
39
+ setItemsRated(newItemsRated);
40
+ }
41
+ }, [savedRatings]);
42
+ const checkThumb = (thumbValue) => {
43
+ if (!itemsRated[ratedItem]) {
44
+ return false;
45
+ }
46
+ return itemsRated[ratedItem] === thumbValue;
47
+ };
48
+ const rateAI = (isThumbsUp) => {
49
+ const aiRating = isThumbsUp ? 'thumbs-up' : 'thumbs-down';
50
+ if (!checkThumb(aiRating)) {
51
+ setItemsRated({
52
+ ...itemsRated,
53
+ [ratedItem]: aiRating,
54
+ });
55
+ onRate?.(aiRating);
56
+ tracks.recordEvent('jetpack_ai_feedback', {
57
+ type: feature,
58
+ rating: aiRating,
59
+ mediaLibraryId: options.mediaLibraryId || null,
60
+ prompt: options.prompt || null,
61
+ revisedPrompt: options.revisedPrompt || null,
62
+ });
63
+ }
64
+ };
65
+ return (_jsxs("div", { className: "ai-assistant-feedback__selection", children: [_jsx(Tooltip, { text: __('I like this', 'jetpack-ai-client'), children: _jsx(Button, { disabled: disabled, icon: thumbsUp, onClick: () => rateAI(true), iconSize: iconSize, showTooltip: false, className: clsx({
66
+ 'ai-assistant-feedback__thumb-selected': checkThumb('thumbs-up'),
67
+ }) }) }), _jsx(Tooltip, { text: __("I don't find this useful", 'jetpack-ai-client'), children: _jsx(Button, { disabled: disabled, icon: thumbsDown, onClick: () => rateAI(false), iconSize: iconSize, showTooltip: false, className: clsx({
68
+ 'ai-assistant-feedback__thumb-selected': checkThumb('thumbs-down'),
69
+ }) }) })] }));
70
+ }
@@ -1,4 +1,5 @@
1
1
  export { AIControl, BlockAIControl, ExtensionAIControl } from './ai-control/index.js';
2
+ export { default as AiFeedbackThumbs } from './ai-feedback/index.js';
2
3
  export { default as AiStatusIndicator } from './ai-status-indicator/index.js';
3
4
  export { default as AudioDurationDisplay } from './audio-duration-display/index.js';
4
5
  export { default as AiModalFooter } from './ai-modal-footer/index.js';
@@ -1,4 +1,5 @@
1
1
  export { AIControl, BlockAIControl, ExtensionAIControl } from './ai-control/index.js';
2
+ export { default as AiFeedbackThumbs } from './ai-feedback/index.js';
2
3
  export { default as AiStatusIndicator } from './ai-status-indicator/index.js';
3
4
  export { default as AudioDurationDisplay } from './audio-duration-display/index.js';
4
5
  export { default as AiModalFooter } from './ai-modal-footer/index.js';
@@ -5,7 +5,7 @@ type FairUsageNoticeProps = {
5
5
  * The fair usage notice component.
6
6
  * @param {FairUsageNoticeProps} props - Fair usage notice component props.
7
7
  * @param {FairUsageNoticeProps.variant} props.variant - The variant of the notice to render.
8
- * @return {ReactElement} the Notice component with the fair usage message.
8
+ * @return the Notice component with the fair usage message.
9
9
  */
10
10
  export declare const FairUsageNotice: ({ variant }: FairUsageNoticeProps) => import("react/jsx-runtime").JSX.Element;
11
11
  export {};
@@ -5,7 +5,7 @@ import useFairUsageNoticeMessage from '../hooks/use-fair-usage-notice-message.js
5
5
  * The fair usage notice component.
6
6
  * @param {FairUsageNoticeProps} props - Fair usage notice component props.
7
7
  * @param {FairUsageNoticeProps.variant} props.variant - The variant of the notice to render.
8
- * @return {ReactElement} the Notice component with the fair usage message.
8
+ * @return the Notice component with the fair usage message.
9
9
  */
10
10
  export const FairUsageNotice = ({ variant = 'error' }) => {
11
11
  const useFairUsageNoticeMessageElement = useFairUsageNoticeMessage();
@@ -10,6 +10,7 @@ import debugFactory from 'debug';
10
10
  /**
11
11
  * Internal dependencies
12
12
  */
13
+ import AiFeedbackThumbs from '../../components/ai-feedback/index.js';
13
14
  import CheckIcon from '../assets/icons/check.js';
14
15
  import LogoIcon from '../assets/icons/logo.js';
15
16
  import MediaIcon from '../assets/icons/media.js';
@@ -80,8 +81,31 @@ const LogoFetching = () => {
80
81
  const LogoEmpty = () => {
81
82
  return (_jsxs(_Fragment, { children: [_jsx("div", { style: { width: 0, height: '229px' } }), _jsx("span", { className: "jetpack-ai-logo-generator-modal-presenter__loading-text", children: __('Once you generate a logo, it will show up here', 'jetpack-ai-client') })] }));
82
83
  };
84
+ const RateLogo = ({ disabled, ratedItem, onRate }) => {
85
+ const { logos, selectedLogo } = useLogoGenerator();
86
+ const savedRatings = logos
87
+ .filter(logo => logo.rating)
88
+ .reduce((acc, logo) => {
89
+ acc[logo.url] = logo.rating;
90
+ return acc;
91
+ }, {});
92
+ return (_jsx(AiFeedbackThumbs, { disabled: disabled, ratedItem: ratedItem, feature: "logo-generator", savedRatings: savedRatings, options: {
93
+ mediaLibraryId: selectedLogo.mediaId,
94
+ prompt: selectedLogo.description,
95
+ }, onRate: onRate }));
96
+ };
83
97
  const LogoReady = ({ siteId, logo, onApplyLogo }) => {
84
- return (_jsxs(_Fragment, { children: [_jsx("img", { src: logo.url, alt: logo.description, className: "jetpack-ai-logo-generator-modal-presenter__logo" }), _jsxs("div", { className: "jetpack-ai-logo-generator-modal-presenter__action-wrapper", children: [_jsx("span", { className: "jetpack-ai-logo-generator-modal-presenter__description", children: logo.description }), _jsxs("div", { className: "jetpack-ai-logo-generator-modal-presenter__actions", children: [_jsx(SaveInLibraryButton, { siteId: siteId }), _jsx(UseOnSiteButton, { onApplyLogo: onApplyLogo })] })] })] }));
98
+ const handleRateLogo = (rating) => {
99
+ // Update localStorage
100
+ updateLogo({
101
+ siteId,
102
+ url: logo.url,
103
+ newUrl: logo.url,
104
+ mediaId: logo.mediaId,
105
+ rating,
106
+ });
107
+ };
108
+ return (_jsxs(_Fragment, { children: [_jsx("img", { src: logo.url, alt: logo.description, className: "jetpack-ai-logo-generator-modal-presenter__logo" }), _jsxs("div", { className: "jetpack-ai-logo-generator-modal-presenter__action-wrapper", children: [_jsx("span", { className: "jetpack-ai-logo-generator-modal-presenter__description", children: logo.description }), _jsxs("div", { className: "jetpack-ai-logo-generator-modal-presenter__actions", children: [_jsx(SaveInLibraryButton, { siteId: siteId }), _jsx(UseOnSiteButton, { onApplyLogo: onApplyLogo }), _jsx(RateLogo, { ratedItem: logo.url, disabled: false, onRate: handleRateLogo })] })] })] }));
85
109
  };
86
110
  const LogoUpdated = ({ logo }) => {
87
111
  return (_jsxs(_Fragment, { children: [_jsx("img", { src: logo.url, alt: logo.description, className: "jetpack-ai-logo-generator-modal-presenter__logo" }), _jsxs("div", { className: "jetpack-ai-logo-generator-modal-presenter__success-wrapper", children: [_jsx(Icon, { icon: _jsx(CheckIcon, {}) }), _jsx("span", { children: __('Your new logo was set to the block!', 'jetpack-ai-client') })] })] }));
@@ -1,3 +1,3 @@
1
- /// <reference types="react" resolution-mode="require"/>
2
- declare const useFairUsageNoticeMessage: () => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
1
+ import { type Element } from '@wordpress/element';
2
+ declare const useFairUsageNoticeMessage: () => Element;
3
3
  export default useFairUsageNoticeMessage;
@@ -285,10 +285,12 @@ User request:${prompt}`;
285
285
  increaseAiAssistantRequestsCount(-logoGenerationCost);
286
286
  throw error;
287
287
  }
288
+ const revisedPrompt = image.data[0].revised_prompt || null;
288
289
  // response_format=url returns object with url, otherwise b64_json
289
290
  const logo = {
290
291
  url: 'data:image/png;base64,' + image.data[0].b64_json,
291
292
  description: prompt,
293
+ revisedPrompt,
292
294
  };
293
295
  try {
294
296
  const savedLogo = await saveLogo(logo);
@@ -296,6 +298,7 @@ User request:${prompt}`;
296
298
  url: savedLogo.mediaURL,
297
299
  description: prompt,
298
300
  mediaId: savedLogo.mediaId,
301
+ revisedPrompt,
299
302
  });
300
303
  }
301
304
  catch (error) {
@@ -6,15 +6,15 @@ import { RemoveFromStorageProps, SaveToStorageProps, UpdateInStorageProps } from
6
6
  /**
7
7
  * Add an entry to the site's logo history.
8
8
  *
9
- * @param {SaveToStorageProps} saveToStorageProps - The properties to save to storage
10
- * @param {SaveToStorageProps.siteId} saveToStorageProps.siteId - The site ID
11
- * @param {SaveToStorageProps.url} saveToStorageProps.url - The URL of the logo
12
- * @param {SaveToStorageProps.description} saveToStorageProps.description - The description of the logo, based on the prompt used to generate it
13
- * @param {SaveToStorageProps.mediaId} saveToStorageProps.mediaId - The media ID of the logo on the backend
14
- *
9
+ * @param {SaveToStorageProps} saveToStorageProps - The properties to save to storage
10
+ * @param {SaveToStorageProps.siteId} saveToStorageProps.siteId - The site ID
11
+ * @param {SaveToStorageProps.url} saveToStorageProps.url - The URL of the logo
12
+ * @param {SaveToStorageProps.description} saveToStorageProps.description - The description of the logo, based on the prompt used to generate it
13
+ * @param {SaveToStorageProps.mediaId} saveToStorageProps.mediaId - The media ID of the logo on the backend
14
+ * @param {SaveToStorageProps.revisedPrompt} saveToStorageProps.revisedPrompt - The revised prompt of the logo
15
15
  * @return {Logo} The logo that was saved
16
16
  */
17
- export declare function stashLogo({ siteId, url, description, mediaId }: SaveToStorageProps): Logo;
17
+ export declare function stashLogo({ siteId, url, description, mediaId, revisedPrompt, }: SaveToStorageProps): Logo;
18
18
  /**
19
19
  * Update an entry in the site's logo history.
20
20
  *
@@ -23,9 +23,10 @@ export declare function stashLogo({ siteId, url, description, mediaId }: SaveToS
23
23
  * @param {UpdateInStorageProps.url} updateInStorageProps.url - The URL of the logo to update
24
24
  * @param {UpdateInStorageProps.newUrl} updateInStorageProps.newUrl - The new URL of the logo
25
25
  * @param {UpdateInStorageProps.mediaId} updateInStorageProps.mediaId - The new media ID of the logo
26
+ * @param {UpdateInStorageProps.rating} updateInStorageProps.rating - The new rating of the logo
26
27
  * @return {Logo} The logo that was updated
27
28
  */
28
- export declare function updateLogo({ siteId, url, newUrl, mediaId }: UpdateInStorageProps): Logo;
29
+ export declare function updateLogo({ siteId, url, newUrl, mediaId, rating }: UpdateInStorageProps): Logo;
29
30
  /**
30
31
  * Get the logo history for a site.
31
32
  *
@@ -3,20 +3,21 @@ const MAX_LOGOS = 10;
3
3
  /**
4
4
  * Add an entry to the site's logo history.
5
5
  *
6
- * @param {SaveToStorageProps} saveToStorageProps - The properties to save to storage
7
- * @param {SaveToStorageProps.siteId} saveToStorageProps.siteId - The site ID
8
- * @param {SaveToStorageProps.url} saveToStorageProps.url - The URL of the logo
9
- * @param {SaveToStorageProps.description} saveToStorageProps.description - The description of the logo, based on the prompt used to generate it
10
- * @param {SaveToStorageProps.mediaId} saveToStorageProps.mediaId - The media ID of the logo on the backend
11
- *
6
+ * @param {SaveToStorageProps} saveToStorageProps - The properties to save to storage
7
+ * @param {SaveToStorageProps.siteId} saveToStorageProps.siteId - The site ID
8
+ * @param {SaveToStorageProps.url} saveToStorageProps.url - The URL of the logo
9
+ * @param {SaveToStorageProps.description} saveToStorageProps.description - The description of the logo, based on the prompt used to generate it
10
+ * @param {SaveToStorageProps.mediaId} saveToStorageProps.mediaId - The media ID of the logo on the backend
11
+ * @param {SaveToStorageProps.revisedPrompt} saveToStorageProps.revisedPrompt - The revised prompt of the logo
12
12
  * @return {Logo} The logo that was saved
13
13
  */
14
- export function stashLogo({ siteId, url, description, mediaId }) {
14
+ export function stashLogo({ siteId, url, description, mediaId, revisedPrompt, }) {
15
15
  const storedContent = getSiteLogoHistory(siteId);
16
16
  const logo = {
17
17
  url,
18
18
  description,
19
19
  mediaId,
20
+ revisedPrompt,
20
21
  };
21
22
  storedContent.push(logo);
22
23
  localStorage.setItem(`logo-history-${siteId}`, JSON.stringify(storedContent.slice(-MAX_LOGOS)));
@@ -30,14 +31,16 @@ export function stashLogo({ siteId, url, description, mediaId }) {
30
31
  * @param {UpdateInStorageProps.url} updateInStorageProps.url - The URL of the logo to update
31
32
  * @param {UpdateInStorageProps.newUrl} updateInStorageProps.newUrl - The new URL of the logo
32
33
  * @param {UpdateInStorageProps.mediaId} updateInStorageProps.mediaId - The new media ID of the logo
34
+ * @param {UpdateInStorageProps.rating} updateInStorageProps.rating - The new rating of the logo
33
35
  * @return {Logo} The logo that was updated
34
36
  */
35
- export function updateLogo({ siteId, url, newUrl, mediaId }) {
37
+ export function updateLogo({ siteId, url, newUrl, mediaId, rating }) {
36
38
  const storedContent = getSiteLogoHistory(siteId);
37
39
  const index = storedContent.findIndex(logo => logo.url === url);
38
40
  if (index > -1) {
39
41
  storedContent[index].url = newUrl;
40
42
  storedContent[index].mediaId = mediaId;
43
+ storedContent[index].rating = rating;
41
44
  }
42
45
  localStorage.setItem(`logo-history-${siteId}`, JSON.stringify(storedContent.slice(-MAX_LOGOS)));
43
46
  return storedContent[index];
@@ -68,6 +71,7 @@ export function getSiteLogoHistory(siteId) {
68
71
  url: logo.url,
69
72
  description: logo.description,
70
73
  mediaId: logo.mediaId,
74
+ rating: logo.rating,
71
75
  }));
72
76
  return storedContent;
73
77
  }
@@ -1,4 +1,4 @@
1
- import type { AiFeatureStateProps, LogoGeneratorStateProp, RequestError } from './types.js';
1
+ import type { AiFeatureStateProps, RequestError } from './types.js';
2
2
  import type { SiteDetails } from '../types.js';
3
3
  /**
4
4
  * Reducer for the Logo Generator store.
@@ -27,7 +27,7 @@ import type { SiteDetails } from '../types.js';
27
27
  * @param {boolean} action.isLoadingHistory - Whether the history is being loaded
28
28
  * @return {LogoGeneratorStateProp} The new state
29
29
  */
30
- export default function reducer(state: LogoGeneratorStateProp, action: {
30
+ export default function reducer(state: import("./types.js").LogoGeneratorStateProp, action: {
31
31
  type: string;
32
32
  feature?: AiFeatureStateProps;
33
33
  count?: number;
@@ -99,6 +99,8 @@ export type Logo = {
99
99
  url: string;
100
100
  description: string;
101
101
  mediaId?: number;
102
+ rating?: string;
103
+ revisedPrompt?: string;
102
104
  };
103
105
  export type RequestError = string | Error | null;
104
106
  export type LogoGeneratorStateProp = {
@@ -78,6 +78,7 @@ export type UpdateInStorageProps = {
78
78
  url: Logo['url'];
79
79
  newUrl: Logo['url'];
80
80
  mediaId: Logo['mediaId'];
81
+ rating?: Logo['rating'];
81
82
  };
82
83
  export type RemoveFromStorageProps = {
83
84
  mediaId: Logo['mediaId'];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@automattic/jetpack-ai-client",
4
- "version": "0.25.1",
4
+ "version": "0.25.3",
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": {
@@ -44,21 +44,22 @@
44
44
  "main": "./build/index.js",
45
45
  "types": "./build/index.d.ts",
46
46
  "dependencies": {
47
- "@automattic/jetpack-base-styles": "^0.6.38",
48
- "@automattic/jetpack-connection": "^0.36.1",
49
- "@automattic/jetpack-shared-extension-utils": "^0.16.1",
47
+ "@automattic/jetpack-base-styles": "^0.6.39",
48
+ "@automattic/jetpack-connection": "^0.36.2",
49
+ "@automattic/jetpack-shared-extension-utils": "^0.16.2",
50
50
  "@microsoft/fetch-event-source": "2.0.1",
51
51
  "@types/react": "18.3.12",
52
52
  "@types/wordpress__block-editor": "11.5.15",
53
- "@wordpress/api-fetch": "7.13.0",
54
- "@wordpress/blob": "4.13.0",
55
- "@wordpress/block-editor": "14.8.0",
56
- "@wordpress/components": "28.13.0",
57
- "@wordpress/compose": "7.13.0",
58
- "@wordpress/data": "10.13.0",
59
- "@wordpress/element": "6.13.0",
60
- "@wordpress/i18n": "5.13.0",
61
- "@wordpress/icons": "10.13.0",
53
+ "@wordpress/api-fetch": "7.14.0",
54
+ "@wordpress/base-styles": "5.14.0",
55
+ "@wordpress/blob": "4.14.0",
56
+ "@wordpress/block-editor": "14.9.0",
57
+ "@wordpress/components": "29.0.0",
58
+ "@wordpress/compose": "7.14.0",
59
+ "@wordpress/data": "10.14.0",
60
+ "@wordpress/element": "6.14.0",
61
+ "@wordpress/i18n": "5.14.0",
62
+ "@wordpress/icons": "10.14.0",
62
63
  "clsx": "2.1.1",
63
64
  "debug": "4.3.4",
64
65
  "markdown-it": "14.0.0",
@@ -0,0 +1,133 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import {
5
+ useAnalytics,
6
+ getJetpackExtensionAvailability,
7
+ } from '@automattic/jetpack-shared-extension-utils';
8
+ import { Button, Tooltip } from '@wordpress/components';
9
+ import { useEffect, useState } from '@wordpress/element';
10
+ import { __ } from '@wordpress/i18n';
11
+ import { thumbsUp, thumbsDown } from '@wordpress/icons';
12
+ import clsx from 'clsx';
13
+ /*
14
+ * Internal dependencies
15
+ */
16
+ import './style.scss';
17
+ /**
18
+ * Types
19
+ */
20
+ import type React from 'react';
21
+
22
+ type AiFeedbackThumbsProps = {
23
+ disabled?: boolean;
24
+ iconSize?: number;
25
+ ratedItem?: string;
26
+ feature?: string;
27
+ savedRatings?: Record< string, string >;
28
+ options?: {
29
+ mediaLibraryId?: number;
30
+ prompt?: string;
31
+ revisedPrompt?: string;
32
+ };
33
+ onRate?: ( rating: string ) => void;
34
+ };
35
+
36
+ /**
37
+ * Get the availability of a feature.
38
+ *
39
+ * @param {string} feature - The feature to check availability for.
40
+ * @return {boolean} - Whether the feature is available.
41
+ */
42
+ function getFeatureAvailability( feature: string ): boolean {
43
+ return getJetpackExtensionAvailability( feature ).available === true;
44
+ }
45
+
46
+ /**
47
+ * AiFeedbackThumbs component.
48
+ *
49
+ * @param {AiFeedbackThumbsProps} props - component props.
50
+ * @return {React.ReactElement} - rendered component.
51
+ */
52
+ export default function AiFeedbackThumbs( {
53
+ disabled = false,
54
+ iconSize = 24,
55
+ ratedItem = '',
56
+ feature = '',
57
+ savedRatings = {},
58
+ options = {},
59
+ onRate,
60
+ }: AiFeedbackThumbsProps ): React.ReactElement {
61
+ if ( ! getFeatureAvailability( 'ai-response-feedback' ) ) {
62
+ return null;
63
+ }
64
+
65
+ const [ itemsRated, setItemsRated ] = useState( {} );
66
+ const { tracks } = useAnalytics();
67
+
68
+ useEffect( () => {
69
+ const newItemsRated = { ...savedRatings, ...itemsRated };
70
+
71
+ if ( JSON.stringify( newItemsRated ) !== JSON.stringify( itemsRated ) ) {
72
+ setItemsRated( newItemsRated );
73
+ }
74
+ }, [ savedRatings ] );
75
+
76
+ const checkThumb = ( thumbValue: string ) => {
77
+ if ( ! itemsRated[ ratedItem ] ) {
78
+ return false;
79
+ }
80
+
81
+ return itemsRated[ ratedItem ] === thumbValue;
82
+ };
83
+
84
+ const rateAI = ( isThumbsUp: boolean ) => {
85
+ const aiRating = isThumbsUp ? 'thumbs-up' : 'thumbs-down';
86
+
87
+ if ( ! checkThumb( aiRating ) ) {
88
+ setItemsRated( {
89
+ ...itemsRated,
90
+ [ ratedItem ]: aiRating,
91
+ } );
92
+
93
+ onRate?.( aiRating );
94
+
95
+ tracks.recordEvent( 'jetpack_ai_feedback', {
96
+ type: feature,
97
+ rating: aiRating,
98
+ mediaLibraryId: options.mediaLibraryId || null,
99
+ prompt: options.prompt || null,
100
+ revisedPrompt: options.revisedPrompt || null,
101
+ } );
102
+ }
103
+ };
104
+
105
+ return (
106
+ <div className="ai-assistant-feedback__selection">
107
+ <Tooltip text={ __( 'I like this', 'jetpack-ai-client' ) }>
108
+ <Button
109
+ disabled={ disabled }
110
+ icon={ thumbsUp }
111
+ onClick={ () => rateAI( true ) }
112
+ iconSize={ iconSize }
113
+ showTooltip={ false }
114
+ className={ clsx( {
115
+ 'ai-assistant-feedback__thumb-selected': checkThumb( 'thumbs-up' ),
116
+ } ) }
117
+ />
118
+ </Tooltip>
119
+ <Tooltip text={ __( "I don't find this useful", 'jetpack-ai-client' ) }>
120
+ <Button
121
+ disabled={ disabled }
122
+ icon={ thumbsDown }
123
+ onClick={ () => rateAI( false ) }
124
+ iconSize={ iconSize }
125
+ showTooltip={ false }
126
+ className={ clsx( {
127
+ 'ai-assistant-feedback__thumb-selected': checkThumb( 'thumbs-down' ),
128
+ } ) }
129
+ />
130
+ </Tooltip>
131
+ </div>
132
+ );
133
+ }
@@ -0,0 +1,16 @@
1
+ @import "@wordpress/base-styles/colors";
2
+
3
+ .ai-assistant-feedback {
4
+ &__selection {
5
+ display: flex;
6
+
7
+ .components-button.has-icon {
8
+ padding: 0;
9
+ min-width: 28px;
10
+ }
11
+ }
12
+
13
+ &__thumb-selected {
14
+ color: var( --wp-components-color-accent, var( --wp-admin-theme-color, #3858e9 ) );
15
+ }
16
+ }
@@ -1,4 +1,5 @@
1
1
  export { AIControl, BlockAIControl, ExtensionAIControl } from './ai-control/index.js';
2
+ export { default as AiFeedbackThumbs } from './ai-feedback/index.js';
2
3
  export { default as AiStatusIndicator } from './ai-status-indicator/index.js';
3
4
  export { default as AudioDurationDisplay } from './audio-duration-display/index.js';
4
5
  export { default as AiModalFooter } from './ai-modal-footer/index.js';
@@ -1,9 +1,5 @@
1
1
  import { Notice } from '@wordpress/components';
2
2
  import useFairUsageNoticeMessage from '../hooks/use-fair-usage-notice-message.js';
3
- /**
4
- * Types
5
- */
6
- import type { ReactElement } from 'react';
7
3
 
8
4
  type FairUsageNoticeProps = {
9
5
  variant?: 'error' | 'muted';
@@ -13,7 +9,7 @@ type FairUsageNoticeProps = {
13
9
  * The fair usage notice component.
14
10
  * @param {FairUsageNoticeProps} props - Fair usage notice component props.
15
11
  * @param {FairUsageNoticeProps.variant} props.variant - The variant of the notice to render.
16
- * @return {ReactElement} the Notice component with the fair usage message.
12
+ * @return the Notice component with the fair usage message.
17
13
  */
18
14
  export const FairUsageNotice = ( { variant = 'error' }: FairUsageNoticeProps ) => {
19
15
  const useFairUsageNoticeMessageElement = useFairUsageNoticeMessage();
@@ -9,6 +9,7 @@ import debugFactory from 'debug';
9
9
  /**
10
10
  * Internal dependencies
11
11
  */
12
+ import AiFeedbackThumbs from '../../components/ai-feedback/index.js';
12
13
  import CheckIcon from '../assets/icons/check.js';
13
14
  import LogoIcon from '../assets/icons/logo.js';
14
15
  import MediaIcon from '../assets/icons/media.js';
@@ -152,11 +153,50 @@ const LogoEmpty: React.FC = () => {
152
153
  );
153
154
  };
154
155
 
156
+ const RateLogo: React.FC< {
157
+ disabled: boolean;
158
+ ratedItem: string;
159
+ onRate: ( rating: string ) => void;
160
+ } > = ( { disabled, ratedItem, onRate } ) => {
161
+ const { logos, selectedLogo } = useLogoGenerator();
162
+ const savedRatings = logos
163
+ .filter( logo => logo.rating )
164
+ .reduce( ( acc, logo ) => {
165
+ acc[ logo.url ] = logo.rating;
166
+ return acc;
167
+ }, {} );
168
+
169
+ return (
170
+ <AiFeedbackThumbs
171
+ disabled={ disabled }
172
+ ratedItem={ ratedItem }
173
+ feature="logo-generator"
174
+ savedRatings={ savedRatings }
175
+ options={ {
176
+ mediaLibraryId: selectedLogo.mediaId,
177
+ prompt: selectedLogo.description,
178
+ } }
179
+ onRate={ onRate }
180
+ />
181
+ );
182
+ };
183
+
155
184
  const LogoReady: React.FC< {
156
185
  siteId: string;
157
186
  logo: Logo;
158
187
  onApplyLogo: ( mediaId: number ) => void;
159
188
  } > = ( { siteId, logo, onApplyLogo } ) => {
189
+ const handleRateLogo = ( rating: string ) => {
190
+ // Update localStorage
191
+ updateLogo( {
192
+ siteId,
193
+ url: logo.url,
194
+ newUrl: logo.url,
195
+ mediaId: logo.mediaId,
196
+ rating,
197
+ } );
198
+ };
199
+
160
200
  return (
161
201
  <>
162
202
  <img
@@ -171,6 +211,7 @@ const LogoReady: React.FC< {
171
211
  <div className="jetpack-ai-logo-generator-modal-presenter__actions">
172
212
  <SaveInLibraryButton siteId={ siteId } />
173
213
  <UseOnSiteButton onApplyLogo={ onApplyLogo } />
214
+ <RateLogo ratedItem={ logo.url } disabled={ false } onRate={ handleRateLogo } />
174
215
  </div>
175
216
  </div>
176
217
  </>
@@ -1,5 +1,5 @@
1
1
  import { useSelect } from '@wordpress/data';
2
- import { createInterpolateElement } from '@wordpress/element';
2
+ import { createInterpolateElement, type Element } from '@wordpress/element';
3
3
  import { __, sprintf } from '@wordpress/i18n';
4
4
  import getRedirectUrl from '../../../../components/tools/jp-redirect/index.js';
5
5
  /**
@@ -11,7 +11,7 @@ import { STORE_NAME } from '../store/index.js';
11
11
  */
12
12
  import type { Selectors } from '../store/types.js';
13
13
 
14
- const useFairUsageNoticeMessage = () => {
14
+ const useFairUsageNoticeMessage = (): Element => {
15
15
  const { usagePeriod } = useSelect( select => {
16
16
  const selectors: Selectors = select( STORE_NAME );
17
17
  return {
@@ -412,10 +412,13 @@ User request:${ prompt }`;
412
412
  throw error;
413
413
  }
414
414
 
415
+ const revisedPrompt = image.data[ 0 ].revised_prompt || null;
416
+
415
417
  // response_format=url returns object with url, otherwise b64_json
416
418
  const logo: Logo = {
417
419
  url: 'data:image/png;base64,' + image.data[ 0 ].b64_json,
418
420
  description: prompt,
421
+ revisedPrompt,
419
422
  };
420
423
 
421
424
  try {
@@ -424,6 +427,7 @@ User request:${ prompt }`;
424
427
  url: savedLogo.mediaURL,
425
428
  description: prompt,
426
429
  mediaId: savedLogo.mediaId,
430
+ revisedPrompt,
427
431
  } );
428
432
  } catch ( error ) {
429
433
  storeLogo( logo );
@@ -10,21 +10,28 @@ const MAX_LOGOS = 10;
10
10
  /**
11
11
  * Add an entry to the site's logo history.
12
12
  *
13
- * @param {SaveToStorageProps} saveToStorageProps - The properties to save to storage
14
- * @param {SaveToStorageProps.siteId} saveToStorageProps.siteId - The site ID
15
- * @param {SaveToStorageProps.url} saveToStorageProps.url - The URL of the logo
16
- * @param {SaveToStorageProps.description} saveToStorageProps.description - The description of the logo, based on the prompt used to generate it
17
- * @param {SaveToStorageProps.mediaId} saveToStorageProps.mediaId - The media ID of the logo on the backend
18
- *
13
+ * @param {SaveToStorageProps} saveToStorageProps - The properties to save to storage
14
+ * @param {SaveToStorageProps.siteId} saveToStorageProps.siteId - The site ID
15
+ * @param {SaveToStorageProps.url} saveToStorageProps.url - The URL of the logo
16
+ * @param {SaveToStorageProps.description} saveToStorageProps.description - The description of the logo, based on the prompt used to generate it
17
+ * @param {SaveToStorageProps.mediaId} saveToStorageProps.mediaId - The media ID of the logo on the backend
18
+ * @param {SaveToStorageProps.revisedPrompt} saveToStorageProps.revisedPrompt - The revised prompt of the logo
19
19
  * @return {Logo} The logo that was saved
20
20
  */
21
- export function stashLogo( { siteId, url, description, mediaId }: SaveToStorageProps ) {
21
+ export function stashLogo( {
22
+ siteId,
23
+ url,
24
+ description,
25
+ mediaId,
26
+ revisedPrompt,
27
+ }: SaveToStorageProps ) {
22
28
  const storedContent = getSiteLogoHistory( siteId );
23
29
 
24
30
  const logo: Logo = {
25
31
  url,
26
32
  description,
27
33
  mediaId,
34
+ revisedPrompt,
28
35
  };
29
36
 
30
37
  storedContent.push( logo );
@@ -45,9 +52,10 @@ export function stashLogo( { siteId, url, description, mediaId }: SaveToStorageP
45
52
  * @param {UpdateInStorageProps.url} updateInStorageProps.url - The URL of the logo to update
46
53
  * @param {UpdateInStorageProps.newUrl} updateInStorageProps.newUrl - The new URL of the logo
47
54
  * @param {UpdateInStorageProps.mediaId} updateInStorageProps.mediaId - The new media ID of the logo
55
+ * @param {UpdateInStorageProps.rating} updateInStorageProps.rating - The new rating of the logo
48
56
  * @return {Logo} The logo that was updated
49
57
  */
50
- export function updateLogo( { siteId, url, newUrl, mediaId }: UpdateInStorageProps ) {
58
+ export function updateLogo( { siteId, url, newUrl, mediaId, rating }: UpdateInStorageProps ) {
51
59
  const storedContent = getSiteLogoHistory( siteId );
52
60
 
53
61
  const index = storedContent.findIndex( logo => logo.url === url );
@@ -55,6 +63,7 @@ export function updateLogo( { siteId, url, newUrl, mediaId }: UpdateInStoragePro
55
63
  if ( index > -1 ) {
56
64
  storedContent[ index ].url = newUrl;
57
65
  storedContent[ index ].mediaId = mediaId;
66
+ storedContent[ index ].rating = rating;
58
67
  }
59
68
 
60
69
  localStorage.setItem(
@@ -96,6 +105,7 @@ export function getSiteLogoHistory( siteId: string ) {
96
105
  url: logo.url,
97
106
  description: logo.description,
98
107
  mediaId: logo.mediaId,
108
+ rating: logo.rating,
99
109
  } ) );
100
110
 
101
111
  return storedContent;
@@ -30,12 +30,7 @@ import {
30
30
  ACTION_SET_IS_LOADING_HISTORY,
31
31
  } from './constants.js';
32
32
  import INITIAL_STATE from './initial-state.js';
33
- import type {
34
- AiFeatureStateProps,
35
- LogoGeneratorStateProp,
36
- RequestError,
37
- TierLimitProp,
38
- } from './types.js';
33
+ import type { AiFeatureStateProps, RequestError, TierLimitProp } from './types.js';
39
34
  import type { SiteDetails } from '../types.js';
40
35
 
41
36
  /**
@@ -140,6 +140,8 @@ export type Logo = {
140
140
  url: string;
141
141
  description: string;
142
142
  mediaId?: number;
143
+ rating?: string;
144
+ revisedPrompt?: string;
143
145
  };
144
146
 
145
147
  export type RequestError = string | Error | null;
@@ -92,6 +92,7 @@ export type UpdateInStorageProps = {
92
92
  url: Logo[ 'url' ];
93
93
  newUrl: Logo[ 'url' ];
94
94
  mediaId: Logo[ 'mediaId' ];
95
+ rating?: Logo[ 'rating' ];
95
96
  };
96
97
 
97
98
  export type RemoveFromStorageProps = {