Skip to content

Commit

Permalink
Jetpack AI: show current featured image on modal (#40631)
Browse files Browse the repository at this point in the history
* add changelog

* accept previous images on use-ai-image hook to preload existing image(s), adapt pointer/current refs/states

* retrieve current featured image to show on modal, if available. add selector/store types, fix linter issues with types as well

* use exported store and type from wordpress/editor

* use cleaner types

* use a single call for all editorStore selects

* split selector to use media ID as dependency to fetch media object

* get featured image URL from getEditedPostAttribute

* simplify types

* Revert "get featured image URL from getEditedPostAttribute"

This reverts commit 7e23832.

* add debug calls to figure out why modal isn't showing the featured image on first open

* move getMedia selector to ai image hook, use selector/useEffect combo to try and get the value

* refactor var name to reflect value

* rename var so it makes a bit more sense of what it is

* set fixed image height on image modal

* if we've loaded a previous image, generate a mock image before triggering the generation process

* consider libraryId besides image prop to decide and use on image src. Also, change how disabling feedback is evaluated

* if image was loaded then consider regenerate

* wee fixes on carrousel

* remove heavy debugs

* remove comments

---------

Co-authored-by: Douglas <[email protected]>
  • Loading branch information
CGastrell and dhasilva authored Dec 23, 2024
1 parent 4ebe3c8 commit 325eaaa
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: other

Jetpack AI: featured image generator modal noww shows current featured image if present
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
$scale-factor: 0.55;

.ai-assistant-image {
&__blank {
display: flex;
Expand Down Expand Up @@ -47,6 +49,7 @@
display: flex;
align-items: center;
overflow: hidden;
height: calc( 768px * $scale-factor );

.ai-carrousel {
&__prev {
Expand Down Expand Up @@ -97,7 +100,7 @@

&-footer-left {
display: flex;
width: 25%;
flex-grow: 1;
}

&-counter {
Expand All @@ -106,7 +109,6 @@
align-items: center;
font-size: 13px;
padding: 6px 0;
width: 50%;

.ai-carrousel {
&__prev,
Expand All @@ -129,14 +131,17 @@
}

&-image {
width: 78%;
max-height: calc( 768px * $scale-factor );
max-width: calc( 1024px * $scale-factor );
width: auto;
height: auto;
margin: auto 0;
}

&-image-container {
display: flex;
width: 100%;
height: auto;
height: 100%;
position: absolute;
left: 8px;
transform: translateX( 100% );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { AiFeedbackThumbs } from '@automattic/jetpack-ai-client';
import { Spinner } from '@wordpress/components';
import { useEffect, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Icon, chevronLeft, chevronRight } from '@wordpress/icons';
import clsx from 'clsx';
Expand Down Expand Up @@ -62,6 +63,7 @@ export default function Carrousel( {
handleNextImage: () => void;
actions?: React.JSX.Element;
} ) {
const [ imageFeedbackDisabled, setImageFeedbackDisabled ] = useState( false );
const prevButton = (
<button className="ai-carrousel__prev" onClick={ handlePreviousImage }>
<Icon
Expand All @@ -84,33 +86,37 @@ export default function Carrousel( {
</button>
);

const total = images?.filter?.( item => item?.generating || Object.hasOwn( item, 'image' ) )
?.length;
const total = images?.filter?.(
item => item?.generating || Object.hasOwn( item, 'image' ) || Object.hasOwn( item, 'libraryId' )
)?.length;

const actual = current === 0 && total === 0 ? 0 : current + 1;

const aiFeedbackDisabled = imageData => {
const { image, generating, error } = imageData;
useEffect( () => {
const imageData = images[ current ];
if ( ! imageData ) {
setImageFeedbackDisabled( true );
}

const { image, generating, error } = imageData || {};

// disable if there's an empty modal
if ( ! image && ! generating && ! error ) {
return true;
return setImageFeedbackDisabled( true );
}

// also disable if we're generating or have an error
if ( generating || error ) {
return true;
return setImageFeedbackDisabled( true );
}

// otherwise we're fine
return false;
};
setImageFeedbackDisabled( false );
}, [ current, images ] );

return (
<div className="ai-assistant-image__carrousel">
<div className="ai-assistant-image__carrousel-images">
{ images.length > 1 && prevButton }
{ images.map( ( { image, generating, error, revisedPrompt }, index ) => (
{ images.map( ( { image, generating, error, revisedPrompt, libraryUrl }, index ) => (
<div
key={ `image:` + index }
className={ clsx( 'ai-assistant-image__carrousel-image-container', {
Expand Down Expand Up @@ -146,14 +152,14 @@ export default function Carrousel( {
</BlankImage>
) : (
<>
{ ! generating && ! image ? (
{ ! generating && ! image && ! libraryUrl ? (
<BlankImage>
<AiIcon />
</BlankImage>
) : (
<img
className="ai-assistant-image__carrousel-image"
src={ image }
src={ image || libraryUrl }
alt={ revisedPrompt }
/>
) }
Expand All @@ -174,8 +180,8 @@ export default function Carrousel( {
</div>

<AiFeedbackThumbs
disabled={ aiFeedbackDisabled( images[ current ] ) }
ratedItem={ images[ current ].libraryUrl || '' }
disabled={ imageFeedbackDisabled }
ratedItem={ images[ current ]?.libraryUrl || '' }
iconSize={ 20 }
options={ {
mediaLibraryId: Number( images[ current ].libraryId ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ImageStyle } from '@automattic/jetpack-ai-client';
import { useAnalytics } from '@automattic/jetpack-shared-extension-utils';
import { Button } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { useCallback, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import debugFactory from 'debug';
Expand All @@ -30,6 +31,7 @@ import {
PLACEMENT_MEDIA_SOURCE_DROPDOWN,
} from './types';
import type { ImageResponse } from './hooks/use-ai-image';
import type { EditorSelectors } from './types';

const debug = debugFactory( 'jetpack-ai:featured-image' );

Expand All @@ -49,14 +51,16 @@ export default function FeaturedImage( {
);
const siteType = useSiteType();
const postContent = usePostContent();
const { postTitle, postFeaturedMedia } = useSelect( select => {
const { postTitle, postFeaturedMediaId, isEditorPanelOpened } = useSelect( select => {
return {
// @ts-expect-error - getEditedPostAttribute is not defined in the useSelect type
postTitle: select( 'core/editor' ).getEditedPostAttribute( 'title' ),
// @ts-expect-error - getEditedPostAttribute is not defined in the useSelect type
postFeaturedMedia: select( 'core/editor' ).getEditedPostAttribute( 'featured_media' ),
postTitle: select( editorStore ).getEditedPostAttribute( 'title' ),
postFeaturedMediaId: select( editorStore ).getEditedPostAttribute( 'featured_media' ),
isEditorPanelOpened:
select( editorStore ).isEditorPanelOpened ??
( select( 'core/edit-post' ) as EditorSelectors ).isEditorPanelOpened,
};
}, [] );

const { saveToMediaLibrary } = useSaveToMediaLibrary();
const { tracks } = useAnalytics();
const { recordEvent } = tracks;
Expand All @@ -69,7 +73,7 @@ export default function FeaturedImage( {
const { toggleEditorPanelOpened: toggleEditorPanelOpenedFromEditPost } =
useDispatch( 'core/edit-post' );
const { editPost, toggleEditorPanelOpened: toggleEditorPanelOpenedFromEditor } =
useDispatch( 'core/editor' );
useDispatch( editorStore );

// Get feature data
const { requireUpgrade, requestsCount, requestsLimit, currentTier, costs } = useAiFeature();
Expand All @@ -87,14 +91,6 @@ export default function FeaturedImage( {
// https://github.com/WordPress/gutenberg/blob/fe4d8cb936df52945c01c1863f7b87b58b7cc69f/packages/edit-post/CHANGELOG.md?plain=1#L19
const toggleEditorPanelOpened =
toggleEditorPanelOpenedFromEditor ?? toggleEditorPanelOpenedFromEditPost;
const isEditorPanelOpened = useSelect( select => {
const isOpened =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
( select( 'core/editor' ) as any ).isEditorPanelOpened ??
// eslint-disable-next-line @typescript-eslint/no-explicit-any
( select( 'core/edit-post' ) as any ).isEditorPanelOpened;
return isOpened;
}, [] );

const {
pointer,
Expand All @@ -113,6 +109,7 @@ export default function FeaturedImage( {
cost: featuredImageCost,
type: 'featured-image-generation',
feature: FEATURED_IMAGE_FEATURE_NAME,
previousMediaId: postFeaturedMediaId,
} );

const handleModalClose = useCallback( () => {
Expand Down Expand Up @@ -202,7 +199,7 @@ export default function FeaturedImage( {
style: style,
} );

setCurrent( crrt => crrt + 1 );
setCurrent( () => images.length );
processImageGeneration( {
userPrompt,
postContent: postTitle + '\n\n' + postContent,
Expand All @@ -229,6 +226,7 @@ export default function FeaturedImage( {
postTitle,
postContent,
notEnoughRequests,
images,
]
);

Expand Down Expand Up @@ -341,7 +339,7 @@ export default function FeaturedImage( {
const generateAgainText = __( 'Generate another image', 'jetpack' );
const generateText = __( 'Generate', 'jetpack' );

const hasContent = postContent || postTitle;
const hasContent = postContent || postTitle ? true : false;
const hasPrompt = hasContent ? prompt.length >= 0 : prompt.length >= 3;
const disableInput = notEnoughRequests || currentPointer?.generating || requireUpgrade;
const disableAction = disableInput || ( ! hasContent && ! hasPrompt );
Expand All @@ -361,7 +359,11 @@ export default function FeaturedImage( {
<Button
onClick={ handleAccept }
variant="primary"
disabled={ ! currentImage?.image || currentImage?.generating }
disabled={
! currentImage?.image ||
currentImage?.generating ||
currentImage?.libraryId === postFeaturedMediaId
}
>
{ __( 'Set as featured image', 'jetpack' ) }
</Button>
Expand All @@ -385,7 +387,7 @@ export default function FeaturedImage( {
) }
<AiImageModal
postContent={ hasContent }
autoStart={ hasContent && ! postFeaturedMedia }
autoStart={ hasContent && ! postFeaturedMediaId }
autoStartAction={ handleFirstGenerate }
images={ images }
currentIndex={ current }
Expand All @@ -395,7 +397,9 @@ export default function FeaturedImage( {
placement={ placement }
onClose={ handleModalClose }
onTryAgain={ handleTryAgain }
onGenerate={ pointer?.current > 0 ? handleRegenerate : handleGenerate }
onGenerate={
pointer?.current > 0 || postFeaturedMediaId ? handleRegenerate : handleGenerate
}
generating={ currentPointer?.generating }
notEnoughRequests={ notEnoughRequests }
requireUpgrade={ requireUpgrade }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
ImageStyle,
askQuestionSync,
} from '@automattic/jetpack-ai-client';
import { useDispatch } from '@wordpress/data';
import { useCallback, useRef, useState } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { cleanForSlug } from '@wordpress/url';
/**
Expand All @@ -19,7 +19,7 @@ import useSaveToMediaLibrary from '../../../hooks/use-save-to-media-library';
/**
* Types
*/
import { FEATURED_IMAGE_FEATURE_NAME, GENERAL_IMAGE_FEATURE_NAME } from '../types';
import { CoreSelectors, FEATURED_IMAGE_FEATURE_NAME, GENERAL_IMAGE_FEATURE_NAME } from '../types';
import type { CarrouselImageData, CarrouselImages } from '../components/carrousel';
import type { RoleType } from '@automattic/jetpack-ai-client';
import type { FeatureControl } from 'extensions/store/wordpress-com/types.js';
Expand All @@ -42,19 +42,24 @@ export default function useAiImage( {
type,
cost,
autoStart = true,
previousMediaId,
}: {
feature: AiImageFeature;
type: AiImageType;
cost: number;
autoStart?: boolean;
previousMediaId?: number;
} ) {
const { generateImageWithParameters } = useImageGenerator();
const { increaseRequestsCount, featuresControl } = useAiFeature();
const { saveToMediaLibrary } = useSaveToMediaLibrary();
const { createNotice } = useDispatch( 'core/notices' );

/* Images Control */
// pointer keeps track of request/generation iteration
const pointer = useRef( 0 );
// and current keeps track of what is the image exposed at the moment
// TODO: should current be any relevant here? It's just modal/carrousel logic after all
const [ current, setCurrent ] = useState( 0 );
const [ images, setImages ] = useState< CarrouselImages >( [ { generating: autoStart } ] );

Expand All @@ -75,6 +80,25 @@ export default function useAiImage( {
} );
}, [] );

// the selec/useEffect combo...
const loadedMedia = useSelect(
( select: ( store ) => CoreSelectors ) => select( 'core' )?.getMedia?.( previousMediaId ),
[ previousMediaId ]
);
useEffect( () => {
if ( loadedMedia ) {
updateImages(
{
image: loadedMedia.source_url,
libraryId: loadedMedia.id,
libraryUrl: loadedMedia.source_url,
generating: false,
},
pointer.current
);
}
}, [ loadedMedia, updateImages ] );

/*
* Function to show a snackbar notice on the editor.
*/
Expand Down Expand Up @@ -123,6 +147,9 @@ export default function useAiImage( {
style?: string;
} ) => {
return new Promise< ImageResponse >( ( resolve, reject ) => {
if ( previousMediaId && pointer.current === 0 ) {
pointer.current++;
}
updateImages( { generating: true, error: null }, pointer.current );

// Ensure the site has enough requests to generate the image.
Expand Down Expand Up @@ -208,12 +235,13 @@ export default function useAiImage( {
saveToMediaLibrary,
showSnackbarNotice,
getImageNameSuggestion,
previousMediaId,
]
);

const handlePreviousImage = useCallback( () => {
setCurrent( Math.max( current - 1, 0 ) );
}, [ current, setCurrent ] );
}, [ current ] );

const handleNextImage = useCallback( () => {
setCurrent( Math.min( current + 1, images.length - 1 ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,16 @@ export const IMAGE_GENERATION_MODEL_STABLE_DIFFUSION = 'stable-diffusion' as con
export const IMAGE_GENERATION_MODEL_DALL_E_3 = 'dall-e-3' as const;
export const PLACEMENT_MEDIA_SOURCE_DROPDOWN = 'media-source-dropdown' as const;
export const PLACEMENT_BLOCK_PLACEHOLDER_BUTTON = 'block-placeholder-button' as const;

export interface EditorSelectors {
// actually getEditedPostAttribute can bring different values, but for our current use, number is fine (media ID)
getEditedPostAttribute: ( attribute: string ) => number;
isEditorPanelOpened: ( panel: string ) => boolean;
}

export interface CoreSelectors {
getMedia: ( mediaId: number ) => {
id: number;
source_url: string;
} | null;
}

0 comments on commit 325eaaa

Please sign in to comment.