@automattic/jetpack-ai-client 0.25.2 → 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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,10 @@ 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
+
8
12
  ## [0.25.2] - 2024-12-16
9
13
  ### Changed
10
14
  - Updated package dependencies. [#40564]
@@ -483,6 +487,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
483
487
  - AI Client: stop using smart document visibility handling on the fetchEventSource library, so it does not restart the completion when changing tabs. [#32004]
484
488
  - Updated package dependencies. [#31468] [#31659] [#31785]
485
489
 
490
+ [0.25.3]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.2...v0.25.3
486
491
  [0.25.2]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.1...v0.25.2
487
492
  [0.25.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.0...v0.25.1
488
493
  [0.25.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.24.3...v0.25.0
@@ -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';
@@ -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') })] })] }));
@@ -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
  }
@@ -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.2",
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": {
@@ -51,6 +51,7 @@
51
51
  "@types/react": "18.3.12",
52
52
  "@types/wordpress__block-editor": "11.5.15",
53
53
  "@wordpress/api-fetch": "7.14.0",
54
+ "@wordpress/base-styles": "5.14.0",
54
55
  "@wordpress/blob": "4.14.0",
55
56
  "@wordpress/block-editor": "14.9.0",
56
57
  "@wordpress/components": "29.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';
@@ -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
  </>
@@ -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;
@@ -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 = {