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:
parent
e2c2c52c1c
commit
c45335b936
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Allow jest to pass with no tests
|
|
@ -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-*",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add AI wizard business info step for Customize Your Store task
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add ProgressBar component
|
|
@ -111,3 +111,4 @@ export {
|
|||
ProductFieldSection as __experimentalProductFieldSection,
|
||||
} from './product-section-layout';
|
||||
export { DisplayState } from './display-state';
|
||||
export { ProgressBar } from './progress-bar';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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% */
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
export type aiWizardClosedBeforeCompletionEvent = {
|
||||
type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION';
|
||||
payload: {
|
||||
step: string;
|
||||
};
|
||||
};
|
|
@ -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 } />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 cat’s 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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 }
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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' );
|
||||
}
|
||||
};
|
||||
} );
|
||||
};
|
||||
|
|
|
@ -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:*",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add AI wizard business info step for Customize Your Store task
|
1170
pnpm-lock.yaml
1170
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue