@designcrowd/library.brand-page 5.19.1-id-based-items-2 → 5.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-Ruu5Az32.js → CreatedOnBrandCrowd-de-de-Ekmn0O3s.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-DGUJtvld.js → CreatedOnBrandCrowd-en-us-BjeXlKTS.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-Dg-1uOfV.js → CreatedOnBrandCrowd-es-es-rpNINAMY.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-CRKraZl8.js → CreatedOnBrandCrowd-fr-ca-BL4nSaLj.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-CRKraZl8.js → CreatedOnBrandCrowd-fr-fr-BL4nSaLj.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-bZ9VlEAu.js → CreatedOnBrandCrowd-pt-br-K0r4FyYq.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-bZ9VlEAu.js → CreatedOnBrandCrowd-pt-pt-K0r4FyYq.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-CJYfwZx8.js → CreatedOnDcom-de-de-0XYyEKYc.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-rzA1lVxH.js → CreatedOnDcom-en-us-E8hvwiMd.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-CNwGPjik.js → CreatedOnDcom-es-es-pRuCd7xX.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-DKw7OGIz.js → CreatedOnDcom-fr-ca-Bu4V3jpn.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-WzOEaAZG.js → CreatedOnDcom-fr-fr-CCcrGNz2.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DkualVhm.js → CreatedOnDcom-pt-br-1GTvaLKd.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-w4Nnsdzq.js → CreatedOnDcom-pt-pt-CTIYW10X.js} +1 -1
  15. package/lib/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.d.ts +1 -1
  16. package/lib/src/brand-page/models/index.d.ts +1 -12
  17. package/lib/src/brand-page/models/index.js +3 -0
  18. package/lib/src/brand-page/utils/personalisation.util.js +72 -14
  19. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +1 -1
  20. package/lib/src/brand-page-maker/components/admin-background-settings/AdminBackgroundSettings.mixin.d.ts +6 -0
  21. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +12 -1
  22. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +2 -0
  23. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.js +1 -0
  24. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.mixin.d.ts +1 -1
  25. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +1 -1
  26. package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +1 -1
  27. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +1 -1
  28. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +1 -1
  29. package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +1 -1
  30. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +2 -3
  31. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.js +16 -5
  32. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +0 -5
  33. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +6 -18
  34. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +1 -0
  35. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +3 -0
  36. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +3 -1
  37. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +5 -1
  38. package/lib/src/brand-page-maker/stores/ui/ui-store.d.ts +6 -0
  39. package/lib/src/brand-page-maker/stores/ui/ui-store.js +8 -0
  40. package/lib/src/clients/design-template-client.d.ts +3 -0
  41. package/lib/src/clients/models.d.ts +2 -0
  42. package/lib/src/css/library.brand-page-brandCrowd.min.css +19 -7
  43. package/lib/src/css/library.brand-page-brandPage.min.css +19 -7
  44. package/lib/src/css/library.brand-page-designCom.min.css +19 -7
  45. package/lib/src/css/library.brand-page-preview.min.css +19 -7
  46. package/lib/src/index.mjs +1 -1
  47. package/lib/src/{lib-Dy7MYyqz.js → lib-BfgUqu0B.js} +25060 -24791
  48. package/lib/src/metadata-builder/builders/footer-builder.js +44 -34
  49. package/lib/src/metadata-builder/builders/section-builders/image-gallery-builder.js +68 -57
  50. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +93 -58
  51. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +99 -66
  52. package/lib/src/metadata-builder/builders/section-builders/social-links-builder.js +17 -57
  53. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +85 -51
  54. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +74 -49
  55. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +4 -13
  56. package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +48 -66
  57. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +1 -1
  58. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
  59. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +7 -1
  60. package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +9 -0
  61. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +1 -1
  62. package/lib/src/shared/logo-versions/LogoVersions.mixin.d.ts +6 -0
  63. package/lib/src/shared/models.d.ts +2 -0
  64. package/lib/src/shared/my-designs/MyDesigns.mixin.d.ts +6 -0
  65. package/lib/src/shared/my-uploads/MyMedia.mixin.d.ts +6 -0
  66. package/lib/src/shared/my-uploads/MyUploads.mixin.d.ts +6 -0
  67. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +7 -3
  68. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +7 -3
  69. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +1 -1
  70. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +7 -1
  71. package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.d.ts +10 -439
  72. package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.js +2 -2
  73. package/lib/src/shared/template-settings/models.d.ts +1 -0
  74. package/lib/src/shared/template-settings/store/template-settings-store.d.ts +4 -0
  75. package/lib/src/shared/template-settings/store/template-settings-store.js +79 -11
  76. package/lib/src/shared/unsplash-library/UnsplashLibrary.mixin.d.ts +6 -0
  77. package/lib/src/utils/minify-brand-page-metadata.d.ts +0 -2
  78. package/lib/src/utils/minify-brand-page-metadata.js +1 -8
  79. package/package.json +1 -1
  80. package/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.ts +1 -2
  81. package/src/brand-page/models/index.ts +4 -12
  82. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -0
  83. package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +1 -0
  84. package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +2 -0
  85. package/src/brand-page/utils/personalisation.util.test.ts +35 -0
  86. package/src/brand-page/utils/personalisation.util.ts +119 -23
  87. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +4 -0
  88. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.ts +1 -0
  89. package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.ts +22 -8
  90. package/src/brand-page-maker/components/template-preview/TemplatePreview.vue +5 -10
  91. package/src/brand-page-maker/components/template-preview/TemplatePreviewAiBadge.vue +12 -0
  92. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +0 -6
  93. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +5 -0
  94. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +10 -0
  95. package/src/brand-page-maker/stores/ui/ui-store.ts +16 -0
  96. package/src/clients/design-template-client.ts +3 -0
  97. package/src/clients/models.ts +2 -0
  98. package/src/metadata-builder/builders/footer-builder.test.ts +0 -298
  99. package/src/metadata-builder/builders/footer-builder.ts +65 -56
  100. package/src/metadata-builder/builders/section-builders/image-gallery-builder.ts +88 -62
  101. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +129 -66
  102. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +160 -105
  103. package/src/metadata-builder/builders/section-builders/social-links-builder.ts +20 -65
  104. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +146 -83
  105. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +122 -70
  106. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +4 -13
  107. package/src/metadata-builder/parsers/section-tool-arguments.ts +48 -66
  108. package/src/shared/models.ts +6 -0
  109. package/src/shared/template-settings/components/TemplatesSettings.mixin.ts +2 -2
  110. package/src/shared/template-settings/models.ts +1 -0
  111. package/src/shared/template-settings/store/template-settings-store.ts +105 -18
  112. package/src/utils/minify-brand-page-metadata.ts +2 -14
  113. package/lib/src/metadata-builder/builders/item-id-utils.d.ts +0 -54
  114. package/lib/src/metadata-builder/builders/item-id-utils.js +0 -96
  115. package/src/metadata-builder/builders/item-id-utils.test.ts +0 -184
  116. package/src/metadata-builder/builders/item-id-utils.ts +0 -117
@@ -38,6 +38,12 @@ declare const _default: import("vue").DefineComponent<{}, {
38
38
  setUserPersonalisationData({ personalisationData, }: {
39
39
  personalisationData: import("../../brand-page").UserPersonalisationContent[];
40
40
  }): void;
41
+ setUserPromptContentKey({ userPromptContentKey, }: {
42
+ userPromptContentKey: string | undefined;
43
+ }): void;
44
+ setShowAiTemplatesTab({ showAiTemplatesTab, }: {
45
+ showAiTemplatesTab: boolean;
46
+ }): void;
41
47
  onBackgroundChanged(): void;
42
48
  onContentChanged(): void;
43
49
  onStyleChanged(): void;
@@ -2,8 +2,6 @@ import { type BrandPageModel, type BrandPageType, type NavModel, type PageStyles
2
2
  import type { ContentType } from '../brand-page/models/content.model';
3
3
  import { type AssetEntry } from './build-asset-manifest';
4
4
  type MinifiedFooterSocialLink = {
5
- position: number;
6
- id?: string;
7
5
  imageUrl: string;
8
6
  url: string;
9
7
  };
@@ -164,7 +164,6 @@ const minifyContent = (content, editor, assets) => {
164
164
  ...(Array.isArray(c.items) && {
165
165
  items: c.items.map((item, index) => ({
166
166
  position: index + 1,
167
- ...(item.id && { id: item.id }),
168
167
  ...st('name', item.name, editor),
169
168
  ...st('jobTitle', item.position, editor),
170
169
  ...st('testimonials', item.testimonials, editor),
@@ -192,7 +191,6 @@ const minifyContent = (content, editor, assets) => {
192
191
  ...(Array.isArray(c.items) && {
193
192
  items: c.items.map((item, index) => ({
194
193
  position: index + 1,
195
- ...(item.id && { id: item.id }),
196
194
  ...includeImageWithKey(item.imageUrl),
197
195
  ...st('title', item.title, editor),
198
196
  ...st('paragraph', item.paragraph, editor),
@@ -222,7 +220,6 @@ const minifyContent = (content, editor, assets) => {
222
220
  ...(Array.isArray(c.textItems) && {
223
221
  textItems: c.textItems.map((item, index) => ({
224
222
  position: index + 1,
225
- ...(item.id && { id: item.id }),
226
223
  ...st('text', item.text, editor),
227
224
  ...(includeStyle(item.textStyle) && { textStyle: item.textStyle }),
228
225
  })),
@@ -303,7 +300,6 @@ const minifyContent = (content, editor, assets) => {
303
300
  ...(Array.isArray(c.links) && {
304
301
  links: c.links.map((link, index) => ({
305
302
  position: index + 1,
306
- ...(link.id && { id: link.id }),
307
303
  ...(includeString(link.url) && { url: link.url }),
308
304
  ...includeImageWithKey(link.imageUrl),
309
305
  })),
@@ -343,7 +339,6 @@ const minifyContent = (content, editor, assets) => {
343
339
  ...(Array.isArray(c.images) && {
344
340
  images: c.images.map((img, index) => ({
345
341
  position: index + 1,
346
- ...(img.id && { id: img.id }),
347
342
  ...includeImageWithKey(img.imageUrl),
348
343
  ...st('imageTitle', img.imageTitle, editor),
349
344
  })),
@@ -384,9 +379,7 @@ const minifyFooter = (footer, logoAssetKey) => {
384
379
  logoImageWidth: footer.logoImageWidth || undefined,
385
380
  ...(logoAssetKey && { logoAssetKey }),
386
381
  socialLinks: footer.socialLinks?.length
387
- ? footer.socialLinks.map((l, index) => ({
388
- position: index + 1,
389
- ...(l.id && { id: l.id }),
382
+ ? footer.socialLinks.map((l) => ({
390
383
  imageUrl: l.imageUrl || '',
391
384
  url: l.url || '',
392
385
  }))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "5.19.1-id-based-items-2",
4
+ "version": "5.19.2",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -7,8 +7,7 @@ import type { ImageGalleryImageClickPayload } from '../models';
7
7
  import type { BrandPageContext, ImageGalleryItem } from '../../../models';
8
8
  import { getImageAltText } from '../../../utils';
9
9
 
10
- interface IndexedImageGalleryItem extends Omit<ImageGalleryItem, 'id'> {
11
- // Local rendering index, distinct from the stable string `id` on ImageItem.
10
+ interface IndexedImageGalleryItem extends ImageGalleryItem {
12
11
  id: number;
13
12
  }
14
13
 
@@ -234,13 +234,6 @@ export interface ImageContent extends Content {
234
234
  }
235
235
 
236
236
  export interface ImageItem {
237
- /**
238
- * Stable per-item identifier. Mirrors the section-level `Content.id` one
239
- * level deeper so AI tool calls can reference items by id rather than by
240
- * positional index. Optional for backward compatibility with persisted data
241
- * that predates item ids — backfilled on load (see ensureItemIds).
242
- */
243
- id?: string;
244
237
  imageUrl: string;
245
238
  imageTitle: string;
246
239
  width: number;
@@ -473,8 +466,6 @@ export interface ShapeItem extends ImageItem {
473
466
  }
474
467
 
475
468
  export interface TestimonialItems {
476
- /** Stable per-item identifier. See ImageItem.id. */
477
- id?: string;
478
469
  imageUrl: string;
479
470
  name: string;
480
471
  position: string;
@@ -492,8 +483,6 @@ export interface TestimonialItems {
492
483
  }
493
484
 
494
485
  export interface TextItem {
495
- /** Stable per-item identifier. See ImageItem.id. */
496
- id?: string;
497
486
  text: string;
498
487
  textStyle: TextStyleDeclaration;
499
488
 
@@ -563,7 +552,6 @@ export interface AboutSectionContent extends Content {
563
552
  }
564
553
 
565
554
  export interface LinksItem {
566
- id?: string;
567
555
  imageUrl: string;
568
556
  url: string;
569
557
  size?: string;
@@ -941,6 +929,10 @@ export class BrandPageModel {
941
929
  // Model version
942
930
  v: number;
943
931
 
932
+ // AI personalisation key persisted with the draft so previews survive login/checkout-back/
933
+ // refresh - content is retrieved by this token (user-agnostic), not regenerated (bp-5742).
934
+ userPromptContentKey?: string;
935
+
944
936
  styles: PageStyles;
945
937
 
946
938
  /** @deprecated */
@@ -456,6 +456,7 @@ exports[`migration.util > Banner Contents - text Align and Font Family should no
456
456
  },
457
457
  },
458
458
  },
459
+ "userPromptContentKey": undefined,
459
460
  "v": 17,
460
461
  },
461
462
  "useOldLogoRenderSize": true,
@@ -901,6 +902,7 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
901
902
  },
902
903
  },
903
904
  },
905
+ "userPromptContentKey": undefined,
904
906
  "v": 17,
905
907
  },
906
908
  "useOldLogoRenderSize": true,
@@ -1353,6 +1355,7 @@ exports[`migration.util > migrateBrandPageModel should return brand page model w
1353
1355
  },
1354
1356
  },
1355
1357
  },
1358
+ "userPromptContentKey": undefined,
1356
1359
  "v": 17,
1357
1360
  },
1358
1361
  "useOldLogoRenderSize": true,
@@ -472,6 +472,7 @@ exports[`migration02.ts > migration02 should return converted letterspacing 1`]
472
472
  },
473
473
  },
474
474
  },
475
+ "userPromptContentKey": undefined,
475
476
  "v": 0,
476
477
  },
477
478
  "useOldLogoRenderSize": false,
@@ -446,6 +446,7 @@ BrandPageModel {
446
446
  },
447
447
  },
448
448
  },
449
+ "userPromptContentKey": undefined,
449
450
  "v": 0,
450
451
  }
451
452
  `;
@@ -911,6 +912,7 @@ BrandPageModel {
911
912
  },
912
913
  },
913
914
  },
915
+ "userPromptContentKey": undefined,
914
916
  "v": 0,
915
917
  }
916
918
  `;
@@ -701,6 +701,41 @@ describe('personalisation.util', async () => {
701
701
  ).toBe(title);
702
702
  });
703
703
 
704
+ test('applyUserPersonalisationToBrandPageModelAsync should fall back to a populated section when the block section was not generated', async () => {
705
+ // Arrange - simulate userPrompt AI content that only generated the core sections
706
+ // (profile/about/information/media); email/website/phone are never generated.
707
+ const personalisation = {
708
+ ...PERSONALISATION,
709
+ websiteSection: undefined,
710
+ emailSection: undefined,
711
+ phoneSection: undefined,
712
+ } as unknown as typeof PERSONALISATION;
713
+
714
+ // A block whose role points at the ungenerated website section - without the fallback
715
+ // this renders the designer's placeholder instead of personalised content.
716
+ const model = new BrandPageModel({
717
+ brandPageType: BRAND_PAGE_TYPES.brandSite,
718
+ styles: {
719
+ backgroundStyles: {
720
+ backgroundImageUrl: faker.internet.url(),
721
+ personalisationRole: 'website',
722
+ },
723
+ },
724
+ });
725
+
726
+ // Act
727
+ const result = await applyUserPersonalisationToBrandPageModelAsync(
728
+ personalisation,
729
+ model,
730
+ );
731
+
732
+ // Assert - it borrowed the first populated fallback section (profile).
733
+ expect(result.isModified).toBe(true);
734
+ expect(result.model.styles.backgroundStyles.backgroundImageUrl).toBe(
735
+ PERSONALISATION.profileSection.imageUrls?.[0].imageUrl,
736
+ );
737
+ });
738
+
704
739
  test('applyUserPersonalisationToBrandPageModelAsync should return modified text items when input has text with multiple items', async () => {
705
740
  // Arrange
706
741
  const textContent = {
@@ -326,6 +326,84 @@ const getPersonalisationValueAsync = async (
326
326
  return { value: propertyValue, section };
327
327
  };
328
328
 
329
+ // The userPrompt AI content generation only populates these content-bearing sections
330
+ // (see GenerateAiDesignMakerContentService / GenerateWebsiteContentService - About,
331
+ // Information, Profile, Media). Sections like email/website/phone are never generated, so
332
+ // blocks whose role points at an ungenerated section fall back to this pool.
333
+ const PERSONALISATION_FALLBACK_ROLES: PersonalisationRole[] = [
334
+ 'profile',
335
+ 'about',
336
+ 'information',
337
+ 'media',
338
+ ];
339
+
340
+ // "Effectively empty" = no renderable content (ungenerated userPrompt sections like
341
+ // email/website/phone). A section that has data but lacks one property is NOT empty - it
342
+ // intentionally keeps its original content.
343
+ const isSectionEffectivelyEmpty = (
344
+ section: UserPersonalisationSectionWithIndices | undefined,
345
+ ): boolean => {
346
+ if (!section) {
347
+ return true;
348
+ }
349
+
350
+ return (
351
+ !section.title &&
352
+ !section.name &&
353
+ !section.url &&
354
+ !section.description &&
355
+ !section.descriptionShort &&
356
+ !section.ctaTitle &&
357
+ !section.imageUrls?.length &&
358
+ !section.shapes?.length &&
359
+ !section.items?.length &&
360
+ !section.mainShapesCollection?.tall?.length &&
361
+ !section.mainShapesCollection?.square?.length &&
362
+ !section.mainShapesCollection?.wide?.length
363
+ );
364
+ };
365
+
366
+ // Resolve a property from a specific role's section. Returns null when that section is
367
+ // ungenerated (effectively empty) or cannot supply the property, so the caller can try the
368
+ // next role.
369
+ const resolvePersonalisationValueForRoleAsync = async (
370
+ role: PersonalisationRole,
371
+ property: keyof UserPersonalisationSection | keyof UserPersonalisationItem,
372
+ personalisation: UserPersonalisationWithIndices,
373
+ ): Promise<{
374
+ value: any;
375
+ personalisation: UserPersonalisationWithIndices;
376
+ valuesToUpdate?: Record<string, any>;
377
+ } | null> => {
378
+ const key = `${role}Section` as keyof UserPersonalisationWithIndices;
379
+ const section = personalisation[key] as
380
+ | UserPersonalisationSectionWithIndices
381
+ | undefined;
382
+
383
+ if (isSectionEffectivelyEmpty(section)) {
384
+ return null;
385
+ }
386
+
387
+ const {
388
+ value,
389
+ section: updatedSection,
390
+ valuesToUpdate,
391
+ } = await getPersonalisationValueAsync(
392
+ section as UserPersonalisationSectionWithIndices,
393
+ property,
394
+ );
395
+
396
+ if (value === undefined || value === null) {
397
+ return null;
398
+ }
399
+
400
+ return {
401
+ value,
402
+ personalisation: { ...personalisation, [key]: updatedSection },
403
+ valuesToUpdate,
404
+ };
405
+ };
406
+
329
407
  const getPersonalisationValueFromPropertyAsync = async (
330
408
  propertyKey: string,
331
409
  propertyValue: any,
@@ -342,16 +420,6 @@ const getPersonalisationValueFromPropertyAsync = async (
342
420
  return { value: undefined, personalisation };
343
421
  }
344
422
 
345
- const matchingValue = Object.entries(personalisation).find(
346
- ([key]) => key === `${personalisationRole}Section`,
347
- );
348
-
349
- if (!matchingValue) {
350
- return { value: undefined, personalisation };
351
- }
352
-
353
- const section = matchingValue[1] as UserPersonalisationSectionWithIndices;
354
-
355
423
  const mapping = getPersonalisationMappingForContentType(contentType);
356
424
 
357
425
  const matchingProperty = Object.entries(mapping).find(([, value]) =>
@@ -372,20 +440,48 @@ const getPersonalisationValueFromPropertyAsync = async (
372
440
  typeof propertyValue === 'string' && propertyValue.endsWith('.svg'),
373
441
  });
374
442
 
375
- const {
376
- value,
377
- section: updatedSection,
378
- valuesToUpdate,
379
- } = await getPersonalisationValueAsync(section, property);
443
+ const primaryKey =
444
+ `${personalisationRole}Section` as keyof UserPersonalisationWithIndices;
445
+ const primarySection = personalisation[primaryKey] as
446
+ | UserPersonalisationSectionWithIndices
447
+ | undefined;
448
+
449
+ // When the block's own section has content, use it as-is - this preserves existing
450
+ // behaviour, including legitimately keeping the original when that section lacks a
451
+ // specific property (e.g. no items, or an invalid shape).
452
+ if (!isSectionEffectivelyEmpty(primarySection)) {
453
+ const {
454
+ value,
455
+ section: updatedSection,
456
+ valuesToUpdate,
457
+ } = await getPersonalisationValueAsync(
458
+ primarySection as UserPersonalisationSectionWithIndices,
459
+ property,
460
+ );
380
461
 
381
- return {
382
- value,
383
- personalisation: {
384
- ...personalisation,
385
- [`${personalisationRole}Section`]: updatedSection,
386
- },
387
- valuesToUpdate,
388
- };
462
+ return {
463
+ value,
464
+ personalisation: { ...personalisation, [primaryKey]: updatedSection },
465
+ valuesToUpdate,
466
+ };
467
+ }
468
+
469
+ // The block's section was never generated - fall back to the first populated section that
470
+ // can supply this property, so it doesn't render the "should be AI generated" placeholder.
471
+ // Never reached when the section exists (above) or all are populated (AI Website Builder).
472
+ for (const role of PERSONALISATION_FALLBACK_ROLES) {
473
+ const resolved = await resolvePersonalisationValueForRoleAsync(
474
+ role,
475
+ property,
476
+ personalisation,
477
+ );
478
+
479
+ if (resolved) {
480
+ return resolved;
481
+ }
482
+ }
483
+
484
+ return { value: undefined, personalisation };
389
485
  };
390
486
 
391
487
  type ContentProperty = Record<string, string | any>;
@@ -70,6 +70,8 @@ export interface BrandPageMakerViewModel {
70
70
  initialCategories: InitialCategories;
71
71
  personalisation?: UserPersonalisationContent[];
72
72
  isPersonalisationEnabled?: boolean;
73
+ userPromptContentKey?: string;
74
+ showAiTemplatesTab?: boolean;
73
75
  brandPageType?: BrandPageType;
74
76
  businessName?: string;
75
77
  industry?: string;
@@ -227,6 +229,8 @@ export default defineComponent({
227
229
  personalisationData: this.viewModel.personalisation,
228
230
  isPersonalisationEnabled:
229
231
  this.viewModel.isPersonalisationEnabled || false,
232
+ userPromptContentKey: this.viewModel.userPromptContentKey,
233
+ showAiTemplatesTab: this.viewModel.showAiTemplatesTab || false,
230
234
  brandPageType: this.viewModel.brandPageType,
231
235
  businessName: this.viewModel.businessName,
232
236
  industry: this.viewModel.industry,
@@ -270,6 +270,7 @@ const InitialiseMakerStores = (options: {
270
270
  isContextualUpgradeModalVisible: false,
271
271
  gettyVideos: [],
272
272
  templateSearchTerm: undefined,
273
+ aiTemplateKeywords: undefined,
273
274
  });
274
275
 
275
276
  userStore.setUserState({
@@ -15,10 +15,7 @@ import {
15
15
  getBrandPageViewModelFromBrandPageModel,
16
16
  } from '@brand-page/utils';
17
17
  import { STYLE_THEME_KEYS } from '@brand-page/constants';
18
- import {
19
- TEMPLATE_TABS,
20
- type TemplateSettingsTemplate,
21
- } from '@shared/template-settings/models';
18
+ import type { TemplateSettingsTemplate } from '@shared/template-settings/models';
22
19
  import type { DebounceHandle } from 'lodash.debounce';
23
20
  import debounce from 'lodash.debounce';
24
21
  import { MOBILE_HEIGHT, MOBILE_WIDTH } from '@brand-page-maker/constants';
@@ -74,7 +71,6 @@ export default defineComponent({
74
71
  isBrandSite: computed(
75
72
  () => brandPageMakerStore.brandPageType === BRAND_PAGE_TYPES.brandSite,
76
73
  ),
77
- selectedTab: computed(() => templateSettingsStore.selectedTab.key),
78
74
  renderAsImage: computed(() => templateSettingsStore.renderAsImage),
79
75
  brandPageMakerUserStore,
80
76
  brandPageContainerStore,
@@ -241,10 +237,28 @@ export default defineComponent({
241
237
  return '';
242
238
  }
243
239
 
244
- if (this.selectedTab === TEMPLATE_TABS.AI && this.template.contentKey) {
245
- if (this.userToken) {
246
- previewImageUrl += `&contentKey=${this.template.contentKey}&userToken=${this.userToken}`;
240
+ const isAlreadyPersonalised =
241
+ previewImageUrl.includes('contentKey=') ||
242
+ previewImageUrl.includes('userPromptContentKey=');
243
+
244
+ if (
245
+ this.isPersonalisable &&
246
+ this.template.contentKey &&
247
+ !isAlreadyPersonalised
248
+ ) {
249
+ if (this.userToken && !this.template.usesUserPromptContentKey) {
250
+ previewImageUrl += `&contentKey=${this.template.contentKey}`;
251
+
252
+ // The backend already bakes userToken into brand-site preview URLs. Only add
253
+ // it when missing - two userToken params parse as an array and the preview
254
+ // renderer rejects it ("userToken must be a string type") (bp-5765).
255
+ if (!previewImageUrl.includes('userToken=')) {
256
+ previewImageUrl += `&userToken=${this.userToken}`;
257
+ }
247
258
  } else {
259
+ // userPromptContentKey resolves from the userPrompt S3 bucket (real AI images).
260
+ // Post-purchase designs with a userPrompt key route here too, NOT through the
261
+ // userToken/regular-bucket branch above which holds placeholders (bp-5454).
248
262
  previewImageUrl += `&userPromptContentKey=${this.template.contentKey}`;
249
263
  }
250
264
  }
@@ -2,7 +2,7 @@
2
2
  <div
3
3
  ref="container"
4
4
  data-component="template-preview"
5
- class="template-preview-container hover:!tw-shadow-[0_0_0_3px] hover:!tw-shadow-info-500 tw-overflow-hidden"
5
+ class="template-preview-container hover:!tw-shadow-[0_0_0_3px] hover:!tw-shadow-info-500 tw-overflow-hidden tw-relative tw-isolate"
6
6
  :style="containerStyle"
7
7
  :class="{
8
8
  '!tw-shadow-[0_0_0_3px] !tw-shadow-info-500':
@@ -55,17 +55,10 @@
55
55
  />
56
56
  </div>
57
57
  <!-- End Preview Image -->
58
- <!-- Start personalisation tag -->
59
- <div
60
- v-if="isPersonalisable"
61
- class="tw-absolute tw-bottom-0 tw-right-0 tw-p-2 tw-z-50 tw-text-white"
62
- style="background: lightgreen"
63
- >
64
- <span>Personalisable</span>
65
- </div>
66
- <!-- End personalisation tag -->
67
58
  </div>
68
59
  </div>
60
+ <!-- AI badge outside scaled container so it renders at natural size -->
61
+ <TemplatePreviewAiBadge v-if="isPersonalisable" />
69
62
  </div>
70
63
  </template>
71
64
  <script>
@@ -73,11 +66,13 @@ import { defineComponent } from 'vue';
73
66
  import TemplatePreviewMixin from './TemplatePreview.mixin.ts';
74
67
  import Page from '@brand-page/components/page/Page.vue';
75
68
  import { Loader } from '@designcrowd/fe-shared-lib';
69
+ import TemplatePreviewAiBadge from './TemplatePreviewAiBadge.vue';
76
70
 
77
71
  export default defineComponent({
78
72
  components: {
79
73
  Page,
80
74
  Loader,
75
+ TemplatePreviewAiBadge,
81
76
  },
82
77
  mixins: [TemplatePreviewMixin],
83
78
  setup(props, context) {
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <div
3
+ data-test-ai-design
4
+ class="tw-flex tw-flex-row tw-absolute tw-top-2.5 tw-left-2.5 tw-rounded-full tw-py-1 tw-px-2 tw-text-white tw-text-xs tw-font-bold tw-z-[25] tw-bg-gradient-to-t tw-from-[#d946ef] tw-to-[#8b5cf6]"
5
+ >
6
+ <Icon name="ai" class="tw-text-white tw-pr-1" size="sm" />
7
+ <span>AI</span>
8
+ </div>
9
+ </template>
10
+ <script setup lang="ts">
11
+ import { Icon } from '@designcrowd/fe-shared-lib';
12
+ </script>
@@ -18,7 +18,6 @@ import {
18
18
  type PlanItem,
19
19
  } from '../../../clients/websocket-gateway-client';
20
20
  import { minifyBrandPageMetadata } from '../../../utils/minify-brand-page-metadata';
21
- import { ensureBrandPageItemIds } from '../../../metadata-builder/builders/item-id-utils';
22
21
  import { buildAssetManifest } from '../../../utils/build-asset-manifest';
23
22
  import { usePageNavigationStore } from '../../../shared/page-navigation/store/page-navigation-store';
24
23
  import { navigateToPageByPageId } from '../../routing';
@@ -257,11 +256,6 @@ export function useAiWebsiteBuilderChat(): UseAiChatResult {
257
256
 
258
257
  const buildMessagePayload = async (userPrompt: string) => {
259
258
  const brandPageData = brandPageMakerStore.brandPageData;
260
-
261
- // BP-5603: stamp stable uuid ids onto every nested item BEFORE minifying,
262
- // so the id the AI is shown (and returns as `item_id`) is the exact id the
263
- // executor matches when applying tool calls to this same live model.
264
- ensureBrandPageItemIds(brandPageData);
265
259
  const headerLogoDraftToken =
266
260
  brandPageData.pageHeaderDesktop?.logoImage?.logoDraftToken ??
267
261
  uiStore.logoDraftToken;
@@ -123,6 +123,7 @@ export type InitialisePayload = Omit<
123
123
  logoDraftAssets: LogoDraftAsset[];
124
124
  gettyVideos: GettyVideoEditorViewModelResponse[];
125
125
  templateSearchTerm: string | undefined;
126
+ aiTemplateKeywords: string | undefined;
126
127
  onSearchTermChanged?: OnSearchTermChangedCallback;
127
128
  };
128
129
 
@@ -266,6 +267,10 @@ export const useBrandPageContainerStore = defineStore('brandPageContainer', {
266
267
  onSearchTermChanged: payload.onSearchTermChanged,
267
268
  });
268
269
 
270
+ templateSettingsStore.setAiTemplateKeywords({
271
+ aiTemplateKeywords: payload.aiTemplateKeywords ?? '',
272
+ });
273
+
269
274
  brandPageMakerStore.setSelectedTemplateId({
270
275
  templateId: this.designTemplateToken,
271
276
  });
@@ -125,6 +125,8 @@ export interface InitialisePayload {
125
125
  templateCategories: TemplateCategory[];
126
126
  personalisationData?: UserPersonalisationContent[];
127
127
  isPersonalisationEnabled: boolean;
128
+ userPromptContentKey?: string;
129
+ showAiTemplatesTab?: boolean;
128
130
  businessName: string | undefined;
129
131
  industry: string | undefined;
130
132
  layout?: string;
@@ -1230,6 +1232,8 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
1230
1232
  templateCategories,
1231
1233
  personalisationData,
1232
1234
  isPersonalisationEnabled,
1235
+ userPromptContentKey,
1236
+ showAiTemplatesTab = false,
1233
1237
  businessName,
1234
1238
  industry,
1235
1239
  layout,
@@ -1353,6 +1357,10 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
1353
1357
  });
1354
1358
  }
1355
1359
 
1360
+ uiStore.setUserPromptContentKey({ userPromptContentKey });
1361
+
1362
+ uiStore.setShowAiTemplatesTab({ showAiTemplatesTab });
1363
+
1356
1364
  templateSettingsStore.setTemplateCategories({
1357
1365
  categories: templateCategories,
1358
1366
  });
@@ -1800,6 +1808,7 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
1800
1808
  const navigationStyleSettingsStore = useUiNavigationStyleSettingsStore();
1801
1809
  const shopStore = useShopConfigStore();
1802
1810
  const pageNavigationStore = usePageNavigationStore();
1811
+ const uiStore = useUiStore();
1803
1812
 
1804
1813
  const pages = pageSettingsStore.pages.map((p) => ({
1805
1814
  ...p,
@@ -1817,6 +1826,7 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
1817
1826
 
1818
1827
  return {
1819
1828
  v: GetLatestModelVersion(),
1829
+ userPromptContentKey: uiStore.userPromptContentKey,
1820
1830
  pages,
1821
1831
  styles: {
1822
1832
  logoStyles,
@@ -48,6 +48,8 @@ export const useUiStore = defineStore('ui', {
48
48
  },
49
49
  personalisationData: undefined,
50
50
  isPersonalisationEnabled: false,
51
+ userPromptContentKey: undefined,
52
+ showAiTemplatesTab: false,
51
53
  }),
52
54
  actions: {
53
55
  setLogoTheme(payload: { logoTheme: LogoTheme }) {
@@ -78,6 +80,20 @@ export const useUiStore = defineStore('ui', {
78
80
  }) {
79
81
  this.personalisationData = personalisationData;
80
82
  },
83
+ setUserPromptContentKey({
84
+ userPromptContentKey,
85
+ }: {
86
+ userPromptContentKey: string | undefined;
87
+ }) {
88
+ this.userPromptContentKey = userPromptContentKey;
89
+ },
90
+ setShowAiTemplatesTab({
91
+ showAiTemplatesTab,
92
+ }: {
93
+ showAiTemplatesTab: boolean;
94
+ }) {
95
+ this.showAiTemplatesTab = showAiTemplatesTab;
96
+ },
81
97
  onBackgroundChanged() {
82
98
  const brandPageMakerStore = useBrandPageMakerStore();
83
99
 
@@ -11,6 +11,7 @@ interface DesignTemplateClientOptions {}
11
11
  interface GetTemplatesAsyncOptions {
12
12
  categoryName?: string;
13
13
  searchTerm?: string;
14
+ keywords?: string;
14
15
  side?: string;
15
16
  logoDraftToken?: string;
16
17
  logoVariation?: number;
@@ -22,6 +23,8 @@ interface GetTemplatesAsyncOptions {
22
23
  locale?: string;
23
24
  logoS3Token?: string;
24
25
  logoTemplateToken?: string;
26
+ pageSection?: string;
27
+ pageName?: string;
25
28
  }
26
29
 
27
30
  export const getTemplatesEndpoint = (templateTypeName: string) =>