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

Change Calypso Logo generator prompts #97171

Merged
merged 8 commits into from
Dec 12, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@
}
}

.components-button {
&:focus:not(:disabled):not(.is-primary) {
box-shadow: 0 0 0 2px var(--color-link, #3858e9);
}
.components-button.is-link {
padding: 0 4px;
text-decoration: none;

&.is-link {
text-decoration: none;
&:not(:disabled) {
color: var(--color-link, #3858e9);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@
display: flex;
font-size: $font-body-extra-small;
line-height: 20px;
gap: 16px;

.jetpack-ai-logo-generator-icon {
margin-right: 4px;

// tricky SVG
path {
fill: currentColor
};
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import { recordTracksEvent } from '@automattic/calypso-analytics';
import { Button, Tooltip } from '@wordpress/components';
import { Button, Tooltip, SelectControl } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { Icon, info } from '@wordpress/icons';
import debugFactory from 'debug';
Expand All @@ -20,7 +20,9 @@ import AiIcon from '../assets/icons/ai';
import { useCheckout } from '../hooks/use-checkout';
import useLogoGenerator from '../hooks/use-logo-generator';
import useRequestErrors from '../hooks/use-request-errors';
import { IMAGE_STYLE_NONE } from '../store/constants';
import { UpgradeNudge } from './upgrade-nudge';
import type { ImageStyle, ImageStyleObject, LogoGeneratorFeatureControl } from '../store/types';
import './prompt.scss';

const debug = debugFactory( 'jetpack-ai-calypso:prompt-box' );
Expand All @@ -31,6 +33,9 @@ export const Prompt: React.FC< { initialPrompt?: string } > = ( { initialPrompt
const { enhancePromptFetchError, logoFetchError } = useRequestErrors();
const { nextTierCheckoutURL: checkoutUrl, hasNextTier } = useCheckout();
const hasPrompt = prompt?.length >= MINIMUM_PROMPT_LENGTH;
const [ style, setStyle ] = useState< ImageStyle >( IMAGE_STYLE_NONE );
const [ styles, setStyles ] = useState< Array< ImageStyleObject > >( [] );
const [ showStyleSelector, setShowStyleSelector ] = useState( false );

const {
generateLogo,
Expand Down Expand Up @@ -70,6 +75,9 @@ export const Prompt: React.FC< { initialPrompt?: string } > = ( { initialPrompt
const currentLimit = featureData?.currentTier?.value || 0;
const currentUsage = featureData?.usagePeriod?.requestsCount || 0;
const isUnlimited = currentLimit === 1;
const featureControl = featureData?.featuresControl?.[
'logo-generator'
] as LogoGeneratorFeatureControl;

useEffect( () => {
if ( currentLimit - currentUsage <= 0 ) {
Expand All @@ -87,9 +95,9 @@ export const Prompt: React.FC< { initialPrompt?: string } > = ( { initialPrompt
}, [ prompt ] );

const onGenerate = useCallback( async () => {
recordTracksEvent( EVENT_GENERATE, { context, tool: 'image' } );
generateLogo( { prompt } );
}, [ context, generateLogo, prompt ] );
recordTracksEvent( EVENT_GENERATE, { context, tool: 'image', style } );
generateLogo( { prompt, style } );
}, [ context, generateLogo, prompt, style ] );

const onPromptInput = ( event: React.ChangeEvent< HTMLInputElement > ) => {
setPrompt( event.target.textContent || '' );
Expand Down Expand Up @@ -117,6 +125,35 @@ export const Prompt: React.FC< { initialPrompt?: string } > = ( { initialPrompt
recordTracksEvent( EVENT_UPGRADE, { context, placement: EVENT_PLACEMENT_INPUT_FOOTER } );
};

// initialize styles dropdown
useEffect( () => {
const isEnabled = featureControl?.enabled || false;
const imageStyles = featureControl?.styles || [];
if ( isEnabled && imageStyles && imageStyles.length > 0 ) {
// Sort styles to have "None" first
setStyles(
[
imageStyles.find( ( { value } ) => value === IMAGE_STYLE_NONE ),
...imageStyles.filter( ( { value } ) => ! [ IMAGE_STYLE_NONE ].includes( value ) ),
].filter( ( v ): v is ImageStyleObject => !! v ) // Type guard to filter out undefined values
);
setShowStyleSelector( true );
setStyle( IMAGE_STYLE_NONE );
}
}, [ featureControl ] );

const updateStyle = useCallback(
( imageStyle: ImageStyle ) => {
debug( 'change style', imageStyle );
setStyle( imageStyle );
recordTracksEvent( 'jetpack_ai_image_generator_switch_style', {
context,
style: imageStyle,
} );
},
[ setStyle, context ]
);

return (
<div className="jetpack-ai-logo-generator__prompt">
<div className="jetpack-ai-logo-generator__prompt-header">
Expand All @@ -132,6 +169,16 @@ export const Prompt: React.FC< { initialPrompt?: string } > = ( { initialPrompt
<AiIcon />
<span>{ enhanceButtonLabel }</span>
</Button>
{ showStyleSelector && (
<div>
<SelectControl
__nextHasNoMarginBottom
value={ style }
options={ styles }
onChange={ updateStyle }
/>
</div>
) }
</div>
</div>
<div className="jetpack-ai-logo-generator__prompt-query">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,23 @@ const useLogoGenerator = () => {
const langName =
languages.find( ( language ) => language.langSlug === locale )?.name ?? 'English';

const firstPromptGenerationPrompt = `Generate a simple and short prompt asking for a logo based on the site's name and description. The prompt should be in ${ langName } (${ locale }).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for moving this to the backend!

Example for a site named "The minimalist fashion blog", described as "Daily inspiration for all things fashion": A logo for a minimalist fashion site focused on daily sartorial inspiration with a clean and modern aesthetic that is sleek and sophisticated.
Another example, now for a site called "El observatorio de aves", described as "Un sitio dedicado a nuestros compañeros y compañeras entusiastas de la observación de aves.": Un logo para un sitio web dedicado a la observación de aves, capturando la esencia de la naturaleza y la pasión por la avifauna en un diseño elegante y representativo, reflejando una estética natural y apasionada por la vida silvestre.

Site name: ${ name }
Site description: ${ description }`;
const messages = [
{
role: 'jetpack-ai',
context: {
type: 'jetpack-ai-generate-logo-prompt',
name,
description,
language: langName,
locale,
},
},
];

const body = {
question: firstPromptGenerationPrompt,
feature: 'jetpack-ai-logo-generator',
stream: false,
messages,
};

const data = await wpcomLimitedRequest< {
Expand Down Expand Up @@ -183,9 +189,11 @@ For example: user's prompt: A logo for an ice cream shop. Returned prompt: A log

const generateImage = async function ( {
prompt,
style = 'none',
}: {
prompt: string;
} ): Promise< { data: Array< { url: string } > } > {
style?: string;
} ): Promise< { data: Array< { url?: string; b64_json?: string; revised_prompt?: string } > } > {
setLogoFetchError( null );

try {
Expand All @@ -207,12 +215,34 @@ The image should contain a single icon, without variations, color palettes or di

User request:${ prompt }`;

const body = {
const body: {
prompt: string;
feature: string;
response_format: string;
style?: string;
messages?: Array< { role: string; context: Record< string, unknown > } >;
} = {
prompt: imageGenerationPrompt,
feature: 'jetpack-ai-logo-generator',
response_format: 'url',
};

if ( style ) {
body[ 'response_format' ] = 'b64_json';
body[ 'style' ] = style;
body[ 'messages' ] = [
{
role: 'jetpack-ai',
context: {
type: 'ai-assistant-generate-logo',
request: prompt,
name,
description,
},
},
];
}

const data = await wpcomLimitedRequest( {
apiNamespace: 'wpcom/v2',
path: '/jetpack-ai-image',
Expand All @@ -221,6 +251,10 @@ User request:${ prompt }`;
body,
} );

if ( style ) {
return data as { data: { url?: string; b64_json?: string }[] };
}

return data as { data: { url: string }[] };
} catch ( error ) {
setLogoFetchError( error );
Expand Down Expand Up @@ -256,6 +290,7 @@ User request:${ prompt }`;
const { ID: mediaId, URL: mediaURL } = await saveToMediaLibrary( {
siteId,
url: logo.url,
logo,
attrs: {
caption: logo.description,
description: logo.description,
Expand Down Expand Up @@ -316,7 +351,13 @@ User request:${ prompt }`;
[ siteId, addLogoToHistory ]
);

const generateLogo = async function ( { prompt }: { prompt: string } ): Promise< void > {
const generateLogo = async function ( {
prompt,
style = 'none',
}: {
prompt: string;
style?: string;
} ): Promise< void > {
debug( 'Generating logo for site', siteId );

setIsRequestingImage( true );
Expand All @@ -338,7 +379,7 @@ User request:${ prompt }`;
let image;

try {
image = await generateImage( { prompt } );
image = await generateImage( { prompt, style } );

if ( ! image || ! image.data.length ) {
throw new Error( 'No image returned' );
Expand All @@ -350,8 +391,11 @@ User request:${ prompt }`;

// response_format=url returns object with url, otherwise b64_json
const logo: Logo = {
url: image.data[ 0 ].url,
url: image.data[ 0 ].b64_json
? 'data:image/png;base64,' + image.data[ 0 ].b64_json
: image.data[ 0 ].url || '',
description: prompt,
revisedPrompt: image.data[ 0 ].revised_prompt,
};

try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
/**
* External dependencies
* Internal dependencies
*/
import wpcomLimitedRequest from './wpcom-limited-request';
/**
* Types
*/
import type { SaveToMediaLibraryProps, SaveToMediaLibraryResponseProps } from '../../types';

export async function saveToMediaLibrary( { siteId, url, attrs = {} }: SaveToMediaLibraryProps ) {
export async function saveToMediaLibrary( {
siteId,
url,
logo = { url: '', description: '' },
attrs = {},
}: SaveToMediaLibraryProps ) {
// if the logo has a URL and it's a base64 string, use formData as expected by the API
if ( logo.url && logo.url.includes( 'data:image/png;base64,' ) ) {
// new FLUX response should send the logo object with a b64_json string
const fileResponse = await fetch( logo.url );
const blob = await fileResponse.blob();
const file = new File( [ blob ], 'site-logo.png', { type: 'image/png' } );

const formData: ( string | File )[][] = [
[ 'media[]', file ],
[ 'attrs[]', JSON.stringify( attrs ) ],
];

const response = await wpcomLimitedRequest< SaveToMediaLibraryResponseProps >( {
path: `/sites/${ String( siteId ) }/media/new`,
apiVersion: '1.1',
method: 'POST',
formData,
} );
return response.media[ 0 ];
}

const body = {
media_urls: [ url ],
attrs: [ attrs ],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function mapAiFeatureResponseToAiFeatureProps(
nextTier: response[ 'next-tier' ],
tierPlansEnabled: !! response[ 'tier-plans-enabled' ],
costs: response.costs,
featuresControl: response[ 'features-control' ],
};
}

Expand Down
22 changes: 22 additions & 0 deletions packages/jetpack-ai-calypso/src/logo-generator/store/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,25 @@ export const ACTION_SET_ENHANCE_PROMPT_FETCH_ERROR = 'SET_ENHANCE_PROMPT_FETCH_E
export const ACTION_SET_LOGO_FETCH_ERROR = 'SET_LOGO_FETCH_ERROR';
export const ACTION_SET_SAVE_TO_LIBRARY_ERROR = 'SET_SAVE_TO_LIBRARY_ERROR';
export const ACTION_SET_LOGO_UPDATE_ERROR = 'SET_LOGO_UPDATE_ERROR';

// image styles
export const IMAGE_STYLE_ENHANCE = 'enhance';
export const IMAGE_STYLE_ANIME = 'anime';
export const IMAGE_STYLE_PHOTOGRAPHIC = 'photographic';
export const IMAGE_STYLE_DIGITAL_ART = 'digital-art';
export const IMAGE_STYLE_COMICBOOK = 'comicbook';
export const IMAGE_STYLE_FANTASY_ART = 'fantasy-art';
export const IMAGE_STYLE_ANALOG_FILM = 'analog-film';
export const IMAGE_STYLE_NEONPUNK = 'neonpunk';
export const IMAGE_STYLE_ISOMETRIC = 'isometric';
export const IMAGE_STYLE_LOWPOLY = 'lowpoly';
export const IMAGE_STYLE_ORIGAMI = 'origami';
export const IMAGE_STYLE_LINE_ART = 'line-art';
export const IMAGE_STYLE_CRAFT_CLAY = 'craft-clay';
export const IMAGE_STYLE_CINEMATIC = 'cinematic';
export const IMAGE_STYLE_3D_MODEL = '3d-model';
export const IMAGE_STYLE_PIXEL_ART = 'pixel-art';
export const IMAGE_STYLE_TEXTURE = 'texture';
export const IMAGE_STYLE_MONTY_PYTHON = 'monty-python';
export const IMAGE_STYLE_AUTO = 'auto';
export const IMAGE_STYLE_NONE = 'none';
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const INITIAL_STATE: LogoGeneratorStateProp = {
asyncRequestTimerId: 0,
isRequestingImage: false,
},
featuresControl: {},
},
},
history: [],
Expand Down
Loading
Loading