CYS - Add LookAndFeel and ToneOfVoice pages (#39979)

* Add ProgressBar component to @woocommerce/components

* Add changelog

* Remove html.wp-toolbar in fullscreen mode

* Add base style

* Add Tell us a bit more about your business page

* Fix merge conflict issues

* Send BUSINESS_INFO_DESCRIPTION_COMPLETE event when continue button is clicked

* Remove duplicated style import

* Add changefile(s) from automation for the following project(s): @woocommerce/components, woocommerce

* Lint fix

* Add 'Look and Feel' and 'Tone of voice' pages';

* Use correct classname

* Minor changes

* Textearea color should be gray-900 after the user enter text
* guide font weight should be 500

* Fix layout shift when a choice is selected

* Fix choices width for tone of voice page

* Use context value for the default

* Revert button margin top

* Fix default selection

* Add X button

* Decrease the margin by 20px to accommodate the height of the close button

* Add close action

* Include @woocommerce/ai package

* Add AI service

* Use AI service

* Parse JSON from in function

* Fix assignLookAndTone event type

* Update plugins/woocommerce-admin/client/customize-store/design-with-ai/components/choice/choice.scss

Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>

* Update plugins/woocommerce-admin/client/customize-store/design-with-ai/services.ts

Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>

* Log when AI API endpoint request fails

* Add spinner when user clicks the continue button

* streamlined unnecessary isRequesting context and forwarded close event

* pnpm-lock changes from trunk

* lint fixes

* ai package test passWithNoTests

* changelog

* reset pnpm-lock to trunk

* Dev: update pnpm-lock.yaml and jest preset config (#40045)

* Update pnpm-lock.yaml

* Update jest-preset config to fix unexpected token error

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
Co-authored-by: rjchow <me@rjchow.com>
This commit is contained in:
Moon 2023-09-05 23:21:09 -07:00 committed by GitHub
parent e2c2c52c1c
commit c45335b936
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1228 additions and 830 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Allow jest to pass with no tests

View File

@ -73,7 +73,7 @@
},
"scripts": {
"turbo:build": "pnpm run build:js && pnpm run build:css",
"turbo:test": "jest --config ./jest.config.json",
"turbo:test": "jest --config ./jest.config.json --passWithNoTests",
"prepare": "composer install",
"changelog": "composer exec -- changelogger",
"clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*",

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add AI wizard business info step for Customize Your Store task

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add ProgressBar component

View File

@ -111,3 +111,4 @@ export {
ProductFieldSection as __experimentalProductFieldSection,
} from './product-section-layout';
export { DisplayState } from './display-state';
export { ProgressBar } from './progress-bar';

View File

@ -0,0 +1,46 @@
/**
* External dependencies
*/
import { HTMLAttributes, createElement } from 'react';
/**
* Internal dependencies
*/
type ProgressBarProps = {
className?: string;
percent?: number;
color?: string;
bgcolor?: string;
};
export const ProgressBar = ( {
className = '',
percent = 0,
color = '#674399',
bgcolor = 'var(--wp-admin-theme-color)',
}: ProgressBarProps ) => {
const containerStyles = {
backgroundColor: bgcolor,
};
const fillerStyles: HTMLAttributes< HTMLDivElement >[ 'style' ] = {
backgroundColor: color,
width: `${ percent }%`,
display: percent === 0 ? 'none' : 'inherit',
};
return (
<div className={ `woocommerce-progress-bar ${ className }` }>
<div
className="woocommerce-progress-bar__container"
style={ containerStyles }
>
<div
className="woocommerce-progress-bar__filler"
style={ fillerStyles }
/>
</div>
</div>
);
};

View File

@ -0,0 +1,16 @@
/**
* External dependencies
*/
import { ProgressBar } from '@woocommerce/components';
import { createElement } from '@wordpress/element';
export const Basic = () => (
<div style={ { background: '#fff', height: '200px', padding: '20px' } }>
<ProgressBar percent={ 20 } bgcolor={ '#eeeeee' } color={ '#007cba' } />
</div>
);
export default {
title: 'WooCommerce Admin/components/ProgressBar',
component: ProgressBar,
};

View File

@ -0,0 +1,10 @@
.woocommerce-progress-bar__container {
height: 8px;
width: 100%;
}
// Min width equal to height. This means small values look like each other, but all bars have a consistent radius.
.woocommerce-progress-bar__filler {
height: 100%;
min-width: 8px;
}

View File

@ -59,3 +59,4 @@
@import 'experimental-select-tree-control/select-tree.scss';
@import 'product-section-layout/style.scss';
@import 'tree-select-control/index.scss';
@import 'progress-bar/style.scss';

View File

@ -3,6 +3,16 @@
*/
const path = require( 'path' );
// These modules need to be transformed because they are not transpiled to CommonJS.
const transformModules = [ 'is-plain-obj', 'memize' ];
// Ignore all node_modules except for the ones we need to transform.
const transformIgnorePatterns = [
`node_modules/(?!.pnpm/${ transformModules.join(
'|.pnpm/'
) }|${ transformModules.join( '|' ) })`,
'/build/',
];
module.exports = {
moduleNameMapper: {
tinymce: path.resolve( __dirname, 'build/mocks/tinymce' ),
@ -44,12 +54,10 @@ module.exports = {
'<rootDir>/.*/build-module/',
'<rootDir>/tests/e2e/',
],
transformIgnorePatterns: [
`node_modules/(?!.pnpm/is-plain-obj|is-plain-obj)`,
'/build/',
],
transformIgnorePatterns,
transform: {
'^.+\\is-plain-obj/index\\.js$': 'babel-jest',
'^.+\\memize/dist/index\\.js$': 'babel-jest',
'^.+\\.[jt]sx?$': 'ts-jest',
},
testEnvironment: 'jest-environment-jsdom',

View File

@ -11,7 +11,7 @@ import './choice.scss';
type Props = {
className?: string;
selected: boolean;
title: string;
title: string | React.ReactNode;
name: string;
value: string;
onChange: ( value: string ) => void;

View File

@ -9,8 +9,13 @@ import { assign } from 'xstate';
import {
designWithAiStateMachineContext,
designWithAiStateMachineEvents,
completionAPIResponse,
} from './types';
import { businessInfoDescriptionCompleteEvent } from './pages';
import {
businessInfoDescriptionCompleteEvent,
lookAndFeelCompleteEvent,
toneOfVoiceCompleteEvent,
} from './pages';
const assignBusinessInfoDescription = assign<
designWithAiStateMachineContext,
@ -23,6 +28,55 @@ const assignBusinessInfoDescription = assign<
};
},
} );
const assignLookAndFeel = assign<
designWithAiStateMachineContext,
designWithAiStateMachineEvents
>( {
lookAndFeel: ( context, event: unknown ) => {
return {
choice: ( event as lookAndFeelCompleteEvent ).payload,
};
},
} );
const assignToneOfVoice = assign<
designWithAiStateMachineContext,
designWithAiStateMachineEvents
>( {
toneOfVoice: ( context, event: unknown ) => {
return {
choice: ( event as toneOfVoiceCompleteEvent ).payload,
};
},
} );
const assignLookAndTone = assign<
designWithAiStateMachineContext,
designWithAiStateMachineEvents
>( {
lookAndFeel: ( context, event: unknown ) => {
return {
choice: ( event as { data: completionAPIResponse } ).data.look,
};
},
toneOfVoice: ( context, event: unknown ) => {
return {
choice: ( event as { data: completionAPIResponse } ).data.tone,
};
},
} );
const logAIAPIRequestError = () => {
// log AI API request error
// eslint-disable-next-line no-console
console.log( 'API Request error' );
};
export const actions = {
assignBusinessInfoDescription,
assignLookAndFeel,
assignToneOfVoice,
assignLookAndTone,
logAIAPIRequestError,
};

View File

@ -0,0 +1,48 @@
.woocommerce-cys-choice-container {
display: flex;
flex-direction: column;
padding: 16px;
border: 1px solid $gray-200;
border-radius: 2px;
width: 100%;
cursor: pointer;
&[data-selected] {
border-radius: 2px;
border-color: transparent;
box-shadow: 0 0 0 2px var(--wp-admin-theme-color);
}
&:focus-visible {
border: 2px solid var(--wp-admin-theme-color);
padding: $gap-large $gap;
}
}
.woocommerce-cys-choice {
display: flex;
flex-direction: column;
.woocommerce-cys-choice-input {
opacity: 0;
position: absolute;
}
label {
color: #1e1e1e;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 125% */
letter-spacing: -0.24px;
}
p {
margin: 0;
color: #757575;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 123.077% */
}
}

View File

@ -0,0 +1,71 @@
/**
* External dependencies
*/
import classNames from 'classnames';
/**
* Internal dependencies
*/
import './choice.scss';
type Props = {
className?: string;
selected: boolean;
title: string;
subtitle?: string;
name: string;
value: string;
onChange: ( value: string ) => void;
};
export const Choice = ( {
className,
selected,
title,
subtitle,
name,
value,
onChange,
}: Props ) => {
const changeHandler = () => {
onChange( value );
};
const inputId = 'woocommerce-' + value.replace( /_/g, '-' );
return (
<div
role="radio"
className={ classNames(
'woocommerce-cys-choice-container',
className
) }
onClick={ changeHandler }
onKeyDown={ ( e ) => {
if ( e.key === 'Enter' ) {
changeHandler();
}
} }
data-selected={ selected ? selected : null }
tabIndex={ 0 }
>
<div className="woocommerce-cys-choice">
<input
className="woocommerce-cys-choice-input"
id={ inputId }
name={ name }
type="radio"
value={ value }
checked={ !! selected }
onChange={ changeHandler }
data-selected={ selected ? selected : null }
// Stop the input from being focused when the parent div is clicked
tabIndex={ -1 }
></input>
<label htmlFor={ inputId } className="choice__title">
{ title }
</label>
{ subtitle && <p className="choice__subtitle">{ subtitle }</p> }
</div>
</div>
);
};

View File

@ -0,0 +1,11 @@
.close-cys-design-with-ai {
transition: none;
border: none;
padding: 0;
margin: 20px 0 0 20px;
height: 24px;
&:focus {
outline: 0 !important;
box-shadow: none !important;
}
}

View File

@ -0,0 +1,38 @@
/**
* External dependencies
*/
import { Button } from '@wordpress/components';
/**
* Internal dependencies
*/
import './close-button.scss';
type Props = {
onClick?: () => void;
};
export const CloseButton = ( { onClick }: Props ) => {
return (
<div>
<Button
className="close-cys-design-with-ai"
onClick={ onClick ? onClick : () => {} }
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.40456 5L19 19M5 19L18.5954 5"
stroke="#1E1E1E"
strokeWidth="1.5"
/>
</svg>
</Button>
</div>
);
};

View File

@ -0,0 +1,6 @@
export type aiWizardClosedBeforeCompletionEvent = {
type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION';
payload: {
step: string;
};
};

View File

@ -3,7 +3,7 @@
*/
import { useMachine, useSelector } from '@xstate/react';
import { useEffect, useState } from '@wordpress/element';
import { Sender } from 'xstate';
import { AnyInterpreter, Sender } from 'xstate';
/**
* Internal dependencies
@ -29,13 +29,17 @@ export type DesignWithAiComponentMeta = {
component: DesignWithAiComponent;
};
export const DesignWithAiController = ( {}: {
sendEventToParent: Sender< customizeStoreStateMachineEvents >;
export const DesignWithAiController = ( {
parentMachine,
}: {
parentMachine?: AnyInterpreter;
sendEventToParent?: Sender< customizeStoreStateMachineEvents >;
} ) => {
const [ state, send, service ] = useMachine(
designWithAiStateMachineDefinition,
{
devTools: process.env.NODE_ENV === 'development',
parent: parentMachine,
}
);
@ -78,10 +82,10 @@ export const DesignWithAiController = ( {}: {
};
//loader should send event 'THEME_SUGGESTED' when it's done
export const DesignWithAi: CustomizeStoreComponent = ( { sendEvent } ) => {
export const DesignWithAi: CustomizeStoreComponent = ( { parentMachine } ) => {
return (
<>
<DesignWithAiController sendEventToParent={ sendEvent } />
<DesignWithAiController parentMachine={ parentMachine } />
</>
);
};

View File

@ -2,11 +2,16 @@
* External dependencies
*/
import { useState } from '@wordpress/element';
import { TextareaControl, Button, Spinner } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { ProgressBar } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
import { CloseButton } from '../components/close-button/close-button';
import { aiWizardClosedBeforeCompletionEvent } from '../events';
export type businessInfoDescriptionCompleteEvent = {
type: 'BUSINESS_INFO_DESCRIPTION_COMPLETE';
@ -16,35 +21,104 @@ export const BusinessInfoDescription = ( {
sendEvent,
context,
}: {
sendEvent: ( event: businessInfoDescriptionCompleteEvent ) => void;
sendEvent: (
event:
| businessInfoDescriptionCompleteEvent
| aiWizardClosedBeforeCompletionEvent
) => void;
context: designWithAiStateMachineContext;
} ) => {
const [ businessInfoDescription, setBusinessInfoDescription ] = useState(
context.businessInfoDescription.descriptionText
);
const [ isRequesting, setIsRequesting ] = useState( false );
return (
<div>
<h1>Business Info Description</h1>
<div>{ JSON.stringify( context ) }</div>
{ /* add a controlled text area that saves to state */ }
<input
type="text"
value={ businessInfoDescription }
onChange={ ( e ) =>
setBusinessInfoDescription( e.target.value )
}
<ProgressBar
percent={ 20 }
color={ 'var(--wp-admin-theme-color)' }
bgcolor={ 'transparent' }
/>
<button
onClick={ () =>
<CloseButton
onClick={ () => {
sendEvent( {
type: 'BUSINESS_INFO_DESCRIPTION_COMPLETE',
payload: businessInfoDescription,
} )
}
>
complete
</button>
type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION',
payload: { step: 'business-info-description' },
} );
} }
/>
<div className="woocommerce-cys-design-with-ai woocommerce-cys-layout">
<div className="woocommerce-cys-page">
<h1>
{ __(
'Tell us a bit more about your business',
'woocommerce'
) }
</h1>
<TextareaControl
onChange={ ( businessInfo ) => {
setBusinessInfoDescription( businessInfo );
} }
value={ businessInfoDescription }
placeholder={ __(
'E.g., At Cool Cat Shades, we sell sunglasses specially designed for our stylish feline friends. Designed and developed with a cats comfort in mind, our range of sunglasses are fashionable accessories our furry friends can wear all day. We currently offer 50 different styles and variations of shades, with plans to add more in the near future.',
'woocommerce'
) }
/>
<div className="woocommerce-cys-design-with-ai-guide">
<p>
{ __(
'The more detail you provide, the better job our AI can do!',
'woocommerce'
) }
</p>
<p>{ __( 'Try to include:', 'woocommerce' ) }</p>
<ul>
<li>
{ __( 'What you want to sell', 'woocommerce' ) }
</li>
<li>
{ __(
'How many products you plan on displaying',
'woocommerce'
) }
</li>
<li>
{ __(
'What makes your business unique',
'woocommerce'
) }
</li>
<li>
{ __(
'Who your target audience is',
'woocommerce'
) }
</li>
</ul>
</div>
<Button
variant="primary"
onClick={ () => {
setIsRequesting( true );
sendEvent( {
type: 'BUSINESS_INFO_DESCRIPTION_COMPLETE',
payload: businessInfoDescription,
} );
} }
disabled={
businessInfoDescription.length === 0 || isRequesting
}
>
{ isRequesting ? (
<Spinner />
) : (
__( 'Continue', 'woocommerce' )
) }
</Button>
</div>
</div>
</div>
);
};

View File

@ -1,26 +1,115 @@
/**
* External dependencies
*/
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { ProgressBar } from '@woocommerce/components';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
import { Choice } from '../components/choice/choice';
import { CloseButton } from '../components/close-button/close-button';
import { aiWizardClosedBeforeCompletionEvent } from '../events';
export type lookAndFeelCompleteEvent = {
type: 'LOOK_AND_FEEL_COMPLETE';
payload: string;
};
export const LookAndFeel = ( {
sendEvent,
context,
}: {
sendEvent: ( event: { type: 'LOOK_AND_FEEL_COMPLETE' } ) => void;
sendEvent: (
event: lookAndFeelCompleteEvent | aiWizardClosedBeforeCompletionEvent
) => void;
context: designWithAiStateMachineContext;
} ) => {
const choices = [
{
title: __( 'Contemporary', 'woocommerce' ),
subtitle: __(
'Clean lines, neutral colors, sleek and modern look',
'woocommerce'
),
},
{
title: __( 'Classic', 'woocommerce' ),
subtitle: __(
'Elegant and timeless look with nostalgic touch.',
'woocommerce'
),
},
{
title: __( 'Bold', 'woocommerce' ),
subtitle: __(
'Vibrant look with eye-catching colors and visuals.',
'woocommerce'
),
},
];
const [ look, setLook ] = useState< string >(
context.lookAndFeel.choice === ''
? choices[ 0 ].title
: context.lookAndFeel.choice
);
return (
<div>
<h1>Look and Feel</h1>
<div>{ JSON.stringify( context ) }</div>
<button
onClick={ () =>
sendEvent( { type: 'LOOK_AND_FEEL_COMPLETE' } )
}
>
complete
</button>
<ProgressBar
percent={ 60 }
color={ 'var(--wp-admin-theme-color)' }
bgcolor={ 'transparent' }
/>
<CloseButton
onClick={ () => {
sendEvent( {
type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION',
payload: { step: 'look-and-feel' },
} );
} }
/>
<div className="woocommerce-cys-design-with-ai-look-and-feel woocommerce-cys-layout">
<div className="woocommerce-cys-page">
<h1>
{ __(
'How would you like your store to look?',
'woocommerce'
) }
</h1>
<div className="choices">
{ choices.map( ( { title, subtitle } ) => {
return (
<Choice
key={ title }
name="user-profile-choice"
title={ title }
subtitle={ subtitle }
selected={ look === title }
value={ title }
onChange={ ( _title ) => {
setLook( _title );
} }
/>
);
} ) }
</div>
<Button
variant="primary"
onClick={ () => {
sendEvent( {
type: 'LOOK_AND_FEEL_COMPLETE',
payload: look,
} );
} }
>
{ __( 'Continue', 'woocommerce' ) }
</Button>
</div>
</div>
</div>
);
};

View File

@ -1,26 +1,112 @@
/**
* External dependencies
*/
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { ProgressBar } from '@woocommerce/components';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
import { Choice } from '../components/choice/choice';
import { CloseButton } from '../components/close-button/close-button';
import { aiWizardClosedBeforeCompletionEvent } from '../events';
export type toneOfVoiceCompleteEvent = {
type: 'TONE_OF_VOICE_COMPLETE';
payload: string;
};
export const ToneOfVoice = ( {
sendEvent,
context,
}: {
sendEvent: ( event: { type: 'TONE_OF_VOICE_COMPLETE' } ) => void;
sendEvent: (
event: toneOfVoiceCompleteEvent | aiWizardClosedBeforeCompletionEvent
) => void;
context: designWithAiStateMachineContext;
} ) => {
const choices = [
{
title: __( 'Informal', 'woocommerce' ),
subtitle: __(
'Relaxed and friendly, like a conversation with a friend.',
'woocommerce'
),
},
{
title: __( 'Neutral', 'woocommerce' ),
subtitle: __(
'Impartial tone with casual expressions without slang.',
'woocommerce'
),
},
{
title: __( 'Formal', 'woocommerce' ),
subtitle: __(
'Direct yet respectful, serious and professional.',
'woocommerce'
),
},
];
const [ sound, setSound ] = useState< string >(
context.toneOfVoice.choice === ''
? choices[ 0 ].title
: context.toneOfVoice.choice
);
return (
<div>
<h1>Tone of Voice</h1>
<div>{ JSON.stringify( context ) }</div>
<button
onClick={ () =>
sendEvent( { type: 'TONE_OF_VOICE_COMPLETE' } )
}
>
complete
</button>
<ProgressBar
percent={ 80 }
color={ 'var(--wp-admin-theme-color)' }
bgcolor={ 'transparent' }
/>
<CloseButton
onClick={ () => {
sendEvent( {
type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION',
payload: { step: 'tone-of-voice' },
} );
} }
/>
<div className="woocommerce-cys-design-with-ai-tone-of-voice woocommerce-cys-layout">
<div className="woocommerce-cys-page">
<h1>
{ __( 'How would you like to sound?', 'woocommerce' ) }
</h1>
<div className="choices">
{ choices.map( ( { title, subtitle } ) => {
return (
<Choice
key={ title }
name="user-profile-choice"
title={ title }
subtitle={ subtitle }
selected={ sound === title }
value={ title }
onChange={ ( _title ) => {
setSound( _title );
} }
/>
);
} ) }
</div>
<Button
variant="primary"
onClick={ () => {
sendEvent( {
type: 'TONE_OF_VOICE_COMPLETE',
payload: sound,
} );
} }
>
{ __( 'Continue', 'woocommerce' ) }
</Button>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,48 @@
/**
* External dependencies
*/
import { __experimentalRequestJetpackToken as requestJetpackToken } from '@woocommerce/ai';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from './types';
export const getLookAndTone = async (
context: designWithAiStateMachineContext
) => {
const prompt = [
'You are a WordPress theme expert.',
'Analyze the following store description and determine the look and tone of the theme.',
'For look, you can choose between Contemporary, Classic, and Bold.',
'For tone of the description, you can choose between Informal, Neutral, and Formal.',
'Your response should be in json with look and tone values.',
'\n',
context.businessInfoDescription.descriptionText,
];
const { token } = await requestJetpackToken();
const url = new URL(
'https://public-api.wordpress.com/wpcom/v2/text-completion'
);
url.searchParams.append( 'prompt', prompt.join( '\n' ) );
url.searchParams.append( 'token', token );
url.searchParams.append( 'feature', 'woo_cys' );
url.searchParams.append( '_fields', 'completion' );
const data: {
completion: string;
} = await apiFetch( {
url: url.toString(),
method: 'POST',
} );
return JSON.parse( data.completion );
};
export const services = {
getLookAndTone,
};

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { createMachine } from 'xstate';
import { createMachine, sendParent } from 'xstate';
/**
* Internal dependencies
@ -17,7 +17,7 @@ import {
ApiCallLoader,
} from './pages';
import { actions } from './actions';
import { services } from './services';
export const designWithAiStateMachineDefinition = createMachine(
{
id: 'designWithAi',
@ -30,7 +30,9 @@ export const designWithAiStateMachineDefinition = createMachine(
context: {
businessInfoDescription: {
descriptionText: '',
isMakignRequest: false,
},
lookAndFeel: {
choice: '',
},
@ -56,14 +58,31 @@ export const designWithAiStateMachineDefinition = createMachine(
},
on: {
BUSINESS_INFO_DESCRIPTION_COMPLETE: {
actions: [ 'assignBusinessInfoDescription' ],
actions: [
'assignBusinessInfoDescription',
'assignAIAPIRequestStarted',
],
target: 'postBusinessInfoDescription',
},
},
},
postBusinessInfoDescription: {
always: {
target: '#lookAndFeel',
invoke: {
src: 'getLookAndTone',
onError: {
actions: [
'assignAIAPIRequestFinished',
'logAIAPIRequestError',
],
target: '#lookAndFeel',
},
onDone: {
actions: [
'assignAIAPIRequestFinished',
'assignLookAndTone',
],
target: '#lookAndFeel',
},
},
},
},
@ -83,6 +102,7 @@ export const designWithAiStateMachineDefinition = createMachine(
},
on: {
LOOK_AND_FEEL_COMPLETE: {
actions: [ 'assignLookAndFeel' ],
target: 'postLookAndFeel',
},
},
@ -109,6 +129,7 @@ export const designWithAiStateMachineDefinition = createMachine(
},
on: {
TONE_OF_VOICE_COMPLETE: {
actions: [ 'assignToneOfVoice' ],
target: 'postToneOfVoice',
},
},
@ -140,13 +161,12 @@ export const designWithAiStateMachineDefinition = createMachine(
},
on: {
AI_WIZARD_CLOSED_BEFORE_COMPLETION: {
// TODO: handle this event when the 'x' is clicked at any point
// probably bail (to where?) and log the tracks for which step it is in plus
// whatever details might be helpful to know why they bailed
actions: sendParent( ( _context, event ) => event ),
},
},
},
{
actions,
services,
}
);

View File

@ -1,6 +1,7 @@
export type designWithAiStateMachineContext = {
businessInfoDescription: {
descriptionText: string;
isMakignRequest?: boolean;
};
lookAndFeel: {
choice: string;
@ -12,7 +13,7 @@ export type designWithAiStateMachineContext = {
// we can retrieve them in preBusinessInfoDescription and then assign them here
};
export type designWithAiStateMachineEvents =
| { type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION' }
| { type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION'; payload: { step: string } }
| {
type: 'BUSINESS_INFO_DESCRIPTION_COMPLETE';
payload: string;
@ -29,3 +30,8 @@ export type designWithAiStateMachineEvents =
| {
type: 'API_CALL_TO_AI_FAILED';
};
export type completionAPIResponse = {
look: string;
tone: string;
};

View File

@ -24,13 +24,13 @@ import {
customizeStoreStateMachineContext,
} from './types';
import { ThemeCard } from './intro/theme-cards';
import './style.scss';
export type customizeStoreStateMachineEvents =
| introEvents
| designWithAiEvents
| assemblerHubEvents;
| assemblerHubEvents
| { type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION'; payload: { step: string } };
export const customizeStoreStateMachineServices = {
...introServices,
@ -131,6 +131,13 @@ export const customizeStoreStateMachineDefinition = createMachine( {
backToHomescreen: {},
appearanceTask: {},
},
on: {
AI_WIZARD_CLOSED_BEFORE_COMPLETION: {
actions: () => {
// TODO: when navigation has been implemented, this should navigate back to the Intro screen
},
},
},
} );
export const CustomizeStoreController = ( {
@ -187,6 +194,7 @@ export const CustomizeStoreController = ( {
>
{ CurrentComponent ? (
<CurrentComponent
parentMachine={ service }
sendEvent={ send }
context={ state.context }
/>

View File

@ -16,3 +16,94 @@
padding-right: 0;
}
}
body.woocommerce-customize-store.js.is-fullscreen-mode {
margin-top: 0 !important;
}
.woocommerce-cys-layout {
display: flex;
flex-direction: column;
align-items: center;
h1 {
text-align: center;
color: #000;
font-size: 32px;
font-style: normal;
font-weight: 500;
line-height: 40px; /* 125% */
margin-top: 100px;
margin-bottom: 60px;
}
.woocommerce-cys-page {
width: 615px;
display: flex;
flex-direction: column;
align-items: center;
}
button.is-primary {
width: 404px;
display: block;
height: 48px;
margin-top: 32px;
}
}
.woocommerce-cys-design-with-ai {
.components-base-control {
width: 404px;
textarea {
margin: 0 0 12px 0;
height: 167px;
border-radius: 2px;
border: 1px solid #bbb;
background: #fff;
color: $gray-900;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
&::placeholder {
color: #757575;
}
}
}
.woocommerce-cys-design-with-ai-guide {
padding: 12px;
width: 404px;
border-radius: 2px;
background: var(--gutenberg-transparent-blueberry, rgba(56, 88, 233, 0.04));
color: var(--gutenberg-gray-800, #2f2f2f);
p {
padding: 0;
margin: 0;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 153.846% */
}
ul {
list-style: inside;
margin-left: 8px;
li {
font-weight: 400;
}
}
}
}
.woocommerce-cys-design-with-ai-look-and-feel,
.woocommerce-cys-design-with-ai-tone-of-voice {
.choices {
width: 404px;
display: flex;
flex-direction: column;
gap: 16px;
}
}

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { Sender } from 'xstate';
import { AnyInterpreter, Sender } from 'xstate';
/**
* Internal dependencies
@ -10,6 +10,7 @@ import { customizeStoreStateMachineEvents } from '.';
import { ThemeCard } from './intro/theme-cards';
export type CustomizeStoreComponent = ( props: {
parentMachine: AnyInterpreter;
sendEvent: Sender< customizeStoreStateMachineEvents >;
context: customizeStoreStateMachineContext;
} ) => React.ReactElement | null;

View File

@ -104,14 +104,22 @@ export const getTimeFrame = ( timeInMs ) => {
*/
export const useFullScreen = ( classes ) => {
useEffect( () => {
const hasToolbarClass =
document.documentElement.classList.contains( 'wp-toolbar' );
document.body.classList.remove( 'woocommerce-admin-is-loading' );
document.body.classList.add( classes );
document.body.classList.add( 'woocommerce-admin-full-screen' );
document.body.classList.add( 'is-wp-toolbar-disabled' );
if ( hasToolbarClass ) {
document.documentElement.classList.remove( 'wp-toolbar' );
}
return () => {
document.body.classList.remove( classes );
document.body.classList.remove( 'woocommerce-admin-full-screen' );
document.body.classList.remove( 'is-wp-toolbar-disabled' );
if ( hasToolbarClass ) {
document.documentElement.classList.add( 'wp-toolbar' );
}
};
} );
};

View File

@ -145,6 +145,7 @@
"@typescript-eslint/parser": "^5.54.0",
"@woocommerce/admin-e2e-tests": "workspace:*",
"@woocommerce/admin-layout": "workspace:*",
"@woocommerce/ai": "workspace:0.1.0-beta.0",
"@woocommerce/components": "workspace:*",
"@woocommerce/csv-export": "workspace:*",
"@woocommerce/currency": "workspace:*",

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add AI wizard business info step for Customize Your Store task

File diff suppressed because it is too large Load Diff