Add cys ai header/footer (#40276)
* Add cys ai header suggestion * Add cys ai footer suggestion * Add changefile(s) from automation for the following project(s): woocommerce * Fix types --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
parent
b9b4dc852c
commit
48342fbc66
|
@ -14,6 +14,8 @@ import {
|
||||||
designWithAiStateMachineEvents,
|
designWithAiStateMachineEvents,
|
||||||
FontPairing,
|
FontPairing,
|
||||||
LookAndToneCompletionResponse,
|
LookAndToneCompletionResponse,
|
||||||
|
Header,
|
||||||
|
Footer,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { aiWizardClosedBeforeCompletionEvent } from './events';
|
import { aiWizardClosedBeforeCompletionEvent } from './events';
|
||||||
import {
|
import {
|
||||||
|
@ -110,6 +112,42 @@ const assignFontPairing = assign<
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
const assignHeader = assign<
|
||||||
|
designWithAiStateMachineContext,
|
||||||
|
designWithAiStateMachineEvents
|
||||||
|
>( {
|
||||||
|
aiSuggestions: ( context, event: unknown ) => {
|
||||||
|
return {
|
||||||
|
...context.aiSuggestions,
|
||||||
|
header: (
|
||||||
|
event as {
|
||||||
|
data: {
|
||||||
|
response: Header;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).data.response.slug,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
const assignFooter = assign<
|
||||||
|
designWithAiStateMachineContext,
|
||||||
|
designWithAiStateMachineEvents
|
||||||
|
>( {
|
||||||
|
aiSuggestions: ( context, event: unknown ) => {
|
||||||
|
return {
|
||||||
|
...context.aiSuggestions,
|
||||||
|
footer: (
|
||||||
|
event as {
|
||||||
|
data: {
|
||||||
|
response: Footer;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).data.response.slug,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
const logAIAPIRequestError = () => {
|
const logAIAPIRequestError = () => {
|
||||||
// log AI API request error
|
// log AI API request error
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
@ -178,6 +216,8 @@ export const actions = {
|
||||||
assignLookAndTone,
|
assignLookAndTone,
|
||||||
assignDefaultColorPalette,
|
assignDefaultColorPalette,
|
||||||
assignFontPairing,
|
assignFontPairing,
|
||||||
|
assignHeader,
|
||||||
|
assignFooter,
|
||||||
logAIAPIRequestError,
|
logAIAPIRequestError,
|
||||||
updateQueryStep,
|
updateQueryStep,
|
||||||
recordTracksStepViewed,
|
recordTracksStepViewed,
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const footerChoices = [
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/footer-simple-menu-and-cart',
|
||||||
|
label: 'Footer with Simple Menu and Cart',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/footer-with-3-menus',
|
||||||
|
label: 'Footer with 3 Menus',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/footer-large',
|
||||||
|
label: 'Large Footer',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const allowedFooter: string[] = footerChoices.map( ( footer ) => footer.slug );
|
||||||
|
|
||||||
|
export const footerValidator = z.object( {
|
||||||
|
slug: z.string().refine( ( slug ) => allowedFooter.includes( slug ), {
|
||||||
|
message: 'Footer not part of allowed list',
|
||||||
|
} ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
export const defaultFooter = {
|
||||||
|
queryId: 'default_footer',
|
||||||
|
|
||||||
|
// make sure version is updated every time the prompt is changed
|
||||||
|
version: '2023-09-19',
|
||||||
|
prompt: ( businessDescription: string, look: string, tone: string ) => {
|
||||||
|
return `
|
||||||
|
You are a WordPress theme expert. Analyse the following store description, merchant's chosen look and tone, and determine the most appropriate footer.
|
||||||
|
Respond only with one footer and only its JSON.
|
||||||
|
|
||||||
|
Chosen look and tone: ${ look } look, ${ tone } tone.
|
||||||
|
Business description: ${ businessDescription }
|
||||||
|
|
||||||
|
Footer to choose from:
|
||||||
|
${ JSON.stringify( footerChoices ) }
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
responseValidation: footerValidator.parse,
|
||||||
|
};
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const headerChoices = [
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/header-essential',
|
||||||
|
label: 'Essential Header',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/header-centered-menu-with-search',
|
||||||
|
label: 'Centered Menu with search Header',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/header-minimal',
|
||||||
|
label: 'Minimal Header',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/header-large',
|
||||||
|
label: 'Large Header',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const allowedHeaders: string[] = headerChoices.map( ( header ) => header.slug );
|
||||||
|
|
||||||
|
export const headerValidator = z.object( {
|
||||||
|
slug: z.string().refine( ( slug ) => allowedHeaders.includes( slug ), {
|
||||||
|
message: 'Header not part of allowed list',
|
||||||
|
} ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
export const defaultHeader = {
|
||||||
|
queryId: 'default_header',
|
||||||
|
|
||||||
|
// make sure version is updated every time the prompt is changed
|
||||||
|
version: '2023-09-19',
|
||||||
|
prompt: ( businessDescription: string, look: string, tone: string ) => {
|
||||||
|
return `
|
||||||
|
You are a WordPress theme expert. Analyse the following store description, merchant's chosen look and tone, and determine the most appropriate header.
|
||||||
|
Respond only with one header and only its JSON.
|
||||||
|
|
||||||
|
Chosen look and tone: ${ look } look, ${ tone } tone.
|
||||||
|
Business description: ${ businessDescription }
|
||||||
|
|
||||||
|
Headers to choose from:
|
||||||
|
${ JSON.stringify( headerChoices ) }
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
responseValidation: headerValidator.parse,
|
||||||
|
};
|
|
@ -1,3 +1,5 @@
|
||||||
export * from './colorChoices';
|
export * from './colorChoices';
|
||||||
export * from './lookAndTone';
|
export * from './lookAndTone';
|
||||||
export * from './fontPairings';
|
export * from './fontPairings';
|
||||||
|
export * from './header';
|
||||||
|
export * from './footer';
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { footerValidator } from '..';
|
||||||
|
|
||||||
|
describe( 'footerValidator', () => {
|
||||||
|
it( 'should validate when footer is part of the allowed list', () => {
|
||||||
|
const validFooter = { slug: 'woocommerce-blocks/footer-large' };
|
||||||
|
expect( () => footerValidator.parse( validFooter ) ).not.toThrow();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should not validate when footer is not part of the allowed list', () => {
|
||||||
|
const invalidFooter = {
|
||||||
|
slug: 'woocommerce-blocks/footer-large-invalid',
|
||||||
|
};
|
||||||
|
expect( () => footerValidator.parse( invalidFooter ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"custom\\",
|
||||||
|
\\"message\\": \\"Footer not part of allowed list\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"slug\\"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should not validate when slug is not a string', () => {
|
||||||
|
const invalidType = { slug: 123 };
|
||||||
|
expect( () => footerValidator.parse( invalidType ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"number\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"slug\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Expected string, received number\\"
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { headerValidator } from '..';
|
||||||
|
|
||||||
|
describe( 'headerValidator', () => {
|
||||||
|
it( 'should validate when header is part of the allowed list', () => {
|
||||||
|
const validHeader = { slug: 'woocommerce-blocks/header-large' };
|
||||||
|
expect( () => headerValidator.parse( validHeader ) ).not.toThrow();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should not validate when header is not part of the allowed list', () => {
|
||||||
|
const invalidHeader = {
|
||||||
|
slug: 'woocommerce-blocks/header-large-invalid',
|
||||||
|
};
|
||||||
|
expect( () => headerValidator.parse( invalidHeader ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"custom\\",
|
||||||
|
\\"message\\": \\"Header not part of allowed list\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"slug\\"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should not validate when slug is not a string', () => {
|
||||||
|
const invalidType = { slug: 123 };
|
||||||
|
expect( () => headerValidator.parse( invalidType ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"number\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"slug\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Expected string, received number\\"
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -11,6 +11,8 @@ import {
|
||||||
designWithAiStateMachineContext,
|
designWithAiStateMachineContext,
|
||||||
designWithAiStateMachineEvents,
|
designWithAiStateMachineEvents,
|
||||||
FontPairing,
|
FontPairing,
|
||||||
|
Header,
|
||||||
|
Footer,
|
||||||
ColorPaletteResponse,
|
ColorPaletteResponse,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {
|
import {
|
||||||
|
@ -21,7 +23,12 @@ import {
|
||||||
} from './pages';
|
} from './pages';
|
||||||
import { actions } from './actions';
|
import { actions } from './actions';
|
||||||
import { services } from './services';
|
import { services } from './services';
|
||||||
import { defaultColorPalette, fontPairings } from './prompts';
|
import {
|
||||||
|
defaultColorPalette,
|
||||||
|
fontPairings,
|
||||||
|
defaultHeader,
|
||||||
|
defaultFooter,
|
||||||
|
} from './prompts';
|
||||||
|
|
||||||
export const hasStepInUrl = (
|
export const hasStepInUrl = (
|
||||||
_ctx: unknown,
|
_ctx: unknown,
|
||||||
|
@ -72,6 +79,8 @@ export const designWithAiStateMachineDefinition = createMachine(
|
||||||
aiSuggestions: {
|
aiSuggestions: {
|
||||||
defaultColorPalette: {} as ColorPaletteResponse,
|
defaultColorPalette: {} as ColorPaletteResponse,
|
||||||
fontPairing: '' as FontPairing[ 'pair_name' ],
|
fontPairing: '' as FontPairing[ 'pair_name' ],
|
||||||
|
header: '' as Header[ 'slug' ],
|
||||||
|
footer: '' as Footer[ 'slug' ],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
initial: 'navigate',
|
initial: 'navigate',
|
||||||
|
@ -312,6 +321,44 @@ export const designWithAiStateMachineDefinition = createMachine(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
chooseHeader: {
|
||||||
|
invoke: {
|
||||||
|
src: 'queryAiEndpoint',
|
||||||
|
data: ( context ) => {
|
||||||
|
return {
|
||||||
|
...defaultHeader,
|
||||||
|
prompt: defaultHeader.prompt(
|
||||||
|
context.businessInfoDescription
|
||||||
|
.descriptionText,
|
||||||
|
context.lookAndFeel.choice,
|
||||||
|
context.toneOfVoice.choice
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
actions: [ 'assignHeader' ],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chooseFooter: {
|
||||||
|
invoke: {
|
||||||
|
src: 'queryAiEndpoint',
|
||||||
|
data: ( context ) => {
|
||||||
|
return {
|
||||||
|
...defaultFooter,
|
||||||
|
prompt: defaultFooter.prompt(
|
||||||
|
context.businessInfoDescription
|
||||||
|
.descriptionText,
|
||||||
|
context.lookAndFeel.choice,
|
||||||
|
context.toneOfVoice.choice
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
actions: [ 'assignFooter' ],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
postApiCallLoader: {},
|
postApiCallLoader: {},
|
||||||
|
|
|
@ -7,8 +7,10 @@ import { z } from 'zod';
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
colorPaletteValidator,
|
colorPaletteValidator,
|
||||||
colorPaletteResponseValidator,
|
|
||||||
fontChoiceValidator,
|
fontChoiceValidator,
|
||||||
|
headerValidator,
|
||||||
|
footerValidator,
|
||||||
|
colorPaletteResponseValidator,
|
||||||
} from './prompts';
|
} from './prompts';
|
||||||
|
|
||||||
export type designWithAiStateMachineContext = {
|
export type designWithAiStateMachineContext = {
|
||||||
|
@ -24,6 +26,8 @@ export type designWithAiStateMachineContext = {
|
||||||
aiSuggestions: {
|
aiSuggestions: {
|
||||||
defaultColorPalette: ColorPaletteResponse;
|
defaultColorPalette: ColorPaletteResponse;
|
||||||
fontPairing: FontPairing[ 'pair_name' ];
|
fontPairing: FontPairing[ 'pair_name' ];
|
||||||
|
header: Header[ 'slug' ];
|
||||||
|
footer: Footer[ 'slug' ];
|
||||||
};
|
};
|
||||||
// If we require more data from options, previously provided core profiler details,
|
// If we require more data from options, previously provided core profiler details,
|
||||||
// we can retrieve them in preBusinessInfoDescription and then assign them here
|
// we can retrieve them in preBusinessInfoDescription and then assign them here
|
||||||
|
@ -60,3 +64,7 @@ export type ColorPaletteResponse = z.infer<
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type FontPairing = z.infer< typeof fontChoiceValidator >;
|
export type FontPairing = z.infer< typeof fontChoiceValidator >;
|
||||||
|
|
||||||
|
export type Header = z.infer< typeof headerValidator >;
|
||||||
|
|
||||||
|
export type Footer = z.infer< typeof footerValidator >;
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add cys ai header/footer
|
Loading…
Reference in New Issue