Skip to content

Commit

Permalink
Jetpack AI: decouple modal prompt input for reusability (#39864)
Browse files Browse the repository at this point in the history
* decouple prompt input from prompt component, export it to make it available for other modals

* allow passing down CTA button label as prop

* implement AiModalPromptInput on GP image generation modal

* changelog

* fix wee css issue
  • Loading branch information
CGastrell authored Oct 25, 2024
1 parent 4282bc0 commit 4f0039e
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 121 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

AI Client: decouple prompt input as component and export it for reusability
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { __, sprintf } from '@wordpress/i18n';
import { Icon, info } from '@wordpress/icons';
import debugFactory from 'debug';
import { useCallback, useEffect, useState, useRef } from 'react';
import { Dispatch, SetStateAction } from 'react';
/**
* Internal dependencies
*/
Expand Down Expand Up @@ -37,6 +38,81 @@ type PromptProps = {
initialPrompt?: string;
};

export const AiModalPromptInput = ( {
prompt = '',
setPrompt = () => {},
disabled = false,
generateHandler = () => {},
placeholder = '',
buttonLabel = '',
}: {
prompt: string;
setPrompt: Dispatch< SetStateAction< string > >;
disabled: boolean;
generateHandler: () => void;
placeholder?: string;
buttonLabel?: string;
} ) => {
const inputRef = useRef< HTMLDivElement | null >( null );
const hasPrompt = prompt?.length >= MINIMUM_PROMPT_LENGTH;

const onPromptInput = ( event: React.ChangeEvent< HTMLInputElement > ) => {
setPrompt( event.target.textContent || '' );
};

const onPromptPaste = ( event: React.ClipboardEvent< HTMLInputElement > ) => {
event.preventDefault();

const selection = event.currentTarget.ownerDocument.getSelection();
if ( ! selection || ! selection.rangeCount ) {
return;
}

// Paste plain text only
const text = event.clipboardData.getData( 'text/plain' );

selection.deleteFromDocument();
const range = selection.getRangeAt( 0 );
range.insertNode( document.createTextNode( text ) );
selection.collapseToEnd();

setPrompt( inputRef.current?.textContent || '' );
};

const onKeyDown = ( event: React.KeyboardEvent ) => {
if ( event.key === 'Enter' ) {
event.preventDefault();
generateHandler();
}
};

return (
<div className="jetpack-ai-logo-generator__prompt-query">
<div
role="textbox"
tabIndex={ 0 }
ref={ inputRef }
contentEditable={ ! disabled }
// The content editable div is expected to be updated by the enhance prompt, so warnings are suppressed
suppressContentEditableWarning
className="prompt-query__input"
onInput={ onPromptInput }
onPaste={ onPromptPaste }
onKeyDown={ onKeyDown }
data-placeholder={ placeholder }
></div>
<Button
variant="primary"
className="jetpack-ai-logo-generator__prompt-submit"
onClick={ generateHandler }
disabled={ disabled || ! hasPrompt }
>
{ buttonLabel || __( 'Generate', 'jetpack-ai-client' ) }
</Button>
</div>
);
};

export const Prompt = ( { initialPrompt = '' }: PromptProps ) => {
const { tracks } = useAnalytics();
const { recordEvent: recordTracksEvent } = tracks;
Expand Down Expand Up @@ -143,29 +219,6 @@ export const Prompt = ( { initialPrompt = '' }: PromptProps ) => {
}
}, [ context, generateLogo, prompt, style ] );

const onPromptInput = ( event: React.ChangeEvent< HTMLInputElement > ) => {
setPrompt( event.target.textContent || '' );
};

const onPromptPaste = ( event: React.ClipboardEvent< HTMLInputElement > ) => {
event.preventDefault();

const selection = event.currentTarget.ownerDocument.getSelection();
if ( ! selection || ! selection.rangeCount ) {
return;
}

// Paste plain text only
const text = event.clipboardData.getData( 'text/plain' );

selection.deleteFromDocument();
const range = selection.getRangeAt( 0 );
range.insertNode( document.createTextNode( text ) );
selection.collapseToEnd();

setPrompt( inputRef.current?.textContent || '' );
};

const onUpgradeClick = () => {
recordTracksEvent( EVENT_UPGRADE, { context, placement: EVENT_PLACEMENT_INPUT_FOOTER } );
};
Expand All @@ -179,13 +232,6 @@ export const Prompt = ( { initialPrompt = '' }: PromptProps ) => {
[ context, setStyle, recordTracksEvent ]
);

const onKeyDown = ( event: React.KeyboardEvent ) => {
if ( event.key === 'Enter' ) {
event.preventDefault();
onGenerate();
}
};

return (
<div className="jetpack-ai-logo-generator__prompt">
<div className="jetpack-ai-logo-generator__prompt-header">
Expand All @@ -212,32 +258,16 @@ export const Prompt = ( { initialPrompt = '' }: PromptProps ) => {
/>
) }
</div>
<div className="jetpack-ai-logo-generator__prompt-query">
<div
role="textbox"
tabIndex={ 0 }
ref={ inputRef }
contentEditable={ ! isBusy && ! requireUpgrade }
// The content editable div is expected to be updated by the enhance prompt, so warnings are suppressed
suppressContentEditableWarning
className="prompt-query__input"
onInput={ onPromptInput }
onPaste={ onPromptPaste }
onKeyDown={ onKeyDown }
data-placeholder={ __(
'Describe your site or simply ask for a logo specifying some details about it',
'jetpack-ai-client'
) }
></div>
<Button
variant="primary"
className="jetpack-ai-logo-generator__prompt-submit"
onClick={ onGenerate }
disabled={ isBusy || requireUpgrade || ! hasPrompt }
>
{ __( 'Generate', 'jetpack-ai-client' ) }
</Button>
</div>
<AiModalPromptInput
prompt={ prompt }
setPrompt={ setPrompt }
generateHandler={ onGenerate }
disabled={ isBusy || requireUpgrade }
placeholder={ __(
'Describe your site or simply ask for a logo specifying some details about it',
'jetpack-ai-client'
) }
/>
<div className="jetpack-ai-logo-generator__prompt-footer">
{ ! isUnlimited && ! requireUpgrade && (
<div className="jetpack-ai-logo-generator__prompt-requests">
Expand Down
1 change: 1 addition & 0 deletions projects/js-packages/ai-client/src/logo-generator/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './components/generator-modal.js';
export { AiModalPromptInput } from './components/prompt.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: other

Jetpack AI: use new exported component for AI generation modal on GP image generation
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
&__actions {
width: 100%;
display: flex;
justify-content: center;
}

&__user-prompt {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/**
* External dependencies
*/
import { Button, Tooltip, KeyboardShortcuts } from '@wordpress/components';
import { AiModalPromptInput } from '@automattic/jetpack-ai-client';
import { Button } from '@wordpress/components';
import { useCallback, useRef, useState, useEffect } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { __ } from '@wordpress/i18n';
import { Icon, external } from '@wordpress/icons';
/**
* Internal dependencies
Expand Down Expand Up @@ -34,13 +35,11 @@ export default function AiImageModal( {
isUnlimited = false,
upgradeDescription = null,
hasError = false,
postContent = null,
handlePreviousImage = () => {},
handleNextImage = () => {},
acceptButton = null,
autoStart = false,
autoStartAction = null,
generateButtonLabel = null,
instructionsPlaceholder = null,
}: {
title: string;
Expand Down Expand Up @@ -72,13 +71,6 @@ export default function AiImageModal( {
const [ userPrompt, setUserPrompt ] = useState( '' );
const triggeredAutoGeneration = useRef( false );

const handleUserPromptChange = useCallback(
( e: React.ChangeEvent< HTMLTextAreaElement > ) => {
setUserPrompt( e.target.value.trim() );
},
[ setUserPrompt ]
);

const handleTryAgain = useCallback( () => {
onTryAgain?.( { userPrompt } );
}, [ onTryAgain, userPrompt ] );
Expand All @@ -87,37 +79,13 @@ export default function AiImageModal( {
onGenerate?.( { userPrompt } );
}, [ onGenerate, userPrompt ] );

const costTooltipTextSingular = __( '1 request per image', 'jetpack' );

const costTooltipTextPlural = sprintf(
// Translators: %d is the cost of generating one image.
__( '%d requests per image', 'jetpack' ),
cost
);

const costTooltipText = cost === 1 ? costTooltipTextSingular : costTooltipTextPlural;

// Controllers
const instructionsDisabled = notEnoughRequests || generating || requireUpgrade;
const upgradePromptVisible = ( requireUpgrade || notEnoughRequests ) && ! generating;
const counterVisible = Boolean( ! isUnlimited && cost && currentLimit );
const tryAgainButtonDisabled = ! userPrompt && ! postContent;
const generateButtonDisabled =
notEnoughRequests || generating || ( ! userPrompt && ! postContent );

const tryAgainButton = (
<Button onClick={ handleTryAgain } variant="secondary" disabled={ tryAgainButtonDisabled }>
{ __( 'Try again', 'jetpack' ) }
</Button>
);

const generateButton = (
<Tooltip text={ costTooltipText } placement="bottom">
<Button onClick={ handleGenerate } variant="secondary" disabled={ generateButtonDisabled }>
{ generateButtonLabel }
</Button>
</Tooltip>
);
const generateLabel = __( 'Generate', 'jetpack' );
const tryAgainLabel = __( 'Try again', 'jetpack' );

/**
* Trigger image generation automatically.
Expand All @@ -136,28 +104,14 @@ export default function AiImageModal( {
{ open && (
<AiAssistantModal handleClose={ onClose } title={ title }>
<div className="ai-image-modal__content">
<div className="ai-image-modal__user-prompt">
<div className="ai-image-modal__user-prompt-textarea">
<KeyboardShortcuts
bindGlobal
shortcuts={ {
enter: () => {
if ( ! generateButtonDisabled ) {
handleGenerate();
}
},
} }
>
<textarea
disabled={ instructionsDisabled }
maxLength={ 1000 }
rows={ 2 }
onChange={ handleUserPromptChange }
placeholder={ instructionsPlaceholder }
></textarea>
</KeyboardShortcuts>
</div>
</div>
<AiModalPromptInput
prompt={ userPrompt }
setPrompt={ setUserPrompt }
disabled={ instructionsDisabled }
generateHandler={ hasError ? handleTryAgain : handleGenerate }
placeholder={ instructionsPlaceholder }
buttonLabel={ hasError ? tryAgainLabel : generateLabel }
/>
{ upgradePromptVisible && (
<QuotaExceededMessage
description={ upgradeDescription }
Expand All @@ -175,11 +129,6 @@ export default function AiImageModal( {
/>
) }
</div>
<div className="ai-image-modal__actions-right">
<div className="ai-image-modal__action-buttons">
{ hasError ? tryAgainButton : generateButton }
</div>
</div>
</div>
<div className="ai-image-modal__image-canvas">
<Carrousel
Expand Down

0 comments on commit 4f0039e

Please sign in to comment.