Add Short Description "Write With AI" Button + Inform about automatic generation (#39805)
* Add "Write With AI" button to product short description media buttons area.
This commit is contained in:
parent
a66a48108f
commit
8ec91504ff
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
[Woo AI] Add a Write with AI button for the short description field in product editor.
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f04d60d67b018f03857889eabb4fde67",
|
||||
"content-hash": "4422aa30be46b03afe33056e2f26359c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "composer/installers",
|
||||
|
|
|
@ -20,6 +20,7 @@ class Woo_AI_Product_Text_Generation {
|
|||
public function __construct() {
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'add_woo_ai_register_script' ) );
|
||||
add_action( 'media_buttons', array( $this, 'add_gpt_button' ), 40 );
|
||||
add_action( 'media_buttons', array( $this, 'add_short_description_gpt_button' ), 50 );
|
||||
add_action( 'edit_form_before_permalink', array( $this, 'add_name_generation_form' ) );
|
||||
add_filter( 'the_editor', array( $this, 'add_gpt_form' ), 10, 1 );
|
||||
}
|
||||
|
@ -82,6 +83,19 @@ class Woo_AI_Product_Text_Generation {
|
|||
echo '<div id="woocommerce-ai-app-product-gpt-button"></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add gpt button to the editor.
|
||||
*
|
||||
* @param String $editor_id Editor Id.
|
||||
*/
|
||||
public function add_short_description_gpt_button( $editor_id ) {
|
||||
if ( 'excerpt' !== $editor_id || ( ! current_user_can( 'edit_posts' ) && ! current_user_can( 'edit_pages' ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<div id="woocommerce-ai-app-product-short-description-gpt-button"></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the form and button for generating product title suggestions to the editor.
|
||||
*/
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"version": "0.2.0",
|
||||
"homepage": "http://github.com/woocommerce/woo-ai",
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/jquery": "^3.5.16",
|
||||
"@types/react": "^17.0.2",
|
||||
|
@ -17,6 +18,7 @@
|
|||
"@types/wordpress__components": "^19.10.3",
|
||||
"@woocommerce/dependency-extraction-webpack-plugin": "workspace:*",
|
||||
"@woocommerce/eslint-plugin": "workspace:*",
|
||||
"@wordpress/data": "wp-6.0",
|
||||
"@wordpress/env": "^8.2.0",
|
||||
"@wordpress/prettier-config": "2.17.0",
|
||||
"@wordpress/scripts": "^19.2.4",
|
||||
|
@ -27,6 +29,7 @@
|
|||
"uglify-js": "^3.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automattic/tour-kit": "^1.1.1",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@types/prop-types": "^15.7.4",
|
||||
"@types/react-outside-click-handler": "^1.3.1",
|
||||
|
@ -41,6 +44,7 @@
|
|||
"@wordpress/i18n": "wp-6.0",
|
||||
"@wordpress/notices": "wp-6.0",
|
||||
"@wordpress/plugins": "wp-6.0",
|
||||
"@wordpress/preferences": "wp-6.0",
|
||||
"debug": "^4.3.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-query": "^3.39.3"
|
||||
|
@ -48,7 +52,6 @@
|
|||
"peerDependencies": {
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@wordpress/data": "wp-6.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
|
|
|
@ -2,13 +2,12 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import React from 'react';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import MagicIcon from '../../assets/images/icons/magic.svg';
|
||||
import { MIN_TITLE_LENGTH_FOR_DESCRIPTION } from '../constants';
|
||||
|
||||
type MagicButtonProps = {
|
||||
title?: string;
|
||||
|
@ -17,6 +16,10 @@ type MagicButtonProps = {
|
|||
label: string;
|
||||
};
|
||||
|
||||
type WriteItForMeBtnProps = MagicButtonProps & {
|
||||
disabledMessage?: string;
|
||||
};
|
||||
|
||||
const MagicButton = ( {
|
||||
title,
|
||||
label,
|
||||
|
@ -40,24 +43,14 @@ const MagicButton = ( {
|
|||
export const WriteItForMeBtn = ( {
|
||||
disabled,
|
||||
onClick,
|
||||
}: Omit< MagicButtonProps, 'title' | 'label' > ) => {
|
||||
disabledMessage,
|
||||
}: Omit< WriteItForMeBtnProps, 'title' | 'label' > ) => {
|
||||
return (
|
||||
<MagicButton
|
||||
disabled={ disabled }
|
||||
onClick={ onClick }
|
||||
label={ __( 'Write with AI', 'woocommerce' ) }
|
||||
title={
|
||||
disabled
|
||||
? sprintf(
|
||||
/* translators: %d: Minimum characters for product title */
|
||||
__(
|
||||
'Please create a product title before generating a description. It must be %d characters or longer.',
|
||||
'woocommerce'
|
||||
),
|
||||
MIN_TITLE_LENGTH_FOR_DESCRIPTION
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
title={ disabled ? disabledMessage : undefined }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -71,7 +64,7 @@ export const StopCompletionBtn = ( {
|
|||
disabled={ disabled }
|
||||
onClick={ onClick }
|
||||
label={ __( 'Stop writing…', 'woocommerce' ) }
|
||||
title={ __( 'Stop generating the description.', 'woocommerce' ) }
|
||||
title={ __( 'Stop generating content.', 'woocommerce' ) }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from './random-loading-message';
|
||||
export * from './description-completion-buttons';
|
||||
export * from './info-modal';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './info-modal';
|
|
@ -0,0 +1,34 @@
|
|||
.shortDescriptionGenerated-spotlight {
|
||||
max-width: 400px;
|
||||
padding:20px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
|
||||
span {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size:16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
color:#fff;
|
||||
background-color: #0074a2;
|
||||
border: 1px solid lightblue;
|
||||
float:right;
|
||||
border:none;
|
||||
margin-bottom:20px;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: darken(#0074a2, 10%); // Slightly darken the blue on hover and focus
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: darken(#0074a2, 15%); // Darken a bit more on active state
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useDispatch, select } from '@wordpress/data';
|
||||
import { store as preferencesStore } from '@wordpress/preferences';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import TourSpotlight from '../tour-spotlight/tour-spotlight';
|
||||
import './info-modal.scss';
|
||||
|
||||
interface InfoModalProps {
|
||||
id: string;
|
||||
message: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const InfoModal: React.FC< InfoModalProps > = ( {
|
||||
id,
|
||||
message,
|
||||
title,
|
||||
} ) => {
|
||||
const anchorElement = document.querySelector( '#postexcerpt' );
|
||||
const hasBeenDismissedBefore = select( preferencesStore ).get(
|
||||
'woo-ai-plugin',
|
||||
`modalDismissed-${ id }`
|
||||
);
|
||||
|
||||
const { set } = useDispatch( preferencesStore );
|
||||
|
||||
if ( ! anchorElement || hasBeenDismissedBefore ) return null;
|
||||
|
||||
const closeTour = () => {
|
||||
set( 'woo-ai-plugin', `modalDismissed-${ id }`, true );
|
||||
};
|
||||
|
||||
return (
|
||||
<TourSpotlight
|
||||
title={ title }
|
||||
description={ message }
|
||||
onDismiss={ closeTour }
|
||||
reference={ '#postexcerpt' }
|
||||
className={ `${ id }-spotlight` }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './tour-spotlight';
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import Tour, { TourStepRendererProps } from '@automattic/tour-kit';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import React from 'react';
|
||||
|
||||
type TourSpotlightProps = {
|
||||
onDismiss: () => void;
|
||||
title: string;
|
||||
description: string;
|
||||
reference: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function TourSpotlight( {
|
||||
onDismiss,
|
||||
title,
|
||||
description,
|
||||
reference,
|
||||
className,
|
||||
}: TourSpotlightProps ) {
|
||||
const [ showTour, setShowTour ] = useState( true );
|
||||
|
||||
const handleDismiss = () => {
|
||||
setShowTour( false );
|
||||
onDismiss();
|
||||
};
|
||||
// Define a configuration for the tour, passing along a handler for closing.
|
||||
const config = {
|
||||
steps: [
|
||||
{
|
||||
referenceElements: {
|
||||
desktop: reference,
|
||||
},
|
||||
meta: {
|
||||
description,
|
||||
},
|
||||
},
|
||||
],
|
||||
renderers: {
|
||||
tourStep: ( { currentStepIndex }: TourStepRendererProps ) => {
|
||||
return (
|
||||
<div className={ className }>
|
||||
<h3>{ title }</h3>
|
||||
<p>
|
||||
{
|
||||
config.steps[ currentStepIndex ].meta
|
||||
.description
|
||||
}
|
||||
</p>
|
||||
<Button onClick={ handleDismiss }>
|
||||
{ __( 'Got it', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
tourMinimized: () => <div />,
|
||||
},
|
||||
closeHandler: () => handleDismiss(),
|
||||
options: {},
|
||||
};
|
||||
|
||||
if ( ! showTour ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Tour config={ config } />;
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export const WOO_AI_PLUGIN_FEATURE_NAME = 'woo_ai_plugin';
|
||||
export const MAX_TITLE_LENGTH = 200;
|
||||
export const MIN_TITLE_LENGTH_FOR_DESCRIPTION = 15;
|
||||
export const MIN_DESC_LENGTH_FOR_SHORT_DESC = 100;
|
||||
|
|
|
@ -1,2 +1,8 @@
|
|||
declare module '@wordpress/notices';
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
declare const wp: any;
|
||||
declare module '@wordpress/data';
|
||||
declare module '@wordpress/preferences';
|
||||
declare module '@wordpress/notices';
|
|
@ -1,6 +0,0 @@
|
|||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '@wordpress/data';
|
|
@ -1,11 +1,39 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { setTinyContent, getTinyContent } from '../utils/tiny-tools';
|
||||
import {
|
||||
getTinyContentObject,
|
||||
setTinyContent,
|
||||
getTinyContent,
|
||||
TinyContent,
|
||||
} from '../utils/tiny-tools';
|
||||
|
||||
export const useTinyEditor = ( editorId?: string ) => {
|
||||
const [ editor, setEditor ] = useState< TinyContent | null >( null );
|
||||
|
||||
useEffect( () => {
|
||||
const fetchEditor = () => {
|
||||
const editorInstance = getTinyContentObject( editorId );
|
||||
if ( editorInstance ) {
|
||||
setEditor( editorInstance );
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener( 'load', fetchEditor );
|
||||
|
||||
return () => {
|
||||
document.removeEventListener( 'load', fetchEditor );
|
||||
};
|
||||
}, [ editorId ] );
|
||||
|
||||
return {
|
||||
setContent: ( str: string ) => setTinyContent( str, editorId ),
|
||||
getContent: () => getTinyContent( editorId ),
|
||||
getEditorObject: () => editor,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,9 +9,14 @@ import { QueryClient, QueryClientProvider } from 'react-query';
|
|||
*/
|
||||
import { WriteItForMeButtonContainer } from './product-description';
|
||||
import { ProductNameSuggestions } from './product-name';
|
||||
import { WriteShortDescriptionButtonContainer } from './product-short-description';
|
||||
import setPreferencesPersistence from './utils/preferencesPersistence';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
// This sets up loading and saving the plugin's preferences.
|
||||
setPreferencesPersistence();
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const renderComponent = ( Component, rootElement ) => {
|
||||
|
@ -39,7 +44,15 @@ const nameSuggestionsRoot = document.getElementById(
|
|||
'woocommerce-ai-app-product-name-suggestions'
|
||||
);
|
||||
|
||||
const shortDescriptionButtonRoot = document.getElementById(
|
||||
'woocommerce-ai-app-product-short-description-gpt-button'
|
||||
);
|
||||
|
||||
if ( window.JP_CONNECTION_INITIAL_STATE?.connectionStatus?.isActive ) {
|
||||
renderComponent( WriteItForMeButtonContainer, descriptionButtonRoot );
|
||||
renderComponent( ProductNameSuggestions, nameSuggestionsRoot );
|
||||
renderComponent(
|
||||
WriteShortDescriptionButtonContainer,
|
||||
shortDescriptionButtonRoot
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
@import "product-description/product-description.scss";
|
||||
@import "product-name/product-name.scss";
|
||||
@import 'product-description/product-description.scss';
|
||||
@import 'product-name/product-name.scss';
|
||||
@import 'product-short-description/product-short-description.scss';
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useState, useEffect, useRef } from '@wordpress/element';
|
||||
import {
|
||||
__experimentalUseCompletion as useCompletion,
|
||||
|
@ -18,7 +18,7 @@ import {
|
|||
MIN_TITLE_LENGTH_FOR_DESCRIPTION,
|
||||
WOO_AI_PLUGIN_FEATURE_NAME,
|
||||
} from '../constants';
|
||||
import { StopCompletionBtn, WriteItForMeBtn } from '../components';
|
||||
import { InfoModal, StopCompletionBtn, WriteItForMeBtn } from '../components';
|
||||
import { useFeedbackSnackbar, useStoreBranding, useTinyEditor } from '../hooks';
|
||||
import {
|
||||
getProductName,
|
||||
|
@ -29,24 +29,11 @@ import {
|
|||
recordTracksFactory,
|
||||
} from '../utils';
|
||||
import { Attribute } from '../utils/types';
|
||||
import { translateApiErrors as getApiError } from '../utils/apiErrors';
|
||||
import { buildShortDescriptionPrompt } from '../product-short-description/product-short-description-button-container';
|
||||
|
||||
const DESCRIPTION_MAX_LENGTH = 300;
|
||||
|
||||
const getApiError = ( error?: string ) => {
|
||||
switch ( error ) {
|
||||
case 'connection_error':
|
||||
return __(
|
||||
'❗ We were unable to reach the experimental service. Please check back in shortly.',
|
||||
'woocommerce'
|
||||
);
|
||||
default:
|
||||
return __(
|
||||
`❗ We encountered an issue with this experimental feature. Please check back in shortly.`,
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const recordDescriptionTracks = recordTracksFactory(
|
||||
'description_completion',
|
||||
() => ( {
|
||||
|
@ -161,12 +148,16 @@ export function WriteItForMeButtonContainer() {
|
|||
);
|
||||
};
|
||||
|
||||
title?.addEventListener( 'keyup', updateTitleHandler );
|
||||
title?.addEventListener( 'change', updateTitleHandler );
|
||||
// We have to keep track of manually typing, pasting, undo/redo, and when description is generated.
|
||||
const eventsToTrack = [ 'keyup', 'change', 'undo', 'redo', 'paste' ];
|
||||
for ( const event of eventsToTrack ) {
|
||||
title?.addEventListener( event, updateTitleHandler );
|
||||
}
|
||||
|
||||
return () => {
|
||||
title?.removeEventListener( 'keyup', updateTitleHandler );
|
||||
title?.removeEventListener( 'change', updateTitleHandler );
|
||||
for ( const event of eventsToTrack ) {
|
||||
title?.removeEventListener( event, updateTitleHandler );
|
||||
}
|
||||
};
|
||||
}, [ titleEl ] );
|
||||
|
||||
|
@ -257,19 +248,11 @@ export function WriteItForMeButtonContainer() {
|
|||
|
||||
try {
|
||||
await requestCompletion( prompt );
|
||||
if ( ! shortTinyEditor.getContent() || shortDescriptionGenerated ) {
|
||||
await requestShortCompletion(
|
||||
[
|
||||
'Please write a high-converting Meta Description for the WooCommerce product description below.',
|
||||
'It should strictly adhere to the following guidelines:',
|
||||
'It should entice someone from a search results page to click on the product link.',
|
||||
'It should be no more than 155 characters so that the entire meta description fits within the space provided by the search engine result without being cut off or truncated.',
|
||||
'It should explain what users will see if they click on the product page link.',
|
||||
'Do not wrap in double quotes or use any other special characters.',
|
||||
`It should include the target keyword for the product.`,
|
||||
`Here is the full product description: \n${ tinyEditor.getContent() }`,
|
||||
].join( '\n' )
|
||||
);
|
||||
const longDescription = tinyEditor.getContent();
|
||||
if ( ! longDescription || shortDescriptionGenerated ) {
|
||||
const shortDescriptionPrompt =
|
||||
buildShortDescriptionPrompt( longDescription );
|
||||
await requestShortCompletion( shortDescriptionPrompt );
|
||||
setShortDescriptionGenerated( true );
|
||||
}
|
||||
} catch ( err ) {
|
||||
|
@ -280,9 +263,31 @@ export function WriteItForMeButtonContainer() {
|
|||
return completionActive ? (
|
||||
<StopCompletionBtn onClick={ stopCompletion } />
|
||||
) : (
|
||||
<>
|
||||
<WriteItForMeBtn
|
||||
disabled={ ! writeItForMeEnabled }
|
||||
onClick={ onWriteItForMeClick }
|
||||
disabledMessage={ sprintf(
|
||||
/* translators: %d: Message shown when short description button is disabled because of a minimum description length */
|
||||
__(
|
||||
'Please create a product title before generating a description. It must be at least %d characters long.',
|
||||
'woocommerce'
|
||||
),
|
||||
MIN_TITLE_LENGTH_FOR_DESCRIPTION
|
||||
) }
|
||||
/>
|
||||
{ shortDescriptionGenerated && (
|
||||
<InfoModal
|
||||
id="shortDescriptionGenerated"
|
||||
// message should be translatable.
|
||||
message={ __(
|
||||
'The short description was automatically generated by AI using the long description. This normally appears at the top of your product pages.',
|
||||
'woocommerce'
|
||||
) }
|
||||
// title should also be translatable.
|
||||
title={ __( 'Short Description Generated', 'woocommerce' ) }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './product-short-description-button-container';
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import {
|
||||
__experimentalUseCompletion as useCompletion,
|
||||
UseCompletionError,
|
||||
} from '@woocommerce/ai';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
MIN_DESC_LENGTH_FOR_SHORT_DESC,
|
||||
WOO_AI_PLUGIN_FEATURE_NAME,
|
||||
} from '../constants';
|
||||
import { StopCompletionBtn, WriteItForMeBtn } from '../components';
|
||||
import { useTinyEditor } from '../hooks';
|
||||
import { getPostId, recordTracksFactory } from '../utils';
|
||||
import { translateApiErrors as getApiError } from '../utils/apiErrors';
|
||||
|
||||
const recordShortDescriptionTracks = recordTracksFactory(
|
||||
'short_description_completion',
|
||||
() => ( {
|
||||
post_id: getPostId(),
|
||||
} )
|
||||
);
|
||||
|
||||
export function buildShortDescriptionPrompt( longDesc: string ) {
|
||||
return [
|
||||
'Please write a high-converting Meta Description for the WooCommerce product description below.',
|
||||
'It should strictly adhere to the following guidelines:',
|
||||
'It should entice someone from a search results page to click on the product link.',
|
||||
'It should be no more than 155 characters so that the entire meta description fits within the space provided by the search engine result without being cut off or truncated.',
|
||||
'It should explain what users will see if they click on the product page link.',
|
||||
'Do not wrap in double quotes or use any other special characters.',
|
||||
'It should include the target keyword for the product.',
|
||||
`Here is the full product description: \n${ longDesc }`,
|
||||
].join( '\n' );
|
||||
}
|
||||
|
||||
export function WriteShortDescriptionButtonContainer() {
|
||||
const { createWarningNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
const [ fetching, setFetching ] = useState< boolean >( false );
|
||||
const tinyEditor = useTinyEditor();
|
||||
const shortTinyEditor = useTinyEditor( 'excerpt' );
|
||||
const [ postContent, setPostContent ] = useState( '' );
|
||||
|
||||
const handleUseCompletionError = ( err: UseCompletionError ) => {
|
||||
createWarningNotice( getApiError( err.code ?? '' ) );
|
||||
setFetching( false );
|
||||
// eslint-disable-next-line no-console
|
||||
console.error( err );
|
||||
};
|
||||
|
||||
const { requestCompletion, completionActive, stopCompletion } =
|
||||
useCompletion( {
|
||||
feature: WOO_AI_PLUGIN_FEATURE_NAME,
|
||||
onStreamMessage: ( content ) => {
|
||||
shortTinyEditor.setContent( content );
|
||||
},
|
||||
onStreamError: handleUseCompletionError,
|
||||
onCompletionFinished: ( reason, content ) => {
|
||||
recordShortDescriptionTracks( 'stop', {
|
||||
reason,
|
||||
character_count: content.length,
|
||||
} );
|
||||
|
||||
setFetching( false );
|
||||
},
|
||||
} );
|
||||
|
||||
useEffect( () => {
|
||||
recordShortDescriptionTracks( 'view_button' );
|
||||
}, [] );
|
||||
|
||||
// This effect sets up the 'init' listener to update the 'isEditorReady' state
|
||||
useEffect( () => {
|
||||
const editor = tinyEditor.getEditorObject();
|
||||
if ( editor ) {
|
||||
// Set the content on initial page load.
|
||||
setPostContent( tinyEditor.getContent() );
|
||||
// Register a listener for the tinyMCE editor to update the postContent state.
|
||||
const changeHandler = () => {
|
||||
setPostContent( tinyEditor.getContent() );
|
||||
};
|
||||
editor.on( 'input', changeHandler );
|
||||
editor.on( 'change', changeHandler );
|
||||
return () => {
|
||||
editor.off( 'input', changeHandler );
|
||||
editor.off( 'change', changeHandler );
|
||||
};
|
||||
}
|
||||
}, [ tinyEditor ] );
|
||||
|
||||
const writeItForMeEnabled =
|
||||
! fetching && postContent.length >= MIN_DESC_LENGTH_FOR_SHORT_DESC;
|
||||
const onWriteItForMeClick = async () => {
|
||||
setFetching( true );
|
||||
|
||||
const prompt = buildShortDescriptionPrompt( tinyEditor.getContent() );
|
||||
recordShortDescriptionTracks( 'start', {
|
||||
prompt,
|
||||
} );
|
||||
|
||||
try {
|
||||
await requestCompletion( prompt );
|
||||
} catch ( err ) {
|
||||
handleUseCompletionError( err as UseCompletionError );
|
||||
}
|
||||
};
|
||||
|
||||
return completionActive ? (
|
||||
<StopCompletionBtn onClick={ stopCompletion } />
|
||||
) : (
|
||||
<WriteItForMeBtn
|
||||
disabled={ ! writeItForMeEnabled }
|
||||
onClick={ onWriteItForMeClick }
|
||||
disabledMessage={ sprintf(
|
||||
/* translators: %d: Message shown when short description button is disabled because of a minimum description length */
|
||||
__(
|
||||
'Please write a product description before generating a short description. It must be at least %d characters long.',
|
||||
'woocommerce'
|
||||
),
|
||||
MIN_DESC_LENGTH_FOR_SHORT_DESC
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
// Why do we need this but the other button works without it?
|
||||
div#woocommerce-ai-app-product-short-description-gpt-button {
|
||||
display: inline-block;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export const translateApiErrors = ( error?: string ) => {
|
||||
switch ( error ) {
|
||||
case 'connection_error':
|
||||
return __(
|
||||
'❗ We were unable to reach the experimental service. Please check back in shortly.',
|
||||
'woocommerce'
|
||||
);
|
||||
default:
|
||||
return __(
|
||||
`❗ We encountered an issue with this experimental feature. Please check back in shortly.`,
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { store as preferencesStore } from '@wordpress/preferences';
|
||||
|
||||
const setPreferencesPersistence = () =>
|
||||
dispatch( preferencesStore ).setPersistenceLayer( {
|
||||
get: async () => {
|
||||
const savedPreferences = window.localStorage.getItem(
|
||||
'woo-ai-plugin-prefs'
|
||||
);
|
||||
return savedPreferences ? JSON.parse( savedPreferences ) : {};
|
||||
},
|
||||
set: ( preferences: object ) => {
|
||||
window.localStorage.setItem(
|
||||
'woo-ai-plugin-prefs',
|
||||
JSON.stringify( preferences )
|
||||
);
|
||||
},
|
||||
} );
|
||||
|
||||
export default setPreferencesPersistence;
|
|
@ -1,7 +1,9 @@
|
|||
type TinyContent = {
|
||||
export type TinyContent = {
|
||||
getContent: () => string;
|
||||
setContent: ( str: string ) => void;
|
||||
id: string;
|
||||
on: ( event: string, callback: ( event: Event ) => void ) => void;
|
||||
off: ( event: string, callback: ( event: Event ) => void ) => void;
|
||||
};
|
||||
|
||||
declare const tinymce: {
|
||||
|
@ -9,7 +11,7 @@ declare const tinymce: {
|
|||
editors: TinyContent[];
|
||||
};
|
||||
|
||||
const getTinyContentObject = ( editorId = 'content' ) =>
|
||||
export const getTinyContentObject = ( editorId = 'content' ) =>
|
||||
typeof tinymce === 'object'
|
||||
? tinymce.editors.find(
|
||||
( editor: { id: string } ) => editor.id === editorId
|
||||
|
@ -38,5 +40,5 @@ export const setTinyContent = ( str: string, editorId?: string ) => {
|
|||
};
|
||||
|
||||
export const getTinyContent = ( editorId?: string ) => {
|
||||
return getTinyContentObject( editorId )?.getContent();
|
||||
return getTinyContentObject( editorId )?.getContent() ?? '';
|
||||
};
|
||||
|
|
|
@ -23,6 +23,11 @@ module.exports = {
|
|||
},
|
||||
resolve: {
|
||||
extensions: [ '.js', '.jsx', '.tsx', '.ts' ],
|
||||
fallback: {
|
||||
stream: false,
|
||||
path: false,
|
||||
fs: false,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
...defaultConfig.plugins.filter(
|
||||
|
|
788
pnpm-lock.yaml
788
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue