From d3d39a81d6614d016663a710338d177063fc49de Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:53:28 +0530 Subject: [PATCH 1/3] fix: Prevent duplicate chat creation in the web widget during latency (#10745) --- app/javascript/shared/components/Button.vue | 3 +- .../widget/components/PreChat/Form.vue | 8 +++-- app/javascript/widget/router.js | 35 ++++++++++++++++++- .../widget/store/modules/appConfig.js | 13 +++++++ .../modules/specs/appConfig/actions.spec.js | 7 ++++ .../modules/specs/appConfig/getters.spec.js | 6 ++++ .../modules/specs/appConfig/mutations.spec.js | 8 +++++ .../specs/conversation/actions.spec.js | 1 + app/javascript/widget/store/types.js | 1 + app/javascript/widget/views/PreChatForm.vue | 16 ++++++--- 10 files changed, 90 insertions(+), 8 deletions(-) diff --git a/app/javascript/shared/components/Button.vue b/app/javascript/shared/components/Button.vue index e49c380e0..5d4213d0b 100644 --- a/app/javascript/shared/components/Button.vue +++ b/app/javascript/shared/components/Button.vue @@ -24,7 +24,8 @@ export default { }, computed: { buttonClassName() { - let className = 'text-white py-3 px-4 rounded shadow-sm leading-4'; + let className = + 'text-white py-3 px-4 rounded shadow-sm leading-4 cursor-pointer disabled:opacity-50'; if (this.type === 'clear') { className = 'flex mx-auto mt-4 text-xs leading-3 w-auto text-black-600'; } diff --git a/app/javascript/widget/components/PreChat/Form.vue b/app/javascript/widget/components/PreChat/Form.vue index 6b2c93cd6..280cfbf43 100644 --- a/app/javascript/widget/components/PreChat/Form.vue +++ b/app/javascript/widget/components/PreChat/Form.vue @@ -52,9 +52,13 @@ export default { ...mapGetters({ widgetColor: 'appConfig/getWidgetColor', isCreating: 'conversation/getIsCreating', + isConversationRouting: 'appConfig/getIsUpdatingRoute', activeCampaign: 'campaign/getActiveCampaign', currentUser: 'contacts/getCurrentUser', }), + isCreatingConversation() { + return this.isCreating || this.isConversationRouting; + }, textColor() { return getContrastingTextColor(this.widgetColor); }, @@ -337,9 +341,9 @@ export default { block :bg-color="widgetColor" :text-color="textColor" - :disabled="isCreating" + :disabled="isCreatingConversation" > - + {{ $t('START_CONVERSATION') }} diff --git a/app/javascript/widget/router.js b/app/javascript/widget/router.js index 418fd9ae6..e8230c2d9 100755 --- a/app/javascript/widget/router.js +++ b/app/javascript/widget/router.js @@ -1,7 +1,8 @@ import { createRouter, createWebHashHistory } from 'vue-router'; import ViewWithHeader from './components/layouts/ViewWithHeader.vue'; +import store from './store'; -export default createRouter({ +const router = createRouter({ history: createWebHashHistory(), routes: [ { @@ -42,3 +43,35 @@ export default createRouter({ }, ], }); + +/** + * Navigation Guards to Handle Route Transitions + * + * Purpose: + * Prevents duplicate form submissions and API calls during route transitions, + * especially important in high-latency scenarios. + * + * Flow: + * 1. beforeEach: Sets isUpdatingRoute to true at start of navigation + * 2. Component buttons/actions check this flag to prevent duplicate actions + * 3. afterEach: Resets the flag once navigation is complete + * + * Implementation note: + * Handling it globally, so that we can use it across all components + * to ensure consistent UI behavior during all route transitions. + * + * @see https://github.com/chatwoot/chatwoot/issues/10736 + */ + +router.beforeEach(async (to, from, next) => { + // Prevent any user interactions during route transition + await store.dispatch('appConfig/setRouteTransitionState', true); + next(); +}); + +router.afterEach(() => { + // Re-enable user interactions after navigation is complete + store.dispatch('appConfig/setRouteTransitionState', false); +}); + +export default router; diff --git a/app/javascript/widget/store/modules/appConfig.js b/app/javascript/widget/store/modules/appConfig.js index a7df60d29..44a13c4c4 100644 --- a/app/javascript/widget/store/modules/appConfig.js +++ b/app/javascript/widget/store/modules/appConfig.js @@ -5,6 +5,7 @@ import { SET_WIDGET_APP_CONFIG, SET_WIDGET_COLOR, TOGGLE_WIDGET_OPEN, + SET_ROUTE_UPDATE_STATE, } from '../types'; const state = { @@ -19,6 +20,7 @@ const state = { widgetColor: '', widgetStyle: 'standard', darkMode: 'light', + isUpdatingRoute: false, }; export const getters = { @@ -31,6 +33,7 @@ export const getters = { isWidgetStyleFlat: $state => $state.widgetStyle === 'flat', darkMode: $state => $state.darkMode, getShowUnreadMessagesDialog: $state => $state.showUnreadMessagesDialog, + getIsUpdatingRoute: _state => _state.isUpdatingRoute, }; export const actions = { @@ -69,6 +72,13 @@ export const actions = { setBubbleVisibility({ commit }, hideMessageBubble) { commit(SET_BUBBLE_VISIBILITY, hideMessageBubble); }, + setRouteTransitionState: async ({ commit }, status) => { + // Handles the routing state during navigation to different screen + // Called before the navigation starts and after navigation completes + // Handling this state in app/javascript/widget/router.js + // See issue: https://github.com/chatwoot/chatwoot/issues/10736 + commit(SET_ROUTE_UPDATE_STATE, status); + }, }; export const mutations = { @@ -96,6 +106,9 @@ export const mutations = { [SET_COLOR_SCHEME]($state, darkMode) { $state.darkMode = darkMode; }, + [SET_ROUTE_UPDATE_STATE]($state, status) { + $state.isUpdatingRoute = status; + }, }; export default { diff --git a/app/javascript/widget/store/modules/specs/appConfig/actions.spec.js b/app/javascript/widget/store/modules/specs/appConfig/actions.spec.js index 4c66600f2..a99079562 100644 --- a/app/javascript/widget/store/modules/specs/appConfig/actions.spec.js +++ b/app/javascript/widget/store/modules/specs/appConfig/actions.spec.js @@ -31,4 +31,11 @@ describe('#actions', () => { expect(commit.mock.calls).toEqual([['SET_COLOR_SCHEME', 'dark']]); }); }); + + describe('#setRouteTransitionState', () => { + it('creates actions properly', () => { + actions.setRouteTransitionState({ commit }, false); + expect(commit.mock.calls).toEqual([['SET_ROUTE_UPDATE_STATE', false]]); + }); + }); }); diff --git a/app/javascript/widget/store/modules/specs/appConfig/getters.spec.js b/app/javascript/widget/store/modules/specs/appConfig/getters.spec.js index ab0c6e6ca..5d3db77bc 100644 --- a/app/javascript/widget/store/modules/specs/appConfig/getters.spec.js +++ b/app/javascript/widget/store/modules/specs/appConfig/getters.spec.js @@ -19,4 +19,10 @@ describe('#getters', () => { expect(getters.getShowUnreadMessagesDialog(state)).toEqual(true); }); }); + describe('#getIsUpdatingRoute', () => { + it('returns correct value', () => { + const state = { isUpdatingRoute: true }; + expect(getters.getIsUpdatingRoute(state)).toEqual(true); + }); + }); }); diff --git a/app/javascript/widget/store/modules/specs/appConfig/mutations.spec.js b/app/javascript/widget/store/modules/specs/appConfig/mutations.spec.js index e79235e28..e25fea368 100644 --- a/app/javascript/widget/store/modules/specs/appConfig/mutations.spec.js +++ b/app/javascript/widget/store/modules/specs/appConfig/mutations.spec.js @@ -32,4 +32,12 @@ describe('#mutations', () => { expect(state.darkMode).toEqual('dark'); }); }); + + describe('#SET_ROUTE_UPDATE_STATE', () => { + it('sets dark mode properly', () => { + const state = { isUpdatingRoute: false }; + mutations.SET_ROUTE_UPDATE_STATE(state, true); + expect(state.isUpdatingRoute).toEqual(true); + }); + }); }); diff --git a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js index dbf6f692b..39b8afe1a 100644 --- a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js +++ b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js @@ -17,6 +17,7 @@ describe('#actions', () => { messages: [{ id: 1, content: 'This is a test message' }], }, }); + let windowSpy = vi.spyOn(window, 'window', 'get'); windowSpy.mockImplementation(() => ({ WOOT_WIDGET: { diff --git a/app/javascript/widget/store/types.js b/app/javascript/widget/store/types.js index 17398b682..b4c2a968f 100644 --- a/app/javascript/widget/store/types.js +++ b/app/javascript/widget/store/types.js @@ -7,3 +7,4 @@ export const UPDATE_CONVERSATION_ATTRIBUTES = 'UPDATE_CONVERSATION_ATTRIBUTES'; export const TOGGLE_WIDGET_OPEN = 'TOGGLE_WIDGET_OPEN'; export const SET_REFERRER_HOST = 'SET_REFERRER_HOST'; export const SET_BUBBLE_VISIBILITY = 'SET_BUBBLE_VISIBILITY'; +export const SET_ROUTE_UPDATE_STATE = 'SET_ROUTE_UPDATE_STATE'; diff --git a/app/javascript/widget/views/PreChatForm.vue b/app/javascript/widget/views/PreChatForm.vue index d47e03736..8eac55808 100644 --- a/app/javascript/widget/views/PreChatForm.vue +++ b/app/javascript/widget/views/PreChatForm.vue @@ -12,12 +12,20 @@ export default { }, mixins: [configMixin, routerMixin], mounted() { - emitter.on(ON_CONVERSATION_CREATED, () => { - // Redirect to messages page after conversation is created - this.replaceRoute('messages'); - }); + // Register event listener for conversation creation + emitter.on(ON_CONVERSATION_CREATED, this.handleConversationCreated); + }, + beforeUnmount() { + emitter.off(ON_CONVERSATION_CREATED, this.handleConversationCreated); }, methods: { + handleConversationCreated() { + // Redirect to messages page after conversation is created + this.replaceRoute('messages'); + // Only after successful navigation, reset the isUpdatingRoute UIflag in app/javascript/widget/router.js + // See issue: https://github.com/chatwoot/chatwoot/issues/10736 + }, + onSubmit({ fullName, emailAddress, From b429ce0ad588b4f74efa055807a262fd455e118e Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:18:14 +0530 Subject: [PATCH 2/3] fix: Prevent template variables from becoming links (#10725) # Pull Request Template ## Description **Issue** This PR fixes template variables in messages (e.g., {{customer.name}}) that were being incorrectly converted to clickable links by the `MessageFormatter's linkify` functionality. This caused formatting issues and broken links. **Solution** Added a `linkify` parameter to `MessageFormatter` to optionally disable link conversion ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? **Screenshots** **Before** image **After** image ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../Campaigns/CampaignCard/CampaignCard.vue | 2 +- .../specs/useMessageFormatter.spec.js | 12 +++++ .../shared/composables/useMessageFormatter.js | 6 ++- .../shared/helpers/MessageFormatter.js | 52 +++++++++++-------- .../helpers/specs/MessageFormatter.spec.js | 7 +++ 5 files changed, 55 insertions(+), 24 deletions(-) diff --git a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue index 229553253..304e55347 100644 --- a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue +++ b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue @@ -98,7 +98,7 @@ const inboxIcon = computed(() => {
diff --git a/app/javascript/shared/composables/specs/useMessageFormatter.spec.js b/app/javascript/shared/composables/specs/useMessageFormatter.spec.js index e9e3962d7..8021fc50d 100644 --- a/app/javascript/shared/composables/specs/useMessageFormatter.spec.js +++ b/app/javascript/shared/composables/specs/useMessageFormatter.spec.js @@ -30,6 +30,18 @@ describe('useMessageFormatter', () => { ' { + const message = 'Check https://example.com and {{user.id}}'; + const result = messageFormatter.formatMessage( + message, + false, + false, + false + ); + expect(result).not.toContain(' { diff --git a/app/javascript/shared/composables/useMessageFormatter.js b/app/javascript/shared/composables/useMessageFormatter.js index 16b7f25a6..974d113fd 100644 --- a/app/javascript/shared/composables/useMessageFormatter.js +++ b/app/javascript/shared/composables/useMessageFormatter.js @@ -14,11 +14,13 @@ export const useMessageFormatter = () => { * @param {boolean} isAPrivateNote - Whether the message is a private note. * @returns {string} - The formatted message. */ - const formatMessage = (message, isATweet, isAPrivateNote) => { + // TODO: ref:https://github.com/chatwoot/chatwoot/pull/10725#discussion_r1925300874 + const formatMessage = (message, isATweet, isAPrivateNote, linkify) => { const messageFormatter = new MessageFormatter( message, isATweet, - isAPrivateNote + isAPrivateNote, + linkify ); return messageFormatter.formattedMessage; }; diff --git a/app/javascript/shared/helpers/MessageFormatter.js b/app/javascript/shared/helpers/MessageFormatter.js index 06bd8bfae..c87209fd4 100644 --- a/app/javascript/shared/helpers/MessageFormatter.js +++ b/app/javascript/shared/helpers/MessageFormatter.js @@ -1,6 +1,7 @@ import mila from 'markdown-it-link-attributes'; import mentionPlugin from './markdownIt/link'; import MarkdownIt from 'markdown-it'; + const setImageHeight = inlineToken => { const imgSrc = inlineToken.attrGet('src'); if (!imgSrc) return; @@ -30,25 +31,27 @@ const imgResizeManager = md => { }); }; -const md = MarkdownIt({ - html: false, - xhtmlOut: true, - breaks: true, - langPrefix: 'language-', - linkify: true, - typographer: true, - quotes: '\u201c\u201d\u2018\u2019', - maxNesting: 20, -}) - .use(mentionPlugin) - .use(imgResizeManager) - .use(mila, { - attrs: { - class: 'link', - rel: 'noreferrer noopener nofollow', - target: '_blank', - }, - }); +const createMarkdownInstance = (linkify = true) => { + return MarkdownIt({ + html: false, + xhtmlOut: true, + breaks: true, + langPrefix: 'language-', + linkify, + typographer: true, + quotes: '\u201c\u201d\u2018\u2019', + maxNesting: 20, + }) + .use(mentionPlugin) + .use(imgResizeManager) + .use(mila, { + attrs: { + class: 'link', + rel: 'noreferrer noopener nofollow', + target: '_blank', + }, + }); +}; const TWITTER_USERNAME_REGEX = /(^|[^@\w])@(\w{1,15})\b/g; const TWITTER_USERNAME_REPLACEMENT = '$1[@$2](http://twitter.com/$2)'; @@ -56,10 +59,17 @@ const TWITTER_HASH_REGEX = /(^|\s)#(\w+)/g; const TWITTER_HASH_REPLACEMENT = '$1[#$2](https://twitter.com/hashtag/$2)'; class MessageFormatter { - constructor(message, isATweet = false, isAPrivateNote = false) { + constructor( + message, + isATweet = false, + isAPrivateNote = false, + linkify = true + ) { this.message = message || ''; this.isAPrivateNote = isAPrivateNote; this.isATweet = isATweet; + this.linkify = linkify; + this.md = createMarkdownInstance(linkify); } formatMessage() { @@ -74,7 +84,7 @@ class MessageFormatter { TWITTER_HASH_REPLACEMENT ); } - return md.render(updatedMessage); + return this.md.render(updatedMessage); } get formattedMessage() { diff --git a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js index 47760f7a8..a685cb0da 100644 --- a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js +++ b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js @@ -16,6 +16,13 @@ describe('#MessageFormatter', () => { '

Chatwoot is an opensource tool. https://www.chatwoot.com

' ); }); + it('should not convert template variables to links when linkify is disabled', () => { + const message = 'Hey {{customer.name}}, check https://chatwoot.com'; + const formatter = new MessageFormatter(message, false, false, false); + expect(formatter.formattedMessage).toMatch( + '

Hey {{customer.name}}, check https://chatwoot.com

' + ); + }); }); describe('parses heading to strong', () => { From ef7bf66476173588d7dff64c88aba4ac5bf40c81 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 24 Jan 2025 22:51:09 +0530 Subject: [PATCH 3/3] feat: Add frontend changes for Captain limits (#10749) This PR introduces several improvements to the Captain AI dashboard section: - New billing page, with new colors, layout and meters for Captain usage - Updated the base paywall component to use new colors - Updated PageLayout.vue, it's more generic and can be used for other pages as well - Use flags to toggle empty state and loading state - Add prop for `featureFlag` to show the paywall slot based on feature enabled on account - Update `useAccount` to add a `isCloudFeatureEnabled` - **Removed feature flag checks from captain route definitions**, so the captain entry will always be visible on the sidebar - Add banner to Captain pages for the following cases - Responses usage is over 80% - Documents limit is fully exhausted ### Screenshots
Free plan

![CleanShot 2025-01-22 at 18 37 11@2x](https://github.com/user-attachments/assets/17d3ddba-9095-4e81-9b6f-45b5f69e6a3f) ![CleanShot 2025-01-22 at 18 37 04@2x](https://github.com/user-attachments/assets/df9bb0a6-085f-45da-97d4-74cbcc33fc7e)

Paid plan

![CleanShot 2025-01-22 at 18 36 45@2x](https://github.com/user-attachments/assets/a7ccf9d4-143b-49e4-8149-83c7a7985023) ![CleanShot 2025-01-22 at 20 23 57@2x](https://github.com/user-attachments/assets/c6ce35ba-e537-486d-85c8-4cc2d4e76438)

--------- Co-authored-by: Sojan Jose Co-authored-by: Pranav Co-authored-by: Muhsin Keloth --- .../components-next/EmptyStateLayout.vue | 12 +- .../components-next/banner/Banner.vue | 73 +++++ .../components-next/captain/PageLayout.vue | 60 +++- .../captain/assistant/AssistantCard.vue | 51 ++-- .../captain/assistant/DocumentCard.vue | 37 ++- .../captain/assistant/InboxCard.vue | 6 +- .../captain/assistant/ResponseCard.vue | 6 +- .../captain/pageComponents/Paywall.vue | 41 +++ .../pageComponents/document/LimitBanner.vue | 40 +++ .../emptyStates/AssistantPageEmptyState.vue | 1 + .../emptyStates/DocumentPageEmptyState.vue | 1 + .../emptyStates/InboxPageEmptyState.vue | 1 + .../emptyStates/ResponsePageEmptyState.vue | 1 + .../pageComponents/response/LimitBanner.vue | 42 +++ .../components-next/sidebar/Sidebar.vue | 5 + .../components-next/sidebar/SidebarGroup.vue | 2 + .../sidebar/SidebarGroupLeaf.vue | 12 +- .../sidebar/SidebarSubGroup.vue | 7 +- .../components-next/sidebar/provider.js | 4 + .../dashboard/composables/useAccount.js | 12 +- .../dashboard/composables/useCaptain.js | 46 +++ .../dashboard/helper/featureHelper.js | 1 + .../i18n/locale/en/integrations.json | 28 +- .../dashboard/i18n/locale/en/settings.json | 16 +- .../dashboard/captain/assistants/Index.vue | 50 +-- .../captain/assistants/inboxes/Index.vue | 46 ++- .../dashboard/captain/captain.routes.js | 6 +- .../dashboard/captain/documents/Index.vue | 65 ++-- .../dashboard/captain/responses/Index.vue | 106 ++++--- .../dashboard/settings/SettingsLayout.vue | 2 +- .../dashboard/settings/billing/Index.vue | 288 +++++++++++------- .../settings/billing/billing.routes.js | 4 +- .../billing/components/BillingCard.vue | 25 ++ .../billing/components/BillingHeader.vue | 26 ++ .../billing/components/BillingItem.vue | 40 --- .../billing/components/BillingMeter.vue | 45 +++ .../billing/components/DetailItem.vue | 23 ++ .../settings/components/BasePaywallModal.vue | 41 +-- .../enterprise/api/v1/accounts_controller.rb | 3 +- .../billing/handle_stripe_event_service.rb | 2 +- .../api/v1/accounts_controller_spec.rb | 12 + 41 files changed, 920 insertions(+), 369 deletions(-) create mode 100644 app/javascript/dashboard/components-next/banner/Banner.vue create mode 100644 app/javascript/dashboard/components-next/captain/pageComponents/Paywall.vue create mode 100644 app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue create mode 100644 app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue create mode 100644 app/javascript/dashboard/composables/useCaptain.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingCard.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingHeader.vue delete mode 100644 app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingItem.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingMeter.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/billing/components/DetailItem.vue diff --git a/app/javascript/dashboard/components-next/EmptyStateLayout.vue b/app/javascript/dashboard/components-next/EmptyStateLayout.vue index 04c478ba6..686d4b8a4 100644 --- a/app/javascript/dashboard/components-next/EmptyStateLayout.vue +++ b/app/javascript/dashboard/components-next/EmptyStateLayout.vue @@ -1,4 +1,6 @@ @@ -16,7 +22,7 @@ defineProps({ class="relative flex flex-col items-center justify-center w-full h-full overflow-hidden" >
- + + +
diff --git a/app/javascript/dashboard/components-next/banner/Banner.vue b/app/javascript/dashboard/components-next/banner/Banner.vue new file mode 100644 index 000000000..cc5af3272 --- /dev/null +++ b/app/javascript/dashboard/components-next/banner/Banner.vue @@ -0,0 +1,73 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/PageLayout.vue b/app/javascript/dashboard/components-next/captain/PageLayout.vue index b0d10b889..1ce03cec5 100644 --- a/app/javascript/dashboard/components-next/captain/PageLayout.vue +++ b/app/javascript/dashboard/components-next/captain/PageLayout.vue @@ -1,8 +1,12 @@ + + diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue b/app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue new file mode 100644 index 000000000..d4f512f39 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/document/LimitBanner.vue @@ -0,0 +1,40 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue index 0ae292eb4..a12fcce74 100644 --- a/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue @@ -15,6 +15,7 @@ const onClick = () => {