CYS: Add homepage template AI completion and revamped header footer (#40363)

* Moved homepage templates, added header and footer to homepage templates,  revamped templates to use metadata.

removed header and footer completion calls

* Lint

* Slight adjustment to completion prompt and changelog

* Lint

* Use header and footer in 'Change your homepage'

* Add test

* Lint

* Add back homepage templates exclusion header and footer for assembler use

* Add test for useHomeTemplates

* Lint
This commit is contained in:
Ilyas Foo 2023-09-25 18:30:31 +08:00 committed by GitHub
parent 8685fd211e
commit 0b2ad50a21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 355 additions and 134 deletions

View File

@ -0,0 +1,56 @@
/**
* External dependencies
*/
import { renderHook } from '@testing-library/react-hooks';
/**
* Internal dependencies
*/
import { useHomeTemplates, getTemplatePatterns } from '../use-home-templates';
import { usePatterns } from '../use-patterns';
// Mocking the dependent hooks and data
jest.mock( '../use-patterns' );
jest.mock( '~/customize-store/data/homepageTemplates', () => ( {
HOMEPAGE_TEMPLATES: {
template1: { blocks: [ 'header', 'content1', 'content2', 'footer' ] },
template2: { blocks: [ 'header', 'content3', 'footer' ] },
},
} ) );
const mockUsePatterns = usePatterns;
const mockPatternsByName = {
header: { name: 'header', content: '<div>Header</div>' },
content1: { name: 'content1', content: '<div>Content1</div>' },
content2: { name: 'content2', content: '<div>Content2</div>' },
content3: { name: 'content3', content: '<div>Content3</div>' },
footer: { name: 'footer', content: '<div>Footer</div>' },
};
describe( 'useHomeTemplates', () => {
beforeEach( () => {
mockUsePatterns.mockReturnValue( {
blockPatterns: Object.values( mockPatternsByName ),
isLoading: false,
} );
} );
it( 'should return home templates without first and last items', () => {
const { result } = renderHook( () => useHomeTemplates() );
// The expected result based on the HOMEPAGE_TEMPLATES and mock patterns
const expectedResult = {
template1: getTemplatePatterns(
[ 'content1', 'content2' ],
mockPatternsByName
),
template2: getTemplatePatterns(
[ 'content3' ],
mockPatternsByName
),
};
expect( result.current.homeTemplates ).toEqual( expectedResult );
} );
} );

View File

@ -10,72 +10,7 @@ import { parse } from '@wordpress/blocks';
* Internal dependencies
*/
import { usePatterns, Pattern, PatternWithBlocks } from './use-patterns';
// TODO: It might be better to create an API endpoint to get the templates.
export const LARGE_BUSINESS_TEMPLATES = {
template1: [
'a8c/cover-image-with-left-aligned-call-to-action',
'woocommerce-blocks/featured-products-5-item-grid',
'woocommerce-blocks/featured-products-fresh-and-tasty',
'woocommerce-blocks/featured-category-triple',
'a8c/3-column-testimonials',
'a8c/quotes-2',
'woocommerce-blocks/social-follow-us-in-social-media',
],
template2: [
'woocommerce-blocks/hero-product-split',
'woocommerce-blocks/featured-products-fresh-and-tasty',
'woocommerce-blocks/featured-category-triple',
'woocommerce-blocks/featured-products-fresh-and-tasty',
'a8c/three-columns-with-images-and-text',
'woocommerce-blocks/testimonials-3-columns',
'a8c/subscription',
],
template3: [
'a8c/call-to-action-7',
'a8c/3-column-testimonials',
'woocommerce-blocks/featured-products-fresh-and-tasty',
'woocommerce-blocks/featured-category-cover-image',
'woocommerce-blocks/featured-products-5-item-grid',
'woocommerce-blocks/featured-products-5-item-grid',
'woocommerce-blocks/social-follow-us-in-social-media',
],
};
export const SMALL_MEDIUM_BUSINESS_TEMPLATES = {
template1: [
'woocommerce-blocks/featured-products-fresh-and-tasty',
'woocommerce-blocks/testimonials-single',
'woocommerce-blocks/hero-product-3-split',
'a8c/contact-8',
],
template2: [
'a8c/about-me-4',
'a8c/product-feature-with-buy-button',
'woocommerce-blocks/featured-products-fresh-and-tasty',
'a8c/subscription',
'woocommerce-blocks/testimonials-3-columns',
'a8c/contact-with-map-on-the-left',
],
template3: [
'a8c/heading-and-video',
'a8c/3-column-testimonials',
'woocommerce-blocks/product-hero',
'a8c/quotes-2',
'a8c/product-feature-with-buy-button',
'a8c/simple-two-column-layout',
'woocommerce-blocks/social-follow-us-in-social-media',
],
};
const TEMPLATES = {
template1: LARGE_BUSINESS_TEMPLATES.template1,
template2: LARGE_BUSINESS_TEMPLATES.template2,
template3: LARGE_BUSINESS_TEMPLATES.template3,
template4: SMALL_MEDIUM_BUSINESS_TEMPLATES.template1,
template5: SMALL_MEDIUM_BUSINESS_TEMPLATES.template2,
template6: SMALL_MEDIUM_BUSINESS_TEMPLATES.template3,
};
import { HOMEPAGE_TEMPLATES } from '~/customize-store/data/homepageTemplates';
export const getTemplatePatterns = (
template: string[],
@ -106,6 +41,7 @@ export const patternsToNameMap = ( blockPatterns: Pattern[] ) =>
{}
);
// Returns home template patterns excluding header and footer.
export const useHomeTemplates = () => {
const { blockPatterns, isLoading } = usePatterns();
@ -116,7 +52,7 @@ export const useHomeTemplates = () => {
const homeTemplates = useMemo( () => {
if ( isLoading ) return {};
const recommendedTemplates = TEMPLATES;
const recommendedTemplates = HOMEPAGE_TEMPLATES;
return Object.entries( recommendedTemplates ).reduce(
(
@ -125,7 +61,7 @@ export const useHomeTemplates = () => {
) => {
if ( templateName in recommendedTemplates ) {
acc[ templateName ] = getTemplatePatterns(
template,
template.blocks.slice( 1, -1 ),
patternsByName
);
}

View File

@ -97,7 +97,7 @@ export const SidebarNavigationScreenHomepage = () => {
shownPatterns={ homePatterns }
blockPatterns={ homePatterns }
onClickPattern={ onClickPattern }
label={ 'Hompeage' }
label={ 'Homepage' }
orientation="vertical"
category={ 'homepage' }
isDraggable={ false }

View File

@ -0,0 +1,160 @@
// TODO: It might be better to create an API endpoint to get the templates.
export const HOMEPAGE_TEMPLATES = {
template1: {
blocks: [
// Header
'woocommerce-blocks/header-centered-menu-with-search',
// Body
'a8c/cover-image-with-left-aligned-call-to-action',
'woocommerce-blocks/featured-products-5-item-grid',
'woocommerce-blocks/featured-products-fresh-and-tasty',
'woocommerce-blocks/featured-category-triple',
'a8c/3-column-testimonials',
'a8c/quotes-2',
'woocommerce-blocks/social-follow-us-in-social-media',
// Footer
'woocommerce-blocks/footer-with-3-menus',
],
metadata: {
businessType: [ 'e-commerce', 'large-business' ],
contentFocus: [ 'featured products' ],
audience: [ 'general' ],
design: [ 'contemporary' ],
features: [
'fullwidth-image-banner',
'testimonials',
'social-media',
'search',
],
complexity: 'high',
},
},
template2: {
blocks: [
// Header
'woocommerce-blocks/header-essential',
// Body
'woocommerce-blocks/hero-product-split',
'woocommerce-blocks/featured-products-fresh-and-tasty',
'woocommerce-blocks/featured-category-triple',
'woocommerce-blocks/featured-products-fresh-and-tasty',
'a8c/three-columns-with-images-and-text',
'woocommerce-blocks/testimonials-3-columns',
'a8c/subscription',
// Footer
'woocommerce-blocks/footer-large',
],
metadata: {
businessType: [ 'e-commerce', 'subscription', 'large-business' ],
contentFocus: [ 'catalog' ],
audience: [ 'general' ],
design: [ 'contemporary' ],
features: [ 'small-banner', 'testimonials', 'newsletter' ],
complexity: 'high',
},
},
template3: {
blocks: [
// Header
'woocommerce-blocks/header-centered-menu-with-search',
// Body
'a8c/call-to-action-7',
'a8c/3-column-testimonials',
'woocommerce-blocks/featured-products-fresh-and-tasty',
'woocommerce-blocks/featured-category-cover-image',
'woocommerce-blocks/featured-products-5-item-grid',
'woocommerce-blocks/featured-products-5-item-grid',
'woocommerce-blocks/social-follow-us-in-social-media',
// Footer
'woocommerce-blocks/footer-with-3-menus',
],
metadata: {
businessType: [ 'subscription', 'large-business' ],
contentFocus: [ 'catalog', 'call-to-action' ],
audience: [ 'general' ],
design: [ 'contemporary' ],
features: [ 'small-banner', 'social-media' ],
complexity: 'high',
},
},
template4: {
blocks: [
// Header
'woocommerce-blocks/header-essential',
// Body
'woocommerce-blocks/featured-products-fresh-and-tasty',
'woocommerce-blocks/testimonials-single',
'woocommerce-blocks/hero-product-3-split',
'a8c/contact-8',
// Footer
'woocommerce-blocks/footer-simple-menu-and-cart', // This is supposed to be the "Footer with Newsletter Subscription Form"
],
metadata: {
businessType: [ 'e-commerce', 'small-medium-business' ],
contentFocus: [ 'products' ],
audience: [ 'focused' ],
design: [ 'sleek' ],
features: [ 'single-testimonial', 'contact', 'call-to-action' ],
complexity: 'medium',
},
},
template5: {
blocks: [
// Header
'woocommerce-blocks/header-essential',
// Body
'a8c/about-me-4',
'a8c/product-feature-with-buy-button',
'woocommerce-blocks/featured-products-fresh-and-tasty',
'a8c/subscription',
'woocommerce-blocks/testimonials-3-columns',
'a8c/contact-with-map-on-the-left',
// Footer
'woocommerce-blocks/footer-simple-menu-and-cart',
],
metadata: {
businessType: [ 'e-commerce', 'small-medium-business' ],
contentFocus: [ 'products', 'featured-product' ],
audience: [ 'focused' ],
design: [ 'sleek' ],
features: [ 'testimonial' ],
complexity: 'medium',
},
},
template6: {
blocks: [
// Header
'woocommerce-blocks/header-minimal',
// Body
'a8c/heading-and-video',
'a8c/3-column-testimonials',
'woocommerce-blocks/product-hero',
'a8c/quotes-2',
'a8c/product-feature-with-buy-button',
'a8c/simple-two-column-layout',
'woocommerce-blocks/social-follow-us-in-social-media',
// Footer
'woocommerce-blocks/footer-simple-menu-and-cart',
],
metadata: {
businessType: [ 'e-commerce', 'creative', 'small-medium-business' ],
contentFocus: [ 'services', 'storytelling', 'video' ],
audience: [ 'focused' ],
design: [ 'modern' ],
features: [ 'social-media', 'video' ],
complexity: 'high',
},
},
};

View File

@ -18,6 +18,7 @@ import {
LookAndToneCompletionResponse,
Header,
Footer,
HomepageTemplate,
} from './types';
import { aiWizardClosedBeforeCompletionEvent } from './events';
import {
@ -150,6 +151,24 @@ const assignFooter = assign<
},
} );
const assignHomepageTemplate = assign<
designWithAiStateMachineContext,
designWithAiStateMachineEvents
>( {
aiSuggestions: ( context, event: unknown ) => {
return {
...context.aiSuggestions,
homepageTemplate: (
event as {
data: {
response: HomepageTemplate;
};
}
).data.response.homepage_template,
};
},
} );
const updateWooAiStoreDescriptionOption = ( descriptionText: string ) => {
return dispatch( OPTIONS_STORE_NAME ).updateOptions( {
woo_ai_describe_store_description: descriptionText,
@ -243,6 +262,7 @@ export const actions = {
assignFontPairing,
assignHeader,
assignFooter,
assignHomepageTemplate,
logAIAPIRequestError,
updateQueryStep,
recordTracksStepViewed,

View File

@ -0,0 +1,41 @@
/**
* External dependencies
*/
import { z } from 'zod';
/**
* Internal dependencies
*/
import { HOMEPAGE_TEMPLATES } from '~/customize-store/data/homepageTemplates';
const allowedTemplates: string[] = Object.keys( HOMEPAGE_TEMPLATES );
export const homepageTemplateValidator = z.object( {
homepage_template: z
.string()
.refine( ( template ) => allowedTemplates.includes( template ), {
message: 'Template not part of allowed list',
} ),
} );
export const defaultHomepageTemplate = {
queryId: 'default_template',
// make sure version is updated every time the prompt is changed
version: '2023-09-21',
prompt: ( businessDescription: string, look: string, tone: string ) => {
return `
You are a WordPress theme expert and a business analyst. Analyse the following store description, merchant's chosen look and tone, template metadata, and determine the most appropriate template.
Consider the business size based on business description, where some templates are more suited for small and medium businesses, and some for large businesses.
Use metadata with the key "businessType" to determine the business size and type.
This is important, respond only with ONE template identifier (e.g., { "homepage_template": "template1" }). Do not explain or add any other information since it will fail the validation.
Chosen look and tone: ${ look } look, ${ tone } tone.
Business description: ${ businessDescription }
Templates to choose from:
${ JSON.stringify( HOMEPAGE_TEMPLATES ) }
`;
},
responseValidation: homepageTemplateValidator.parse,
};

View File

@ -3,3 +3,4 @@ export * from './lookAndTone';
export * from './fontPairings';
export * from './header';
export * from './footer';
export * from './homepageTemplate';

View File

@ -0,0 +1,49 @@
/**
* Internal dependencies
*/
import { homepageTemplateValidator } from '..';
describe( 'homepageTemplateValidator', () => {
it( 'should validate when template is part of the allowed list', () => {
const validTemplate = { homepage_template: 'template1' };
expect( () =>
homepageTemplateValidator.parse( validTemplate )
).not.toThrow();
} );
it( 'should not validate when template is not part of the allowed list', () => {
const invalidTemplate = {
homepage_template: 'nonexistingtemplate',
};
expect( () => homepageTemplateValidator.parse( invalidTemplate ) )
.toThrowErrorMatchingInlineSnapshot( `
"[
{
\\"code\\": \\"custom\\",
\\"message\\": \\"Template not part of allowed list\\",
\\"path\\": [
\\"homepage_template\\"
]
}
]"
` );
} );
it( 'should not validate when template is not a string', () => {
const invalidType = { homepage_template: 123 };
expect( () => homepageTemplateValidator.parse( invalidType ) )
.toThrowErrorMatchingInlineSnapshot( `
"[
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"number\\",
\\"path\\": [
\\"homepage_template\\"
],
\\"message\\": \\"Expected string, received number\\"
}
]"
` );
} );
} );

View File

@ -24,9 +24,8 @@ import { COLOR_PALETTES } from '../assembler-hub/sidebar/global-styles/color-pal
import {
patternsToNameMap,
getTemplatePatterns,
LARGE_BUSINESS_TEMPLATES,
SMALL_MEDIUM_BUSINESS_TEMPLATES,
} from '../assembler-hub/hooks/use-home-templates';
import { HOMEPAGE_TEMPLATES } from '../data/homepageTemplates';
const browserPopstateHandler =
() => ( sendBack: Sender< { type: 'EXTERNAL_URL_UPDATE' } > ) => {
@ -278,36 +277,21 @@ const updateGlobalStyles = async ( {
// Update the current theme template
const updateTemplate = async ( {
headerSlug,
businessSize,
homepageTemplateId,
footerSlug,
}: {
headerSlug: string;
businessSize: 'SMB' | 'LB';
homepageTemplateId:
| keyof typeof SMALL_MEDIUM_BUSINESS_TEMPLATES
| keyof typeof LARGE_BUSINESS_TEMPLATES;
footerSlug: string;
homepageTemplateId: keyof typeof HOMEPAGE_TEMPLATES;
} ) => {
const patterns = ( await resolveSelect(
coreStore
// @ts-ignore No types for this exist yet.
).getBlockPatterns() ) as Pattern[];
const patternsByName = patternsToNameMap( patterns );
const headerPattern = patternsByName[ headerSlug ];
const footerPattern = patternsByName[ footerSlug ];
const homepageTemplate = getTemplatePatterns(
businessSize === 'SMB'
? SMALL_MEDIUM_BUSINESS_TEMPLATES[ homepageTemplateId ]
: LARGE_BUSINESS_TEMPLATES[ homepageTemplateId ],
HOMEPAGE_TEMPLATES[ homepageTemplateId ].blocks,
patternsByName
);
const content = [ headerPattern, ...homepageTemplate, footerPattern ]
const content = [ ...homepageTemplate ]
.filter( Boolean )
.map( ( pattern ) => pattern.content )
.join( '\n\n' );
@ -356,11 +340,9 @@ export const assembleSite = async (
try {
await updateTemplate( {
headerSlug: context.aiSuggestions.header,
// TODO: Get from context
businessSize: 'SMB',
homepageTemplateId: 'template1',
footerSlug: context.aiSuggestions.footer,
homepageTemplateId: context.aiSuggestions
.homepageTemplate as keyof typeof HOMEPAGE_TEMPLATES,
} );
recordEvent( 'customize_your_store_ai_update_template_success' );
} catch ( error ) {

View File

@ -11,9 +11,8 @@ import {
designWithAiStateMachineContext,
designWithAiStateMachineEvents,
FontPairing,
Header,
Footer,
ColorPaletteResponse,
HomepageTemplate,
} from './types';
import {
BusinessInfoDescription,
@ -26,8 +25,7 @@ import { services } from './services';
import {
defaultColorPalette,
fontPairings,
defaultHeader,
defaultFooter,
defaultHomepageTemplate,
} from './prompts';
export const hasStepInUrl = (
@ -79,8 +77,7 @@ export const designWithAiStateMachineDefinition = createMachine(
aiSuggestions: {
defaultColorPalette: {} as ColorPaletteResponse,
fontPairing: '' as FontPairing[ 'pair_name' ],
header: '' as Header[ 'slug' ],
footer: '' as Footer[ 'slug' ],
homepageTemplate: '' as HomepageTemplate[ 'homepage_template' ],
},
},
initial: 'navigate',
@ -346,7 +343,7 @@ export const designWithAiStateMachineDefinition = createMachine(
success: { type: 'final' },
},
},
chooseHeader: {
chooseHomepageTemplate: {
initial: 'pending',
states: {
pending: {
@ -354,8 +351,8 @@ export const designWithAiStateMachineDefinition = createMachine(
src: 'queryAiEndpoint',
data: ( context ) => {
return {
...defaultHeader,
prompt: defaultHeader.prompt(
...defaultHomepageTemplate,
prompt: defaultHomepageTemplate.prompt(
context
.businessInfoDescription
.descriptionText,
@ -367,36 +364,9 @@ export const designWithAiStateMachineDefinition = createMachine(
};
},
onDone: {
actions: [ 'assignHeader' ],
target: 'success',
},
},
},
success: { type: 'final' },
},
},
chooseFooter: {
initial: 'pending',
states: {
pending: {
invoke: {
src: 'queryAiEndpoint',
data: ( context ) => {
return {
...defaultFooter,
prompt: defaultFooter.prompt(
context
.businessInfoDescription
.descriptionText,
context.lookAndFeel
.choice,
context.toneOfVoice
.choice
),
};
},
onDone: {
actions: [ 'assignFooter' ],
actions: [
'assignHomepageTemplate',
],
target: 'success',
},
},

View File

@ -12,6 +12,7 @@ import {
headerValidator,
footerValidator,
colorPaletteResponseValidator,
homepageTemplateValidator,
} from './prompts';
export type designWithAiStateMachineContext = {
@ -27,8 +28,7 @@ export type designWithAiStateMachineContext = {
aiSuggestions: {
defaultColorPalette: ColorPaletteResponse;
fontPairing: FontPairing[ 'pair_name' ];
header: Header[ 'slug' ];
footer: Footer[ 'slug' ];
homepageTemplate: HomepageTemplate[ 'homepage_template' ];
};
// If we require more data from options, previously provided core profiler details,
// we can retrieve them in preBusinessInfoDescription and then assign them here
@ -70,3 +70,5 @@ export type FontPairing = z.infer< typeof fontChoiceValidator >;
export type Header = z.infer< typeof headerValidator >;
export type Footer = z.infer< typeof footerValidator >;
export type HomepageTemplate = z.infer< typeof homepageTemplateValidator >;

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add homepage template AI completion and revamped header footer