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(); };