@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
@@ -44,7 +44,9 @@ import type {
44
44
  ThreadMessage,
45
45
  TextThreadMessage,
46
46
  SendMessageOptions,
47
+ AgentAction,
47
48
  } from './types';
49
+ import { partitionAiToolCalls } from './ui-actions';
48
50
 
49
51
  const BUILD_TIMEOUT_MS = 600000;
50
52
 
@@ -454,7 +456,11 @@ export function useAiWebsiteBuilderChat(): UseAiChatResult {
454
456
  const callApiBuildAsync = async (
455
457
  userPrompt: string,
456
458
  userMessageId: string,
457
- ): Promise<{ text: string | null; isError?: boolean }> => {
459
+ ): Promise<{
460
+ text: string | null;
461
+ isError?: boolean;
462
+ actions?: AgentAction[];
463
+ }> => {
458
464
  const connectionId = await ensureConnectedAsync();
459
465
  const wsClient = activeBuildWsClient!;
460
466
 
@@ -525,297 +531,323 @@ export function useAiWebsiteBuilderChat(): UseAiChatResult {
525
531
  });
526
532
  }
527
533
 
528
- return new Promise<{ text: string | null; isError?: boolean }>(
529
- (resolve, reject) => {
530
- let settled = false;
531
- let retryCount = 0;
532
- let lastAiMessage: string | null = null;
533
- let handler: ((msg: WebSocketMessage) => void) | null = null;
534
- // Vacant-slot queue scoped to this build session. Persists across
535
- // retry rounds so a failed CREATE returns to its planned slot via
536
- // a synthesised `move_before_content_id`.
537
- const vacantSlots: VacantSlotMap = new Map();
538
-
539
- const finishBuild = () => {
540
- aiThreadStore.isBuildInProgress = false;
541
- wsClient.setBuildActive(false);
542
- wsClient.stopResponseHeartbeat();
543
-
544
- if (handler) wsClient.offMessage(handler);
545
- };
534
+ return new Promise<{
535
+ text: string | null;
536
+ isError?: boolean;
537
+ actions?: AgentAction[];
538
+ }>((resolve, reject) => {
539
+ let settled = false;
540
+ let retryCount = 0;
541
+ let lastAiMessage: string | null = null;
542
+ let lastActions: AgentAction[] | undefined;
543
+ let handler: ((msg: WebSocketMessage) => void) | null = null;
544
+ // Vacant-slot queue scoped to this build session. Persists across
545
+ // retry rounds so a failed CREATE returns to its planned slot via
546
+ // a synthesised `move_before_content_id`.
547
+ const vacantSlots: VacantSlotMap = new Map();
548
+
549
+ const finishBuild = () => {
550
+ aiThreadStore.isBuildInProgress = false;
551
+ wsClient.setBuildActive(false);
552
+ wsClient.stopResponseHeartbeat();
553
+
554
+ if (handler) wsClient.offMessage(handler);
555
+ };
546
556
 
547
- const timeout = setTimeout(() => {
548
- if (settled) return;
557
+ const timeout = setTimeout(() => {
558
+ if (settled) return;
549
559
 
550
- settled = true;
551
- finishBuild();
552
- reject(new Error('Build timeout'));
553
- }, BUILD_TIMEOUT_MS);
560
+ settled = true;
561
+ finishBuild();
562
+ reject(new Error('Build timeout'));
563
+ }, BUILD_TIMEOUT_MS);
554
564
 
555
- handler = (msg: WebSocketMessage) => {
556
- if (settled) return;
565
+ // Transition the build to "done" exactly once (idempotent). Returns
566
+ // false when already settled, so callers can early-return.
567
+ const markDone = (): boolean => {
568
+ if (settled) return false;
557
569
 
558
- try {
559
- if (msg.buildId !== aiThreadStore.buildId) return;
570
+ settled = true;
571
+ clearTimeout(timeout);
572
+ finishBuild();
560
573
 
561
- if (isStopped.value) {
562
- settled = true;
563
- clearTimeout(timeout);
564
- finishBuild();
565
- resolve({ text: null });
574
+ return true;
575
+ };
566
576
 
567
- return;
568
- }
577
+ // The single terminal path. Every resolve goes through here, so "where
578
+ // does this Promise settle?" has one answer.
579
+ const settle = (result: {
580
+ text: string | null;
581
+ isError?: boolean;
582
+ actions?: AgentAction[];
583
+ }): void => {
584
+ if (markDone()) resolve(result);
585
+ };
569
586
 
570
- if (msg.type === 'chat_started' && msg.message) {
571
- withTypingMessage((m) => {
572
- m.initialMessage = msg.message;
573
- });
587
+ // Streaming progress frames mutate the in-flight typing bubble and never
588
+ // settle the build. Returns true when `msg` was a progress frame.
589
+ const applyProgressUpdate = (msg: WebSocketMessage): boolean => {
590
+ if (msg.type === 'chat_started' && msg.message) {
591
+ withTypingMessage((m) => {
592
+ m.initialMessage = msg.message;
593
+ });
574
594
 
575
- return;
576
- }
595
+ return true;
596
+ }
577
597
 
578
- if (msg.type === 'plan_announced' && msg.items) {
579
- withTypingMessage((m) => {
580
- if (!m.planItems) m.planItems = msg.items as PlanItem[];
581
- });
598
+ if (msg.type === 'plan_announced' && msg.items) {
599
+ withTypingMessage((m) => {
600
+ if (!m.planItems) m.planItems = msg.items as PlanItem[];
601
+ });
582
602
 
583
- return;
584
- }
603
+ return true;
604
+ }
585
605
 
586
- if (msg.type === 'item_complete' && msg.itemId) {
587
- const itemId = msg.itemId;
606
+ if (msg.type === 'item_complete' && msg.itemId) {
607
+ const itemId = msg.itemId;
588
608
 
589
- withTypingMessage((m) => {
590
- if (!m.completedItemIds?.includes(itemId)) {
591
- m.completedItemIds = [
592
- ...(m.completedItemIds ?? []),
593
- itemId,
594
- ];
595
- }
596
- });
597
-
598
- return;
609
+ withTypingMessage((m) => {
610
+ if (!m.completedItemIds?.includes(itemId)) {
611
+ m.completedItemIds = [...(m.completedItemIds ?? []), itemId];
599
612
  }
613
+ });
600
614
 
601
- if (msg.type === 'server_response') {
602
- const toolCalls = (msg.toolCalls ||
603
- []) as AiAssistantToolCall[];
604
- const aiResponse: AiAssistantResponse = {
605
- threadId: aiThreadStore.threadId || '',
606
- toolCalls,
607
- message: msg.message || null,
608
- type: 'tool_calls',
609
- totalPaidToolCalls: 0,
610
- timestamp: Date.now(),
611
- };
612
-
613
- (async () => {
614
- try {
615
- // No tool calls = build cycle complete (success, max retries, or no retryable sections)
616
- if (toolCalls.length === 0) {
617
- // Defence-in-depth: a server_response with no toolCalls AND
618
- // no message text is a degenerate response — it means the
619
- // backend graph reached END without producing anything
620
- // actionable. The server-side fix in stream_with_push now
621
- // converts this to a build_error frame, but if a future
622
- // regression slips through we must NOT show "Your website
623
- // has been updated" — that hides real failures from users
624
- // and from monitoring.
625
- const hasMeaningfulMessage = Boolean(
626
- aiResponse.message || lastAiMessage,
627
- );
628
-
629
- if (!hasMeaningfulMessage) {
630
- raygunUtil.safeSendRaygunError(
631
- new Error(
632
- 'AI build server_response had empty toolCalls and empty message',
633
- ),
634
- {
635
- tags: [
636
- 'feature:ai-website-builder',
637
- 'failure:degenerate_server_response',
638
- ],
639
- customData: {
640
- build_id: aiThreadStore.buildId ?? null,
641
- thread_id: aiThreadStore.threadId ?? null,
642
- },
643
- },
644
- );
645
- settled = true;
646
- clearTimeout(timeout);
647
- finishBuild();
648
- resolve({
649
- text: 'Something went wrong while updating your website. Please try again.',
650
- isError: true,
651
- });
652
-
653
- return;
654
- }
655
-
656
- settled = true;
657
- clearTimeout(timeout);
658
- finishBuild();
659
- await snapshotStore.refreshSnapshotAsync();
660
- resolve({
661
- text: aiResponse.message || lastAiMessage || '',
662
- });
663
-
664
- return;
665
- }
666
-
667
- // Tool calls present — apply locally and submit results back
668
- const { toolResults, aiMessage } =
669
- await applyToolCallsAsync(
670
- aiResponse,
671
- userMessageId,
672
- vacantSlots,
673
- );
674
-
675
- lastAiMessage = aiMessage;
676
-
677
- // Submit all tool results (success + failure) so the AI pipeline
678
- // persists ToolMessages matching every tool_call in the conversation.
679
- const failedResults = toolResults.filter(
680
- (tr) => tr.success !== 'success',
681
- );
682
-
683
- aiLogger.log('tool submission for results', {
684
- total: toolResults.length,
685
- failed: failedResults.length,
686
- failedResults,
687
- retryCount,
688
- });
689
-
690
- const submitResult =
691
- await client.submitToolResultsBuildAsync(
692
- aiThreadStore.threadId!,
693
- {
694
- toolCalls: toolResults,
695
- agentType: AI_ASSISTANT_AGENT_TYPES.WEBSITE_BUILDER,
696
- connectionId,
697
- retryCount,
698
- userPlan,
699
- },
700
- );
701
-
702
- if (failedResults.length === 0) {
703
- settled = true;
704
- clearTimeout(timeout);
705
- finishBuild();
706
- resolve({
707
- text: aiMessage || 'Your website has been updated.',
708
- });
709
-
710
- return;
711
- }
712
-
713
- // Update buildId so the handler matches subsequent WS messages
714
- if (submitResult.buildId) {
715
- aiThreadStore.buildId = submitResult.buildId;
716
- }
717
-
718
- // eslint-disable-next-line no-plusplus
719
- retryCount++;
720
- } catch (err) {
721
- aiLogger.log('server_response apply error', { err });
722
- // Client-side apply failure (metadata builder, tool-call parser,
723
- // missing field, etc.) — server thinks the build succeeded but we
724
- // couldn't materialise the changes. Mark the chat bubble as an
725
- // error so the user sees the real outcome, and report to Raygun
726
- // so the failure is visible in monitoring (the server didn't see it).
727
- raygunUtil.safeSendRaygunError(
728
- err instanceof Error
729
- ? err
730
- : new Error('AI apply failed: non-Error thrown'),
731
- {
732
- tags: [
733
- 'feature:ai-website-builder',
734
- 'failure:apply_error',
735
- 'stage:client_apply',
736
- ],
737
- customData: {
738
- thread_id: aiThreadStore.threadId ?? null,
739
- build_id: aiThreadStore.buildId ?? null,
740
- },
741
- },
742
- );
743
- settled = true;
744
- clearTimeout(timeout);
745
- finishBuild();
746
- resolve({
747
- text: "I couldn't apply those changes to your website. Please try again.",
748
- isError: true,
749
- });
750
- }
751
- })();
752
-
753
- return;
754
- }
615
+ return true;
616
+ }
755
617
 
756
- if (msg.type === 'server_error' || msg.type === 'build_error') {
757
- settled = true;
758
- clearTimeout(timeout);
759
- finishBuild();
760
-
761
- // Backend already reports the underlying pipeline failure to
762
- // Raygun via its structured handler (stage:run_build /
763
- // stage:section_builder / etc. with full Python traceback).
764
- // We report the frontend's RECEIPT of the error frame as a
765
- // separate signal — distinct tag, distinct grouping. This
766
- // gives triangulation: both sides have entry → normal crash;
767
- // frontend only → backend Raygun outage; backend only → WS
768
- // frame didn't reach client (different bug shape).
769
- const wsMsg = msg as {
770
- type: string;
771
- code?: string;
772
- message?: string;
773
- error?: string;
774
- buildId?: string;
775
- };
618
+ return false;
619
+ };
776
620
 
621
+ // Apply one round of build tool calls: settle on completion/failure, or
622
+ // bump retryCount so the handler processes the next round's frame.
623
+ const processServerResponse = async (
624
+ aiResponse: AiAssistantResponse,
625
+ ): Promise<void> => {
626
+ const toolCalls = aiResponse.toolCalls ?? [];
627
+
628
+ try {
629
+ // No tool calls = build cycle complete (success, max retries, or no retryable sections).
630
+ if (toolCalls.length === 0) {
631
+ // Defence-in-depth: empty toolCalls AND empty message is a
632
+ // degenerate response (graph reached END producing nothing). Never
633
+ // show "Your website has been updated" — it hides real failures.
634
+ const hasMeaningfulMessage = Boolean(
635
+ aiResponse.message || lastAiMessage,
636
+ );
637
+
638
+ if (!hasMeaningfulMessage) {
777
639
  raygunUtil.safeSendRaygunError(
778
640
  new Error(
779
- `AI build ${wsMsg.type}: ${wsMsg.code ?? wsMsg.error ?? 'unknown'}`,
641
+ 'AI build server_response had empty toolCalls and empty message',
780
642
  ),
781
643
  {
782
644
  tags: [
783
645
  'feature:ai-website-builder',
784
- 'failure:websocket_error_received',
785
- `frame_type:${wsMsg.type}`,
646
+ 'failure:degenerate_server_response',
786
647
  ],
787
648
  customData: {
788
- code: wsMsg.code ?? null,
789
- message: wsMsg.message ?? null,
790
- error: wsMsg.error ?? null,
791
- build_id: wsMsg.buildId ?? null,
649
+ build_id: aiThreadStore.buildId ?? null,
792
650
  thread_id: aiThreadStore.threadId ?? null,
793
651
  },
794
652
  },
795
653
  );
796
-
797
- resolve({ text: pickWsErrorMessage(msg), isError: true });
654
+ settle({
655
+ text: 'Something went wrong while updating your website. Please try again.',
656
+ isError: true,
657
+ });
798
658
 
799
659
  return;
800
660
  }
801
- } catch (err) {
802
- aiLogger.log('Unexpected error in WS message handler', {
803
- err,
804
- type: msg.type,
661
+
662
+ // Mark settled BEFORE the await so concurrent frames are ignored.
663
+ if (!markDone()) return;
664
+
665
+ await snapshotStore.refreshSnapshotAsync();
666
+ resolve({
667
+ text: aiResponse.message || lastAiMessage || '',
668
+ actions: lastActions,
805
669
  });
806
670
 
807
- if (!settled) {
808
- settled = true;
809
- clearTimeout(timeout);
810
- finishBuild();
811
- resolve({ text: 'An error occurred during the build.' });
812
- }
671
+ return;
672
+ }
673
+
674
+ // Tool calls present — apply locally and submit results back.
675
+ const { toolResults, aiMessage } = await applyToolCallsAsync(
676
+ aiResponse,
677
+ userMessageId,
678
+ vacantSlots,
679
+ );
680
+
681
+ lastAiMessage = aiMessage;
682
+
683
+ // Submit all tool results (success + failure) so the AI pipeline
684
+ // persists ToolMessages matching every tool_call in the conversation.
685
+ const failedResults = toolResults.filter(
686
+ (tr) => tr.success !== 'success',
687
+ );
688
+
689
+ aiLogger.log('tool submission for results', {
690
+ total: toolResults.length,
691
+ failed: failedResults.length,
692
+ failedResults,
693
+ retryCount,
694
+ });
695
+
696
+ const submitResult = await client.submitToolResultsBuildAsync(
697
+ aiThreadStore.threadId!,
698
+ {
699
+ toolCalls: toolResults,
700
+ agentType: AI_ASSISTANT_AGENT_TYPES.WEBSITE_BUILDER,
701
+ connectionId,
702
+ retryCount,
703
+ userPlan,
704
+ },
705
+ );
706
+
707
+ if (failedResults.length === 0) {
708
+ settle({
709
+ text: aiMessage || 'Your website has been updated.',
710
+ actions: lastActions,
711
+ });
712
+
713
+ return;
714
+ }
715
+
716
+ // Failed sections will retry: refresh buildId so the handler matches
717
+ // the next round's frames, and count the round.
718
+ if (submitResult.buildId) {
719
+ aiThreadStore.buildId = submitResult.buildId;
813
720
  }
721
+
722
+ retryCount += 1;
723
+ } catch (err) {
724
+ aiLogger.log('server_response apply error', { err });
725
+ // Client-side apply failure (metadata builder, tool-call parser,
726
+ // missing field, etc.) — the server thinks the build succeeded but we
727
+ // couldn't materialise the changes. Surface it and report to Raygun
728
+ // (the server didn't see this failure).
729
+ raygunUtil.safeSendRaygunError(
730
+ err instanceof Error
731
+ ? err
732
+ : new Error('AI apply failed: non-Error thrown'),
733
+ {
734
+ tags: [
735
+ 'feature:ai-website-builder',
736
+ 'failure:apply_error',
737
+ 'stage:client_apply',
738
+ ],
739
+ customData: {
740
+ thread_id: aiThreadStore.threadId ?? null,
741
+ build_id: aiThreadStore.buildId ?? null,
742
+ },
743
+ },
744
+ );
745
+ settle({
746
+ text: "I couldn't apply those changes to your website. Please try again.",
747
+ isError: true,
748
+ });
749
+ }
750
+ };
751
+
752
+ const handleBuildError = (msg: WebSocketMessage): void => {
753
+ if (!markDone()) return;
754
+
755
+ // Backend already reports the underlying pipeline failure to Raygun
756
+ // (with full traceback). We report the frontend's RECEIPT of the error
757
+ // frame as a separate signal — distinct tag/grouping — for triangulation:
758
+ // both sides → normal crash; frontend only → backend Raygun outage;
759
+ // backend only → WS frame didn't reach client.
760
+ const wsMsg = msg as {
761
+ type: string;
762
+ code?: string;
763
+ message?: string;
764
+ error?: string;
765
+ buildId?: string;
814
766
  };
815
767
 
816
- wsClient.onMessage(handler);
817
- },
818
- );
768
+ raygunUtil.safeSendRaygunError(
769
+ new Error(
770
+ `AI build ${wsMsg.type}: ${wsMsg.code ?? wsMsg.error ?? 'unknown'}`,
771
+ ),
772
+ {
773
+ tags: [
774
+ 'feature:ai-website-builder',
775
+ 'failure:websocket_error_received',
776
+ `frame_type:${wsMsg.type}`,
777
+ ],
778
+ customData: {
779
+ code: wsMsg.code ?? null,
780
+ message: wsMsg.message ?? null,
781
+ error: wsMsg.error ?? null,
782
+ build_id: wsMsg.buildId ?? null,
783
+ thread_id: aiThreadStore.threadId ?? null,
784
+ },
785
+ },
786
+ );
787
+
788
+ resolve({ text: pickWsErrorMessage(msg), isError: true });
789
+ };
790
+
791
+ // Single dispatch point: ignore stale/already-settled frames, then route
792
+ // by frame kind. Progress frames update the bubble; server_response and
793
+ // error frames are the only ones that can settle the build.
794
+ handler = (msg: WebSocketMessage) => {
795
+ if (settled) return;
796
+
797
+ try {
798
+ if (msg.buildId !== aiThreadStore.buildId) return;
799
+
800
+ if (isStopped.value) {
801
+ settle({ text: null });
802
+
803
+ return;
804
+ }
805
+
806
+ if (applyProgressUpdate(msg)) return;
807
+
808
+ if (msg.type === 'server_response') {
809
+ // CTA buttons (e.g. Upgrade) ride in toolCalls but are presentation,
810
+ // not page edits. Split them out so only real mutations reach the
811
+ // metadata-builder and the tool-result submission — a UI action must
812
+ // never be answered with a tool result (it isn't in the agent's
813
+ // conversation), or the next turn on the thread fails.
814
+ const { mutationToolCalls, actions } = partitionAiToolCalls(
815
+ (msg.toolCalls || []) as AiAssistantToolCall[],
816
+ );
817
+
818
+ if (actions.length) lastActions = actions;
819
+
820
+ void processServerResponse({
821
+ threadId: aiThreadStore.threadId || '',
822
+ toolCalls: mutationToolCalls,
823
+ message: msg.message || null,
824
+ type: 'tool_calls',
825
+ totalPaidToolCalls: 0,
826
+ timestamp: Date.now(),
827
+ });
828
+
829
+ return;
830
+ }
831
+
832
+ if (msg.type === 'server_error' || msg.type === 'build_error') {
833
+ handleBuildError(msg);
834
+
835
+ return;
836
+ }
837
+ } catch (err) {
838
+ aiLogger.log('Unexpected error in WS message handler', {
839
+ err,
840
+ type: msg.type,
841
+ });
842
+ settle({
843
+ text: 'An error occurred during the build.',
844
+ isError: true,
845
+ });
846
+ }
847
+ };
848
+
849
+ wsClient.onMessage(handler);
850
+ });
819
851
  } catch (err) {
820
852
  aiThreadStore.isBuildInProgress = false;
821
853
  wsClient.setBuildActive(false);
@@ -896,6 +928,18 @@ export function useAiWebsiteBuilderChat(): UseAiChatResult {
896
928
  if (result.isError) {
897
929
  threadItems.value[messageIndex].isError = true;
898
930
  }
931
+
932
+ // Attach agent-driven actions (e.g. upgrade CTA) only on successful
933
+ // (non-error) responses. Suppressed for premium users client-side as
934
+ // a defence-in-depth guard (server also vetoes via eligibility logic).
935
+ if (
936
+ !result.isError &&
937
+ result.actions?.length &&
938
+ !userStore.hasPremiumSubscription
939
+ ) {
940
+ (threadItems.value[messageIndex] as TextThreadMessage).actions =
941
+ result.actions;
942
+ }
899
943
  } else {
900
944
  threadItems.value[messageIndex].text =
901
945
  'Failed to apply changes. Please try again.';
@@ -997,6 +1041,11 @@ export function useAiWebsiteBuilderChat(): UseAiChatResult {
997
1041
  completedItemIds:
998
1042
  (msg as TextThreadMessage).completedItemIds ?? [],
999
1043
  }),
1044
+ ...(msg.type === 'text' &&
1045
+ !msg.isTyping &&
1046
+ (msg as TextThreadMessage).actions?.length && {
1047
+ actions: (msg as TextThreadMessage).actions,
1048
+ }),
1000
1049
  } as Record<string, unknown>,
1001
1050
  };
1002
1051
  });
@@ -99,6 +99,7 @@ export const UPGRADE_CTA_LOCATIONS = {
99
99
 
100
100
  export const AI_CTA_LOCATIONS = {
101
101
  AI_BUILDER_TAB: 'ai_builder_tab',
102
+ AI_BUILDER_CHAT_UPSELL: 'ai_builder_chat_upsell',
102
103
  };
103
104
 
104
105
  export const TRIGGER_TYPE = {
@@ -12,6 +12,8 @@
12
12
  "modernDisplayName": "modern",
13
13
  "uniqueDisplayName": "einzigartig",
14
14
  "allFontStylesDisplayName": "Alle Schriftstile",
15
+ "searchFontsPlaceholder": "Schriftarten durchsuchen",
16
+ "noFontsFoundMessage": "Keine Schriftarten gefunden",
15
17
  "settingsSaveSuccessMessage": "Einstellungen wurden erfolgreich gespeichert"
16
18
  }
17
19
  }
@@ -12,6 +12,8 @@
12
12
  "modernDisplayName": "moderna",
13
13
  "uniqueDisplayName": "única",
14
14
  "allFontStylesDisplayName": "Todos los estilos de fuente",
15
+ "searchFontsPlaceholder": "Busca fuentes",
16
+ "noFontsFoundMessage": "No se encontraron fuentes",
15
17
  "settingsSaveSuccessMessage": "La configuración se ha guardado correctamente"
16
18
  }
17
19
  }
@@ -12,6 +12,8 @@
12
12
  "modernDisplayName": "Moderne",
13
13
  "uniqueDisplayName": "Unique",
14
14
  "allFontStylesDisplayName": "Tous les styles de police",
15
+ "searchFontsPlaceholder": "Rechercher des polices",
16
+ "noFontsFoundMessage": "Aucune police trouvée",
15
17
  "settingsSaveSuccessMessage": "Paramètres enregistrés avec succès"
16
18
  }
17
19
  }
@@ -12,6 +12,8 @@
12
12
  "modernDisplayName": "Moderne",
13
13
  "uniqueDisplayName": "Unique",
14
14
  "allFontStylesDisplayName": "Tous les styles de police",
15
+ "searchFontsPlaceholder": "Rechercher des polices",
16
+ "noFontsFoundMessage": "Aucune police trouvée",
15
17
  "settingsSaveSuccessMessage": "Paramètres enregistrés avec succès"
16
18
  }
17
19
  }