diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 480cdf0529f9e..2c0398584de7a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -138,6 +138,9 @@ importers:
'@wordpress/api-fetch':
specifier: 7.14.0
version: 7.14.0
+ '@wordpress/base-styles':
+ specifier: 5.14.0
+ version: 5.14.0
'@wordpress/blob':
specifier: 4.14.0
version: 4.14.0
diff --git a/projects/js-packages/ai-client/changelog/change-jetpack-ai-rate-logo-generator b/projects/js-packages/ai-client/changelog/change-jetpack-ai-rate-logo-generator
new file mode 100644
index 0000000000000..7050a2b22fc3a
--- /dev/null
+++ b/projects/js-packages/ai-client/changelog/change-jetpack-ai-rate-logo-generator
@@ -0,0 +1,4 @@
+Significance: patch
+Type: added
+
+Jetpack AI: Add thumbs up/down component to AI logo generator
diff --git a/projects/js-packages/ai-client/package.json b/projects/js-packages/ai-client/package.json
index f101983f9841d..30c9e9be2f2bf 100644
--- a/projects/js-packages/ai-client/package.json
+++ b/projects/js-packages/ai-client/package.json
@@ -52,6 +52,7 @@
"@types/react": "18.3.12",
"@types/wordpress__block-editor": "11.5.15",
"@wordpress/api-fetch": "7.14.0",
+ "@wordpress/base-styles": "5.14.0",
"@wordpress/blob": "4.14.0",
"@wordpress/block-editor": "14.9.0",
"@wordpress/components": "29.0.0",
diff --git a/projects/js-packages/ai-client/src/components/ai-feedback/index.tsx b/projects/js-packages/ai-client/src/components/ai-feedback/index.tsx
new file mode 100644
index 0000000000000..a6f6ed67ebbf1
--- /dev/null
+++ b/projects/js-packages/ai-client/src/components/ai-feedback/index.tsx
@@ -0,0 +1,133 @@
+/**
+ * External dependencies
+ */
+import {
+ useAnalytics,
+ getJetpackExtensionAvailability,
+} from '@automattic/jetpack-shared-extension-utils';
+import { Button, Tooltip } from '@wordpress/components';
+import { useEffect, useState } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { thumbsUp, thumbsDown } from '@wordpress/icons';
+import clsx from 'clsx';
+/*
+ * Internal dependencies
+ */
+import './style.scss';
+/**
+ * Types
+ */
+import type React from 'react';
+
+type AiFeedbackThumbsProps = {
+ disabled?: boolean;
+ iconSize?: number;
+ ratedItem?: string;
+ feature?: string;
+ savedRatings?: Record< string, string >;
+ options?: {
+ mediaLibraryId?: number;
+ prompt?: string;
+ revisedPrompt?: string;
+ };
+ onRate?: ( rating: string ) => void;
+};
+
+/**
+ * Get the availability of a feature.
+ *
+ * @param {string} feature - The feature to check availability for.
+ * @return {boolean} - Whether the feature is available.
+ */
+function getFeatureAvailability( feature: string ): boolean {
+ return getJetpackExtensionAvailability( feature ).available === true;
+}
+
+/**
+ * AiFeedbackThumbs component.
+ *
+ * @param {AiFeedbackThumbsProps} props - component props.
+ * @return {React.ReactElement} - rendered component.
+ */
+export default function AiFeedbackThumbs( {
+ disabled = false,
+ iconSize = 24,
+ ratedItem = '',
+ feature = '',
+ savedRatings = {},
+ options = {},
+ onRate,
+}: AiFeedbackThumbsProps ): React.ReactElement {
+ if ( ! getFeatureAvailability( 'ai-response-feedback' ) ) {
+ return null;
+ }
+
+ const [ itemsRated, setItemsRated ] = useState( {} );
+ const { tracks } = useAnalytics();
+
+ useEffect( () => {
+ const newItemsRated = { ...savedRatings, ...itemsRated };
+
+ if ( JSON.stringify( newItemsRated ) !== JSON.stringify( itemsRated ) ) {
+ setItemsRated( newItemsRated );
+ }
+ }, [ savedRatings ] );
+
+ const checkThumb = ( thumbValue: string ) => {
+ if ( ! itemsRated[ ratedItem ] ) {
+ return false;
+ }
+
+ return itemsRated[ ratedItem ] === thumbValue;
+ };
+
+ const rateAI = ( isThumbsUp: boolean ) => {
+ const aiRating = isThumbsUp ? 'thumbs-up' : 'thumbs-down';
+
+ if ( ! checkThumb( aiRating ) ) {
+ setItemsRated( {
+ ...itemsRated,
+ [ ratedItem ]: aiRating,
+ } );
+
+ onRate?.( aiRating );
+
+ tracks.recordEvent( 'jetpack_ai_feedback', {
+ type: feature,
+ rating: aiRating,
+ mediaLibraryId: options.mediaLibraryId || null,
+ prompt: options.prompt || null,
+ revisedPrompt: options.revisedPrompt || null,
+ } );
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/style.scss b/projects/js-packages/ai-client/src/components/ai-feedback/style.scss
similarity index 70%
rename from projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/style.scss
rename to projects/js-packages/ai-client/src/components/ai-feedback/style.scss
index 9697d17aff1ad..89b8f9f2053bc 100644
--- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/style.scss
+++ b/projects/js-packages/ai-client/src/components/ai-feedback/style.scss
@@ -11,6 +11,6 @@
}
&__thumb-selected {
- color: var(--wp-components-color-accent,var(--wp-admin-theme-color,#3858e9));
+ color: var( --wp-components-color-accent, var( --wp-admin-theme-color, #3858e9 ) );
}
}
diff --git a/projects/js-packages/ai-client/src/components/index.ts b/projects/js-packages/ai-client/src/components/index.ts
index 20d5714db6baf..1ef8b40b80426 100644
--- a/projects/js-packages/ai-client/src/components/index.ts
+++ b/projects/js-packages/ai-client/src/components/index.ts
@@ -1,4 +1,5 @@
export { AIControl, BlockAIControl, ExtensionAIControl } from './ai-control/index.js';
+export { default as AiFeedbackThumbs } from './ai-feedback/index.js';
export { default as AiStatusIndicator } from './ai-status-indicator/index.js';
export { default as AudioDurationDisplay } from './audio-duration-display/index.js';
export { default as AiModalFooter } from './ai-modal-footer/index.js';
diff --git a/projects/js-packages/ai-client/src/logo-generator/components/logo-presenter.tsx b/projects/js-packages/ai-client/src/logo-generator/components/logo-presenter.tsx
index e539e1d751b6a..2b7e63ff9c9e6 100644
--- a/projects/js-packages/ai-client/src/logo-generator/components/logo-presenter.tsx
+++ b/projects/js-packages/ai-client/src/logo-generator/components/logo-presenter.tsx
@@ -9,6 +9,7 @@ import debugFactory from 'debug';
/**
* Internal dependencies
*/
+import AiFeedbackThumbs from '../../components/ai-feedback/index.js';
import CheckIcon from '../assets/icons/check.js';
import LogoIcon from '../assets/icons/logo.js';
import MediaIcon from '../assets/icons/media.js';
@@ -152,11 +153,50 @@ const LogoEmpty: React.FC = () => {
);
};
+const RateLogo: React.FC< {
+ disabled: boolean;
+ ratedItem: string;
+ onRate: ( rating: string ) => void;
+} > = ( { disabled, ratedItem, onRate } ) => {
+ const { logos, selectedLogo } = useLogoGenerator();
+ const savedRatings = logos
+ .filter( logo => logo.rating )
+ .reduce( ( acc, logo ) => {
+ acc[ logo.url ] = logo.rating;
+ return acc;
+ }, {} );
+
+ return (
+
+ );
+};
+
const LogoReady: React.FC< {
siteId: string;
logo: Logo;
onApplyLogo: ( mediaId: number ) => void;
} > = ( { siteId, logo, onApplyLogo } ) => {
+ const handleRateLogo = ( rating: string ) => {
+ // Update localStorage
+ updateLogo( {
+ siteId,
+ url: logo.url,
+ newUrl: logo.url,
+ mediaId: logo.mediaId,
+ rating,
+ } );
+ };
+
return (
<>
+
>
diff --git a/projects/js-packages/ai-client/src/logo-generator/hooks/use-logo-generator.ts b/projects/js-packages/ai-client/src/logo-generator/hooks/use-logo-generator.ts
index 70acbf3fe98e0..d2d232da900d0 100644
--- a/projects/js-packages/ai-client/src/logo-generator/hooks/use-logo-generator.ts
+++ b/projects/js-packages/ai-client/src/logo-generator/hooks/use-logo-generator.ts
@@ -412,10 +412,13 @@ User request:${ prompt }`;
throw error;
}
+ const revisedPrompt = image.data[ 0 ].revised_prompt || null;
+
// response_format=url returns object with url, otherwise b64_json
const logo: Logo = {
url: 'data:image/png;base64,' + image.data[ 0 ].b64_json,
description: prompt,
+ revisedPrompt,
};
try {
@@ -424,6 +427,7 @@ User request:${ prompt }`;
url: savedLogo.mediaURL,
description: prompt,
mediaId: savedLogo.mediaId,
+ revisedPrompt,
} );
} catch ( error ) {
storeLogo( logo );
diff --git a/projects/js-packages/ai-client/src/logo-generator/lib/logo-storage.ts b/projects/js-packages/ai-client/src/logo-generator/lib/logo-storage.ts
index 81e9b08319575..e6c6bb1176e83 100644
--- a/projects/js-packages/ai-client/src/logo-generator/lib/logo-storage.ts
+++ b/projects/js-packages/ai-client/src/logo-generator/lib/logo-storage.ts
@@ -10,21 +10,28 @@ const MAX_LOGOS = 10;
/**
* Add an entry to the site's logo history.
*
- * @param {SaveToStorageProps} saveToStorageProps - The properties to save to storage
- * @param {SaveToStorageProps.siteId} saveToStorageProps.siteId - The site ID
- * @param {SaveToStorageProps.url} saveToStorageProps.url - The URL of the logo
- * @param {SaveToStorageProps.description} saveToStorageProps.description - The description of the logo, based on the prompt used to generate it
- * @param {SaveToStorageProps.mediaId} saveToStorageProps.mediaId - The media ID of the logo on the backend
- *
+ * @param {SaveToStorageProps} saveToStorageProps - The properties to save to storage
+ * @param {SaveToStorageProps.siteId} saveToStorageProps.siteId - The site ID
+ * @param {SaveToStorageProps.url} saveToStorageProps.url - The URL of the logo
+ * @param {SaveToStorageProps.description} saveToStorageProps.description - The description of the logo, based on the prompt used to generate it
+ * @param {SaveToStorageProps.mediaId} saveToStorageProps.mediaId - The media ID of the logo on the backend
+ * @param {SaveToStorageProps.revisedPrompt} saveToStorageProps.revisedPrompt - The revised prompt of the logo
* @return {Logo} The logo that was saved
*/
-export function stashLogo( { siteId, url, description, mediaId }: SaveToStorageProps ) {
+export function stashLogo( {
+ siteId,
+ url,
+ description,
+ mediaId,
+ revisedPrompt,
+}: SaveToStorageProps ) {
const storedContent = getSiteLogoHistory( siteId );
const logo: Logo = {
url,
description,
mediaId,
+ revisedPrompt,
};
storedContent.push( logo );
@@ -45,9 +52,10 @@ export function stashLogo( { siteId, url, description, mediaId }: SaveToStorageP
* @param {UpdateInStorageProps.url} updateInStorageProps.url - The URL of the logo to update
* @param {UpdateInStorageProps.newUrl} updateInStorageProps.newUrl - The new URL of the logo
* @param {UpdateInStorageProps.mediaId} updateInStorageProps.mediaId - The new media ID of the logo
+ * @param {UpdateInStorageProps.rating} updateInStorageProps.rating - The new rating of the logo
* @return {Logo} The logo that was updated
*/
-export function updateLogo( { siteId, url, newUrl, mediaId }: UpdateInStorageProps ) {
+export function updateLogo( { siteId, url, newUrl, mediaId, rating }: UpdateInStorageProps ) {
const storedContent = getSiteLogoHistory( siteId );
const index = storedContent.findIndex( logo => logo.url === url );
@@ -55,6 +63,7 @@ export function updateLogo( { siteId, url, newUrl, mediaId }: UpdateInStoragePro
if ( index > -1 ) {
storedContent[ index ].url = newUrl;
storedContent[ index ].mediaId = mediaId;
+ storedContent[ index ].rating = rating;
}
localStorage.setItem(
@@ -96,6 +105,7 @@ export function getSiteLogoHistory( siteId: string ) {
url: logo.url,
description: logo.description,
mediaId: logo.mediaId,
+ rating: logo.rating,
} ) );
return storedContent;
diff --git a/projects/js-packages/ai-client/src/logo-generator/store/types.ts b/projects/js-packages/ai-client/src/logo-generator/store/types.ts
index b34f3b9f8a22e..d8c0f0fbcb1bf 100644
--- a/projects/js-packages/ai-client/src/logo-generator/store/types.ts
+++ b/projects/js-packages/ai-client/src/logo-generator/store/types.ts
@@ -140,6 +140,8 @@ export type Logo = {
url: string;
description: string;
mediaId?: number;
+ rating?: string;
+ revisedPrompt?: string;
};
export type RequestError = string | Error | null;
diff --git a/projects/js-packages/ai-client/src/logo-generator/types.ts b/projects/js-packages/ai-client/src/logo-generator/types.ts
index e54cf774ac98e..2a617a2b97d58 100644
--- a/projects/js-packages/ai-client/src/logo-generator/types.ts
+++ b/projects/js-packages/ai-client/src/logo-generator/types.ts
@@ -92,6 +92,7 @@ export type UpdateInStorageProps = {
url: Logo[ 'url' ];
newUrl: Logo[ 'url' ];
mediaId: Logo[ 'mediaId' ];
+ rating?: Logo[ 'rating' ];
};
export type RemoveFromStorageProps = {
diff --git a/projects/plugins/jetpack/changelog/change-jetpack-ai-rate-logo-generator b/projects/plugins/jetpack/changelog/change-jetpack-ai-rate-logo-generator
new file mode 100644
index 0000000000000..931f46e447031
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/change-jetpack-ai-rate-logo-generator
@@ -0,0 +1,4 @@
+Significance: patch
+Type: other
+
+Jetpack AI: Add thumbs up/down component to AI logo generator
diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/utils/get-feature-availability.ts b/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/utils/get-feature-availability.ts
index b18ea2a171d07..f6be56c958493 100644
--- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/utils/get-feature-availability.ts
+++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/utils/get-feature-availability.ts
@@ -3,6 +3,7 @@
*/
import { getJetpackExtensionAvailability } from '@automattic/jetpack-shared-extension-utils';
+// TODO: Move to the AI Client js-package
export function getFeatureAvailability( feature: string ): boolean {
return getJetpackExtensionAvailability( feature ).available === true;
}
diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/index.tsx
deleted file mode 100644
index fb99d2cb0fd0d..0000000000000
--- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/index.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { useAnalytics } from '@automattic/jetpack-shared-extension-utils';
-import { Button, Tooltip } from '@wordpress/components';
-import { __ } from '@wordpress/i18n';
-import { thumbsUp, thumbsDown } from '@wordpress/icons';
-import clsx from 'clsx';
-import { useState } from 'react';
-import { getFeatureAvailability } from '../../../../blocks/ai-assistant/lib/utils/get-feature-availability';
-
-import './style.scss';
-
-export default function AiFeedbackThumbs( {
- disabled = false,
- iconSize = 24,
- ratedItem,
- feature,
-} ) {
- const [ itemsRated, setItemsRated ] = useState( {} );
- const { tracks } = useAnalytics();
-
- const rateAI = ( isThumbsUp: boolean ) => {
- const aiRating = isThumbsUp ? 'thumbs-up' : 'thumbs-down';
-
- setItemsRated( {
- ...itemsRated,
- [ ratedItem ]: aiRating,
- } );
-
- tracks.recordEvent( 'jetpack_ai_feedback', {
- type: feature,
- rating: aiRating,
- } );
- };
-
- const checkThumb = ( thumbValue: string ) => {
- if ( ! itemsRated[ ratedItem ] ) {
- return false;
- }
-
- return itemsRated[ ratedItem ] === thumbValue;
- };
-
- return getFeatureAvailability( 'ai-response-feedback' ) ? (
-
-
-
-
-
-
- ) : (
- <>>
- );
-}
diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/components/carrousel.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/components/carrousel.tsx
index 97fc5acec4950..f9dec07e3cb2b 100644
--- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/components/carrousel.tsx
+++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/components/carrousel.tsx
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+import { AiFeedbackThumbs } from '@automattic/jetpack-ai-client';
import { Spinner } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { Icon, chevronLeft, chevronRight } from '@wordpress/icons';
@@ -8,13 +9,14 @@ import clsx from 'clsx';
/**
* Internal dependencies
*/
-import AiFeedbackThumbs from '../../ai-feedback';
import AiIcon from '../../ai-icon';
import './carrousel.scss';
export type CarrouselImageData = {
image?: string;
libraryId?: number | string;
+ prompt?: string;
+ revisedPrompt?: string;
libraryUrl?: string;
generating?: boolean;
error?: {
@@ -108,7 +110,7 @@ export default function Carrousel( {
{ images.length > 1 && prevButton }
- { images.map( ( { image, generating, error }, index ) => (
+ { images.map( ( { image, generating, error, revisedPrompt }, index ) => (
) : (
-
+
) }
>
) }
@@ -171,6 +177,11 @@ export default function Carrousel( {
disabled={ aiFeedbackDisabled( images[ current ] ) }
ratedItem={ images[ current ].libraryUrl || '' }
iconSize={ 20 }
+ options={ {
+ mediaLibraryId: Number( images[ current ].libraryId ),
+ prompt: images[ current ].prompt,
+ revisedPrompt: images[ current ].revisedPrompt,
+ } }
feature="image-generator"
/>
diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/hooks/use-ai-image.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/hooks/use-ai-image.ts
index b8897913d99a4..bd2fcdd96695a 100644
--- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/hooks/use-ai-image.ts
+++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/hooks/use-ai-image.ts
@@ -167,7 +167,9 @@ export default function useAiImage( {
.then( result => {
if ( result.data.length > 0 ) {
const image = 'data:image/png;base64,' + result.data[ 0 ].b64_json;
- updateImages( { image }, pointer.current );
+ const prompt = userPrompt || null;
+ const revisedPrompt = result.data[ 0 ].revised_prompt || null;
+ updateImages( { image, prompt, revisedPrompt }, pointer.current );
updateRequestsCount();
saveToMediaLibrary( image, name )
.then( savedImage => {
@@ -181,7 +183,7 @@ export default function useAiImage( {
image,
libraryId: savedImage?.id,
libraryUrl: savedImage?.url,
- revisedPrompt: result.data[ 0 ].revised_prompt || '',
+ revisedPrompt,
} );
} )
.catch( () => {