/** * External dependencies */ import { useAnalytics } from '@automattic/jetpack-shared-extension-utils'; import { Button, Tooltip, SelectControl } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { Icon, info } from '@wordpress/icons'; import debugFactory from 'debug'; import { useCallback, useEffect, useState, useRef, Dispatch, SetStateAction } from 'react'; /** * Internal dependencies */ import { IMAGE_STYLE_NONE, IMAGE_STYLE_AUTO } from '../../hooks/use-image-generator/constants.ts'; import AiIcon from '../assets/icons/ai.tsx'; import { EVENT_GENERATE, MINIMUM_PROMPT_LENGTH, EVENT_UPGRADE, EVENT_PLACEMENT_INPUT_FOOTER, EVENT_SWITCH_STYLE, EVENT_GUESS_STYLE, } from '../constants.ts'; import { useCheckout } from '../hooks/use-checkout.ts'; import useLogoGenerator from '../hooks/use-logo-generator.ts'; import useRequestErrors from '../hooks/use-request-errors.ts'; import { FairUsageNotice } from './fair-usage-notice.tsx'; import { UpgradeNudge } from './upgrade-nudge.tsx'; import './prompt.scss'; /** * Types */ import type { ImageStyle, ImageStyleObject } from '../../hooks/use-image-generator/constants.ts'; import type { ChangeEvent, ClipboardEvent, KeyboardEvent } from 'react'; const debug = debugFactory( 'jetpack-ai-calypso:prompt-box' ); type PromptProps = { initialPrompt?: string; }; export const AiModalPromptInput = ( { prompt = '', setPrompt = () => {}, disabled = false, actionDisabled = false, generateHandler = () => {}, placeholder = '', buttonLabel = '', }: { prompt: string; setPrompt: Dispatch< SetStateAction< string > >; disabled: boolean; actionDisabled: boolean; generateHandler: () => void; placeholder?: string; buttonLabel?: string; } ) => { const inputRef = useRef< HTMLDivElement | null >( null ); const onPromptInput = ( event: ChangeEvent< HTMLInputElement > ) => { setPrompt( event.target.textContent || '' ); }; const onPromptPaste = ( event: 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: KeyboardEvent ) => { if ( event.key === 'Enter' ) { event.preventDefault(); generateHandler(); } event.stopPropagation(); }; useEffect( () => { // Update prompt text node when prop changes if ( inputRef.current && inputRef.current.textContent !== prompt ) { inputRef.current.textContent = prompt; } }, [ prompt ] ); // fix for contenteditable divs not being able to be cleared by the user // as per default browser behavior const onKeyUp = () => { if ( inputRef.current?.textContent === '' ) { inputRef.current.innerHTML = ''; } }; return (
); }; export const Prompt = ( { initialPrompt = '' }: PromptProps ) => { const { tracks } = useAnalytics(); const { recordEvent: recordTracksEvent } = tracks; const [ prompt, setPrompt ] = useState< string >( initialPrompt ); const [ requestsRemaining, setRequestsRemaining ] = useState( 0 ); const { enhancePromptFetchError, logoFetchError } = useRequestErrors(); const { nextTierCheckoutURL: checkoutUrl, hasNextTier } = useCheckout(); const hasPrompt = prompt?.length >= MINIMUM_PROMPT_LENGTH; const [ showStyleSelector, setShowStyleSelector ] = useState( false ); const [ style, setStyle ] = useState< ImageStyle >( null ); const [ styles, setStyles ] = useState< Array< ImageStyleObject > >( [] ); const { generateLogo, enhancePrompt, setIsEnhancingPrompt, isBusy, isEnhancingPrompt, site, getAiAssistantFeature, requireUpgrade, context, tierPlansEnabled, imageStyles, guessStyle, } = useLogoGenerator(); const enhancingLabel = __( 'Enhancing…', 'jetpack-ai-client' ); const enhanceLabel = __( 'Enhance prompt', 'jetpack-ai-client' ); const enhanceButtonLabel = isEnhancingPrompt ? enhancingLabel : enhanceLabel; const inputRef = useRef< HTMLDivElement | null >( null ); const onEnhance = useCallback( async () => { debug( 'Enhancing prompt', prompt ); setIsEnhancingPrompt( true ); recordTracksEvent( EVENT_GENERATE, { context, tool: 'enhance-prompt' } ); try { const enhancedPrompt = await enhancePrompt( { prompt } ); setPrompt( enhancedPrompt ); setIsEnhancingPrompt( false ); } catch ( error ) { debug( 'Error enhancing prompt', error ); setIsEnhancingPrompt( false ); } }, [ context, enhancePrompt, prompt, setIsEnhancingPrompt ] ); const featureData = getAiAssistantFeature( String( site?.id || '' ) ); const currentLimit = featureData?.currentTier?.value || 0; const currentUsage = featureData?.usagePeriod?.requestsCount || 0; const isUnlimited = currentLimit === 1; useEffect( () => { if ( currentLimit - currentUsage <= 0 ) { setRequestsRemaining( 0 ); } else { setRequestsRemaining( currentLimit - currentUsage ); } }, [ currentLimit, currentUsage ] ); useEffect( () => { // Update prompt text node after enhancement if ( inputRef.current && inputRef.current.textContent !== prompt ) { inputRef.current.textContent = prompt; } }, [ prompt ] ); useEffect( () => { if ( imageStyles && imageStyles.length > 0 ) { // Sort styles to have "None" and "Auto" first setStyles( [ imageStyles.find( ( { value } ) => value === IMAGE_STYLE_NONE ), imageStyles.find( ( { value } ) => value === IMAGE_STYLE_AUTO ), ...imageStyles.filter( ( { value } ) => ! [ IMAGE_STYLE_NONE, IMAGE_STYLE_AUTO ].includes( value ) ), ].filter( v => v ) // simplest way to get rid of empty values ); setShowStyleSelector( true ); setStyle( IMAGE_STYLE_NONE ); } else { setStyles( [] ); setShowStyleSelector( false ); setStyle( null ); } }, [ imageStyles ] ); const onGenerate = useCallback( async () => { debug( context ); if ( style === IMAGE_STYLE_AUTO ) { setIsEnhancingPrompt( true ); recordTracksEvent( EVENT_GUESS_STYLE, { context, tool: 'image' } ); const guessedStyle = ( await guessStyle( prompt ) ) || IMAGE_STYLE_NONE; setStyle( guessedStyle ); recordTracksEvent( EVENT_GENERATE, { context, tool: 'image', style: guessedStyle } ); setIsEnhancingPrompt( false ); generateLogo( { prompt, style: guessedStyle } ); } else { recordTracksEvent( EVENT_GENERATE, { context, tool: 'image', style } ); generateLogo( { prompt, style } ); } }, [ context, generateLogo, prompt, style ] ); const onUpgradeClick = () => { recordTracksEvent( EVENT_UPGRADE, { context, placement: EVENT_PLACEMENT_INPUT_FOOTER } ); }; const updateStyle = useCallback( ( imageStyle: ImageStyle ) => { debug( 'change style', imageStyle ); setStyle( imageStyle ); recordTracksEvent( EVENT_SWITCH_STYLE, { context, style: imageStyle } ); }, [ context, setStyle, recordTracksEvent ] ); return (
{ __( 'Describe your site:', 'jetpack-ai-client' ) }
{ showStyleSelector && ( ) }
{ ! isUnlimited && ! requireUpgrade && (
{ sprintf( // translators: %d is the number of requests __( '%d requests remaining.', 'jetpack-ai-client' ), requestsRemaining ) }
{ hasNextTier && ( <>   ) }  
) } { requireUpgrade && tierPlansEnabled && } { requireUpgrade && ! tierPlansEnabled && } { enhancePromptFetchError && (
{ __( 'Error enhancing prompt. Please try again.', 'jetpack-ai-client' ) }
) } { logoFetchError && (
{ __( 'Error generating logo. Please try again.', 'jetpack-ai-client' ) }
) }
); };