Skip to content

Commit

Permalink
Jetpack AI: Add thumbs up/down component to AI logo generator (#40610)
Browse files Browse the repository at this point in the history
* Jetpack AI: Add thumbs up/down component to AI logo generator

* changelog

* Attemp #1 to fix some build errors

* changelog

* add base-styles to jetpack ai client

* move AiFeedbackThumbs to ai client

* avoid multiple events on same rating

* store rating with other logo information

* fix issue with persisting ratings with modal open

* add mediaLibraryId, prompt and revisedPrompt to event

---------

Co-authored-by: Douglas <[email protected]>
  • Loading branch information
mwatson and dhasilva authored Dec 20, 2024
1 parent 80a5b84 commit fc13aac
Show file tree
Hide file tree
Showing 16 changed files with 232 additions and 86 deletions.
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Jetpack AI: Add thumbs up/down component to AI logo generator
1 change: 1 addition & 0 deletions projects/js-packages/ai-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
133 changes: 133 additions & 0 deletions projects/js-packages/ai-client/src/components/ai-feedback/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="ai-assistant-feedback__selection">
<Tooltip text={ __( 'I like this', 'jetpack-ai-client' ) }>
<Button
disabled={ disabled }
icon={ thumbsUp }
onClick={ () => rateAI( true ) }
iconSize={ iconSize }
showTooltip={ false }
className={ clsx( {
'ai-assistant-feedback__thumb-selected': checkThumb( 'thumbs-up' ),
} ) }
/>
</Tooltip>
<Tooltip text={ __( "I don't find this useful", 'jetpack-ai-client' ) }>
<Button
disabled={ disabled }
icon={ thumbsDown }
onClick={ () => rateAI( false ) }
iconSize={ iconSize }
showTooltip={ false }
className={ clsx( {
'ai-assistant-feedback__thumb-selected': checkThumb( 'thumbs-down' ),
} ) }
/>
</Tooltip>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) );
}
}
1 change: 1 addition & 0 deletions projects/js-packages/ai-client/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<AiFeedbackThumbs
disabled={ disabled }
ratedItem={ ratedItem }
feature="logo-generator"
savedRatings={ savedRatings }
options={ {
mediaLibraryId: selectedLogo.mediaId,
prompt: selectedLogo.description,
} }
onRate={ onRate }
/>
);
};

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 (
<>
<img
Expand All @@ -171,6 +211,7 @@ const LogoReady: React.FC< {
<div className="jetpack-ai-logo-generator-modal-presenter__actions">
<SaveInLibraryButton siteId={ siteId } />
<UseOnSiteButton onApplyLogo={ onApplyLogo } />
<RateLogo ratedItem={ logo.url } disabled={ false } onRate={ handleRateLogo } />
</div>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -424,6 +427,7 @@ User request:${ prompt }`;
url: savedLogo.mediaURL,
description: prompt,
mediaId: savedLogo.mediaId,
revisedPrompt,
} );
} catch ( error ) {
storeLogo( logo );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand All @@ -45,16 +52,18 @@ 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 );

if ( index > -1 ) {
storedContent[ index ].url = newUrl;
storedContent[ index ].mediaId = mediaId;
storedContent[ index ].rating = rating;
}

localStorage.setItem(
Expand Down Expand Up @@ -96,6 +105,7 @@ export function getSiteLogoHistory( siteId: string ) {
url: logo.url,
description: logo.description,
mediaId: logo.mediaId,
rating: logo.rating,
} ) );

return storedContent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ export type Logo = {
url: string;
description: string;
mediaId?: number;
rating?: string;
revisedPrompt?: string;
};

export type RequestError = string | Error | null;
Expand Down
1 change: 1 addition & 0 deletions projects/js-packages/ai-client/src/logo-generator/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export type UpdateInStorageProps = {
url: Logo[ 'url' ];
newUrl: Logo[ 'url' ];
mediaId: Logo[ 'mediaId' ];
rating?: Logo[ 'rating' ];
};

export type RemoveFromStorageProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: other

Jetpack AI: Add thumbs up/down component to AI logo generator
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading

0 comments on commit fc13aac

Please sign in to comment.