import {
useAnalytics,
PLAN_TYPE_UNLIMITED,
usePlanType,
} from '@automattic/jetpack-shared-extension-utils';
import { Button } from '@wordpress/components';
import { useCallback, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import debugFactory from 'debug';
import './style.scss';
import useAiFeature from '../../hooks/use-ai-feature/index.ts';
import usePostContent from '../../hooks/use-post-content.ts';
import useSaveToMediaLibrary from '../../hooks/use-save-to-media-library.ts';
import AiImageModal from './components/ai-image-modal.tsx';
import useAiImage from './hooks/use-ai-image.ts';
import useSiteType from './hooks/use-site-type.ts';
import {
IMAGE_GENERATION_MODEL_STABLE_DIFFUSION,
IMAGE_GENERATION_MODEL_DALL_E_3,
GENERAL_IMAGE_FEATURE_NAME,
} from './types.ts';
import type { ReactElement } from 'react';
/**
* The type for the callback function that is called when the user selects an image.
*/
type SetImageCallbackProps = {
id: number;
url: string;
mime?: string;
};
type GeneralPurposeImageProps = {
placement: string;
onClose?: () => void;
onSetImage?: ( image: SetImageCallbackProps ) => void;
};
const debug = debugFactory( 'jetpack-ai:general-purpose-image' );
/**
* GeneralPurposeImage component
* @param {GeneralPurposeImageProps} props - The component properties.
* @return {ReactElement} - rendered component.
*/
export default function GeneralPurposeImage( {
placement,
onClose = () => {},
onSetImage = () => {},
}: GeneralPurposeImageProps ) {
const [ isFeaturedImageModalVisible, setIsFeaturedImageModalVisible ] = useState( true );
const siteType = useSiteType();
const { getPostContent } = usePostContent();
const { saveToMediaLibrary } = useSaveToMediaLibrary();
const { tracks } = useAnalytics();
const { recordEvent } = tracks;
const [ prompt, setPrompt ] = useState( '' );
// Get feature data
const { requireUpgrade, requestsCount, requestsLimit, currentTier, costs } = useAiFeature();
const planType = usePlanType( currentTier );
const generalImageCost = costs?.[ GENERAL_IMAGE_FEATURE_NAME ]?.activeModel ?? 10;
const generalImageActiveModel =
generalImageCost === costs?.[ GENERAL_IMAGE_FEATURE_NAME ]?.stableDiffusion
? IMAGE_GENERATION_MODEL_STABLE_DIFFUSION
: IMAGE_GENERATION_MODEL_DALL_E_3;
const isUnlimited = planType === PLAN_TYPE_UNLIMITED;
const requestsBalance = requestsLimit - requestsCount;
const notEnoughRequests = requestsBalance < generalImageCost;
const {
current,
setCurrent,
processImageGeneration,
handlePreviousImage,
handleNextImage,
currentImage,
currentPointer,
images,
pointer,
imageStyles,
guessStyle,
} = useAiImage( {
cost: generalImageCost,
autoStart: false,
type: 'general-image-generation',
feature: GENERAL_IMAGE_FEATURE_NAME,
} );
const hasPrompt = prompt.length >= 3;
const disableInput = notEnoughRequests || currentPointer?.generating || requireUpgrade;
const disableAction = disableInput || ! hasPrompt;
const handleModalClose = useCallback( () => {
setIsFeaturedImageModalVisible( false );
onClose?.();
}, [ onClose ] );
const handleGenerate = useCallback(
async ( { userPrompt, style }: { userPrompt?: string; style?: string } ) => {
debug( 'handleGenerate', userPrompt, style );
// track the generate image event
recordEvent( 'jetpack_ai_general_image_generation_generate_image', {
placement,
model: generalImageActiveModel,
site_type: siteType,
style,
} );
processImageGeneration( {
userPrompt,
postContent: getPostContent(),
notEnoughRequests,
style,
} ).catch( error => {
recordEvent( 'jetpack_ai_general_image_generation_error', {
placement,
error: error?.message,
model: generalImageActiveModel,
site_type: siteType,
style,
} );
} );
},
[
recordEvent,
placement,
generalImageActiveModel,
siteType,
processImageGeneration,
getPostContent,
notEnoughRequests,
]
);
const handleRegenerate = useCallback(
( { userPrompt, style }: { userPrompt?: string; style?: string } ) => {
debug( 'handleRegenerate', userPrompt );
// track the regenerate image event
recordEvent( 'jetpack_ai_general_image_generation_generate_another_image', {
placement,
model: generalImageActiveModel,
site_type: siteType,
style,
} );
setCurrent( crrt => crrt + 1 );
processImageGeneration( {
userPrompt,
postContent: getPostContent(),
notEnoughRequests,
style,
} ).catch( error => {
recordEvent( 'jetpack_ai_general_image_generation_error', {
placement,
error: error?.message,
model: generalImageActiveModel,
site_type: siteType,
} );
} );
},
[
recordEvent,
placement,
generalImageActiveModel,
siteType,
processImageGeneration,
getPostContent,
notEnoughRequests,
setCurrent,
]
);
const handleTryAgain = useCallback(
( { userPrompt, style }: { userPrompt?: string; style?: string } ) => {
debug( 'handleTryAgain', userPrompt );
// track the try again event
recordEvent( 'jetpack_ai_general_image_generation_try_again', {
placement,
model: generalImageActiveModel,
site_type: siteType,
style,
} );
processImageGeneration( {
userPrompt,
postContent: getPostContent(),
notEnoughRequests,
style,
} ).catch( error => {
recordEvent( 'jetpack_ai_general_image_generation_error', {
placement,
error: error?.message,
model: generalImageActiveModel,
site_type: siteType,
} );
} );
},
[
recordEvent,
placement,
generalImageActiveModel,
siteType,
processImageGeneration,
getPostContent,
notEnoughRequests,
]
);
const handleAccept = useCallback( () => {
// track the accept/use image event
recordEvent( 'jetpack_ai_general_image_generation_use_image', {
placement,
model: generalImageActiveModel,
site_type: siteType,
} );
const setImage = ( { id, url, mime } ) => {
onSetImage?.( { id, url, mime } );
handleModalClose();
};
// If the image is already in the media library, use it directly, if it failed for some reason
// save it to the media library and then use it.
if ( currentImage?.libraryId ) {
setImage( {
id: currentImage?.libraryId,
url: currentImage?.libraryUrl,
// Default to image/png for cached images (AI generates PNG)
mime: 'image/png',
} );
} else {
saveToMediaLibrary( currentImage?.image ).then( image => {
setImage( {
id: image.id,
url: image.url,
mime: image.mime,
} );
} );
}
}, [
recordEvent,
placement,
generalImageActiveModel,
siteType,
currentImage?.libraryId,
currentImage?.libraryUrl,
currentImage?.image,
onSetImage,
handleModalClose,
saveToMediaLibrary,
] );
const generateAgainText = __( 'Generate another image', 'jetpack-ai-client' );
const generateText = __( 'Generate', 'jetpack-ai-client' );
const upgradeDescription = notEnoughRequests
? sprintf(
// Translators: %d is the cost of generating a featured image.
__(
"Image generation costs %d requests per image. You don't have enough requests to generate another image.",
'jetpack-ai-client'
),
generalImageCost
)
: null;
const acceptButton = (
);
return (
0 ? handleRegenerate : handleGenerate }
generating={ currentPointer?.generating }
notEnoughRequests={ notEnoughRequests }
requireUpgrade={ requireUpgrade }
upgradeDescription={ upgradeDescription }
currentLimit={ requestsLimit }
currentUsage={ requestsCount }
isUnlimited={ isUnlimited }
hasError={ Boolean( currentPointer?.error ) }
handlePreviousImage={ handlePreviousImage }
handleNextImage={ handleNextImage }
acceptButton={ acceptButton }
generateButtonLabel={ pointer?.current > 0 ? generateAgainText : generateText }
instructionsPlaceholder={ __(
"Describe the image you'd like to create and select a style.",
'jetpack-ai-client'
) }
imageStyles={ imageStyles }
onGuessStyle={ guessStyle }
prompt={ prompt }
setPrompt={ setPrompt }
inputDisabled={ disableInput }
actionDisabled={ disableAction }
/>
);
}