Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jetpack AI: Add thumbs up/down component to AI logo generator #40610

Merged
merged 10 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading