@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,5 +1,5 @@
1
1
  import { openBlock as e, createElementBlock as l, createStaticVNode as a } from "vue";
2
- import { _ as t } from "./lib-mcvVokSM.js";
2
+ import { _ as t } from "./lib-BP3rUcsW.js";
3
3
  const i = {}, h = {
4
4
  width: "275",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as e, createElementBlock as l, createStaticVNode as a } from "vue";
2
- import { _ as t } from "./lib-mcvVokSM.js";
2
+ import { _ as t } from "./lib-BP3rUcsW.js";
3
3
  const i = {}, h = {
4
4
  width: "251",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as e, createElementBlock as l, createStaticVNode as a } from "vue";
2
- import { _ as t } from "./lib-mcvVokSM.js";
2
+ import { _ as t } from "./lib-BP3rUcsW.js";
3
3
  const i = {}, h = {
4
4
  width: "250",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as e, createElementBlock as l, createStaticVNode as a } from "vue";
2
- import { _ as t } from "./lib-mcvVokSM.js";
2
+ import { _ as t } from "./lib-BP3rUcsW.js";
3
3
  const i = {}, h = {
4
4
  width: "237",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as e, createElementBlock as l, createStaticVNode as a } from "vue";
2
- import { _ as t } from "./lib-mcvVokSM.js";
2
+ import { _ as t } from "./lib-BP3rUcsW.js";
3
3
  const i = {}, h = {
4
4
  width: "237",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as e, createElementBlock as l, createStaticVNode as a } from "vue";
2
- import { _ as t } from "./lib-mcvVokSM.js";
2
+ import { _ as t } from "./lib-BP3rUcsW.js";
3
3
  const i = {}, h = {
4
4
  width: "263",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as e, createElementBlock as l, createStaticVNode as a } from "vue";
2
- import { _ as t } from "./lib-mcvVokSM.js";
2
+ import { _ as t } from "./lib-BP3rUcsW.js";
3
3
  const i = {}, h = {
4
4
  width: "263",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as H, createElementBlock as V, createStaticVNode as t } from "vue";
2
- import { _ as e } from "./lib-mcvVokSM.js";
2
+ import { _ as e } from "./lib-BP3rUcsW.js";
3
3
  const M = {}, Z = {
4
4
  width: "242",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as H, createElementBlock as V, createStaticVNode as t } from "vue";
2
- import { _ as e } from "./lib-mcvVokSM.js";
2
+ import { _ as e } from "./lib-BP3rUcsW.js";
3
3
  const i = {}, M = {
4
4
  width: "231",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as H, createElementBlock as V, createStaticVNode as t } from "vue";
2
- import { _ as e } from "./lib-mcvVokSM.js";
2
+ import { _ as e } from "./lib-BP3rUcsW.js";
3
3
  const M = {}, Z = {
4
4
  width: "230",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as H, createElementBlock as V, createStaticVNode as t } from "vue";
2
- import { _ as e } from "./lib-mcvVokSM.js";
2
+ import { _ as e } from "./lib-BP3rUcsW.js";
3
3
  const i = {}, l = {
4
4
  width: "216",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as H, createElementBlock as V, createStaticVNode as t } from "vue";
2
- import { _ as e } from "./lib-mcvVokSM.js";
2
+ import { _ as e } from "./lib-BP3rUcsW.js";
3
3
  const i = {}, l = {
4
4
  width: "216",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as H, createElementBlock as V, createStaticVNode as t } from "vue";
2
- import { _ as M } from "./lib-mcvVokSM.js";
2
+ import { _ as M } from "./lib-BP3rUcsW.js";
3
3
  const Z = {}, e = {
4
4
  width: "232",
5
5
  height: "36",
@@ -1,5 +1,5 @@
1
1
  import { openBlock as H, createElementBlock as V, createStaticVNode as t } from "vue";
2
- import { _ as M } from "./lib-mcvVokSM.js";
2
+ import { _ as M } from "./lib-BP3rUcsW.js";
3
3
  const Z = {}, e = {
4
4
  width: "232",
5
5
  height: "36",
@@ -1,5 +1,7 @@
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
+ export type { AgentAction };
3
5
  export interface BaseThreadMessage {
4
6
  id: string;
5
7
  role: 'user' | 'agent';
@@ -14,6 +16,7 @@ export interface TextThreadMessage extends BaseThreadMessage {
14
16
  initialMessage?: string;
15
17
  planItems?: PlanItem[];
16
18
  completedItemIds?: string[];
19
+ actions?: AgentAction[];
17
20
  }
18
21
  export interface ImageThreadMessage extends BaseThreadMessage {
19
22
  type: 'image';
@@ -0,0 +1,29 @@
1
+ import type { AiAssistantToolCall } from '../../../clients/ai-assistant-client';
2
+ export declare const UI_ACTIONS: {
3
+ readonly UPGRADE: "upgrade";
4
+ };
5
+ export declare const AI_CHAT_UI_ACTIONS: {
6
+ readonly upgrade: {
7
+ readonly label: "Upgrade";
8
+ readonly icon: "upgrade-alt";
9
+ };
10
+ };
11
+ export type AgentActionType = keyof typeof AI_CHAT_UI_ACTIONS;
12
+ export type AgentAction = {
13
+ type: AgentActionType;
14
+ label: string;
15
+ icon?: string;
16
+ };
17
+ /**
18
+ * Split server tool calls into page mutations and UI-action CTAs.
19
+ *
20
+ * Mutations are executed by the metadata-builder and answered with tool results;
21
+ * UI actions (e.g. the Upgrade button) are rendered only. UI actions MUST stay out
22
+ * of `mutationToolCalls`: they are never executed and never get a tool result, so
23
+ * leaking one into the mutation set would leave an unresolved tool call and break
24
+ * the next LLM turn on the same thread.
25
+ */
26
+ export declare function partitionAiToolCalls(toolCalls: AiAssistantToolCall[]): {
27
+ mutationToolCalls: AiAssistantToolCall[];
28
+ actions: AgentAction[];
29
+ };
@@ -0,0 +1,32 @@
1
+ // Action identities — must match the tool names emitted by the AI pipeline.
2
+ export const UI_ACTIONS = {
3
+ UPGRADE: 'upgrade',
4
+ };
5
+ export const AI_CHAT_UI_ACTIONS = {
6
+ [UI_ACTIONS.UPGRADE]: { label: 'Upgrade', icon: 'upgrade-alt' },
7
+ };
8
+ // `hasOwnProperty` (not `in`) so inherited Object keys like "constructor" are never
9
+ // mistaken for a UI action.
10
+ const isUiActionFn = (fn) => !!fn && Object.prototype.hasOwnProperty.call(AI_CHAT_UI_ACTIONS, fn);
11
+ /**
12
+ * Split server tool calls into page mutations and UI-action CTAs.
13
+ *
14
+ * Mutations are executed by the metadata-builder and answered with tool results;
15
+ * UI actions (e.g. the Upgrade button) are rendered only. UI actions MUST stay out
16
+ * of `mutationToolCalls`: they are never executed and never get a tool result, so
17
+ * leaking one into the mutation set would leave an unresolved tool call and break
18
+ * the next LLM turn on the same thread.
19
+ */
20
+ export function partitionAiToolCalls(toolCalls) {
21
+ const mutationToolCalls = [];
22
+ const actions = [];
23
+ for (const tc of toolCalls) {
24
+ if (isUiActionFn(tc.function)) {
25
+ actions.push({ type: tc.function, ...AI_CHAT_UI_ACTIONS[tc.function] });
26
+ }
27
+ else {
28
+ mutationToolCalls.push(tc);
29
+ }
30
+ }
31
+ return { mutationToolCalls, actions };
32
+ }
@@ -16,6 +16,7 @@ import { pickErrorMessage, pickWsErrorMessage, buildSubmitErrorReport, } from '.
16
16
  import raygunUtil from '../../../brand-page/utils/raygun.util';
17
17
  import { useEventTracker, AI_CTA_LOCATIONS } from '../useEventTracker';
18
18
  import { Logger } from '../../../metadata-builder/logger';
19
+ import { partitionAiToolCalls } from './ui-actions';
19
20
  const BUILD_TIMEOUT_MS = 600000;
20
21
  const aiLogger = new Logger();
21
22
  let activeBuildWsClient = null;
@@ -363,6 +364,7 @@ export function useAiWebsiteBuilderChat() {
363
364
  let settled = false;
364
365
  let retryCount = 0;
365
366
  let lastAiMessage = null;
367
+ let lastActions;
366
368
  let handler = null;
367
369
  // Vacant-slot queue scoped to this build session. Persists across
368
370
  // retry rounds so a failed CREATE returns to its planned slot via
@@ -382,191 +384,206 @@ export function useAiWebsiteBuilderChat() {
382
384
  finishBuild();
383
385
  reject(new Error('Build timeout'));
384
386
  }, BUILD_TIMEOUT_MS);
385
- handler = (msg) => {
387
+ // Transition the build to "done" exactly once (idempotent). Returns
388
+ // false when already settled, so callers can early-return.
389
+ const markDone = () => {
386
390
  if (settled)
387
- return;
391
+ return false;
392
+ settled = true;
393
+ clearTimeout(timeout);
394
+ finishBuild();
395
+ return true;
396
+ };
397
+ // The single terminal path. Every resolve goes through here, so "where
398
+ // does this Promise settle?" has one answer.
399
+ const settle = (result) => {
400
+ if (markDone())
401
+ resolve(result);
402
+ };
403
+ // Streaming progress frames mutate the in-flight typing bubble and never
404
+ // settle the build. Returns true when `msg` was a progress frame.
405
+ const applyProgressUpdate = (msg) => {
406
+ if (msg.type === 'chat_started' && msg.message) {
407
+ withTypingMessage((m) => {
408
+ m.initialMessage = msg.message;
409
+ });
410
+ return true;
411
+ }
412
+ if (msg.type === 'plan_announced' && msg.items) {
413
+ withTypingMessage((m) => {
414
+ if (!m.planItems)
415
+ m.planItems = msg.items;
416
+ });
417
+ return true;
418
+ }
419
+ if (msg.type === 'item_complete' && msg.itemId) {
420
+ const itemId = msg.itemId;
421
+ withTypingMessage((m) => {
422
+ if (!m.completedItemIds?.includes(itemId)) {
423
+ m.completedItemIds = [...(m.completedItemIds ?? []), itemId];
424
+ }
425
+ });
426
+ return true;
427
+ }
428
+ return false;
429
+ };
430
+ // Apply one round of build tool calls: settle on completion/failure, or
431
+ // bump retryCount so the handler processes the next round's frame.
432
+ const processServerResponse = async (aiResponse) => {
433
+ const toolCalls = aiResponse.toolCalls ?? [];
388
434
  try {
389
- if (msg.buildId !== aiThreadStore.buildId)
390
- return;
391
- if (isStopped.value) {
392
- settled = true;
393
- clearTimeout(timeout);
394
- finishBuild();
395
- resolve({ text: null });
396
- return;
397
- }
398
- if (msg.type === 'chat_started' && msg.message) {
399
- withTypingMessage((m) => {
400
- m.initialMessage = msg.message;
435
+ // No tool calls = build cycle complete (success, max retries, or no retryable sections).
436
+ if (toolCalls.length === 0) {
437
+ // Defence-in-depth: empty toolCalls AND empty message is a
438
+ // degenerate response (graph reached END producing nothing). Never
439
+ // show "Your website has been updated" — it hides real failures.
440
+ const hasMeaningfulMessage = Boolean(aiResponse.message || lastAiMessage);
441
+ if (!hasMeaningfulMessage) {
442
+ raygunUtil.safeSendRaygunError(new Error('AI build server_response had empty toolCalls and empty message'), {
443
+ tags: [
444
+ 'feature:ai-website-builder',
445
+ 'failure:degenerate_server_response',
446
+ ],
447
+ customData: {
448
+ build_id: aiThreadStore.buildId ?? null,
449
+ thread_id: aiThreadStore.threadId ?? null,
450
+ },
451
+ });
452
+ settle({
453
+ text: 'Something went wrong while updating your website. Please try again.',
454
+ isError: true,
455
+ });
456
+ return;
457
+ }
458
+ // Mark settled BEFORE the await so concurrent frames are ignored.
459
+ if (!markDone())
460
+ return;
461
+ await snapshotStore.refreshSnapshotAsync();
462
+ resolve({
463
+ text: aiResponse.message || lastAiMessage || '',
464
+ actions: lastActions,
401
465
  });
402
466
  return;
403
467
  }
404
- if (msg.type === 'plan_announced' && msg.items) {
405
- withTypingMessage((m) => {
406
- if (!m.planItems)
407
- m.planItems = msg.items;
468
+ // Tool calls present — apply locally and submit results back.
469
+ const { toolResults, aiMessage } = await applyToolCallsAsync(aiResponse, userMessageId, vacantSlots);
470
+ lastAiMessage = aiMessage;
471
+ // Submit all tool results (success + failure) so the AI pipeline
472
+ // persists ToolMessages matching every tool_call in the conversation.
473
+ const failedResults = toolResults.filter((tr) => tr.success !== 'success');
474
+ aiLogger.log('tool submission for results', {
475
+ total: toolResults.length,
476
+ failed: failedResults.length,
477
+ failedResults,
478
+ retryCount,
479
+ });
480
+ const submitResult = await client.submitToolResultsBuildAsync(aiThreadStore.threadId, {
481
+ toolCalls: toolResults,
482
+ agentType: AI_ASSISTANT_AGENT_TYPES.WEBSITE_BUILDER,
483
+ connectionId,
484
+ retryCount,
485
+ userPlan,
486
+ });
487
+ if (failedResults.length === 0) {
488
+ settle({
489
+ text: aiMessage || 'Your website has been updated.',
490
+ actions: lastActions,
408
491
  });
409
492
  return;
410
493
  }
411
- if (msg.type === 'item_complete' && msg.itemId) {
412
- const itemId = msg.itemId;
413
- withTypingMessage((m) => {
414
- if (!m.completedItemIds?.includes(itemId)) {
415
- m.completedItemIds = [
416
- ...(m.completedItemIds ?? []),
417
- itemId,
418
- ];
419
- }
420
- });
494
+ // Failed sections will retry: refresh buildId so the handler matches
495
+ // the next round's frames, and count the round.
496
+ if (submitResult.buildId) {
497
+ aiThreadStore.buildId = submitResult.buildId;
498
+ }
499
+ retryCount += 1;
500
+ }
501
+ catch (err) {
502
+ aiLogger.log('server_response apply error', { err });
503
+ // Client-side apply failure (metadata builder, tool-call parser,
504
+ // missing field, etc.) — the server thinks the build succeeded but we
505
+ // couldn't materialise the changes. Surface it and report to Raygun
506
+ // (the server didn't see this failure).
507
+ raygunUtil.safeSendRaygunError(err instanceof Error
508
+ ? err
509
+ : new Error('AI apply failed: non-Error thrown'), {
510
+ tags: [
511
+ 'feature:ai-website-builder',
512
+ 'failure:apply_error',
513
+ 'stage:client_apply',
514
+ ],
515
+ customData: {
516
+ thread_id: aiThreadStore.threadId ?? null,
517
+ build_id: aiThreadStore.buildId ?? null,
518
+ },
519
+ });
520
+ settle({
521
+ text: "I couldn't apply those changes to your website. Please try again.",
522
+ isError: true,
523
+ });
524
+ }
525
+ };
526
+ const handleBuildError = (msg) => {
527
+ if (!markDone())
528
+ return;
529
+ // Backend already reports the underlying pipeline failure to Raygun
530
+ // (with full traceback). We report the frontend's RECEIPT of the error
531
+ // frame as a separate signal — distinct tag/grouping — for triangulation:
532
+ // both sides → normal crash; frontend only → backend Raygun outage;
533
+ // backend only → WS frame didn't reach client.
534
+ const wsMsg = msg;
535
+ raygunUtil.safeSendRaygunError(new Error(`AI build ${wsMsg.type}: ${wsMsg.code ?? wsMsg.error ?? 'unknown'}`), {
536
+ tags: [
537
+ 'feature:ai-website-builder',
538
+ 'failure:websocket_error_received',
539
+ `frame_type:${wsMsg.type}`,
540
+ ],
541
+ customData: {
542
+ code: wsMsg.code ?? null,
543
+ message: wsMsg.message ?? null,
544
+ error: wsMsg.error ?? null,
545
+ build_id: wsMsg.buildId ?? null,
546
+ thread_id: aiThreadStore.threadId ?? null,
547
+ },
548
+ });
549
+ resolve({ text: pickWsErrorMessage(msg), isError: true });
550
+ };
551
+ // Single dispatch point: ignore stale/already-settled frames, then route
552
+ // by frame kind. Progress frames update the bubble; server_response and
553
+ // error frames are the only ones that can settle the build.
554
+ handler = (msg) => {
555
+ if (settled)
556
+ return;
557
+ try {
558
+ if (msg.buildId !== aiThreadStore.buildId)
559
+ return;
560
+ if (isStopped.value) {
561
+ settle({ text: null });
421
562
  return;
422
563
  }
564
+ if (applyProgressUpdate(msg))
565
+ return;
423
566
  if (msg.type === 'server_response') {
424
- const toolCalls = (msg.toolCalls ||
425
- []);
426
- const aiResponse = {
567
+ // CTA buttons (e.g. Upgrade) ride in toolCalls but are presentation,
568
+ // not page edits. Split them out so only real mutations reach the
569
+ // metadata-builder and the tool-result submission — a UI action must
570
+ // never be answered with a tool result (it isn't in the agent's
571
+ // conversation), or the next turn on the thread fails.
572
+ const { mutationToolCalls, actions } = partitionAiToolCalls((msg.toolCalls || []));
573
+ if (actions.length)
574
+ lastActions = actions;
575
+ void processServerResponse({
427
576
  threadId: aiThreadStore.threadId || '',
428
- toolCalls,
577
+ toolCalls: mutationToolCalls,
429
578
  message: msg.message || null,
430
579
  type: 'tool_calls',
431
580
  totalPaidToolCalls: 0,
432
581
  timestamp: Date.now(),
433
- };
434
- (async () => {
435
- try {
436
- // No tool calls = build cycle complete (success, max retries, or no retryable sections)
437
- if (toolCalls.length === 0) {
438
- // Defence-in-depth: a server_response with no toolCalls AND
439
- // no message text is a degenerate response — it means the
440
- // backend graph reached END without producing anything
441
- // actionable. The server-side fix in stream_with_push now
442
- // converts this to a build_error frame, but if a future
443
- // regression slips through we must NOT show "Your website
444
- // has been updated" — that hides real failures from users
445
- // and from monitoring.
446
- const hasMeaningfulMessage = Boolean(aiResponse.message || lastAiMessage);
447
- if (!hasMeaningfulMessage) {
448
- raygunUtil.safeSendRaygunError(new Error('AI build server_response had empty toolCalls and empty message'), {
449
- tags: [
450
- 'feature:ai-website-builder',
451
- 'failure:degenerate_server_response',
452
- ],
453
- customData: {
454
- build_id: aiThreadStore.buildId ?? null,
455
- thread_id: aiThreadStore.threadId ?? null,
456
- },
457
- });
458
- settled = true;
459
- clearTimeout(timeout);
460
- finishBuild();
461
- resolve({
462
- text: 'Something went wrong while updating your website. Please try again.',
463
- isError: true,
464
- });
465
- return;
466
- }
467
- settled = true;
468
- clearTimeout(timeout);
469
- finishBuild();
470
- await snapshotStore.refreshSnapshotAsync();
471
- resolve({
472
- text: aiResponse.message || lastAiMessage || '',
473
- });
474
- return;
475
- }
476
- // Tool calls present — apply locally and submit results back
477
- const { toolResults, aiMessage } = await applyToolCallsAsync(aiResponse, userMessageId, vacantSlots);
478
- lastAiMessage = aiMessage;
479
- // Submit all tool results (success + failure) so the AI pipeline
480
- // persists ToolMessages matching every tool_call in the conversation.
481
- const failedResults = toolResults.filter((tr) => tr.success !== 'success');
482
- aiLogger.log('tool submission for results', {
483
- total: toolResults.length,
484
- failed: failedResults.length,
485
- failedResults,
486
- retryCount,
487
- });
488
- const submitResult = await client.submitToolResultsBuildAsync(aiThreadStore.threadId, {
489
- toolCalls: toolResults,
490
- agentType: AI_ASSISTANT_AGENT_TYPES.WEBSITE_BUILDER,
491
- connectionId,
492
- retryCount,
493
- userPlan,
494
- });
495
- if (failedResults.length === 0) {
496
- settled = true;
497
- clearTimeout(timeout);
498
- finishBuild();
499
- resolve({
500
- text: aiMessage || 'Your website has been updated.',
501
- });
502
- return;
503
- }
504
- // Update buildId so the handler matches subsequent WS messages
505
- if (submitResult.buildId) {
506
- aiThreadStore.buildId = submitResult.buildId;
507
- }
508
- // eslint-disable-next-line no-plusplus
509
- retryCount++;
510
- }
511
- catch (err) {
512
- aiLogger.log('server_response apply error', { err });
513
- // Client-side apply failure (metadata builder, tool-call parser,
514
- // missing field, etc.) — server thinks the build succeeded but we
515
- // couldn't materialise the changes. Mark the chat bubble as an
516
- // error so the user sees the real outcome, and report to Raygun
517
- // so the failure is visible in monitoring (the server didn't see it).
518
- raygunUtil.safeSendRaygunError(err instanceof Error
519
- ? err
520
- : new Error('AI apply failed: non-Error thrown'), {
521
- tags: [
522
- 'feature:ai-website-builder',
523
- 'failure:apply_error',
524
- 'stage:client_apply',
525
- ],
526
- customData: {
527
- thread_id: aiThreadStore.threadId ?? null,
528
- build_id: aiThreadStore.buildId ?? null,
529
- },
530
- });
531
- settled = true;
532
- clearTimeout(timeout);
533
- finishBuild();
534
- resolve({
535
- text: "I couldn't apply those changes to your website. Please try again.",
536
- isError: true,
537
- });
538
- }
539
- })();
582
+ });
540
583
  return;
541
584
  }
542
585
  if (msg.type === 'server_error' || msg.type === 'build_error') {
543
- settled = true;
544
- clearTimeout(timeout);
545
- finishBuild();
546
- // Backend already reports the underlying pipeline failure to
547
- // Raygun via its structured handler (stage:run_build /
548
- // stage:section_builder / etc. with full Python traceback).
549
- // We report the frontend's RECEIPT of the error frame as a
550
- // separate signal — distinct tag, distinct grouping. This
551
- // gives triangulation: both sides have entry → normal crash;
552
- // frontend only → backend Raygun outage; backend only → WS
553
- // frame didn't reach client (different bug shape).
554
- const wsMsg = msg;
555
- raygunUtil.safeSendRaygunError(new Error(`AI build ${wsMsg.type}: ${wsMsg.code ?? wsMsg.error ?? 'unknown'}`), {
556
- tags: [
557
- 'feature:ai-website-builder',
558
- 'failure:websocket_error_received',
559
- `frame_type:${wsMsg.type}`,
560
- ],
561
- customData: {
562
- code: wsMsg.code ?? null,
563
- message: wsMsg.message ?? null,
564
- error: wsMsg.error ?? null,
565
- build_id: wsMsg.buildId ?? null,
566
- thread_id: aiThreadStore.threadId ?? null,
567
- },
568
- });
569
- resolve({ text: pickWsErrorMessage(msg), isError: true });
586
+ handleBuildError(msg);
570
587
  return;
571
588
  }
572
589
  }
@@ -575,12 +592,10 @@ export function useAiWebsiteBuilderChat() {
575
592
  err,
576
593
  type: msg.type,
577
594
  });
578
- if (!settled) {
579
- settled = true;
580
- clearTimeout(timeout);
581
- finishBuild();
582
- resolve({ text: 'An error occurred during the build.' });
583
- }
595
+ settle({
596
+ text: 'An error occurred during the build.',
597
+ isError: true,
598
+ });
584
599
  }
585
600
  };
586
601
  wsClient.onMessage(handler);
@@ -650,6 +665,15 @@ export function useAiWebsiteBuilderChat() {
650
665
  if (result.isError) {
651
666
  threadItems.value[messageIndex].isError = true;
652
667
  }
668
+ // Attach agent-driven actions (e.g. upgrade CTA) only on successful
669
+ // (non-error) responses. Suppressed for premium users client-side as
670
+ // a defence-in-depth guard (server also vetoes via eligibility logic).
671
+ if (!result.isError &&
672
+ result.actions?.length &&
673
+ !userStore.hasPremiumSubscription) {
674
+ threadItems.value[messageIndex].actions =
675
+ result.actions;
676
+ }
653
677
  }
654
678
  else {
655
679
  threadItems.value[messageIndex].text =
@@ -736,6 +760,11 @@ export function useAiWebsiteBuilderChat() {
736
760
  planItems: msg.planItems ?? [],
737
761
  completedItemIds: msg.completedItemIds ?? [],
738
762
  }),
763
+ ...(msg.type === 'text' &&
764
+ !msg.isTyping &&
765
+ msg.actions?.length && {
766
+ actions: msg.actions,
767
+ }),
739
768
  },
740
769
  };
741
770
  });
@@ -81,6 +81,7 @@ export declare const UPGRADE_CTA_LOCATIONS: {
81
81
  };
82
82
  export declare const AI_CTA_LOCATIONS: {
83
83
  AI_BUILDER_TAB: string;
84
+ AI_BUILDER_CHAT_UPSELL: string;
84
85
  };
85
86
  export declare const TRIGGER_TYPE: {
86
87
  AUTOMATIC: string;
@@ -62,6 +62,7 @@ export const UPGRADE_CTA_LOCATIONS = {
62
62
  };
63
63
  export const AI_CTA_LOCATIONS = {
64
64
  AI_BUILDER_TAB: 'ai_builder_tab',
65
+ AI_BUILDER_CHAT_UPSELL: 'ai_builder_chat_upsell',
65
66
  };
66
67
  export const TRIGGER_TYPE = {
67
68
  AUTOMATIC: 'automatic',