@designcrowd/library.brand-page 5.18.2 → 5.18.3-actionable-buttons-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 (78) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DB2oJ5vl.js → CreatedOnBrandCrowd-de-de-HPO35Atc.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-fbk0Gf2G.js → CreatedOnBrandCrowd-en-us-Cl4aaalq.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-Ch4Q9__I.js → CreatedOnBrandCrowd-es-es-B_2laZxX.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-DaHJ6OMH.js → CreatedOnBrandCrowd-fr-ca-BTauzkB5.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-DaHJ6OMH.js → CreatedOnBrandCrowd-fr-fr-BTauzkB5.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-BgF8UCqy.js → CreatedOnBrandCrowd-pt-br-B0V4UZv3.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-BgF8UCqy.js → CreatedOnBrandCrowd-pt-pt-B0V4UZv3.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-j3rcGaGp.js → CreatedOnDcom-de-de-DvX7MlWL.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-Cx4eQPRU.js → CreatedOnDcom-en-us-BnXBQ4jL.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-D-1APCL9.js → CreatedOnDcom-es-es-lKzARICU.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-DAHdBw0D.js → CreatedOnDcom-fr-ca-BXBuLMp3.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-BYAw2kXr.js → CreatedOnDcom-fr-fr-wUZo-ppX.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-CdSknjZg.js → CreatedOnDcom-pt-br-Dcpf3PTV.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-DzyZ2j80.js → CreatedOnDcom-pt-pt-BUN3ynDi.js} +1 -1
  15. package/lib/src/brand-page-maker/composables/ai-chat/types.d.ts +3 -0
  16. package/lib/src/brand-page-maker/composables/ai-chat/ui-actions.d.ts +29 -0
  17. package/lib/src/brand-page-maker/composables/ai-chat/ui-actions.js +32 -0
  18. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +201 -172
  19. package/lib/src/brand-page-maker/composables/useEventTracker.d.ts +1 -0
  20. package/lib/src/brand-page-maker/composables/useEventTracker.js +1 -0
  21. package/lib/src/{bundled-translations-CnareLWC.js → bundled-translations-B5Fkt3qd.js} +1 -1
  22. package/lib/src/{bundled-translations.de-DE-2Ys7epLO.js → bundled-translations.de-DE-DpdVePM9.js} +1 -1
  23. package/lib/src/bundled-translations.es-ES-BsIcrzZF.js +175 -0
  24. package/lib/src/bundled-translations.fr-CA-G1XCp2Qh.js +175 -0
  25. package/lib/src/bundled-translations.fr-FR-BdwLXOnB.js +175 -0
  26. package/lib/src/bundled-translations.pt-BR-Bvn5ePe7.js +175 -0
  27. package/lib/src/bundled-translations.pt-PT-CDtkb2k1.js +175 -0
  28. package/lib/src/bundles/bundled-translations.de-DE.json +2 -0
  29. package/lib/src/bundles/bundled-translations.es-ES.json +2 -0
  30. package/lib/src/bundles/bundled-translations.fr-CA.json +2 -0
  31. package/lib/src/bundles/bundled-translations.fr-FR.json +2 -0
  32. package/lib/src/bundles/bundled-translations.json +2 -0
  33. package/lib/src/bundles/bundled-translations.pt-BR.json +2 -0
  34. package/lib/src/bundles/bundled-translations.pt-PT.json +2 -0
  35. package/lib/src/css/library.brand-page-brandCrowd.min.css +102 -19
  36. package/lib/src/css/library.brand-page-brandPage.min.css +102 -19
  37. package/lib/src/css/library.brand-page-designCom.min.css +102 -19
  38. package/lib/src/css/library.brand-page-preview.min.css +102 -19
  39. package/lib/src/index.mjs +1 -1
  40. package/lib/src/{lib-mcvVokSM.js → lib-BP3rUcsW.js} +17328 -17189
  41. package/lib/src/plugins/google-maps-plugin/google-maps-plugin.d.ts +14 -2
  42. package/lib/src/plugins/google-maps-plugin/google-maps-plugin.js +43 -13
  43. package/package.json +2 -2
  44. package/src/brand-page-maker/components/ai-text-edit/AiActionButtons.vue +34 -7
  45. package/src/brand-page-maker/components/ai-text-edit/AiChatbox.vue +3 -0
  46. package/src/brand-page-maker/components/ai-text-edit/TextBubble.vue +7 -1
  47. package/src/brand-page-maker/components/ai-website-builder/AiWebsiteBuilder.vue +21 -1
  48. package/src/brand-page-maker/components/brandpage-settings/BrandPageSettings.vue +8 -0
  49. package/src/brand-page-maker/composables/ai-chat/types.ts +6 -0
  50. package/src/brand-page-maker/composables/ai-chat/ui-actions.test.ts +80 -0
  51. package/src/brand-page-maker/composables/ai-chat/ui-actions.ts +58 -0
  52. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +305 -256
  53. package/src/brand-page-maker/composables/useEventTracker.ts +1 -0
  54. package/src/brand-page-maker/constants/i18n/constants.de-DE.json +2 -0
  55. package/src/brand-page-maker/constants/i18n/constants.es-ES.json +2 -0
  56. package/src/brand-page-maker/constants/i18n/constants.fr-CA.json +2 -0
  57. package/src/brand-page-maker/constants/i18n/constants.fr-FR.json +2 -0
  58. package/src/brand-page-maker/constants/i18n/constants.json +2 -0
  59. package/src/brand-page-maker/constants/i18n/constants.pt-BR.json +2 -0
  60. package/src/brand-page-maker/constants/i18n/constants.pt-PT.json +2 -0
  61. package/src/bundles/bundled-translations.de-DE.json +2 -0
  62. package/src/bundles/bundled-translations.es-ES.json +2 -0
  63. package/src/bundles/bundled-translations.fr-CA.json +2 -0
  64. package/src/bundles/bundled-translations.fr-FR.json +2 -0
  65. package/src/bundles/bundled-translations.json +2 -0
  66. package/src/bundles/bundled-translations.pt-BR.json +2 -0
  67. package/src/bundles/bundled-translations.pt-PT.json +2 -0
  68. package/src/plugins/google-maps-plugin/google-maps-plugin.ts +58 -18
  69. package/src/shared/content-settings/components/google-address-field/GoogleAddressField.vue +148 -15
  70. package/src/shared/font-family-selector/FontPicker.vue +91 -4
  71. package/lib/src/bundled-translations.es-ES-Cjp-G8rT.js +0 -175
  72. package/lib/src/bundled-translations.fr-CA-CsaW4Oae.js +0 -175
  73. package/lib/src/bundled-translations.fr-FR-DIT2dd8J.js +0 -175
  74. package/lib/src/bundled-translations.pt-BR-BamoBpDW.js +0 -175
  75. package/lib/src/bundled-translations.pt-PT-C9A4hY_e.js +0 -175
  76. package/lib/src/shared/content-settings/components/google-address-field/GoogleAddressField.mixin.d.ts +0 -54
  77. package/lib/src/shared/content-settings/components/google-address-field/GoogleAddressField.mixin.js +0 -67
  78. package/src/shared/content-settings/components/google-address-field/GoogleAddressField.mixin.ts +0 -84
@@ -1,4 +1,3 @@
1
- export declare const PLACE_CHANGED = "place_changed";
2
1
  export declare const PLACE_ID_QUERY_PREFIX = "place_id:";
3
2
  export interface GoogleMapsPluginOptions {
4
3
  readonly apiKey: string;
@@ -9,13 +8,26 @@ export interface GetEmbedGoogleMapsOptions {
9
8
  readonly query: string;
10
9
  readonly locale?: string;
11
10
  }
11
+ export interface AddressDetails {
12
+ readonly formattedAddress: string;
13
+ readonly placeId: string;
14
+ }
12
15
  export declare class GoogleMapsPlugin {
13
16
  private initialised;
14
17
  private initialising;
15
18
  private readonly _options;
16
19
  constructor(options: GoogleMapsPluginOptions);
17
20
  private initialise;
18
- createAutoCompleteWidget(input: HTMLInputElement, options?: google.maps.places.AutocompleteOptions): Promise<google.maps.places.Autocomplete>;
21
+ private getPlacesLibrary;
22
+ /**
23
+ * Creates Google's official Places Autocomplete widget (new Places API).
24
+ * It renders its own input + dropdown with built-in icons, formatting,
25
+ * keyboard/a11y support and the required "Powered by Google" attribution,
26
+ * and manages session tokens internally. Listen for `gmp-select` to read the
27
+ * chosen prediction, then pass it to {@link fetchAddressDetails}.
28
+ */
29
+ createPlaceAutocompleteElement(options?: google.maps.places.PlaceAutocompleteElementOptions): Promise<google.maps.places.PlaceAutocompleteElement>;
30
+ fetchAddressDetails(prediction: google.maps.places.PlacePrediction): Promise<AddressDetails | undefined>;
19
31
  private createGoogleMapsScriptElement;
20
32
  private getGoogleScriptUrl;
21
33
  getEmbedGoogleMapsSrc(options: GetEmbedGoogleMapsOptions): string;
@@ -8,7 +8,6 @@ const delayInMS = (delay) => {
8
8
  const DELAY_MS = 100;
9
9
  const MAP_MODE_PLACE = 'place';
10
10
  const SCRIPT_TAG_NAME = 'SCRIPT';
11
- export const PLACE_CHANGED = 'place_changed';
12
11
  export const PLACE_ID_QUERY_PREFIX = 'place_id:';
13
12
  export class GoogleMapsPlugin {
14
13
  initialised = false;
@@ -30,16 +29,23 @@ export class GoogleMapsPlugin {
30
29
  return;
31
30
  }
32
31
  const url = this.getGoogleScriptUrl();
33
- let googleMapsScript;
34
- googleMapsScript = Array.from(document.head.children).find((c) => c.tagName.toUpperCase() === SCRIPT_TAG_NAME &&
32
+ const existingScript = Array.from(document.head.children).find((c) => c.tagName.toUpperCase() === SCRIPT_TAG_NAME &&
35
33
  c.hasAttribute('src') &&
36
34
  c.getAttribute('src') === url);
37
- if (googleMapsScript) {
38
- return;
35
+ if (!existingScript) {
36
+ document.head.appendChild(this.createGoogleMapsScriptElement(url));
39
37
  }
40
- googleMapsScript = this.createGoogleMapsScriptElement(url);
41
- document.head.appendChild(googleMapsScript);
42
- while (typeof google !== 'object') {
38
+ // Wait for the specific class we use. The `places` library loads eagerly
39
+ // via the `libraries=places` script param, so the new Places classes are
40
+ // on the `google.maps.places` namespace once ready. We avoid
41
+ // `importLibrary` (absent in some hosts that already loaded Maps).
42
+ //
43
+ // `typeof google === 'undefined'` must be checked first and on its own:
44
+ // `typeof` only suppresses ReferenceError for a bare identifier, so
45
+ // touching `google.maps` while `google` is still undeclared (before the
46
+ // script executes) throws "google is not defined".
47
+ while (typeof google === 'undefined' ||
48
+ typeof google.maps?.places?.PlaceAutocompleteElement !== 'function') {
43
49
  await delayInMS(DELAY_MS);
44
50
  }
45
51
  this.initialised = true;
@@ -48,16 +54,40 @@ export class GoogleMapsPlugin {
48
54
  this.initialising = false;
49
55
  }
50
56
  }
51
- async createAutoCompleteWidget(input, options) {
57
+ async getPlacesLibrary() {
52
58
  await this.initialise();
53
59
  while (!this.initialised) {
54
60
  await delayInMS(DELAY_MS);
55
61
  }
56
- const autoComplete = new google.maps.places.Autocomplete(input, options);
62
+ return google.maps.places;
63
+ }
64
+ /**
65
+ * Creates Google's official Places Autocomplete widget (new Places API).
66
+ * It renders its own input + dropdown with built-in icons, formatting,
67
+ * keyboard/a11y support and the required "Powered by Google" attribution,
68
+ * and manages session tokens internally. Listen for `gmp-select` to read the
69
+ * chosen prediction, then pass it to {@link fetchAddressDetails}.
70
+ */
71
+ async createPlaceAutocompleteElement(options) {
72
+ const { PlaceAutocompleteElement } = await this.getPlacesLibrary();
73
+ return new PlaceAutocompleteElement({
74
+ requestedLanguage: this._options.locale,
75
+ ...options,
76
+ });
77
+ }
78
+ async fetchAddressDetails(prediction) {
79
+ const place = prediction.toPlace();
57
80
  // NOTE: additional fields may incur a charge, check before adding a new field
58
81
  // See https://developers.google.com/maps/billing-and-pricing/pricing#basic-data for more information
59
- autoComplete.setFields(['formatted_address', 'place_id']);
60
- return autoComplete;
82
+ // The widget's session token is included automatically on this first fetch.
83
+ await place.fetchFields({ fields: ['formattedAddress', 'id'] });
84
+ if (!place.formattedAddress || !place.id) {
85
+ return undefined;
86
+ }
87
+ return {
88
+ formattedAddress: place.formattedAddress,
89
+ placeId: place.id,
90
+ };
61
91
  }
62
92
  createGoogleMapsScriptElement(url) {
63
93
  const scriptElement = document.createElement(SCRIPT_TAG_NAME);
@@ -67,7 +97,7 @@ export class GoogleMapsPlugin {
67
97
  return scriptElement;
68
98
  }
69
99
  getGoogleScriptUrl() {
70
- const baseUrl = `https://maps.googleapis.com/maps/api/js?key=${this._options.apiKey}&libraries=places`;
100
+ const baseUrl = `https://maps.googleapis.com/maps/api/js?key=${this._options.apiKey}&libraries=places&loading=async`;
71
101
  return this._options.locale
72
102
  ? `${baseUrl}&language=${this._options.locale}`
73
103
  : baseUrl;
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.18.2",
4
+ "version": "5.18.3-actionable-buttons-2",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -141,7 +141,7 @@
141
141
  "@types/dompurify": "3.2.0",
142
142
  "@types/express": "4.17.21",
143
143
  "@types/faker": "6.6.8",
144
- "@types/google.maps": "3.55.1",
144
+ "@types/google.maps": "3.65.1",
145
145
  "@types/istanbul-lib-report": "3.0.3",
146
146
  "@types/jquery": "3.5.30",
147
147
  "@types/lodash.clonedeep": "4.5.9",
@@ -9,13 +9,25 @@
9
9
  icon-left="stop"
10
10
  @click="$emit('stop-generation')"
11
11
  />
12
- <Button
13
- v-else-if="showUseButton"
14
- :label="useButtonLabel"
15
- variant="primary"
16
- size="small"
17
- @click="$emit('use-content')"
18
- />
12
+ <template v-else>
13
+ <Button
14
+ v-for="action in actions"
15
+ :key="action.type"
16
+ :label="action.label"
17
+ variant="primary"
18
+ size="small"
19
+ :icon-left="action.icon || undefined"
20
+ classes="ai-cta-button"
21
+ @click="$emit('action-click', action.type)"
22
+ />
23
+ <Button
24
+ v-if="showUseButton"
25
+ :label="useButtonLabel"
26
+ variant="primary"
27
+ size="small"
28
+ @click="$emit('use-content')"
29
+ />
30
+ </template>
19
31
  <Button
20
32
  v-if="showRegenerateButton && !isTyping"
21
33
  label="Regenerate"
@@ -31,12 +43,14 @@
31
43
  <script setup lang="ts">
32
44
  import { Button } from '@designcrowd/fe-shared-lib';
33
45
  import { generateAiModalTr } from '../../../composables/useTranslate';
46
+ import type { AgentAction } from '../../composables/ai-chat/types';
34
47
 
35
48
  interface Props {
36
49
  isTyping?: boolean;
37
50
  showUseButton?: boolean;
38
51
  showRegenerateButton?: boolean;
39
52
  useButtonLabel?: string;
53
+ actions?: AgentAction[];
40
54
  }
41
55
 
42
56
  withDefaults(defineProps<Props>(), {
@@ -44,11 +58,24 @@ withDefaults(defineProps<Props>(), {
44
58
  showUseButton: false,
45
59
  showRegenerateButton: false,
46
60
  useButtonLabel: 'Use text',
61
+ actions: () => [],
47
62
  });
48
63
 
49
64
  defineEmits<{
50
65
  'use-content': [];
51
66
  'stop-generation': [];
52
67
  regenerate: [];
68
+ 'action-click': [type: AgentAction['type']];
53
69
  }>();
54
70
  </script>
71
+
72
+ <style scoped>
73
+ /* The upgrade glyph (`upgrade-alt`) is a hairline faceted gem that barely reads
74
+ at button size. Give it a stroke in the current text colour so the icon is
75
+ distinguishable from afar. Scoped to the CTA button via the `ai-cta-button`
76
+ class passed to the design-system Button. */
77
+ :deep(.ai-cta-button svg path) {
78
+ stroke: currentColor;
79
+ stroke-width: 1;
80
+ }
81
+ </style>
@@ -11,6 +11,7 @@
11
11
  @use-text="$emit('use-text', message.props.text)"
12
12
  @stop-generation="$emit('stop-generation')"
13
13
  @use-image="$emit('use-image', $event)"
14
+ @action-click="$emit('action-click', $event)"
14
15
  >
15
16
  <template v-if="message.content" #default>
16
17
  {{ message.content }}
@@ -49,6 +50,7 @@ import { ref, watch, nextTick, onMounted } from 'vue';
49
50
  import { Button } from '@designcrowd/fe-shared-lib';
50
51
  import TextBubble from './TextBubble.vue';
51
52
  import ImageBubble from './ImageBubble.vue';
53
+ import type { AgentAction } from '../../composables/ai-chat/types';
52
54
 
53
55
  interface Message {
54
56
  type: 'text' | 'image';
@@ -86,6 +88,7 @@ const emit = defineEmits<{
86
88
  'stop-generation': [];
87
89
  'use-image': [images: any[]];
88
90
  'prompt-click': [prompt: SuggestedPrompt];
91
+ 'action-click': [type: AgentAction['type']];
89
92
  }>();
90
93
 
91
94
  const textareaRef = ref<HTMLTextAreaElement | null>(null);
@@ -32,10 +32,12 @@
32
32
  :is-typing="isTyping || isAnimating"
33
33
  :show-use-button="showUseButton"
34
34
  :show-regenerate-button="showRegenerateButton"
35
+ :actions="actions"
35
36
  use-button-label="Use text"
36
37
  @use-content="$emit('use-text')"
37
38
  @stop-generation="$emit('stop-generation')"
38
39
  @regenerate="$emit('regenerate')"
40
+ @action-click="$emit('action-click', $event)"
39
41
  >
40
42
  <slot name="actions" />
41
43
  </AiActionButtons>
@@ -59,6 +61,7 @@ import AiActionButtons from './AiActionButtons.vue';
59
61
  import AiProgressFeed from './AiProgressFeed.vue';
60
62
  import { generateAiModalTr } from '../../../composables/useTranslate';
61
63
  import { type PlanItem } from '../../../clients/websocket-gateway-client';
64
+ import type { AgentAction } from '../../composables/ai-chat/types';
62
65
 
63
66
  interface Props {
64
67
  text?: string;
@@ -75,6 +78,7 @@ interface Props {
75
78
  initialMessage?: string;
76
79
  planItems?: PlanItem[];
77
80
  completedItemIds?: string[];
81
+ actions?: AgentAction[];
78
82
  }
79
83
 
80
84
  const props = withDefaults(defineProps<Props>(), {
@@ -92,12 +96,14 @@ const props = withDefaults(defineProps<Props>(), {
92
96
  initialMessage: undefined,
93
97
  planItems: () => [],
94
98
  completedItemIds: () => [],
99
+ actions: () => [],
95
100
  });
96
101
 
97
102
  defineEmits<{
98
103
  'use-text': [];
99
104
  'stop-generation': [];
100
105
  regenerate: [];
106
+ 'action-click': [type: AgentAction['type']];
101
107
  }>();
102
108
 
103
109
  const copiedFeedback = ref(false);
@@ -220,7 +226,7 @@ const showActions = computed(() => {
220
226
  props.isAi &&
221
227
  !props.isTyping &&
222
228
  !isAnimating.value &&
223
- props.showUseButton
229
+ (props.showUseButton || (props.actions?.length ?? 0) > 0)
224
230
  );
225
231
  });
226
232
  </script>
@@ -14,6 +14,7 @@
14
14
  class="tw-flex-1 tw-overflow-hidden tw-px-5"
15
15
  @send="onSendMessage"
16
16
  @stop-generation="onStopGeneration"
17
+ @action-click="onActionClick"
17
18
  />
18
19
  </div>
19
20
  </template>
@@ -24,12 +25,22 @@ import { storeToRefs } from 'pinia';
24
25
  import SideBarSettingsHeader from '../../../shared/common/SideBarSettingsHeader.vue';
25
26
  import AiChatbox from '../ai-text-edit/AiChatbox.vue';
26
27
  import { useUiSideBarStore } from '../../../shared/side-bar/store';
27
- import { useSnapshotStore, useAiWebsiteBuilderThreadStore } from '../../stores';
28
+ import {
29
+ useSnapshotStore,
30
+ useAiWebsiteBuilderThreadStore,
31
+ useBrandPageContainerStore,
32
+ } from '../../stores';
28
33
  import { SIDE_BAR_TAB_NAMES } from '../../constants';
29
34
  import { useAiWebsiteBuilderChat } from '../../composables/ai-chat/useAiWebsiteBuilderChat';
35
+ import {
36
+ UI_ACTIONS,
37
+ type AgentActionType,
38
+ } from '../../composables/ai-chat/ui-actions';
39
+ import { AI_CTA_LOCATIONS } from '../../composables/useEventTracker';
30
40
 
31
41
  const sideBarStore = useUiSideBarStore();
32
42
  const snapshotStore = useSnapshotStore();
43
+ const brandPageContainerStore = useBrandPageContainerStore();
33
44
  const aiWebsiteBuilderThreadStore = useAiWebsiteBuilderThreadStore();
34
45
  const { promptText } = storeToRefs(aiWebsiteBuilderThreadStore);
35
46
 
@@ -60,6 +71,15 @@ const onStopGeneration = async () => {
60
71
  await stopGenerationAsync();
61
72
  };
62
73
 
74
+ const onActionClick = async (type: AgentActionType) => {
75
+ if (type === UI_ACTIONS.UPGRADE) {
76
+ await brandPageContainerStore.showUpgradeModalAndSaveAsync(
77
+ undefined,
78
+ AI_CTA_LOCATIONS.AI_BUILDER_CHAT_UPSELL,
79
+ );
80
+ }
81
+ };
82
+
63
83
  defineExpose({
64
84
  stopGenerationAsync,
65
85
  });
@@ -14,6 +14,7 @@
14
14
  v-if="isPublished"
15
15
  data-component="collapsible-panel"
16
16
  :title="commonTr('domains')"
17
+ :is-collapsed="true"
17
18
  >
18
19
  <!-- Start URL Section -->
19
20
  <UrlSettings
@@ -27,6 +28,7 @@
27
28
  <CollapsiblePanel
28
29
  data-component="collapsible-panel"
29
30
  :title="brandPageSettingsTr('seoSettings')"
31
+ :is-collapsed="true"
30
32
  >
31
33
  <PageSettings
32
34
  v-if="designDraftToken"
@@ -60,6 +62,7 @@
60
62
  <CollapsiblePanel
61
63
  data-component="collapsible-panel"
62
64
  :title="brandPageSettingsTr('integrationSettings')"
65
+ :is-collapsed="true"
63
66
  >
64
67
  <GoogleAnalytics
65
68
  :value="measurementID"
@@ -72,6 +75,7 @@
72
75
  <CollapsiblePanel
73
76
  data-component="collapsible-panel"
74
77
  :title="brandPageSettingsTr('socialShareTitle')"
78
+ :is-collapsed="true"
75
79
  >
76
80
  <SocialShare
77
81
  :value="ogImageUrl"
@@ -86,6 +90,7 @@
86
90
  v-if="isPaymentProvidersSectionEnabled && !isPrePurchase"
87
91
  data-component="collapsible-panel"
88
92
  :title="brandPageSettingsTr('paymentProvidersTitle')"
93
+ :is-collapsed="true"
89
94
  >
90
95
  <div class="tw-mt-5">
91
96
  <SideBarSectionTitle
@@ -115,6 +120,7 @@
115
120
  v-if="designDraftToken"
116
121
  data-component="collapsible-panel"
117
122
  :title="commonTr('sensitiveContent')"
123
+ :is-collapsed="true"
118
124
  >
119
125
  <div class="tw-mt-2">
120
126
  <SensitiveContent
@@ -130,6 +136,7 @@
130
136
  ref="saveContactPanel"
131
137
  data-component="collapsible-panel"
132
138
  :title="saveContactSettingsTr('saveContactLabel')"
139
+ :is-collapsed="true"
133
140
  >
134
141
  <div class="tw-mt-2">
135
142
  <SaveContact
@@ -142,6 +149,7 @@
142
149
  <CollapsiblePanel
143
150
  data-component="collapsible-panel"
144
151
  :title="brandPageSettingsTr('cookieCollection')"
152
+ :is-collapsed="true"
145
153
  >
146
154
  <CookieSettings :view-model="brandPageViewModel.cookieOptions" />
147
155
  </CollapsiblePanel>
@@ -1,5 +1,10 @@
1
1
  import type { ComputedRef, Ref } from 'vue';
2
2
  import { type PlanItem } from '../../../clients/websocket-gateway-client';
3
+ import type { AgentAction } from './ui-actions';
4
+
5
+ // Re-exported for the chat components that consume it; defined alongside the
6
+ // UI-action registry in ./ui-actions.
7
+ export type { AgentAction };
3
8
 
4
9
  export interface BaseThreadMessage {
5
10
  id: string;
@@ -16,6 +21,7 @@ export interface TextThreadMessage extends BaseThreadMessage {
16
21
  initialMessage?: string;
17
22
  planItems?: PlanItem[];
18
23
  completedItemIds?: string[];
24
+ actions?: AgentAction[];
19
25
  }
20
26
 
21
27
  export interface ImageThreadMessage extends BaseThreadMessage {
@@ -0,0 +1,80 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ partitionAiToolCalls,
4
+ UI_ACTIONS,
5
+ AI_CHAT_UI_ACTIONS,
6
+ } from './ui-actions';
7
+ import type { AiAssistantToolCall } from '../../../clients/ai-assistant-client';
8
+
9
+ const tc = (fn: string, toolId = fn, args = '{}'): AiAssistantToolCall => ({
10
+ toolId,
11
+ function: fn,
12
+ arguments: args,
13
+ });
14
+
15
+ describe('partitionAiToolCalls', () => {
16
+ // The core invariant: a UI-action CTA must never land in mutationToolCalls,
17
+ // because those feed applyToolCallsAsync -> toolResults -> submitToolResults.
18
+ // A UI action has no tool result, so leaking one would leave an unresolved
19
+ // tool call in the thread and break the next LLM turn.
20
+ it('keeps an upgrade CTA out of the mutation set', () => {
21
+ const mutation = tc('build_banner_section', 'm1');
22
+ const upgrade = tc(UI_ACTIONS.UPGRADE, 'u1');
23
+
24
+ const { mutationToolCalls, actions } = partitionAiToolCalls([
25
+ mutation,
26
+ upgrade,
27
+ ]);
28
+
29
+ expect(mutationToolCalls).toEqual([mutation]);
30
+ expect(
31
+ mutationToolCalls.some((t) => t.function === UI_ACTIONS.UPGRADE),
32
+ ).toBe(false);
33
+ expect(actions).toEqual([
34
+ { type: UI_ACTIONS.UPGRADE, ...AI_CHAT_UI_ACTIONS[UI_ACTIONS.UPGRADE] },
35
+ ]);
36
+ });
37
+
38
+ it('routes a standalone upgrade to actions with zero mutations', () => {
39
+ const { mutationToolCalls, actions } = partitionAiToolCalls([
40
+ tc(UI_ACTIONS.UPGRADE),
41
+ ]);
42
+
43
+ expect(mutationToolCalls).toEqual([]);
44
+ expect(actions.map((a) => a.type)).toEqual([UI_ACTIONS.UPGRADE]);
45
+ });
46
+
47
+ it('treats unknown tool names as mutations (never swallows a real tool call)', () => {
48
+ const unknown = tc('some_future_tool');
49
+
50
+ const { mutationToolCalls, actions } = partitionAiToolCalls([unknown]);
51
+
52
+ expect(mutationToolCalls).toEqual([unknown]);
53
+ expect(actions).toEqual([]);
54
+ });
55
+
56
+ it('does not treat inherited Object keys (e.g. "constructor") as UI actions', () => {
57
+ const ctor = tc('constructor');
58
+
59
+ const { mutationToolCalls, actions } = partitionAiToolCalls([ctor]);
60
+
61
+ expect(mutationToolCalls).toEqual([ctor]);
62
+ expect(actions).toEqual([]);
63
+ });
64
+
65
+ it('resolves label/icon from the registry, not the wire arguments', () => {
66
+ const { actions } = partitionAiToolCalls([
67
+ tc(UI_ACTIONS.UPGRADE, 'u1', '{"label":"WRONG","icon":"WRONG"}'),
68
+ ]);
69
+
70
+ expect(actions[0].label).toBe('Upgrade');
71
+ expect(actions[0].icon).toBe('upgrade-alt');
72
+ });
73
+
74
+ it('returns empty partitions for no tool calls', () => {
75
+ expect(partitionAiToolCalls([])).toEqual({
76
+ mutationToolCalls: [],
77
+ actions: [],
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,58 @@
1
+ // Single source of truth for the AI-chat actionable buttons (the Upgrade CTA, etc.).
2
+ //
3
+ // The AI pipeline decides WHICH action to surface and WHETHER the user is
4
+ // eligible, then emits only the action identity as a tool call. How each action
5
+ // looks — its button label and icon — is a frontend concern and lives here.
6
+ // Click behaviour is wired in AiWebsiteBuilder, where the relevant stores live.
7
+ import type { AiAssistantToolCall } from '../../../clients/ai-assistant-client';
8
+
9
+ // Action identities — must match the tool names emitted by the AI pipeline.
10
+ export const UI_ACTIONS = {
11
+ UPGRADE: 'upgrade',
12
+ } as const;
13
+
14
+ export const AI_CHAT_UI_ACTIONS = {
15
+ [UI_ACTIONS.UPGRADE]: { label: 'Upgrade', icon: 'upgrade-alt' },
16
+ } as const;
17
+
18
+ export type AgentActionType = keyof typeof AI_CHAT_UI_ACTIONS;
19
+
20
+ // A resolved actionable button shown under an AI message. `type` identifies the
21
+ // action; `label`/`icon` are resolved from AI_CHAT_UI_ACTIONS, never from the wire.
22
+ export type AgentAction = {
23
+ type: AgentActionType;
24
+ label: string;
25
+ icon?: string;
26
+ };
27
+
28
+ // `hasOwnProperty` (not `in`) so inherited Object keys like "constructor" are never
29
+ // mistaken for a UI action.
30
+ const isUiActionFn = (fn: string | undefined): fn is AgentActionType =>
31
+ !!fn && Object.prototype.hasOwnProperty.call(AI_CHAT_UI_ACTIONS, fn);
32
+
33
+ /**
34
+ * Split server tool calls into page mutations and UI-action CTAs.
35
+ *
36
+ * Mutations are executed by the metadata-builder and answered with tool results;
37
+ * UI actions (e.g. the Upgrade button) are rendered only. UI actions MUST stay out
38
+ * of `mutationToolCalls`: they are never executed and never get a tool result, so
39
+ * leaking one into the mutation set would leave an unresolved tool call and break
40
+ * the next LLM turn on the same thread.
41
+ */
42
+ export function partitionAiToolCalls(toolCalls: AiAssistantToolCall[]): {
43
+ mutationToolCalls: AiAssistantToolCall[];
44
+ actions: AgentAction[];
45
+ } {
46
+ const mutationToolCalls: AiAssistantToolCall[] = [];
47
+ const actions: AgentAction[] = [];
48
+
49
+ for (const tc of toolCalls) {
50
+ if (isUiActionFn(tc.function)) {
51
+ actions.push({ type: tc.function, ...AI_CHAT_UI_ACTIONS[tc.function] });
52
+ } else {
53
+ mutationToolCalls.push(tc);
54
+ }
55
+ }
56
+
57
+ return { mutationToolCalls, actions };
58
+ }