Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jetpack AI: decouple modal prompt input for reusability #39864

Merged
merged 6 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading