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 } /> ); }