2023-09-21 05:30:57 +00:00
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
2023-09-06 06:21:09 +00:00
/ * *
* External dependencies
* /
import { __experimentalRequestJetpackToken as requestJetpackToken } from '@woocommerce/ai' ;
import apiFetch from '@wordpress/api-fetch' ;
2023-09-21 05:30:57 +00:00
import { OPTIONS_STORE_NAME } from '@woocommerce/data' ;
2023-09-28 03:15:38 +00:00
import { Sender , assign , createMachine , actions } from 'xstate' ;
2023-09-21 05:30:57 +00:00
import { dispatch , resolveSelect } from '@wordpress/data' ;
// @ts-ignore No types for this exist yet.
import { store as coreStore } from '@wordpress/core-data' ;
// @ts-ignore No types for this exist yet.
import { mergeBaseAndUserConfigs } from '@wordpress/edit-site/build-module/components/global-styles/global-styles-provider' ;
2023-09-06 06:21:09 +00:00
/ * *
* Internal dependencies
* /
2023-09-19 08:41:52 +00:00
import { designWithAiStateMachineContext } from './types' ;
2023-09-21 05:30:57 +00:00
import { FONT_PAIRINGS } from '../assembler-hub/sidebar/global-styles/font-pairing-variations/constants' ;
import { COLOR_PALETTES } from '../assembler-hub/sidebar/global-styles/color-palette-variations/constants' ;
2023-09-25 10:30:31 +00:00
import { HOMEPAGE_TEMPLATES } from '../data/homepageTemplates' ;
2024-01-11 14:32:16 +00:00
import { updateTemplate } from '../data/actions' ;
2024-01-17 14:09:12 +00:00
import { installAndActivateTheme as setTheme } from '../data/service' ;
import { THEME_SLUG } from '../data/constants' ;
2024-04-23 17:38:06 +00:00
import { trackEvent } from '../tracking' ;
2024-06-10 13:00:33 +00:00
import { installPatterns } from '../design-without-ai/services' ;
2023-09-07 09:05:47 +00:00
2023-09-28 03:15:38 +00:00
const { escalate } = actions ;
2023-09-19 08:41:52 +00:00
const browserPopstateHandler =
( ) = > ( sendBack : Sender < { type : 'EXTERNAL_URL_UPDATE' } > ) = > {
const popstateHandler = ( ) = > {
sendBack ( { type : 'EXTERNAL_URL_UPDATE' } ) ;
} ;
window . addEventListener ( 'popstate' , popstateHandler ) ;
return ( ) = > {
window . removeEventListener ( 'popstate' , popstateHandler ) ;
} ;
} ;
export const getCompletion = async < ValidResponseObject > ( {
queryId ,
prompt ,
version ,
responseValidation ,
retryCount ,
2023-11-15 09:46:20 +00:00
abortSignal = AbortSignal . timeout ( 10000 ) ,
2023-09-19 08:41:52 +00:00
} : {
queryId : string ;
prompt : string ;
version : string ;
responseValidation : ( arg0 : string ) = > ValidResponseObject ;
retryCount : number ;
2023-11-15 09:46:20 +00:00
abortSignal? : AbortSignal ;
2023-09-19 08:41:52 +00:00
} ) = > {
const { token } = await requestJetpackToken ( ) ;
let data : {
completion : string ;
} ;
let parsedCompletionJson ;
try {
const url = new URL (
'https://public-api.wordpress.com/wpcom/v2/text-completion'
) ;
2023-09-07 09:05:47 +00:00
2023-09-19 08:41:52 +00:00
url . searchParams . append ( 'feature' , 'woo_cys' ) ;
2023-09-07 09:05:47 +00:00
2023-09-19 08:41:52 +00:00
data = await apiFetch ( {
url : url.toString ( ) ,
method : 'POST' ,
data : {
token ,
prompt ,
_fields : 'completion' ,
} ,
2023-11-15 09:46:20 +00:00
signal : abortSignal ,
2023-09-19 08:41:52 +00:00
} ) ;
} catch ( error ) {
2024-04-23 17:38:06 +00:00
trackEvent ( 'customize_your_store_ai_completion_api_error' , {
2023-09-19 08:41:52 +00:00
query_id : queryId ,
version ,
retry_count : retryCount ,
error_type : 'api_request_error' ,
} ) ;
throw error ;
}
2023-09-07 09:05:47 +00:00
try {
2023-09-19 08:41:52 +00:00
parsedCompletionJson = JSON . parse ( data . completion ) ;
2023-09-07 09:05:47 +00:00
} catch {
2024-04-23 17:38:06 +00:00
trackEvent ( 'customize_your_store_ai_completion_response_error' , {
2023-09-19 08:41:52 +00:00
query_id : queryId ,
version ,
retry_count : retryCount ,
error_type : 'json_parse_error' ,
response : data.completion ,
} ) ;
throw new Error (
` Error validating Jetpack AI text completions response for ${ queryId } `
2023-09-07 09:05:47 +00:00
) ;
}
2023-09-19 08:41:52 +00:00
try {
const validatedResponse = responseValidation ( parsedCompletionJson ) ;
2024-04-23 17:38:06 +00:00
trackEvent ( 'customize_your_store_ai_completion_success' , {
2023-09-19 08:41:52 +00:00
query_id : queryId ,
version ,
retry_count : retryCount ,
} ) ;
return validatedResponse ;
} catch ( error ) {
2024-04-23 17:38:06 +00:00
trackEvent ( 'customize_your_store_ai_completion_response_error' , {
2023-09-19 08:41:52 +00:00
query_id : queryId ,
version ,
retry_count : retryCount ,
2023-09-07 09:05:47 +00:00
error_type : 'valid_json_invalid_values' ,
2023-09-19 08:41:52 +00:00
response : data.completion ,
} ) ;
throw error ;
}
2023-09-07 09:05:47 +00:00
} ;
2023-09-06 06:21:09 +00:00
2023-09-19 08:41:52 +00:00
export const queryAiEndpoint = createMachine (
{
id : 'query-ai-endpoint' ,
predictableActionArguments : true ,
initial : 'init' ,
context : {
// these values are all overwritten by incoming parameters
prompt : '' ,
queryId : '' ,
version : '' ,
responseValidation : ( ) = > true ,
retryCount : 0 ,
validatedResponse : { } as unknown ,
} ,
states : {
init : {
always : 'querying' ,
entry : [ 'setRetryCount' ] ,
} ,
querying : {
invoke : {
src : 'getCompletion' ,
onDone : {
target : 'success' ,
actions : [ 'handleAiResponse' ] ,
} ,
onError : {
target : 'error' ,
} ,
} ,
} ,
error : {
always : [
{
cond : ( context ) = > context . retryCount >= 3 ,
2023-09-28 03:15:38 +00:00
target : 'querying' ,
actions : [
// Throw an error to be caught by the parent machine.
escalate ( ( ) = > ( {
data : 'Max retries exceeded' ,
} ) ) ,
] ,
2023-09-19 08:41:52 +00:00
} ,
{
target : 'querying' ,
actions : assign ( {
retryCount : ( context ) = > context . retryCount + 1 ,
} ) ,
} ,
] ,
} ,
success : {
type : 'final' ,
data : ( context ) = > {
return {
result : 'success' ,
response : context.validatedResponse ,
} ;
} ,
} ,
} ,
} ,
{
actions : {
handleAiResponse : assign ( {
validatedResponse : ( _context , event : unknown ) = >
( event as { data : unknown } ) . data ,
} ) ,
setRetryCount : assign ( {
retryCount : 0 ,
} ) ,
} ,
services : {
getCompletion ,
} ,
}
) ;
2023-09-12 06:32:50 +00:00
2024-01-04 11:11:00 +00:00
const resetPatternsAndProducts = ( ) = > async ( ) = > {
await dispatch ( OPTIONS_STORE_NAME ) . updateOptions ( {
woocommerce_blocks_allow_ai_connection : 'yes' ,
} ) ;
const response = await apiFetch < {
is_ai_generated : boolean ;
} > ( {
path : '/wc/private/ai/store-info' ,
method : 'GET' ,
} ) ;
if ( response . is_ai_generated ) {
return ;
}
return Promise . all ( [
apiFetch ( {
path : '/wc/private/ai/patterns' ,
method : 'DELETE' ,
} ) ,
apiFetch ( {
path : '/wc/private/ai/products' ,
method : 'DELETE' ,
} ) ,
] ) ;
} ;
2023-09-21 05:30:57 +00:00
export const updateStorePatterns = async (
context : designWithAiStateMachineContext
) = > {
try {
// TODO: Probably move this to a more appropriate place with a check. We should set this when the user granted permissions during the onboarding phase.
await dispatch ( OPTIONS_STORE_NAME ) . updateOptions ( {
2023-12-06 12:49:28 +00:00
woocommerce_blocks_allow_ai_connection : 'yes' ,
2023-09-21 05:30:57 +00:00
} ) ;
2023-11-06 10:51:11 +00:00
const { images } = await apiFetch < {
2023-09-26 12:34:47 +00:00
ai_content_generated : boolean ;
2024-01-05 16:56:51 +00:00
images : { images : Array < unknown > ; search_term : string } ;
2023-11-06 10:51:11 +00:00
} > ( {
path : '/wc/private/ai/images' ,
2023-09-21 05:30:57 +00:00
method : 'POST' ,
data : {
business_description :
context . businessInfoDescription . descriptionText ,
} ,
} ) ;
2023-09-26 12:34:47 +00:00
2024-01-05 16:56:51 +00:00
const { is_ai_generated } = await apiFetch < {
is_ai_generated : boolean ;
} > ( {
path : '/wc/private/ai/store-info' ,
method : 'GET' ,
} ) ;
2024-04-29 08:24:48 +00:00
if ( ! images ) {
if ( ! is_ai_generated ) {
2024-01-05 16:56:51 +00:00
throw new Error (
'AI content not generated: images not available'
) ;
}
2024-01-04 11:11:00 +00:00
await resetPatternsAndProducts ( ) ( ) ;
return ;
}
2023-11-06 10:51:11 +00:00
const [ response ] = await Promise . all < {
ai_content_generated : boolean ;
product_content : Array < {
title : string ;
description : string ;
image : {
src : string ;
alt : string ;
} ;
} > ;
additional_errors? : unknown [ ] ;
} > ( [
apiFetch ( {
path : '/wc/private/ai/products' ,
method : 'POST' ,
data : {
business_description :
context . businessInfoDescription . descriptionText ,
images ,
} ,
} ) ,
apiFetch ( {
path : '/wc/private/ai/patterns' ,
method : 'POST' ,
data : {
business_description :
context . businessInfoDescription . descriptionText ,
images ,
} ,
} ) ,
] ) ;
const productContents = response . product_content . map (
( product , index ) = > {
return apiFetch ( {
path : '/wc/private/ai/product' ,
method : 'POST' ,
data : {
products_information : product ,
2023-12-07 10:55:35 +00:00
last_product :
index === response . product_content . length - 1 ,
2023-11-06 10:51:11 +00:00
} ,
} ) ;
}
) ;
await Promise . all ( [
. . . productContents ,
apiFetch ( {
path : '/wc/private/ai/business-description' ,
method : 'POST' ,
data : {
business_description :
context . businessInfoDescription . descriptionText ,
} ,
} ) ,
2023-11-24 13:40:37 +00:00
apiFetch ( {
path : '/wc/private/ai/store-title' ,
method : 'POST' ,
data : {
business_description :
context . businessInfoDescription . descriptionText ,
} ,
} ) ,
2023-11-06 10:51:11 +00:00
] ) ;
2023-09-26 12:34:47 +00:00
if ( ! response . ai_content_generated ) {
throw new Error (
'AI content not generated: ' + response . additional_errors
? JSON . stringify ( response . additional_errors )
: ''
) ;
}
2023-09-21 05:30:57 +00:00
} catch ( error ) {
2024-04-23 17:38:06 +00:00
trackEvent ( 'customize_your_store_update_store_pattern_api_error' , {
2023-09-21 05:30:57 +00:00
error : error instanceof Error ? error . message : 'unknown' ,
} ) ;
throw error ;
}
} ;
// Update the current global styles of theme
const updateGlobalStyles = async ( {
colorPaletteName = COLOR_PALETTES [ 0 ] . title ,
fontPairingName = FONT_PAIRINGS [ 0 ] . title ,
} : {
colorPaletteName : string ;
fontPairingName : string ;
} ) = > {
const colorPalette = COLOR_PALETTES . find (
( palette ) = > palette . title === colorPaletteName
) ;
const fontPairing = FONT_PAIRINGS . find (
( pairing ) = > pairing . title === fontPairingName
) ;
2023-11-06 07:46:28 +00:00
// @ts-ignore No types for this exist yet.
const { invalidateResolutionForStoreSelector } = dispatch ( coreStore ) ;
invalidateResolutionForStoreSelector (
'__experimentalGetCurrentGlobalStylesId'
) ;
2023-09-21 05:30:57 +00:00
const globalStylesId = await resolveSelect (
coreStore
// @ts-ignore No types for this exist yet.
) . __experimentalGetCurrentGlobalStylesId ( ) ;
// @ts-ignore No types for this exist yet.
const { saveEntityRecord } = dispatch ( coreStore ) ;
await saveEntityRecord (
'root' ,
'globalStyles' ,
{
id : globalStylesId ,
styles : mergeBaseAndUserConfigs (
colorPalette ? . styles || { } ,
fontPairing ? . styles || { }
) ,
settings : mergeBaseAndUserConfigs (
colorPalette ? . settings || { } ,
fontPairing ? . settings || { }
) ,
} ,
{
throwOnError : true ,
}
) ;
} ;
export const assembleSite = async (
context : designWithAiStateMachineContext
) = > {
try {
await updateGlobalStyles ( {
colorPaletteName : context.aiSuggestions.defaultColorPalette.default ,
fontPairingName : context.aiSuggestions.fontPairing ,
} ) ;
2024-04-23 17:38:06 +00:00
trackEvent ( 'customize_your_store_ai_update_global_styles_success' ) ;
2023-09-21 05:30:57 +00:00
} catch ( error ) {
// eslint-disable-next-line no-console
console . error ( error ) ;
2024-04-23 17:38:06 +00:00
trackEvent (
2023-09-21 05:30:57 +00:00
'customize_your_store_ai_update_global_styles_response_error' ,
{
error : error instanceof Error ? error . message : 'unknown' ,
}
) ;
2023-11-06 07:46:28 +00:00
throw error ;
2023-09-21 05:30:57 +00:00
}
try {
await updateTemplate ( {
// TODO: Get from context
2023-09-25 10:30:31 +00:00
homepageTemplateId : context.aiSuggestions
. homepageTemplate as keyof typeof HOMEPAGE_TEMPLATES ,
2023-09-21 05:30:57 +00:00
} ) ;
2024-04-23 17:38:06 +00:00
trackEvent ( 'customize_your_store_ai_update_template_success' ) ;
2023-09-21 05:30:57 +00:00
} catch ( error ) {
// eslint-disable-next-line no-console
console . error ( error ) ;
2024-04-23 17:38:06 +00:00
trackEvent ( 'customize_your_store_ai_update_template_response_error' , {
2023-09-21 05:30:57 +00:00
error : error instanceof Error ? error . message : 'unknown' ,
} ) ;
2023-11-06 07:46:28 +00:00
throw error ;
2023-09-21 05:30:57 +00:00
}
} ;
2023-10-03 07:41:48 +00:00
const installAndActivateTheme = async ( ) = > {
try {
2024-01-17 14:09:12 +00:00
await setTheme ( THEME_SLUG ) ;
2023-10-03 07:41:48 +00:00
} catch ( error ) {
2024-04-23 17:38:06 +00:00
trackEvent (
2023-10-03 07:41:48 +00:00
'customize_your_store_ai_install_and_activate_theme_error' ,
{
2024-01-17 14:09:12 +00:00
theme : THEME_SLUG ,
2023-10-03 07:41:48 +00:00
error : error instanceof Error ? error . message : 'unknown' ,
}
) ;
throw error ;
}
} ;
2023-09-22 12:43:42 +00:00
const saveAiResponseToOption = ( context : designWithAiStateMachineContext ) = > {
return dispatch ( OPTIONS_STORE_NAME ) . updateOptions ( {
2023-10-12 06:35:46 +00:00
woocommerce_customize_store_ai_suggestions : {
. . . context . aiSuggestions ,
lookAndFeel : context.lookAndFeel.choice ,
} ,
2023-09-22 12:43:42 +00:00
} ) ;
} ;
2023-09-06 06:21:09 +00:00
export const services = {
2023-09-12 06:32:50 +00:00
browserPopstateHandler ,
2023-09-19 08:41:52 +00:00
queryAiEndpoint ,
2023-09-21 05:30:57 +00:00
assembleSite ,
updateStorePatterns ,
2023-09-22 12:43:42 +00:00
saveAiResponseToOption ,
2023-10-03 07:41:48 +00:00
installAndActivateTheme ,
2023-12-07 08:42:16 +00:00
resetPatternsAndProducts ,
2024-06-10 13:00:33 +00:00
installPatterns ,
2023-09-06 06:21:09 +00:00
} ;