From a9a6ead418982947bbdda98050d3f8f2d3fc08d9 Mon Sep 17 00:00:00 2001 From: Luiz Kowalski Date: Wed, 1 May 2024 18:17:40 -0300 Subject: [PATCH] AI Featured Image: add media source entry point for the tool (#37166) * 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 --- ...atured-image-add-external-media-entrypoint | 4 + .../ai-assistant-plugin-sidebar/index.tsx | 8 +- .../components/featured-image/index.tsx | 102 ++++++++++++++---- .../components/modal/index.tsx | 7 +- .../shared/external-media/constants.js | 1 + .../external-media/media-button/media-menu.js | 7 +- .../media-button/media-sources.js | 28 ++++- .../shared/external-media/sources/index.js | 14 +++ .../sources/jetpack-ai-featured-image.js | 14 +++ 9 files changed, 157 insertions(+), 28 deletions(-) create mode 100644 projects/plugins/jetpack/changelog/update-ai-featured-image-add-external-media-entrypoint create mode 100644 projects/plugins/jetpack/extensions/shared/external-media/sources/jetpack-ai-featured-image.js diff --git a/projects/plugins/jetpack/changelog/update-ai-featured-image-add-external-media-entrypoint b/projects/plugins/jetpack/changelog/update-ai-featured-image-add-external-media-entrypoint new file mode 100644 index 0000000000000..7d2f48997ea00 --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-ai-featured-image-add-external-media-entrypoint @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +AI Featured Image: add entry point on the media source dropdown menu. diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx index bd352edf422fb..54a0a8616cf25 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx @@ -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'; @@ -132,7 +132,11 @@ export default function AiAssistantPluginSidebar() { { isAIFeaturedImageAvailable && ( - + ) } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/featured-image/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/featured-image/index.tsx index 628999d3deeb9..930a56270de90 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/featured-image/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/featured-image/index.tsx @@ -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'; /** @@ -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 } = @@ -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(); @@ -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, @@ -144,6 +179,7 @@ export default function FeaturedImage( { busy, disabled }: { busy: boolean; disa updateImages( { generating: false, error: e }, pointer.current ); } ); }, [ + notEnoughRequests, updateImages, generateImage, postContent, @@ -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 > ) => { @@ -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( () => { @@ -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. @@ -267,17 +321,21 @@ export default function FeaturedImage( { busy, disabled }: { busy: boolean; disa return (
-

{ __( 'Create and use an AI generated featured image for your post.', 'jetpack' ) }

- + { placement === FEATURED_IMAGE_PLACEMENT_JETPACK_SIDEBAR && ( + <> +

{ __( 'Create and use an AI generated featured image for your post.', 'jetpack' ) }

+ + + ) } { isFeaturedImageModalVisible && ( - +
diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/modal/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/modal/index.tsx index cbf26b000347e..414248b5abf15 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/modal/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/modal/index.tsx @@ -46,7 +46,12 @@ export default function AiAssistantModal( { maxWidth?: number; } ) { return ( - +

diff --git a/projects/plugins/jetpack/extensions/shared/external-media/constants.js b/projects/plugins/jetpack/extensions/shared/external-media/constants.js index ba083a2fccab6..bbc748af1fac2 100644 --- a/projects/plugins/jetpack/extensions/shared/external-media/constants.js +++ b/projects/plugins/jetpack/extensions/shared/external-media/constants.js @@ -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 = '/'; diff --git a/projects/plugins/jetpack/extensions/shared/external-media/media-button/media-menu.js b/projects/plugins/jetpack/extensions/shared/external-media/media-button/media-menu.js index adb5b1edf3499..c3793b4209f99 100644 --- a/projects/plugins/jetpack/extensions/shared/external-media/media-button/media-menu.js +++ b/projects/plugins/jetpack/extensions/shared/external-media/media-button/media-menu.js @@ -74,7 +74,12 @@ function MediaButtonMenu( props ) { { __( 'Media Library', 'jetpack' ) } - + ) } diff --git a/projects/plugins/jetpack/extensions/shared/external-media/media-button/media-sources.js b/projects/plugins/jetpack/extensions/shared/external-media/media-button/media-sources.js index b6c9653a53b2c..adc7a519d3454 100644 --- a/projects/plugins/jetpack/extensions/shared/external-media/media-button/media-sources.js +++ b/projects/plugins/jetpack/extensions/shared/external-media/media-button/media-sources.js @@ -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 ( { originalButton && originalButton( { open } ) } @@ -19,6 +29,20 @@ function MediaSources( { originalButton = null, onClick = () => {}, open, setSou ) ) } + { isFeatured && + featuredImageExclusiveMediaSources.map( ( { icon, id, label } ) => ( + { + onClick(); + setSource( id ); + } } + > + { label } + + ) ) } +
{ externalMediaSources.map( ( { icon, id, label } ) => ( diff --git a/projects/plugins/jetpack/extensions/shared/external-media/sources/index.js b/projects/plugins/jetpack/extensions/shared/external-media/sources/index.js index 943967e686518..55eb690d838bd 100644 --- a/projects/plugins/jetpack/extensions/shared/external-media/sources/index.js +++ b/projects/plugins/jetpack/extensions/shared/external-media/sources/index.js @@ -1,3 +1,4 @@ +import { aiAssistantIcon } from '@automattic/jetpack-ai-client'; import { __ } from '@wordpress/i18n'; import { GooglePhotosIcon, OpenverseIcon, PexelsIcon, JetpackMobileAppIcon } from '../../icons'; import { @@ -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'; @@ -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, @@ -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; } diff --git a/projects/plugins/jetpack/extensions/shared/external-media/sources/jetpack-ai-featured-image.js b/projects/plugins/jetpack/extensions/shared/external-media/sources/jetpack-ai-featured-image.js new file mode 100644 index 0000000000000..ac1b4abebdd96 --- /dev/null +++ b/projects/plugins/jetpack/extensions/shared/external-media/sources/jetpack-ai-featured-image.js @@ -0,0 +1,14 @@ +import FeaturedImage, { + FEATURED_IMAGE_PLACEMENT_MEDIA_SOURCE_DROPDOWN, +} from '../../../plugins/ai-assistant-plugin/components/featured-image'; + +function JetpackAIFeaturedImage( { onClose = () => {} } ) { + return ( + + ); +} + +export default JetpackAIFeaturedImage;