Generating the short description on the product editor (#39237)

* [Woo AI] Generate short description after long description is generated.
This commit is contained in:
Joel Thiessen 2023-08-07 05:03:52 -07:00 committed by GitHub
parent c4f00719de
commit 144bf08293
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 135 additions and 68 deletions

View File

@ -4,4 +4,4 @@ Various code snippets you can add to your site to enable custom functionality:
- [Add a message above the login / register form](./before-login--register-form.md)
- [Change number of related products output](./number-of-products-per-row.md)
- [Unhook and remove WooCommerce emails](./unhook--remove-woocommerce-emails.md);
- [Unhook and remove WooCommerce emails](./unhook--remove-woocommerce-emails.md)

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Returning promise from requestCompletion instead of event source.

View File

@ -109,7 +109,16 @@ export const useCompletion = ( {
completionSource.current = suggestionsSource;
return suggestionsSource;
return new Promise( ( resolve ) => {
( completionSource.current as EventSource ).addEventListener(
'message',
( event: MessageEvent ) => {
if ( event.data === '[DONE]' ) {
resolve( event.data );
}
}
);
} );
} catch ( e ) {
throw createExtendedError(
'An error occurred while connecting to the completion service',

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Generating short description after long description on product editor.

View File

@ -2,3 +2,5 @@ declare module '*.svg' {
const content: string;
export default content;
}
declare module '@wordpress/data';

View File

@ -3,10 +3,6 @@
*/
import React from 'react';
import { createInterpolateElement, useState } from '@wordpress/element';
// TODO: Re-add "@types/wordpress__data" package to resolve this, causing other issues until pnpm 8.6.0 is usable
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @woocommerce/dependency-group
import { useDispatch } from '@wordpress/data';
type ShowSnackbarProps = {
@ -28,57 +24,60 @@ export const useFeedbackSnackbar = () => {
onPositiveResponse,
onNegativeResponse,
}: ShowSnackbarProps ) => {
const noticePromise: unknown = createNotice( 'info', label, {
type: 'snackbar',
explicitDismiss: true,
actions: [
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
label: createInterpolateElement(
'<ThumbsUp /> <ThumbsDown />',
{
ThumbsUp: (
<span
className="woo-ai-feedback-snackbar-action"
data-response="positive"
>
👍
</span>
),
ThumbsDown: (
<span
className="woo-ai-feedback-snackbar-action"
data-response="negative"
>
👎
</span>
),
}
),
onClick: ( e: React.MouseEvent< HTMLButtonElement > ) => {
const response = (
e.target as HTMLSpanElement
).getAttribute( 'data-response' );
const noticePromise: Promise< NoticeItem > = createNotice(
'info',
label,
{
type: 'snackbar',
explicitDismiss: true,
actions: [
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
label: createInterpolateElement(
'<ThumbsUp /> <ThumbsDown />',
{
ThumbsUp: (
<span
className="woo-ai-feedback-snackbar-action"
data-response="positive"
>
👍
</span>
),
ThumbsDown: (
<span
className="woo-ai-feedback-snackbar-action"
data-response="negative"
>
👎
</span>
),
}
),
onClick: (
e: React.MouseEvent< HTMLButtonElement >
) => {
const response = (
e.target as HTMLSpanElement
).getAttribute( 'data-response' );
if ( response === 'positive' ) {
onPositiveResponse();
}
if ( response === 'positive' ) {
onPositiveResponse();
}
if ( response === 'negative' ) {
onNegativeResponse();
}
if ( response === 'negative' ) {
onNegativeResponse();
}
},
},
},
],
} );
( noticePromise as Promise< NoticeItem > ).then(
( item: NoticeItem ) => {
setNoticeId( item.notice.id );
],
}
);
return noticePromise as Promise< NoticeItem >;
return noticePromise.then( ( item: NoticeItem ) => {
setNoticeId( item.notice.id );
} );
};
return {

View File

@ -3,6 +3,9 @@
*/
import { setTinyContent, getTinyContent } from '../utils/tiny-tools';
export const useTinyEditor = () => {
return { setContent: setTinyContent, getContent: getTinyContent };
export const useTinyEditor = ( editorId?: string ) => {
return {
setContent: ( str: string ) => setTinyContent( str, editorId ),
getContent: () => getTinyContent( editorId ),
};
};

View File

@ -7,6 +7,7 @@ import {
__experimentalUseCompletion as useCompletion,
UseCompletionError,
} from '@woocommerce/ai';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
@ -30,7 +31,7 @@ import { Attribute } from '../utils/types';
const DESCRIPTION_MAX_LENGTH = 300;
const getApiError = ( error: string ) => {
const getApiError = ( error?: string ) => {
switch ( error ) {
case 'connection_error':
return __(
@ -39,7 +40,7 @@ const getApiError = ( error: string ) => {
);
default:
return __(
`❗ We're currently experiencing high demand for our experimental feature. Please check back in shortly.`,
`❗ We encountered an issue with this experimental feature. Please check back in shortly.`,
'woocommerce'
);
}
@ -53,19 +54,29 @@ const recordDescriptionTracks = recordTracksFactory(
);
export function WriteItForMeButtonContainer() {
const { createWarningNotice } = useDispatch( 'core/notices' );
const titleEl = useRef< HTMLInputElement >(
document.querySelector( '#title' )
);
const [ fetching, setFetching ] = useState< boolean >( false );
const [ shortDescriptionGenerated, setShortDescriptionGenerated ] =
useState< boolean >( false );
const [ productTitle, setProductTitle ] = useState< string >(
titleEl.current?.value || ''
);
const tinyEditor = useTinyEditor();
const handleCompletionError = ( error: UseCompletionError ) =>
tinyEditor.setContent( getApiError( error.code ?? '' ) );
const shortTinyEditor = useTinyEditor( 'excerpt' );
const { showSnackbar, removeSnackbar } = useFeedbackSnackbar();
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,
@ -76,7 +87,7 @@ export function WriteItForMeButtonContainer() {
tinyEditor.setContent( content );
}
},
onStreamError: handleCompletionError,
onStreamError: handleUseCompletionError,
onCompletionFinished: ( reason, content ) => {
recordDescriptionTracks( 'stop', {
reason,
@ -107,6 +118,17 @@ export function WriteItForMeButtonContainer() {
},
} );
const { requestCompletion: requestShortCompletion } = useCompletion( {
feature: WOO_AI_PLUGIN_FEATURE_NAME,
onStreamMessage: ( content ) => shortTinyEditor.setContent( content ),
onStreamError: handleUseCompletionError,
onCompletionFinished: ( reason, content ) => {
if ( reason === 'finished' ) {
shortTinyEditor.setContent( content );
}
},
} );
useEffect( () => {
const title = titleEl.current;
@ -189,8 +211,23 @@ 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' )
);
setShortDescriptionGenerated( true );
}
} catch ( err ) {
handleCompletionError( err as UseCompletionError );
handleUseCompletionError( err as UseCompletionError );
}
};

View File

@ -1,19 +1,28 @@
type TinyContent = {
getContent: () => string;
setContent: ( str: string ) => void;
id: string;
};
declare const tinymce: { get: ( str: string ) => TinyContent };
declare const tinymce: {
get: ( str: string ) => TinyContent;
editors: TinyContent[];
};
const getTinyContentObject = () =>
typeof tinymce === 'object' ? tinymce.get( 'content' ) : null;
const getTinyContentObject = ( editorId = 'content' ) =>
typeof tinymce === 'object'
? tinymce.editors.find(
( editor: { id: string } ) => editor.id === editorId
)
: null;
export const setTinyContent = ( str: string ) => {
export const setTinyContent = ( str: string, editorId?: string ) => {
if ( ! str.length ) {
return;
}
const contentTinyMCE = getTinyContentObject();
const contentTinyMCE = getTinyContentObject( editorId );
if ( contentTinyMCE ) {
contentTinyMCE.setContent( str );
} else {
@ -28,6 +37,6 @@ export const setTinyContent = ( str: string ) => {
}
};
export const getTinyContent = () => {
return getTinyContentObject()?.getContent();
export const getTinyContent = ( editorId?: string ) => {
return getTinyContentObject( editorId )?.getContent();
};