Compare commits

..
Author SHA1 Message Date
Tanmay Sharma d9348883ed add default payment method for topup and change of subscription 2025-11-24 19:09:00 +05:30
Tanmay Deep Sharma aeb708a053 Merge remote-tracking branch 'origin/feature/stripe_v2' into feature/stripe_v2_fe 2025-11-07 03:39:08 +05:30
Tanmay Deep Sharma ce8f40debb remove stripe v2 client 2025-11-07 03:37:14 +05:30
Tanmay Deep Sharma 5ca8aec7c3 default plan is hacker plan for customer creation 2025-11-07 02:33:49 +05:30
Tanmay Deep Sharma 4b8b33c4fd stripe_billing_version should be integer two 2025-11-07 02:29:37 +05:30
Tanmay Deep Sharma e3df65ef09 UI for billing portal should setup up billing if customer_id is not present 2025-11-07 02:29:15 +05:30
Tanmay Deep Sharma 063e9654bc stripe_billing_version should be integer two 2025-11-07 02:23:58 +05:30
Tanmay Deep Sharma b7035de400 fix webhook handler spec 2025-11-07 00:05:05 +05:30
Tanmay Deep Sharma 156220e0e2 fix customer creation flow 2025-11-07 00:02:48 +05:30
Tanmay Deep Sharma cec1a64d6f fix customer creation flow 2025-11-07 00:02:11 +05:30
Tanmay Deep Sharma 8b0caa6909 update pricing plan credits and topup 2025-11-06 19:40:08 +05:30
Tanmay Deep Sharma 051eeae8ec update pricing plan credits and topup 2025-11-06 19:39:15 +05:30
Tanmay Deep Sharma d51cea9d6b Merge remote-tracking branch 'origin/feature/stripe_v2' into feature/stripe_v2_fe 2025-11-06 19:18:12 +05:30
Tanmay Deep Sharma 56160bfc63 fix rspec for handle stripe event service rspec 2025-11-06 18:56:30 +05:30
Tanmay Deep Sharma abca836f0c revert gitignore local change 2025-11-06 18:37:42 +05:30
Tanmay Deep Sharma 64640092aa Merge remote-tracking branch 'origin/feature/stripe_v2' into feature/stripe_v2 2025-11-06 18:10:37 +05:30
Tanmay Deep Sharma d478f6e42e fxi the subscription provision service spec 2025-11-06 18:10:19 +05:30
Sivin VargheseandGitHub 2dcd071dec Merge branch 'feature/stripe_v2' into feature/stripe_v2_fe 2025-11-06 16:36:26 +05:30
Sivin VargheseandGitHub ea34071553 Merge branch 'develop' into feature/stripe_v2 2025-11-06 16:36:14 +05:30
Sivin VargheseandGitHub ce4d52926c Merge branch 'feature/stripe_v2' into feature/stripe_v2_fe 2025-11-06 16:34:03 +05:30
Tanmay Deep Sharma fb86b1354a make stripe billing version consistent 2025-11-06 15:31:06 +05:30
Tanmay Deep Sharma ab28802d5a update gemfile stripe version to alpha 2025-11-06 15:26:53 +05:30
Tanmay Deep Sharma 797714f5aa remove the billing config to render v2 2025-11-06 15:26:26 +05:30
Tanmay Deep Sharma b2cc5d113e update gemfile stripe version to alpha 2025-11-06 15:25:57 +05:30
Tanmay Deep Sharma 4eca7382f8 Merge remote-tracking branch 'origin/feature/stripe_v2' into feature/stripe_v2_fe 2025-11-06 14:10:44 +05:30
Tanmay Deep Sharma 0fe60d137e Merge remote-tracking branch 'origin/feature/stripe_v2_fe' into feature/stripe_v2_fe 2025-11-06 14:07:13 +05:30
Tanmay Deep Sharma f6ec3f3a72 use stripe gem beta version 2025-11-06 14:06:48 +05:30
Tanmay Deep Sharma d51148951f use stripe gem beta version 2025-11-06 13:38:47 +05:30
Tanmay Deep Sharma 79bb3d0ef3 update spec for handle stripe event 2025-11-06 03:38:09 +05:30
Tanmay Deep Sharma 7418882a4f make codebase a bit more modular 2025-11-06 03:32:43 +05:30
Tanmay Deep Sharma d38bd5139e set strip billing version in custom attributes 2025-11-06 02:07:44 +05:30
Tanmay Deep Sharma fcc6973245 remove pricing plan creation 2025-11-06 02:02:35 +05:30
Tanmay Deep Sharma 2590c57893 remove un-necessary serialisation 2025-11-05 23:52:22 +05:30
Tanmay Deep Sharma f225e2fcd8 add credit sync job 2025-11-05 23:32:07 +05:30
Tanmay Deep Sharma b626e0cbf1 use account limits to store the captain limits 2025-11-05 16:41:01 +05:30
Tanmay Deep Sharma eb4f93b821 use limits api to render usage 2025-11-05 16:40:47 +05:30
Tanmay Deep Sharma 4a0c61ddb7 use account limits to store the captain limits 2025-11-05 16:38:29 +05:30
Sivin VargheseandGitHub dc4dfe60e4 Merge branch 'feature/stripe_v2' into feature/stripe_v2_fe 2025-11-05 15:34:45 +05:30
Tanmay Deep Sharma d3b4c7225a Merge remote-tracking branch 'origin/feature/stripe_v2' into feature/stripe_v2_fe 2025-11-05 13:17:18 +05:30
Tanmay Deep Sharma 35f42e71f6 fix cursor review comments 2025-11-05 13:16:36 +05:30
Tanmay Deep Sharma eb04882e5a remove credit transactions table and use custome attributes instead 2025-11-05 12:37:23 +05:30
Shivam Mishra 3a4aa5f837 faet: update design for upgrade modal 2025-11-04 15:18:26 +05:30
Shivam Mishra bfb2d2b2bb fix: topup dialog discount 2025-11-04 14:46:39 +05:30
Shivam Mishra aa0f36245f fix: credit topup 2025-11-04 14:40:37 +05:30
Shivam Mishra 8f83d03354 feat: update credit balance UI 2025-11-04 14:15:32 +05:30
Shivam Mishra 643f7415c1 feat: cleanup plan summary 2025-11-04 13:43:56 +05:30
iamsivin ebee38d4a5 chore: Update num of seats design 2025-11-04 13:30:13 +05:30
Sivin VargheseandGitHub 16ac2dba17 Merge branch 'feature/stripe_v2' into feature/stripe_v2_fe 2025-11-04 11:28:10 +05:30
Tanmay Deep Sharma e511527f32 Merge remote-tracking branch 'origin/develop' into feature/stripe_v2 2025-11-04 11:25:28 +05:30
iamsivin bd19d596b3 chore: Minor fix 2025-11-03 19:37:15 +05:30
iamsivin 6a0878f893 chore: Minor fix 2025-11-03 19:16:09 +05:30
iamsivin cdececcc67 chore: Clean up 2025-11-03 18:27:08 +05:30
iamsivin ffdcee7d86 chore: Improve transition 2025-11-03 17:32:01 +05:30
iamsivin 05c4ee099b chore: Fix flag 2025-11-03 16:26:44 +05:30
iamsivin 643316e19a chore: Update credit sections 2025-11-03 11:59:12 +05:30
iamsivin a0cc8e2f3f chore: Update the Pricing plan section 2025-10-31 18:03:45 +05:30
iamsivin fb0f5cd696 chore: Update credit grants 2025-10-31 15:46:52 +05:30
iamsivin 6211406681 chore: Minor fix 2025-10-31 14:18:54 +05:30
iamsivin 76ff8bdd35 chore: Update cancel sub modal 2025-10-31 14:17:25 +05:30
Sivin VargheseandGitHub 10cd5da09d Merge branch 'feature/stripe_v2' into feature/stripe_v2_fe 2025-10-30 20:54:27 +05:30
Tanmay Deep Sharma ce5a859a4f create and pay an invoice on subscription change 2025-10-30 15:52:30 +05:30
Tanmay Deep Sharma 32e0915a20 create and pay an invoice on subscription change 2025-10-30 15:51:51 +05:30
Tanmay Deep Sharma 94985bbc77 remove update subscription flow overall 2025-10-29 21:37:06 +05:30
Tanmay Deep Sharma 65559b47fd remove update subscription flow overall from UI 2025-10-29 21:36:45 +05:30
Tanmay Deep Sharma 40608f4494 remove update subscription flow overall 2025-10-29 21:35:08 +05:30
Tanmay Deep Sharma f31d9a3e76 store next billing dates 2025-10-29 20:30:16 +05:30
Tanmay Deep Sharma eb94eac9a2 show next billing details 2025-10-29 20:29:47 +05:30
Tanmay Deep Sharma 52f673e56c store next billing dates 2025-10-29 20:29:09 +05:30
Tanmay Deep Sharma 4aa567ee3d Merge remote-tracking branch 'origin/feature/stripe_v2' into feature/stripe_v2_fe 2025-10-29 18:43:19 +05:30
Tanmay Sharma 79082bea58 return failure in case event is blank 2025-10-29 17:48:09 +05:30
Tanmay Sharma b9f68fc839 do not deduct or refund credits for audio transactions 2025-10-29 17:46:39 +05:30
Tanmay Sharma e10ef03490 fix rspec for topup 2025-10-29 17:28:03 +05:30
Tanmay Deep Sharma 6f9f2c32c9 handle cancellation properly 2025-10-29 15:50:12 +05:30
Tanmay Deep Sharma 24606ab5ef handle cancellation properly 2025-10-29 15:49:41 +05:30
Tanmay Deep Sharma dd54cf5af3 Merge remote-tracking branch 'origin/feature/stripe_v2' into feature/stripe_v2_fe 2025-10-29 03:59:13 +05:30
Tanmay Deep Sharma b91f8012cd add pricing plan api fix 2025-10-29 03:58:44 +05:30
Tanmay Deep Sharma f999112671 fix rspec 2025-10-29 03:24:12 +05:30
Tanmay Deep Sharma be37c5b80c sanity changes 2025-10-29 03:06:34 +05:30
Tanmay Deep Sharma abeb938bfd remove topup list if on hacker plan 2025-10-29 03:05:49 +05:30
Tanmay Deep Sharma 81c1ab73e6 sanity changes 2025-10-29 03:03:10 +05:30
Tanmay Deep Sharma 1689d5cf5c add fe changes 2025-10-28 22:59:57 +05:30
Tanmay Deep Sharma 3970149b06 remove resume feature 2025-10-28 22:45:57 +05:30
Tanmay Deep Sharma 9c33ce50b3 Merge remote-tracking branch 'origin/develop' into feature/stripe_v2 2025-10-28 22:25:19 +05:30
Tanmay Deep Sharma 36a2412f42 support changing pricing plans 2025-10-28 22:22:48 +05:30
Tanmay Sharma 3ba4340042 remove update subscription flow 2025-10-27 14:16:39 +05:30
Tanmay Sharma 411048715d self review backend changes 2025-10-27 02:17:43 +05:30
Tanmay Sharma 9a7a2486e4 Merge remote-tracking branch 'origin/feature/stripe_v2' into feature/stripe_v2 2025-10-27 00:43:44 +05:30
Tanmay Sharma 39af360a18 implement checkout endpoint for subscription in a better way 2025-10-27 00:28:52 +05:30
Tanmay Sharma ac1743514b use env variable for stripe v2 2025-10-16 16:37:52 +05:30
Sivin VargheseandGitHub c1df37b41d Merge branch 'develop' into feature/stripe_v2 2025-10-14 19:23:56 +05:30
Tanmay Sharma 7c3b07c507 code cleanup 2025-10-13 21:06:05 +05:30
Tanmay Sharma 62203da528 fix up extra env 2025-10-13 13:29:00 +05:30
Tanmay Sharma 029294e16b Merge remote-tracking branch 'origin/feature/stripe_v2' into feature/stripe_v2 2025-10-12 17:26:53 +05:30
Tanmay Sharma 2c095bed86 fix spec rubcop 2025-10-12 17:26:07 +05:30
Tanmay Deep SharmaandGitHub 7c6508dd04 Merge branch 'develop' into feature/stripe_v2 2025-10-12 15:47:27 +04:00
Tanmay Sharma cef23891af cleanup code, remove unnecessary 2025-10-12 17:02:16 +05:30
Tanmay Sharma 19a851c01d cleanup code, remove unnecessary 2025-10-11 11:56:16 +04:00
Tanmay Sharma a22e11c810 cleanup code 2025-10-11 10:25:58 +04:00
Tanmay Sharma efe6e64dcd cleanup code 2025-10-11 10:22:46 +04:00
Tanmay Deep Sharma d6f5c627eb fix rspec and customer subscription process 2025-10-10 13:03:27 +02:00
Tanmay Sharma 51b4a8f9fc cleanup 2025-10-09 17:38:27 +02:00
Tanmay Sharma 1268b2cf55 fix schema file 2025-10-09 16:24:32 +02:00
Tanmay Sharma 125ae5523a fix stripe v2 calls 2025-10-09 09:52:56 +02:00
Tanmay Sharma 59f60936a7 fix migration files 2025-10-09 08:32:13 +02:00
Tanmay Deep Sharma 216a2c0e73 do not disable rubocop 2025-10-09 01:04:47 +02:00
Tanmay Deep Sharma 708296a746 code clean up 2025-10-09 00:37:55 +02:00
Tanmay Deep Sharma b9168664f7 stripe v2 fix meter usage 2025-10-08 23:29:31 +02:00
Tanmay Deep Sharma 01cd7b878d stripe v2 init 2025-10-08 10:38:38 +02:00
Tanmay Deep Sharma acb2816826 stripe v2 init 2025-10-08 10:11:23 +02:00
159 changed files with 4683 additions and 2058 deletions
+1 -4
View File
@@ -8,10 +8,7 @@ module BillingHelper
# Return false if not plans are configured, so that no checks are enforced
return false if default_plan.blank?
# Handle both string and hash formats for default_plan
default_plan_name = default_plan.is_a?(Hash) ? default_plan['name'] : default_plan
account.custom_attributes['plan_name'].nil? || account.custom_attributes['plan_name'] == default_plan_name
account.custom_attributes['plan_name'].nil? || account.custom_attributes['plan_name'] == default_plan['name']
end
def conversations_this_month(account)
+13
View File
@@ -1,5 +1,6 @@
<script>
import { mapGetters } from 'vuex';
import AddAccountModal from './components/app/AddAccountModal.vue';
import LoadingState from './components/widgets/LoadingState.vue';
import NetworkNotification from './components/NetworkNotification.vue';
import UpdateBanner from './components/app/UpdateBanner.vue';
@@ -24,6 +25,7 @@ export default {
name: 'App',
components: {
AddAccountModal,
LoadingState,
NetworkNotification,
UpdateBanner,
@@ -49,6 +51,7 @@ export default {
},
data() {
return {
showAddAccountModal: false,
latestChatwootVersion: null,
reconnectService: null,
};
@@ -61,12 +64,21 @@ export default {
authUIFlags: 'getAuthUIFlags',
accountUIFlags: 'accounts/getUIFlags',
}),
hasAccounts() {
const { accounts = [] } = this.currentUser || {};
return accounts.length > 0;
},
hideOnOnboardingView() {
return !isOnOnboardingView(this.$route);
},
},
watch: {
currentUser() {
if (!this.hasAccounts) {
this.showAddAccountModal = true;
}
},
currentAccountId: {
immediate: true,
handler() {
@@ -144,6 +156,7 @@ export default {
<component :is="Component" />
</transition>
</router-view>
<AddAccountModal :show="showAddAccountModal" :has-accounts="hasAccounts" />
<WootSnackbarBox />
<NetworkNotification />
</div>
@@ -23,6 +23,35 @@ class EnterpriseAccountAPI extends ApiClient {
action_type: action,
});
}
// V2 Billing APIs
creditGrants() {
return axios.get(`${this.url}credit_grants`);
}
v2PricingPlans() {
return axios.get(`${this.url}v2_pricing_plans`);
}
v2TopupOptions() {
return axios.get(`${this.url}v2_topup_options`);
}
v2Topup(data) {
return axios.post(`${this.url}v2_topup`, data);
}
v2Subscribe(data) {
return axios.post(`${this.url}v2_subscribe`, data);
}
cancelSubscription(data = {}) {
return axios.post(`${this.url}cancel_subscription`, data);
}
changePricingPlan(data) {
return axios.post(`${this.url}change_pricing_plan`, data);
}
}
export default new EnterpriseAccountAPI();
@@ -1,17 +1,11 @@
<script setup>
import { ref, computed } from 'vue';
import { OnClickOutside } from '@vueuse/components';
import { useRoute } from 'vue-router';
import { useMapGetter } from 'dashboard/composables/store.js';
import { computed } from 'vue';
import { usePolicy } from 'dashboard/composables/usePolicy';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import BackButton from 'dashboard/components/widgets/BackButton.vue';
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import Policy from 'dashboard/components/policy.vue';
import AssistantSwitcher from 'dashboard/components-next/captain/pageComponents/switcher/AssistantSwitcher.vue';
import CreateAssistantDialog from 'dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue';
const props = defineProps({
currentPage: {
@@ -62,32 +56,11 @@ const props = defineProps({
type: Boolean,
default: true,
},
showAssistantSwitcher: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['click', 'close', 'update:currentPage']);
const route = useRoute();
const { shouldShowPaywall } = usePolicy();
const showAssistantSwitcherDropdown = ref(false);
const createAssistantDialogRef = ref(null);
const assistants = useMapGetter('captainAssistants/getRecords');
const currentAssistantId = computed(() => route.params.assistantId);
const activeAssistantName = computed(() => {
return (
assistants.value?.find(
assistant => assistant.id === Number(currentAssistantId.value)
)?.name || ''
);
});
const showPaywall = computed(() => {
return shouldShowPaywall(props.featureFlag);
});
@@ -99,15 +72,6 @@ const handleButtonClick = () => {
const handlePageChange = event => {
emit('update:currentPage', event);
};
const toggleAssistantSwitcher = () => {
showAssistantSwitcherDropdown.value = !showAssistantSwitcherDropdown.value;
};
const handleCreateAssistant = () => {
showAssistantSwitcherDropdown.value = false;
createAssistantDialogRef.value.dialogRef.open();
};
</script>
<template>
@@ -118,48 +82,9 @@ const handleCreateAssistant = () => {
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
>
<div class="flex gap-4 items-center">
<BackButton v-if="backUrl" :back-url="backUrl" />
<BackButton v-if="backUrl" :to="backUrl" />
<slot name="headerTitle">
<div v-if="showAssistantSwitcher" class="flex items-center gap-2">
<div class="flex items-center gap-1">
<span
v-if="activeAssistantName"
class="text-xl font-medium truncate text-n-slate-12"
>
{{ activeAssistantName }}
</span>
<div v-if="activeAssistantName" class="relative group">
<OnClickOutside
@trigger="showAssistantSwitcherDropdown = false"
>
<Button
icon="i-lucide-chevron-down"
variant="ghost"
color="slate"
size="xs"
class="rounded-md group-hover:bg-n-slate-3 hover:bg-n-slate-3 [&>span]:size-4"
@click="toggleAssistantSwitcher"
/>
<AssistantSwitcher
v-if="showAssistantSwitcherDropdown"
class="absolute ltr:left-0 rtl:right-0 top-9"
@close="showAssistantSwitcherDropdown = false"
@create-assistant="handleCreateAssistant"
/>
</OnClickOutside>
</div>
<Icon
v-if="activeAssistantName"
icon="i-lucide-chevron-right"
class="size-6 text-n-slate-11"
/>
<span class="text-xl font-medium text-n-slate-11">
{{ headerTitle }}
</span>
</div>
</div>
<span v-else class="text-xl font-medium text-n-slate-12">
<span class="text-xl font-medium text-n-slate-12">
{{ headerTitle }}
</span>
</slot>
@@ -172,24 +97,21 @@ const handleCreateAssistant = () => {
</div>
</div>
<div class="flex gap-2">
<slot name="search" />
<div
v-if="!showPaywall && buttonLabel"
v-on-clickaway="() => emit('close')"
class="relative group/captain-button"
>
<Policy :permissions="buttonPolicy">
<Button
:label="buttonLabel"
icon="i-lucide-plus"
size="sm"
class="group-hover/captain-button:brightness-110"
@click="handleButtonClick"
/>
</Policy>
<slot name="action" />
</div>
<div
v-if="!showPaywall && buttonLabel"
v-on-clickaway="() => emit('close')"
class="relative group/campaign-button"
>
<Policy :permissions="buttonPolicy">
<Button
:label="buttonLabel"
icon="i-lucide-plus"
size="sm"
class="group-hover/campaign-button:brightness-110"
@click="handleButtonClick"
/>
</Policy>
<slot name="action" />
</div>
</div>
<slot name="subHeader" />
@@ -222,6 +144,5 @@ const handleCreateAssistant = () => {
@update:current-page="handlePageChange"
/>
</footer>
<CreateAssistantDialog ref="createAssistantDialogRef" type="create" />
</section>
</template>
@@ -0,0 +1,91 @@
<script setup>
import { useRouter } from 'vue-router';
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
defineProps({
isFetching: {
type: Boolean,
default: false,
},
isEmpty: {
type: Boolean,
default: false,
},
currentPage: {
type: Number,
default: 1,
},
totalCount: {
type: Number,
default: 100,
},
itemsPerPage: {
type: Number,
default: 25,
},
showPaginationFooter: {
type: Boolean,
default: false,
},
breadcrumbItems: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update:currentPage']);
const router = useRouter();
const handlePageChange = event => {
emit('update:currentPage', event);
};
const handleBreadcrumbClick = item => {
router.push({
name: item.routeName,
});
};
</script>
<template>
<section
class="px-6 flex flex-col w-full h-screen overflow-y-auto bg-n-background"
>
<div class="max-w-[60rem] mx-auto flex flex-col w-full h-full mb-4">
<header class="mb-7 sticky top-0 bg-n-background pt-4 z-20">
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
</header>
<main class="flex gap-16 w-full flex-1 pb-16">
<section
v-if="$slots.body || $slots.emptyState || isFetching"
class="flex flex-col w-full"
>
<div
v-if="isFetching"
class="flex items-center justify-center py-10 text-n-slate-11"
>
<Spinner />
</div>
<div v-else-if="isEmpty">
<slot name="emptyState" />
</div>
<slot v-else name="body" />
</section>
<section v-if="$slots.controls" class="flex w-full">
<slot name="controls" />
</section>
</main>
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10 pb-4">
<PaginationFooter
:current-page="currentPage"
:total-items="totalCount"
:items-per-page="itemsPerPage"
@update:current-page="handlePageChange"
/>
</footer>
</div>
</section>
</template>
@@ -76,11 +76,12 @@ const handleAction = ({ action, value }) => {
<template>
<CardLayout>
<div class="flex justify-between w-full gap-1">
<h6
class="text-base font-normal text-n-slate-12 line-clamp-1 hover:underline transition-colors"
<router-link
:to="{ name: 'captain_assistants_edit', params: { assistantId: id } }"
class="text-base text-n-slate-12 line-clamp-1 hover:underline transition-colors"
>
{{ name }}
</h6>
</router-link>
<div class="flex items-center gap-2">
<div
v-on-clickaway="() => toggleDropdown(false)"
@@ -1,5 +1,5 @@
<script setup>
import { ref, watch } from 'vue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import NextButton from 'dashboard/components-next/button/Button.vue';
import MessageList from './MessageList.vue';
@@ -29,16 +29,6 @@ const resetConversation = () => {
newMessage.value = '';
};
// Watch for assistant ID changes and reset conversation
watch(
() => assistantId,
(newId, oldId) => {
if (oldId && newId !== oldId) {
resetConversation();
}
}
);
const sendMessage = async () => {
if (!newMessage.value.trim() || isLoading.value) return;
@@ -84,7 +74,7 @@ const sendMessage = async () => {
</h3>
<NextButton
ghost
sm
size="small"
icon="i-lucide-rotate-ccw"
@click="resetConversation"
/>
@@ -107,7 +97,7 @@ const sendMessage = async () => {
/>
<NextButton
ghost
sm
size="small"
:disabled="!newMessage.trim()"
icon="i-lucide-send"
@click="sendMessage"
@@ -0,0 +1,68 @@
<script setup>
import { computed, ref } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n';
import { OnClickOutside } from '@vueuse/components';
import Button from 'dashboard/components-next/button/Button.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
const props = defineProps({
assistantId: {
type: [String, Number],
required: true,
},
});
const emit = defineEmits(['update']);
const { t } = useI18n();
const isFilterOpen = ref(false);
const assistants = useMapGetter('captainAssistants/getRecords');
const assistantOptions = computed(() => [
{
label: t(`CAPTAIN.RESPONSES.FILTER.ALL_ASSISTANTS`),
value: 'all',
action: 'filter',
},
...assistants.value.map(assistant => ({
value: assistant.id,
label: assistant.name,
action: 'filter',
})),
]);
const selectedAssistantLabel = computed(() => {
const assistant = assistantOptions.value.find(
option => option.value === props.assistantId
);
return t('CAPTAIN.RESPONSES.FILTER.ASSISTANT', {
selected: assistant ? assistant.label : '',
});
});
const handleAssistantFilterChange = ({ value }) => {
isFilterOpen.value = false;
emit('update', value);
};
</script>
<template>
<OnClickOutside @trigger="isFilterOpen = false">
<Button
:label="selectedAssistantLabel"
icon="i-lucide-chevron-down"
size="sm"
color="slate"
trailing-icon
class="max-w-48"
@click="isFilterOpen = !isFilterOpen"
/>
<DropdownMenu
v-if="isFilterOpen"
:menu-items="assistantOptions"
class="mt-2"
@action="handleAssistantFilterChange"
/>
</OnClickOutside>
</template>
@@ -18,7 +18,7 @@ const props = defineProps({
validator: value => ['create', 'edit'].includes(value),
},
});
const emit = defineEmits(['close', 'created']);
const emit = defineEmits(['close']);
const { t } = useI18n();
const store = useStore();
@@ -35,18 +35,8 @@ const i18nKey = computed(
() => `CAPTAIN.ASSISTANTS.${props.type.toUpperCase()}`
);
const createAssistant = async assistantDetails => {
try {
const newAssistant = await store.dispatch(
'captainAssistants/create',
assistantDetails
);
emit('created', newAssistant);
} catch (error) {
const errorMessage = error?.message || t(`${i18nKey.value}.ERROR_MESSAGE`);
useAlert(errorMessage);
}
};
const createAssistant = assistantDetails =>
store.dispatch('captainAssistants/create', assistantDetails);
const handleSubmit = async updatedAssistant => {
try {
@@ -0,0 +1,333 @@
<script setup>
import { reactive, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
import { useMapGetter } from 'dashboard/composables/store';
import Input from 'dashboard/components-next/input/Input.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
import Accordion from 'dashboard/components-next/Accordion/Accordion.vue';
const props = defineProps({
mode: {
type: String,
required: true,
validator: value => ['edit', 'create'].includes(value),
},
assistant: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['submit']);
const { t } = useI18n();
const formState = {
uiFlags: useMapGetter('captainAssistants/getUIFlags'),
};
const initialState = {
name: '',
description: '',
productName: '',
welcomeMessage: '',
handoffMessage: '',
resolutionMessage: '',
instructions: '',
features: {
conversationFaqs: false,
memories: false,
citations: false,
},
temperature: 1,
};
const state = reactive({ ...initialState });
const validationRules = {
name: { required, minLength: minLength(1) },
description: { required, minLength: minLength(1) },
productName: { required, minLength: minLength(1) },
welcomeMessage: { minLength: minLength(1) },
handoffMessage: { minLength: minLength(1) },
resolutionMessage: { minLength: minLength(1) },
instructions: { minLength: minLength(1) },
};
const v$ = useVuelidate(validationRules, state);
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
const getErrorMessage = field => {
return v$.value[field].$error ? v$.value[field].$errors[0].$message : '';
};
const formErrors = computed(() => ({
name: getErrorMessage('name'),
description: getErrorMessage('description'),
productName: getErrorMessage('productName'),
welcomeMessage: getErrorMessage('welcomeMessage'),
handoffMessage: getErrorMessage('handoffMessage'),
resolutionMessage: getErrorMessage('resolutionMessage'),
instructions: getErrorMessage('instructions'),
}));
const updateStateFromAssistant = assistant => {
const { config = {} } = assistant;
state.name = assistant.name;
state.description = assistant.description;
state.productName = config.product_name;
state.welcomeMessage = config.welcome_message;
state.handoffMessage = config.handoff_message;
state.resolutionMessage = config.resolution_message;
state.instructions = config.instructions;
state.features = {
conversationFaqs: config.feature_faq || false,
memories: config.feature_memory || false,
citations: config.feature_citation || false,
};
state.temperature = config.temperature || 1;
};
const handleBasicInfoUpdate = async () => {
const result = await Promise.all([
v$.value.name.$validate(),
v$.value.description.$validate(),
v$.value.productName.$validate(),
]).then(results => results.every(Boolean));
if (!result) return;
const payload = {
name: state.name,
description: state.description,
config: {
...props.assistant.config,
product_name: state.productName,
},
};
emit('submit', payload);
};
const handleSystemMessagesUpdate = async () => {
const result = await Promise.all([
v$.value.welcomeMessage.$validate(),
v$.value.handoffMessage.$validate(),
v$.value.resolutionMessage.$validate(),
]).then(results => results.every(Boolean));
if (!result) return;
const payload = {
config: {
...props.assistant.config,
welcome_message: state.welcomeMessage,
handoff_message: state.handoffMessage,
resolution_message: state.resolutionMessage,
},
};
emit('submit', payload);
};
const handleInstructionsUpdate = async () => {
const result = await v$.value.instructions.$validate();
if (!result) return;
const payload = {
config: {
...props.assistant.config,
temperature: state.temperature || 1,
instructions: state.instructions,
},
};
emit('submit', payload);
};
const handleFeaturesUpdate = () => {
const payload = {
config: {
...props.assistant.config,
feature_faq: state.features.conversationFaqs,
feature_memory: state.features.memories,
feature_citation: state.features.citations,
},
};
emit('submit', payload);
};
watch(
() => props.assistant,
newAssistant => {
if (props.mode === 'edit' && newAssistant) {
updateStateFromAssistant(newAssistant);
}
},
{ immediate: true }
);
</script>
<template>
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
<!-- Basic Information Section -->
<Accordion
:title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.BASIC_INFO')"
is-open
>
<div class="flex flex-col gap-4 pt-4">
<Input
v-model="state.name"
:label="t('CAPTAIN.ASSISTANTS.FORM.NAME.LABEL')"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.NAME.PLACEHOLDER')"
:message="formErrors.name"
:message-type="formErrors.name ? 'error' : 'info'"
/>
<Editor
v-model="state.description"
:label="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.LABEL')"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.PLACEHOLDER')"
:message="formErrors.description"
:message-type="formErrors.description ? 'error' : 'info'"
/>
<Input
v-model="state.productName"
:label="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.LABEL')"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.PLACEHOLDER')"
:message="formErrors.productName"
:message-type="formErrors.productName ? 'error' : 'info'"
/>
<div class="flex justify-end">
<Button
size="small"
:loading="isLoading"
@click="handleBasicInfoUpdate"
>
{{ t('CAPTAIN.ASSISTANTS.FORM.UPDATE') }}
</Button>
</div>
</div>
</Accordion>
<!-- Instructions Section -->
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.INSTRUCTIONS')">
<div class="flex flex-col gap-4">
<Editor
v-model="state.instructions"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
:message="formErrors.instructions"
:max-length="20000"
:message-type="formErrors.instructions ? 'error' : 'info'"
/>
<div class="flex flex-col gap-2 mt-4">
<label class="text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.LABEL') }}
</label>
<div class="flex items-center gap-4">
<input
v-model="state.temperature"
type="range"
min="0"
max="1"
step="0.1"
class="w-full"
/>
<span class="text-sm text-n-slate-12">{{ state.temperature }}</span>
</div>
<p class="text-sm text-n-slate-11 italic">
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.DESCRIPTION') }}
</p>
</div>
<div class="flex justify-end">
<Button
size="small"
:loading="isLoading"
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
@click="handleInstructionsUpdate"
/>
</div>
</div>
</Accordion>
<!-- Greeting Messages Section -->
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.SYSTEM_MESSAGES')">
<div class="flex flex-col gap-4 pt-4">
<Editor
v-model="state.handoffMessage"
:label="t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.LABEL')"
:placeholder="
t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.PLACEHOLDER')
"
:message="formErrors.handoffMessage"
:message-type="formErrors.handoffMessage ? 'error' : 'info'"
/>
<Editor
v-model="state.resolutionMessage"
:label="t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.LABEL')"
:placeholder="
t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.PLACEHOLDER')
"
:message="formErrors.resolutionMessage"
:message-type="formErrors.resolutionMessage ? 'error' : 'info'"
/>
<div class="flex justify-end">
<Button
size="small"
:loading="isLoading"
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
@click="handleSystemMessagesUpdate"
/>
</div>
</div>
</Accordion>
<!-- Features Section -->
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.FEATURES')">
<div class="flex flex-col gap-4 pt-4">
<div class="flex flex-col gap-2">
<label class="text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.TITLE') }}
</label>
<div class="flex flex-col gap-2">
<label class="flex items-center gap-2">
<input
v-model="state.features.conversationFaqs"
type="checkbox"
/>
{{
t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS')
}}
</label>
<label class="flex items-center gap-2">
<input v-model="state.features.memories" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
</label>
<label class="flex items-center gap-2">
<input v-model="state.features.citations" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
</label>
</div>
</div>
<div class="flex justify-end">
<Button
size="small"
:loading="isLoading"
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
@click="handleFeaturesUpdate"
/>
</div>
</div>
</Accordion>
</form>
</template>
@@ -3,8 +3,6 @@ import { reactive, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useVuelidate } from '@vuelidate/core';
import { minLength } from '@vuelidate/validators';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { useAccount } from 'dashboard/composables/useAccount';
import Button from 'dashboard/components-next/button/Button.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
@@ -19,16 +17,10 @@ const props = defineProps({
const emit = defineEmits(['submit']);
const { t } = useI18n();
const { isCloudFeatureEnabled } = useAccount();
const isCaptainV2Enabled = computed(() =>
isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN_V2)
);
const initialState = {
handoffMessage: '',
resolutionMessage: '',
instructions: '',
temperature: 1,
};
@@ -37,7 +29,6 @@ const state = reactive({ ...initialState });
const validationRules = {
handoffMessage: { minLength: minLength(1) },
resolutionMessage: { minLength: minLength(1) },
instructions: { minLength: minLength(1) },
};
const v$ = useVuelidate(validationRules, state);
@@ -49,30 +40,20 @@ const getErrorMessage = field => {
const formErrors = computed(() => ({
handoffMessage: getErrorMessage('handoffMessage'),
resolutionMessage: getErrorMessage('resolutionMessage'),
instructions: getErrorMessage('instructions'),
}));
const updateStateFromAssistant = assistant => {
const { config = {} } = assistant;
state.handoffMessage = config.handoff_message;
state.resolutionMessage = config.resolution_message;
state.instructions = config.instructions;
state.temperature = config.temperature || 1;
};
const handleSystemMessagesUpdate = async () => {
const validations = [
const result = await Promise.all([
v$.value.handoffMessage.$validate(),
v$.value.resolutionMessage.$validate(),
];
if (!isCaptainV2Enabled.value) {
validations.push(v$.value.instructions.$validate());
}
const result = await Promise.all(validations).then(results =>
results.every(Boolean)
);
]).then(results => results.every(Boolean));
if (!result) return;
const payload = {
@@ -84,10 +65,6 @@ const handleSystemMessagesUpdate = async () => {
},
};
if (!isCaptainV2Enabled.value) {
payload.config.instructions = state.instructions;
}
emit('submit', payload);
};
@@ -118,16 +95,6 @@ watch(
:message-type="formErrors.resolutionMessage ? 'error' : 'info'"
/>
<Editor
v-if="!isCaptainV2Enabled"
v-model="state.instructions"
:label="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.LABEL')"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
:message="formErrors.instructions"
:max-length="20000"
:message-type="formErrors.instructions ? 'error' : 'info'"
/>
<div class="flex flex-col gap-2">
<label class="text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.LABEL') }}
@@ -8,13 +8,6 @@ import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import DocumentForm from './DocumentForm.vue';
defineProps({
assistantId: {
type: Number,
required: true,
},
});
const emit = defineEmits(['close']);
const { t } = useI18n();
const store = useStore();
@@ -55,11 +48,7 @@ defineExpose({ dialogRef });
:show-confirm-button="false"
@close="handleClose"
>
<DocumentForm
:assistant-id="assistantId"
@submit="handleSubmit"
@cancel="handleCancel"
/>
<DocumentForm @submit="handleSubmit" @cancel="handleCancel" />
<template #footer />
</Dialog>
</template>
@@ -2,7 +2,7 @@
import { reactive, computed, ref, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useVuelidate } from '@vuelidate/core';
import { minLength, requiredIf, url } from '@vuelidate/validators';
import { required, minLength, requiredIf, url } from '@vuelidate/validators';
import { useMapGetter } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
@@ -10,13 +10,6 @@ import Input from 'dashboard/components-next/input/Input.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
const props = defineProps({
assistantId: {
type: Number,
required: true,
},
});
const emit = defineEmits(['submit', 'cancel']);
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
@@ -25,11 +18,13 @@ const { t } = useI18n();
const formState = {
uiFlags: useMapGetter('captainDocuments/getUIFlags'),
assistants: useMapGetter('captainAssistants/getRecords'),
};
const initialState = {
name: '',
url: '',
assistantId: null,
documentType: 'url',
pdfFile: null,
};
@@ -43,11 +38,19 @@ const validationRules = {
url: requiredIf(() => state.documentType === 'url' && url),
minLength: requiredIf(() => state.documentType === 'url' && minLength(1)),
},
assistantId: { required },
pdfFile: {
required: requiredIf(() => state.documentType === 'pdf'),
},
};
const assistantList = computed(() =>
formState.assistants.value.map(assistant => ({
value: assistant.id,
label: assistant.name,
}))
);
const documentTypeOptions = [
{ value: 'url', label: t('CAPTAIN.DOCUMENTS.FORM.TYPE.URL') },
{ value: 'pdf', label: t('CAPTAIN.DOCUMENTS.FORM.TYPE.PDF') },
@@ -67,6 +70,7 @@ const getErrorMessage = (field, errorKey) => {
const formErrors = computed(() => ({
url: getErrorMessage('url', 'URL'),
assistantId: getErrorMessage('assistantId', 'ASSISTANT'),
pdfFile: getErrorMessage('pdfFile', 'PDF_FILE'),
}));
@@ -102,7 +106,7 @@ const openFileDialog = () => {
const prepareDocumentDetails = () => {
const formData = new FormData();
formData.append('document[assistant_id]', props.assistantId);
formData.append('document[assistant_id]', state.assistantId);
if (state.documentType === 'url') {
formData.append('document[external_link]', state.url);
@@ -214,6 +218,21 @@ const handleSubmit = async () => {
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.NAME.PLACEHOLDER')"
/>
<div class="flex flex-col gap-1">
<label for="assistant" class="mb-0.5 text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.DOCUMENTS.FORM.ASSISTANT.LABEL') }}
</label>
<ComboBox
id="assistant"
v-model="state.assistantId"
:options="assistantList"
:has-error="!!formErrors.assistantId"
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.ASSISTANT.PLACEHOLDER')"
class="[&>div>button]:bg-n-alpha-black2"
:message="formErrors.assistantId"
/>
</div>
<div class="flex gap-3 justify-between items-center w-full">
<Button
type="button"
@@ -3,7 +3,6 @@ import { ref, computed } from 'vue';
import { useStore } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import ResponseForm from './ResponseForm.vue';
@@ -22,7 +21,6 @@ const props = defineProps({
const emit = defineEmits(['close']);
const { t } = useI18n();
const store = useStore();
const route = useRoute();
const dialogRef = ref(null);
const responseForm = ref(null);
@@ -41,15 +39,9 @@ const createResponse = responseDetails =>
const handleSubmit = async updatedResponse => {
try {
if (props.type === 'edit') {
await updateResponse({
...updatedResponse,
assistant_id: route.params.assistantId,
});
await updateResponse(updatedResponse);
} else {
await createResponse({
...updatedResponse,
assistant_id: route.params.assistantId,
});
await createResponse(updatedResponse);
}
useAlert(t(`${i18nKey.value}.SUCCESS_MESSAGE`));
dialogRef.value.close();
@@ -8,6 +8,7 @@ import { useMapGetter } from 'dashboard/composables/store';
import Input from 'dashboard/components-next/input/Input.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
const props = defineProps({
mode: {
@@ -20,17 +21,18 @@ const props = defineProps({
default: () => ({}),
},
});
const emit = defineEmits(['submit', 'cancel']);
const { t } = useI18n();
const formState = {
uiFlags: useMapGetter('captainResponses/getUIFlags'),
assistants: useMapGetter('captainAssistants/getRecords'),
};
const initialState = {
question: '',
answer: '',
assistantId: null,
};
const state = reactive({ ...initialState });
@@ -38,8 +40,16 @@ const state = reactive({ ...initialState });
const validationRules = {
question: { required, minLength: minLength(1) },
answer: { required, minLength: minLength(1) },
assistantId: { required },
};
const assistantList = computed(() =>
formState.assistants.value.map(assistant => ({
value: assistant.id,
label: assistant.name,
}))
);
const v$ = useVuelidate(validationRules, state);
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
@@ -53,6 +63,7 @@ const getErrorMessage = (field, errorKey) => {
const formErrors = computed(() => ({
question: getErrorMessage('question', 'QUESTION'),
answer: getErrorMessage('answer', 'ANSWER'),
assistantId: getErrorMessage('assistantId', 'ASSISTANT'),
}));
const handleCancel = () => emit('cancel');
@@ -60,6 +71,7 @@ const handleCancel = () => emit('cancel');
const prepareDocumentDetails = () => ({
question: state.question,
answer: state.answer,
assistant_id: state.assistantId,
});
const handleSubmit = async () => {
@@ -74,11 +86,12 @@ const handleSubmit = async () => {
const updateStateFromResponse = response => {
if (!response) return;
const { question, answer } = response;
const { question, answer, assistant } = response;
Object.assign(state, {
question,
answer,
assistantId: assistant.id,
});
};
@@ -102,6 +115,7 @@ watch(
:message="formErrors.question"
:message-type="formErrors.question ? 'error' : 'info'"
/>
<Editor
v-model="state.answer"
:label="t('CAPTAIN.RESPONSES.FORM.ANSWER.LABEL')"
@@ -110,6 +124,22 @@ watch(
:max-length="10000"
:message-type="formErrors.answer ? 'error' : 'info'"
/>
<div class="flex flex-col gap-1">
<label for="assistant" class="mb-0.5 text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.RESPONSES.FORM.ASSISTANT.LABEL') }}
</label>
<ComboBox
id="assistant"
v-model="state.assistantId"
:options="assistantList"
:has-error="!!formErrors.assistantId"
:placeholder="t('CAPTAIN.RESPONSES.FORM.ASSISTANT.PLACEHOLDER')"
class="[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:dark:outline-n-weak [&>div>button:not(.focused)]:hover:!outline-n-slate-6"
:message="formErrors.assistantId"
/>
</div>
<div class="flex items-center justify-between w-full gap-3">
<Button
type="button"
@@ -1,143 +0,0 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
import Button from 'dashboard/components-next/button/Button.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
const emit = defineEmits(['close', 'createAssistant']);
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const store = useStore();
const assistants = useMapGetter('captainAssistants/getRecords');
const currentAssistantId = computed(() => route.params.assistantId);
const isAssistantActive = assistant => {
return assistant.id === Number(currentAssistantId.value);
};
const fetchDataForRoute = async (routeName, assistantId) => {
const dataFetchMap = {
captain_assistants_responses_index: async () => {
await store.dispatch('captainResponses/get', { assistantId });
await store.dispatch('captainResponses/fetchPendingCount', assistantId);
},
captain_assistants_responses_pending: async () => {
await store.dispatch('captainResponses/get', {
assistantId,
status: 'pending',
});
},
captain_assistants_documents_index: async () => {
await store.dispatch('captainDocuments/get', { assistantId });
},
captain_assistants_scenarios_index: async () => {
await store.dispatch('captainScenarios/get', { assistantId });
},
captain_assistants_playground_index: () => {
// Playground doesn't need pre-fetching, it loads on interaction
},
captain_assistants_inboxes_index: async () => {
await store.dispatch('captainInboxes/get', { assistantId });
},
captain_tools_index: async () => {
await store.dispatch('captainCustomTools/get', { page: 1 });
},
captain_assistants_settings_index: async () => {
await store.dispatch('captainAssistants/show', assistantId);
},
};
const fetchFn = dataFetchMap[routeName];
if (fetchFn) {
await fetchFn();
}
};
const handleAssistantChange = async assistant => {
if (isAssistantActive(assistant)) return;
const currentRouteName = route.name;
const targetRouteName =
currentRouteName || 'captain_assistants_responses_index';
await fetchDataForRoute(targetRouteName, assistant.id);
await router.push({
name: targetRouteName,
params: {
accountId: route.params.accountId,
assistantId: assistant.id,
},
});
emit('close');
};
const openCreateAssistantDialog = () => {
emit('createAssistant');
emit('close');
};
</script>
<template>
<div
class="pt-5 pb-3 bg-n-alpha-3 backdrop-blur-[100px] outline outline-n-container outline-1 z-50 absolute w-[27.5rem] rounded-xl shadow-md flex flex-col gap-4"
>
<div
class="flex items-center justify-between gap-4 px-6 pb-3 border-b border-n-alpha-2"
>
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2">
<h2
class="text-base font-medium cursor-pointer text-n-slate-12 w-fit hover:underline"
>
{{ t('CAPTAIN.ASSISTANT_SWITCHER.ASSISTANTS') }}
</h2>
</div>
<p class="text-sm text-n-slate-11">
{{ t('CAPTAIN.ASSISTANT_SWITCHER.SWITCH_ASSISTANT') }}
</p>
</div>
<Button
:label="t('CAPTAIN.ASSISTANT_SWITCHER.NEW_ASSISTANT')"
color="slate"
icon="i-lucide-plus"
size="sm"
class="!bg-n-alpha-2 hover:!bg-n-alpha-3"
@click="openCreateAssistantDialog"
/>
</div>
<div v-if="assistants.length > 0" class="flex flex-col gap-2 px-4">
<Button
v-for="assistant in assistants"
:key="assistant.id"
:label="assistant.name"
variant="ghost"
color="slate"
trailing-icon
:icon="isAssistantActive(assistant) ? 'i-lucide-check' : ''"
class="!justify-end !px-2 !py-2 hover:!bg-n-alpha-2 [&>.i-lucide-check]:text-n-teal-10 h-9"
size="sm"
@click="handleAssistantChange(assistant)"
>
<span class="text-sm font-medium truncate text-n-slate-12">
{{ assistant.name || '' }}
</span>
<Avatar
v-if="assistant"
:name="assistant.name"
:size="20"
icon-name="i-lucide-bot"
rounded-full
/>
</Button>
</div>
</div>
</template>
@@ -78,10 +78,8 @@ const handleSuggestion = opt => {
</p>
<router-link
:to="{
name: 'captain_assistants_create_index',
params: {
accountId: route.params.accountId,
},
name: 'captain_assistants_index',
params: { accountId: route.params.accountId },
}"
class="text-n-slate-11 underline hover:text-n-slate-12"
>
@@ -0,0 +1,61 @@
<script setup>
import Icon from 'dashboard/components-next/icon/Icon.vue';
defineProps({
color: {
type: String,
default: 'blue',
validator: value => ['ruby', 'blue', 'teal', 'amber'].includes(value),
},
icon: {
type: String,
default: 'i-lucide-info',
},
title: {
type: String,
default: null,
},
message: {
type: String,
default: null,
},
});
const colorClasses = {
ruby: 'outline-n-ruby-4 bg-n-ruby-3 text-n-ruby-11',
blue: 'outline-n-blue-4 bg-n-blue-3 text-n-blue-11',
teal: 'outline-n-teal-4 bg-n-teal-3 text-n-teal-11',
amber: 'outline-n-amber-4 bg-n-amber-3 text-n-amber-11',
};
const titleColorClasses = {
ruby: 'text-n-ruby-11',
blue: 'text-n-blue-11',
teal: 'text-n-teal-11',
amber: 'text-n-amber-11',
};
</script>
<template>
<div
class="p-4 rounded-xl outline outline-1 -outline-offset-1"
:class="colorClasses[color]"
>
<div class="flex gap-3 items-start">
<Icon :icon="icon" class="flex-shrink-0 mt-0.5" />
<div class="flex-1 space-y-1">
<h4
v-if="title"
class="text-sm font-semibold"
:class="titleColorClasses[color]"
>
{{ title }}
</h4>
<p v-if="message" class="mb-0 text-sm">
{{ message }}
</p>
<slot />
</div>
</div>
</div>
</template>
@@ -221,70 +221,26 @@ const menuItems = computed(() => {
name: 'Captain',
icon: 'i-woot-captain',
label: t('SIDEBAR.CAPTAIN'),
activeOn: ['captain_assistants_create_index'],
children: [
{
name: 'FAQs',
label: t('SIDEBAR.CAPTAIN_RESPONSES'),
activeOn: [
'captain_assistants_responses_index',
'captain_assistants_responses_pending',
],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_responses_index',
}),
name: 'Assistants',
label: t('SIDEBAR.CAPTAIN_ASSISTANTS'),
to: accountScopedRoute('captain_assistants_index'),
},
{
name: 'Documents',
label: t('SIDEBAR.CAPTAIN_DOCUMENTS'),
activeOn: ['captain_assistants_documents_index'],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_documents_index',
}),
to: accountScopedRoute('captain_documents_index'),
},
{
name: 'Scenarios',
label: t('SIDEBAR.CAPTAIN_SCENARIOS'),
activeOn: ['captain_assistants_scenarios_index'],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_scenarios_index',
}),
},
{
name: 'Playground',
label: t('SIDEBAR.CAPTAIN_PLAYGROUND'),
activeOn: ['captain_assistants_playground_index'],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_playground_index',
}),
},
{
name: 'Inboxes',
label: t('SIDEBAR.CAPTAIN_INBOXES'),
activeOn: ['captain_assistants_inboxes_index'],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_inboxes_index',
}),
name: 'Responses',
label: t('SIDEBAR.CAPTAIN_RESPONSES'),
to: accountScopedRoute('captain_responses_index'),
},
{
name: 'Tools',
label: t('SIDEBAR.CAPTAIN_TOOLS'),
activeOn: ['captain_tools_index'],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_tools_index',
}),
},
{
name: 'Settings',
label: t('SIDEBAR.CAPTAIN_SETTINGS'),
activeOn: [
'captain_assistants_settings_index',
'captain_assistants_guidelines_index',
'captain_assistants_guardrails_index',
],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_settings_index',
}),
to: accountScopedRoute('captain_tools_index'),
},
],
},
@@ -19,46 +19,19 @@ export function useSidebarContext() {
return '/';
};
// Helper to find route definition by name without resolving
const findRouteByName = name => {
const routes = router.getRoutes();
return routes.find(route => route.name === name);
};
const resolvePermissions = to => {
if (!to) return [];
// If navigationPath param exists, get the target route definition
if (to.params?.navigationPath) {
const targetRoute = findRouteByName(to.params.navigationPath);
return targetRoute?.meta?.permissions ?? [];
}
return router.resolve(to)?.meta?.permissions ?? [];
if (to) return router.resolve(to)?.meta?.permissions ?? [];
return [];
};
const resolveFeatureFlag = to => {
if (!to) return '';
// If navigationPath param exists, get the target route definition
if (to.params?.navigationPath) {
const targetRoute = findRouteByName(to.params.navigationPath);
return targetRoute?.meta?.featureFlag || '';
}
return router.resolve(to)?.meta?.featureFlag || '';
if (to) return router.resolve(to)?.meta?.featureFlag || '';
return '';
};
const resolveInstallationType = to => {
if (!to) return [];
// If navigationPath param exists, get the target route definition
if (to.params?.navigationPath) {
const targetRoute = findRouteByName(to.params.navigationPath);
return targetRoute?.meta?.installationTypes || [];
}
return router.resolve(to)?.meta?.installationTypes || [];
if (to) return router.resolve(to)?.meta?.installationTypes || [];
return [];
};
const isAllowed = to => {
@@ -14,7 +14,7 @@ import {
} from 'dashboard/constants/automation';
/**
* This is a shared composables that holds utilities used to build dropdown and file options
* This is a shared composables that holds utilites used to build dropdown and file options
* @returns {Object} An object containing various automation-related functions and computed properties.
*/
export default function useAutomationValues() {
+1
View File
@@ -41,6 +41,7 @@ export const FEATURE_FLAGS = {
CAPTAIN_V2: 'captain_integration_v2',
SAML: 'saml',
QUOTED_EMAIL_REPLY: 'quoted_email_reply',
STRIPE_V2: 'stripe_v2',
};
export const PREMIUM_FEATURES = [
@@ -21,7 +21,7 @@ export const initializeAudioAlerts = user => {
enable_audio_alerts: audioAlertType,
alert_if_unread_assigned_conversation_exist: alertIfUnreadConversationExist,
notification_tone: audioAlertTone,
// UI Settings can be undefined initially as we don't send the
// UI Settings can be undefined initally as we don't send the
// entire payload for the user during the signup process.
} = uiSettings || {};
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "حذف",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "حذف جهة الاتصال"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Изтрий",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Изтриване на контакта"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Esborrar",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Contacte esborrat"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Vymazat",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Slet",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Slet kontakt"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Löschen",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Kontakt löschen"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Διαγραφή",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Διαγραφή Επαφής"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -341,11 +341,6 @@
"CAPTAIN": {
"NAME": "Captain",
"HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant"
},
"COPILOT": {
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
@@ -485,7 +480,9 @@
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"SETTINGS": {
"HEADER": "Settings",
"BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"BASIC_SETTINGS": {
"TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -502,16 +499,15 @@
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
},
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
}
}
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
}
},
"OPTIONS": {
@@ -530,6 +526,9 @@
"GUARDRAILS": {
"TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -575,6 +574,9 @@
"RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -620,6 +622,9 @@
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})",
@@ -717,6 +722,11 @@
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
}
},
"DELETE": {
@@ -911,6 +921,12 @@
"LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
}
},
"EDIT": {
@@ -251,12 +251,6 @@
"ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended",
"MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
}
},
"COMPONENTS": {
@@ -311,10 +305,6 @@
"CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Inboxes",
"CAPTAIN_SETTINGS": "Settings",
"HOME": "Home",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
@@ -392,6 +382,11 @@
"DESCRIPTION": "View your previous invoices, edit your billing details, or cancel your subscription.",
"BUTTON_TXT": "Go to the billing portal"
},
"V2_BILLING_PORTAL": {
"TITLE": "Manage your billing",
"DESCRIPTION": "View your credits balance, top up, manage subscriptions, and view transactions.",
"BUTTON_TXT": "Go to billing dashboard"
},
"CAPTAIN": {
"TITLE": "Captain",
"DESCRIPTION": "Manage usage and credits for Captain AI.",
@@ -407,6 +402,120 @@
},
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again."
},
"BILLING_SETTINGS_V2": {
"TITLE": "Billing",
"DESCRIPTION": "Manage your usage-based subscription with flexible credit-based pricing. Top up anytime or subscribe to a plan.",
"VIEW_PRICING": "View Pricing",
"OPEN_STRIPE_DASHBOARD": "Open Stripe Dashboard",
"CREDITS_BALANCE": {
"TITLE": "Captain Credits",
"DESCRIPTION": "Your current credit balance and recent transactions.",
"CURRENT_BALANCE": "Current Balance",
"CREDITS": "credits",
"REFRESH": "Refresh",
"BUY_CREDITS": "Buy Credits",
"VIEW_HISTORY": "View History",
"MONTHLY_CREDITS": "Monthly Credits",
"TOPUP_CREDITS": "Top-up Credits",
"USED_THIS_MONTH": "Used This Month",
"TOTAL_USED": "Total Used",
"USAGE_BASED_INFO": "Credits are consumed as you use Captain"
},
"PLAN_SUMMARY": {
"NAME": "Plan Name",
"PRICE": "Price",
"PER_MONTH": "{price}/month",
"RENEWAL_DATE": "Renewal Date",
"CREDITS_INCLUDED": "Captain AI Credits Included",
"NUMBER_OF_SEATS": "Number of Seats",
"ADJUST_SEATS_HINT": "Adjust seats to scale your plan",
"UPDATE": "Update",
"VIEW_ALL_PLANS": "View All Plans",
"CANCEL_PLAN": "Cancel Plan"
},
"PRICING_PLANS": {
"TITLE": "Pricing Plans",
"DESCRIPTION": "Choose a plan that fits your team. All plans include credits that reset monthly.",
"CURRENT_PLAN": "Current Subscription",
"RECOMMENDED": "Recommended",
"MONTH": "month",
"SEATS": "seats",
"CREDITS_PER_SEAT": "credits/month",
"MIN_SEATS": "Minimum seats",
"SUBSCRIBE": "Subscribe",
"SWITCH_PLAN": "Switch Plan",
"BLOCKED": "Blocked",
"PENDING_TITLE": "Pending Changes Scheduled",
"PENDING_MESSAGE": "Your subscription changes will take effect on your next billing date.",
"NEXT_PLAN": "Next Plan",
"NEXT_QUANTITY": "Next Quantity",
"EFFECTIVE_DATE": "Effective Date",
"CANCELLING_TITLE": "Subscription Ending",
"CANCELLING_MESSAGE": "Your subscription will be cancelled at the end of the current billing period. You can continue using your current plan until then.",
"CANCELLING_BLOCKED_INFO": "You cannot change plans or subscribe to new plans while your subscription is ending.",
"ENDS_ON": "Subscription ends on",
"NO_PLANS": "No pricing plans available.",
"SUBSCRIBE_SUCCESS": "Successfully subscribed to the plan! Your credits have been updated.",
"CANCEL_SUCCESS": "Subscription cancelled successfully.",
"UPDATE_QUANTITY_SUCCESS": "Subscription quantity updated successfully!",
"CHANGE_PLAN_SUCCESS": "Successfully changed to the new plan! Your credits have been updated."
},
"TOPUP_OPTIONS": {
"TITLE": "Top-up Credits",
"DESCRIPTION": "Add more credits to your account, to ensure uninterrupted Captain usage.",
"CREDITS": "Credits",
"PRICE": "Price",
"BUY_NOW": "Buy Now",
"NO_OPTIONS": "No top-up options available.",
"TOPUP_SUCCESS": "Credits added successfully! Your new balance is available now.",
"NO_PAYMENT_METHOD_ERROR": "No payment method found. Please add a payment method in your Stripe dashboard before making a purchase. Click the 'Open Stripe Dashboard' button above to add one.",
"UPGRADE_REQUIRED": "Upgrade your plan to purchase top-up credits.",
"UPGRADE_MESSAGE": "Top-ups are only available for Startup, Business, and Enterprise plans. Please upgrade your plan to purchase additional credits."
},
"CREDIT_GRANTS": {
"TITLE": "Credit History",
"CREDITS": "credits",
"EXPIRES": "Expires",
"NO_EXPIRY": "Never expires",
"ADDED": "Added on {date}",
"VOIDED": "Voided",
"NO_GRANTS": "No credit grants yet.",
"TOPUPS": "Topups",
"MONTHLY": "Monthly"
},
"SUBSCRIBE_MODAL": {
"TITLE": "Confirm Subscription",
"DESCRIPTION": "You are subscribing to {planName} for {planPrice} per seat with {credits} included credits for Captain AI.",
"NUMBER_OF_SEATS": "Number of seats",
"MIN": "Min",
"MAX": "Max",
"BASE_PRICE": "Base price per seat",
"INCLUDED_CREDITS": "Included credits",
"CREDITS": "credits",
"MONTHLY_TOTAL": "Monthly Total",
"CANCEL": "Cancel",
"CONFIRM": "Confirm Subscription"
},
"TOPUP_MODAL": {
"TITLE": "Confirm Top-up",
"DESCRIPTION": "Top up your account with {credits} credits for {price}. Your card will be immediately charged.",
"CREDITS": "Credits",
"PRICE": "Price",
"INFO": "Credits will be added to your account immediately and never expire.",
"CANCEL": "Cancel",
"CONFIRM": "Confirm Top-up"
},
"CANCEL_MODAL": {
"TITLE": "Cancel Subscription",
"WARNING_TITLE": "Are you sure?",
"WARNING_MESSAGE": "Cancelling your subscription will stop monthly credit renewals. Any remaining credits will still be available until used.",
"REASON_LABEL": "Reason for cancellation (optional)",
"REASON_PLACEHOLDER": "Help us improve by sharing why you're cancelling...",
"INFO": "You can resubscribe anytime to continue receiving monthly credits.",
"KEEP_SUBSCRIPTION": "Keep Subscription",
"CONFIRM_CANCEL": "Confirm Cancellation"
}
},
"SECURITY_SETTINGS": {
"TITLE": "Security",
"DESCRIPTION": "Manage your account security settings.",
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Eliminar",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Eliminar contacto"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "حذف",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "حذف مخاطب"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Poista",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Poista yhteystieto"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Supprimer",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Supprimer le contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "מחק",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "מחק איש קשר"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Izbriši",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Törlés",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Kontakt törlése"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Hapus",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Hapus kontak"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Eyða",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Eyða tengilið"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -13,7 +13,7 @@
"BROWSER": "Browser",
"OS": "Sistema Operativo",
"INITIATED_FROM": "Iniziato da",
"INITIATED_AT": "Avviata alle",
"INITIATED_AT": "Iniziato alle",
"IP_ADDRESS": "Indirizzo IP",
"CREATED_AT_LABEL": "Creato il",
"NEW_MESSAGE": "Nuovo messaggio",
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "Nessuna etichetta disponibile.",
"SELECTED_COUNT": "{count} selezionate",
"CLEAR_SELECTION": "Annulla selezione",
"SELECT_ALL": "Seleziona tutto ({count})",
"DELETE_CONTACTS": "Elimina",
"DELETE_SUCCESS": "Contatti eliminati con successo.",
"DELETE_FAILED": "Impossibile eliminare i contatti.",
"DELETE_DIALOG": {
"TITLE": "Elimina i contatti selezionati",
"SINGULAR_TITLE": "Elimina il contatto selezionato",
"DESCRIPTION": "Questo eliminerà definitivamente i {count} contatti selezionati. Questa azione non può essere annullata.",
"SINGULAR_DESCRIPTION": "Questo eliminerà definitivamente il contatto selezionato. Questa azione non può essere annullata.",
"CONFIRM_MULTIPLE": "Elimina contatti",
"CONFIRM_SINGLE": "Elimina contatto"
}
"SELECT_ALL": "Seleziona tutto ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -1,6 +1,6 @@
{
"CONVERSATION": {
"SELECT_A_CONVERSATION": "Seleziona una conversazione dal pannello a sinistra",
"SELECT_A_CONVERSATION": "Si prega di selezionare una conversazione dal pannello a sinistra",
"CSAT_REPLY_MESSAGE": "Valuta la conversazione",
"404": "Siamo spiacenti, non siamo riusciti a trovare la conversazione. Riprova",
"SWITCH_VIEW_LAYOUT": "Cambia layout",
@@ -6,7 +6,7 @@
"LOADING": "Caricamento notifiche",
"404": "Non ci sono notifiche attive in questo gruppo.",
"NO_NOTIFICATIONS": "Nessuna notifica",
"NOTE": "Notifiche da tutte le Inbox",
"NOTE": "Notifiche da tutte le inbox sottoscritte",
"NO_MESSAGES_AVAILABLE": "Oops! Impossibile recuperare i messaggi",
"SNOOZED_UNTIL": "Posticipata fino a",
"SNOOZED_UNTIL_TOMORROW": "Posticipata fino a domani",
@@ -45,8 +45,8 @@
"MARK_AS_UNREAD": "Segna come da leggere",
"SNOOZE": "Posticipa",
"DELETE": "Elimina",
"MARK_ALL_READ": "Segna tutte come lette",
"DELETE_ALL": "Elimina tutte",
"MARK_ALL_READ": "Segna tutto come letto",
"DELETE_ALL": "Elimina tutto",
"DELETE_ALL_READ": "Elimina tutte le lette"
},
"DISPLAY_MENU": {
@@ -252,7 +252,7 @@
},
"BUSINESS_ACCOUNT_ID": {
"LABEL": "ID Account Business",
"PLACEHOLDER": "Inserisci l'ID dell'Account Business ottenuto dalla dashboard sviluppatore di Facebook.",
"PLACEHOLDER": "Si prega di inserire l'ID dell'account business ottenuto dalla dashboard sviluppatore di Facebook.",
"ERROR": "Inserisci un valore valido."
},
"WEBHOOK_VERIFY_TOKEN": {
@@ -107,7 +107,7 @@
},
"HELP_TEXT": {
"TITLE": "Stai utilizzando l'integrazione Slack",
"BODY": "Con questa integrazione, tutte le conversazioni in arrivo verranno sincronizzate nel canale ***{selectedChannelName}*** del tuo workspace Slack. Potrai gestire tutte le conversazioni con i clienti direttamente dal canale e non perderai mai un messaggio.\n\nEcco le principali funzionalità dell’integrazione:\n\n**Rispondi alle conversazioni direttamente da Slack:** Per rispondere a una conversazione nel canale Slack ***{selectedChannelName}***, ti basta scrivere il tuo messaggio e inviarlo come thread. Questo genererà automaticamente una risposta al cliente tramite Chatwoot. Semplice, no?\n\n**Crea note private:** Se vuoi aggiungere note private invece di risposte, inizia il messaggio con ***`note:`***. In questo modo il messaggio resterà privato e non sarà visibile al cliente.\n\n**Associa un profilo operatore:** Se la persona che risponde su Slack ha un profilo operatore in app con lo stesso indirizzo email, le risposte verranno associate automaticamente a quel profilo. In questo modo potrai sapere facilmente chi ha risposto e quando. Se invece chi risponde non ha un profilo operatore associato, le risposte verranno inviate al cliente dal profilo del bot.",
"BODY": "Con questa integrazione, tutte le conversazioni in arrivo verranno sincronizzate nel canale ***{selectedChannelName}*** del tuo workspace Slack. Potrai gestire tutte le conversazioni con i clienti direttamente dal canale e non perderai mai un messaggio.\n\nEcco le principali funzionalità dell’integrazione:\n\n**Rispondi alle conversazioni direttamente da Slack:** Per rispondere a una conversazione nel canale Slack ***{selectedChannelName}***, ti basta scrivere il tuo messaggio e inviarlo come thread. Questo genererà automaticamente una risposta al cliente tramite Chatwoot. Semplice, no?\n\n**Crea note private:** Se vuoi aggiungere note private invece di risposte, inizia il messaggio con ***`note:`***. In questo modo il messaggio resterà privato e non sarà visibile al cliente.\n\n**Associa un profilo operatore:** Se la persona che risponde su Slack ha un profilo operatore in Chatwoot con lo stesso indirizzo email, le risposte verranno associate automaticamente a quel profilo. In questo modo potrai sapere facilmente chi ha risposto e quando. Se invece chi risponde non ha un profilo operatore associato, le risposte verranno inviate al cliente dal profilo del bot.",
"SELECTED": "selezionato"
},
"SELECT_CHANNEL": {
@@ -472,7 +472,7 @@
"FEATURES": {
"TITLE": "Funzionalità",
"ALLOW_CONVERSATION_FAQS": "Genera FAQ dalle conversazioni risolte",
"ALLOW_MEMORIES": "Salva memorie e dettagli chiave dalle interazioni con i clienti.",
"ALLOW_MEMORIES": "Cattura i dettagli chiave come memorie dalle interazioni con i clienti.",
"ALLOW_CITATIONS": "Includi citazioni alle fonti nelle risposte"
}
},
@@ -694,8 +694,8 @@
"HEADER": "Documents",
"ADD_NEW": "Create a new document",
"RELATED_RESPONSES": {
"TITLE": "FAQ Correlate",
"DESCRIPTION": "Queste FAQ vengono generate direttamente dai Documenti."
"TITLE": "Related FAQs",
"DESCRIPTION": "These FAQs are generated directly from the document."
},
"FORM_DESCRIPTION": "Enter the URL of the document to add it as a knowledge source and choose the assistant to associate it with.",
"CREATE": {
@@ -740,7 +740,7 @@
"ERROR_MESSAGE": "Si è verificato un errore durante l'eliminazione del documento, riprova."
},
"OPTIONS": {
"VIEW_RELATED_RESPONSES": "Visualizza FAQ Correlate",
"VIEW_RELATED_RESPONSES": "Visualizza Risposte Correlate",
"DELETE_DOCUMENT": "Elimina Documento"
},
"EMPTY_STATE": {
@@ -895,7 +895,7 @@
"FILTER": {
"ASSISTANT": "Assistente: {selected}",
"STATUS": "Stato: {selected}",
"ALL_ASSISTANTS": "Tutti"
"ALL_ASSISTANTS": "Tutte"
},
"STATUS": {
"TITLE": "Stato",
@@ -193,7 +193,7 @@
}
},
"LABEL_REPORTS": {
"HEADER": "Panoramica Etichette",
"HEADER": "Panoramica etichette",
"DESCRIPTION": "Monitora la performance delle etichette con metriche chiave, tra cui conversazioni, tempi di risposta, tempi di risoluzione e casi risolti. Fai clic sul nome di un'etichetta per approfondimenti dettagliati.",
"LOADING_CHART": "Caricamento dati grafici...",
"NO_ENOUGH_DATA": "Non ci sono abbastanza dati per generare il report, riprova più tardi.",
@@ -331,7 +331,7 @@
}
},
"TEAM_REPORTS": {
"HEADER": "Panoramica Team",
"HEADER": "Panoramica team",
"DESCRIPTION": "Ottieni un'istantanea delle prestazioni del tuo team con metriche essenziali, tra cui conversazioni, tempi di risposta, tempi di risoluzione e casi risolti. Fare clic su un nome del team per maggiori dettagli.",
"LOADING_CHART": "Caricamento dati grafici...",
"NO_ENOUGH_DATA": "Non ci sono abbastanza dati per generare il report, riprova più tardi.",
@@ -399,7 +399,7 @@
}
},
"CSAT_REPORTS": {
"HEADER": "Report CSAT",
"HEADER": "Rapporti CSAT",
"NO_RECORDS": "Non ci sono risposte ai sondaggi CSAT disponibili.",
"DOWNLOAD": "Scarica report CSAT",
"DOWNLOAD_FAILED": "Download dei report CSAT non riuscito",
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "削除",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "連絡先を削除"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "삭제",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "연락처 지우기"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Ištrinti",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Trinti kontaktą"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "Atlasīti {count}",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Dzēst",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Dzēst kontaktpersonu"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "ഇല്ലാതാക്കുക",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "കോൺടാക്റ്റ് ഇല്ലാതാക്കുക"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Padamkan",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Verwijderen",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Contactpersoon verwijderen"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Slett",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Usuń",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Usuń kontakt"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Selecionar todas ({count})",
"DELETE_CONTACTS": "Excluir",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Excluir contacto"
}
"SELECT_ALL": "Selecionar todas ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selecionado",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Selecionar todos ({count})",
"DELETE_CONTACTS": "Excluir",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Excluir contato"
}
"SELECT_ALL": "Selecionar todos ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Şterge",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Șterge contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "Выбрано {count}",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Выбрать все ({count})",
"DELETE_CONTACTS": "Удалить",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Удалить контакт"
}
"SELECT_ALL": "Выбрать все ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Vymazať",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Vymazať kontakt"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Izbriši",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Obriši kontakt"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Radera",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Ta bort kontakt"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "ลบ",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "ลบผู้ติดต่อ"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -578,19 +578,8 @@
"DESCRIPTION": "Select the labels you want to add to the selected contacts.",
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Seçimi temizle",
"SELECT_ALL": "Tümünü seç ({count})",
"DELETE_CONTACTS": "Sil",
"DELETE_SUCCESS": "Kişiler başarıyla silindi.",
"DELETE_FAILED": "Kişiler silinemedi.",
"DELETE_DIALOG": {
"TITLE": "Seçilen kişileri sil",
"SINGULAR_TITLE": "Seçilen kişiyi sil",
"DESCRIPTION": "Bu işlem, seçilen {count} kişileri kalıcı olarak silecektir. Bu eylem geri alınamaz.",
"SINGULAR_DESCRIPTION": "Bu işlem, seçilen kişiyi kalıcı olarak silecektir. Bu eylem geri alınamaz.",
"CONFIRM_MULTIPLE": "Kişileri sil",
"CONFIRM_SINGLE": "Kişiyi Sil"
}
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Tümünü seç ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -32,7 +32,7 @@
"LOADING_CONVERSATIONS": "Sohbetler Yükleniyor\n",
"CANNOT_REPLY": "Nedeniyle cevap veremezsiniz",
"24_HOURS_WINDOW": "24 saat mesaj penceresi kısıtlaması",
"API_HOURS_WINDOW": "Bu konuşmaya yalnızca {hours} saat içinde cevap verebilirsiniz",
"API_HOURS_WINDOW": "Bu sohbete yalnızca {hours} saat içinde yanıt verebilirsiniz",
"NOT_ASSIGNED_TO_YOU": "Bu görüşme size atanmamış. Bu konuşmayı kendinize atamak ister misiniz?",
"ASSIGN_TO_ME": "Bana ata",
"BOT_HANDOFF_MESSAGE": "Şu anda bir asistan veya bot tarafından yürütülen bir konuşmaya yanıt veriyorsunuz.",
@@ -42,7 +42,7 @@
"BOT_HANDOFF_ERROR": "Konuşma devralınamadı. Lütfen tekrar deneyin.",
"TWILIO_WHATSAPP_CAN_REPLY": "Bu konuşmaya yalnızca şablon mesaj kullanarak yanıt verebilirsiniz, çünkü",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 saat mesaj penceresi kısıtlaması",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "Bu Instagram hesabı, yeni Instagram kanal gelen kutusuna taşındı. Tüm yeni mesajlar orada görünecektir. Artık bu konuşmadan mesaj gönderemeyeceksiniz.",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "Bu Instagram hesabı yeni Instagram kanal gelen kutusuna taşındı. Tüm yeni mesajlar orada görünecek. Bu sohbetten artık mesaj gönderemezsiniz.",
"REPLYING_TO": "Cevap veriyorsun:",
"REMOVE_SELECTION": "Seçimi Kaldır",
"DOWNLOAD": "İndir",
@@ -139,8 +139,8 @@
}
},
"DELETE_CONVERSATION": {
"TITLE": "#{conversationId} numaralı konuşmayı sil",
"DESCRIPTION": "Bu konuşmayı silmek istediğinizden emin misiniz?",
"TITLE": "#{conversationId} numaralı sohbeti sil",
"DESCRIPTION": "Bu sohbeti silmek istediğinizden emin misiniz?",
"CONFIRM": "Sil"
},
"CARD_CONTEXT_MENU": {
@@ -159,7 +159,7 @@
"ASSIGN_LABEL": "Etiket ata",
"AGENTS_LOADING": "Temsilciler Yükleniyor...",
"ASSIGN_TEAM": "Takım ata",
"DELETE": "Konuşmayı sil",
"DELETE": "Sohbeti sil",
"OPEN_IN_NEW_TAB": "Yeni sekmede aç",
"COPY_LINK": "Konuşma bağlantısını kopyala",
"COPY_LINK_SUCCESS": "Konuşma bağlantısı panoya kopyalandı",
@@ -244,8 +244,8 @@
"ASSIGN_LABEL_SUCCESFUL": "Etiket başarıyla atandı",
"ASSIGN_LABEL_FAILED": "Etiket ataması yapılamadı",
"CHANGE_TEAM": "Takım değişti",
"SUCCESS_DELETE_CONVERSATION": "Konuşma başarıyla silindi",
"FAIL_DELETE_CONVERSATION": "Konuşma silinemedi! Tekrar deneyin",
"SUCCESS_DELETE_CONVERSATION": "Sohbet başarıyla silindi",
"FAIL_DELETE_CONVERSATION": "Sohbet silinemedi! Tekrar deneyin",
"FILE_SIZE_LIMIT": "Dosya, {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB ek dosya sınırını aşıyor",
"MESSAGE_ERROR": "Bu mesaj gönderilemiyor, lütfen daha sonra tekrar deneyin",
"SENT_BY": "Tarafından gönderildi:",
@@ -1,8 +1,8 @@
{
"GENERAL_SETTINGS": {
"LIMIT_MESSAGES": {
"CONVERSATION": "Konuşma limitini aştınız. Hacker planı yalnızca 500 konuşmaya izin vermektedir.",
"INBOXES": "Gelen kutusu limitini aştınız. Hacker planı yalnızca web sitesi canlı sohbetini desteklemektedir. E-posta, WhatsApp gibi ek gelen kutuları ücretli bir plan gerektirir.",
"CONVERSATION": "Sohbet sınırını aştınız. Hacker planı yalnızca 500 sohbete izin verir.",
"INBOXES": "Gelen kutusu sınırını aştınız. Hacker planı yalnızca web sitesi canlı sohbetini destekler. E-posta, WhatsApp gibi ek gelen kutuları ücretli plan gerektirir.",
"AGENTS": "Temsilci sınırını aştınız. Planınız yalnızca {allowedAgents} temsilciye izin veriyor.",
"NON_ADMIN": "Tüm özellikleri kullanmaya devam etmek için lütfen yöneticinizle iletişime geçin ve planı yükseltin."
},
@@ -20,10 +20,10 @@
"BUTTON_TEXT": "Hesabınızı Silin",
"CONFIRM": {
"TITLE": "Hesabı Sil",
"MESSAGE": "Hesabınızı silmek geri alınamaz bir işlemdir. Kalıcı olarak silmek istediğinizi onaylamak için aşağıya hesap adınızı girin.",
"MESSAGE": "Hesabınızı silmek geri alınamaz. Kalıcı olarak silmek istediğinizi onaylamak için aşağıya hesap adınızı girin.",
"BUTTON_TEXT": "Sil",
"DISMISS": "İptal Et",
"PLACE_HOLDER": "Onaylamak için lütfen {accountName} yazın"
"PLACE_HOLDER": "{accountName} yazarak onaylayın"
},
"SUCCESS": "Hesap silinmek üzere işaretlendi",
"FAILURE": "Hesap silinemedi, tekrar deneyin!",
@@ -45,11 +45,11 @@
"NOTE": "API tabanlı bir entegrasyon oluşturuyorsanız bu kimlik gereklidir"
},
"AUTO_RESOLVE": {
"TITLE": "Konuşmaları Otomatik Çöz",
"NOTE": "Bu yapılandırma, belirli bir hareketsizlik süresinden sonra konuşmayı otomatik olarak çözümlemenize olanak tanır.",
"TITLE": "Sohbetleri otomatik çöz",
"NOTE": "Bu yapılandırma, belirli bir süre etkinlik olmadığında sohbeti otomatik olarak çözmenizi sağlar.",
"DURATION": {
"LABEL": "Hareketsizlik süresi",
"HELP": "Konuşmanın otomatik olarak çözümlenmesinden önceki hareketsizlik süresi",
"LABEL": "Etkinliksizlik süresi",
"HELP": "Sohbetin otomatik olarak çözüleceği etkinliksizlik süresi",
"PLACEHOLDER": "30",
"ERROR": "Otomatik çözme süresi 10 dakika ile 999 gün arasında olmalıdır",
"API": {
@@ -59,8 +59,8 @@
},
"MESSAGE": {
"LABEL": "Özel otomatik çözüm mesajı",
"PLACEHOLDER": "Konuşma, 15 günlük hareketsizlik nedeniyle sistem tarafından çözümlendi olarak işaretlendi",
"HELP": "Konuşma otomatik olarak çözüldükten sonra müşteriye gönderilen mesaj"
"PLACEHOLDER": "Sohbet, 15 gün etkinlik olmadığı için sistem tarafından çözüldü olarak işaretlendi",
"HELP": "Sohbet otomatik olarak çözüldükten sonra müşteriye gönderilen mesaj"
},
"PREFERENCES": "Tercihler",
"LABEL": {
@@ -94,19 +94,19 @@
},
"AUTO_RESOLVE_IGNORE_WAITING": {
"LABEL": "Yanıtsız sohbetleri hariç tut",
"HELP": "Etkinleştirildiğinde, sistem hâlâ bir temsilcinin yanıtını bekleyen konuşmaları çözümlemeyi atlayacaktır."
"HELP": "Etkinleştirildiğinde, sistem hâlâ temsilci yanıtı bekleyen sohbetleri çözmeyi atlayacaktır."
},
"AUDIO_TRANSCRIPTION": {
"TITLE": "Sesli Mesajları Metne Çevir",
"NOTE": "Konuşmalardaki sesli mesajları otomatik olarak metne çevirin. Bir sesli mesaj gönderildiğinde veya alındığında bir metin dökümü (transkript) oluşturun ve mesajın yanında görüntüleyin.",
"TITLE": "Sesli Mesajları Yazıya Döktür",
"NOTE": "Sohbetlerdeki sesli mesajları otomatik olarak yazıya dökün. Bir sesli mesaj gönderildiğinde veya alındığında bir metin dökümü oluşturun ve mesajın yanında gösterin.",
"API": {
"SUCCESS": "Sesli mesaj transkripsiyon ayarı başarıyla güncellendi",
"ERROR": "Ses transkripsiyon ayarı güncellenirken hata oluştu"
"ERROR": "Sesli yazıya dökme ayarı güncellenemedi"
}
},
"AUTO_RESOLVE_DURATION": {
"LABEL": "Çözümleme için hareketsizlik süresi",
"HELP": "Bir konuşmanın, hiçbir etkinlik olmazsa otomatik olarak çözümlenmesi gereken süre",
"LABEL": "Çözüm için etkinliksizlik süresi",
"HELP": "Bir sohbette etkinlik yoksa otomatik çözüm için süre",
"PLACEHOLDER": "30",
"ERROR": "Otomatik çözme süresi 10 dakika ile 999 gün arasında olmalıdır",
"API": {
@@ -115,8 +115,8 @@
},
"UPDATE_BUTTON": "Güncelleme",
"MESSAGE_LABEL": "Özel çözüm mesajı",
"MESSAGE_PLACEHOLDER": "Konuşma, 15 günlük hareketsizlik nedeniyle sistem tarafından çözümlendi olarak işaretlendi",
"MESSAGE_HELP": "Bu mesaj, bir konuşma hareketsizlik nedeniyle sistem tarafından otomatik olarak çözümlendiğinde müşteriye gönderilir."
"MESSAGE_PLACEHOLDER": "Sohbet, 15 gün etkinlik olmadığı için sistem tarafından çözüldü olarak işaretlendi",
"MESSAGE_HELP": "Bu mesaj, bir sohbet sistem tarafından etkinliksizlik nedeniyle otomatik olarak çözüldüğünde müşteriye gönderilir."
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Hesabınız için e-posta ile iletişim devre dışı bırakıldı.",
@@ -51,10 +51,10 @@
"INSTAGRAM": {
"CONTINUE_WITH_INSTAGRAM": "Instagram ile devam et",
"CONNECT_YOUR_INSTAGRAM_PROFILE": "Instagram profilinizi bağlayın",
"HELP": "Instagram profilinizi kanal olarak eklemek için, 'Instagram ile Devam Et' seçeneğine tıklayarak Instagram Profilinizin kimliğini doğrulamanız gerekmektedir. ",
"HELP": "Bu Instagram hesabı yeni Instagram kanal gelen kutusuna taşındı. Artık bu gelen kutusundan Instagram mesajı gönderip alamayacaksınız ",
"ERROR_MESSAGE": "Instagram'a bağlanırken bir hata oluştu, lütfen tekrar deneyin",
"ERROR_AUTH": "Instagram'a bağlanırken bir hata oluştu, lütfen tekrar deneyin",
"NEW_INBOX_SUGGESTION": "Bu Instagram hesabı daha önce farklı bir gelen kutusuna bağlıydı ve şimdi buraya taşındı. Tüm yeni mesajlar burada görünecektir. Eski gelen kutusu, bu hesap için artık mesaj gönderemeyecek veya alamayacaktır.",
"NEW_INBOX_SUGGESTION": "Bu Instagram hesabı daha önce farklı bir gelen kutusuna bağlıydı ve şimdi buraya taşındı. Tüm yeni mesajlar burada görünecek. Eski gelen kutusu artık bu hesap için mesaj gönderip alamayacak.",
"DUPLICATE_INBOX_BANNER": "Bu Instagram hesabı yeni Instagram kanal gelen kutusuna taşındı. Artık bu gelen kutusundan Instagram mesajı gönderip alamayacaksınız."
},
"TWITTER": {
@@ -341,9 +341,9 @@
"TWILIO_STATUS_URL_TITLE": "Twilio Status Callback URL",
"TWILIO_STATUS_URL_SUBTITLE": "Configure this URL as the Status Callback URL on your Twilio phone number."
},
"SUBMIT_BUTTON": "Ses Kanalı Oluştur",
"SUBMIT_BUTTON": "Create Voice Channel",
"API": {
"ERROR_MESSAGE": "Ses kanalı oluşturulamadı"
"ERROR_MESSAGE": "We were not able to create the voice channel"
}
},
"API_CHANNEL": {
@@ -437,7 +437,7 @@
},
"FACEBOOK": {
"TITLE": "Facebook\n",
"DESCRIPTION": "Facebook sayfanızı bağlayın"
"DESCRIPTION": "Connect your Facebook page"
},
"WHATSAPP": {
"TITLE": "WhatsApp",
@@ -445,31 +445,31 @@
},
"EMAIL": {
"TITLE": "E-Posta",
"DESCRIPTION": "Gmail, Outlook veya diğer sağlayıcılarla bağlantı kurun"
"DESCRIPTION": "Connect with Gmail, Outlook, or other providers"
},
"SMS": {
"TITLE": "SMS",
"DESCRIPTION": "SMS kanalını Twilio veya Bandwidth ile entegre edin"
"DESCRIPTION": "Integrate SMS channel with Twilio or bandwidth"
},
"API": {
"TITLE": "API",
"DESCRIPTION": "API'mizi kullanarak özel bir kanal oluşturun"
"DESCRIPTION": "Make a custom channel using our API"
},
"TELEGRAM": {
"TITLE": "Telegram",
"DESCRIPTION": "Bot token kullanarak Telegram kanalını yapılandırın"
"DESCRIPTION": "Configure Telegram channel using Bot token"
},
"LINE": {
"TITLE": "Line",
"DESCRIPTION": "Line kanalınızı entegre edin"
"DESCRIPTION": "Integrate your Line channel"
},
"INSTAGRAM": {
"TITLE": "Instagram",
"DESCRIPTION": "Instagram hesabınızı bağlayın"
"DESCRIPTION": "Connect your instagram account"
},
"VOICE": {
"TITLE": "Ses",
"DESCRIPTION": "Twilio Voice ile entegre edin"
"DESCRIPTION": "Integrate with Twilio Voice"
}
}
},
@@ -526,9 +526,9 @@
"BUTTON_TEXT": "Beni oraya götür",
"MORE_SETTINGS": "Daha fazla ayar",
"WEBSITE_SUCCESS": "Bir web sitesi kanalı oluşturmayı başarıyla tamamladınız. Aşağıda gösterilen kodu kopyalayın ve web sitenize yapıştırın. Bir müşteri canlı sohbeti bir dahaki sefere kullandığında, konuşma otomatik olarak gelen kutunuzda görünecektir.",
"WHATSAPP_QR_INSTRUCTION": "WhatsApp gelen kutunuzu hızlıca test etmek için yukarıdaki QR kodu tarayın",
"MESSENGER_QR_INSTRUCTION": "Facebook Messenger gelen kutunuzu hızlıca test etmek için yukarıdaki QR kodu tarayın",
"TELEGRAM_QR_INSTRUCTION": "Telegram gelen kutunuzu hızlıca test etmek için yukarıdaki QR kodu tarayın"
"WHATSAPP_QR_INSTRUCTION": "Scan the QR code above to quickly test your WhatsApp inbox",
"MESSENGER_QR_INSTRUCTION": "Scan the QR code above to quickly test your Facebook Messenger inbox",
"TELEGRAM_QR_INSTRUCTION": "Scan the QR code above to quickly test your Telegram inbox"
},
"REAUTH": "Yeniden yetkilendir",
"VIEW": "Görünüm",
@@ -607,37 +607,37 @@
"BUSINESS_HOURS": "İş Saatleri",
"WIDGET_BUILDER": "Widget Oluşturucu",
"BOT_CONFIGURATION": "Bot Yapılandırma",
"ACCOUNT_HEALTH": "Hesap Sağlığı",
"ACCOUNT_HEALTH": "Account Health",
"CSAT": "CSAT"
},
"ACCOUNT_HEALTH": {
"TITLE": "WhatsApp hesabınızı yönetin",
"DESCRIPTION": "WhatsApp hesap durumunuzu, mesajlaşma limitlerinizi ve kalitesini inceleyin. Gerekirse ayarları güncelleyin veya sorunları çözün",
"GO_TO_SETTINGS": "Meta Business Manager'a gidin",
"NO_DATA": "Sağlık verisi mevcut değil",
"TITLE": "Manage your WhatsApp account",
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
"GO_TO_SETTINGS": "Go to Meta Business Manager",
"NO_DATA": "Health data is not available",
"FIELDS": {
"DISPLAY_PHONE_NUMBER": {
"LABEL": "Telefon numarasını göster",
"TOOLTIP": "Müşterilere gösterilen telefon numarası"
"LABEL": "Display phone number",
"TOOLTIP": "Phone number displayed to customers"
},
"VERIFIED_NAME": {
"LABEL": "İşletme adı",
"TOOLTIP": "WhatsApp tarafından onaylanmış işletme adı"
"LABEL": "Business name",
"TOOLTIP": "Business name verified by WhatsApp"
},
"DISPLAY_NAME_STATUS": {
"LABEL": "Görünen adın durumu",
"TOOLTIP": "İşletme adı doğrulamanızın durumu"
"LABEL": "Display name status",
"TOOLTIP": "Status of your business name verification"
},
"QUALITY_RATING": {
"LABEL": "Kalite puanı",
"TOOLTIP": "Hesabınız için WhatsApp kalite puanı"
"LABEL": "Quality rating",
"TOOLTIP": "WhatsApp quality rating for your account"
},
"MESSAGING_LIMIT_TIER": {
"LABEL": "Mesajlaşma limiti seviyesi",
"TOOLTIP": "Hesabınız için günlük mesajlaşma limiti"
"LABEL": "Messaging limit tier",
"TOOLTIP": "Daily messaging limit for your account"
},
"ACCOUNT_MODE": {
"LABEL": "Hesap modu",
"LABEL": "Account mode",
"TOOLTIP": "Current operating mode of your WhatsApp account"
}
},
@@ -784,7 +784,7 @@
},
"CSAT": {
"TITLE": "CSAT'yi etkinleştir",
"SUBTITLE": "Müşterilerin destek deneyimleri hakkındaki düşüncelerini anlamak için konuşma sonunda CSAT anketlerini (Müşteri Memnuniyeti Anketleri) otomatik olarak tetikleyin. Memnuniyet eğilimlerini takip edin ve zaman içinde iyileştirme alanlarını belirleyin.",
"SUBTITLE": "Müşterilerin destek deneyimleri hakkında ne hissettiklerini anlamak için sohbetlerin sonunda otomatik olarak CSAT anketleri başlatın. Memnuniyet eğilimlerini takip edin ve zaman içinde iyileştirme alanlarını belirleyin.",
"DISPLAY_TYPE": {
"LABEL": "Görüntüleme türü"
},
@@ -802,7 +802,7 @@
},
"SELECT_PLACEHOLDER": "etiketleri seç"
},
"NOTE": "Not: CSAT anketleri, konuşma başına yalnızca bir kez gönderilir",
"NOTE": "Not: CSAT anketleri her sohbet için yalnızca bir kez gönderilir",
"API": {
"SUCCESS_MESSAGE": "CSAT ayarları başarıyla güncellendi",
"ERROR_MESSAGE": "CSAT ayarları güncellenemedi. Lütfen daha sonra tekrar deneyin."
@@ -34,7 +34,7 @@
"SUBSCRIPTIONS": {
"LABEL": "Olaylar",
"EVENTS": {
"CONVERSATION_CREATED": "Konuşma Oluşturuldu",
"CONVERSATION_CREATED": "Görüşme Oluşturuldu",
"CONVERSATION_STATUS_CHANGED": "Görüşme Durumu Değişti",
"CONVERSATION_UPDATED": "Görüşme Güncellendi",
"MESSAGE_CREATED": "Mesaj Oluşturuldu",
@@ -42,8 +42,8 @@
"WEBWIDGET_TRIGGERED": "Kullanıcı tarafından canlı sohbet widget'ı açıldı",
"CONTACT_CREATED": "Kişi Oluşturuldu",
"CONTACT_UPDATED": "Kişi Güncellendi",
"CONVERSATION_TYPING_ON": "Konuşmada Yazıyor Açık",
"CONVERSATION_TYPING_OFF": "Konuşmada Yazıyor Kapalı"
"CONVERSATION_TYPING_ON": "Sohbet Yazıyor Açık",
"CONVERSATION_TYPING_OFF": "Sohbet Yazıyor Kapalı"
}
},
"END_POINT": {
@@ -327,7 +327,7 @@
},
"CTA": {
"TITLE": "Linear'a bağlan",
"AGENT_DESCRIPTION": "Linear çalışma alanı bağlı değil. Bu entegrasyonu kullanmak için yöneticinizden bir çalışma alanı bağlamasını isteyin.",
"AGENT_DESCRIPTION": "Doğrusal çalışma alanı bağlı değil. Bu entegrasyonu kullanmak için yöneticinizden bir çalışma alanı bağlamasını isteyin.",
"DESCRIPTION": "Linear çalışma alanı bağlı değil. Bu entegrasyonu kullanmak için çalışma alanınızı bağlamak üzere aşağıdaki düğmeye tıklayın.",
"BUTTON_TEXT": "Linear çalışma alanını bağlayın"
}
@@ -348,7 +348,7 @@
"TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts",
"PANEL_TITLE": "Copilot ile başlayın",
"KICK_OFF_MESSAGE": "Hızlı bir özet mi gerekiyor, geçmiş konuşmaları mı kontrol etmek istiyorsunuz, yoksa daha iyi bir yanıt mı tasarlamak istiyorsunuz? Copilot işleri hızlandırmak için burada.",
"KICK_OFF_MESSAGE": "Hızlı bir özet mi lazım, geçmiş sohbetleri mi kontrol etmek istiyorsunuz ya da daha iyi bir yanıt mı yazmak istiyorsunuz? Copilot işleri hızlandırmak için burada.",
"SEND_MESSAGE": "Mesajı Gönder...",
"EMPTY_MESSAGE": "Yanıt oluşturulurken bir hata oluştu. Lütfen tekrar deneyin.",
"LOADER": "Captain is thinking",
@@ -371,8 +371,8 @@
"CONTENT": "Sohbetin müşterinin ihtiyaçlarını ne kadar karşıladığını gözden geçirin. Ton, açıklık ve etkililik açısından 5 üzerinden bir puan verin."
},
"HIGH_PRIORITY": {
"LABEL": "Yüksek öncelikli konuşmalar",
"CONTENT": "Tüm yüksek öncelikli açık konuşmaların bir özetini verin. Konuşma kimliği, müşteri adı (varsa), son mesaj içeriği ve atanan temsilciyi ekleyin. Gerekirse duruma göre gruplayın."
"LABEL": "Yüksek öncelikli sohbetler",
"CONTENT": "Tüm yüksek öncelikli açık sohbetlerin bir özetini verin. Sohbet kimliği, müşteri adı (varsa), son mesaj içeriği ve atanan temsilciyi ekleyin. Gerekirse duruma göre gruplayın."
},
"LIST_CONTACTS": {
"LABEL": "Kişileri listele",
@@ -383,7 +383,7 @@
"PLAYGROUND": {
"USER": "Sen",
"ASSISTANT": "Assistant",
"MESSAGE_PLACEHOLDER": "Mesajınızı yazın...",
"MESSAGE_PLACEHOLDER": "Mesajınız...",
"HEADER": "Oyun Alanı",
"DESCRIPTION": "Bu oyun alanını asistanınıza mesaj göndermek ve yanıtlarının doğru, hızlı ve beklediğiniz tonda olup olmadığını kontrol etmek için kullanın.",
"CREDIT_NOTE": "Buradan gönderilen mesajlar Captain kredilerinize sayılacaktır."
@@ -194,7 +194,7 @@
},
"LABEL_REPORTS": {
"HEADER": "Etiketler Genel Bakış",
"DESCRIPTION": "Etiket performansını; konuşmalar, yanıt süreleri, çözüm süreleri ve çözümlenen vakalar dahil olmak üzere temel metriklerle takip edin. Ayrıntılı analizler için bir etiket adına tıklayın.",
"DESCRIPTION": "Konuşmalar, yanıt süreleri, çözüm süreleri ve çözülmüş vakalar gibi temel ölçümlerle etiket performansını takip edin. Ayrıntılı içgörüler için bir etiket adına tıklayın.",
"LOADING_CHART": "Grafik verileri yükleniyor...",
"NO_ENOUGH_DATA": "Rapor oluşturmak için yeterli veri yok, Lütfen daha sonra tekrar deneyin.",
"DOWNLOAD_LABEL_REPORTS": "Etiket raporlarını indir",
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Видалити",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Видалити контакт"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "حذف کریں۔",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "کانٹیکٹ حذف کریں۔"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "Xoá",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Xoá liên hệ"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} 已选择",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "全选 ({count})",
"DELETE_CONTACTS": "删除",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "删除联系人"
}
"SELECT_ALL": "全选 ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})",
"DELETE_CONTACTS": "刪除",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "刪除聯絡人"
}
"SELECT_ALL": "Select all ({count})"
},
"COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": {
@@ -0,0 +1,86 @@
<script setup>
import { computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useStore } from 'dashboard/composables/store';
import { useMapGetter } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import EditAssistantForm from '../../../../components-next/captain/pageComponents/assistant/EditAssistantForm.vue';
import AssistantPlayground from 'dashboard/components-next/captain/assistant/AssistantPlayground.vue';
import AssistantSettings from 'dashboard/routes/dashboard/captain/assistants/settings/Settings.vue';
const route = useRoute();
const store = useStore();
const { t } = useI18n();
const assistantId = route.params.assistantId;
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
const isFetching = computed(() => uiFlags.value.fetchingItem);
const assistant = computed(() =>
store.getters['captainAssistants/getRecord'](Number(assistantId))
);
const isFeatureEnabledonAccount = useMapGetter(
'accounts/isFeatureEnabledonAccount'
);
const currentAccountId = useMapGetter('getCurrentAccountId');
const isCaptainV2Enabled = isFeatureEnabledonAccount.value(
currentAccountId.value,
FEATURE_FLAGS.CAPTAIN_V2
);
const isAssistantAvailable = computed(() => !!assistant.value?.id);
const handleSubmit = async updatedAssistant => {
try {
await store.dispatch('captainAssistants/update', {
id: assistantId,
...updatedAssistant,
});
useAlert(t('CAPTAIN.ASSISTANTS.EDIT.SUCCESS_MESSAGE'));
} catch (error) {
const errorMessage =
error?.message || t('CAPTAIN.ASSISTANTS.EDIT.ERROR_MESSAGE');
useAlert(errorMessage);
}
};
onMounted(() => {
if (!isAssistantAvailable.value || !isCaptainV2Enabled) {
store.dispatch('captainAssistants/show', assistantId);
}
});
</script>
<template>
<AssistantSettings v-if="isCaptainV2Enabled" />
<PageLayout
v-else
:header-title="assistant?.name"
:show-pagination-footer="false"
:is-fetching="isFetching"
:show-know-more="false"
:back-url="{ name: 'captain_assistants_index' }"
>
<template #body>
<div v-if="!isAssistantAvailable">
{{ t('CAPTAIN.ASSISTANTS.EDIT.NOT_FOUND') }}
</div>
<div v-else class="flex gap-4 h-full">
<div class="flex-1 lg:overflow-auto pr-4 h-full md:h-auto">
<EditAssistantForm
:assistant="assistant"
mode="edit"
@submit="handleSubmit"
/>
</div>
<div class="w-[400px] hidden lg:block h-full">
<AssistantPlayground :assistant-id="Number(assistantId)" />
</div>
</div>
</template>
</PageLayout>
</template>
@@ -1,57 +1,91 @@
<script setup>
import { computed, ref, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { useMapGetter } from 'dashboard/composables/store';
import { computed, onMounted, ref, nextTick } from 'vue';
import { useMapGetter, useStore } from 'dashboard/composables/store';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { useRouter } from 'vue-router';
import { useAccount } from 'dashboard/composables/useAccount';
import AssistantCard from 'dashboard/components-next/captain/assistant/AssistantCard.vue';
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
import CreateAssistantDialog from 'dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue';
import AssistantPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue';
import FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue';
import LimitBanner from 'dashboard/components-next/captain/pageComponents/response/LimitBanner.vue';
const { isOnChatwootCloud } = useAccount();
const router = useRouter();
const store = useStore();
const dialogType = ref('');
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
const assistants = useMapGetter('captainAssistants/getRecords');
const isFetching = computed(() => uiFlags.value.fetchingList);
const selectedAssistant = ref(null);
const deleteAssistantDialog = ref(null);
const handleDelete = () => {
deleteAssistantDialog.value.dialogRef.open();
};
const createAssistantDialog = ref(null);
const router = useRouter();
const handleCreate = () => {
dialogType.value = 'create';
nextTick(() => createAssistantDialog.value.dialogRef.open());
};
const handleEdit = () => {
router.push({
name: 'captain_assistants_edit',
params: { assistantId: selectedAssistant.value.id },
});
};
const handleViewConnectedInboxes = () => {
router.push({
name: 'captain_assistants_inboxes_index',
params: { assistantId: selectedAssistant.value.id },
});
};
const handleAction = ({ action, id }) => {
selectedAssistant.value = assistants.value.find(
assistant => id === assistant.id
);
nextTick(() => {
if (action === 'delete') {
handleDelete();
}
if (action === 'edit') {
handleEdit();
}
if (action === 'viewConnectedInboxes') {
handleViewConnectedInboxes();
}
});
};
const handleCreateClose = () => {
dialogType.value = '';
selectedAssistant.value = null;
};
const handleAfterCreate = newAssistant => {
// Navigate directly to documents page with the new assistant ID
if (newAssistant?.id) {
router.push({
name: 'captain_assistants_responses_index',
params: {
accountId: router.currentRoute.value.params.accountId,
assistantId: newAssistant.id,
},
});
}
};
onMounted(() => store.dispatch('captainAssistants/get'));
</script>
<template>
<PageLayout
:header-title="$t('CAPTAIN.ASSISTANTS.HEADER')"
:button-label="$t('CAPTAIN.ASSISTANTS.ADD_NEW')"
:button-policy="['administrator']"
:show-pagination-footer="false"
:is-fetching="isFetching"
:is-empty="!assistants.length"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
is-empty
@click="handleCreate"
>
<template #knowMore>
@@ -73,13 +107,36 @@ const handleAfterCreate = newAssistant => {
<CaptainPaywall />
</template>
<template #body>
<LimitBanner class="mb-5" />
<div class="flex flex-col gap-4">
<AssistantCard
v-for="assistant in assistants"
:id="assistant.id"
:key="assistant.id"
:name="assistant.name"
:description="assistant.description"
:updated-at="assistant.updated_at || assistant.created_at"
:created-at="assistant.created_at"
@action="handleAction"
/>
</div>
</template>
<DeleteDialog
v-if="selectedAssistant"
ref="deleteAssistantDialog"
:entity="selectedAssistant"
type="Assistants"
/>
<CreateAssistantDialog
v-if="dialogType"
ref="createAssistantDialog"
:type="dialogType"
:selected-assistant="selectedAssistant"
@close="handleCreateClose"
@created="handleAfterCreate"
/>
</PageLayout>
</template>
@@ -10,7 +10,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
import Input from 'dashboard/components-next/input/Input.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
import SuggestedRules from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
import AddNewRulesInput from 'dashboard/components-next/captain/assistant/AddNewRulesInput.vue';
@@ -23,26 +23,29 @@ const route = useRoute();
const store = useStore();
const { uiSettings, updateUISettings } = useUISettings();
const assistantId = route.params.assistantId;
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
const assistantId = computed(() => Number(route.params.assistantId));
const isFetching = computed(() => uiFlags.value.fetchingItem);
const assistant = computed(() =>
store.getters['captainAssistants/getRecord'](assistantId.value)
store.getters['captainAssistants/getRecord'](Number(assistantId))
);
const searchQuery = ref('');
const newInlineRule = ref('');
const newDialogRule = ref('');
const guardrailsContent = computed(() => assistant.value?.guardrails || []);
const breadcrumbItems = computed(() => {
return [
{
label: t('CAPTAIN.ASSISTANTS.SETTINGS.BREADCRUMB.ASSISTANT'),
routeName: 'captain_assistants_index',
},
{ label: assistant.value?.name, routeName: 'captain_assistants_edit' },
{ label: t('CAPTAIN.ASSISTANTS.GUARDRAILS.BREADCRUMB.TITLE') },
];
});
const backUrl = computed(() => ({
name: 'captain_assistants_settings_index',
params: {
accountId: route.params.accountId,
assistantId: assistantId.value,
},
}));
const guardrailsContent = computed(() => assistant.value?.guardrails || []);
const displayGuardrails = computed(() =>
guardrailsContent.value.map((c, idx) => ({ id: idx, content: c }))
@@ -110,7 +113,7 @@ const selectedCountLabel = computed(() => {
const saveGuardrails = async list => {
await store.dispatch('captainAssistants/update', {
id: assistantId.value,
id: assistantId,
assistant: { guardrails: list },
});
};
@@ -173,13 +176,9 @@ const addAllExample = () => {
</script>
<template>
<PageLayout
:header-title="$t('CAPTAIN.ASSISTANTS.GUARDRAILS.TITLE')"
<SettingsPageLayout
:breadcrumb-items="breadcrumbItems"
:is-fetching="isFetching"
:back-url="backUrl"
:show-know-more="false"
:show-pagination-footer="false"
:show-assistant-switcher="false"
>
<template #body>
<SettingsHeader
@@ -298,5 +297,5 @@ const addAllExample = () => {
/>
</div>
</template>
</PageLayout>
</SettingsPageLayout>
</template>
@@ -10,7 +10,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
import Input from 'dashboard/components-next/input/Input.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
import SuggestedRules from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
import AddNewRulesInput from 'dashboard/components-next/captain/assistant/AddNewRulesInput.vue';
@@ -23,29 +23,32 @@ const route = useRoute();
const store = useStore();
const { uiSettings, updateUISettings } = useUISettings();
const assistantId = route.params.assistantId;
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
const assistantId = computed(() => Number(route.params.assistantId));
const isFetching = computed(() => uiFlags.value.fetchingItem);
const assistant = computed(() =>
store.getters['captainAssistants/getRecord'](assistantId.value)
store.getters['captainAssistants/getRecord'](Number(assistantId))
);
const searchQuery = ref('');
const newInlineRule = ref('');
const newDialogRule = ref('');
const breadcrumbItems = computed(() => {
return [
{
label: t('CAPTAIN.ASSISTANTS.SETTINGS.BREADCRUMB.ASSISTANT'),
routeName: 'captain_assistants_index',
},
{ label: assistant.value?.name, routeName: 'captain_assistants_edit' },
{ label: t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.TITLE') },
];
});
const guidelinesContent = computed(
() => assistant.value?.response_guidelines || []
);
const backUrl = computed(() => ({
name: 'captain_assistants_settings_index',
params: {
accountId: route.params.accountId,
assistantId: assistantId.value,
},
}));
const displayGuidelines = computed(() =>
guidelinesContent.value.map((c, idx) => ({ id: idx, content: c }))
);
@@ -116,7 +119,7 @@ const selectedCountLabel = computed(() => {
const saveGuidelines = async list => {
await store.dispatch('captainAssistants/update', {
id: assistantId.value,
id: assistantId,
assistant: { response_guidelines: list },
});
};
@@ -180,13 +183,9 @@ const addAllExample = async () => {
</script>
<template>
<PageLayout
:header-title="$t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.TITLE')"
<SettingsPageLayout
:breadcrumb-items="breadcrumbItems"
:is-fetching="isFetching"
:back-url="backUrl"
:show-know-more="false"
:show-pagination-footer="false"
:show-assistant-switcher="false"
>
<template #body>
<SettingsHeader
@@ -322,5 +321,5 @@ const addAllExample = async () => {
/>
</div>
</template>
</PageLayout>
</SettingsPageLayout>
</template>
@@ -1,9 +1,14 @@
<script setup>
import { computed, onMounted, ref, nextTick } from 'vue';
import { useMapGetter, useStore } from 'dashboard/composables/store';
import { computed, onBeforeMount, onMounted, ref, nextTick } from 'vue';
import {
useMapGetter,
useStore,
useStoreGetters,
} from 'dashboard/composables/store';
import { useRoute } from 'vue-router';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import BackButton from 'dashboard/components/widgets/BackButton.vue';
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import ConnectInboxDialog from 'dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue';
@@ -47,7 +52,13 @@ const handleCreateClose = () => {
selectedInbox.value = null;
};
const getters = useStoreGetters();
const assistantId = Number(route.params.assistantId);
const assistant = computed(() =>
getters['captainAssistants/getRecord'].value(assistantId)
);
onBeforeMount(() => store.dispatch('captainAssistants/show', assistantId));
onMounted(() =>
store.dispatch('captainInboxes/get', {
assistantId: assistantId,
@@ -57,16 +68,27 @@ onMounted(() =>
<template>
<PageLayout
:header-title="$t('CAPTAIN.INBOXES.HEADER')"
:button-label="$t('CAPTAIN.INBOXES.ADD_NEW')"
:button-policy="['administrator']"
:is-fetching="isFetchingAssistant || isFetching"
:is-empty="!captainInboxes.length"
:show-pagination-footer="false"
:show-know-more="false"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
@click="handleCreate"
>
<template v-if="!isFetchingAssistant" #headerTitle>
<div class="flex flex-row items-center gap-4">
<BackButton compact />
<span
class="flex items-center gap-1 text-lg font-medium text-n-slate-12"
>
{{ assistant.name }}
<span class="i-lucide-chevron-right text-xl text-n-slate-10" />
{{ $t('CAPTAIN.INBOXES.HEADER') }}
</span>
</div>
</template>
<template #emptyState>
<InboxPageEmptyState @click="handleCreate" />
</template>
@@ -1,24 +0,0 @@
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import AssistantPlayground from 'dashboard/components-next/captain/assistant/AssistantPlayground.vue';
const route = useRoute();
const assistantId = computed(() => Number(route.params.assistantId));
</script>
<template>
<PageLayout
:header-title="$t('CAPTAIN.PLAYGROUND.HEADER')"
show-assistant-switcher
:show-pagination-footer="false"
class="h-full"
>
<template #body>
<div class="flex flex-col h-full">
<AssistantPlayground :assistant-id="assistantId" />
</div>
</template>
</PageLayout>
</template>
@@ -10,7 +10,7 @@ import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import Button from 'dashboard/components-next/button/Button.vue';
import Input from 'dashboard/components-next/input/Input.vue';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
import SuggestedScenarios from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
import ScenariosCard from 'dashboard/components-next/captain/assistant/ScenariosCard.vue';
@@ -22,14 +22,28 @@ const route = useRoute();
const store = useStore();
const { uiSettings, updateUISettings } = useUISettings();
const { formatMessage } = useMessageFormatter();
const assistantId = computed(() => Number(route.params.assistantId));
const assistantId = route.params.assistantId;
const uiFlags = useMapGetter('captainScenarios/getUIFlags');
const isFetching = computed(() => uiFlags.value.fetchingList);
const assistant = computed(() =>
store.getters['captainAssistants/getRecord'](Number(assistantId))
);
const scenarios = useMapGetter('captainScenarios/getRecords');
const searchQuery = ref('');
const breadcrumbItems = computed(() => {
return [
{
label: t('CAPTAIN.ASSISTANTS.SETTINGS.BREADCRUMB.ASSISTANT'),
routeName: 'captain_assistants_index',
},
{ label: assistant.value?.name, routeName: 'captain_assistants_edit' },
{ label: t('CAPTAIN.ASSISTANTS.SCENARIOS.BREADCRUMB.TITLE') },
];
});
const LINK_INSTRUCTION_CLASS =
'[&_a[href^="tool://"]]:text-n-iris-11 [&_a:not([href^="tool://"])]:text-n-slate-12 [&_a]:pointer-events-none [&_a]:cursor-default';
@@ -105,7 +119,7 @@ const updateScenario = async scenario => {
try {
await store.dispatch('captainScenarios/update', {
id: scenario.id,
assistantId: assistantId.value,
assistantId: route.params.assistantId,
...scenario,
tools: getToolsFromInstruction(scenario.instruction),
});
@@ -122,7 +136,7 @@ const deleteScenario = async id => {
try {
await store.dispatch('captainScenarios/delete', {
id,
assistantId: assistantId.value,
assistantId: route.params.assistantId,
});
useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.DELETE.SUCCESS'));
} catch (error) {
@@ -140,7 +154,7 @@ const bulkDeleteScenarios = async ids => {
idsArray.map(id =>
store.dispatch('captainScenarios/delete', {
id,
assistantId: assistantId.value,
assistantId: route.params.assistantId,
})
)
);
@@ -151,7 +165,7 @@ const bulkDeleteScenarios = async ids => {
const addScenario = async scenario => {
try {
await store.dispatch('captainScenarios/create', {
assistantId: assistantId.value,
assistantId: route.params.assistantId,
...scenario,
tools: getToolsFromInstruction(scenario.instruction),
});
@@ -168,7 +182,7 @@ const addAllExampleScenarios = async () => {
try {
scenariosExample.forEach(async scenario => {
await store.dispatch('captainScenarios/create', {
assistantId: assistantId.value,
assistantId: route.params.assistantId,
...scenario,
});
});
@@ -183,18 +197,16 @@ const addAllExampleScenarios = async () => {
onMounted(() => {
store.dispatch('captainScenarios/get', {
assistantId: assistantId.value,
assistantId: assistantId,
});
store.dispatch('captainTools/getTools');
});
</script>
<template>
<PageLayout
:header-title="$t('CAPTAIN.DOCUMENTS.HEADER')"
<SettingsPageLayout
:breadcrumb-items="breadcrumbItems"
:is-fetching="isFetching"
:show-know-more="false"
:show-pagination-footer="false"
>
<template #body>
<SettingsHeader
@@ -298,5 +310,5 @@ onMounted(() => {
</div>
</div>
</template>
</PageLayout>
</SettingsPageLayout>
</template>
@@ -1,40 +1,28 @@
<script setup>
import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import { useStore } from 'dashboard/composables/store';
import { useMapGetter } from 'dashboard/composables/store';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { useAccount } from 'dashboard/composables/useAccount';
import Button from 'dashboard/components-next/button/Button.vue';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
import AssistantBasicSettingsForm from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue';
import AssistantSystemSettingsForm from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantSystemSettingsForm.vue';
import AssistantControlItems from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantControlItems.vue';
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
const { t } = useI18n();
const { isCloudFeatureEnabled } = useAccount();
const isCaptainV2Enabled = computed(() =>
isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN_V2)
);
const route = useRoute();
const router = useRouter();
const store = useStore();
const deleteAssistantDialog = ref(null);
const assistantId = route.params.assistantId;
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
const assistants = useMapGetter('captainAssistants/getRecords');
const isFetching = computed(() => uiFlags.value.fetchingItem);
const assistantId = computed(() => Number(route.params.assistantId));
const assistant = computed(() =>
store.getters['captainAssistants/getRecord'](assistantId.value)
store.getters['captainAssistants/getRecord'](Number(assistantId))
);
const isAssistantAvailable = computed(() => !!assistant.value?.id);
const controlItems = computed(() => {
return [
{
@@ -46,6 +34,15 @@ const controlItems = computed(() => {
),
routeName: 'captain_assistants_guardrails_index',
},
{
name: t(
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.TITLE'
),
description: t(
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.DESCRIPTION'
),
routeName: 'captain_assistants_scenarios_index',
},
{
name: t(
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.RESPONSE_GUIDELINES.TITLE'
@@ -58,10 +55,32 @@ const controlItems = computed(() => {
];
});
const breadcrumbItems = computed(() => {
const activeControlItem = controlItems.value?.find(
item => item.routeName === route.name
);
return [
{
label: t('CAPTAIN.ASSISTANTS.SETTINGS.BREADCRUMB.ASSISTANT'),
routeName: 'captain_assistants_index',
},
{ label: assistant.value?.name, routeName: 'captain_assistants_edit' },
...(activeControlItem
? [
{
label: activeControlItem.name,
routeName: activeControlItem.routeName,
},
]
: []),
];
});
const handleSubmit = async updatedAssistant => {
try {
await store.dispatch('captainAssistants/update', {
id: assistantId.value,
id: assistantId,
...updatedAssistant,
});
useAlert(t('CAPTAIN.ASSISTANTS.EDIT.SUCCESS_MESSAGE'));
@@ -72,126 +91,64 @@ const handleSubmit = async updatedAssistant => {
}
};
const handleDelete = () => {
deleteAssistantDialog.value.dialogRef.open();
};
const handleDeleteSuccess = () => {
// Get remaining assistants after deletion
const remainingAssistants = assistants.value.filter(
a => a.id !== assistantId.value
);
if (remainingAssistants.length > 0) {
// Navigate to the first available assistant's settings
const nextAssistant = remainingAssistants[0];
router.push({
name: 'captain_assistants_settings_index',
params: {
accountId: route.params.accountId,
assistantId: nextAssistant.id,
},
});
} else {
// No assistants left, redirect to create assistant page
router.push({
name: 'captain_assistants_create_index',
params: { accountId: route.params.accountId },
});
onMounted(() => {
if (!isAssistantAvailable.value) {
store.dispatch('captainAssistants/show', assistantId);
}
};
});
</script>
<template>
<PageLayout
:header-title="$t('CAPTAIN.ASSISTANTS.SETTINGS.HEADER')"
<SettingsPageLayout
:breadcrumb-items="breadcrumbItems"
:is-fetching="isFetching"
:show-pagination-footer="false"
:show-know-more="false"
:class="{
'[&>header>div]:max-w-[80rem] [&>main>div]:max-w-[80rem]':
isCaptainV2Enabled,
}"
class="[&>div]:max-w-[80rem]"
>
<template #body>
<div
class="gap-6 lg:gap-16 pb-8"
:class="{ 'grid grid-cols-2': isCaptainV2Enabled }"
>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-6">
<SettingsHeader
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.TITLE')"
:description="
t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.DESCRIPTION')
"
/>
<AssistantBasicSettingsForm
:assistant="assistant"
@submit="handleSubmit"
/>
</div>
<span class="h-px w-full bg-n-weak mt-2" />
<div class="flex flex-col gap-6">
<SettingsHeader
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.SYSTEM_SETTINGS.TITLE')"
:description="
t('CAPTAIN.ASSISTANTS.SETTINGS.SYSTEM_SETTINGS.DESCRIPTION')
"
/>
<AssistantSystemSettingsForm
:assistant="assistant"
@submit="handleSubmit"
/>
</div>
<span class="h-px w-full bg-n-weak mt-2" />
<div class="flex items-end justify-between w-full gap-4">
<div class="flex flex-col gap-2">
<h6 class="text-n-slate-12 text-base font-medium">
{{ t('CAPTAIN.ASSISTANTS.SETTINGS.DELETE.TITLE') }}
</h6>
<span class="text-n-slate-11 text-sm">
{{ t('CAPTAIN.ASSISTANTS.SETTINGS.DELETE.DESCRIPTION') }}
</span>
</div>
<div class="flex-shrink-0">
<Button
:label="
t('CAPTAIN.ASSISTANTS.SETTINGS.DELETE.BUTTON_TEXT', {
assistantName: assistant.name,
})
"
color="ruby"
class="max-w-56 !w-fit"
@click="handleDelete"
/>
</div>
</div>
</div>
<div v-if="isCaptainV2Enabled" class="flex flex-col gap-6">
<SettingsHeader
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.TITLE')"
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.TITLE')"
:description="
t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.DESCRIPTION')
t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.DESCRIPTION')
"
/>
<div class="flex flex-col gap-6">
<AssistantControlItems
v-for="item in controlItems"
:key="item.name"
:control-item="item"
/>
</div>
<AssistantBasicSettingsForm
:assistant="assistant"
@submit="handleSubmit"
/>
</div>
<span class="h-px w-full bg-n-weak mt-2" />
<div class="flex flex-col gap-6">
<SettingsHeader
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.SYSTEM_SETTINGS.TITLE')"
:description="
t('CAPTAIN.ASSISTANTS.SETTINGS.SYSTEM_SETTINGS.DESCRIPTION')
"
/>
<AssistantSystemSettingsForm
:assistant="assistant"
@submit="handleSubmit"
/>
</div>
</div>
</template>
<DeleteDialog
v-if="assistant"
ref="deleteAssistantDialog"
:entity="assistant"
type="Assistants"
translation-key="ASSISTANTS"
@delete-success="handleDeleteSuccess"
/>
</PageLayout>
<template #controls>
<div class="flex flex-col gap-6">
<SettingsHeader
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.TITLE')"
:description="
t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.DESCRIPTION')
"
/>
<div class="flex flex-col gap-6">
<AssistantControlItems
v-for="item in controlItems"
:key="item.name"
:control-item="item"
/>
</div>
</div>
</template>
</SettingsPageLayout>
</template>
@@ -1,14 +1,10 @@
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
import { frontendURL } from '../../../helper/URLHelper';
import CaptainPageRouteView from './pages/CaptainPageRouteView.vue';
import AssistantsIndexPage from './pages/AssistantsIndexPage.vue';
import AssistantEmptyStateIndex from './assistants/Index.vue';
import AssistantSettingsIndex from './assistants/settings/Settings.vue';
import AssistantIndex from './assistants/Index.vue';
import AssistantEdit from './assistants/Edit.vue';
// import AssistantSettings from './assistants/settings/Settings.vue';
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
import AssistantPlaygroundIndex from './assistants/playground/Index.vue';
import AssistantGuardrailsIndex from './assistants/guardrails/Index.vue';
import AssistantGuidelinesIndex from './assistants/guidelines/Index.vue';
import AssistantScenariosIndex from './assistants/scenarios/Index.vue';
@@ -17,90 +13,14 @@ import ResponsesIndex from './responses/Index.vue';
import ResponsesPendingIndex from './responses/Pending.vue';
import CustomToolsIndex from './tools/Index.vue';
const meta = {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
};
const metaV2 = {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN_V2,
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
};
const assistantRoutes = [
{
path: frontendURL('accounts/:accountId/captain/:assistantId/faqs'),
component: ResponsesIndex,
name: 'captain_assistants_responses_index',
meta,
},
{
path: frontendURL('accounts/:accountId/captain/:assistantId/documents'),
component: DocumentsIndex,
name: 'captain_assistants_documents_index',
meta,
},
{
path: frontendURL('accounts/:accountId/captain/:assistantId/tools'),
component: CustomToolsIndex,
name: 'captain_tools_index',
meta: metaV2,
},
{
path: frontendURL('accounts/:accountId/captain/:assistantId/scenarios'),
component: AssistantScenariosIndex,
name: 'captain_assistants_scenarios_index',
meta: metaV2,
},
{
path: frontendURL('accounts/:accountId/captain/:assistantId/playground'),
component: AssistantPlaygroundIndex,
name: 'captain_assistants_playground_index',
meta,
},
{
path: frontendURL('accounts/:accountId/captain/:assistantId/inboxes'),
component: AssistantInboxesIndex,
name: 'captain_assistants_inboxes_index',
meta,
},
{
path: frontendURL('accounts/:accountId/captain/:assistantId/faqs/pending'),
component: ResponsesPendingIndex,
name: 'captain_assistants_responses_pending',
meta,
},
{
path: frontendURL('accounts/:accountId/captain/:assistantId/settings'),
component: AssistantSettingsIndex,
name: 'captain_assistants_settings_index',
meta,
},
// Settings sub-pages (guardrails and guidelines)
{
path: frontendURL(
'accounts/:accountId/captain/:assistantId/settings/guardrails'
),
component: AssistantGuardrailsIndex,
name: 'captain_assistants_guardrails_index',
meta: metaV2,
},
{
path: frontendURL(
'accounts/:accountId/captain/:assistantId/settings/guidelines'
),
component: AssistantGuidelinesIndex,
name: 'captain_assistants_guidelines_index',
meta: metaV2,
},
export const routes = [
{
path: frontendURL('accounts/:accountId/captain/assistants'),
component: AssistantEmptyStateIndex,
name: 'captain_assistants_create_index',
component: AssistantIndex,
name: 'captain_assistants_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
@@ -108,26 +28,128 @@ const assistantRoutes = [
},
},
{
path: frontendURL('accounts/:accountId/captain/:navigationPath'),
component: AssistantsIndexPage,
name: 'captain_assistants_index',
meta,
},
];
export const routes = [
{
path: frontendURL('accounts/:accountId/captain'),
component: CaptainPageRouteView,
redirect: to => {
return {
name: 'captain_assistants_index',
params: {
navigationPath: 'captain_assistants_responses_index',
...to.params,
},
};
path: frontendURL('accounts/:accountId/captain/assistants/:assistantId'),
component: AssistantEdit,
name: 'captain_assistants_edit',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
path: frontendURL(
'accounts/:accountId/captain/assistants/:assistantId/inboxes'
),
component: AssistantInboxesIndex,
name: 'captain_assistants_inboxes_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
path: frontendURL(
'accounts/:accountId/captain/assistants/:assistantId/guardrails'
),
component: AssistantGuardrailsIndex,
name: 'captain_assistants_guardrails_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
path: frontendURL(
'accounts/:accountId/captain/assistants/:assistantId/scenarios'
),
component: AssistantScenariosIndex,
name: 'captain_assistants_scenarios_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
path: frontendURL(
'accounts/:accountId/captain/assistants/:assistantId/guidelines'
),
component: AssistantGuidelinesIndex,
name: 'captain_assistants_guidelines_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
path: frontendURL('accounts/:accountId/captain/documents'),
component: DocumentsIndex,
name: 'captain_documents_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
path: frontendURL('accounts/:accountId/captain/responses'),
component: ResponsesIndex,
name: 'captain_responses_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
path: frontendURL('accounts/:accountId/captain/responses/pending'),
component: ResponsesPendingIndex,
name: 'captain_responses_pending',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
},
{
path: frontendURL('accounts/:accountId/captain/tools'),
component: CustomToolsIndex,
name: 'captain_tools_index',
meta: {
permissions: ['administrator', 'agent'],
featureFlag: FEATURE_FLAGS.CAPTAIN_V2,
installationTypes: [
INSTALLATION_TYPES.CLOUD,
INSTALLATION_TYPES.ENTERPRISE,
],
},
children: [...assistantRoutes],
},
];
@@ -1,7 +1,6 @@
<script setup>
import { computed, onMounted, ref, nextTick } from 'vue';
import { useMapGetter, useStore } from 'dashboard/composables/store';
import { useRoute } from 'vue-router';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { useAccount } from 'dashboard/composables/useAccount';
@@ -11,20 +10,20 @@ import PageLayout from 'dashboard/components-next/captain/PageLayout.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 FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue';
import LimitBanner from 'dashboard/components-next/captain/pageComponents/document/LimitBanner.vue';
const route = useRoute();
const store = useStore();
const { isOnChatwootCloud } = useAccount();
const uiFlags = useMapGetter('captainDocuments/getUIFlags');
const documents = useMapGetter('captainDocuments/getRecords');
const assistants = useMapGetter('captainAssistants/getRecords');
const isFetching = computed(() => uiFlags.value.fetchingList);
const documentsMeta = useMapGetter('captainDocuments/getMeta');
const selectedAssistantId = computed(() => Number(route.params.assistantId));
const selectedAssistant = ref('all');
const selectedDocument = ref(null);
const deleteDocumentDialog = ref(null);
@@ -38,6 +37,12 @@ const showCreateDialog = ref(false);
const createDocumentDialog = ref(null);
const relationQuestionDialog = ref(null);
const shouldShowAssistantSelector = computed(() => {
if (assistants.value.length === 0) return false;
return !isFetching.value;
});
const handleShowRelatedDocument = () => {
showRelatedResponses.value = true;
nextTick(() => relationQuestionDialog.value.dialogRef.open());
@@ -72,12 +77,17 @@ const handleAction = ({ action, id }) => {
const fetchDocuments = (page = 1) => {
const filterParams = { page };
if (selectedAssistantId.value) {
filterParams.assistantId = selectedAssistantId.value;
if (selectedAssistant.value !== 'all') {
filterParams.assistantId = selectedAssistant.value;
}
store.dispatch('captainDocuments/get', filterParams);
};
const handleAssistantFilterChange = assistant => {
selectedAssistant.value = assistant;
fetchDocuments();
};
const onPageChange = page => fetchDocuments(page);
const onDeleteSuccess = () => {
@@ -87,6 +97,9 @@ const onDeleteSuccess = () => {
};
onMounted(() => {
if (!assistants.value.length) {
store.dispatch('captainAssistants/get');
}
fetchDocuments();
});
</script>
@@ -101,7 +114,6 @@ onMounted(() => {
:show-pagination-footer="!isFetching && !!documents.length"
:is-fetching="isFetching"
:is-empty="!documents.length"
:show-know-more="false"
:feature-flag="FEATURE_FLAGS.CAPTAIN"
@update:current-page="onPageChange"
@click="handleCreateDocument"
@@ -126,6 +138,15 @@ onMounted(() => {
<CaptainPaywall />
</template>
<template #controls>
<div v-if="shouldShowAssistantSelector" class="mb-4 -mt-3 flex gap-3">
<AssistantSelector
:assistant-id="selectedAssistant"
@update="handleAssistantFilterChange"
/>
</div>
</template>
<template #body>
<LimitBanner class="mb-5" />
@@ -152,7 +173,6 @@ onMounted(() => {
<CreateDocumentDialog
v-if="showCreateDialog"
ref="createDocumentDialog"
:assistant-id="selectedAssistantId"
@close="handleCreateDialogClose"
/>
<DeleteDialog
@@ -1,89 +0,0 @@
<script setup>
import { computed, nextTick, onMounted } from 'vue';
import { useStore } from 'vuex';
import { useRoute, useRouter } from 'vue-router';
import { useUISettings } from 'dashboard/composables/useUISettings';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
const store = useStore();
const router = useRouter();
const { uiSettings } = useUISettings();
const route = useRoute();
const assistants = computed(
() => store.getters['captainAssistants/getRecords']
);
const isAssistantPresent = assistantId => {
return !!assistants.value.find(a => a.id === Number(assistantId));
};
const routeToView = (name, params) => {
router.replace({ name, params, replace: true });
};
const generateRouterParams = () => {
const { last_active_assistant_id: lastActiveAssistantId } =
uiSettings.value || {};
if (isAssistantPresent(lastActiveAssistantId)) {
return {
assistantId: lastActiveAssistantId,
};
}
if (assistants.value.length > 0) {
const { id: assistantId } = assistants.value[0];
return { assistantId };
}
return null;
};
const routeToLastActiveAssistant = () => {
const params = generateRouterParams();
// No assistants found, redirect to create page
if (!params) {
return routeToView('captain_assistants_create_index', {
accountId: route.params.accountId,
});
}
const { navigationPath } = route.params;
const isAValidRoute = [
'captain_assistants_responses_index', // Faq page
'captain_assistants_documents_index', // Document page
'captain_assistants_scenarios_index', // Scenario page
'captain_assistants_playground_index', // Playground page
'captain_assistants_inboxes_index', // Inboxes page
'captain_tools_index', // Tools page
'captain_assistants_settings_index', // Settings page
].includes(navigationPath);
const navigateTo = isAValidRoute
? navigationPath
: 'captain_assistants_responses_index';
return routeToView(navigateTo, {
accountId: route.params.accountId,
...params,
});
};
const performRouting = async () => {
await store.dispatch('captainAssistants/get');
nextTick(() => routeToLastActiveAssistant());
};
onMounted(() => performRouting());
</script>
<template>
<div
class="flex items-center justify-center w-full bg-n-background text-n-slate-11"
>
<Spinner />
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More