From 144bf08293c7505772839235150d10e97fd5d8f9 Mon Sep 17 00:00:00 2001
From: Joel Thiessen <444632+joelclimbsthings@users.noreply.github.com>
Date: Mon, 7 Aug 2023 05:03:52 -0700
Subject: [PATCH] Generating the short description on the product editor
(#39237)
* [Woo AI] Generate short description after long description is generated.
---
docs/snippets/README.md | 2 +-
.../add-update-short-description-39151 | 4 +
packages/js/ai/src/hooks/useCompletion.ts | 11 ++-
.../add-update-short-description-39151 | 4 +
plugins/woo-ai/src/customer.d.ts | 2 +
.../woo-ai/src/hooks/useFeedbackSnackbar.tsx | 99 +++++++++----------
plugins/woo-ai/src/hooks/useTinyEditor.ts | 7 +-
.../product-description-button-container.tsx | 51 ++++++++--
plugins/woo-ai/src/utils/tiny-tools.ts | 23 +++--
9 files changed, 135 insertions(+), 68 deletions(-)
create mode 100644 packages/js/ai/changelog/add-update-short-description-39151
create mode 100644 plugins/woo-ai/changelog/add-update-short-description-39151
diff --git a/docs/snippets/README.md b/docs/snippets/README.md
index 4b2f80396f8..b7ad765ea5a 100644
--- a/docs/snippets/README.md
+++ b/docs/snippets/README.md
@@ -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);
\ No newline at end of file
+- [Unhook and remove WooCommerce emails](./unhook--remove-woocommerce-emails.md)
\ No newline at end of file
diff --git a/packages/js/ai/changelog/add-update-short-description-39151 b/packages/js/ai/changelog/add-update-short-description-39151
new file mode 100644
index 00000000000..7451d3b0357
--- /dev/null
+++ b/packages/js/ai/changelog/add-update-short-description-39151
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Returning promise from requestCompletion instead of event source.
diff --git a/packages/js/ai/src/hooks/useCompletion.ts b/packages/js/ai/src/hooks/useCompletion.ts
index 228d061d5c7..790f3d68f1a 100644
--- a/packages/js/ai/src/hooks/useCompletion.ts
+++ b/packages/js/ai/src/hooks/useCompletion.ts
@@ -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',
diff --git a/plugins/woo-ai/changelog/add-update-short-description-39151 b/plugins/woo-ai/changelog/add-update-short-description-39151
new file mode 100644
index 00000000000..1f6c9d941d7
--- /dev/null
+++ b/plugins/woo-ai/changelog/add-update-short-description-39151
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Generating short description after long description on product editor.
diff --git a/plugins/woo-ai/src/customer.d.ts b/plugins/woo-ai/src/customer.d.ts
index 4333ece7830..ad01f9d04a5 100644
--- a/plugins/woo-ai/src/customer.d.ts
+++ b/plugins/woo-ai/src/customer.d.ts
@@ -2,3 +2,5 @@ declare module '*.svg' {
const content: string;
export default content;
}
+
+declare module '@wordpress/data';
diff --git a/plugins/woo-ai/src/hooks/useFeedbackSnackbar.tsx b/plugins/woo-ai/src/hooks/useFeedbackSnackbar.tsx
index a1724423035..b605498c7fc 100644
--- a/plugins/woo-ai/src/hooks/useFeedbackSnackbar.tsx
+++ b/plugins/woo-ai/src/hooks/useFeedbackSnackbar.tsx
@@ -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: (
-
- 👎
-
- ),
- }
- ),
- 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: (
+
+ 👎
+
+ ),
+ }
+ ),
+ 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 {
diff --git a/plugins/woo-ai/src/hooks/useTinyEditor.ts b/plugins/woo-ai/src/hooks/useTinyEditor.ts
index 1fe98619c8d..3d27e861f01 100644
--- a/plugins/woo-ai/src/hooks/useTinyEditor.ts
+++ b/plugins/woo-ai/src/hooks/useTinyEditor.ts
@@ -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 ),
+ };
};
diff --git a/plugins/woo-ai/src/product-description/product-description-button-container.tsx b/plugins/woo-ai/src/product-description/product-description-button-container.tsx
index 0c0044749fb..6d86f7df3df 100644
--- a/plugins/woo-ai/src/product-description/product-description-button-container.tsx
+++ b/plugins/woo-ai/src/product-description/product-description-button-container.tsx
@@ -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 );
}
};
diff --git a/plugins/woo-ai/src/utils/tiny-tools.ts b/plugins/woo-ai/src/utils/tiny-tools.ts
index 89c3f291a81..c183742f464 100644
--- a/plugins/woo-ai/src/utils/tiny-tools.ts
+++ b/plugins/woo-ai/src/utils/tiny-tools.ts
@@ -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();
};