Merge branch 'develop' into fix/cw-4085

This commit is contained in:
Pranav
2025-05-01 16:41:44 -07:00
committed by GitHub
982 changed files with 25286 additions and 6037 deletions
+17
View File
@@ -1,9 +1,26 @@
/* global axios */
import ApiClient from './ApiClient';
class AgentBotsAPI extends ApiClient {
constructor() {
super('agent_bots', { accountScoped: true });
}
create(data) {
return axios.post(this.url, data, {
headers: { 'Content-Type': 'multipart/form-data' },
});
}
update(id, data) {
return axios.patch(`${this.url}/${id}`, data, {
headers: { 'Content-Type': 'multipart/form-data' },
});
}
deleteAgentBotAvatar(botId) {
return axios.delete(`${this.url}/${botId}/avatar`);
}
}
export default new AgentBotsAPI();
@@ -14,6 +14,13 @@ class CaptainAssistant extends ApiClient {
},
});
}
playground({ assistantId, messageContent, messageHistory }) {
return axios.post(`${this.url}/${assistantId}/playground`, {
message_content: messageContent,
message_history: messageHistory,
});
}
}
export default new CaptainAssistant();
@@ -0,0 +1,14 @@
/* global axios */
import ApiClient from '../ApiClient';
class InstagramChannel extends ApiClient {
constructor() {
super('instagram', { accountScoped: true });
}
generateAuthorization(payload) {
return axios.post(`${this.url}/authorization`, payload);
}
}
export default new InstagramChannel();
@@ -17,6 +17,12 @@ class EnterpriseAccountAPI extends ApiClient {
getLimits() {
return axios.get(`${this.url}limits`);
}
toggleDeletion(action) {
return axios.post(`${this.url}toggle_deletion`, {
action_type: action,
});
}
}
export default new EnterpriseAccountAPI();
@@ -10,6 +10,7 @@ describe('#enterpriseAccountAPI', () => {
expect(accountAPI).toHaveProperty('update');
expect(accountAPI).toHaveProperty('delete');
expect(accountAPI).toHaveProperty('checkout');
expect(accountAPI).toHaveProperty('toggleDeletion');
});
describe('API calls', () => {
@@ -42,5 +43,21 @@ describe('#enterpriseAccountAPI', () => {
'/enterprise/api/v1/subscription'
);
});
it('#toggleDeletion with delete action', () => {
accountAPI.toggleDeletion('delete');
expect(axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/toggle_deletion',
{ action_type: 'delete' }
);
});
it('#toggleDeletion with undelete action', () => {
accountAPI.toggleDeletion('undelete');
expect(axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/toggle_deletion',
{ action_type: 'undelete' }
);
});
});
});
@@ -29,6 +29,19 @@
--iris-11: 87 83 198;
--iris-12: 39 41 98;
--blue-1: 251 253 255;
--blue-2: 245 249 255;
--blue-3: 233 243 255;
--blue-4: 218 236 255;
--blue-5: 201 226 255;
--blue-6: 181 213 255;
--blue-7: 155 195 252;
--blue-8: 117 171 247;
--blue-9: 39 129 246;
--blue-10: 16 115 233;
--blue-11: 8 109 224;
--blue-12: 11 50 101;
--ruby-1: 255 252 253;
--ruby-2: 255 247 248;
--ruby-3: 254 234 237;
@@ -131,6 +144,19 @@
--iris-11: 158 177 255;
--iris-12: 224 223 254;
--blue-1: 10 17 28;
--blue-2: 15 24 38;
--blue-3: 15 39 72;
--blue-4: 10 49 99;
--blue-5: 18 61 117;
--blue-6: 29 84 134;
--blue-7: 40 89 156;
--blue-8: 48 106 186;
--blue-9: 39 129 246;
--blue-10: 21 116 231;
--blue-11: 126 182 255;
--blue-12: 205 227 255;
--ruby-1: 25 17 19;
--ruby-2: 30 21 23;
--ruby-3: 58 20 30;
@@ -0,0 +1,39 @@
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
title: { type: String, required: true },
isOpen: { type: Boolean, default: false },
});
const isExpanded = ref(props.isOpen);
const toggleAccordion = () => {
isExpanded.value = !isExpanded.value;
};
watch(
() => props.isOpen,
newValue => {
isExpanded.value = newValue;
}
);
</script>
<template>
<div class="border rounded-lg border-n-slate-4">
<button
class="flex items-center justify-between w-full p-4 text-left"
@click="toggleAccordion"
>
<span class="text-sm font-medium text-n-slate-12">{{ title }}</span>
<span
class="w-5 h-5 transition-transform duration-200 i-lucide-chevron-down"
:class="{ 'rotate-180': isExpanded }"
/>
</button>
<div v-if="isExpanded" class="p-4 pt-0">
<slot />
</div>
</div>
</template>
@@ -51,6 +51,7 @@ defineExpose({ dialogRef, contactsFormRef, onSuccess });
<Button
:label="t('DIALOG.BUTTONS.CANCEL')"
variant="link"
type="reset"
class="h-10 hover:!no-underline hover:text-n-brand"
@click="closeDialog"
/>
@@ -31,10 +31,6 @@ const sortMenus = [
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.EMAIL'),
value: 'email',
},
{
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.PHONE_NUMBER'),
value: 'phone_number',
},
{
label: t('CONTACTS_LAYOUT.HEADER.ACTIONS.SORT_BY.OPTIONS.COMPANY'),
value: 'company_name',
@@ -87,8 +87,10 @@ useKeyboardEvents(keyboardEvents);
<ContactNoteItem
v-for="note in notes"
:key="note.id"
class="mx-6 py-4"
:note="note"
:written-by="getWrittenBy(note)"
allow-delete
@delete="onDelete"
/>
</div>
@@ -1,6 +1,8 @@
<script setup>
import { useTemplateRef, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { dynamicTime } from 'shared/helpers/timeHelper';
import { useToggle } from '@vueuse/core';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import Button from 'dashboard/components-next/button/Button.vue';
@@ -14,39 +16,63 @@ const props = defineProps({
type: String,
required: true,
},
allowDelete: {
type: Boolean,
default: false,
},
collapsible: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['delete']);
const noteContentRef = useTemplateRef('noteContentRef');
const needsCollapse = ref(false);
const [isExpanded, toggleExpanded] = useToggle();
const { t } = useI18n();
const { formatMessage } = useMessageFormatter();
const handleDelete = () => {
emit('delete', props.note.id);
};
onMounted(() => {
if (props.collapsible) {
// Check if content height exceeds approximately 4 lines
// Assuming line height is ~1.625 and font size is ~14px
const threshold = 14 * 1.625 * 4; // ~84px
needsCollapse.value = noteContentRef.value?.clientHeight > threshold;
}
});
</script>
<template>
<div
class="flex flex-col gap-2 py-2 mx-6 border-b border-n-strong group/note"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5 py-2.5 min-w-0">
<div class="flex flex-col gap-2 border-b border-n-strong group/note">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 min-w-0">
<Avatar
:name="note?.user?.name || 'Bot'"
:src="note?.user?.thumbnail || '/assets/images/chatwoot_bot.png'"
:src="
note?.user?.name
? note?.user?.thumbnail
: '/assets/images/chatwoot_bot.png'
"
:size="16"
rounded-full
/>
<div class="min-w-0 truncate">
<span class="inline-flex items-center gap-1 text-sm text-n-slate-11">
<span class="font-medium">{{ writtenBy }}</span>
<span class="font-medium text-n-slate-12">{{ writtenBy }}</span>
{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.WROTE') }}
<span class="font-medium">{{ dynamicTime(note.createdAt) }}</span>
<span class="font-medium text-n-slate-12">
{{ dynamicTime(note.createdAt) }}
</span>
</span>
</div>
</div>
<Button
v-if="allowDelete"
variant="faded"
color="ruby"
size="xs"
@@ -56,8 +82,28 @@ const handleDelete = () => {
/>
</div>
<p
ref="noteContentRef"
v-dompurify-html="formatMessage(note.content || '')"
class="mb-0 prose-sm prose-p:mb-1 prose-p:mt-0 prose-ul:mb-1 prose-ul:mt-0 text-n-slate-12"
class="mb-0 prose-sm prose-p:text-sm prose-p:leading-relaxed prose-p:mb-1 prose-p:mt-0 prose-ul:mb-1 prose-ul:mt-0 text-n-slate-12"
:class="{
'line-clamp-4': collapsible && !isExpanded && needsCollapse,
}"
/>
<p v-if="collapsible && needsCollapse">
<Button
variant="faded"
color="blue"
size="xs"
:icon="isExpanded ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
@click="() => toggleExpanded()"
>
<template v-if="isExpanded">
{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.COLLAPSE') }}
</template>
<template v-else>
{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.EXPAND') }}
</template>
</Button>
</p>
</div>
</template>
@@ -7,8 +7,8 @@ import { useStore, useStoreGetters } from 'dashboard/composables/store';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
import { shouldBeUrl } from 'shared/helpers/Validators';
import { required, minLength, helpers } from '@vuelidate/validators';
import { shouldBeUrl, isValidSlug } from 'shared/helpers/Validators';
import Button from 'dashboard/components-next/button/Button.vue';
import Input from 'dashboard/components-next/input/Input.vue';
@@ -61,7 +61,16 @@ const liveChatWidgets = computed(() => {
const rules = {
name: { required, minLength: minLength(2) },
slug: { required },
slug: {
required: helpers.withMessage(
() => t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR'),
required
),
isValidSlug: helpers.withMessage(
() => t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.FORMAT_ERROR'),
isValidSlug
),
},
homePageLink: { shouldBeUrl },
};
@@ -71,9 +80,9 @@ const nameError = computed(() =>
v$.value.name.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.ERROR') : ''
);
const slugError = computed(() =>
v$.value.slug.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR') : ''
);
const slugError = computed(() => {
return v$.value.slug.$errors[0]?.$message || '';
});
const homePageLinkError = computed(() =>
v$.value.homePageLink.$error
@@ -6,8 +6,9 @@ import { useAlert, useTrack } from 'dashboard/composables';
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
import { required, minLength, helpers } from '@vuelidate/validators';
import { buildPortalURL } from 'dashboard/helper/portalHelper';
import { isValidSlug } from 'shared/helpers/Validators';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import Input from 'dashboard/components-next/input/Input.vue';
@@ -31,7 +32,16 @@ const state = reactive({
const rules = {
name: { required, minLength: minLength(2) },
slug: { required },
slug: {
required: helpers.withMessage(
() => t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR'),
required
),
isValidSlug: helpers.withMessage(
() => t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.FORMAT_ERROR'),
isValidSlug
),
},
};
const v$ = useVuelidate(rules, state);
@@ -40,9 +50,9 @@ const nameError = computed(() =>
v$.value.name.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.ERROR') : ''
);
const slugError = computed(() =>
v$.value.slug.$error ? t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.ERROR') : ''
);
const slugError = computed(() => {
return v$.value.slug.$errors[0]?.$message || '';
});
const isSubmitDisabled = computed(() => v$.value.$invalid);
@@ -131,6 +141,7 @@ defineExpose({ dialogRef });
:message="
nameError || t('HELP_CENTER.CREATE_PORTAL_DIALOG.NAME.MESSAGE')
"
@blur="v$.name.$touch()"
/>
<Input
id="portal-slug"
@@ -140,6 +151,8 @@ defineExpose({ dialogRef });
:label="t('HELP_CENTER.CREATE_PORTAL_DIALOG.SLUG.LABEL')"
:message-type="slugError ? 'error' : 'info'"
:message="slugError || buildPortalURL(state.slug)"
@input="v$.slug.$touch()"
@blur="v$.slug.$touch()"
/>
</div>
</Dialog>
@@ -2,6 +2,7 @@
import { computed } from 'vue';
import { usePolicy } from 'dashboard/composables/usePolicy';
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';
@@ -23,6 +24,10 @@ const props = defineProps({
type: String,
default: '',
},
backUrl: {
type: [String, Object],
default: '',
},
buttonPolicy: {
type: Array,
default: () => [],
@@ -39,6 +44,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
showKnowMore: {
type: Boolean,
default: true,
},
isEmpty: {
type: Boolean,
default: false,
@@ -73,19 +82,23 @@ const handlePageChange = event => {
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" :to="backUrl" />
<slot name="headerTitle">
<span class="text-xl font-medium text-n-slate-12">
{{ headerTitle }}
</span>
</slot>
<div v-if="!isEmpty" class="flex items-center gap-2">
<div
v-if="!isEmpty && showKnowMore"
class="flex items-center gap-2"
>
<div class="w-0.5 h-4 rounded-2xl bg-n-weak" />
<slot name="knowMore" />
</div>
</div>
<div
v-if="!showPaywall"
v-if="!showPaywall && buttonLabel"
v-on-clickaway="() => emit('close')"
class="relative group/campaign-button"
>
@@ -104,7 +117,7 @@ const handlePageChange = event => {
</div>
</header>
<main class="flex-1 px-6 overflow-y-auto xl:px-0">
<div class="w-full max-w-[60rem] mx-auto py-4">
<div class="w-full max-w-[60rem] h-full mx-auto py-4">
<slot v-if="!showPaywall" name="controls" />
<div
v-if="isFetching"
@@ -76,9 +76,12 @@ const handleAction = ({ action, value }) => {
<template>
<CardLayout>
<div class="flex justify-between w-full gap-1">
<span class="text-base text-n-slate-12 line-clamp-1">
<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 }}
</span>
</router-link>
<div class="flex items-center gap-2">
<div
v-on-clickaway="() => toggleDropdown(false)"
@@ -0,0 +1,111 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import NextButton from 'dashboard/components-next/button/Button.vue';
import MessageList from './MessageList.vue';
import CaptainAssistant from 'dashboard/api/captain/assistant';
const { assistantId } = defineProps({
assistantId: {
type: Number,
required: true,
},
});
const { t } = useI18n();
const messages = ref([]);
const newMessage = ref('');
const isLoading = ref(false);
const formatMessagesForApi = () => {
return messages.value.map(message => ({
role: message.sender,
content: message.content,
}));
};
const resetConversation = () => {
messages.value = [];
newMessage.value = '';
};
const sendMessage = async () => {
if (!newMessage.value.trim() || isLoading.value) return;
const userMessage = {
content: newMessage.value,
sender: 'user',
timestamp: new Date().toISOString(),
};
messages.value.push(userMessage);
const currentMessage = newMessage.value;
newMessage.value = '';
try {
isLoading.value = true;
const { data } = await CaptainAssistant.playground({
assistantId,
messageContent: currentMessage,
messageHistory: formatMessagesForApi(),
});
messages.value.push({
content: data.response,
sender: 'assistant',
timestamp: new Date().toISOString(),
});
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error getting assistant response:', error);
} finally {
isLoading.value = false;
}
};
</script>
<template>
<div
class="flex flex-col h-full rounded-lg p-4 border border-n-slate-4 text-n-slate-11"
>
<div class="mb-4">
<div class="flex justify-between items-center mb-1">
<h3 class="text-lg font-medium">
{{ t('CAPTAIN.PLAYGROUND.HEADER') }}
</h3>
<NextButton
ghost
size="small"
icon="i-lucide-rotate-ccw"
@click="resetConversation"
/>
</div>
<p class="text-sm text-n-slate-11">
{{ t('CAPTAIN.PLAYGROUND.DESCRIPTION') }}
</p>
</div>
<MessageList :messages="messages" :is-loading="isLoading" />
<div
class="flex items-center bg-n-solid-1 outline outline-n-container rounded-lg p-3"
>
<input
v-model="newMessage"
class="flex-1 bg-transparent border-none focus:outline-none text-sm mb-0"
:placeholder="t('CAPTAIN.PLAYGROUND.MESSAGE_PLACEHOLDER')"
@keyup.enter="sendMessage"
/>
<NextButton
ghost
size="small"
:disabled="!newMessage.trim()"
icon="i-lucide-send"
@click="sendMessage"
/>
</div>
<p class="text-xs text-n-slate-11 pt-2 text-center">
{{ t('CAPTAIN.PLAYGROUND.CREDIT_NOTE') }}
</p>
</div>
</template>
@@ -0,0 +1,91 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref, watch, nextTick } from 'vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
const props = defineProps({
messages: {
type: Array,
required: true,
},
isLoading: {
type: Boolean,
default: false,
},
});
const messageContainer = ref(null);
const { t } = useI18n();
const { formatMessage } = useMessageFormatter();
const isUserMessage = sender => sender === 'user';
const getMessageAlignment = sender =>
isUserMessage(sender) ? 'justify-end' : 'justify-start';
const getMessageDirection = sender =>
isUserMessage(sender) ? 'flex-row-reverse' : 'flex-row';
const getAvatarName = sender =>
isUserMessage(sender)
? t('CAPTAIN.PLAYGROUND.USER')
: t('CAPTAIN.PLAYGROUND.ASSISTANT');
const getMessageStyle = sender =>
isUserMessage(sender)
? 'bg-n-strong text-n-white'
: 'bg-n-solid-iris text-n-slate-12';
const scrollToBottom = async () => {
await nextTick();
if (messageContainer.value) {
messageContainer.value.scrollTop = messageContainer.value.scrollHeight;
}
};
watch(() => props.messages.length, scrollToBottom);
</script>
<template>
<div ref="messageContainer" class="flex-1 overflow-y-auto mb-4 space-y-2">
<div
v-for="(message, index) in messages"
:key="index"
class="flex"
:class="getMessageAlignment(message.sender)"
>
<div
class="flex items-start gap-1.5"
:class="getMessageDirection(message.sender)"
>
<Avatar :name="getAvatarName(message.sender)" rounded-full :size="24" />
<div
class="max-w-[80%] rounded-lg p-3 text-sm"
:class="getMessageStyle(message.sender)"
>
<div v-html="formatMessage(message.content)" />
</div>
</div>
</div>
<div v-if="isLoading" class="flex justify-start">
<div class="flex items-start gap-1.5">
<Avatar :name="getAvatarName('assistant')" rounded-full :size="24" />
<div
class="max-w-sm rounded-lg p-3 text-sm bg-n-solid-iris text-n-slate-12"
>
<div class="flex gap-1">
<div class="w-2 h-2 rounded-full bg-n-iris-10 animate-bounce" />
<div
class="w-2 h-2 rounded-full bg-n-iris-10 animate-bounce [animation-delay:0.2s]"
/>
<div
class="w-2 h-2 rounded-full bg-n-iris-10 animate-bounce [animation-delay:0.4s]"
/>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,306 @@
<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,
},
};
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,
};
};
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,
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,
instructions: state.instructions,
},
};
emit('submit', payload);
};
const handleFeaturesUpdate = () => {
const payload = {
config: {
...props.assistant.config,
feature_faq: state.features.conversationFaqs,
feature_memory: state.features.memories,
},
};
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 pt-4">
<Editor
v-model="state.instructions"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
:message="formErrors.instructions"
:max-length="2000"
:message-type="formErrors.instructions ? 'error' : 'info'"
/>
<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"
class="form-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"
class="form-checkbox"
/>
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
</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>
@@ -33,6 +33,8 @@ const insertIntoRichEditor = computed(() => {
);
});
const hasEmptyMessageContent = computed(() => !props.message?.content);
const useCopilotResponse = () => {
if (insertIntoRichEditor.value) {
emitter.emit(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, props.message?.content);
@@ -53,9 +55,17 @@ const useCopilotResponse = () => {
/>
<div class="flex flex-col gap-1 text-n-slate-12">
<div class="font-medium">{{ $t('CAPTAIN.NAME') }}</div>
<div v-dompurify-html="messageContent" class="prose-sm break-words" />
<span v-if="hasEmptyMessageContent" class="text-n-ruby-11">
{{ $t('CAPTAIN.COPILOT.EMPTY_MESSAGE') }}
</span>
<div
v-else
v-dompurify-html="messageContent"
class="prose-sm break-words"
/>
<div class="flex flex-row mt-1">
<Button
v-if="!hasEmptyMessageContent"
:label="$t('CAPTAIN.COPILOT.USE')"
faded
sm
@@ -125,7 +125,10 @@ defineExpose({ open, close });
<slot />
<!-- Dialog content will be injected here -->
<slot name="footer">
<div class="flex items-center justify-between w-full gap-3">
<div
v-if="showCancelButton || showConfirmButton"
class="flex items-center justify-between w-full gap-3"
>
<Button
v-if="showCancelButton"
variant="faded"
@@ -12,6 +12,7 @@ export function useChannelIcon(inbox) {
'Channel::TwitterProfile': 'i-ri-twitter-x-fill',
'Channel::WebWidget': 'i-ri-global-fill',
'Channel::Whatsapp': 'i-ri-whatsapp-fill',
'Channel::Instagram': 'i-ri-instagram-fill',
};
const providerIconMap = {
@@ -19,10 +19,17 @@ const {
isAWebWidgetInbox,
isAWhatsAppChannel,
isAnEmailChannel,
isAnInstagramChannel,
} = useInbox();
const { status, isPrivate, createdAt, sourceId, messageType } =
useMessageContext();
const {
status,
isPrivate,
createdAt,
sourceId,
messageType,
contentAttributes,
} = useMessageContext();
const readableTime = computed(() =>
messageTimestamp(createdAt.value, 'LLL d, h:mm a')
@@ -30,6 +37,11 @@ const readableTime = computed(() =>
const showStatusIndicator = computed(() => {
if (isPrivate.value) return false;
// Don't show status for failed messages, we already show error message
if (status.value === MESSAGE_STATUS.FAILED) return false;
// Don't show status for deleted messages
if (contentAttributes.value?.deleted) return false;
if (messageType.value === MESSAGE_TYPES.OUTGOING) return true;
if (messageType.value === MESSAGE_TYPES.TEMPLATE) return true;
@@ -47,7 +59,8 @@ const isSent = computed(() => {
isATwilioChannel.value ||
isAFacebookInbox.value ||
isASmsInbox.value ||
isATelegramChannel.value
isATelegramChannel.value ||
isAnInstagramChannel.value
) {
return sourceId.value && status.value === MESSAGE_STATUS.SENT;
}
@@ -86,7 +99,8 @@ const isRead = computed(() => {
if (
isAWhatsAppChannel.value ||
isATwilioChannel.value ||
isAFacebookInbox.value
isAFacebookInbox.value ||
isAnInstagramChannel.value
) {
return sourceId.value && status.value === MESSAGE_STATUS.READ;
}
@@ -102,7 +116,6 @@ const statusToShow = computed(() => {
if (isRead.value) return MESSAGE_STATUS.READ;
if (isDelivered.value) return MESSAGE_STATUS.DELIVERED;
if (isSent.value) return MESSAGE_STATUS.SENT;
if (status.value === MESSAGE_STATUS.FAILED) return MESSAGE_STATUS.FAILED;
return MESSAGE_STATUS.PROGRESS;
});
@@ -0,0 +1,24 @@
<script setup>
import { defineProps, defineEmits } from 'vue';
defineProps({
showingOriginal: Boolean,
});
defineEmits(['toggle']);
</script>
<template>
<span>
<span
class="text-xs text-n-slate-11 cursor-pointer hover:underline select-none"
@click="$emit('toggle')"
>
{{
showingOriginal
? $t('CONVERSATION.VIEW_TRANSLATED')
: $t('CONVERSATION.VIEW_ORIGINAL')
}}
</span>
</span>
</template>
@@ -9,9 +9,11 @@ import BaseBubble from 'next/message/bubbles/Base.vue';
import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue';
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
import EmailMeta from './EmailMeta.vue';
import TranslationToggle from 'dashboard/components-next/message/TranslationToggle.vue';
import { useMessageContext } from '../../provider.js';
import { MESSAGE_TYPES } from 'next/message/constants.js';
import { useTranslations } from 'dashboard/composables/useTranslations';
const { content, contentAttributes, attachments, messageType } =
useMessageContext();
@@ -19,35 +21,77 @@ const { content, contentAttributes, attachments, messageType } =
const isExpandable = ref(false);
const isExpanded = ref(false);
const showQuotedMessage = ref(false);
const renderOriginal = ref(false);
const contentContainer = useTemplateRef('contentContainer');
onMounted(() => {
isExpandable.value = contentContainer.value?.scrollHeight > 400;
});
const isOutgoing = computed(() => {
return messageType.value === MESSAGE_TYPES.OUTGOING;
});
const isOutgoing = computed(() => messageType.value === MESSAGE_TYPES.OUTGOING);
const isIncoming = computed(() => !isOutgoing.value);
const textToShow = computed(() => {
const { hasTranslations, translationContent } =
useTranslations(contentAttributes);
const originalEmailText = computed(() => {
const text =
contentAttributes?.value?.email?.textContent?.full ?? content.value;
return text?.replace(/\n/g, '<br>');
});
// Use TextContent as the default to fullHTML
const originalEmailHtml = computed(
() =>
contentAttributes?.value?.email?.htmlContent?.full ??
originalEmailText.value
);
const messageContent = computed(() => {
// If translations exist and we're showing translations (not original)
if (hasTranslations.value && !renderOriginal.value) {
return translationContent.value;
}
// Otherwise show original content
return content.value;
});
const textToShow = computed(() => {
// If translations exist and we're showing translations (not original)
if (hasTranslations.value && !renderOriginal.value) {
return translationContent.value;
}
// Otherwise show original text
return originalEmailText.value;
});
const fullHTML = computed(() => {
return contentAttributes?.value?.email?.htmlContent?.full ?? textToShow.value;
// If translations exist and we're showing translations (not original)
if (hasTranslations.value && !renderOriginal.value) {
return translationContent.value;
}
// Otherwise show original HTML
return originalEmailHtml.value;
});
const unquotedHTML = computed(() => {
return EmailQuoteExtractor.extractQuotes(fullHTML.value);
const unquotedHTML = computed(() =>
EmailQuoteExtractor.extractQuotes(fullHTML.value)
);
const hasQuotedMessage = computed(() =>
EmailQuoteExtractor.hasQuotes(fullHTML.value)
);
// Ensure unique keys for <Letter> when toggling between original and translated views.
// This forces Vue to re-render the component and update content correctly.
const translationKeySuffix = computed(() => {
if (renderOriginal.value) return 'original';
if (hasTranslations.value) return 'translated';
return 'original';
});
const hasQuotedMessage = computed(() => {
return EmailQuoteExtractor.hasQuotes(fullHTML.value);
});
const handleSeeOriginal = () => {
renderOriginal.value = !renderOriginal.value;
};
</script>
<template>
@@ -75,7 +119,7 @@ const hasQuotedMessage = computed(() => {
>
<div
v-if="isExpandable && !isExpanded"
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end bg-gradient-to-t from-n-gray-3 via-n-gray-3 via-20% to-transparent"
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end bg-gradient-to-t from-n-slate-4 via-n-slate-4 via-20% to-transparent"
>
<button
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
@@ -88,11 +132,12 @@ const hasQuotedMessage = computed(() => {
<FormattedContent
v-if="isOutgoing && content"
class="text-n-slate-12"
:content="content"
:content="messageContent"
/>
<template v-else>
<Letter
v-if="showQuotedMessage"
:key="`letter-quoted-${translationKeySuffix}`"
class-name="prose prose-bubble !max-w-none letter-render"
:allowed-css-properties="[
...allowedCssProperties,
@@ -104,6 +149,7 @@ const hasQuotedMessage = computed(() => {
/>
<Letter
v-else
:key="`letter-unquoted-${translationKeySuffix}`"
class-name="prose prose-bubble !max-w-none letter-render"
:html="unquotedHTML"
:allowed-css-properties="[
@@ -135,6 +181,12 @@ const hasQuotedMessage = computed(() => {
</button>
</div>
</section>
<TranslationToggle
v-if="hasTranslations"
class="py-2 px-3"
:showing-original="renderOriginal"
@toggle="handleSeeOriginal"
/>
<section
v-if="Array.isArray(attachments) && attachments.length"
class="px-4 pb-4 space-y-2"
@@ -3,16 +3,16 @@ import { computed, ref } from 'vue';
import BaseBubble from 'next/message/bubbles/Base.vue';
import FormattedContent from './FormattedContent.vue';
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
import TranslationToggle from 'dashboard/components-next/message/TranslationToggle.vue';
import { MESSAGE_TYPES } from '../../constants';
import { useMessageContext } from '../../provider.js';
import { useTranslations } from 'dashboard/composables/useTranslations';
const { content, attachments, contentAttributes, messageType } =
useMessageContext();
const hasTranslations = computed(() => {
const { translations = {} } = contentAttributes.value;
return Object.keys(translations || {}).length > 0;
});
const { hasTranslations, translationContent } =
useTranslations(contentAttributes);
const renderOriginal = ref(false);
@@ -22,8 +22,7 @@ const renderContent = computed(() => {
}
if (hasTranslations.value) {
const translations = contentAttributes.value.translations;
return translations[Object.keys(translations)[0]];
return translationContent.value;
}
return content.value;
@@ -37,12 +36,6 @@ const isEmpty = computed(() => {
return !content.value && !attachments.value?.length;
});
const viewToggleKey = computed(() => {
return renderOriginal.value
? 'CONVERSATION.VIEW_TRANSLATED'
: 'CONVERSATION.VIEW_ORIGINAL';
});
const handleSeeOriginal = () => {
renderOriginal.value = !renderOriginal.value;
};
@@ -55,15 +48,12 @@ const handleSeeOriginal = () => {
{{ $t('CONVERSATION.NO_CONTENT') }}
</span>
<FormattedContent v-if="renderContent" :content="renderContent" />
<span class="-mt-3">
<span
v-if="hasTranslations"
class="text-xs text-n-slate-11 cursor-pointer hover:underline"
@click="handleSeeOriginal"
>
{{ $t(viewToggleKey) }}
</span>
</span>
<TranslationToggle
v-if="hasTranslations"
class="-mt-3"
:showing-original="renderOriginal"
@toggle="handleSeeOriginal"
/>
<AttachmentChips :attachments="attachments" class="gap-2" />
<template v-if="isTemplate">
<div
@@ -39,7 +39,7 @@ const textColorClass = computed(() => {
docx: 'dark:text-[#D6E1FF] text-[#1F2D5C]', // indigo-12
json: 'text-n-slate-12',
odt: 'dark:text-[#D6E1FF] text-[#1F2D5C]', // indigo-12
pdf: 'text-n-ruby-12',
pdf: 'text-n-slate-12',
ppt: 'dark:text-[#FFE0C2] text-[#582D1D]',
pptx: 'dark:text-[#FFE0C2] text-[#582D1D]',
rar: 'dark:text-[#EDEEF0] text-[#2F265F]',
@@ -8,7 +8,6 @@ import { useStore } from 'vuex';
import { useI18n } from 'vue-i18n';
import { useStorage } from '@vueuse/core';
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import Button from 'dashboard/components-next/button/Button.vue';
import SidebarGroup from './SidebarGroup.vue';
@@ -37,18 +36,6 @@ const toggleShortcutModalFn = show => {
}
};
const currentAccountId = useMapGetter('getCurrentAccountId');
const isFeatureEnabledonAccount = useMapGetter(
'accounts/isFeatureEnabledonAccount'
);
const showV4Routes = computed(() => {
return isFeatureEnabledonAccount.value(
currentAccountId.value,
FEATURE_FLAGS.REPORT_V4
);
});
useSidebarKeyboardShortcuts(toggleShortcutModalFn);
// We're using localStorage to store the expanded item in the sidebar
@@ -116,32 +103,7 @@ const newReportRoutes = () => [
},
];
const oldReportRoutes = () => [
{
name: 'Reports Agent',
label: t('SIDEBAR.REPORTS_AGENT'),
to: accountScopedRoute('agent_reports'),
},
{
name: 'Reports Label',
label: t('SIDEBAR.REPORTS_LABEL'),
to: accountScopedRoute('label_reports'),
},
{
name: 'Reports Inbox',
label: t('SIDEBAR.REPORTS_INBOX'),
to: accountScopedRoute('inbox_reports'),
},
{
name: 'Reports Team',
label: t('SIDEBAR.REPORTS_TEAM'),
to: accountScopedRoute('team_reports'),
},
];
const reportRoutes = computed(() =>
showV4Routes.value ? newReportRoutes() : oldReportRoutes()
);
const reportRoutes = computed(() => newReportRoutes());
const menuItems = computed(() => {
return [
@@ -26,6 +26,12 @@ const showAccountSwitcher = computed(
() => userAccounts.value.length > 1 && currentAccount.value.name
);
const sortedCurrentUserAccounts = computed(() => {
return [...(currentUser.value.accounts || [])].sort((a, b) =>
a.name.localeCompare(b.name)
);
});
const onChangeAccount = newId => {
const accountUrl = `/app/accounts/${newId}/dashboard`;
window.location.href = accountUrl;
@@ -70,7 +76,7 @@ const emitNewAccount = () => {
<DropdownBody v-if="showAccountSwitcher" class="min-w-80 z-50">
<DropdownSection :title="t('SIDEBAR_ITEMS.SWITCH_ACCOUNT')">
<DropdownItem
v-for="account in currentUser.accounts"
v-for="account in sortedCurrentUserAccounts"
:id="`account-${account.id}`"
:key="account.id"
class="cursor-pointer"
@@ -127,7 +127,6 @@ const settings = accountId => ({
meta: {
permissions: ['administrator'],
},
globalConfigFlag: 'csmlEditorHost',
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
toStateName: 'agent_bots',
featureFlag: FEATURE_FLAGS.AGENT_BOTS,
@@ -49,13 +49,6 @@ export default {
return !!this.menuItem.children;
},
isMenuItemVisible() {
if (this.menuItem.globalConfigFlag) {
// this checks for the `csmlEditorHost` flag in the global config
// if this is present, we toggle the CSML editor menu item
// TODO: This is very specific, and can be handled better, fix it
return !!this.globalConfig[this.menuItem.globalConfigFlag];
}
let isFeatureEnabled = true;
if (this.menuItem.featureFlag) {
isFeatureEnabled = this.isFeatureEnabledonAccount(
@@ -218,14 +218,14 @@ const emitDateRange = () => {
/>
<div
v-if="showDatePicker"
class="flex absolute top-9 ltr:left-0 rtl:right-0 z-30 shadow-md select-none w-[880px] h-[490px] rounded-2xl border border-slate-50 dark:border-slate-800 bg-white dark:bg-slate-800"
class="flex absolute top-9 ltr:left-0 rtl:right-0 z-30 shadow-md select-none w-[880px] h-[490px] rounded-2xl bg-n-alpha-3 backdrop-blur-[100px] border-0 outline outline-1 outline-n-container"
>
<CalendarDateRange
:selected-range="selectedRange"
@set-range="setDateRange"
/>
<div
class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-slate-50 dark:border-slate-700/50"
class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-n-strong"
>
<div class="flex justify-around h-fit">
<!-- Calendars for Start and End Dates -->
@@ -251,12 +251,12 @@ const emitDateRange = () => {
@validate="updateManualInput($event, calendar)"
@error="handleManualInputError($event)"
/>
<div class="py-5 border-b border-slate-50 dark:border-slate-700/50">
<div class="py-5 border-b border-n-strong">
<div
class="flex flex-col items-center gap-2 px-5 min-w-[340px] max-h-[352px]"
:class="
calendar === START_CALENDAR &&
'ltr:border-r rtl:border-l border-slate-50 dark:border-slate-700/50'
'ltr:border-r rtl:border-l border-n-strong'
"
>
<CalendarYear
@@ -1,6 +1,8 @@
<script setup>
import { CALENDAR_PERIODS } from '../helpers/DatePickerHelper';
import NextButton from 'dashboard/components-next/button/Button.vue';
defineProps({
calendarType: {
type: String,
@@ -38,42 +40,38 @@ const onClickSetView = (type, mode) => {
<template>
<div class="flex items-start justify-between w-full h-9">
<button
class="p-1 rounded-lg hover:bg-slate-75 dark:hover:bg-slate-700/50 rtl:rotate-180"
<NextButton
slate
ghost
xs
icon="i-lucide-chevron-left"
class="rtl:rotate-180"
@click="onClickPrev(calendarType)"
>
<fluent-icon
icon="chevron-left"
size="14"
class="text-slate-900 dark:text-slate-50"
/>
</button>
/>
<div class="flex items-center gap-1">
<button
v-if="firstButtonLabel"
class="p-0 text-sm font-medium text-center text-slate-800 dark:text-slate-50 hover:text-woot-600 dark:hover:text-woot-600"
class="p-0 text-sm font-medium text-center text-n-slate-12 hover:text-n-brand"
@click="onClickSetView(calendarType, viewMode)"
>
{{ firstButtonLabel }}
</button>
<button
v-if="buttonLabel"
class="p-0 text-sm font-medium text-center text-slate-800 dark:text-slate-50"
:class="{ 'hover:text-woot-600 dark:hover:text-woot-600': viewMode }"
class="p-0 text-sm font-medium text-center text-n-slate-12"
:class="{ 'hover:text-n-brand': viewMode }"
@click="onClickSetView(calendarType, YEAR)"
>
{{ buttonLabel }}
</button>
</div>
<button
class="p-1 rounded-lg hover:bg-slate-75 dark:hover:bg-slate-700/50 rtl:rotate-180"
<NextButton
slate
ghost
xs
icon="i-lucide-chevron-right"
class="rtl:rotate-180"
@click="onClickNext(calendarType)"
>
<fluent-icon
icon="chevron-right"
size="14"
class="text-slate-900 dark:text-slate-50"
/>
</button>
/>
</div>
</template>
@@ -65,7 +65,7 @@ const validateDate = () => {
<input
v-model="localDateValue"
type="text"
class="reset-base border bg-slate-25 dark:bg-slate-900 ring-offset-ash-900 border-slate-50 dark:border-slate-700/50 w-full disabled:text-slate-200 dark:disabled:text-slate-700 disabled:cursor-not-allowed text-slate-800 dark:text-slate-50 px-1.5 py-1 text-sm rounded-xl h-10"
class="!text-sm !mb-0 disabled:!outline-n-strong"
:placeholder="dateFormat"
:disabled="isDisabled"
@keypress.enter="validateDate"
@@ -18,7 +18,7 @@ const setDateRange = range => {
<template>
<div class="w-[200px] flex flex-col items-start">
<h4
class="w-full px-5 py-4 text-sm font-medium capitalize text-start text-slate-600 dark:text-slate-200"
class="w-full px-5 py-4 text-sm font-medium capitalize text-start text-n-slate-12"
>
{{ $t('DATE_PICKER.DATE_RANGE_OPTIONS.TITLE') }}
</h4>
@@ -26,11 +26,11 @@ const setDateRange = range => {
<button
v-for="range in dateRanges"
:key="range.label"
class="w-full px-5 py-3 text-sm font-medium truncate border-none rounded-none text-start hover:bg-slate-50 dark:hover:bg-slate-700"
class="w-full px-5 py-3 text-sm font-medium truncate border-none rounded-none text-start hover:bg-n-alpha-2 dark:hover:bg-n-solid-3"
:class="
range.value === selectedRange
? 'text-slate-800 dark:text-slate-50 bg-slate-50 dark:bg-slate-700'
: 'text-slate-600 dark:text-slate-200'
? 'text-n-slate-12 bg-n-alpha-1 dark:bg-n-solid-active'
: 'text-n-slate-12'
"
@click="setDateRange(range)"
>
@@ -1,4 +1,6 @@
<script setup>
import NextButton from 'dashboard/components-next/button/Button.vue';
const emit = defineEmits(['clear', 'change']);
const onClickClear = () => {
@@ -11,18 +13,19 @@ const onClickApply = () => {
</script>
<template>
<div class="h-[56px] flex justify-between px-5 py-3 items-center">
<button
class="p-1.5 rounded-lg w-fit text-sm font-medium text-slate-600 dark:text-slate-200 hover:text-slate-800 dark:hover:text-slate-100"
<div class="h-[56px] flex justify-between gap-2 px-2 py-3 items-center">
<NextButton
slate
ghost
sm
:label="$t('DATE_PICKER.CLEAR_BUTTON')"
@click="onClickClear"
>
{{ $t('DATE_PICKER.CLEAR_BUTTON') }}
</button>
<button
class="p-1.5 rounded-lg w-fit text-sm font-medium text-woot-500 dark:text-woot-300 hover:text-woot-700 dark:hover:text-woot-500"
/>
<NextButton
sm
ghost
:label="$t('DATE_PICKER.APPLY_BUTTON')"
@click="onClickApply"
>
{{ $t('DATE_PICKER.APPLY_BUTTON') }}
</button>
/>
</div>
</template>
@@ -71,10 +71,12 @@ const selectMonth = index => {
<button
v-for="(month, index) in months"
:key="index"
class="p-2 text-sm font-medium text-center text-slate-800 dark:text-slate-50 w-[92px] h-10 rounded-lg py-2.5 px-2 hover:bg-slate-75 dark:hover:bg-slate-700"
class="p-2 text-sm font-medium text-center text-n-slate-12 w-[92px] h-10 rounded-lg py-2.5 px-2"
:class="{
'bg-woot-600 dark:bg-woot-600 text-white dark:text-white hover:bg-woot-500 dark:bg-woot-700':
'bg-n-brand text-white hover:bg-n-blue-10':
index === activeMonthIndex,
'hover:bg-n-alpha-2 dark:hover:bg-n-solid-3':
index !== activeMonthIndex,
}"
@click="selectMonth(index)"
>
@@ -107,17 +107,16 @@ const isNextDayInRange = day => {
};
const dayClasses = day => ({
'text-slate-500 dark:text-slate-400 pointer-events-none':
!isInCurrentMonth(day),
'text-slate-800 dark:text-slate-50 hover:text-slate-800 dark:hover:text-white hover:bg-woot-100 dark:hover:bg-woot-700':
'text-n-slate-10 pointer-events-none': !isInCurrentMonth(day),
'text-n-slate-12 hover:text-n-slate-12 hover:bg-n-blue-6 dark:hover:bg-n-blue-7':
isInCurrentMonth(day),
'bg-woot-600 dark:bg-woot-600 text-white dark:text-white':
'bg-n-brand text-white':
isSelectedStartOrEndDate(day) && isInCurrentMonth(day),
'bg-woot-50 dark:bg-woot-800':
'bg-n-blue-4 dark:bg-n-blue-5':
(isInRange(day) || isHoveringInRange(day)) &&
!isSelectedStartOrEndDate(day) &&
isInCurrentMonth(day),
'outline outline-1 outline-woot-200 -outline-offset-1 dark:outline-woot-700 text-woot-600 dark:text-woot-400':
'outline outline-1 outline-n-blue-8 -outline-offset-1 !text-n-blue-text':
isToday(props.currentDate, day) && !isSelectedStartOrEndDate(day),
});
</script>
@@ -164,7 +163,7 @@ const dayClasses = day => ({
!isLastDayOfMonth(day) &&
isInCurrentMonth(day)
"
class="absolute bottom-0 w-6 h-8 ltr:-right-4 rtl:-left-4 bg-woot-50 dark:bg-woot-800 -z-10"
class="absolute bottom-0 w-6 h-8 ltr:-right-4 rtl:-left-4 bg-n-blue-4 dark:bg-n-blue-5 -z-10"
/>
</div>
</div>
@@ -72,10 +72,10 @@ const selectYear = year => {
<button
v-for="year in years"
:key="year"
class="p-2 text-sm font-medium text-center text-slate-800 dark:text-slate-50 w-[144px] h-10 rounded-lg py-2.5 px-2 hover:bg-slate-75 dark:hover:bg-slate-700"
class="p-2 text-sm font-medium text-center text-n-slate-12 w-[144px] h-10 rounded-lg py-2.5 px-2"
:class="{
'bg-woot-600 dark:bg-woot-600 text-white dark:text-white hover:bg-woot-500 dark:hover:bg-woot-700':
year === activeYear,
'bg-n-brand text-white hover:bg-n-blue-10': year === activeYear,
'hover:bg-n-alpha-2 dark:hover:bg-n-solid-3': year !== activeYear,
}"
@click="selectYear(year)"
>
@@ -48,7 +48,7 @@ const openDatePicker = () => {
<template>
<button
class="inline-flex relative items-center rounded-lg gap-2 py-1.5 px-3 h-8 bg-slate-50 dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800 active:bg-slate-75 dark:active:bg-slate-800"
class="inline-flex relative items-center rounded-lg gap-2 py-1.5 px-3 h-8 bg-n-alpha-2 hover:bg-n-alpha-1 active:bg-n-alpha-1"
@click="openDatePicker"
>
<fluent-icon
@@ -17,6 +17,9 @@ export default {
hasFbConfigured() {
return window.chatwootConfig?.fbAppId;
},
hasInstagramConfigured() {
return window.chatwootConfig?.instagramAppId;
},
isActive() {
const { key } = this.channel;
if (Object.keys(this.enabledFeatures).length === 0) {
@@ -32,6 +35,12 @@ export default {
return this.enabledFeatures.channel_email;
}
if (key === 'instagram') {
return (
this.enabledFeatures.channel_instagram && this.hasInstagramConfigured
);
}
return [
'website',
'twilio',
@@ -40,6 +49,7 @@ export default {
'sms',
'telegram',
'line',
'instagram',
].includes(key);
},
},
@@ -220,6 +220,7 @@ const plugins = computed(() => {
trigger: '@',
showMenu: showUserMentions,
searchTerm: mentionSearchKey,
isAllowed: () => props.isPrivate,
}),
createSuggestionPlugin({
trigger: '/',
@@ -774,10 +775,24 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
}
.ProseMirror-prompt {
@apply z-[9999] bg-slate-25 dark:bg-slate-700 rounded-md border border-solid border-slate-75 dark:border-slate-800 shadow-lg;
@apply z-[9999] bg-n-alpha-3 backdrop-blur-[100px] border border-n-strong p-6 shadow-xl rounded-xl;
h5 {
@apply dark:text-slate-25 text-slate-800;
@apply text-n-slate-12 mb-1.5;
}
.ProseMirror-prompt-buttons {
button {
@apply h-8 px-3;
&[type='submit'] {
@apply bg-n-brand text-white hover:bg-n-brand/90;
}
&[type='button'] {
@apply bg-n-slate-9/10 text-n-slate-12 hover:bg-n-slate-9/20;
}
}
}
}
@@ -328,11 +328,24 @@ export default {
}
.ProseMirror-prompt {
z-index: var(--z-index-highest);
background: var(--white);
box-shadow: var(--shadow-large);
border-radius: var(--border-radius-normal);
border: 1px solid var(--color-border);
min-width: 25rem;
@apply z-[9999] bg-n-alpha-3 min-w-80 backdrop-blur-[100px] border border-n-strong p-6 shadow-xl rounded-xl;
h5 {
@apply text-n-slate-12 mb-1.5;
}
.ProseMirror-prompt-buttons {
button {
@apply h-8 px-3;
&[type='submit'] {
@apply bg-n-brand text-white hover:bg-n-brand/90;
}
&[type='button'] {
@apply bg-n-slate-9/10 text-n-slate-12 hover:bg-n-slate-9/20;
}
}
}
}
</style>
@@ -10,6 +10,7 @@ import {
ALLOWED_FILE_TYPES,
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
ALLOWED_FILE_TYPES_FOR_LINE,
ALLOWED_FILE_TYPES_FOR_INSTAGRAM,
} from 'shared/constants/messages';
import VideoCallButton from '../VideoCallButton.vue';
import AIAssistanceButton from '../AIAssistanceButton.vue';
@@ -113,6 +114,10 @@ export default {
type: String,
required: true,
},
conversationType: {
type: String,
default: '',
},
},
emits: [
'replaceText',
@@ -187,6 +192,9 @@ export default {
showAudioPlayStopButton() {
return this.showAudioRecorder && this.isRecordingAudio;
},
isInstagramDM() {
return this.conversationType === 'instagram_direct_message';
},
allowedFileTypes() {
if (this.isATwilioWhatsAppChannel) {
return ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP;
@@ -194,6 +202,10 @@ export default {
if (this.isALineChannel) {
return ALLOWED_FILE_TYPES_FOR_LINE;
}
if (this.isAnInstagramChannel || this.isInstagramDM) {
return ALLOWED_FILE_TYPES_FOR_INSTAGRAM;
}
return ALLOWED_FILE_TYPES;
},
enableDragAndDrop() {
@@ -38,6 +38,7 @@ import { REPLY_POLICY } from 'shared/constants/links';
import wootConstants from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { FEATURE_FLAGS } from '../../../featureFlags';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
import NextButton from 'dashboard/components-next/button/Button.vue';
@@ -203,6 +204,22 @@ export default {
return contactLastSeenAt;
},
// Check there is a instagram inbox exists with the same instagram_id
hasDuplicateInstagramInbox() {
const instagramId = this.inbox.instagram_id;
const { additional_attributes: additionalAttributes = {} } = this.inbox;
const instagramInbox =
this.$store.getters['inboxes/getInstagramInboxByInstagramId'](
instagramId
);
return (
this.inbox.channel_type === INBOX_TYPES.FB &&
additionalAttributes.type === 'instagram_direct_message' &&
instagramInbox
);
},
replyWindowBannerMessage() {
if (this.isAWhatsAppChannel) {
return this.$t('CONVERSATION.TWILIO_WHATSAPP_CAN_REPLY');
@@ -212,24 +229,37 @@ export default {
if (additionalAttributes) {
const {
agent_reply_time_window_message: agentReplyTimeWindowMessage,
agent_reply_time_window: agentReplyTimeWindow,
} = additionalAttributes;
return agentReplyTimeWindowMessage;
return (
agentReplyTimeWindowMessage ||
this.$t('CONVERSATION.API_HOURS_WINDOW', {
hours: agentReplyTimeWindow,
})
);
}
return '';
}
return this.$t('CONVERSATION.CANNOT_REPLY');
},
replyWindowLink() {
if (this.isAWhatsAppChannel) {
if (this.isAFacebookInbox || this.isAnInstagramChannel) {
return REPLY_POLICY.FACEBOOK;
}
if (this.isAWhatsAppCloudChannel) {
return REPLY_POLICY.WHATSAPP_CLOUD;
}
if (!this.isAPIInbox) {
return REPLY_POLICY.TWILIO_WHATSAPP;
}
return '';
},
replyWindowLinkText() {
if (this.isAWhatsAppChannel) {
if (
this.isAWhatsAppChannel ||
this.isAFacebookInbox ||
this.isAnInstagramChannel
) {
return this.$t('CONVERSATION.24_HOURS_WINDOW');
}
if (!this.isAPIInbox) {
@@ -557,11 +587,17 @@ export default {
<Banner
v-if="!currentChat.can_reply"
color-scheme="alert"
class="mt-2 mx-2 rounded-lg overflow-hidden"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="replyWindowBannerMessage"
:href-link="replyWindowLink"
:href-link-text="replyWindowLinkText"
/>
<Banner
v-else-if="hasDuplicateInstagramInbox"
color-scheme="alert"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="$t('CONVERSATION.OLD_INSTAGRAM_INBOX_REPLY_BANNER')"
/>
<div class="flex justify-end">
<NextButton
faded
@@ -241,15 +241,27 @@ export default {
if (this.isAFacebookInbox) {
return MESSAGE_MAX_LENGTH.FACEBOOK;
}
if (this.isAWhatsAppChannel) {
if (this.isAnInstagramChannel) {
return MESSAGE_MAX_LENGTH.INSTAGRAM;
}
if (this.isATwilioWhatsAppChannel) {
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
}
if (this.isAWhatsAppCloudChannel) {
return MESSAGE_MAX_LENGTH.WHATSAPP_CLOUD;
}
if (this.isASmsInbox) {
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
}
if (this.isAnEmailChannel) {
return MESSAGE_MAX_LENGTH.EMAIL;
}
if (this.isATwilioSMSChannel) {
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
}
if (this.isAWhatsAppChannel) {
return MESSAGE_MAX_LENGTH.WHATSAPP_CLOUD;
}
return MESSAGE_MAX_LENGTH.GENERAL;
},
showFileUpload() {
@@ -261,7 +273,8 @@ export default {
this.isAnEmailChannel ||
this.isASmsInbox ||
this.isATelegramChannel ||
this.isALineChannel
this.isALineChannel ||
this.isAnInstagramChannel
);
},
replyButtonLabel() {
@@ -387,9 +400,14 @@ export default {
},
},
watch: {
currentChat(conversation) {
currentChat(conversation, oldConversation) {
const { can_reply: canReply } = conversation;
this.setCCAndToEmailsFromLastChat();
if (oldConversation && oldConversation.id !== conversation.id) {
// Only update email fields when switching to a completely different conversation (by ID)
// This prevents overwriting user input (e.g., CC/BCC fields) when performing actions
// like self-assign or other updates that do not actually change the conversation context
this.setCCAndToEmailsFromLastChat();
}
if (this.isOnPrivateNote) {
return;
@@ -405,13 +423,12 @@ export default {
},
// When moving from one conversation to another, the store may not have the
// list of all the messages. A fetch is subsequently made to get the messages.
// However, this update does not trigger the `currentChat` watcher.
// We can add a deep watcher to it, but then, that would be too broad of a net to cast
// And would impact performance too. So we watch the messages directly.
// The watcher here is `deep` too, because the messages array is mutated and
// not replaced. So, a shallow watcher would not catch the change.
'currentChat.messages': {
handler() {
// This watcher handles two main cases:
// 1. When switching conversations and messages are fetched/updated, ensures CC/BCC fields are set from the latest OUTGOING/INCOMING email (not activity/private messages).
// 2. Fixes and issue where CC/BCC fields could be reset/lost after assignment/activity actions or message mutations that did not represent a true email context change.
lastEmail: {
handler(lastEmail) {
if (!lastEmail) return;
this.setCCAndToEmailsFromLastChat();
},
deep: true,
@@ -685,7 +702,11 @@ export default {
this.isATwilioWhatsAppChannel ||
this.isAWhatsAppCloudChannel ||
this.is360DialogWhatsAppChannel;
if (isOnWhatsApp && !this.isPrivate) {
// When users send messages containing both text and attachments on Instagram, Instagram treats them as separate messages.
// Although Chatwoot combines these into a single message, Instagram sends separate echo events for each component.
// This can create duplicate messages in Chatwoot. To prevent this issue, we'll handle text and attachments as separate messages.
const isOnInstagram = this.isAnInstagramChannel;
if ((isOnWhatsApp || isOnInstagram) && !this.isPrivate) {
this.sendMessageAsMultipleMessages(this.message);
} else {
const messagePayload = this.getMessagePayload(this.message);
@@ -702,7 +723,7 @@ export default {
}
},
sendMessageAsMultipleMessages(message) {
const messages = this.getMessagePayloadForWhatsapp(message);
const messages = this.getMultipleMessagesPayload(message);
messages.forEach(messagePayload => {
this.sendMessage(messagePayload);
});
@@ -934,11 +955,11 @@ export default {
return payload;
},
getMessagePayloadForWhatsapp(message) {
getMultipleMessagesPayload(message) {
const multipleMessagePayload = [];
if (this.attachedFiles && this.attachedFiles.length) {
let caption = message;
let caption = this.isAnInstagramChannel ? '' : message;
this.attachedFiles.forEach(attachment => {
const attachedFile = this.globalConfig.directUploadsEnabled
? attachment.blobSignedId
@@ -953,9 +974,19 @@ export default {
attachmentPayload = this.setReplyToInPayload(attachmentPayload);
multipleMessagePayload.push(attachmentPayload);
caption = '';
// For WhatsApp, only the first attachment gets a caption
if (!this.isAnInstagramChannel) caption = '';
});
} else {
}
const hasNoAttachments =
!this.attachedFiles || !this.attachedFiles.length;
// For Instagram, we need a separate text message
// For WhatsApp, we only need a text message if there are no attachments
if (
(this.isAnInstagramChannel && this.message) ||
(!this.isAnInstagramChannel && hasNoAttachments)
) {
let messagePayload = {
conversationId: this.currentChat.id,
message,
@@ -1076,7 +1107,7 @@ export default {
v-if="showSelfAssignBanner"
action-button-variant="ghost"
color-scheme="secondary"
class="banner--self-assign mx-2 mb-2 rounded-lg"
class="mx-2 mb-2 rounded-lg banner--self-assign"
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
has-action-button
:action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')"
@@ -1135,7 +1166,7 @@ export default {
v-else-if="!showRichContentEditor"
ref="messageInput"
v-model="message"
class="input"
class="rounded-none input"
:placeholder="messagePlaceHolder"
:min-height="4"
:signature="signatureToApply"
@@ -1195,6 +1226,7 @@ export default {
:mode="replyType"
:on-file-upload="onFileUpload"
:on-send="onSendReply"
:conversation-type="conversationType"
:recording-audio-duration-text="recordingAudioDurationText"
:recording-audio-state="recordingAudioState"
:send-button-text="replyButtonLabel"
@@ -69,10 +69,6 @@ const onAgentSelect = index => {
v-if="items.length"
ref="tagAgentsRef"
class="vertical dropdown menu mention--box bg-n-solid-1 p-1 rounded-xl text-sm overflow-auto absolute w-full z-20 shadow-md left-0 leading-[1.2] bottom-full max-h-[12.5rem] border border-solid border-n-strong"
:class="{
'border-b-[0.5rem] border-solid border-white dark:!border-slate-700':
items.length <= 4,
}"
>
<li
v-for="(agent, index) in items"
@@ -56,7 +56,7 @@ const unlinkIssue = () => {
<template>
<div
class="absolute flex flex-col items-start bg-white dark:bg-slate-800 z-50 px-4 py-3 border border-solid border-ash-200 w-[384px] rounded-xl gap-4 max-h-96 overflow-auto"
class="absolute flex flex-col items-start bg-n-alpha-3 backdrop-blur-[100px] z-50 px-4 py-3 border border-solid border-n-container w-[384px] rounded-xl gap-4 max-h-96 overflow-auto"
>
<div class="flex flex-col w-full">
<IssueHeader
@@ -66,37 +66,37 @@ const unlinkIssue = () => {
@unlink-issue="unlinkIssue"
/>
<span class="mt-2 text-sm font-medium text-ash-900">
<span class="mt-2 text-sm font-medium text-n-slate-12">
{{ issue.title }}
</span>
<span
v-if="issue.description"
class="mt-1 text-sm text-ash-800 line-clamp-3"
class="mt-1 text-sm text-n-slate-11 line-clamp-3"
>
{{ issue.description }}
</span>
</div>
<div class="flex flex-row items-center h-6 gap-2">
<UserAvatarWithName v-if="assignee" :user="assignee" class="py-1" />
<div v-if="assignee" class="w-px h-3 bg-ash-200" />
<div v-if="assignee" class="w-px h-3 bg-n-slate-4" />
<div class="flex items-center gap-1 py-1">
<fluent-icon
icon="status"
size="14"
:style="{ color: issue.state.color }"
/>
<h6 class="text-xs text-ash-900">
<h6 class="text-xs text-n-slate-12">
{{ issue.state.name }}
</h6>
</div>
<div v-if="priorityLabel" class="w-px h-3 bg-ash-200" />
<div v-if="priorityLabel" class="w-px h-3 bg-n-slate-4" />
<div v-if="priorityLabel" class="flex items-center gap-1 py-1">
<fluent-icon
:icon="`priority-${priorityLabel.toLowerCase()}`"
size="14"
view-box="0 0 12 12"
/>
<h6 class="text-xs text-ash-900">{{ priorityLabel }}</h6>
<h6 class="text-xs text-n-slate-12">{{ priorityLabel }}</h6>
</div>
</div>
<div v-if="labels.length" class="flex flex-wrap items-center gap-1">
@@ -111,7 +111,7 @@ const unlinkIssue = () => {
/>
</div>
<div class="flex items-center">
<span class="text-xs text-ash-800">
<span class="text-xs text-n-slate-11">
{{
$t('INTEGRATION_SETTINGS.LINEAR.ISSUE.CREATED_AT', {
createdAt: formattedDate,
@@ -100,14 +100,17 @@ onMounted(() => {
</script>
<template>
<div class="relative" :class="{ group: linkedIssue }">
<div
class="relative after:content-[''] after:h-5 after:bg-transparent after:top-5 after:w-full after:block after:absolute after:z-0"
:class="{ group: linkedIssue }"
>
<Button
v-on-clickaway="closeIssue"
v-tooltip="tooltipText"
sm
ghost
slate
class="!gap-1"
class="!gap-1 group-hover:bg-n-alpha-2"
@click="openIssue"
>
<fluent-icon
@@ -124,7 +127,7 @@ onMounted(() => {
v-if="linkedIssue"
:issue="linkedIssue.issue"
:link-id="linkedIssue.id"
class="absolute right-0 top-[40px] invisible group-hover:visible"
class="absolute right-0 top-[36px] invisible group-hover:visible"
@unlink-issue="unlinkIssue"
/>
<woot-modal
@@ -0,0 +1,39 @@
import { ref } from 'vue';
import { useTranslations } from '../useTranslations';
describe('useTranslations', () => {
it('returns false and null when contentAttributes is null', () => {
const contentAttributes = ref(null);
const { hasTranslations, translationContent } =
useTranslations(contentAttributes);
expect(hasTranslations.value).toBe(false);
expect(translationContent.value).toBeNull();
});
it('returns false and null when translations are missing', () => {
const contentAttributes = ref({});
const { hasTranslations, translationContent } =
useTranslations(contentAttributes);
expect(hasTranslations.value).toBe(false);
expect(translationContent.value).toBeNull();
});
it('returns false and null when translations is an empty object', () => {
const contentAttributes = ref({ translations: {} });
const { hasTranslations, translationContent } =
useTranslations(contentAttributes);
expect(hasTranslations.value).toBe(false);
expect(translationContent.value).toBeNull();
});
it('returns true and correct translation content when translations exist', () => {
const contentAttributes = ref({
translations: { en: 'Hello' },
});
const { hasTranslations, translationContent } =
useTranslations(contentAttributes);
expect(hasTranslations.value).toBe(true);
// Should return the first translation (en: 'Hello')
expect(translationContent.value).toBe('Hello');
});
});
@@ -121,6 +121,10 @@ export const useInbox = () => {
);
});
const isAnInstagramChannel = computed(() => {
return channelType.value === INBOX_TYPES.INSTAGRAM;
});
return {
inbox,
isAFacebookInbox,
@@ -137,5 +141,6 @@ export const useInbox = () => {
isAWhatsAppCloudChannel,
is360DialogWhatsAppChannel,
isAnEmailChannel,
isAnInstagramChannel,
};
};
@@ -7,8 +7,12 @@ import { formatTime } from '@chatwoot/utils';
* @param {string} [accountSummaryKey='getAccountSummary'] - The key for accessing account summary data.
* @returns {Object} An object containing utility functions for report metrics.
*/
export function useReportMetrics(accountSummaryKey = 'getAccountSummary') {
export function useReportMetrics(
accountSummaryKey = 'getAccountSummary',
summarFetchingKey = 'getAccountSummaryFetchingStatus'
) {
const accountSummary = useMapGetter(accountSummaryKey);
const fetchingStatus = useMapGetter(summarFetchingKey);
/**
* Calculates the trend percentage for a given metric.
@@ -53,5 +57,6 @@ export function useReportMetrics(accountSummaryKey = 'getAccountSummary') {
calculateTrend,
isAverageMetricType,
displayMetric,
fetchingStatus,
};
}
@@ -0,0 +1,22 @@
import { computed } from 'vue';
/**
* Composable to extract translation state/content from contentAttributes.
* @param {Ref|Reactive} contentAttributes - Ref or reactive object containing `translations` property
* @returns {Object} { hasTranslations, translationContent }
*/
export function useTranslations(contentAttributes) {
const hasTranslations = computed(() => {
if (!contentAttributes.value) return false;
const { translations = {} } = contentAttributes.value;
return Object.keys(translations || {}).length > 0;
});
const translationContent = computed(() => {
if (!hasTranslations.value) return null;
const translations = contentAttributes.value.translations;
return translations[Object.keys(translations)[0]];
});
return { hasTranslations, translationContent };
}
@@ -6,6 +6,7 @@ export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
{ name: 'macros' },
{ name: 'conversation_info' },
{ name: 'contact_attributes' },
{ name: 'contact_notes' },
{ name: 'previous_conversation' },
{ name: 'conversation_participants' },
{ name: 'shopify_orders' },
@@ -36,7 +37,7 @@ const useConversationSidebarItemsOrder = uiSettings => {
const { conversation_sidebar_items_order: itemsOrder } = uiSettings.value;
// If the sidebar order is not set, use the default order.
if (!itemsOrder) {
return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
return [...DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER];
}
// Create a copy of itemsOrder to avoid mutating the original store object.
const itemsOrderCopy = [...itemsOrder];
+1
View File
@@ -34,6 +34,7 @@ export const FEATURE_FLAGS = {
CUSTOM_ROLES: 'custom_roles',
CHATWOOT_V4: 'chatwoot_v4',
REPORT_V4: 'report_v4',
CHANNEL_INSTAGRAM: 'channel_instagram',
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
CHAT_PRESERVE_USER_SCROLL: 'chat_preserve_user_scroll',
};
+6
View File
@@ -9,6 +9,7 @@ export const INBOX_TYPES = {
TELEGRAM: 'Channel::Telegram',
LINE: 'Channel::Line',
SMS: 'Channel::Sms',
INSTAGRAM: 'Channel::Instagram',
};
const INBOX_ICON_MAP_FILL = {
@@ -20,6 +21,7 @@ const INBOX_ICON_MAP_FILL = {
[INBOX_TYPES.EMAIL]: 'i-ri-mail-fill',
[INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-fill',
[INBOX_TYPES.LINE]: 'i-ri-line-fill',
[INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-fill',
};
const DEFAULT_ICON_FILL = 'i-ri-chat-1-fill';
@@ -33,6 +35,7 @@ const INBOX_ICON_MAP_LINE = {
[INBOX_TYPES.EMAIL]: 'i-ri-mail-line',
[INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-line',
[INBOX_TYPES.LINE]: 'i-ri-line-line',
[INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-line',
};
const DEFAULT_ICON_LINE = 'i-ri-chat-1-line';
@@ -118,6 +121,9 @@ export const getInboxClassByType = (type, phoneNumber) => {
case INBOX_TYPES.LINE:
return 'brand-line';
case INBOX_TYPES.INSTAGRAM:
return 'brand-instagram';
default:
return 'chat';
}
@@ -16,6 +16,15 @@ export const getUserPermissions = (user, accountId) => {
return currentAccount.permissions || [];
};
export const getUserRole = (user, accountId) => {
const currentAccount = getCurrentAccount(user, accountId) || {};
if (currentAccount.custom_role_id) {
return 'custom_role';
}
return currentAccount.role || 'agent';
};
const isPermissionsPresentInRoute = route =>
route.meta && route.meta.permissions;
@@ -1,6 +1,44 @@
export const buildPortalURL = portalSlug => {
const { hostURL, helpCenterURL } = window.chatwootConfig;
/**
* Formats a custom domain with https protocol if needed
* @param {string} customDomain - The custom domain to format
* @returns {string} Formatted domain with https protocol
*/
const formatCustomDomain = customDomain =>
customDomain.startsWith('https') ? customDomain : `https://${customDomain}`;
/**
* Gets the default base URL from configuration
* @returns {string} The default base URL
* @throws {Error} If no valid base URL is found
*/
const getDefaultBaseURL = () => {
const { hostURL, helpCenterURL } = window.chatwootConfig || {};
const baseURL = helpCenterURL || hostURL || '';
if (!baseURL) {
throw new Error('No valid base URL found in configuration');
}
return baseURL;
};
/**
* Gets the base URL from configuration or custom domain
* @param {string} [customDomain] - Optional custom domain for the portal
* @returns {string} The base URL for the portal
*/
const getPortalBaseURL = customDomain =>
customDomain ? formatCustomDomain(customDomain) : getDefaultBaseURL();
/**
* Builds a portal URL using the provided portal slug and optional custom domain
* @param {string} portalSlug - The slug identifier for the portal
* @param {string} [customDomain] - Optional custom domain for the portal
* @returns {string} The complete portal URL
* @throws {Error} If portalSlug is not provided or invalid
*/
export const buildPortalURL = (portalSlug, customDomain) => {
const baseURL = getPortalBaseURL(customDomain);
return `${baseURL}/hc/${portalSlug}`;
};
@@ -8,9 +46,10 @@ export const buildPortalArticleURL = (
portalSlug,
categorySlug,
locale,
articleSlug
articleSlug,
customDomain
) => {
const portalURL = buildPortalURL(portalSlug);
const portalURL = buildPortalURL(portalSlug, customDomain);
return `${portalURL}/articles/${articleSlug}`;
};
@@ -36,7 +36,7 @@ describe('#resolveActionName', () => {
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).not.toEqual(
MACRO_ACTION_TYPES[0].label
);
expect(resolveActionName('change_priority')).toEqual('Change Priority');
expect(resolveActionName('change_priority')).toEqual('CHANGE_PRIORITY'); // Translated
});
});
@@ -25,5 +25,47 @@ describe('PortalHelper', () => {
).toEqual('https://help.chatwoot.com/hc/handbook/articles/article-slug');
window.chatwootConfig = {};
});
it('returns the correct url with custom domain', () => {
window.chatwootConfig = {
hostURL: 'https://app.chatwoot.com',
helpCenterURL: 'https://help.chatwoot.com',
};
expect(
buildPortalArticleURL(
'handbook',
'culture',
'fr',
'article-slug',
'custom-domain.dev'
)
).toEqual('https://custom-domain.dev/hc/handbook/articles/article-slug');
});
it('handles https in custom domain correctly', () => {
window.chatwootConfig = {
hostURL: 'https://app.chatwoot.com',
helpCenterURL: 'https://help.chatwoot.com',
};
expect(
buildPortalArticleURL(
'handbook',
'culture',
'fr',
'article-slug',
'https://custom-domain.dev'
)
).toEqual('https://custom-domain.dev/hc/handbook/articles/article-slug');
});
it('uses hostURL when helpCenterURL is not available', () => {
window.chatwootConfig = {
hostURL: 'https://app.chatwoot.com',
helpCenterURL: '',
};
expect(
buildPortalArticleURL('handbook', 'culture', 'fr', 'article-slug')
).toEqual('https://app.chatwoot.com/hc/handbook/articles/article-slug');
});
});
});
@@ -2,23 +2,13 @@
"AGENT_BOTS": {
"HEADER": "Bots",
"LOADING_EDITOR": "Loading editor...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.You can manage your bots from this page or create new ones using the 'Configure new bot' button.",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot name",
"PLACEHOLDER": "Name your bot.",
"ERROR": "Bot name is required."
},
"DESCRIPTION": {
"LABEL": "Bot description",
"PLACEHOLDER": "What does this bot do?"
},
"BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above.",
"API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
},
"SUBMIT": "Validate and save"
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "System",
"AVATAR": {
"SUCCESS_DELETE": "Bot avatar deleted successfully",
"ERROR_DELETE": "Error deleting bot avatar, please try again"
},
"BOT_CONFIGURATION": {
"TITLE": "Select an agent bot",
@@ -32,7 +22,7 @@
"SELECT_PLACEHOLDER": "Select bot"
},
"ADD": {
"TITLE": "Configure new bot",
"TITLE": "Add Bot",
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully.",
@@ -40,16 +30,22 @@
}
},
"LIST": {
"404": "No bots found. You can create a bot by clicking the 'Configure new bot' button",
"404": "No bots found. You can create a bot by clicking the 'Add Bot' button.",
"LOADING": "Fetching bots...",
"TYPE": "Bot type"
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "Webhook URL"
}
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"TITLE": "Delete bot",
"SUBMIT": "Delete",
"CANCEL_BUTTON_TEXT": "Cancel",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
"CONFIRM": {
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure you want to delete {name}?",
"YES": "Yes, Delete",
"NO": "No, Keep"
},
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully.",
"ERROR_MESSAGE": "Could not delete bot. Please try again."
@@ -57,17 +53,44 @@
},
"EDIT": {
"BUTTON_TEXT": "Edit",
"LOADING": "Fetching bots...",
"TITLE": "Edit bot",
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully.",
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"FORM": {
"AVATAR": {
"LABEL": "Bot avatar"
},
"NAME": {
"LABEL": "Bot name",
"PLACEHOLDER": "Enter bot name",
"REQUIRED": "Bot name is required"
},
"DESCRIPTION": {
"LABEL": "Description",
"PLACEHOLDER": "What does this bot do?"
},
"WEBHOOK_URL": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "https://example.com/webhook",
"REQUIRED": "Webhook URL is required"
},
"ERRORS": {
"NAME": "Bot name is required",
"URL": "Webhook URL is required",
"VALID_URL": "Please enter a valid URL starting with http:// or https://"
},
"CANCEL": "Cancel",
"CREATE": "Create Bot",
"UPDATE": "Update Bot"
},
"WEBHOOK": {
"DESCRIPTION": "Configure a webhook bot to integrate with your custom services. The bot will receive and process events from conversations and can respond to them."
},
"TYPES": {
"WEBHOOK": "Webhook bot",
"CSML": "CSML bot"
"WEBHOOK": "Webhook bot"
}
}
}
@@ -126,6 +126,44 @@
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
},
"NONE_OPTION": "None"
"NONE_OPTION": "None",
"EVENTS": {
"CONVERSATION_CREATED": "Conversation Created",
"CONVERSATION_UPDATED": "Conversation Updated",
"MESSAGE_CREATED": "Message Created",
"CONVERSATION_OPENED": "Conversation Opened"
},
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"RESOLVE_CONVERSATION": "Resolve Conversation",
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "Change Priority",
"ADD_SLA": "Add SLA"
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Inbox",
"CONVERSATION_LANGUAGE": "Conversation Language",
"PHONE_NUMBER": "Phone Number",
"STATUS": "Status",
"BROWSER_LANGUAGE": "Browser Language",
"MAIL_SUBJECT": "Email Subject",
"COUNTRY_NAME": "Country",
"REFERER_LINK": "Referrer Link",
"ASSIGNEE_NAME": "Assignee",
"TEAM_NAME": "Team",
"PRIORITY": "Priority"
}
}
}
@@ -544,6 +544,9 @@
"WROTE": "wrote",
"YOU": "You",
"SAVE": "Save note",
"EXPAND": "Expand",
"COLLAPSE": "Collapse",
"NO_NOTES": "No notes, you can add notes from the contact details page.",
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above."
}
},
@@ -32,10 +32,12 @@
"LOADING_CONVERSATIONS": "Loading Conversations",
"CANNOT_REPLY": "You cannot reply due to",
"24_HOURS_WINDOW": "24 hour message window restriction",
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
"ASSIGN_TO_ME": "Assign to me",
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. All new messages will show up there. You wont be able to send messages from this conversation anymore.",
"REPLYING_TO": "You are replying to:",
"REMOVE_SELECTION": "Remove Selection",
"DOWNLOAD": "Download",
@@ -293,6 +295,7 @@
"CONVERSATION_ACTIONS": "Conversation Actions",
"CONVERSATION_LABELS": "Conversation Labels",
"CONVERSATION_INFO": "Conversation Information",
"CONTACT_NOTES": "Contact Notes",
"CONTACT_ATTRIBUTES": "Contact Attributes",
"PREVIOUS_CONVERSATION": "Previous Conversations",
"MACROS": "Macros",
@@ -1,5 +1,11 @@
{
"GENERAL_SETTINGS": {
"LIMIT_MESSAGES": {
"CONVERSATION": "You have exceeded the conversation limit. Hacker plan allows only 500 conversations.",
"INBOXES": "You have exceeded the inbox limit. Hacker plan only supports website live-chat. Additional inboxes like email, WhatsApp etc. require a paid plan.",
"AGENTS": "You have exceeded the agent limit. Hacker plan allows only 2 agents.",
"NON_ADMIN": "Please contact your administrator to upgrade the plan and continue using all features."
},
"TITLE": "Account settings",
"SUBMIT": "Update settings",
"BACK": "Back",
@@ -8,6 +14,26 @@
"ERROR": "Could not update settings, try again!",
"SUCCESS": "Successfully updated account settings"
},
"ACCOUNT_DELETE_SECTION": {
"TITLE": "Delete your Account",
"NOTE": "Once you delete your account, all your data will be deleted.",
"BUTTON_TEXT": "Delete Your Account",
"CONFIRM": {
"TITLE": "Delete Account",
"MESSAGE": "Deleting your Account is irreversible. Enter your account name below to confirm you want to permanently delete it.",
"BUTTON_TEXT": "Delete",
"DISMISS": "Cancel",
"PLACE_HOLDER": "Please type {accountName} to confirm"
},
"SUCCESS": "Account marked for deletion",
"FAILURE": "Could not delete account, try again!",
"SCHEDULED_DELETION": {
"TITLE": "Account Scheduled for Deletion",
"MESSAGE_MANUAL": "This account is scheduled for deletion on {deletionDate}. This was requested by an administrator. You can cancel the deletion before this date.",
"MESSAGE_INACTIVITY": "This account is scheduled for deletion on {deletionDate} due to account inactivity. You can cancel the deletion before this date.",
"CLEAR_BUTTON": "Cancel Scheduled Deletion"
}
},
"FORM": {
"ERROR": "Please fix form errors",
"GENERAL_SECTION": {
@@ -51,6 +77,7 @@
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
"LEARN_MORE": "Learn more",
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
"UPGRADE": "Upgrade to continue using Chatwoot",
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
"OPEN_BILLING": "Open billing"
},
@@ -696,7 +696,8 @@
"SLUG": {
"LABEL": "Slug",
"PLACEHOLDER": "user-guide",
"ERROR": "Slug is required"
"ERROR": "Slug is required",
"FORMAT_ERROR": "Please enter a valid slug, for eg: user-guide"
}
},
"PORTAL_SETTINGS": {
@@ -43,7 +43,17 @@
"INBOX_NAME": "Inbox Name",
"ADD_NAME": "Add a name for your inbox",
"PICK_NAME": "Pick a Name for your Inbox",
"PICK_A_VALUE": "Pick a value"
"PICK_A_VALUE": "Pick a value",
"CREATE_INBOX": "Create Inbox"
},
"INSTAGRAM": {
"CONTINUE_WITH_INSTAGRAM": "Continue with Instagram",
"CONNECT_YOUR_INSTAGRAM_PROFILE": "Connect your Instagram Profile",
"HELP": "To add your Instagram profile as a channel, you need to authenticate your Instagram Profile by clicking on 'Continue with Instagram' ",
"ERROR_MESSAGE": "There was an error connecting to Instagram, please try again",
"ERROR_AUTH": "There was an error connecting to Instagram, please try again",
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You wont be able to send/receive Instagram messages from this inbox anymore."
},
"TWITTER": {
"HELP": "To add your Twitter profile as a channel, you need to authenticate your Twitter Profile by clicking on 'Sign in with Twitter' ",
@@ -753,7 +763,8 @@
"EMAIL": "Email",
"TELEGRAM": "Telegram",
"LINE": "Line",
"API": "API Channel"
"API": "API Channel",
"INSTAGRAM": "Instagram"
}
}
}
@@ -329,12 +329,21 @@
"HEADER_KNOW_MORE": "Know more",
"COPILOT": {
"SEND_MESSAGE": "Send message...",
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
"LOADER": "Captain is thinking",
"YOU": "You",
"USE": "Use this",
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
},
"PLAYGROUND": {
"USER": "You",
"ASSISTANT": "Assistant",
"MESSAGE_PLACEHOLDER": "Type your message...",
"HEADER": "Playground",
"DESCRIPTION": "Use this playground to send messages to your assistant and check if it responds accurately, quickly, and in the tone you expect.",
"CREDIT_NOTE": "Messages sent here will count toward your Captain credits."
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
"AVAILABLE_ON": "Captain is not available on the free plan.",
@@ -373,20 +382,41 @@
"ERROR_MESSAGE": "There was an error creating the assistant, please try again."
},
"FORM": {
"UPDATE": "Update",
"SECTIONS": {
"BASIC_INFO": "Basic Information",
"SYSTEM_MESSAGES": "System Messages",
"INSTRUCTIONS": "Instructions",
"FEATURES": "Features",
"TOOLS": "Tools "
},
"NAME": {
"LABEL": "Assistant Name",
"PLACEHOLDER": "Enter a name for the assistant",
"ERROR": "Please provide a name for the assistant"
"LABEL": "Name",
"PLACEHOLDER": "Enter assistant name"
},
"DESCRIPTION": {
"LABEL": "Assistant Description",
"PLACEHOLDER": "Describe how and where this assistant will be used",
"ERROR": "A description is required"
"LABEL": "Description",
"PLACEHOLDER": "Enter assistant description"
},
"PRODUCT_NAME": {
"LABEL": "Product Name",
"PLACEHOLDER": "Enter the name of the product this assistant is designed for",
"ERROR": "The product name is required"
"PLACEHOLDER": "Enter product name"
},
"WELCOME_MESSAGE": {
"LABEL": "Welcome Message",
"PLACEHOLDER": "Enter welcome message"
},
"HANDOFF_MESSAGE": {
"LABEL": "Handoff Message",
"PLACEHOLDER": "Enter handoff message"
},
"RESOLUTION_MESSAGE": {
"LABEL": "Resolution Message",
"PLACEHOLDER": "Enter resolution message"
},
"INSTRUCTIONS": {
"LABEL": "Instructions",
"PLACEHOLDER": "Enter instructions for the assistant"
},
"FEATURES": {
"TITLE": "Features",
@@ -397,7 +427,8 @@
"EDIT": {
"TITLE": "Update the assistant",
"SUCCESS_MESSAGE": "The assistant has been successfully updated",
"ERROR_MESSAGE": "There was an error updating the assistant, please try again."
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"OPTIONS": {
"EDIT_ASSISTANT": "Edit Assistant",
@@ -83,6 +83,22 @@
"ACTION_PARAMETERS_REQUIRED": "Action parameters are required",
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
},
"ACTIONS": {
"ASSIGN_TEAM": "Assign a Team",
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"RESOLVE_CONVERSATION": "Resolve Conversation",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "Change Priority",
"ADD_PRIVATE_NOTE": "Add a Private Note",
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
}
}
}
@@ -387,7 +387,8 @@
"LABEL": "Company Name",
"PLACEHOLDER": "Wayne Enterprises"
},
"SUBMIT": "Submit"
"SUBMIT": "Submit",
"CANCEL": "Cancel"
}
},
"KEYBOARD_SHORTCUTS": {
@@ -2,23 +2,13 @@
"AGENT_BOTS": {
"HEADER": "الروبوتات",
"LOADING_EDITOR": "جار جلب المحرر...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.You can manage your bots from this page or create new ones using the 'Configure new bot' button.",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "اسم الروبوت",
"PLACEHOLDER": "قم بتسمية الروبوت الخاص بك.",
"ERROR": "اسم الروبوت مطلوب."
},
"DESCRIPTION": {
"LABEL": "وصف الروبوت",
"PLACEHOLDER": "ماذا يفعل هذا الروبوت؟"
},
"BOT_CONFIG": {
"ERROR": "يرجى إدخال تكوين نبوت CSML الخاص بك أعلاه.",
"API_ERROR": "تكوين CSML الخاص بك غير صالح. يرجى إصلاحه والمحاولة مرة أخرى."
},
"SUBMIT": "التحقق والحفظ"
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "النظام",
"AVATAR": {
"SUCCESS_DELETE": "Bot avatar deleted successfully",
"ERROR_DELETE": "Error deleting bot avatar, please try again"
},
"BOT_CONFIGURATION": {
"TITLE": "اختر الروبوت",
@@ -32,7 +22,7 @@
"SELECT_PLACEHOLDER": "اختر الروبوت"
},
"ADD": {
"TITLE": "تكوين روبوت جديد",
"TITLE": "Add Bot",
"CANCEL_BUTTON_TEXT": "إلغاء",
"API": {
"SUCCESS_MESSAGE": "تمت إضافة الروبوت بنجاح.",
@@ -40,16 +30,22 @@
}
},
"LIST": {
"404": "لم يتم العثور على أي روبوتات. يمكنك إنشاء الروبوت بالنقر على زر 'تكوين روبوت جديد' ↗",
"404": "No bots found. You can create a bot by clicking the 'Add Bot' button.",
"LOADING": "جار جلب الروبوتات...",
"TYPE": "نوع الروبوت"
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "رابط Webhook"
}
},
"DELETE": {
"BUTTON_TEXT": "حذف",
"TITLE": "حذف الروبوت",
"SUBMIT": "حذف",
"CANCEL_BUTTON_TEXT": "إلغاء",
"DESCRIPTION": "هل أنت متأكد أنك تريد حذف هذا الروبوت؟ هذا الإجراء لا يمكن التراجع عنه.",
"CONFIRM": {
"TITLE": "تأكيد الحذف",
"MESSAGE": "Are you sure you want to delete {name}?",
"YES": "نعم، احذف",
"NO": "لا، احتفظ"
},
"API": {
"SUCCESS_MESSAGE": "تم حذف الروبوت بنجاح.",
"ERROR_MESSAGE": "تعذر حذف الروبوت. يرجى المحاولة مرة أخرى."
@@ -57,17 +53,44 @@
},
"EDIT": {
"BUTTON_TEXT": "تعديل",
"LOADING": "جار جلب الروبوتات...",
"TITLE": "تعديل الروبوت",
"CANCEL_BUTTON_TEXT": "إلغاء",
"API": {
"SUCCESS_MESSAGE": "تم تحديث الروبوت بنجاح.",
"ERROR_MESSAGE": "تعذر تحديث الروبوت. يرجى المحاولة مرة أخرى."
}
},
"FORM": {
"AVATAR": {
"LABEL": "Bot avatar"
},
"NAME": {
"LABEL": "اسم الروبوت",
"PLACEHOLDER": "Enter bot name",
"REQUIRED": "اسم الروبوت مطلوب"
},
"DESCRIPTION": {
"LABEL": "الوصف",
"PLACEHOLDER": "ماذا يفعل هذا الروبوت؟"
},
"WEBHOOK_URL": {
"LABEL": "رابط Webhook",
"PLACEHOLDER": "https://example.com/webhook",
"REQUIRED": "Webhook URL is required"
},
"ERRORS": {
"NAME": "اسم الروبوت مطلوب",
"URL": "Webhook URL is required",
"VALID_URL": "Please enter a valid URL starting with http:// or https://"
},
"CANCEL": "إلغاء",
"CREATE": "Create Bot",
"UPDATE": "Update Bot"
},
"WEBHOOK": {
"DESCRIPTION": "Configure a webhook bot to integrate with your custom services. The bot will receive and process events from conversations and can respond to them."
},
"TYPES": {
"WEBHOOK": "روبوت الـWebhook",
"CSML": "بوت CSML"
"WEBHOOK": "روبوت الـWebhook"
}
}
}
@@ -126,6 +126,44 @@
"ATLEAST_ONE_CONDITION_REQUIRED": "شرط واحد على الأقل مطلوب",
"ATLEAST_ONE_ACTION_REQUIRED": "إجراء واحد على الأقل مطلوب"
},
"NONE_OPTION": "لا شيء"
"NONE_OPTION": "لا شيء",
"EVENTS": {
"CONVERSATION_CREATED": "تم إنشاء المحادثة",
"CONVERSATION_UPDATED": "تم تحديث المحادثة",
"MESSAGE_CREATED": "Message Created",
"CONVERSATION_OPENED": "Conversation Opened"
},
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "كتم المحادثة",
"SNOOZE_CONVERSATION": "تأجيل المحادثة",
"RESOLVE_CONVERSATION": "إعادة فتح المحادثة",
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "تغيير الأولوية",
"ADD_SLA": "Add SLA"
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "البريد الإلكتروني",
"INBOX": "صندوق الوارد",
"CONVERSATION_LANGUAGE": "Conversation Language",
"PHONE_NUMBER": "رقم الهاتف",
"STATUS": "الحالة",
"BROWSER_LANGUAGE": "لغة المتصفح",
"MAIL_SUBJECT": "Email Subject",
"COUNTRY_NAME": "الدولة",
"REFERER_LINK": "Referrer Link",
"ASSIGNEE_NAME": "المكلَّف",
"TEAM_NAME": "الفريق",
"PRIORITY": "الأولوية"
}
}
}
@@ -544,6 +544,9 @@
"WROTE": "كتب",
"YOU": "أنت",
"SAVE": "Save note",
"EXPAND": "Expand",
"COLLAPSE": "Collapse",
"NO_NOTES": "No notes, you can add notes from the contact details page.",
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above."
}
},
@@ -32,10 +32,12 @@
"LOADING_CONVERSATIONS": "جاري جلب المحادثات",
"CANNOT_REPLY": "لا يمكنك الرد بسبب",
"24_HOURS_WINDOW": "قيد نافذة الـ 24 ساعة",
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
"NOT_ASSIGNED_TO_YOU": "لم يتم تعيين هذه المحادثة لك. هل ترغب في تعيين هذه المحادثة لنفسك؟",
"ASSIGN_TO_ME": "إسناد لي",
"TWILIO_WHATSAPP_CAN_REPLY": "يمكنك فقط الرد على هذه المحادثة باستخدام رسالة قالب بسبب",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "قيد نافذة الـ 24 ساعة",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. All new messages will show up there. You wont be able to send messages from this conversation anymore.",
"REPLYING_TO": "أنت ترد على:",
"REMOVE_SELECTION": "إزالة التحديد",
"DOWNLOAD": "تحميل",
@@ -293,6 +295,7 @@
"CONVERSATION_ACTIONS": "إجراءات المحادثة",
"CONVERSATION_LABELS": "وسوم المحادثة",
"CONVERSATION_INFO": "معلومات المحادثة",
"CONTACT_NOTES": "Contact Notes",
"CONTACT_ATTRIBUTES": "سمات جهة الاتصال",
"PREVIOUS_CONVERSATION": "المحادثات السابقة",
"MACROS": "ماكروس",
@@ -1,5 +1,11 @@
{
"GENERAL_SETTINGS": {
"LIMIT_MESSAGES": {
"CONVERSATION": "You have exceeded the conversation limit. Hacker plan allows only 500 conversations.",
"INBOXES": "You have exceeded the inbox limit. Hacker plan only supports website live-chat. Additional inboxes like email, WhatsApp etc. require a paid plan.",
"AGENTS": "You have exceeded the agent limit. Hacker plan allows only 2 agents.",
"NON_ADMIN": "Please contact your administrator to upgrade the plan and continue using all features."
},
"TITLE": "إعدادات الحساب",
"SUBMIT": "تحديث الإعدادات",
"BACK": "العودة",
@@ -8,6 +14,26 @@
"ERROR": "تعذر تحديث الإعدادات، الرجاء المحاولة مرة أخرى!",
"SUCCESS": "تم تحديث إعدادات الحساب بنجاح"
},
"ACCOUNT_DELETE_SECTION": {
"TITLE": "Delete your Account",
"NOTE": "Once you delete your account, all your data will be deleted.",
"BUTTON_TEXT": "Delete Your Account",
"CONFIRM": {
"TITLE": "Delete Account",
"MESSAGE": "Deleting your Account is irreversible. Enter your account name below to confirm you want to permanently delete it.",
"BUTTON_TEXT": "حذف",
"DISMISS": "إلغاء",
"PLACE_HOLDER": "الرجاء كتابة {accountName} للتأكيد"
},
"SUCCESS": "Account marked for deletion",
"FAILURE": "Could not delete account, try again!",
"SCHEDULED_DELETION": {
"TITLE": "Account Scheduled for Deletion",
"MESSAGE_MANUAL": "This account is scheduled for deletion on {deletionDate}. This was requested by an administrator. You can cancel the deletion before this date.",
"MESSAGE_INACTIVITY": "This account is scheduled for deletion on {deletionDate} due to account inactivity. You can cancel the deletion before this date.",
"CLEAR_BUTTON": "Cancel Scheduled Deletion"
}
},
"FORM": {
"ERROR": "الرجاء إصلاح الأخطاء في النموذج",
"GENERAL_SECTION": {
@@ -51,6 +77,7 @@
"UPDATE_CHATWOOT": "يتوفر تحديث {latestChatwootVersion} لـ Chatwoot. الرجاء التحديث.",
"LEARN_MORE": "اعرف المزيد",
"PAYMENT_PENDING": "الدفعة الخاصة بك معلقة. الرجاء تحديث معلومات الدفع الخاصة بك للاستمرار في استخدام Chatwoot",
"UPGRADE": "Upgrade to continue using Chatwoot",
"LIMITS_UPGRADE": "لقد تجاوز حسابك حدود الاستخدام، يرجى ترقية خطتك للاستمرار في استخدام Chatwoot",
"OPEN_BILLING": "فتح الفواتير"
},
@@ -696,7 +696,8 @@
"SLUG": {
"LABEL": "Slug",
"PLACEHOLDER": "user-guide",
"ERROR": "Slug is required"
"ERROR": "Slug is required",
"FORMAT_ERROR": "Please enter a valid slug, for eg: user-guide"
}
},
"PORTAL_SETTINGS": {
@@ -43,7 +43,17 @@
"INBOX_NAME": "اسم صندوق الوارد لقناة التواصل",
"ADD_NAME": "قم بتعيين اسم لصندوق الوارد الخاص بقناتك الجديدة",
"PICK_NAME": "Pick a Name for your Inbox",
"PICK_A_VALUE": "اختر قيمة"
"PICK_A_VALUE": "اختر قيمة",
"CREATE_INBOX": "إنشاء قناة تواصل"
},
"INSTAGRAM": {
"CONTINUE_WITH_INSTAGRAM": "Continue with Instagram",
"CONNECT_YOUR_INSTAGRAM_PROFILE": "Connect your Instagram Profile",
"HELP": "To add your Instagram profile as a channel, you need to authenticate your Instagram Profile by clicking on 'Continue with Instagram' ",
"ERROR_MESSAGE": "There was an error connecting to Instagram, please try again",
"ERROR_AUTH": "There was an error connecting to Instagram, please try again",
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You wont be able to send/receive Instagram messages from this inbox anymore."
},
"TWITTER": {
"HELP": "لإضافة حساب تويتر الخاص بك كقناة تواصل، تحتاج إلى مصادقة حسابك على تويتر بك بالنقر على زر \"تسجيل الدخول باستخدام تويتر\" ",
@@ -753,7 +763,8 @@
"EMAIL": "البريد الإلكتروني",
"TELEGRAM": "تيليجرام",
"LINE": "Line",
"API": "قناة API"
"API": "قناة API",
"INSTAGRAM": "Instagram"
}
}
}
@@ -329,12 +329,21 @@
"HEADER_KNOW_MORE": "Know more",
"COPILOT": {
"SEND_MESSAGE": "إرسال الرسالة...",
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
"LOADER": "Captain is thinking",
"YOU": "أنت",
"USE": "Use this",
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
},
"PLAYGROUND": {
"USER": "أنت",
"ASSISTANT": "Assistant",
"MESSAGE_PLACEHOLDER": "أكتب رسالتك...",
"HEADER": "Playground",
"DESCRIPTION": "Use this playground to send messages to your assistant and check if it responds accurately, quickly, and in the tone you expect.",
"CREDIT_NOTE": "Messages sent here will count toward your Captain credits."
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
"AVAILABLE_ON": "Captain is not available on the free plan.",
@@ -373,20 +382,41 @@
"ERROR_MESSAGE": "There was an error creating the assistant, please try again."
},
"FORM": {
"UPDATE": "تحديث",
"SECTIONS": {
"BASIC_INFO": "Basic Information",
"SYSTEM_MESSAGES": "System Messages",
"INSTRUCTIONS": "Instructions",
"FEATURES": "الخصائص",
"TOOLS": "Tools "
},
"NAME": {
"LABEL": "Assistant Name",
"PLACEHOLDER": "Enter a name for the assistant",
"ERROR": "Please provide a name for the assistant"
"LABEL": "الاسم",
"PLACEHOLDER": "Enter assistant name"
},
"DESCRIPTION": {
"LABEL": "Assistant Description",
"PLACEHOLDER": "Describe how and where this assistant will be used",
"ERROR": "A description is required"
"LABEL": "الوصف",
"PLACEHOLDER": "Enter assistant description"
},
"PRODUCT_NAME": {
"LABEL": "Product Name",
"PLACEHOLDER": "Enter the name of the product this assistant is designed for",
"ERROR": "The product name is required"
"PLACEHOLDER": "Enter product name"
},
"WELCOME_MESSAGE": {
"LABEL": "Welcome Message",
"PLACEHOLDER": "Enter welcome message"
},
"HANDOFF_MESSAGE": {
"LABEL": "Handoff Message",
"PLACEHOLDER": "Enter handoff message"
},
"RESOLUTION_MESSAGE": {
"LABEL": "Resolution Message",
"PLACEHOLDER": "Enter resolution message"
},
"INSTRUCTIONS": {
"LABEL": "Instructions",
"PLACEHOLDER": "Enter instructions for the assistant"
},
"FEATURES": {
"TITLE": "الخصائص",
@@ -397,7 +427,8 @@
"EDIT": {
"TITLE": "Update the assistant",
"SUCCESS_MESSAGE": "The assistant has been successfully updated",
"ERROR_MESSAGE": "There was an error updating the assistant, please try again."
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"OPTIONS": {
"EDIT_ASSISTANT": "Edit Assistant",
@@ -83,6 +83,22 @@
"ACTION_PARAMETERS_REQUIRED": "معلمات الإجراء مطلوبة",
"ATLEAST_ONE_CONDITION_REQUIRED": "شرط واحد على الأقل مطلوب",
"ATLEAST_ONE_ACTION_REQUIRED": "إجراء واحد على الأقل مطلوب"
},
"ACTIONS": {
"ASSIGN_TEAM": "Assign a Team",
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "كتم المحادثة",
"SNOOZE_CONVERSATION": "تأجيل المحادثة",
"RESOLVE_CONVERSATION": "إعادة فتح المحادثة",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "تغيير الأولوية",
"ADD_PRIVATE_NOTE": "Add a Private Note",
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
}
}
}
@@ -387,7 +387,8 @@
"LABEL": "اسم المنشأة",
"PLACEHOLDER": "مؤسسة Wayne"
},
"SUBMIT": "إرسال"
"SUBMIT": "إرسال",
"CANCEL": "إلغاء"
}
},
"KEYBOARD_SHORTCUTS": {
@@ -2,23 +2,13 @@
"AGENT_BOTS": {
"HEADER": "Bots",
"LOADING_EDITOR": "Loading editor...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.You can manage your bots from this page or create new ones using the 'Configure new bot' button.",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot name",
"PLACEHOLDER": "Name your bot.",
"ERROR": "Bot name is required."
},
"DESCRIPTION": {
"LABEL": "Bot description",
"PLACEHOLDER": "What does this bot do?"
},
"BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above.",
"API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
},
"SUBMIT": "Validate and save"
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "System",
"AVATAR": {
"SUCCESS_DELETE": "Bot avatar deleted successfully",
"ERROR_DELETE": "Error deleting bot avatar, please try again"
},
"BOT_CONFIGURATION": {
"TITLE": "Select an agent bot",
@@ -32,7 +22,7 @@
"SELECT_PLACEHOLDER": "Select bot"
},
"ADD": {
"TITLE": "Configure new bot",
"TITLE": "Add Bot",
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully.",
@@ -40,16 +30,22 @@
}
},
"LIST": {
"404": "No bots found. You can create a bot by clicking the 'Configure new bot' button",
"404": "No bots found. You can create a bot by clicking the 'Add Bot' button.",
"LOADING": "Fetching bots...",
"TYPE": "Bot type"
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "Webhook URL"
}
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"TITLE": "Delete bot",
"SUBMIT": "Delete",
"CANCEL_BUTTON_TEXT": "Cancel",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
"CONFIRM": {
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure you want to delete {name}?",
"YES": "Yes, Delete",
"NO": "No, Keep"
},
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully.",
"ERROR_MESSAGE": "Could not delete bot. Please try again."
@@ -57,17 +53,44 @@
},
"EDIT": {
"BUTTON_TEXT": "Edit",
"LOADING": "Fetching bots...",
"TITLE": "Edit bot",
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully.",
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"FORM": {
"AVATAR": {
"LABEL": "Bot avatar"
},
"NAME": {
"LABEL": "Bot name",
"PLACEHOLDER": "Enter bot name",
"REQUIRED": "Bot name is required"
},
"DESCRIPTION": {
"LABEL": "Description",
"PLACEHOLDER": "What does this bot do?"
},
"WEBHOOK_URL": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "https://example.com/webhook",
"REQUIRED": "Webhook URL is required"
},
"ERRORS": {
"NAME": "Bot name is required",
"URL": "Webhook URL is required",
"VALID_URL": "Please enter a valid URL starting with http:// or https://"
},
"CANCEL": "Cancel",
"CREATE": "Create Bot",
"UPDATE": "Update Bot"
},
"WEBHOOK": {
"DESCRIPTION": "Configure a webhook bot to integrate with your custom services. The bot will receive and process events from conversations and can respond to them."
},
"TYPES": {
"WEBHOOK": "Webhook bot",
"CSML": "CSML bot"
"WEBHOOK": "Webhook bot"
}
}
}
@@ -126,6 +126,44 @@
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
},
"NONE_OPTION": "None"
"NONE_OPTION": "None",
"EVENTS": {
"CONVERSATION_CREATED": "Conversation Created",
"CONVERSATION_UPDATED": "Conversation Updated",
"MESSAGE_CREATED": "Message Created",
"CONVERSATION_OPENED": "Conversation Opened"
},
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"RESOLVE_CONVERSATION": "Resolve Conversation",
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "Change Priority",
"ADD_SLA": "Add SLA"
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Inbox",
"CONVERSATION_LANGUAGE": "Conversation Language",
"PHONE_NUMBER": "Phone Number",
"STATUS": "Status",
"BROWSER_LANGUAGE": "Browser Language",
"MAIL_SUBJECT": "Email Subject",
"COUNTRY_NAME": "Country",
"REFERER_LINK": "Referrer Link",
"ASSIGNEE_NAME": "Assignee",
"TEAM_NAME": "Team",
"PRIORITY": "Priority"
}
}
}
@@ -544,6 +544,9 @@
"WROTE": "wrote",
"YOU": "You",
"SAVE": "Save note",
"EXPAND": "Expand",
"COLLAPSE": "Collapse",
"NO_NOTES": "No notes, you can add notes from the contact details page.",
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above."
}
},
@@ -32,10 +32,12 @@
"LOADING_CONVERSATIONS": "Loading Conversations",
"CANNOT_REPLY": "You cannot reply due to",
"24_HOURS_WINDOW": "24 hour message window restriction",
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
"ASSIGN_TO_ME": "Assign to me",
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. All new messages will show up there. You wont be able to send messages from this conversation anymore.",
"REPLYING_TO": "You are replying to:",
"REMOVE_SELECTION": "Remove Selection",
"DOWNLOAD": "Download",
@@ -293,6 +295,7 @@
"CONVERSATION_ACTIONS": "Conversation Actions",
"CONVERSATION_LABELS": "Conversation Labels",
"CONVERSATION_INFO": "Conversation Information",
"CONTACT_NOTES": "Contact Notes",
"CONTACT_ATTRIBUTES": "Contact Attributes",
"PREVIOUS_CONVERSATION": "Previous Conversations",
"MACROS": "Macros",
@@ -1,5 +1,11 @@
{
"GENERAL_SETTINGS": {
"LIMIT_MESSAGES": {
"CONVERSATION": "You have exceeded the conversation limit. Hacker plan allows only 500 conversations.",
"INBOXES": "You have exceeded the inbox limit. Hacker plan only supports website live-chat. Additional inboxes like email, WhatsApp etc. require a paid plan.",
"AGENTS": "You have exceeded the agent limit. Hacker plan allows only 2 agents.",
"NON_ADMIN": "Please contact your administrator to upgrade the plan and continue using all features."
},
"TITLE": "Account settings",
"SUBMIT": "Update settings",
"BACK": "Back",
@@ -8,6 +14,26 @@
"ERROR": "Could not update settings, try again!",
"SUCCESS": "Successfully updated account settings"
},
"ACCOUNT_DELETE_SECTION": {
"TITLE": "Delete your Account",
"NOTE": "Once you delete your account, all your data will be deleted.",
"BUTTON_TEXT": "Delete Your Account",
"CONFIRM": {
"TITLE": "Delete Account",
"MESSAGE": "Deleting your Account is irreversible. Enter your account name below to confirm you want to permanently delete it.",
"BUTTON_TEXT": "Delete",
"DISMISS": "Cancel",
"PLACE_HOLDER": "Please type {accountName} to confirm"
},
"SUCCESS": "Account marked for deletion",
"FAILURE": "Could not delete account, try again!",
"SCHEDULED_DELETION": {
"TITLE": "Account Scheduled for Deletion",
"MESSAGE_MANUAL": "This account is scheduled for deletion on {deletionDate}. This was requested by an administrator. You can cancel the deletion before this date.",
"MESSAGE_INACTIVITY": "This account is scheduled for deletion on {deletionDate} due to account inactivity. You can cancel the deletion before this date.",
"CLEAR_BUTTON": "Cancel Scheduled Deletion"
}
},
"FORM": {
"ERROR": "Please fix form errors",
"GENERAL_SECTION": {
@@ -51,6 +77,7 @@
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
"LEARN_MORE": "Learn more",
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
"UPGRADE": "Upgrade to continue using Chatwoot",
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
"OPEN_BILLING": "Open billing"
},
@@ -696,7 +696,8 @@
"SLUG": {
"LABEL": "Slug",
"PLACEHOLDER": "user-guide",
"ERROR": "Slug is required"
"ERROR": "Slug is required",
"FORMAT_ERROR": "Please enter a valid slug, for eg: user-guide"
}
},
"PORTAL_SETTINGS": {
@@ -43,7 +43,17 @@
"INBOX_NAME": "Inbox Name",
"ADD_NAME": "Add a name for your inbox",
"PICK_NAME": "Pick a Name for your Inbox",
"PICK_A_VALUE": "Pick a value"
"PICK_A_VALUE": "Pick a value",
"CREATE_INBOX": "Create Inbox"
},
"INSTAGRAM": {
"CONTINUE_WITH_INSTAGRAM": "Continue with Instagram",
"CONNECT_YOUR_INSTAGRAM_PROFILE": "Connect your Instagram Profile",
"HELP": "To add your Instagram profile as a channel, you need to authenticate your Instagram Profile by clicking on 'Continue with Instagram' ",
"ERROR_MESSAGE": "There was an error connecting to Instagram, please try again",
"ERROR_AUTH": "There was an error connecting to Instagram, please try again",
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You wont be able to send/receive Instagram messages from this inbox anymore."
},
"TWITTER": {
"HELP": "To add your Twitter profile as a channel, you need to authenticate your Twitter Profile by clicking on 'Sign in with Twitter' ",
@@ -753,7 +763,8 @@
"EMAIL": "Email",
"TELEGRAM": "Telegram",
"LINE": "Line",
"API": "API Channel"
"API": "API Channel",
"INSTAGRAM": "Instagram"
}
}
}
@@ -329,12 +329,21 @@
"HEADER_KNOW_MORE": "Know more",
"COPILOT": {
"SEND_MESSAGE": "Send message...",
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
"LOADER": "Captain is thinking",
"YOU": "You",
"USE": "Use this",
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
},
"PLAYGROUND": {
"USER": "You",
"ASSISTANT": "Assistant",
"MESSAGE_PLACEHOLDER": "Type your message...",
"HEADER": "Playground",
"DESCRIPTION": "Use this playground to send messages to your assistant and check if it responds accurately, quickly, and in the tone you expect.",
"CREDIT_NOTE": "Messages sent here will count toward your Captain credits."
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
"AVAILABLE_ON": "Captain is not available on the free plan.",
@@ -373,20 +382,41 @@
"ERROR_MESSAGE": "There was an error creating the assistant, please try again."
},
"FORM": {
"UPDATE": "Update",
"SECTIONS": {
"BASIC_INFO": "Basic Information",
"SYSTEM_MESSAGES": "System Messages",
"INSTRUCTIONS": "Instructions",
"FEATURES": "Features",
"TOOLS": "Tools "
},
"NAME": {
"LABEL": "Assistant Name",
"PLACEHOLDER": "Enter a name for the assistant",
"ERROR": "Please provide a name for the assistant"
"LABEL": "Name",
"PLACEHOLDER": "Enter assistant name"
},
"DESCRIPTION": {
"LABEL": "Assistant Description",
"PLACEHOLDER": "Describe how and where this assistant will be used",
"ERROR": "A description is required"
"LABEL": "Description",
"PLACEHOLDER": "Enter assistant description"
},
"PRODUCT_NAME": {
"LABEL": "Product Name",
"PLACEHOLDER": "Enter the name of the product this assistant is designed for",
"ERROR": "The product name is required"
"PLACEHOLDER": "Enter product name"
},
"WELCOME_MESSAGE": {
"LABEL": "Welcome Message",
"PLACEHOLDER": "Enter welcome message"
},
"HANDOFF_MESSAGE": {
"LABEL": "Handoff Message",
"PLACEHOLDER": "Enter handoff message"
},
"RESOLUTION_MESSAGE": {
"LABEL": "Resolution Message",
"PLACEHOLDER": "Enter resolution message"
},
"INSTRUCTIONS": {
"LABEL": "Instructions",
"PLACEHOLDER": "Enter instructions for the assistant"
},
"FEATURES": {
"TITLE": "Features",
@@ -397,7 +427,8 @@
"EDIT": {
"TITLE": "Update the assistant",
"SUCCESS_MESSAGE": "The assistant has been successfully updated",
"ERROR_MESSAGE": "There was an error updating the assistant, please try again."
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"OPTIONS": {
"EDIT_ASSISTANT": "Edit Assistant",
@@ -83,6 +83,22 @@
"ACTION_PARAMETERS_REQUIRED": "Action parameters are required",
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
},
"ACTIONS": {
"ASSIGN_TEAM": "Assign a Team",
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"RESOLVE_CONVERSATION": "Resolve Conversation",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "Change Priority",
"ADD_PRIVATE_NOTE": "Add a Private Note",
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
}
}
}
@@ -387,7 +387,8 @@
"LABEL": "Company Name",
"PLACEHOLDER": "Wayne Enterprises"
},
"SUBMIT": "Submit"
"SUBMIT": "Submit",
"CANCEL": "Cancel"
}
},
"KEYBOARD_SHORTCUTS": {
@@ -2,23 +2,13 @@
"AGENT_BOTS": {
"HEADER": "Bots",
"LOADING_EDITOR": "Loading editor...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.You can manage your bots from this page or create new ones using the 'Configure new bot' button.",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot name",
"PLACEHOLDER": "Name your bot.",
"ERROR": "Bot name is required."
},
"DESCRIPTION": {
"LABEL": "Bot description",
"PLACEHOLDER": "What does this bot do?"
},
"BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above.",
"API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
},
"SUBMIT": "Validate and save"
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "System",
"AVATAR": {
"SUCCESS_DELETE": "Bot avatar deleted successfully",
"ERROR_DELETE": "Error deleting bot avatar, please try again"
},
"BOT_CONFIGURATION": {
"TITLE": "Select an agent bot",
@@ -32,7 +22,7 @@
"SELECT_PLACEHOLDER": "Select bot"
},
"ADD": {
"TITLE": "Configure new bot",
"TITLE": "Add Bot",
"CANCEL_BUTTON_TEXT": "Отмени",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully.",
@@ -40,16 +30,22 @@
}
},
"LIST": {
"404": "No bots found. You can create a bot by clicking the 'Configure new bot' button",
"404": "No bots found. You can create a bot by clicking the 'Add Bot' button.",
"LOADING": "Fetching bots...",
"TYPE": "Bot type"
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "Webhook URL"
}
},
"DELETE": {
"BUTTON_TEXT": "Изтрий",
"TITLE": "Delete bot",
"SUBMIT": "Изтрий",
"CANCEL_BUTTON_TEXT": "Отмени",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
"CONFIRM": {
"TITLE": "Потвърди изтриването",
"MESSAGE": "Are you sure you want to delete {name}?",
"YES": "Да, изтрий",
"NO": "Не, запази"
},
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully.",
"ERROR_MESSAGE": "Could not delete bot. Please try again."
@@ -57,17 +53,44 @@
},
"EDIT": {
"BUTTON_TEXT": "Редактирай",
"LOADING": "Fetching bots...",
"TITLE": "Edit bot",
"CANCEL_BUTTON_TEXT": "Отмени",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully.",
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"FORM": {
"AVATAR": {
"LABEL": "Bot avatar"
},
"NAME": {
"LABEL": "Bot name",
"PLACEHOLDER": "Enter bot name",
"REQUIRED": "Bot name is required"
},
"DESCRIPTION": {
"LABEL": "Описание",
"PLACEHOLDER": "What does this bot do?"
},
"WEBHOOK_URL": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "https://example.com/webhook",
"REQUIRED": "Webhook URL is required"
},
"ERRORS": {
"NAME": "Bot name is required",
"URL": "Webhook URL is required",
"VALID_URL": "Please enter a valid URL starting with http:// or https://"
},
"CANCEL": "Отмени",
"CREATE": "Create Bot",
"UPDATE": "Update Bot"
},
"WEBHOOK": {
"DESCRIPTION": "Configure a webhook bot to integrate with your custom services. The bot will receive and process events from conversations and can respond to them."
},
"TYPES": {
"WEBHOOK": "Webhook bot",
"CSML": "CSML bot"
"WEBHOOK": "Webhook bot"
}
}
}
@@ -126,6 +126,44 @@
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
},
"NONE_OPTION": "Нито един"
"NONE_OPTION": "Нито един",
"EVENTS": {
"CONVERSATION_CREATED": "Conversation Created",
"CONVERSATION_UPDATED": "Conversation Updated",
"MESSAGE_CREATED": "Message Created",
"CONVERSATION_OPENED": "Conversation Opened"
},
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Заглушаване на разговора",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"RESOLVE_CONVERSATION": "Resolve Conversation",
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "Change Priority",
"ADD_SLA": "Add SLA"
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Входяща кутия",
"CONVERSATION_LANGUAGE": "Conversation Language",
"PHONE_NUMBER": "Phone Number",
"STATUS": "Статус",
"BROWSER_LANGUAGE": "Език на браузъра",
"MAIL_SUBJECT": "Email Subject",
"COUNTRY_NAME": "Държава",
"REFERER_LINK": "Referrer Link",
"ASSIGNEE_NAME": "Assignee",
"TEAM_NAME": "Team",
"PRIORITY": "Priority"
}
}
}
@@ -544,6 +544,9 @@
"WROTE": "wrote",
"YOU": "You",
"SAVE": "Save note",
"EXPAND": "Expand",
"COLLAPSE": "Collapse",
"NO_NOTES": "No notes, you can add notes from the contact details page.",
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above."
}
},
@@ -32,10 +32,12 @@
"LOADING_CONVERSATIONS": "Loading Conversations",
"CANNOT_REPLY": "You cannot reply due to",
"24_HOURS_WINDOW": "24 hour message window restriction",
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
"ASSIGN_TO_ME": "Assign to me",
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. All new messages will show up there. You wont be able to send messages from this conversation anymore.",
"REPLYING_TO": "You are replying to:",
"REMOVE_SELECTION": "Remove Selection",
"DOWNLOAD": "Download",
@@ -293,6 +295,7 @@
"CONVERSATION_ACTIONS": "Conversation Actions",
"CONVERSATION_LABELS": "Етикети на разговора",
"CONVERSATION_INFO": "Conversation Information",
"CONTACT_NOTES": "Contact Notes",
"CONTACT_ATTRIBUTES": "Contact Attributes",
"PREVIOUS_CONVERSATION": "Предишни разговори",
"MACROS": "Macros",
@@ -1,5 +1,11 @@
{
"GENERAL_SETTINGS": {
"LIMIT_MESSAGES": {
"CONVERSATION": "You have exceeded the conversation limit. Hacker plan allows only 500 conversations.",
"INBOXES": "You have exceeded the inbox limit. Hacker plan only supports website live-chat. Additional inboxes like email, WhatsApp etc. require a paid plan.",
"AGENTS": "You have exceeded the agent limit. Hacker plan allows only 2 agents.",
"NON_ADMIN": "Please contact your administrator to upgrade the plan and continue using all features."
},
"TITLE": "Account settings",
"SUBMIT": "Update settings",
"BACK": "Back",
@@ -8,6 +14,26 @@
"ERROR": "Could not update settings, try again!",
"SUCCESS": "Successfully updated account settings"
},
"ACCOUNT_DELETE_SECTION": {
"TITLE": "Delete your Account",
"NOTE": "Once you delete your account, all your data will be deleted.",
"BUTTON_TEXT": "Delete Your Account",
"CONFIRM": {
"TITLE": "Delete Account",
"MESSAGE": "Deleting your Account is irreversible. Enter your account name below to confirm you want to permanently delete it.",
"BUTTON_TEXT": "Изтрий",
"DISMISS": "Отмени",
"PLACE_HOLDER": "Please type {accountName} to confirm"
},
"SUCCESS": "Account marked for deletion",
"FAILURE": "Could not delete account, try again!",
"SCHEDULED_DELETION": {
"TITLE": "Account Scheduled for Deletion",
"MESSAGE_MANUAL": "This account is scheduled for deletion on {deletionDate}. This was requested by an administrator. You can cancel the deletion before this date.",
"MESSAGE_INACTIVITY": "This account is scheduled for deletion on {deletionDate} due to account inactivity. You can cancel the deletion before this date.",
"CLEAR_BUTTON": "Cancel Scheduled Deletion"
}
},
"FORM": {
"ERROR": "Please fix form errors",
"GENERAL_SECTION": {
@@ -51,6 +77,7 @@
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
"LEARN_MORE": "Learn more",
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
"UPGRADE": "Upgrade to continue using Chatwoot",
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
"OPEN_BILLING": "Open billing"
},
@@ -696,7 +696,8 @@
"SLUG": {
"LABEL": "Slug",
"PLACEHOLDER": "user-guide",
"ERROR": "Slug is required"
"ERROR": "Slug is required",
"FORMAT_ERROR": "Please enter a valid slug, for eg: user-guide"
}
},
"PORTAL_SETTINGS": {
@@ -43,7 +43,17 @@
"INBOX_NAME": "Inbox Name",
"ADD_NAME": "Add a name for your inbox",
"PICK_NAME": "Pick a Name for your Inbox",
"PICK_A_VALUE": "Pick a value"
"PICK_A_VALUE": "Pick a value",
"CREATE_INBOX": "Create Inbox"
},
"INSTAGRAM": {
"CONTINUE_WITH_INSTAGRAM": "Continue with Instagram",
"CONNECT_YOUR_INSTAGRAM_PROFILE": "Connect your Instagram Profile",
"HELP": "To add your Instagram profile as a channel, you need to authenticate your Instagram Profile by clicking on 'Continue with Instagram' ",
"ERROR_MESSAGE": "There was an error connecting to Instagram, please try again",
"ERROR_AUTH": "There was an error connecting to Instagram, please try again",
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You wont be able to send/receive Instagram messages from this inbox anymore."
},
"TWITTER": {
"HELP": "To add your Twitter profile as a channel, you need to authenticate your Twitter Profile by clicking on 'Sign in with Twitter' ",
@@ -753,7 +763,8 @@
"EMAIL": "Имейл",
"TELEGRAM": "Telegram",
"LINE": "Line",
"API": "API Channel"
"API": "API Channel",
"INSTAGRAM": "Instagram"
}
}
}
@@ -329,12 +329,21 @@
"HEADER_KNOW_MORE": "Know more",
"COPILOT": {
"SEND_MESSAGE": "Изпрати съобщение...",
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
"LOADER": "Captain is thinking",
"YOU": "You",
"USE": "Use this",
"RESET": "Reset",
"SELECT_ASSISTANT": "Select Assistant"
},
"PLAYGROUND": {
"USER": "You",
"ASSISTANT": "Assistant",
"MESSAGE_PLACEHOLDER": "Напишете вашето съобщение...",
"HEADER": "Playground",
"DESCRIPTION": "Use this playground to send messages to your assistant and check if it responds accurately, quickly, and in the tone you expect.",
"CREDIT_NOTE": "Messages sent here will count toward your Captain credits."
},
"PAYWALL": {
"TITLE": "Upgrade to use Captain AI",
"AVAILABLE_ON": "Captain is not available on the free plan.",
@@ -373,20 +382,41 @@
"ERROR_MESSAGE": "There was an error creating the assistant, please try again."
},
"FORM": {
"UPDATE": "Update",
"SECTIONS": {
"BASIC_INFO": "Basic Information",
"SYSTEM_MESSAGES": "System Messages",
"INSTRUCTIONS": "Instructions",
"FEATURES": "Features",
"TOOLS": "Tools "
},
"NAME": {
"LABEL": "Assistant Name",
"PLACEHOLDER": "Enter a name for the assistant",
"ERROR": "Please provide a name for the assistant"
"LABEL": "Име",
"PLACEHOLDER": "Enter assistant name"
},
"DESCRIPTION": {
"LABEL": "Assistant Description",
"PLACEHOLDER": "Describe how and where this assistant will be used",
"ERROR": "A description is required"
"LABEL": "Описание",
"PLACEHOLDER": "Enter assistant description"
},
"PRODUCT_NAME": {
"LABEL": "Product Name",
"PLACEHOLDER": "Enter the name of the product this assistant is designed for",
"ERROR": "The product name is required"
"PLACEHOLDER": "Enter product name"
},
"WELCOME_MESSAGE": {
"LABEL": "Welcome Message",
"PLACEHOLDER": "Enter welcome message"
},
"HANDOFF_MESSAGE": {
"LABEL": "Handoff Message",
"PLACEHOLDER": "Enter handoff message"
},
"RESOLUTION_MESSAGE": {
"LABEL": "Resolution Message",
"PLACEHOLDER": "Enter resolution message"
},
"INSTRUCTIONS": {
"LABEL": "Instructions",
"PLACEHOLDER": "Enter instructions for the assistant"
},
"FEATURES": {
"TITLE": "Features",
@@ -397,7 +427,8 @@
"EDIT": {
"TITLE": "Update the assistant",
"SUCCESS_MESSAGE": "The assistant has been successfully updated",
"ERROR_MESSAGE": "There was an error updating the assistant, please try again."
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
"NOT_FOUND": "Could not find the assistant. Please try again."
},
"OPTIONS": {
"EDIT_ASSISTANT": "Edit Assistant",
@@ -83,6 +83,22 @@
"ACTION_PARAMETERS_REQUIRED": "Action parameters are required",
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
},
"ACTIONS": {
"ASSIGN_TEAM": "Assign a Team",
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Заглушаване на разговора",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"RESOLVE_CONVERSATION": "Resolve Conversation",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "Change Priority",
"ADD_PRIVATE_NOTE": "Add a Private Note",
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
}
}
}
@@ -387,7 +387,8 @@
"LABEL": "Име на фирма",
"PLACEHOLDER": "Wayne Enterprises"
},
"SUBMIT": "Изпращане"
"SUBMIT": "Изпращане",
"CANCEL": "Отмени"
}
},
"KEYBOARD_SHORTCUTS": {
@@ -2,23 +2,13 @@
"AGENT_BOTS": {
"HEADER": "Bots",
"LOADING_EDITOR": "S'està carregant l'editor...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.You can manage your bots from this page or create new ones using the 'Configure new bot' button.",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Nom del bot",
"PLACEHOLDER": "Anomena el teu bot.",
"ERROR": "El nom del bot és obligatori."
},
"DESCRIPTION": {
"LABEL": "Descripció del bot",
"PLACEHOLDER": "Què fa aquest bot?"
},
"BOT_CONFIG": {
"ERROR": "Introdueix la configuració del bot CSML més amunt.",
"API_ERROR": "La vostra configuració CSML no és vàlida. Arregla-ho i torna-ho a provar."
},
"SUBMIT": "Valida i desa"
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "Sistema",
"AVATAR": {
"SUCCESS_DELETE": "Bot avatar deleted successfully",
"ERROR_DELETE": "Error deleting bot avatar, please try again"
},
"BOT_CONFIGURATION": {
"TITLE": "Selecciona un bot d'agent",
@@ -32,7 +22,7 @@
"SELECT_PLACEHOLDER": "Selecciona el bot"
},
"ADD": {
"TITLE": "Configura el nou bot",
"TITLE": "Add Bot",
"CANCEL_BUTTON_TEXT": "Cancel·la",
"API": {
"SUCCESS_MESSAGE": "Bot afegit correctament.",
@@ -40,16 +30,22 @@
}
},
"LIST": {
"404": "No s'han trobat bots. Pots crear un bot fent clic al botó \"Configura un bot nou\" ↗",
"404": "No bots found. You can create a bot by clicking the 'Add Bot' button.",
"LOADING": "S'estan obtenint bots...",
"TYPE": "Tipus de bot"
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "URL del webhook"
}
},
"DELETE": {
"BUTTON_TEXT": "Esborrar",
"TITLE": "Suprimeix el bot",
"SUBMIT": "Esborrar",
"CANCEL_BUTTON_TEXT": "Cancel·la",
"DESCRIPTION": "Estàs segur que vols suprimir aquest bot? Aquesta acció és irreversible.",
"CONFIRM": {
"TITLE": "Confirma l'esborrat",
"MESSAGE": "Are you sure you want to delete {name}?",
"YES": "Si, esborra",
"NO": "No, segueix"
},
"API": {
"SUCCESS_MESSAGE": "S'ha esborrat el bot correctament.",
"ERROR_MESSAGE": "No s'ha pogut eliminar el bot. Torneu-ho a provar més endavant."
@@ -57,17 +53,44 @@
},
"EDIT": {
"BUTTON_TEXT": "Edita",
"LOADING": "S'estan obtenint bots...",
"TITLE": "Edita el bot",
"CANCEL_BUTTON_TEXT": "Cancel·la",
"API": {
"SUCCESS_MESSAGE": "Bot actualitzat correctament.",
"ERROR_MESSAGE": "No s'ha pogut actualitzar el bot. Torneu-ho a provar."
}
},
"FORM": {
"AVATAR": {
"LABEL": "Bot avatar"
},
"NAME": {
"LABEL": "Nom del bot",
"PLACEHOLDER": "Enter bot name",
"REQUIRED": "El nom del bot és obligatori"
},
"DESCRIPTION": {
"LABEL": "Descripció",
"PLACEHOLDER": "Què fa aquest bot?"
},
"WEBHOOK_URL": {
"LABEL": "URL del webhook",
"PLACEHOLDER": "https://example.com/webhook",
"REQUIRED": "Webhook URL is required"
},
"ERRORS": {
"NAME": "El nom del bot és obligatori",
"URL": "Webhook URL is required",
"VALID_URL": "Please enter a valid URL starting with http:// or https://"
},
"CANCEL": "Cancel·la",
"CREATE": "Create Bot",
"UPDATE": "Update Bot"
},
"WEBHOOK": {
"DESCRIPTION": "Configure a webhook bot to integrate with your custom services. The bot will receive and process events from conversations and can respond to them."
},
"TYPES": {
"WEBHOOK": "Webhook bot",
"CSML": "CSML bot"
"WEBHOOK": "Webhook bot"
}
}
}

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