CYS - AI Offline flow (#41656)

This commit is contained in:
Moon 2023-12-06 04:49:28 -08:00 committed by GitHub
parent d8b2663def
commit 212c5bca3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 333 additions and 65 deletions

View File

@ -154,6 +154,13 @@ export const AssemblerHub: CustomizeStoreComponent = ( props ) => {
useEffect( () => {
onBackButtonClicked( () => setShowExitModal( true ) );
}, [] );
// @ts-expect-error temp fix
// Since we load the assember hub in an iframe, we don't have access to
// xstate's context values.
// This is the best workaround I can think of for now.
// Set the aiOnline value from the parent window so that any child components
// can access it.
props.context.aiOnline = window.parent?.window.cys_aiOnline;
return (
<>

View File

@ -2,9 +2,15 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { useContext, useState } from '@wordpress/element';
import { TourKit, TourKitTypes } from '@woocommerce/components';
import { recordEvent } from '@woocommerce/tracks';
import { Button, Modal } from '@wordpress/components';
/**
* Internal dependencies
*/
import { CustomizeStoreContext } from '..';
export * from './use-onboarding-tour';
type OnboardingTourProps = {
@ -23,7 +29,47 @@ export const OnboardingTour = ( {
const [ placement, setPlacement ] =
useState< TourKitTypes.WooConfig[ 'placement' ] >( 'left' );
const { context } = useContext( CustomizeStoreContext );
const aiOnline = context.aiOnline;
if ( showWelcomeTour ) {
const takeTour = () => {
// Click on "Take a tour" button
recordEvent( 'customize_your_store_assembler_hub_tour_start' );
setShowWelcomeTour( false );
};
const skipTour = () => {
recordEvent( 'customize_your_store_assembler_hub_tour_skip' );
onClose();
};
if ( ! aiOnline ) {
return (
<Modal
className="woocommerce-customize-store__onboarding-welcome-modal"
title={ __( 'Welcome to your store!', 'woocommerce' ) }
onRequestClose={ skipTour }
shouldCloseOnClickOutside={ false }
>
<p>
{ __(
"We encountered some issues while generating content with AI. But don't worry — you can still customize the look and feel of your store, including adding your logo, and changing colors and layouts. Take a quick tour to discover what's possible.",
'woocommerce'
) }
</p>
<div className="woocommerce-customize-store__design-change-warning-modal-footer">
<Button onClick={ skipTour } variant="link">
{ __( 'Skip', 'woocommerce' ) }
</Button>
<Button onClick={ takeTour } variant="primary">
{ __( 'Take a tour', 'woocommerce' ) }
</Button>
</div>
</Modal>
);
}
return (
<TourKit
config={ {
@ -87,15 +133,9 @@ export const OnboardingTour = ( {
closeHandler: ( _steps, _currentStepIndex, source ) => {
if ( source === 'done-btn' ) {
// Click on "Take a tour" button
recordEvent(
'customize_your_store_assembler_hub_tour_start'
);
setShowWelcomeTour( false );
takeTour();
} else {
recordEvent(
'customize_your_store_assembler_hub_tour_skip'
);
onClose();
skipTour();
}
},
} }

View File

@ -1,6 +1,7 @@
/**
* External dependencies
*/
import { createContext } from '@wordpress/element';
import { render, screen } from '@testing-library/react';
import { recordEvent } from '@woocommerce/tracks';
@ -10,6 +11,13 @@ import { recordEvent } from '@woocommerce/tracks';
import { OnboardingTour } from '../index';
jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
jest.mock( '../../', () => ( {
CustomizeStoreContext: createContext( {
context: {
aiOnline: true,
},
} ),
} ) );
describe( 'OnboardingTour', () => {
let props: {

View File

@ -664,6 +664,21 @@
box-shadow: none;
}
&.ai-offline {
.woocommerce-tour-kit-step {
width: 430px;
.components-card__header {
background-image: none;
height: auto;
margin: 0;
}
}
.tour-kit-frame__container {
bottom: 0;
}
}
.woocommerce-tour-kit-step {
width: 335px;
border-radius: 8px;

View File

@ -8,7 +8,10 @@ import { AnyInterpreter, Sender } from 'xstate';
/**
* Internal dependencies
*/
import { CustomizeStoreComponent } from '../types';
import {
CustomizeStoreComponent,
customizeStoreStateMachineContext,
} from '../types';
import { designWithAiStateMachineDefinition } from './state-machine';
import { findComponentMeta } from '~/utils/xstate/find-component';
import {
@ -33,10 +36,15 @@ export type DesignWithAiComponentMeta = {
export const DesignWithAiController = ( {
parentMachine,
parentContext,
}: {
parentMachine?: AnyInterpreter;
sendEventToParent?: Sender< customizeStoreStateMachineEvents >;
parentContext?: customizeStoreStateMachineContext;
} ) => {
// Assign aiOnline value from the parent context if it exists. Otherwise, ai is online by default.
designWithAiStateMachineDefinition.context.aiOnline =
parentContext?.aiOnline ?? true;
const [ state, send, service ] = useMachine(
designWithAiStateMachineDefinition,
{
@ -84,10 +92,16 @@ export const DesignWithAiController = ( {
};
//loader should send event 'THEME_SUGGESTED' when it's done
export const DesignWithAi: CustomizeStoreComponent = ( { parentMachine } ) => {
export const DesignWithAi: CustomizeStoreComponent = ( {
parentMachine,
context,
} ) => {
return (
<>
<DesignWithAiController parentMachine={ parentMachine } />
<DesignWithAiController
parentMachine={ parentMachine }
parentContext={ context }
/>
</>
);
};

View File

@ -205,7 +205,7 @@ export const updateStorePatterns = async (
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( {
woocommerce_blocks_allow_ai_connection: true,
woocommerce_blocks_allow_ai_connection: 'yes',
} );
const { images } = await apiFetch< {
@ -473,6 +473,17 @@ const saveAiResponseToOption = ( context: designWithAiStateMachineContext ) => {
} );
};
const resetPatterns = () => async () => {
await dispatch( OPTIONS_STORE_NAME ).updateOptions( {
woocommerce_blocks_allow_ai_connection: 'yes',
} );
return await apiFetch( {
path: '/wc/private/ai/patterns',
method: 'DELETE',
} );
};
export const services = {
browserPopstateHandler,
queryAiEndpoint,
@ -480,4 +491,5 @@ export const services = {
updateStorePatterns,
saveAiResponseToOption,
installAndActivateTheme,
resetPatterns,
};

View File

@ -38,6 +38,10 @@ export const hasStepInUrl = (
);
};
export const isAiOnline = ( _ctx: designWithAiStateMachineContext ) => {
return _ctx.aiOnline;
};
export const designWithAiStateMachineDefinition = createMachine(
{
id: 'designWithAi',
@ -84,6 +88,7 @@ export const designWithAiStateMachineDefinition = createMachine(
apiCallLoader: {
hasErrors: false,
},
aiOnline: true,
},
initial: 'navigate',
states: {
@ -272,8 +277,19 @@ export const designWithAiStateMachineDefinition = createMachine(
type: 'parallel',
states: {
chooseColorPairing: {
initial: 'pending',
initial: 'executeOrSkip',
states: {
executeOrSkip: {
always: [
{
target: 'pending',
cond: 'isAiOnline',
},
{
target: 'success',
},
],
},
pending: {
invoke: {
src: 'queryAiEndpoint',
@ -307,8 +323,19 @@ export const designWithAiStateMachineDefinition = createMachine(
},
},
chooseFontPairing: {
initial: 'pending',
initial: 'executeOrSkip',
states: {
executeOrSkip: {
always: [
{
target: 'pending',
cond: 'isAiOnline',
},
{
target: 'success',
},
],
},
pending: {
entry: [ 'assignFontPairing' ],
always: {
@ -319,8 +346,19 @@ export const designWithAiStateMachineDefinition = createMachine(
},
},
updateStorePatterns: {
initial: 'pending',
initial: 'executeOrSkip',
states: {
executeOrSkip: {
always: [
{
target: 'pending',
cond: 'isAiOnline',
},
{
target: 'resetPatterns',
},
],
},
pending: {
invoke: {
src: 'updateStorePatterns',
@ -335,6 +373,20 @@ export const designWithAiStateMachineDefinition = createMachine(
},
},
},
resetPatterns: {
invoke: {
src: 'resetPatterns',
onDone: {
target: 'success',
},
onError: {
actions: [
'assignAPICallLoaderError',
],
target: '#toneOfVoice',
},
},
},
success: { type: 'final' },
},
},
@ -429,6 +481,7 @@ export const designWithAiStateMachineDefinition = createMachine(
services,
guards: {
hasStepInUrl,
isAiOnline,
},
}
);

View File

@ -39,6 +39,7 @@ export type designWithAiStateMachineContext = {
// If we require more data from options, previously provided core profiler details,
// we can retrieve them in preBusinessInfoDescription and then assign them here
spawnSaveDescriptionToOptionRef?: ReturnType< typeof spawn >;
aiOnline: boolean;
};
export type designWithAiStateMachineEvents =
| { type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION'; payload: { step: string } }

View File

@ -160,6 +160,7 @@ export const customizeStoreStateMachineDefinition = createMachine( {
transitionalScreen: {
hasCompleteSurvey: false,
},
aiOnline: true,
} as customizeStoreStateMachineContext,
invoke: {
src: 'browserPopstateHandler',
@ -214,22 +215,54 @@ export const customizeStoreStateMachineDefinition = createMachine( {
initial: 'preIntro',
states: {
preIntro: {
invoke: {
src: 'fetchIntroData',
onError: {
actions: 'assignFetchIntroDataError',
target: 'intro',
type: 'parallel',
states: {
checkAiStatus: {
initial: 'pending',
states: {
pending: {
invoke: {
src: 'fetchAiStatus',
onDone: {
actions: 'assignAiStatus',
target: 'success',
},
onError: {
actions: 'assignAiOffline',
target: 'success',
},
},
},
success: { type: 'final' },
},
},
onDone: {
target: 'intro',
actions: [
'assignThemeData',
'assignActiveThemeHasMods',
'assignCustomizeStoreCompleted',
'assignCurrentThemeIsAiGenerated',
],
fetchIntroData: {
initial: 'pending',
states: {
pending: {
invoke: {
src: 'fetchIntroData',
onError: {
actions:
'assignFetchIntroDataError',
target: 'success',
},
onDone: {
target: 'success',
actions: [
'assignThemeData',
'assignActiveThemeHasMods',
'assignCustomizeStoreCompleted',
'assignCurrentThemeIsAiGenerated',
],
},
},
},
success: { type: 'final' },
},
},
},
onDone: 'intro',
},
intro: {
meta: {
@ -285,8 +318,21 @@ export const customizeStoreStateMachineDefinition = createMachine( {
},
},
assemblerHub: {
initial: 'assemblerHub',
initial: 'checkAiStatus',
states: {
checkAiStatus: {
invoke: {
src: 'fetchAiStatus',
onDone: {
actions: 'assignAiStatus',
target: 'assemblerHub',
},
onError: {
actions: 'assignAiOffline',
target: 'assemblerHub',
},
},
},
assemblerHub: {
entry: [
{ type: 'updateQueryStep', step: 'assembler-hub' },
@ -388,6 +434,12 @@ export const CustomizeStoreController = ( {
( cond as { step: string | undefined } ).step
);
},
isAiOnline: ( _ctx ) => {
return _ctx.aiOnline;
},
isAiOffline: ( _ctx ) => {
return ! _ctx.aiOnline;
},
},
} );
}, [ actionOverrides, servicesOverrides ] );

View File

@ -9,6 +9,7 @@ import { recordEvent } from '@woocommerce/tracks';
*/
import { customizeStoreStateMachineEvents } from '..';
import {
aiStatusResponse,
customizeStoreStateMachineContext,
RecommendThemesAPIResponse,
} from '../types';
@ -103,3 +104,37 @@ export const assignCurrentThemeIsAiGenerated = assign<
return { ...context.intro, currentThemeIsAiGenerated };
},
} );
export const assignAiStatus = assign<
customizeStoreStateMachineContext,
customizeStoreStateMachineEvents // this is actually the wrong type for the event but I still don't know how to type this properly
>( {
aiOnline: ( _context, _event ) => {
const indicator = ( _event as DoneInvokeEvent< aiStatusResponse > ).data
.status.indicator;
const status = indicator !== 'critical' && indicator !== 'major';
// @ts-expect-error temp workaround;
window.cys_aiOnline = status;
recordEvent( 'customize_your_store_ai_status', {
online: status ? 'yes' : 'no',
} );
return status;
},
} );
export const assignAiOffline = assign<
customizeStoreStateMachineContext,
customizeStoreStateMachineEvents // this is actually the wrong type for the event but I still don't know how to type this properly
>( {
aiOnline: () => {
// @ts-expect-error temp workaround;
window.cys_aiOnline = false;
recordEvent( 'customize_your_store_ai_status', {
online: 'no',
} );
return false;
},
} );

View File

@ -307,38 +307,3 @@
padding: 0.5rem 0.75rem;
}
}
.woocommerce-customize-store__design-change-warning-modal {
width: 480px;
max-width: 480px;
.components-modal__header {
padding-top: 32px;
}
p {
padding: 16px 0 16px 0;
margin: 0;
}
a,
button {
text-decoration: none !important;
}
.components-button {
padding: 8px 16px;
}
h1 {
line-height: 28px;
font-size: 20px;
color: #1e1e1e;
}
.woocommerce-customize-store__design-change-warning-modal-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
}
}

View File

@ -8,6 +8,19 @@ import { resolveSelect } from '@wordpress/data';
import { ONBOARDING_STORE_NAME, OPTIONS_STORE_NAME } from '@woocommerce/data';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { aiStatusResponse } from '../types';
export const fetchAiStatus = () => async (): Promise< aiStatusResponse > => {
const response = await fetch(
'https://status.openai.com/api/v2/status.json'
);
const data = await response.json();
return data;
};
export const fetchThemeCards = async () => {
const themes = await apiFetch( {
path: '/wc-admin/onboarding/themes/recommended',

View File

@ -44,6 +44,7 @@ describe( 'Intro Banners', () => {
transitionalScreen: {
hasCompleteSurvey: false,
},
aiOnline: true,
} }
currentState={ 'intro' }
parentMachine={ null as unknown as AnyInterpreter }
@ -81,6 +82,7 @@ describe( 'Intro Banners', () => {
transitionalScreen: {
hasCompleteSurvey: false,
},
aiOnline: true,
} }
currentState={ 'intro' }
parentMachine={ null as unknown as AnyInterpreter }
@ -124,6 +126,7 @@ describe( 'Intro Banners', () => {
transitionalScreen: {
hasCompleteSurvey: false,
},
aiOnline: true,
} }
currentState={ 'intro' }
parentMachine={ null as unknown as AnyInterpreter }

View File

@ -42,6 +42,7 @@ describe( 'Intro Modals', () => {
transitionalScreen: {
hasCompleteSurvey: false,
},
aiOnline: true,
} }
currentState={ 'intro' }
parentMachine={ null as unknown as AnyInterpreter }
@ -96,6 +97,7 @@ describe( 'Intro Modals', () => {
transitionalScreen: {
hasCompleteSurvey: false,
},
aiOnline: true,
} }
currentState={ 'intro' }
parentMachine={ null as unknown as AnyInterpreter }
@ -148,6 +150,7 @@ describe( 'Intro Modals', () => {
transitionalScreen: {
hasCompleteSurvey: false,
},
aiOnline: true,
} }
currentState={ 'intro' }
parentMachine={ null as unknown as AnyInterpreter }

View File

@ -180,3 +180,39 @@ body.woocommerce-customize-store.js.is-fullscreen-mode {
transition: opacity 1.2s linear;
opacity: 0;
}
.woocommerce-customize-store__onboarding-welcome-modal,
.woocommerce-customize-store__design-change-warning-modal {
width: 480px;
max-width: 480px;
.components-modal__header {
padding-top: 32px;
}
p {
padding: 16px 0 16px 0;
margin: 0;
}
a,
button {
text-decoration: none !important;
}
.components-button {
padding: 8px 16px;
}
h1 {
line-height: 28px;
font-size: 20px;
color: #1e1e1e;
}
.woocommerce-customize-store__design-change-warning-modal-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
}
}

View File

@ -29,6 +29,12 @@ export type RecommendThemesAPIResponse = {
};
};
export type aiStatusResponse = {
status: {
indicator: 'major' | 'critical' | 'ok';
};
};
export type customizeStoreStateMachineContext = {
themeConfiguration: Record< string, unknown >; // placeholder for theme configuration until we know what it looks like
intro: {
@ -42,4 +48,5 @@ export type customizeStoreStateMachineContext = {
transitionalScreen: {
hasCompleteSurvey: boolean;
};
aiOnline: boolean;
};

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Tweaks for the CYS when the A.I is offline.