Skip to content

Commit

Permalink
AI Featured Image: add media source entry point for the tool (#37166)
Browse files Browse the repository at this point in the history
* Add AI generated image button on external media dropdown

* Accept the placement value as a parameter of the component

* Set the placement when using the component on the Jetpack sidebar

* Rename const to follow the standard of the other existing const

* Support the media source dropdown placement

* Only show the sidebar generate button for the jetpack sidebar placement

* Add extra onClose handling to support the close event on the media source dropdown

* Trigger the image generation automatically when the tool is placed on the media source dropdown

* Fix error when clicking outside of modal, was trying to close without handler; prevent closing as well

* Add flag to ensure only one automattic generation call

* changelog

* Show message when the post has no content

* Prevent image generation when the site does not have enough requests to generate the image
  • Loading branch information
lhkowalski authored May 1, 2024
1 parent 3e9adbe commit a9a6ead
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: other

AI Featured Image: add entry point on the media source dropdown menu.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import useAICheckout from '../../../../blocks/ai-assistant/hooks/use-ai-checkout
import useAiFeature from '../../../../blocks/ai-assistant/hooks/use-ai-feature';
import JetpackPluginSidebar from '../../../../shared/jetpack-plugin-sidebar';
import { TierProp } from '../../../../store/wordpress-com/types';
import FeaturedImage from '../featured-image';
import FeaturedImage, { FEATURED_IMAGE_PLACEMENT_JETPACK_SIDEBAR } from '../featured-image';
import Proofread from '../proofread';
import TitleOptimization from '../title-optimization';
import UsagePanel from '../usage-panel';
Expand Down Expand Up @@ -132,7 +132,11 @@ export default function AiAssistantPluginSidebar() {
{ isAIFeaturedImageAvailable && (
<PanelRow className="jetpack-ai-featured-image-control__header">
<BaseControl label={ __( 'AI Featured Image', 'jetpack' ) }>
<FeaturedImage busy={ isRedirecting } disabled={ requireUpgrade } />
<FeaturedImage
busy={ isRedirecting }
disabled={ requireUpgrade }
placement={ FEATURED_IMAGE_PLACEMENT_JETPACK_SIDEBAR }
/>
</BaseControl>
</PanelRow>
) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useImageGenerator } from '@automattic/jetpack-ai-client';
import { useAnalytics } from '@automattic/jetpack-shared-extension-utils';
import { Button, Tooltip } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback, useRef, useState } from '@wordpress/element';
import { useCallback, useRef, useState, useEffect } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { Icon, external } from '@wordpress/icons';
/**
Expand All @@ -27,9 +27,20 @@ import Carrousel, { CarrouselImageData, CarrouselImages } from './carrousel';
import UsageCounter from './usage-counter';

const FEATURED_IMAGE_FEATURE_NAME = 'featured-post-image';
const JETPACK_SIDEBAR_PLACEMENT = 'jetpack-sidebar';
export const FEATURED_IMAGE_PLACEMENT_JETPACK_SIDEBAR = 'jetpack-sidebar';
export const FEATURED_IMAGE_PLACEMENT_MEDIA_SOURCE_DROPDOWN = 'media-source-dropdown';

export default function FeaturedImage( { busy, disabled }: { busy: boolean; disabled: boolean } ) {
export default function FeaturedImage( {
busy,
disabled,
placement,
onClose = () => {},
}: {
busy: boolean;
disabled: boolean;
placement: string;
onClose?: () => void;
} ) {
const { toggleEditorPanelOpened: toggleEditorPanelOpenedFromEditPost } =
useDispatch( 'core/edit-post' );
const { editPost, toggleEditorPanelOpened: toggleEditorPanelOpenedFromEditor } =
Expand All @@ -41,6 +52,7 @@ export default function FeaturedImage( { busy, disabled }: { busy: boolean; disa
const [ current, setCurrent ] = useState( 0 );
const pointer = useRef( 0 );
const [ userPrompt, setUserPrompt ] = useState( '' );
const triggeredAutoGeneration = useRef( false );

const { enableComplementaryArea } = useDispatch( 'core/interface' );
const { generateImage } = useImageGenerator();
Expand Down Expand Up @@ -118,6 +130,29 @@ export default function FeaturedImage( { busy, disabled }: { busy: boolean; disa
const processImageGeneration = useCallback( () => {
updateImages( { generating: true, error: null }, pointer.current );

// Ensure the site has enough requests to generate the image.
if ( notEnoughRequests ) {
updateImages(
{
generating: false,
error: new Error(
__( "You don't have enough requests to generate another image", 'jetpack' )
),
},
pointer.current
);
return;
}

// Ensure the user prompt or the post content are set.
if ( ! userPrompt && ! postContent ) {
updateImages(
{ generating: false, error: new Error( __( 'No content to generate image', 'jetpack' ) ) },
pointer.current
);
return;
}

generateImage( {
feature: FEATURED_IMAGE_FEATURE_NAME,
postContent,
Expand All @@ -144,6 +179,7 @@ export default function FeaturedImage( { busy, disabled }: { busy: boolean; disa
updateImages( { generating: false, error: e }, pointer.current );
} );
}, [
notEnoughRequests,
updateImages,
generateImage,
postContent,
Expand All @@ -156,34 +192,39 @@ export default function FeaturedImage( { busy, disabled }: { busy: boolean; disa
setIsFeaturedImageModalVisible( ! isFeaturedImageModalVisible );
}, [ isFeaturedImageModalVisible, setIsFeaturedImageModalVisible ] );

const handleModalClose = useCallback( () => {
toggleFeaturedImageModal();
onClose?.();
}, [ toggleFeaturedImageModal, onClose ] );

const handleGenerate = useCallback( () => {
// track the generate image event
recordEvent( 'jetpack_ai_featured_image_generation_generate_image', {
placement: JETPACK_SIDEBAR_PLACEMENT,
placement,
} );

toggleFeaturedImageModal();
processImageGeneration();
}, [ toggleFeaturedImageModal, processImageGeneration, recordEvent ] );
}, [ toggleFeaturedImageModal, processImageGeneration, recordEvent, placement ] );

const handleRegenerate = useCallback( () => {
// track the regenerate image event
recordEvent( 'jetpack_ai_featured_image_generation_generate_another_image', {
placement: JETPACK_SIDEBAR_PLACEMENT,
placement,
} );

processImageGeneration();
setCurrent( crrt => crrt + 1 );
}, [ processImageGeneration, recordEvent ] );
}, [ processImageGeneration, recordEvent, placement ] );

const handleTryAgain = useCallback( () => {
// track the try again event
recordEvent( 'jetpack_ai_featured_image_generation_try_again', {
placement: JETPACK_SIDEBAR_PLACEMENT,
placement,
} );

processImageGeneration();
}, [ processImageGeneration, recordEvent ] );
}, [ processImageGeneration, recordEvent, placement ] );

const handleUserPromptChange = useCallback(
( e: React.ChangeEvent< HTMLTextAreaElement > ) => {
Expand All @@ -201,12 +242,12 @@ export default function FeaturedImage( { busy, disabled }: { busy: boolean; disa
const handleAccept = useCallback( () => {
// track the accept/use image event
recordEvent( 'jetpack_ai_featured_image_generation_use_image', {
placement: JETPACK_SIDEBAR_PLACEMENT,
placement,
} );

const setAsFeaturedImage = image => {
editPost( { featured_media: image } );
toggleFeaturedImageModal();
handleModalClose();

// Open the featured image panel for users to see the new image.
setTimeout( () => {
Expand Down Expand Up @@ -243,10 +284,23 @@ export default function FeaturedImage( { busy, disabled }: { busy: boolean; disa
recordEvent,
saveToMediaLibrary,
toggleEditorPanelOpened,
toggleFeaturedImageModal,
triggerComplementaryArea,
handleModalClose,
placement,
] );

/**
* When the placement is set to FEATURED_IMAGE_PLACEMENT_MEDIA_SOURCE_DROPDOWN, we generate the image automatically.
*/
useEffect( () => {
if ( placement === FEATURED_IMAGE_PLACEMENT_MEDIA_SOURCE_DROPDOWN ) {
if ( ! triggeredAutoGeneration.current ) {
triggeredAutoGeneration.current = true;
handleGenerate();
}
}
}, [ placement, handleGenerate ] );

const modalTitle = __( 'Generate a featured image with AI', 'jetpack' );
const costTooltipText = sprintf(
// Translators: %d is the cost of generating one image.
Expand All @@ -267,17 +321,21 @@ export default function FeaturedImage( { busy, disabled }: { busy: boolean; disa

return (
<div>
<p>{ __( 'Create and use an AI generated featured image for your post.', 'jetpack' ) }</p>
<Button
onClick={ handleGenerate }
isBusy={ busy }
disabled={ ! postContent || disabled || notEnoughRequests }
variant="secondary"
>
{ __( 'Generate image', 'jetpack' ) }
</Button>
{ placement === FEATURED_IMAGE_PLACEMENT_JETPACK_SIDEBAR && (
<>
<p>{ __( 'Create and use an AI generated featured image for your post.', 'jetpack' ) }</p>
<Button
onClick={ handleGenerate }
isBusy={ busy }
disabled={ ! postContent || disabled || notEnoughRequests }
variant="secondary"
>
{ __( 'Generate image', 'jetpack' ) }
</Button>
</>
) }
{ isFeaturedImageModalVisible && (
<AiAssistantModal handleClose={ toggleFeaturedImageModal } title={ modalTitle }>
<AiAssistantModal handleClose={ handleModalClose } title={ modalTitle }>
<div className="ai-assistant-featured-image__content">
<div className="ai-assistant-featured-image__user-prompt">
<div className="ai-assistant-featured-image__user-prompt-textarea">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ export default function AiAssistantModal( {
maxWidth?: number;
} ) {
return (
<Modal __experimentalHideHeader={ hideHeader } className="ai-assistant-modal">
<Modal
__experimentalHideHeader={ hideHeader }
className="ai-assistant-modal"
shouldCloseOnClickOutside={ false }
onRequestClose={ handleClose }
>
<div className="ai-assistant-modal__content" style={ { maxWidth } }>
<ModalHeader requestingState={ requestingState } onClose={ handleClose } title={ title } />
<hr className="ai-assistant-modal__divider" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const SOURCE_GOOGLE_PHOTOS = 'google_photos';
export const SOURCE_OPENVERSE = 'openverse';
export const SOURCE_PEXELS = 'pexels';
export const SOURCE_JETPACK_APP_MEDIA = 'jetpack_app_media';
export const SOURCE_JETPACK_AI_FEATURED_IMAGE = 'jetpack_ai_featured_image';

export const PATH_RECENT = 'recent';
export const PATH_ROOT = '/';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,12 @@ function MediaButtonMenu( props ) {
{ __( 'Media Library', 'jetpack' ) }
</MenuItem>

<MediaSources open={ open } setSource={ setSelectedSource } onClick={ onClose } />
<MediaSources
open={ open }
setSource={ setSelectedSource }
onClick={ onClose }
isFeatured={ isFeatured }
/>
</MenuGroup>
</NavigableMenu>
) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { MenuItem } from '@wordpress/components';
import { Fragment } from '@wordpress/element';
import { internalMediaSources, externalMediaSources } from '../sources';
import {
internalMediaSources,
externalMediaSources,
featuredImageExclusiveMediaSources,
} from '../sources';

function MediaSources( { originalButton = null, onClick = () => {}, open, setSource } ) {
function MediaSources( {
originalButton = null,
onClick = () => {},
open,
setSource,
isFeatured = false,
} ) {
return (
<Fragment>
{ originalButton && originalButton( { open } ) }
Expand All @@ -19,6 +29,20 @@ function MediaSources( { originalButton = null, onClick = () => {}, open, setSou
</MenuItem>
) ) }

{ isFeatured &&
featuredImageExclusiveMediaSources.map( ( { icon, id, label } ) => (
<MenuItem
icon={ icon }
key={ id }
onClick={ () => {
onClick();
setSource( id );
} }
>
{ label }
</MenuItem>
) ) }

<hr style={ { marginLeft: '-8px', marginRight: '-8px' } } />

{ externalMediaSources.map( ( { icon, id, label } ) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { aiAssistantIcon } from '@automattic/jetpack-ai-client';
import { __ } from '@wordpress/i18n';
import { GooglePhotosIcon, OpenverseIcon, PexelsIcon, JetpackMobileAppIcon } from '../../icons';
import {
Expand All @@ -6,8 +7,10 @@ import {
SOURCE_OPENVERSE,
SOURCE_PEXELS,
SOURCE_JETPACK_APP_MEDIA,
SOURCE_JETPACK_AI_FEATURED_IMAGE,
} from '../constants';
import GooglePhotosMedia from './google-photos';
import JetpackAIFeaturedImage from './jetpack-ai-featured-image';
import JetpackAppMedia from './jetpack-app-media';
import OpenverseMedia from './openverse';
import PexelsMedia from './pexels';
Expand All @@ -21,6 +24,15 @@ export const internalMediaSources = [
},
];

export const featuredImageExclusiveMediaSources = [
{
id: SOURCE_JETPACK_AI_FEATURED_IMAGE,
label: __( 'AI Generated Image', 'jetpack' ),
icon: aiAssistantIcon,
keyword: 'jetpack ai',
},
];

export const externalMediaSources = [
{
id: SOURCE_GOOGLE_PHOTOS,
Expand Down Expand Up @@ -78,6 +90,8 @@ export function getExternalLibrary( type ) {
return OpenverseMedia;
} else if ( type === SOURCE_JETPACK_APP_MEDIA ) {
return JetpackAppMedia;
} else if ( type === SOURCE_JETPACK_AI_FEATURED_IMAGE ) {
return JetpackAIFeaturedImage;
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import FeaturedImage, {
FEATURED_IMAGE_PLACEMENT_MEDIA_SOURCE_DROPDOWN,
} from '../../../plugins/ai-assistant-plugin/components/featured-image';

function JetpackAIFeaturedImage( { onClose = () => {} } ) {
return (
<FeaturedImage
placement={ FEATURED_IMAGE_PLACEMENT_MEDIA_SOURCE_DROPDOWN }
onClose={ onClose }
/>
);
}

export default JetpackAIFeaturedImage;

0 comments on commit a9a6ead

Please sign in to comment.