diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue b/app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue
new file mode 100644
index 000000000..0b5462f0b
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/response/LimitBanner.vue
@@ -0,0 +1,42 @@
+
+
+
+
+ {{ $t('CAPTAIN.BANNER.RESPONSES') }}
+
+
diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue
index 449ba9ada..3f2f4ed6f 100644
--- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue
+++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue
@@ -171,20 +171,24 @@ const menuItems = computed(() => {
name: 'Captain',
icon: 'i-woot-captain',
label: t('SIDEBAR.CAPTAIN'),
+ showOnlyOnCloud: true,
children: [
{
name: 'Assistants',
label: t('SIDEBAR.CAPTAIN_ASSISTANTS'),
+ showOnlyOnCloud: true,
to: accountScopedRoute('captain_assistants_index'),
},
{
name: 'Documents',
label: t('SIDEBAR.CAPTAIN_DOCUMENTS'),
+ showOnlyOnCloud: true,
to: accountScopedRoute('captain_documents_index'),
},
{
name: 'Responses',
label: t('SIDEBAR.CAPTAIN_RESPONSES'),
+ showOnlyOnCloud: true,
to: accountScopedRoute('captain_responses_index'),
},
],
@@ -455,6 +459,7 @@ const menuItems = computed(() => {
name: 'Settings Billing',
label: t('SIDEBAR.BILLING'),
icon: 'i-lucide-credit-card',
+ showOnlyOnCloud: true,
to: accountScopedRoute('billing_settings_index'),
},
],
diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue b/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue
index 36caf61fe..305171d10 100644
--- a/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue
+++ b/app/javascript/dashboard/components-next/sidebar/SidebarGroup.vue
@@ -23,6 +23,7 @@ const {
resolvePath,
resolvePermissions,
resolveFeatureFlag,
+ isOnChatwootCloud,
isAllowed,
} = useSidebarContext();
@@ -41,6 +42,7 @@ const hasChildren = computed(
const accessibleItems = computed(() => {
if (!hasChildren.value) return [];
return props.children.filter(child => {
+ if (child.showOnlyOnCloud && !isOnChatwootCloud.value) return false;
// If a item has no link, it means it's just a subgroup header
// So we don't need to check for permissions here, because there's nothing to
// access here anyway
diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarGroupLeaf.vue b/app/javascript/dashboard/components-next/sidebar/SidebarGroupLeaf.vue
index 13bdb040d..a791953b6 100644
--- a/app/javascript/dashboard/components-next/sidebar/SidebarGroupLeaf.vue
+++ b/app/javascript/dashboard/components-next/sidebar/SidebarGroupLeaf.vue
@@ -9,18 +9,28 @@ const props = defineProps({
to: { type: [String, Object], required: true },
icon: { type: [String, Object], default: null },
active: { type: Boolean, default: false },
+ showOnlyOnCloud: { type: Boolean, default: false },
component: { type: Function, default: null },
});
-const { resolvePermissions, resolveFeatureFlag } = useSidebarContext();
+const { resolvePermissions, resolveFeatureFlag, isOnChatwootCloud } =
+ useSidebarContext();
+
+const allowedToShow = computed(() => {
+ if (props.showOnlyOnCloud && !isOnChatwootCloud.value) return false;
+
+ return true;
+});
const shouldRenderComponent = computed(() => {
return typeof props.component === 'function' || isVNode(props.component);
});
+
- props.children.filter(child => isAllowed(child.to))
+ props.children.filter(child => {
+ if (child.showOnlyOnCloud && !isOnChatwootCloud.value) return false;
+ return child.to && isAllowed(child.to);
+ })
);
const hasAccessibleItems = computed(() => {
diff --git a/app/javascript/dashboard/components-next/sidebar/provider.js b/app/javascript/dashboard/components-next/sidebar/provider.js
index d6571dcd9..4d9973213 100644
--- a/app/javascript/dashboard/components-next/sidebar/provider.js
+++ b/app/javascript/dashboard/components-next/sidebar/provider.js
@@ -1,4 +1,5 @@
import { inject, provide } from 'vue';
+import { useMapGetter } from 'dashboard/composables/store';
import { usePolicy } from 'dashboard/composables/usePolicy';
import { useRouter } from 'vue-router';
@@ -11,6 +12,8 @@ export function useSidebarContext() {
}
const router = useRouter();
+ const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
+
const { checkFeatureAllowed, checkPermissions } = usePolicy();
const resolvePath = to => {
@@ -41,6 +44,7 @@ export function useSidebarContext() {
resolvePermissions,
resolveFeatureFlag,
isAllowed,
+ isOnChatwootCloud,
};
}
diff --git a/app/javascript/dashboard/composables/useAccount.js b/app/javascript/dashboard/composables/useAccount.js
index c8c245adb..8ace2a5e1 100644
--- a/app/javascript/dashboard/composables/useAccount.js
+++ b/app/javascript/dashboard/composables/useAccount.js
@@ -13,10 +13,14 @@ export function useAccount() {
*/
const route = useRoute();
const getAccountFn = useMapGetter('accounts/getAccount');
+ const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
+ const isFeatureEnabledonAccount = useMapGetter(
+ 'accounts/isFeatureEnabledonAccount'
+ );
+
const accountId = computed(() => {
return Number(route.params.accountId);
});
-
const currentAccount = computed(() => getAccountFn.value(accountId.value));
/**
@@ -28,6 +32,10 @@ export function useAccount() {
return `/app/accounts/${accountId.value}/${url}`;
};
+ const isCloudFeatureEnabled = feature => {
+ return isFeatureEnabledonAccount.value(currentAccount.value.id, feature);
+ };
+
const accountScopedRoute = (name, params, query) => {
return {
name,
@@ -42,5 +50,7 @@ export function useAccount() {
currentAccount,
accountScopedUrl,
accountScopedRoute,
+ isCloudFeatureEnabled,
+ isOnChatwootCloud,
};
}
diff --git a/app/javascript/dashboard/composables/useCaptain.js b/app/javascript/dashboard/composables/useCaptain.js
new file mode 100644
index 000000000..d28560944
--- /dev/null
+++ b/app/javascript/dashboard/composables/useCaptain.js
@@ -0,0 +1,46 @@
+import { computed } from 'vue';
+import { useStore } from 'dashboard/composables/store.js';
+import { useAccount } from 'dashboard/composables/useAccount';
+import { useCamelCase } from 'dashboard/composables/useTransformKeys';
+import { FEATURE_FLAGS } from 'dashboard/featureFlags';
+
+export function useCaptain() {
+ const store = useStore();
+ const { isCloudFeatureEnabled, currentAccount } = useAccount();
+
+ const captainEnabled = computed(() => {
+ return isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN);
+ });
+
+ const captainLimits = computed(() => {
+ return currentAccount.value?.limits?.captain;
+ });
+
+ const documentLimits = computed(() => {
+ if (captainLimits.value?.documents) {
+ return useCamelCase(captainLimits.value.documents);
+ }
+
+ return null;
+ });
+
+ const responseLimits = computed(() => {
+ if (captainLimits.value?.responses) {
+ return useCamelCase(captainLimits.value.responses);
+ }
+
+ return null;
+ });
+
+ const fetchLimits = () => {
+ store.dispatch('accounts/limits');
+ };
+
+ return {
+ captainEnabled,
+ captainLimits,
+ documentLimits,
+ responseLimits,
+ fetchLimits,
+ };
+}
diff --git a/app/javascript/dashboard/helper/featureHelper.js b/app/javascript/dashboard/helper/featureHelper.js
index 529c0a44e..ee61b0656 100644
--- a/app/javascript/dashboard/helper/featureHelper.js
+++ b/app/javascript/dashboard/helper/featureHelper.js
@@ -18,6 +18,7 @@ const FEATURE_HELP_URLS = {
sla: 'https://chwt.app/hc/sla',
team_management: 'https://chwt.app/hc/teams',
webhook: 'https://chwt.app/hc/webhooks',
+ billing: 'https://chwt.app/pricing',
};
export function getHelpUrlForFeature(featureName) {
diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json
index dd76f0cdb..56f6e9776 100644
--- a/app/javascript/dashboard/i18n/locale/en/integrations.json
+++ b/app/javascript/dashboard/i18n/locale/en/integrations.json
@@ -309,6 +309,22 @@
"USE": "Use this",
"RESET": "Reset"
},
+ "PAYWALL": {
+ "TITLE": "Upgrade to use Captain AI",
+ "AVAILABLE_ON": "Captain is not available on the free plan.",
+ "UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
+ "UPGRADE_NOW": "Upgrade now",
+ "CANCEL_ANYTIME": "You can change or cancel your plan anytime"
+ },
+ "ENTERPRISE_PAYWALL": {
+ "AVAILABLE_ON": "Captain AI feature is only available in a paid plan.",
+ "UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
+ "ASK_ADMIN": "Please reach out to your administrator for the upgrade."
+ },
+ "BANNER": {
+ "RESPONSES": "You've used over 80% of your response limit. To continue using Captain AI, please upgrade.",
+ "DOCUMENTS": "Document limit reached. Upgrade to continue using Captain AI."
+ },
"FORM": {
"CANCEL": "Cancel",
"CREATE": "Create",
@@ -364,7 +380,7 @@
},
"EMPTY_STATE": {
"TITLE": "No assistants available",
- "SUBTITLE": "Create an assistant to provide quick and accurate responses to your users. It can learn from your help articles and past conversations. Click the button below to get started."
+ "SUBTITLE": "Create an assistant to provide quick and accurate responses to your users. It can learn from your help articles and past conversations."
}
},
"DOCUMENTS": {
@@ -406,13 +422,13 @@
},
"EMPTY_STATE": {
"TITLE": "No documents available",
- "SUBTITLE": "Documents are used by your assistant to generate FAQs. You can import documents to provide context for your assistant. Click the button below to get started."
+ "SUBTITLE": "Documents are used by your assistant to generate FAQs. You can import documents to provide context for your assistant."
}
},
"RESPONSES": {
"HEADER": "FAQs",
"ADD_NEW": "Create new FAQ",
- "DOCUMENTABLE" : {
+ "DOCUMENTABLE": {
"CONVERSATION": "Conversation #{id}"
},
"DELETE": {
@@ -422,7 +438,7 @@
"SUCCESS_MESSAGE": "FAQ deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the FAQ, please try again."
},
- "FILTER" :{
+ "FILTER": {
"ASSISTANT": "Assistant: {selected}",
"STATUS": "Status: {selected}",
"ALL_ASSISTANTS": "All"
@@ -470,7 +486,7 @@
},
"EMPTY_STATE": {
"TITLE": "No FAQs Found",
- "SUBTITLE": "FAQs help your assistant provide quick and accurate answers to questions from your customers. They can be generated automatically from your content or can be added manually. Click the button below to create your first FAQ."
+ "SUBTITLE": "FAQs help your assistant provide quick and accurate answers to questions from your customers. They can be generated automatically from your content or can be added manually."
}
},
"INBOXES": {
@@ -501,7 +517,7 @@
},
"EMPTY_STATE": {
"TITLE": "No Connected Inboxes",
- "SUBTITLE": "Connecting an inbox allows the assistant to handle initial questions from your customers before transferring them to you. Click the button below to set it up now."
+ "SUBTITLE": "Connecting an inbox allows the assistant to handle initial questions from your customers before transferring them to you."
}
}
}
diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index e532f8e8f..9f5ad31c1 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -265,7 +265,7 @@
"CAPTAIN": "Captain",
"CAPTAIN_ASSISTANTS": "Assistants",
"CAPTAIN_DOCUMENTS": "Documents",
- "CAPTAIN_RESPONSES" : "FAQs",
+ "CAPTAIN_RESPONSES": "FAQs",
"HOME": "Home",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
@@ -327,15 +327,27 @@
},
"BILLING_SETTINGS": {
"TITLE": "Billing",
+ "DESCRIPTION": "Manage your subscription here, upgrade your plan and get more for your team.",
"CURRENT_PLAN": {
"TITLE": "Current Plan",
- "PLAN_NOTE": "You are currently subscribed to the **{plan}** plan with **{quantity}** licenses"
+ "PLAN_NOTE": "You are currently subscribed to the **{plan}** plan with **{quantity}** licenses",
+ "SEAT_COUNT": "Number of seats",
+ "RENEWS_ON": "Renews on"
},
+ "VIEW_PRICING": "View Pricing",
"MANAGE_SUBSCRIPTION": {
"TITLE": "Manage your subscription",
"DESCRIPTION": "View your previous invoices, edit your billing details, or cancel your subscription.",
"BUTTON_TXT": "Go to the billing portal"
},
+ "CAPTAIN": {
+ "TITLE": "Captain",
+ "DESCRIPTION": "Manage usage and credits for Captain AI.",
+ "BUTTON_TXT": "Buy more credits",
+ "DOCUMENTS": "Documents",
+ "RESPONSES": "Responses",
+ "UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
+ },
"CHAT_WITH_US": {
"TITLE": "Need help?",
"DESCRIPTION": "Do you face any issues in billing? We are here to help.",
diff --git a/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue b/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue
index 6bba32eb2..3ea78e74a 100644
--- a/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/captain/assistants/Index.vue
@@ -1,13 +1,15 @@
-
-
-
-
+
@@ -89,23 +86,22 @@ onMounted(() =>
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
import { computed, onMounted, ref, nextTick } from 'vue';
import { useMapGetter, useStore } from 'dashboard/composables/store';
+import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
import DocumentCard from 'dashboard/components-next/captain/assistant/DocumentCard.vue';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
-import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
+import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
import RelatedResponses from 'dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue';
import CreateDocumentDialog from 'dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue';
import AssistantSelector from 'dashboard/components-next/captain/pageComponents/AssistantSelector.vue';
import DocumentPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue';
+import LimitBanner from 'dashboard/components-next/captain/pageComponents/document/LimitBanner.vue';
const store = useStore();
@@ -103,38 +105,49 @@ onMounted(() => {
-
-
-
-
-
-
-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
-
-
-
+
{{ noRecordsMessage }}
diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue
index cd0b15f4e..8f829ed0c 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue
@@ -1,123 +1,181 @@
-
-
-
-
-
{{ $t('BILLING_SETTINGS.NO_BILLING_USER') }}
-
-
-
-
{{ $t('BILLING_SETTINGS.CURRENT_PLAN.TITLE') }}
-
-
-
+
+
-
-
-
+
+
+
+
+
+
+ {{ $t('BILLING_SETTINGS.MANAGE_SUBSCRIPTION.BUTTON_TXT') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('BILLING_SETTINGS.CAPTAIN.BUTTON_TXT') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('CAPTAIN.PAYWALL.UPGRADE_NOW') }}
+
+
+
+
+
+
+ {{ $t('BILLING_SETTINGS.CHAT_WITH_US.BUTTON_TXT') }}
+
+
+
+
+
-
-
diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/billing.routes.js b/app/javascript/dashboard/routes/dashboard/settings/billing/billing.routes.js
index 9fc09a00a..26b441c75 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/billing/billing.routes.js
+++ b/app/javascript/dashboard/routes/dashboard/settings/billing/billing.routes.js
@@ -1,5 +1,5 @@
import { frontendURL } from '../../../../helper/URLHelper';
-import SettingsContent from '../Wrapper.vue';
+import SettingsWrapper from '../SettingsWrapper.vue';
import Index from './Index.vue';
export default {
@@ -9,7 +9,7 @@ export default {
meta: {
permissions: ['administrator'],
},
- component: SettingsContent,
+ component: SettingsWrapper,
props: {
headerTitle: 'BILLING_SETTINGS.TITLE',
icon: 'credit-card-person',
diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingCard.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingCard.vue
new file mode 100644
index 000000000..d9179b76c
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingCard.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingHeader.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingHeader.vue
new file mode 100644
index 000000000..ef0b8d9cd
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingHeader.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ {{ title }}
+
+
+ {{ description }}
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingItem.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingItem.vue
deleted file mode 100644
index a63744655..000000000
--- a/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingItem.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
{{ title }}
-
- {{ description }}
-
-
-
-
- {{ buttonLabel }}
-
-
-
-
diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingMeter.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingMeter.vue
new file mode 100644
index 000000000..fb057b44a
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/billing/components/BillingMeter.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+ {{ title }}
+
+
{{ consumed }} / {{ totalCount }}
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/components/DetailItem.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/components/DetailItem.vue
new file mode 100644
index 000000000..1c4f2c458
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/billing/components/DetailItem.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+ {{ label }}
+
+
+ {{ value }}
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/components/BasePaywallModal.vue b/app/javascript/dashboard/routes/dashboard/settings/components/BasePaywallModal.vue
index 36a6b3b8e..e2b552d52 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/components/BasePaywallModal.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/components/BasePaywallModal.vue
@@ -1,4 +1,7 @@