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 <douglas.henri@automattic.com>

Committed via a GitHub action: https://github.com/Automattic/jetpack/actions/runs/12438109162

Upstream-Ref: Automattic/jetpack@fc13aac
  • Loading branch information
dhasilva authored and matticbot committed Dec 20, 2024
1 parent 9c26d41 commit 65260fe
Show file tree
Hide file tree
Showing 20 changed files with 376 additions and 26 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.25.3-alpha] - unreleased

This is an alpha version! The changes listed here are not final.

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

## [0.25.2] - 2024-12-16
### Changed
- Updated package dependencies. [#40564]
Expand Down Expand Up @@ -483,6 +490,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- AI Client: stop using smart document visibility handling on the fetchEventSource library, so it does not restart the completion when changing tabs. [#32004]
- Updated package dependencies. [#31468] [#31659] [#31785]

[0.25.3-alpha]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.2...v0.25.3-alpha
[0.25.2]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.1...v0.25.2
[0.25.1]: https://github.com/Automattic/jetpack-ai-client/compare/v0.25.0...v0.25.1
[0.25.0]: https://github.com/Automattic/jetpack-ai-client/compare/v0.24.3...v0.25.0
Expand Down
26 changes: 26 additions & 0 deletions build/ai-client/src/components/ai-feedback/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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;
};
/**
* AiFeedbackThumbs component.
*
* @param {AiFeedbackThumbsProps} props - component props.
* @return {React.ReactElement} - rendered component.
*/
export default function AiFeedbackThumbs({ disabled, iconSize, ratedItem, feature, savedRatings, options, onRate, }: AiFeedbackThumbsProps): React.ReactElement;
export {};
70 changes: 70 additions & 0 deletions build/ai-client/src/components/ai-feedback/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* 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';
/**
* 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) {
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, }) {
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) => {
if (!itemsRated[ratedItem]) {
return false;
}
return itemsRated[ratedItem] === thumbValue;
};
const rateAI = (isThumbsUp) => {
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 (_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({
'ai-assistant-feedback__thumb-selected': checkThumb('thumbs-up'),
}) }) }), _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({
'ai-assistant-feedback__thumb-selected': checkThumb('thumbs-down'),
}) }) })] }));
}
1 change: 1 addition & 0 deletions build/ai-client/src/components/index.d.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
1 change: 1 addition & 0 deletions build/ai-client/src/components/index.js
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
26 changes: 25 additions & 1 deletion build/ai-client/src/logo-generator/components/logo-presenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,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 @@ -80,8 +81,31 @@ const LogoFetching = () => {
const LogoEmpty = () => {
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') })] }));
};
const RateLogo = ({ 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 (_jsx(AiFeedbackThumbs, { disabled: disabled, ratedItem: ratedItem, feature: "logo-generator", savedRatings: savedRatings, options: {
mediaLibraryId: selectedLogo.mediaId,
prompt: selectedLogo.description,
}, onRate: onRate }));
};
const LogoReady = ({ siteId, logo, onApplyLogo }) => {
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 })] })] })] }));
const handleRateLogo = (rating) => {
// Update localStorage
updateLogo({
siteId,
url: logo.url,
newUrl: logo.url,
mediaId: logo.mediaId,
rating,
});
};
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 })] })] })] }));
};
const LogoUpdated = ({ logo }) => {
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') })] })] }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,17 +285,20 @@ User request:${prompt}`;
increaseAiAssistantRequestsCount(-logoGenerationCost);
throw error;
}
const revisedPrompt = image.data[0].revised_prompt || null;
// response_format=url returns object with url, otherwise b64_json
const logo = {
url: 'data:image/png;base64,' + image.data[0].b64_json,
description: prompt,
revisedPrompt,
};
try {
const savedLogo = await saveLogo(logo);
storeLogo({
url: savedLogo.mediaURL,
description: prompt,
mediaId: savedLogo.mediaId,
revisedPrompt,
});
}
catch (error) {
Expand Down
17 changes: 9 additions & 8 deletions build/ai-client/src/logo-generator/lib/logo-storage.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { RemoveFromStorageProps, SaveToStorageProps, UpdateInStorageProps } from
/**
* 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 declare function stashLogo({ siteId, url, description, mediaId }: SaveToStorageProps): Logo;
export declare function stashLogo({ siteId, url, description, mediaId, revisedPrompt, }: SaveToStorageProps): Logo;
/**
* Update an entry in the site's logo history.
*
Expand All @@ -23,9 +23,10 @@ export declare function stashLogo({ siteId, url, description, mediaId }: SaveToS
* @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 declare function updateLogo({ siteId, url, newUrl, mediaId }: UpdateInStorageProps): Logo;
export declare function updateLogo({ siteId, url, newUrl, mediaId, rating }: UpdateInStorageProps): Logo;
/**
* Get the logo history for a site.
*
Expand Down
20 changes: 12 additions & 8 deletions build/ai-client/src/logo-generator/lib/logo-storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@ 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 }) {
export function stashLogo({ siteId, url, description, mediaId, revisedPrompt, }) {
const storedContent = getSiteLogoHistory(siteId);
const logo = {
url,
description,
mediaId,
revisedPrompt,
};
storedContent.push(logo);
localStorage.setItem(`logo-history-${siteId}`, JSON.stringify(storedContent.slice(-MAX_LOGOS)));
Expand All @@ -30,14 +31,16 @@ export function stashLogo({ siteId, url, description, mediaId }) {
* @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 }) {
export function updateLogo({ siteId, url, newUrl, mediaId, rating }) {
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(`logo-history-${siteId}`, JSON.stringify(storedContent.slice(-MAX_LOGOS)));
return storedContent[index];
Expand Down Expand Up @@ -68,6 +71,7 @@ export function getSiteLogoHistory(siteId) {
url: logo.url,
description: logo.description,
mediaId: logo.mediaId,
rating: logo.rating,
}));
return storedContent;
}
Expand Down
2 changes: 2 additions & 0 deletions build/ai-client/src/logo-generator/store/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export type Logo = {
url: string;
description: string;
mediaId?: number;
rating?: string;
revisedPrompt?: string;
};
export type RequestError = string | Error | null;
export type LogoGeneratorStateProp = {
Expand Down
1 change: 1 addition & 0 deletions build/ai-client/src/logo-generator/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type UpdateInStorageProps = {
url: Logo['url'];
newUrl: Logo['url'];
mediaId: Logo['mediaId'];
rating?: Logo['rating'];
};
export type RemoveFromStorageProps = {
mediaId: Logo['mediaId'];
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": false,
"name": "@automattic/jetpack-ai-client",
"version": "0.25.2",
"version": "0.25.3-alpha",
"description": "A JS client for consuming Jetpack AI services",
"homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/ai-client/#readme",
"bugs": {
Expand Down Expand Up @@ -51,6 +51,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
Loading

0 comments on commit 65260fe

Please sign in to comment.