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( () => {