Compare commits

..
Author SHA1 Message Date
Shivam Mishra 1e1f79025e chore: add scripts to test 2025-11-14 15:18:30 +05:30
Tanmay Deep Sharma e84efdccbd Merge remote-tracking branch 'origin/assignment_v2/assignment_service' into assignment_v2/assignment_service 2025-11-12 18:00:34 +05:30
Tanmay Deep Sharma 44da47c845 review changes 2025-11-12 17:59:52 +05:30
Tanmay Deep SharmaandGitHub 0ebe187c8a Merge branch 'develop' into assignment_v2/assignment_service 2025-11-06 01:48:23 +05:30
Tanmay Deep SharmaandGitHub 20d6a11dd9 Merge branch 'develop' into assignment_v2/assignment_service 2025-10-05 15:02:08 +02:00
Tanmay Deep SharmaandGitHub 5dc5037903 Merge branch 'develop' into assignment_v2/assignment_service 2025-09-24 15:50:27 +02:00
Tanmay Sharma 3c7b4f6011 Merge remote-tracking branch 'origin/assignment_v2/assignment_service' into assignment_v2/assignment_service 2025-09-11 13:38:56 +05:30
Tanmay Sharma 73f2aea514 fix user duplication issue for assignment user list 2025-09-11 13:38:22 +05:30
Tanmay Deep Sharma 02424a5a09 fix the exclusion rules 2025-09-10 20:36:08 +05:30
Tanmay Deep Sharma b015c61d71 fix specs 2025-09-04 19:11:18 +07:00
Tanmay Deep Sharma d483974ef1 fix specs 2025-09-04 18:45:22 +07:00
Tanmay Deep Sharma b16b1b6a49 fix balanced assignment spec 2025-09-04 16:53:17 +07:00
Tanmay Deep Sharma b28d00a117 fix rubocop 2025-09-04 16:21:29 +07:00
Tanmay Deep Sharma 2b3ad3dc74 Merge remote-tracking branch 'origin/assignment_v2/assignment_service' into assignment_v2/assignment_service 2025-09-04 16:12:08 +07:00
Tanmay Deep Sharma b95944dd4e update the spec file paths 2025-09-04 16:11:06 +07:00
Tanmay Deep SharmaandGitHub 75ba8e9186 Merge branch 'develop' into assignment_v2/assignment_service 2025-09-03 09:25:54 +07:00
Tanmay Deep Sharma 31969428ae Merge branch 'develop' into assignment_v2/assignment_service 2025-09-02 15:59:11 +07:00
Tanmay Sharma a4fab22e28 fix review changes 2025-08-29 16:26:19 +07:00
Tanmay Sharma 5d27fd798f fix feature listing 2025-08-29 15:49:05 +07:00
Tanmay Sharma ca14eba448 fix review changes 2025-08-29 14:49:13 +07:00
Tanmay Sharma 199a55a8e3 fix codex review 2025-08-28 21:06:06 +07:00
Tanmay Sharma 873094175b add exclusion_rules support 2025-08-28 20:39:30 +07:00
Muhsin KelothandGitHub 096bffa893 Merge branch 'develop' into assignment_v2/assignment_service 2025-08-28 18:33:02 +05:30
Tanmay Sharma ccbc2c5d56 Merge remote-tracking branch 'origin/develop' into assignment_v2/assignment_service 2025-08-28 18:44:11 +07:00
Tanmay Sharma 2c0f235d1d remove capacity status 2025-08-28 18:26:20 +07:00
Tanmay Sharma fc067b7f66 fix capacity test 2025-08-28 17:39:01 +07:00
Tanmay Sharma eacfa2e19a feat: add assignment service 2025-08-28 17:03:11 +07:00
234 changed files with 6982 additions and 5943 deletions
+43 -32
View File
@@ -1,8 +1,5 @@
class Messages::MessageBuilder class Messages::MessageBuilder
include ::FileTypeHelper include ::FileTypeHelper
include ::EmailHelper
include ::DataHelper
attr_reader :message attr_reader :message
def initialize(user, conversation, params) def initialize(user, conversation, params)
@@ -41,12 +38,30 @@ class Messages::MessageBuilder
params = convert_to_hash(@params) params = convert_to_hash(@params)
content_attributes = params.fetch(:content_attributes, {}) content_attributes = params.fetch(:content_attributes, {})
return safe_parse_json(content_attributes) if content_attributes.is_a?(String) return parse_json(content_attributes) if content_attributes.is_a?(String)
return content_attributes if content_attributes.is_a?(Hash) return content_attributes if content_attributes.is_a?(Hash)
{} {}
end end
# Converts the given object to a hash.
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
# Otherwise, returns the object as-is.
def convert_to_hash(obj)
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
obj
end
# Attempts to parse a string as JSON.
# If successful, returns the parsed hash with symbolized names.
# If unsuccessful, returns nil.
def parse_json(content)
JSON.parse(content, symbolize_names: true)
rescue JSON::ParserError
{}
end
def process_attachments def process_attachments
return if @attachments.blank? return if @attachments.blank?
@@ -95,6 +110,12 @@ class Messages::MessageBuilder
email_string.gsub(/\s+/, '').split(',') email_string.gsub(/\s+/, '').split(',')
end end
def validate_email_addresses(all_emails)
all_emails&.each do |email|
raise StandardError, 'Invalid email address' unless email.match?(URI::MailTo::EMAIL_REGEXP)
end
end
def message_type def message_type
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming' if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
raise StandardError, 'Incoming messages are only allowed in Api inboxes' raise StandardError, 'Incoming messages are only allowed in Api inboxes'
@@ -157,17 +178,14 @@ class Messages::MessageBuilder
email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {}) email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {})
normalized_content = normalize_email_body(@message.content) normalized_content = normalize_email_body(@message.content)
# Process liquid templates in normalized content with code block protection
processed_content = process_liquid_in_email_body(normalized_content)
# Use custom HTML content if provided, otherwise generate from message content # Use custom HTML content if provided, otherwise generate from message content
email_attributes[:html_content] = if custom_email_content_provided? email_attributes[:html_content] = if custom_email_content_provided?
build_custom_html_content build_custom_html_content
else else
build_html_content(processed_content) build_html_content(normalized_content)
end end
email_attributes[:text_content] = build_text_content(processed_content) email_attributes[:text_content] = build_text_content(normalized_content)
email_attributes email_attributes
end end
@@ -186,6 +204,22 @@ class Messages::MessageBuilder
text_content text_content
end end
def ensure_indifferent_access(hash)
return {} if hash.blank?
hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access : hash
end
def normalize_email_body(content)
content.to_s.gsub("\r\n", "\n")
end
def render_email_html(content)
return '' if content.blank?
ChatwootMarkdownRenderer.new(content).render_message.to_s
end
def custom_email_content_provided? def custom_email_content_provided?
@params[:email_html_content].present? @params[:email_html_content].present?
end end
@@ -198,27 +232,4 @@ class Messages::MessageBuilder
html_content html_content
end end
# Liquid processing methods for email content
def process_liquid_in_email_body(content)
return content if content.blank?
return content unless should_process_liquid?
# Protect code blocks from liquid processing
modified_content = modified_liquid_content(content)
template = Liquid::Template.parse(modified_content)
template.render(drops_with_sender)
rescue Liquid::Error
content
end
def should_process_liquid?
@message_type == 'outgoing' || @message_type == 'template'
end
def drops_with_sender
message_drops(@conversation).merge({
'agent' => UserDrop.new(sender)
})
end
end end
@@ -22,10 +22,9 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
def edit; end def edit; end
def create def create
params_with_defaults = article_params @article = @portal.articles.create!(article_params)
params_with_defaults[:status] ||= :draft
@article = @portal.articles.create!(params_with_defaults)
@article.associate_root_article(article_params[:associated_article_id]) @article.associate_root_article(article_params[:associated_article_id])
@article.draft!
render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid? render json: { error: @article.errors.messages }, status: :unprocessable_entity and return unless @article.valid?
end end
@@ -23,7 +23,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
private private
def webhook_params def webhook_params
params.require(:webhook).permit(:inbox_id, :name, :url, subscriptions: []) params.require(:webhook).permit(:inbox_id, :url, subscriptions: [])
end end
def fetch_webhook def fetch_webhook
-24
View File
@@ -1,24 +0,0 @@
# Provides utility methods for data transformation, hash manipulation, and JSON parsing.
# This module contains helper methods for converting between different data types,
# normalizing hashes, and safely handling JSON operations.
module DataHelper
# Ensures a hash supports indifferent access (string or symbol keys).
# Returns an empty hash if the input is blank.
def ensure_indifferent_access(hash)
return {} if hash.blank?
hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access : hash
end
def convert_to_hash(obj)
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
obj
end
def safe_parse_json(content)
JSON.parse(content, symbolize_names: true)
rescue JSON::ParserError
{}
end
end
+1 -21
View File
@@ -4,19 +4,6 @@ module EmailHelper
domain.split('.').first domain.split('.').first
end end
def render_email_html(content)
return '' if content.blank?
ChatwootMarkdownRenderer.new(content).render_message.to_s
end
# Raise a standard error if any email address is invalid
def validate_email_addresses(emails_to_test)
emails_to_test&.each do |email|
raise StandardError, 'Invalid email address' unless email.match?(URI::MailTo::EMAIL_REGEXP)
end
end
# ref: https://www.rfc-editor.org/rfc/rfc5233.html # ref: https://www.rfc-editor.org/rfc/rfc5233.html
# This is not a mandatory requirement for email addresses, but it is a common practice. # This is not a mandatory requirement for email addresses, but it is a common practice.
# john+test@xyc.com is the same as john@xyc.com # john+test@xyc.com is the same as john@xyc.com
@@ -34,10 +21,6 @@ module EmailHelper
end end
end end
def normalize_email_body(content)
content.to_s.gsub("\r\n", "\n")
end
def modified_liquid_content(email) def modified_liquid_content(email)
# This regex is used to match the code blocks in the content # This regex is used to match the code blocks in the content
# We don't want to process liquid in code blocks # We don't want to process liquid in code blocks
@@ -46,10 +29,7 @@ module EmailHelper
def message_drops(conversation) def message_drops(conversation)
{ {
'contact' => ContactDrop.new(conversation.contact), 'contact' => ContactDrop.new(conversation.contact)
'conversation' => ConversationDrop.new(conversation),
'inbox' => InboxDrop.new(conversation.inbox),
'account' => AccountDrop.new(conversation.account)
} }
end end
end end
+13
View File
@@ -1,5 +1,6 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import AddAccountModal from './components/app/AddAccountModal.vue';
import LoadingState from './components/widgets/LoadingState.vue'; import LoadingState from './components/widgets/LoadingState.vue';
import NetworkNotification from './components/NetworkNotification.vue'; import NetworkNotification from './components/NetworkNotification.vue';
import UpdateBanner from './components/app/UpdateBanner.vue'; import UpdateBanner from './components/app/UpdateBanner.vue';
@@ -24,6 +25,7 @@ export default {
name: 'App', name: 'App',
components: { components: {
AddAccountModal,
LoadingState, LoadingState,
NetworkNotification, NetworkNotification,
UpdateBanner, UpdateBanner,
@@ -49,6 +51,7 @@ export default {
}, },
data() { data() {
return { return {
showAddAccountModal: false,
latestChatwootVersion: null, latestChatwootVersion: null,
reconnectService: null, reconnectService: null,
}; };
@@ -61,12 +64,21 @@ export default {
authUIFlags: 'getAuthUIFlags', authUIFlags: 'getAuthUIFlags',
accountUIFlags: 'accounts/getUIFlags', accountUIFlags: 'accounts/getUIFlags',
}), }),
hasAccounts() {
const { accounts = [] } = this.currentUser || {};
return accounts.length > 0;
},
hideOnOnboardingView() { hideOnOnboardingView() {
return !isOnOnboardingView(this.$route); return !isOnOnboardingView(this.$route);
}, },
}, },
watch: { watch: {
currentUser() {
if (!this.hasAccounts) {
this.showAddAccountModal = true;
}
},
currentAccountId: { currentAccountId: {
immediate: true, immediate: true,
handler() { handler() {
@@ -144,6 +156,7 @@ export default {
<component :is="Component" /> <component :is="Component" />
</transition> </transition>
</router-view> </router-view>
<AddAccountModal :show="showAddAccountModal" :has-accounts="hasAccounts" />
<WootSnackbarBox /> <WootSnackbarBox />
<NetworkNotification /> <NetworkNotification />
</div> </div>
@@ -1,6 +1,5 @@
<script setup> <script setup>
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import ButtonGroup from 'dashboard/components-next/buttonGroup/ButtonGroup.vue';
import { useUISettings } from 'dashboard/composables/useUISettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
import { computed } from 'vue'; import { computed } from 'vue';
import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { FEATURE_FLAGS } from 'dashboard/featureFlags';
@@ -56,17 +55,17 @@ useKeyboardEvents(keyboardEvents);
</script> </script>
<template> <template>
<ButtonGroup <div
class="flex flex-col justify-center items-center absolute top-36 xl:top-24 ltr:right-2 rtl:left-2 bg-n-solid-2/90 backdrop-blur-lg border border-n-weak/50 rounded-full gap-1.5 p-1.5 shadow-sm transition-shadow duration-200 hover:shadow" class="flex flex-col justify-center items-center absolute top-36 xl:top-24 ltr:right-2 rtl:left-2 bg-n-solid-2 border border-n-weak rounded-full gap-2 p-1"
> >
<Button <Button
v-tooltip.top="$t('CONVERSATION.SIDEBAR.CONTACT')" v-tooltip.top="$t('CONVERSATION.SIDEBAR.CONTACT')"
ghost ghost
slate slate
sm sm
class="!rounded-full transition-all duration-[250ms] ease-out active:!scale-95 active:!brightness-105 active:duration-75" class="!rounded-full"
:class="{ :class="{
'bg-n-alpha-2 active:shadow-sm': isContactSidebarOpen, 'bg-n-alpha-2': isContactSidebarOpen,
}" }"
icon="i-ph-user-bold" icon="i-ph-user-bold"
@click="handleConversationSidebarToggle" @click="handleConversationSidebarToggle"
@@ -76,14 +75,13 @@ useKeyboardEvents(keyboardEvents);
v-tooltip.bottom="$t('CONVERSATION.SIDEBAR.COPILOT')" v-tooltip.bottom="$t('CONVERSATION.SIDEBAR.COPILOT')"
ghost ghost
slate slate
sm class="!rounded-full"
class="!rounded-full transition-all duration-[250ms] ease-out active:!scale-95 active:duration-75"
:class="{ :class="{
'bg-n-alpha-2 !text-n-iris-9 active:!brightness-105 active:shadow-sm': 'bg-n-alpha-2 !text-n-iris-9': isCopilotPanelOpen,
isCopilotPanelOpen,
}" }"
sm
icon="i-woot-captain" icon="i-woot-captain"
@click="handleCopilotSidebarToggle" @click="handleCopilotSidebarToggle"
/> />
</ButtonGroup> </div>
</template> </template>
@@ -21,10 +21,6 @@ const props = defineProps({
enableCannedResponses: { type: Boolean, default: true }, enableCannedResponses: { type: Boolean, default: true },
enabledMenuOptions: { type: Array, default: () => [] }, enabledMenuOptions: { type: Array, default: () => [] },
enableCaptainTools: { type: Boolean, default: false }, enableCaptainTools: { type: Boolean, default: false },
signature: { type: String, default: '' },
allowSignature: { type: Boolean, default: false },
sendWithSignature: { type: Boolean, default: false },
channelType: { type: String, default: '' },
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
@@ -104,10 +100,6 @@ watch(
:enable-canned-responses="enableCannedResponses" :enable-canned-responses="enableCannedResponses"
:enabled-menu-options="enabledMenuOptions" :enabled-menu-options="enabledMenuOptions"
:enable-captain-tools="enableCaptainTools" :enable-captain-tools="enableCaptainTools"
:signature="signature"
:allow-signature="allowSignature"
:send-with-signature="sendWithSignature"
:channel-type="channelType"
@input="handleInput" @input="handleInput"
@focus="handleFocus" @focus="handleFocus"
@blur="handleBlur" @blur="handleBlur"
@@ -14,7 +14,6 @@ import {
} from 'dashboard/helper/portalHelper'; } from 'dashboard/helper/portalHelper';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import ButtonGroup from 'dashboard/components-next/buttonGroup/ButtonGroup.vue';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue'; import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
@@ -141,12 +140,11 @@ const updateArticleStatus = async ({ value }) => {
:disabled="!articleId" :disabled="!articleId"
@click="previewArticle" @click="previewArticle"
/> />
<ButtonGroup class="flex items-center"> <div class="flex items-center">
<Button <Button
:label="t('HELP_CENTER.EDIT_ARTICLE_PAGE.HEADER.PUBLISH')" :label="t('HELP_CENTER.EDIT_ARTICLE_PAGE.HEADER.PUBLISH')"
size="sm" size="sm"
class="ltr:rounded-r-none rtl:rounded-l-none" class="ltr:rounded-r-none rtl:rounded-l-none"
no-animation
:is-loading="isArticlePublishing" :is-loading="isArticlePublishing"
:disabled=" :disabled="
status === ARTICLE_STATUSES.PUBLISHED || status === ARTICLE_STATUSES.PUBLISHED ||
@@ -161,7 +159,6 @@ const updateArticleStatus = async ({ value }) => {
icon="i-lucide-chevron-down" icon="i-lucide-chevron-down"
size="sm" size="sm"
:disabled="!articleId" :disabled="!articleId"
no-animation
class="ltr:rounded-l-none rtl:rounded-r-none" class="ltr:rounded-l-none rtl:rounded-r-none"
@click.stop="showArticleActionMenu = !showArticleActionMenu" @click.stop="showArticleActionMenu = !showArticleActionMenu"
/> />
@@ -173,7 +170,7 @@ const updateArticleStatus = async ({ value }) => {
/> />
</OnClickOutside> </OnClickOutside>
</div> </div>
</ButtonGroup> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -92,6 +92,7 @@ const setSignature = () => {
const toggleMessageSignature = () => { const toggleMessageSignature = () => {
setSignatureFlagForInbox(props.channelType, !sendWithSignature.value); setSignatureFlagForInbox(props.channelType, !sendWithSignature.value);
setSignature();
}; };
// Added this watch to dynamically set signature on target inbox change. // Added this watch to dynamically set signature on target inbox change.
@@ -199,20 +199,16 @@ const handleInboxAction = ({ value, action, ...rest }) => {
state.attachedFiles = []; state.attachedFiles = [];
}; };
const removeSignatureFromMessage = () => {
// Always remove the signature from message content when inbox/contact is removed
// to ensure no leftover signature content remains
const signatureToRemove = inboxTypes.value.isEmailOrWebWidget
? props.messageSignature
: extractTextFromMarkdown(props.messageSignature);
if (signatureToRemove) {
state.message = removeSignature(state.message, signatureToRemove);
}
};
const removeTargetInbox = value => { const removeTargetInbox = value => {
v$.value.$reset(); v$.value.$reset();
removeSignatureFromMessage(); // Remove the signature from message content
// Based on the Advance Editor (used in isEmailOrWebWidget) and Plain editor(all other inboxes except WhatsApp)
if (props.sendWithSignature) {
const signatureToRemove = inboxTypes.value.isEmailOrWebWidget
? props.messageSignature
: extractTextFromMarkdown(props.messageSignature);
state.message = removeSignature(state.message, signatureToRemove);
}
emit('updateTargetInbox', value); emit('updateTargetInbox', value);
state.attachedFiles = []; state.attachedFiles = [];
}; };
@@ -220,7 +216,6 @@ const removeTargetInbox = value => {
const clearSelectedContact = () => { const clearSelectedContact = () => {
emit('clearSelectedContact'); emit('clearSelectedContact');
state.attachedFiles = []; state.attachedFiles = [];
removeSignatureFromMessage();
}; };
const onClickInsertEmoji = emoji => { const onClickInsertEmoji = emoji => {
@@ -359,7 +354,6 @@ const shouldShowMessageEditor = computed(() => {
:is-email-or-web-widget-inbox="inboxTypes.isEmailOrWebWidget" :is-email-or-web-widget-inbox="inboxTypes.isEmailOrWebWidget"
:has-errors="validationStates.isMessageInvalid" :has-errors="validationStates.isMessageInvalid"
:has-attachments="state.attachedFiles.length > 0" :has-attachments="state.attachedFiles.length > 0"
:channel-type="inboxChannelType"
/> />
<AttachmentPreviews <AttachmentPreviews
@@ -17,7 +17,6 @@ const props = defineProps({
hasAttachments: { type: Boolean, default: false }, hasAttachments: { type: Boolean, default: false },
sendWithSignature: { type: Boolean, default: false }, sendWithSignature: { type: Boolean, default: false },
messageSignature: { type: String, default: '' }, messageSignature: { type: String, default: '' },
channelType: { type: String, default: '' },
}); });
const { t } = useI18n(); const { t } = useI18n();
@@ -91,10 +90,6 @@ const replaceText = async message => {
" "
enable-variables enable-variables
:show-character-count="false" :show-character-count="false"
:signature="messageSignature"
allow-signature
:send-with-signature="sendWithSignature"
:channel-type="channelType"
/> />
</template> </template>
<template v-else> <template v-else>
@@ -36,7 +36,6 @@ const props = defineProps({
icon: { type: [String, Object, Function], default: '' }, icon: { type: [String, Object, Function], default: '' },
trailingIcon: { type: Boolean, default: false }, trailingIcon: { type: Boolean, default: false },
isLoading: { type: Boolean, default: false }, isLoading: { type: Boolean, default: false },
noAnimation: { type: Boolean, default: false },
}); });
const slots = useSlots(); const slots = useSlots();
@@ -180,18 +179,12 @@ const STYLE_CONFIG = {
md: 'text-sm font-medium', md: 'text-sm font-medium',
lg: 'text-base', lg: 'text-base',
}, },
clickAnimation: {
xs: 'active:enabled:scale-[0.97]',
sm: 'active:enabled:scale-[0.97]',
md: 'active:enabled:scale-[0.98]',
lg: 'active:enabled:scale-[0.98]',
},
justify: { justify: {
start: 'justify-start', start: 'justify-start',
center: 'justify-center', center: 'justify-center',
end: 'justify-end', end: 'justify-end',
}, },
base: 'inline-flex items-center min-w-0 gap-2 transition-all duration-100 ease-out border-0 rounded-lg outline-1 outline disabled:opacity-50', base: 'inline-flex items-center min-w-0 gap-2 transition-all duration-200 ease-in-out border-0 rounded-lg outline-1 outline disabled:opacity-50',
}; };
const variantClasses = computed(() => { const variantClasses = computed(() => {
@@ -228,12 +221,6 @@ const linkButtonClasses = computed(() => {
return classes.join(' '); return classes.join(' ');
}); });
const animationClasses = computed(() => {
return props.noAnimation
? ''
: STYLE_CONFIG.clickAnimation[computedSize.value];
});
</script> </script>
<template> <template>
@@ -243,7 +230,6 @@ const animationClasses = computed(() => {
[STYLE_CONFIG.base]: true, [STYLE_CONFIG.base]: true,
[isLink ? linkButtonClasses : buttonClasses]: true, [isLink ? linkButtonClasses : buttonClasses]: true,
[STYLE_CONFIG.fontSize[computedSize]]: true, [STYLE_CONFIG.fontSize[computedSize]]: true,
[animationClasses]: true,
[STYLE_CONFIG.justify[computedJustify]]: true, [STYLE_CONFIG.justify[computedJustify]]: true,
'flex-row-reverse': trailingIcon && !isIconOnly, 'flex-row-reverse': trailingIcon && !isIconOnly,
}" }"
@@ -1,20 +0,0 @@
<script setup>
defineProps({
noAnimation: {
type: Boolean,
default: false,
},
});
</script>
<template>
<div
:class="
noAnimation
? ''
: 'has-[button:not(:disabled):active]:scale-[0.98] transition-transform duration-150 ease-out'
"
>
<slot />
</div>
</template>
@@ -1,16 +1,11 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { computed } from 'vue';
import { OnClickOutside } from '@vueuse/components';
import { useRoute } from 'vue-router';
import { useMapGetter } from 'dashboard/composables/store.js';
import { usePolicy } from 'dashboard/composables/usePolicy'; import { usePolicy } from 'dashboard/composables/usePolicy';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import BackButton from 'dashboard/components/widgets/BackButton.vue'; import BackButton from 'dashboard/components/widgets/BackButton.vue';
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue'; import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue'; import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import Policy from 'dashboard/components/policy.vue'; import Policy from 'dashboard/components/policy.vue';
import AssistantSwitcher from 'dashboard/components-next/captain/pageComponents/switcher/AssistantSwitcher.vue';
import CreateAssistantDialog from 'dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue';
const props = defineProps({ const props = defineProps({
currentPage: { currentPage: {
@@ -61,32 +56,11 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
showAssistantSwitcher: {
type: Boolean,
default: true,
},
}); });
const emit = defineEmits(['click', 'close', 'update:currentPage']); const emit = defineEmits(['click', 'close', 'update:currentPage']);
const route = useRoute();
const { shouldShowPaywall } = usePolicy(); const { shouldShowPaywall } = usePolicy();
const showAssistantSwitcherDropdown = ref(false);
const createAssistantDialogRef = ref(null);
const assistants = useMapGetter('captainAssistants/getRecords');
const currentAssistantId = computed(() => route.params.assistantId);
const activeAssistantName = computed(() => {
return (
assistants.value?.find(
assistant => assistant.id === Number(currentAssistantId.value)
)?.name || ''
);
});
const showPaywall = computed(() => { const showPaywall = computed(() => {
return shouldShowPaywall(props.featureFlag); return shouldShowPaywall(props.featureFlag);
}); });
@@ -98,15 +72,6 @@ const handleButtonClick = () => {
const handlePageChange = event => { const handlePageChange = event => {
emit('update:currentPage', event); emit('update:currentPage', event);
}; };
const toggleAssistantSwitcher = () => {
showAssistantSwitcherDropdown.value = !showAssistantSwitcherDropdown.value;
};
const handleCreateAssistant = () => {
showAssistantSwitcherDropdown.value = false;
createAssistantDialogRef.value.dialogRef.open();
};
</script> </script>
<template> <template>
@@ -116,78 +81,37 @@ const handleCreateAssistant = () => {
<div <div
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" 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-3 items-center"> <div class="flex gap-4 items-center">
<BackButton v-if="backUrl" :back-url="backUrl" /> <BackButton v-if="backUrl" :to="backUrl" />
<div v-if="showAssistantSwitcher" class="flex items-center gap-2"> <slot name="headerTitle">
<div class="flex items-center gap-2"> <span class="text-xl font-medium text-n-slate-12">
<span
v-if="activeAssistantName"
class="text-xl font-medium truncate text-n-slate-12"
>
{{ activeAssistantName }}
</span>
<div v-if="activeAssistantName" class="relative group">
<OnClickOutside
@trigger="showAssistantSwitcherDropdown = false"
>
<Button
icon="i-lucide-chevron-down"
variant="ghost"
color="slate"
size="xs"
class="rounded-md group-hover:bg-n-slate-3 hover:bg-n-slate-3 [&>span]:size-4"
@click="toggleAssistantSwitcher"
/>
<AssistantSwitcher
v-if="showAssistantSwitcherDropdown"
class="absolute ltr:left-0 rtl:right-0 top-9"
@close="showAssistantSwitcherDropdown = false"
@create-assistant="handleCreateAssistant"
/>
</OnClickOutside>
</div>
</div>
</div>
<div class="flex items-center gap-4">
<div
v-if="showAssistantSwitcher && headerTitle"
class="w-0.5 h-4 rounded-2xl bg-n-weak"
/>
<span
v-if="headerTitle"
class="text-xl font-medium text-n-slate-12"
>
{{ headerTitle }} {{ headerTitle }}
</span> </span>
<div </slot>
v-if="!isEmpty && showKnowMore" <div
class="flex items-center gap-2" 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 class="w-0.5 h-4 rounded-2xl bg-n-weak" />
</div> <slot name="knowMore" />
</div> </div>
</div> </div>
<div class="flex gap-2"> <div
<slot name="search" /> v-if="!showPaywall && buttonLabel"
<div v-on-clickaway="() => emit('close')"
v-if="!showPaywall && buttonLabel" class="relative group/campaign-button"
v-on-clickaway="() => emit('close')" >
class="relative group/captain-button" <Policy :permissions="buttonPolicy">
> <Button
<Policy :permissions="buttonPolicy"> :label="buttonLabel"
<Button icon="i-lucide-plus"
:label="buttonLabel" size="sm"
icon="i-lucide-plus" class="group-hover/campaign-button:brightness-110"
size="sm" @click="handleButtonClick"
class="group-hover/captain-button:brightness-110" />
@click="handleButtonClick" </Policy>
/> <slot name="action" />
</Policy>
<slot name="action" />
</div>
</div> </div>
</div> </div>
<slot name="subHeader" /> <slot name="subHeader" />
@@ -220,6 +144,5 @@ const handleCreateAssistant = () => {
@update:current-page="handlePageChange" @update:current-page="handlePageChange"
/> />
</footer> </footer>
<CreateAssistantDialog ref="createAssistantDialogRef" type="create" />
</section> </section>
</template> </template>
@@ -0,0 +1,91 @@
<script setup>
import { useRouter } from 'vue-router';
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
defineProps({
isFetching: {
type: Boolean,
default: false,
},
isEmpty: {
type: Boolean,
default: false,
},
currentPage: {
type: Number,
default: 1,
},
totalCount: {
type: Number,
default: 100,
},
itemsPerPage: {
type: Number,
default: 25,
},
showPaginationFooter: {
type: Boolean,
default: false,
},
breadcrumbItems: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update:currentPage']);
const router = useRouter();
const handlePageChange = event => {
emit('update:currentPage', event);
};
const handleBreadcrumbClick = item => {
router.push({
name: item.routeName,
});
};
</script>
<template>
<section
class="px-6 flex flex-col w-full h-screen overflow-y-auto bg-n-background"
>
<div class="max-w-[60rem] mx-auto flex flex-col w-full h-full mb-4">
<header class="mb-7 sticky top-0 bg-n-background pt-4 z-20">
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
</header>
<main class="flex gap-16 w-full flex-1 pb-16">
<section
v-if="$slots.body || $slots.emptyState || isFetching"
class="flex flex-col w-full"
>
<div
v-if="isFetching"
class="flex items-center justify-center py-10 text-n-slate-11"
>
<Spinner />
</div>
<div v-else-if="isEmpty">
<slot name="emptyState" />
</div>
<slot v-else name="body" />
</section>
<section v-if="$slots.controls" class="flex w-full">
<slot name="controls" />
</section>
</main>
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10 pb-4">
<PaginationFooter
:current-page="currentPage"
:total-items="totalCount"
:items-per-page="itemsPerPage"
@update:current-page="handlePageChange"
/>
</footer>
</div>
</section>
</template>
@@ -76,11 +76,12 @@ const handleAction = ({ action, value }) => {
<template> <template>
<CardLayout> <CardLayout>
<div class="flex justify-between w-full gap-1"> <div class="flex justify-between w-full gap-1">
<h6 <router-link
class="text-base font-normal text-n-slate-12 line-clamp-1 hover:underline transition-colors" :to="{ name: 'captain_assistants_edit', params: { assistantId: id } }"
class="text-base text-n-slate-12 line-clamp-1 hover:underline transition-colors"
> >
{{ name }} {{ name }}
</h6> </router-link>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <div
v-on-clickaway="() => toggleDropdown(false)" v-on-clickaway="() => toggleDropdown(false)"
@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, watch } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
import MessageList from './MessageList.vue'; import MessageList from './MessageList.vue';
@@ -29,16 +29,6 @@ const resetConversation = () => {
newMessage.value = ''; newMessage.value = '';
}; };
// Watch for assistant ID changes and reset conversation
watch(
() => assistantId,
(newId, oldId) => {
if (oldId && newId !== oldId) {
resetConversation();
}
}
);
const sendMessage = async () => { const sendMessage = async () => {
if (!newMessage.value.trim() || isLoading.value) return; if (!newMessage.value.trim() || isLoading.value) return;
@@ -75,17 +65,16 @@ const sendMessage = async () => {
<template> <template>
<div <div
class="flex flex-col h-full rounded-xl border py-6 border-n-weak text-n-slate-11" class="flex flex-col h-full rounded-lg p-4 border border-n-slate-4 text-n-slate-11"
> >
<div class="mb-8 px-6"> <div class="mb-4">
<div class="flex justify-between items-center mb-1"> <div class="flex justify-between items-center mb-1">
<h3 class="text-lg font-medium"> <h3 class="text-lg font-medium">
{{ t('CAPTAIN.PLAYGROUND.HEADER') }} {{ t('CAPTAIN.PLAYGROUND.HEADER') }}
</h3> </h3>
<NextButton <NextButton
ghost ghost
sm size="small"
slate
icon="i-lucide-rotate-ccw" icon="i-lucide-rotate-ccw"
@click="resetConversation" @click="resetConversation"
/> />
@@ -98,17 +87,17 @@ const sendMessage = async () => {
<MessageList :messages="messages" :is-loading="isLoading" /> <MessageList :messages="messages" :is-loading="isLoading" />
<div <div
class="flex items-center mx-6 bg-n-background outline outline-1 outline-n-weak rounded-xl p-3" class="flex items-center bg-n-solid-1 outline outline-n-container rounded-lg p-3"
> >
<input <input
v-model="newMessage" v-model="newMessage"
class="flex-1 bg-transparent border-none focus:outline-none text-sm mb-0 text-n-slate-12 placeholder:text-n-slate-10" class="flex-1 bg-transparent border-none focus:outline-none text-sm mb-0"
:placeholder="t('CAPTAIN.PLAYGROUND.MESSAGE_PLACEHOLDER')" :placeholder="t('CAPTAIN.PLAYGROUND.MESSAGE_PLACEHOLDER')"
@keyup.enter="sendMessage" @keyup.enter="sendMessage"
/> />
<NextButton <NextButton
ghost ghost
sm size="small"
:disabled="!newMessage.trim()" :disabled="!newMessage.trim()"
icon="i-lucide-send" icon="i-lucide-send"
@click="sendMessage" @click="sendMessage"
@@ -35,8 +35,8 @@ const getAvatarName = sender =>
const getMessageStyle = sender => const getMessageStyle = sender =>
isUserMessage(sender) isUserMessage(sender)
? 'bg-n-solid-blue text-n-slate-12 rounded-br-sm rounded-bl-xl rounded-t-xl' ? 'bg-n-strong text-n-white'
: 'bg-n-solid-iris text-n-slate-12 rounded-bl-sm rounded-br-xl rounded-t-xl'; : 'bg-n-solid-iris text-n-slate-12';
const scrollToBottom = async () => { const scrollToBottom = async () => {
await nextTick(); await nextTick();
@@ -49,10 +49,7 @@ watch(() => props.messages.length, scrollToBottom);
</script> </script>
<template> <template>
<div <div ref="messageContainer" class="flex-1 overflow-y-auto mb-4 space-y-2">
ref="messageContainer"
class="flex-1 overflow-y-auto mb-4 px-6 space-y-6"
>
<div <div
v-for="(message, index) in messages" v-for="(message, index) in messages"
:key="index" :key="index"
@@ -60,20 +57,15 @@ watch(() => props.messages.length, scrollToBottom);
:class="getMessageAlignment(message.sender)" :class="getMessageAlignment(message.sender)"
> >
<div <div
class="flex items-end gap-1.5 max-w-[90%] md:max-w-[60%]" class="flex items-start gap-1.5"
:class="getMessageDirection(message.sender)" :class="getMessageDirection(message.sender)"
> >
<Avatar <Avatar :name="getAvatarName(message.sender)" rounded-full :size="24" />
:name="getAvatarName(message.sender)"
rounded-full
:size="24"
class="shrink-0"
/>
<div <div
class="px-4 py-3 text-sm [overflow-wrap:break-word]" class="max-w-[80%] rounded-lg p-3 text-sm"
:class="getMessageStyle(message.sender)" :class="getMessageStyle(message.sender)"
> >
<div v-html="formatMessage(message.content)" /> <div class="break-words" v-html="formatMessage(message.content)" />
</div> </div>
</div> </div>
</div> </div>
@@ -0,0 +1,68 @@
<script setup>
import { computed, ref } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n';
import { OnClickOutside } from '@vueuse/components';
import Button from 'dashboard/components-next/button/Button.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
const props = defineProps({
assistantId: {
type: [String, Number],
required: true,
},
});
const emit = defineEmits(['update']);
const { t } = useI18n();
const isFilterOpen = ref(false);
const assistants = useMapGetter('captainAssistants/getRecords');
const assistantOptions = computed(() => [
{
label: t(`CAPTAIN.RESPONSES.FILTER.ALL_ASSISTANTS`),
value: 'all',
action: 'filter',
},
...assistants.value.map(assistant => ({
value: assistant.id,
label: assistant.name,
action: 'filter',
})),
]);
const selectedAssistantLabel = computed(() => {
const assistant = assistantOptions.value.find(
option => option.value === props.assistantId
);
return t('CAPTAIN.RESPONSES.FILTER.ASSISTANT', {
selected: assistant ? assistant.label : '',
});
});
const handleAssistantFilterChange = ({ value }) => {
isFilterOpen.value = false;
emit('update', value);
};
</script>
<template>
<OnClickOutside @trigger="isFilterOpen = false">
<Button
:label="selectedAssistantLabel"
icon="i-lucide-chevron-down"
size="sm"
color="slate"
trailing-icon
class="max-w-48"
@click="isFilterOpen = !isFilterOpen"
/>
<DropdownMenu
v-if="isFilterOpen"
:menu-items="assistantOptions"
class="mt-2"
@action="handleAssistantFilterChange"
/>
</OnClickOutside>
</template>
@@ -18,7 +18,7 @@ const props = defineProps({
validator: value => ['create', 'edit'].includes(value), validator: value => ['create', 'edit'].includes(value),
}, },
}); });
const emit = defineEmits(['close', 'created']); const emit = defineEmits(['close']);
const { t } = useI18n(); const { t } = useI18n();
const store = useStore(); const store = useStore();
@@ -35,18 +35,8 @@ const i18nKey = computed(
() => `CAPTAIN.ASSISTANTS.${props.type.toUpperCase()}` () => `CAPTAIN.ASSISTANTS.${props.type.toUpperCase()}`
); );
const createAssistant = async assistantDetails => { const createAssistant = assistantDetails =>
try { store.dispatch('captainAssistants/create', assistantDetails);
const newAssistant = await store.dispatch(
'captainAssistants/create',
assistantDetails
);
emit('created', newAssistant);
} catch (error) {
const errorMessage = error?.message || t(`${i18nKey.value}.ERROR_MESSAGE`);
useAlert(errorMessage);
}
};
const handleSubmit = async updatedAssistant => { const handleSubmit = async updatedAssistant => {
try { try {
@@ -0,0 +1,333 @@
<script setup>
import { reactive, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
import { useMapGetter } from 'dashboard/composables/store';
import Input from 'dashboard/components-next/input/Input.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
import Accordion from 'dashboard/components-next/Accordion/Accordion.vue';
const props = defineProps({
mode: {
type: String,
required: true,
validator: value => ['edit', 'create'].includes(value),
},
assistant: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['submit']);
const { t } = useI18n();
const formState = {
uiFlags: useMapGetter('captainAssistants/getUIFlags'),
};
const initialState = {
name: '',
description: '',
productName: '',
welcomeMessage: '',
handoffMessage: '',
resolutionMessage: '',
instructions: '',
features: {
conversationFaqs: false,
memories: false,
citations: false,
},
temperature: 1,
};
const state = reactive({ ...initialState });
const validationRules = {
name: { required, minLength: minLength(1) },
description: { required, minLength: minLength(1) },
productName: { required, minLength: minLength(1) },
welcomeMessage: { minLength: minLength(1) },
handoffMessage: { minLength: minLength(1) },
resolutionMessage: { minLength: minLength(1) },
instructions: { minLength: minLength(1) },
};
const v$ = useVuelidate(validationRules, state);
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
const getErrorMessage = field => {
return v$.value[field].$error ? v$.value[field].$errors[0].$message : '';
};
const formErrors = computed(() => ({
name: getErrorMessage('name'),
description: getErrorMessage('description'),
productName: getErrorMessage('productName'),
welcomeMessage: getErrorMessage('welcomeMessage'),
handoffMessage: getErrorMessage('handoffMessage'),
resolutionMessage: getErrorMessage('resolutionMessage'),
instructions: getErrorMessage('instructions'),
}));
const updateStateFromAssistant = assistant => {
const { config = {} } = assistant;
state.name = assistant.name;
state.description = assistant.description;
state.productName = config.product_name;
state.welcomeMessage = config.welcome_message;
state.handoffMessage = config.handoff_message;
state.resolutionMessage = config.resolution_message;
state.instructions = config.instructions;
state.features = {
conversationFaqs: config.feature_faq || false,
memories: config.feature_memory || false,
citations: config.feature_citation || false,
};
state.temperature = config.temperature || 1;
};
const handleBasicInfoUpdate = async () => {
const result = await Promise.all([
v$.value.name.$validate(),
v$.value.description.$validate(),
v$.value.productName.$validate(),
]).then(results => results.every(Boolean));
if (!result) return;
const payload = {
name: state.name,
description: state.description,
config: {
...props.assistant.config,
product_name: state.productName,
},
};
emit('submit', payload);
};
const handleSystemMessagesUpdate = async () => {
const result = await Promise.all([
v$.value.welcomeMessage.$validate(),
v$.value.handoffMessage.$validate(),
v$.value.resolutionMessage.$validate(),
]).then(results => results.every(Boolean));
if (!result) return;
const payload = {
config: {
...props.assistant.config,
welcome_message: state.welcomeMessage,
handoff_message: state.handoffMessage,
resolution_message: state.resolutionMessage,
},
};
emit('submit', payload);
};
const handleInstructionsUpdate = async () => {
const result = await v$.value.instructions.$validate();
if (!result) return;
const payload = {
config: {
...props.assistant.config,
temperature: state.temperature || 1,
instructions: state.instructions,
},
};
emit('submit', payload);
};
const handleFeaturesUpdate = () => {
const payload = {
config: {
...props.assistant.config,
feature_faq: state.features.conversationFaqs,
feature_memory: state.features.memories,
feature_citation: state.features.citations,
},
};
emit('submit', payload);
};
watch(
() => props.assistant,
newAssistant => {
if (props.mode === 'edit' && newAssistant) {
updateStateFromAssistant(newAssistant);
}
},
{ immediate: true }
);
</script>
<template>
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
<!-- Basic Information Section -->
<Accordion
:title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.BASIC_INFO')"
is-open
>
<div class="flex flex-col gap-4 pt-4">
<Input
v-model="state.name"
:label="t('CAPTAIN.ASSISTANTS.FORM.NAME.LABEL')"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.NAME.PLACEHOLDER')"
:message="formErrors.name"
:message-type="formErrors.name ? 'error' : 'info'"
/>
<Editor
v-model="state.description"
:label="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.LABEL')"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.PLACEHOLDER')"
:message="formErrors.description"
:message-type="formErrors.description ? 'error' : 'info'"
/>
<Input
v-model="state.productName"
:label="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.LABEL')"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.PLACEHOLDER')"
:message="formErrors.productName"
:message-type="formErrors.productName ? 'error' : 'info'"
/>
<div class="flex justify-end">
<Button
size="small"
:loading="isLoading"
@click="handleBasicInfoUpdate"
>
{{ t('CAPTAIN.ASSISTANTS.FORM.UPDATE') }}
</Button>
</div>
</div>
</Accordion>
<!-- Instructions Section -->
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.INSTRUCTIONS')">
<div class="flex flex-col gap-4">
<Editor
v-model="state.instructions"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
:message="formErrors.instructions"
:max-length="20000"
:message-type="formErrors.instructions ? 'error' : 'info'"
/>
<div class="flex flex-col gap-2 mt-4">
<label class="text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.LABEL') }}
</label>
<div class="flex items-center gap-4">
<input
v-model="state.temperature"
type="range"
min="0"
max="1"
step="0.1"
class="w-full"
/>
<span class="text-sm text-n-slate-12">{{ state.temperature }}</span>
</div>
<p class="text-sm text-n-slate-11 italic">
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.DESCRIPTION') }}
</p>
</div>
<div class="flex justify-end">
<Button
size="small"
:loading="isLoading"
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
@click="handleInstructionsUpdate"
/>
</div>
</div>
</Accordion>
<!-- Greeting Messages Section -->
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.SYSTEM_MESSAGES')">
<div class="flex flex-col gap-4 pt-4">
<Editor
v-model="state.handoffMessage"
:label="t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.LABEL')"
:placeholder="
t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.PLACEHOLDER')
"
:message="formErrors.handoffMessage"
:message-type="formErrors.handoffMessage ? 'error' : 'info'"
/>
<Editor
v-model="state.resolutionMessage"
:label="t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.LABEL')"
:placeholder="
t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.PLACEHOLDER')
"
:message="formErrors.resolutionMessage"
:message-type="formErrors.resolutionMessage ? 'error' : 'info'"
/>
<div class="flex justify-end">
<Button
size="small"
:loading="isLoading"
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
@click="handleSystemMessagesUpdate"
/>
</div>
</div>
</Accordion>
<!-- Features Section -->
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.FEATURES')">
<div class="flex flex-col gap-4 pt-4">
<div class="flex flex-col gap-2">
<label class="text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.TITLE') }}
</label>
<div class="flex flex-col gap-2">
<label class="flex items-center gap-2">
<input
v-model="state.features.conversationFaqs"
type="checkbox"
/>
{{
t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS')
}}
</label>
<label class="flex items-center gap-2">
<input v-model="state.features.memories" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
</label>
<label class="flex items-center gap-2">
<input v-model="state.features.citations" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
</label>
</div>
</div>
<div class="flex justify-end">
<Button
size="small"
:loading="isLoading"
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
@click="handleFeaturesUpdate"
/>
</div>
</div>
</Accordion>
</form>
</template>
@@ -3,8 +3,6 @@ import { reactive, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { minLength } from '@vuelidate/validators'; import { minLength } from '@vuelidate/validators';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { useAccount } from 'dashboard/composables/useAccount';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue'; import Editor from 'dashboard/components-next/Editor/Editor.vue';
@@ -19,16 +17,10 @@ const props = defineProps({
const emit = defineEmits(['submit']); const emit = defineEmits(['submit']);
const { t } = useI18n(); const { t } = useI18n();
const { isCloudFeatureEnabled } = useAccount();
const isCaptainV2Enabled = computed(() =>
isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN_V2)
);
const initialState = { const initialState = {
handoffMessage: '', handoffMessage: '',
resolutionMessage: '', resolutionMessage: '',
instructions: '',
temperature: 1, temperature: 1,
}; };
@@ -37,7 +29,6 @@ const state = reactive({ ...initialState });
const validationRules = { const validationRules = {
handoffMessage: { minLength: minLength(1) }, handoffMessage: { minLength: minLength(1) },
resolutionMessage: { minLength: minLength(1) }, resolutionMessage: { minLength: minLength(1) },
instructions: { minLength: minLength(1) },
}; };
const v$ = useVuelidate(validationRules, state); const v$ = useVuelidate(validationRules, state);
@@ -49,30 +40,20 @@ const getErrorMessage = field => {
const formErrors = computed(() => ({ const formErrors = computed(() => ({
handoffMessage: getErrorMessage('handoffMessage'), handoffMessage: getErrorMessage('handoffMessage'),
resolutionMessage: getErrorMessage('resolutionMessage'), resolutionMessage: getErrorMessage('resolutionMessage'),
instructions: getErrorMessage('instructions'),
})); }));
const updateStateFromAssistant = assistant => { const updateStateFromAssistant = assistant => {
const { config = {} } = assistant; const { config = {} } = assistant;
state.handoffMessage = config.handoff_message; state.handoffMessage = config.handoff_message;
state.resolutionMessage = config.resolution_message; state.resolutionMessage = config.resolution_message;
state.instructions = config.instructions;
state.temperature = config.temperature || 1; state.temperature = config.temperature || 1;
}; };
const handleSystemMessagesUpdate = async () => { const handleSystemMessagesUpdate = async () => {
const validations = [ const result = await Promise.all([
v$.value.handoffMessage.$validate(), v$.value.handoffMessage.$validate(),
v$.value.resolutionMessage.$validate(), v$.value.resolutionMessage.$validate(),
]; ]).then(results => results.every(Boolean));
if (!isCaptainV2Enabled.value) {
validations.push(v$.value.instructions.$validate());
}
const result = await Promise.all(validations).then(results =>
results.every(Boolean)
);
if (!result) return; if (!result) return;
const payload = { const payload = {
@@ -84,10 +65,6 @@ const handleSystemMessagesUpdate = async () => {
}, },
}; };
if (!isCaptainV2Enabled.value) {
payload.config.instructions = state.instructions;
}
emit('submit', payload); emit('submit', payload);
}; };
@@ -118,16 +95,6 @@ watch(
:message-type="formErrors.resolutionMessage ? 'error' : 'info'" :message-type="formErrors.resolutionMessage ? 'error' : 'info'"
/> />
<Editor
v-if="!isCaptainV2Enabled"
v-model="state.instructions"
:label="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.LABEL')"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
:message="formErrors.instructions"
:max-length="20000"
:message-type="formErrors.instructions ? 'error' : 'info'"
/>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label class="text-sm font-medium text-n-slate-12"> <label class="text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.LABEL') }} {{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.LABEL') }}
@@ -8,13 +8,6 @@ import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue'; import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import DocumentForm from './DocumentForm.vue'; import DocumentForm from './DocumentForm.vue';
defineProps({
assistantId: {
type: Number,
required: true,
},
});
const emit = defineEmits(['close']); const emit = defineEmits(['close']);
const { t } = useI18n(); const { t } = useI18n();
const store = useStore(); const store = useStore();
@@ -55,11 +48,7 @@ defineExpose({ dialogRef });
:show-confirm-button="false" :show-confirm-button="false"
@close="handleClose" @close="handleClose"
> >
<DocumentForm <DocumentForm @submit="handleSubmit" @cancel="handleCancel" />
:assistant-id="assistantId"
@submit="handleSubmit"
@cancel="handleCancel"
/>
<template #footer /> <template #footer />
</Dialog> </Dialog>
</template> </template>
@@ -2,7 +2,7 @@
import { reactive, computed, ref, nextTick } from 'vue'; import { reactive, computed, ref, nextTick } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { minLength, requiredIf, url } from '@vuelidate/validators'; import { required, minLength, requiredIf, url } from '@vuelidate/validators';
import { useMapGetter } from 'dashboard/composables/store'; import { useMapGetter } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
@@ -10,13 +10,6 @@ import Input from 'dashboard/components-next/input/Input.vue';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue'; import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
const props = defineProps({
assistantId: {
type: Number,
required: true,
},
});
const emit = defineEmits(['submit', 'cancel']); const emit = defineEmits(['submit', 'cancel']);
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
@@ -25,11 +18,13 @@ const { t } = useI18n();
const formState = { const formState = {
uiFlags: useMapGetter('captainDocuments/getUIFlags'), uiFlags: useMapGetter('captainDocuments/getUIFlags'),
assistants: useMapGetter('captainAssistants/getRecords'),
}; };
const initialState = { const initialState = {
name: '', name: '',
url: '', url: '',
assistantId: null,
documentType: 'url', documentType: 'url',
pdfFile: null, pdfFile: null,
}; };
@@ -43,11 +38,19 @@ const validationRules = {
url: requiredIf(() => state.documentType === 'url' && url), url: requiredIf(() => state.documentType === 'url' && url),
minLength: requiredIf(() => state.documentType === 'url' && minLength(1)), minLength: requiredIf(() => state.documentType === 'url' && minLength(1)),
}, },
assistantId: { required },
pdfFile: { pdfFile: {
required: requiredIf(() => state.documentType === 'pdf'), required: requiredIf(() => state.documentType === 'pdf'),
}, },
}; };
const assistantList = computed(() =>
formState.assistants.value.map(assistant => ({
value: assistant.id,
label: assistant.name,
}))
);
const documentTypeOptions = [ const documentTypeOptions = [
{ value: 'url', label: t('CAPTAIN.DOCUMENTS.FORM.TYPE.URL') }, { value: 'url', label: t('CAPTAIN.DOCUMENTS.FORM.TYPE.URL') },
{ value: 'pdf', label: t('CAPTAIN.DOCUMENTS.FORM.TYPE.PDF') }, { value: 'pdf', label: t('CAPTAIN.DOCUMENTS.FORM.TYPE.PDF') },
@@ -67,6 +70,7 @@ const getErrorMessage = (field, errorKey) => {
const formErrors = computed(() => ({ const formErrors = computed(() => ({
url: getErrorMessage('url', 'URL'), url: getErrorMessage('url', 'URL'),
assistantId: getErrorMessage('assistantId', 'ASSISTANT'),
pdfFile: getErrorMessage('pdfFile', 'PDF_FILE'), pdfFile: getErrorMessage('pdfFile', 'PDF_FILE'),
})); }));
@@ -102,7 +106,7 @@ const openFileDialog = () => {
const prepareDocumentDetails = () => { const prepareDocumentDetails = () => {
const formData = new FormData(); const formData = new FormData();
formData.append('document[assistant_id]', props.assistantId); formData.append('document[assistant_id]', state.assistantId);
if (state.documentType === 'url') { if (state.documentType === 'url') {
formData.append('document[external_link]', state.url); formData.append('document[external_link]', state.url);
@@ -214,6 +218,21 @@ const handleSubmit = async () => {
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.NAME.PLACEHOLDER')" :placeholder="t('CAPTAIN.DOCUMENTS.FORM.NAME.PLACEHOLDER')"
/> />
<div class="flex flex-col gap-1">
<label for="assistant" class="mb-0.5 text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.DOCUMENTS.FORM.ASSISTANT.LABEL') }}
</label>
<ComboBox
id="assistant"
v-model="state.assistantId"
:options="assistantList"
:has-error="!!formErrors.assistantId"
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.ASSISTANT.PLACEHOLDER')"
class="[&>div>button]:bg-n-alpha-black2"
:message="formErrors.assistantId"
/>
</div>
<div class="flex gap-3 justify-between items-center w-full"> <div class="flex gap-3 justify-between items-center w-full">
<Button <Button
type="button" type="button"
@@ -3,7 +3,6 @@ import { ref, computed } from 'vue';
import { useStore } from 'dashboard/composables/store'; import { useStore } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue'; import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import ResponseForm from './ResponseForm.vue'; import ResponseForm from './ResponseForm.vue';
@@ -22,7 +21,6 @@ const props = defineProps({
const emit = defineEmits(['close']); const emit = defineEmits(['close']);
const { t } = useI18n(); const { t } = useI18n();
const store = useStore(); const store = useStore();
const route = useRoute();
const dialogRef = ref(null); const dialogRef = ref(null);
const responseForm = ref(null); const responseForm = ref(null);
@@ -41,15 +39,9 @@ const createResponse = responseDetails =>
const handleSubmit = async updatedResponse => { const handleSubmit = async updatedResponse => {
try { try {
if (props.type === 'edit') { if (props.type === 'edit') {
await updateResponse({ await updateResponse(updatedResponse);
...updatedResponse,
assistant_id: route.params.assistantId,
});
} else { } else {
await createResponse({ await createResponse(updatedResponse);
...updatedResponse,
assistant_id: route.params.assistantId,
});
} }
useAlert(t(`${i18nKey.value}.SUCCESS_MESSAGE`)); useAlert(t(`${i18nKey.value}.SUCCESS_MESSAGE`));
dialogRef.value.close(); dialogRef.value.close();
@@ -8,6 +8,7 @@ import { useMapGetter } from 'dashboard/composables/store';
import Input from 'dashboard/components-next/input/Input.vue'; import Input from 'dashboard/components-next/input/Input.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue'; import Editor from 'dashboard/components-next/Editor/Editor.vue';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
const props = defineProps({ const props = defineProps({
mode: { mode: {
@@ -20,17 +21,18 @@ const props = defineProps({
default: () => ({}), default: () => ({}),
}, },
}); });
const emit = defineEmits(['submit', 'cancel']); const emit = defineEmits(['submit', 'cancel']);
const { t } = useI18n(); const { t } = useI18n();
const formState = { const formState = {
uiFlags: useMapGetter('captainResponses/getUIFlags'), uiFlags: useMapGetter('captainResponses/getUIFlags'),
assistants: useMapGetter('captainAssistants/getRecords'),
}; };
const initialState = { const initialState = {
question: '', question: '',
answer: '', answer: '',
assistantId: null,
}; };
const state = reactive({ ...initialState }); const state = reactive({ ...initialState });
@@ -38,8 +40,16 @@ const state = reactive({ ...initialState });
const validationRules = { const validationRules = {
question: { required, minLength: minLength(1) }, question: { required, minLength: minLength(1) },
answer: { required, minLength: minLength(1) }, answer: { required, minLength: minLength(1) },
assistantId: { required },
}; };
const assistantList = computed(() =>
formState.assistants.value.map(assistant => ({
value: assistant.id,
label: assistant.name,
}))
);
const v$ = useVuelidate(validationRules, state); const v$ = useVuelidate(validationRules, state);
const isLoading = computed(() => formState.uiFlags.value.creatingItem); const isLoading = computed(() => formState.uiFlags.value.creatingItem);
@@ -53,6 +63,7 @@ const getErrorMessage = (field, errorKey) => {
const formErrors = computed(() => ({ const formErrors = computed(() => ({
question: getErrorMessage('question', 'QUESTION'), question: getErrorMessage('question', 'QUESTION'),
answer: getErrorMessage('answer', 'ANSWER'), answer: getErrorMessage('answer', 'ANSWER'),
assistantId: getErrorMessage('assistantId', 'ASSISTANT'),
})); }));
const handleCancel = () => emit('cancel'); const handleCancel = () => emit('cancel');
@@ -60,6 +71,7 @@ const handleCancel = () => emit('cancel');
const prepareDocumentDetails = () => ({ const prepareDocumentDetails = () => ({
question: state.question, question: state.question,
answer: state.answer, answer: state.answer,
assistant_id: state.assistantId,
}); });
const handleSubmit = async () => { const handleSubmit = async () => {
@@ -74,11 +86,12 @@ const handleSubmit = async () => {
const updateStateFromResponse = response => { const updateStateFromResponse = response => {
if (!response) return; if (!response) return;
const { question, answer } = response; const { question, answer, assistant } = response;
Object.assign(state, { Object.assign(state, {
question, question,
answer, answer,
assistantId: assistant.id,
}); });
}; };
@@ -102,6 +115,7 @@ watch(
:message="formErrors.question" :message="formErrors.question"
:message-type="formErrors.question ? 'error' : 'info'" :message-type="formErrors.question ? 'error' : 'info'"
/> />
<Editor <Editor
v-model="state.answer" v-model="state.answer"
:label="t('CAPTAIN.RESPONSES.FORM.ANSWER.LABEL')" :label="t('CAPTAIN.RESPONSES.FORM.ANSWER.LABEL')"
@@ -110,6 +124,22 @@ watch(
:max-length="10000" :max-length="10000"
:message-type="formErrors.answer ? 'error' : 'info'" :message-type="formErrors.answer ? 'error' : 'info'"
/> />
<div class="flex flex-col gap-1">
<label for="assistant" class="mb-0.5 text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.RESPONSES.FORM.ASSISTANT.LABEL') }}
</label>
<ComboBox
id="assistant"
v-model="state.assistantId"
:options="assistantList"
:has-error="!!formErrors.assistantId"
:placeholder="t('CAPTAIN.RESPONSES.FORM.ASSISTANT.PLACEHOLDER')"
class="[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:dark:outline-n-weak [&>div>button:not(.focused)]:hover:!outline-n-slate-6"
:message="formErrors.assistantId"
/>
</div>
<div class="flex items-center justify-between w-full gap-3"> <div class="flex items-center justify-between w-full gap-3">
<Button <Button
type="button" type="button"
@@ -1,143 +0,0 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
import Button from 'dashboard/components-next/button/Button.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
const emit = defineEmits(['close', 'createAssistant']);
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const store = useStore();
const assistants = useMapGetter('captainAssistants/getRecords');
const currentAssistantId = computed(() => route.params.assistantId);
const isAssistantActive = assistant => {
return assistant.id === Number(currentAssistantId.value);
};
const fetchDataForRoute = async (routeName, assistantId) => {
const dataFetchMap = {
captain_assistants_responses_index: async () => {
await store.dispatch('captainResponses/get', { assistantId });
await store.dispatch('captainResponses/fetchPendingCount', assistantId);
},
captain_assistants_responses_pending: async () => {
await store.dispatch('captainResponses/get', {
assistantId,
status: 'pending',
});
},
captain_assistants_documents_index: async () => {
await store.dispatch('captainDocuments/get', { assistantId });
},
captain_assistants_scenarios_index: async () => {
await store.dispatch('captainScenarios/get', { assistantId });
},
captain_assistants_playground_index: () => {
// Playground doesn't need pre-fetching, it loads on interaction
},
captain_assistants_inboxes_index: async () => {
await store.dispatch('captainInboxes/get', { assistantId });
},
captain_tools_index: async () => {
await store.dispatch('captainCustomTools/get', { page: 1 });
},
captain_assistants_settings_index: async () => {
await store.dispatch('captainAssistants/show', assistantId);
},
};
const fetchFn = dataFetchMap[routeName];
if (fetchFn) {
await fetchFn();
}
};
const handleAssistantChange = async assistant => {
if (isAssistantActive(assistant)) return;
const currentRouteName = route.name;
const targetRouteName =
currentRouteName || 'captain_assistants_responses_index';
await fetchDataForRoute(targetRouteName, assistant.id);
await router.push({
name: targetRouteName,
params: {
accountId: route.params.accountId,
assistantId: assistant.id,
},
});
emit('close');
};
const openCreateAssistantDialog = () => {
emit('createAssistant');
emit('close');
};
</script>
<template>
<div
class="pt-5 pb-3 bg-n-alpha-3 backdrop-blur-[100px] outline outline-n-container outline-1 z-50 absolute w-[27.5rem] rounded-xl shadow-md flex flex-col gap-4"
>
<div
class="flex items-center justify-between gap-4 px-6 pb-3 border-b border-n-alpha-2"
>
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2">
<h2
class="text-base font-medium cursor-pointer text-n-slate-12 w-fit hover:underline"
>
{{ t('CAPTAIN.ASSISTANT_SWITCHER.ASSISTANTS') }}
</h2>
</div>
<p class="text-sm text-n-slate-11">
{{ t('CAPTAIN.ASSISTANT_SWITCHER.SWITCH_ASSISTANT') }}
</p>
</div>
<Button
:label="t('CAPTAIN.ASSISTANT_SWITCHER.NEW_ASSISTANT')"
color="slate"
icon="i-lucide-plus"
size="sm"
class="!bg-n-alpha-2 hover:!bg-n-alpha-3"
@click="openCreateAssistantDialog"
/>
</div>
<div v-if="assistants.length > 0" class="flex flex-col gap-2 px-4">
<Button
v-for="assistant in assistants"
:key="assistant.id"
:label="assistant.name"
variant="ghost"
color="slate"
trailing-icon
:icon="isAssistantActive(assistant) ? 'i-lucide-check' : ''"
class="!justify-end !px-2 !py-2 hover:!bg-n-alpha-2 [&>.i-lucide-check]:text-n-teal-10 h-9"
size="sm"
@click="handleAssistantChange(assistant)"
>
<span class="text-sm font-medium truncate text-n-slate-12">
{{ assistant.name || '' }}
</span>
<Avatar
v-if="assistant"
:name="assistant.name"
:size="20"
icon-name="i-lucide-bot"
rounded-full
/>
</Button>
</div>
</div>
</template>
@@ -96,7 +96,6 @@ watch(
:label="selectedLabel" :label="selectedLabel"
trailing-icon trailing-icon
:disabled="disabled" :disabled="disabled"
no-animation
class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6 focus:outline-n-brand" class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6 focus:outline-n-brand"
:class="{ :class="{
focused: open, focused: open,
@@ -78,10 +78,8 @@ const handleSuggestion = opt => {
</p> </p>
<router-link <router-link
:to="{ :to="{
name: 'captain_assistants_create_index', name: 'captain_assistants_index',
params: { params: { accountId: route.params.accountId },
accountId: route.params.accountId,
},
}" }"
class="text-n-slate-11 underline hover:text-n-slate-12" class="text-n-slate-11 underline hover:text-n-slate-12"
> >
@@ -2,7 +2,6 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import ButtonGroup from 'dashboard/components-next/buttonGroup/ButtonGroup.vue';
import { useUISettings } from 'dashboard/composables/useUISettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
import { useMapGetter } from 'dashboard/composables/store'; import { useMapGetter } from 'dashboard/composables/store';
import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { FEATURE_FLAGS } from 'dashboard/featureFlags';
@@ -54,17 +53,14 @@ const toggleSidebar = () => {
v-if="showCopilotLauncher" v-if="showCopilotLauncher"
class="fixed bottom-4 ltr:right-4 rtl:left-4 z-50" class="fixed bottom-4 ltr:right-4 rtl:left-4 z-50"
> >
<ButtonGroup <div class="rounded-full bg-n-alpha-2 p-1">
class="rounded-full bg-n-alpha-2 backdrop-blur-lg p-1 shadow hover:shadow-md"
>
<Button <Button
icon="i-woot-captain" icon="i-woot-captain"
no-animation class="!rounded-full !bg-n-solid-3 dark:!bg-n-alpha-2 !text-n-slate-12 text-xl"
class="!rounded-full !bg-n-solid-3 dark:!bg-n-alpha-2 !text-n-slate-12 text-xl transition-all duration-200 ease-out hover:brightness-110"
lg lg
@click="toggleSidebar" @click="toggleSidebar"
/> />
</ButtonGroup> </div>
</div> </div>
<template v-else /> <template v-else />
</template> </template>
@@ -8,15 +8,11 @@ import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
const props = defineProps({ const props = defineProps({
menuItems: { menuItems: {
type: Array, type: Array,
default: () => [], required: true,
validator: value => { validator: value => {
return value.every(item => item.action && item.value && item.label); return value.every(item => item.action && item.value && item.label);
}, },
}, },
menuSections: {
type: Array,
default: () => [],
},
thumbnailSize: { thumbnailSize: {
type: Number, type: Number,
default: 20, default: 20,
@@ -46,62 +42,19 @@ const { t } = useI18n();
const searchInput = ref(null); const searchInput = ref(null);
const searchQuery = ref(''); const searchQuery = ref('');
const hasSections = computed(() => props.menuSections.length > 0);
const flattenedMenuItems = computed(() => {
if (!hasSections.value) {
return props.menuItems;
}
return props.menuSections.flatMap(section => section.items || []);
});
const filteredMenuItems = computed(() => { const filteredMenuItems = computed(() => {
if (!searchQuery.value) return flattenedMenuItems.value; if (!searchQuery.value) return props.menuItems;
return flattenedMenuItems.value.filter(item => return props.menuItems.filter(item =>
item.label.toLowerCase().includes(searchQuery.value.toLowerCase()) item.label.toLowerCase().includes(searchQuery.value.toLowerCase())
); );
}); });
const filteredMenuSections = computed(() => {
if (!hasSections.value) {
return [];
}
if (!searchQuery.value) {
return props.menuSections;
}
const query = searchQuery.value.toLowerCase();
return props.menuSections
.map(section => {
const filteredItems = (section.items || []).filter(item =>
item.label.toLowerCase().includes(query)
);
return {
...section,
items: filteredItems,
};
})
.filter(section => section.items.length > 0);
});
const handleAction = item => { const handleAction = item => {
const { action, value, ...rest } = item; const { action, value, ...rest } = item;
emit('action', { action, value, ...rest }); emit('action', { action, value, ...rest });
}; };
const shouldShowEmptyState = computed(() => {
if (hasSections.value) {
return filteredMenuSections.value.length === 0;
}
return filteredMenuItems.value.length === 0;
});
onMounted(() => { onMounted(() => {
if (searchInput.value && props.showSearch) { if (searchInput.value && props.showSearch) {
searchInput.value.focus(); searchInput.value.focus();
@@ -111,122 +64,54 @@ onMounted(() => {
<template> <template>
<div <div
class="bg-n-alpha-3 backdrop-blur-[100px] border-0 outline outline-1 outline-n-container absolute rounded-xl z-50 gap-2 flex flex-col min-w-[136px] shadow-lg pb-2 px-2" class="bg-n-alpha-3 backdrop-blur-[100px] border-0 outline outline-1 outline-n-container absolute rounded-xl z-50 py-2 px-2 gap-2 flex flex-col min-w-[136px] shadow-lg"
:class="{
'pt-2': !showSearch,
}"
> >
<div <div v-if="showSearch" class="relative">
v-if="showSearch" <span class="absolute i-lucide-search size-3.5 top-2 left-3" />
class="sticky top-0 bg-n-alpha-3 backdrop-blur-sm pt-2" <input
> ref="searchInput"
<div class="relative"> v-model="searchQuery"
<span class="absolute i-lucide-search size-3.5 top-2 left-3" /> type="search"
<input :placeholder="
ref="searchInput" searchPlaceholder || t('DROPDOWN_MENU.SEARCH_PLACEHOLDER')
v-model="searchQuery" "
type="search" class="reset-base w-full h-8 py-2 pl-10 pr-2 text-sm focus:outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
:placeholder=" />
searchPlaceholder || t('DROPDOWN_MENU.SEARCH_PLACEHOLDER')
"
class="reset-base w-full h-8 py-2 pl-10 pr-2 text-sm focus:outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
/>
</div>
</div> </div>
<template v-if="hasSections"> <button
<div v-for="(item, index) in filteredMenuItems"
v-for="(section, sectionIndex) in filteredMenuSections" :key="index"
:key="section.title || sectionIndex" type="button"
class="flex flex-col gap-1" class="inline-flex items-center justify-start w-full h-8 min-w-0 gap-2 px-2 py-1.5 transition-all duration-200 ease-in-out border-0 rounded-lg z-60 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2 disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50"
> :class="{
<p 'bg-n-alpha-1 dark:bg-n-solid-active': item.isSelected,
v-if="section.title" 'text-n-ruby-11': item.action === 'delete',
class="px-2 pt-2 text-xs font-medium text-n-slate-11 uppercase tracking-wide" 'text-n-slate-12': item.action !== 'delete',
> }"
{{ section.title }} :disabled="item.disabled"
</p> @click="handleAction(item)"
<button >
v-for="(item, itemIndex) in section.items" <slot name="thumbnail" :item="item">
:key="item.value || itemIndex" <Avatar
type="button" v-if="item.thumbnail"
class="inline-flex items-center justify-start w-full h-8 min-w-0 gap-2 px-2 py-1.5 transition-all duration-200 ease-in-out border-0 rounded-lg z-60 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2 disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50" :name="item.thumbnail.name"
:class="{ :src="item.thumbnail.src"
'bg-n-alpha-1 dark:bg-n-solid-active': item.isSelected, :size="thumbnailSize"
'text-n-ruby-11': item.action === 'delete', rounded-full
'text-n-slate-12': item.action !== 'delete',
}"
:disabled="item.disabled"
@click="handleAction(item)"
>
<slot name="thumbnail" :item="item">
<Avatar
v-if="item.thumbnail"
:name="item.thumbnail.name"
:src="item.thumbnail.src"
:size="thumbnailSize"
rounded-full
/>
</slot>
<Icon
v-if="item.icon"
:icon="item.icon"
class="flex-shrink-0 size-3.5"
/>
<span v-if="item.emoji" class="flex-shrink-0">{{ item.emoji }}</span>
<span
v-if="item.label"
class="min-w-0 text-sm truncate"
:class="labelClass"
>
{{ item.label }}
</span>
</button>
<div
v-if="sectionIndex < filteredMenuSections.length - 1"
class="h-px bg-n-alpha-2 mx-2 my-1"
/> />
</div> </slot>
</template> <Icon v-if="item.icon" :icon="item.icon" class="flex-shrink-0 size-3.5" />
<template v-else> <span v-if="item.emoji" class="flex-shrink-0">{{ item.emoji }}</span>
<button <span
v-for="(item, index) in filteredMenuItems" v-if="item.label"
:key="index" class="min-w-0 text-sm truncate"
type="button" :class="labelClass"
class="inline-flex items-center justify-start w-full h-8 min-w-0 gap-2 px-2 py-1.5 transition-all duration-200 ease-in-out border-0 rounded-lg z-60 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2 disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50"
:class="{
'bg-n-alpha-1 dark:bg-n-solid-active': item.isSelected,
'text-n-ruby-11': item.action === 'delete',
'text-n-slate-12': item.action !== 'delete',
}"
:disabled="item.disabled"
@click="handleAction(item)"
> >
<slot name="thumbnail" :item="item"> {{ item.label }}
<Avatar </span>
v-if="item.thumbnail" </button>
:name="item.thumbnail.name"
:src="item.thumbnail.src"
:size="thumbnailSize"
rounded-full
/>
</slot>
<Icon
v-if="item.icon"
:icon="item.icon"
class="flex-shrink-0 size-3.5"
/>
<span v-if="item.emoji" class="flex-shrink-0">{{ item.emoji }}</span>
<span
v-if="item.label"
class="min-w-0 text-sm truncate"
:class="labelClass"
>
{{ item.label }}
</span>
</button>
</template>
<div <div
v-if="shouldShowEmptyState" v-if="filteredMenuItems.length === 0"
class="text-sm text-n-slate-11 px-2 py-1.5" class="text-sm text-n-slate-11 px-2 py-1.5"
> >
{{ {{
@@ -1,9 +1,10 @@
<script setup> <script setup>
import { defineProps, computed, ref } from 'vue'; import { defineProps, computed, reactive } from 'vue';
import Message from './Message.vue'; import Message from './Message.vue';
import { MESSAGE_TYPES } from './constants.js'; import { MESSAGE_TYPES } from './constants.js';
import { useCamelCase } from 'dashboard/composables/useTransformKeys'; import { useCamelCase } from 'dashboard/composables/useTransformKeys';
import { useMapGetter, useStore } from 'dashboard/composables/store.js'; import { useMapGetter } from 'dashboard/composables/store.js';
import MessageApi from 'dashboard/api/inbox/message.js';
/** /**
* Props definition for the component * Props definition for the component
@@ -39,15 +40,53 @@ const props = defineProps({
}); });
const emit = defineEmits(['retry']); const emit = defineEmits(['retry']);
const store = useStore();
const fetchingConversations = ref(new Set());
const allMessages = computed(() => { const allMessages = computed(() => {
return useCamelCase(props.messages, { deep: true }); return useCamelCase(props.messages, { deep: true });
}); });
const currentChat = useMapGetter('getSelectedChat'); const currentChat = useMapGetter('getSelectedChat');
// Cache for fetched reply messages to avoid duplicate API calls
const fetchedReplyMessages = reactive(new Map());
/**
* Fetches a specific message from the API by trying to get messages around it
* @param {number} messageId - The ID of the message to fetch
* @param {number} conversationId - The ID of the conversation
* @returns {Promise<Object|null>} - The fetched message or null if not found/error
*/
const fetchReplyMessage = async (messageId, conversationId) => {
// Return cached result if already fetched
if (fetchedReplyMessages.has(messageId)) {
return fetchedReplyMessages.get(messageId);
}
try {
const response = await MessageApi.getPreviousMessages({
conversationId,
before: messageId + 100,
after: messageId - 100,
});
const messages = response.data?.payload || [];
const targetMessage = messages.find(msg => msg.id === messageId);
if (targetMessage) {
const camelCaseMessage = useCamelCase(targetMessage);
fetchedReplyMessages.set(messageId, camelCaseMessage);
return camelCaseMessage;
}
// Cache null result to avoid repeated API calls
fetchedReplyMessages.set(messageId, null);
return null;
} catch (error) {
fetchedReplyMessages.set(messageId, null);
return null;
}
};
/** /**
* Determines if a message should be grouped with the next message * Determines if a message should be grouped with the next message
* @param {Number} index - Index of the current message * @param {Number} index - Index of the current message
@@ -87,43 +126,36 @@ const shouldGroupWithNext = (index, searchList) => {
* @returns {Object|null} - The message being replied to, or null if not found * @returns {Object|null} - The message being replied to, or null if not found
*/ */
const getInReplyToMessage = parentMessage => { const getInReplyToMessage = parentMessage => {
if (!parentMessage) return null;
const inReplyToMessageId = const inReplyToMessageId =
parentMessage?.contentAttributes?.inReplyTo ?? parentMessage.contentAttributes?.inReplyTo ??
parentMessage?.content_attributes?.in_reply_to; parentMessage.content_attributes?.in_reply_to;
if (!inReplyToMessageId) return null; if (!inReplyToMessageId) return null;
// 1. Check props messages (already camelCased via allMessages) // Try to find in current messages first
const foundInProps = allMessages.value?.find( let replyMessage = props.messages?.find(msg => msg.id === inReplyToMessageId);
m => m.id === inReplyToMessageId
);
if (foundInProps) return foundInProps;
// 2. Check store messages // Then try store messages
const foundInStore = currentChat.value?.messages?.find( if (!replyMessage && currentChat.value?.messages) {
m => m.id === inReplyToMessageId replyMessage = currentChat.value.messages.find(
); msg => msg.id === inReplyToMessageId
if (foundInStore) return useCamelCase(foundInStore); );
// 3. Fetch if not currently fetching for this conversation
const conversationId = currentChat.value?.id;
if (
conversationId &&
!currentChat.value.allMessagesLoaded &&
!fetchingConversations.value.has(conversationId)
) {
fetchingConversations.value.add(conversationId);
store
.dispatch('fetchPreviousMessages', {
conversationId,
before: currentChat.value.messages?.[0]?.id ?? null,
})
.finally(() => {
fetchingConversations.value.delete(conversationId);
});
} }
return null; // Then check fetch cache
if (!replyMessage && fetchedReplyMessages.has(inReplyToMessageId)) {
replyMessage = fetchedReplyMessages.get(inReplyToMessageId);
}
// If still not found and we have conversation context, fetch it
if (!replyMessage && currentChat.value?.id) {
fetchReplyMessage(inReplyToMessageId, currentChat.value.id);
return null; // Let UI handle loading state
}
return replyMessage ? useCamelCase(replyMessage) : null;
}; };
</script> </script>
@@ -2,7 +2,6 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import ButtonGroup from 'dashboard/components-next/buttonGroup/ButtonGroup.vue';
defineProps({ defineProps({
isMobileSidebarOpen: { isMobileSidebarOpen: {
@@ -46,17 +45,14 @@ const toggleSidebar = () => {
}, },
]" ]"
> >
<ButtonGroup <div class="rounded-full bg-n-alpha-2 p-1">
class="rounded-full bg-n-alpha-2 backdrop-blur-lg p-1 shadow hover:shadow-md"
>
<Button <Button
icon="i-lucide-menu" icon="i-lucide-menu"
no-animation class="!rounded-full !bg-n-solid-3 dark:!bg-n-alpha-2 !text-n-slate-12 text-xl"
class="!rounded-full !bg-n-solid-3 dark:!bg-n-alpha-2 !text-n-slate-12 text-xl transition-all duration-200 ease-out hover:brightness-110"
lg lg
@click="toggleSidebar" @click="toggleSidebar"
/> />
</ButtonGroup> </div>
</div> </div>
<template v-else /> <template v-else />
</template> </template>
@@ -221,70 +221,26 @@ const menuItems = computed(() => {
name: 'Captain', name: 'Captain',
icon: 'i-woot-captain', icon: 'i-woot-captain',
label: t('SIDEBAR.CAPTAIN'), label: t('SIDEBAR.CAPTAIN'),
activeOn: ['captain_assistants_create_index'],
children: [ children: [
{ {
name: 'FAQs', name: 'Assistants',
label: t('SIDEBAR.CAPTAIN_RESPONSES'), label: t('SIDEBAR.CAPTAIN_ASSISTANTS'),
activeOn: [ to: accountScopedRoute('captain_assistants_index'),
'captain_assistants_responses_index',
'captain_assistants_responses_pending',
],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_responses_index',
}),
}, },
{ {
name: 'Documents', name: 'Documents',
label: t('SIDEBAR.CAPTAIN_DOCUMENTS'), label: t('SIDEBAR.CAPTAIN_DOCUMENTS'),
activeOn: ['captain_assistants_documents_index'], to: accountScopedRoute('captain_documents_index'),
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_documents_index',
}),
}, },
{ {
name: 'Scenarios', name: 'Responses',
label: t('SIDEBAR.CAPTAIN_SCENARIOS'), label: t('SIDEBAR.CAPTAIN_RESPONSES'),
activeOn: ['captain_assistants_scenarios_index'], to: accountScopedRoute('captain_responses_index'),
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_scenarios_index',
}),
},
{
name: 'Playground',
label: t('SIDEBAR.CAPTAIN_PLAYGROUND'),
activeOn: ['captain_assistants_playground_index'],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_playground_index',
}),
},
{
name: 'Inboxes',
label: t('SIDEBAR.CAPTAIN_INBOXES'),
activeOn: ['captain_assistants_inboxes_index'],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_inboxes_index',
}),
}, },
{ {
name: 'Tools', name: 'Tools',
label: t('SIDEBAR.CAPTAIN_TOOLS'), label: t('SIDEBAR.CAPTAIN_TOOLS'),
activeOn: ['captain_tools_index'], to: accountScopedRoute('captain_tools_index'),
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_tools_index',
}),
},
{
name: 'Settings',
label: t('SIDEBAR.CAPTAIN_SETTINGS'),
activeOn: [
'captain_assistants_settings_index',
'captain_assistants_guidelines_index',
'captain_assistants_guardrails_index',
],
to: accountScopedRoute('captain_assistants_index', {
navigationPath: 'captain_assistants_settings_index',
}),
}, },
], ],
}, },
@@ -19,46 +19,19 @@ export function useSidebarContext() {
return '/'; return '/';
}; };
// Helper to find route definition by name without resolving
const findRouteByName = name => {
const routes = router.getRoutes();
return routes.find(route => route.name === name);
};
const resolvePermissions = to => { const resolvePermissions = to => {
if (!to) return []; if (to) return router.resolve(to)?.meta?.permissions ?? [];
return [];
// If navigationPath param exists, get the target route definition
if (to.params?.navigationPath) {
const targetRoute = findRouteByName(to.params.navigationPath);
return targetRoute?.meta?.permissions ?? [];
}
return router.resolve(to)?.meta?.permissions ?? [];
}; };
const resolveFeatureFlag = to => { const resolveFeatureFlag = to => {
if (!to) return ''; if (to) return router.resolve(to)?.meta?.featureFlag || '';
return '';
// If navigationPath param exists, get the target route definition
if (to.params?.navigationPath) {
const targetRoute = findRouteByName(to.params.navigationPath);
return targetRoute?.meta?.featureFlag || '';
}
return router.resolve(to)?.meta?.featureFlag || '';
}; };
const resolveInstallationType = to => { const resolveInstallationType = to => {
if (!to) return []; if (to) return router.resolve(to)?.meta?.installationTypes || [];
return [];
// If navigationPath param exists, get the target route definition
if (to.params?.navigationPath) {
const targetRoute = findRouteByName(to.params.navigationPath);
return targetRoute?.meta?.installationTypes || [];
}
return router.resolve(to)?.meta?.installationTypes || [];
}; };
const isAllowed = to => { const isAllowed = to => {
@@ -1,7 +1,5 @@
<script setup> <script setup>
import { computed, ref, onMounted, nextTick } from 'vue'; import { computed } from 'vue';
import { useResizeObserver } from '@vueuse/core';
const props = defineProps({ const props = defineProps({
initialActiveTab: { initialActiveTab: {
type: Number, type: Number,
@@ -24,32 +22,6 @@ const emit = defineEmits(['tabChanged']);
const activeTab = computed(() => props.initialActiveTab); const activeTab = computed(() => props.initialActiveTab);
const tabRefs = ref([]);
const indicatorStyle = ref({});
const enableTransition = ref(false);
const activeElement = computed(() => tabRefs.value[activeTab.value]);
const updateIndicator = () => {
if (!activeElement.value) return;
indicatorStyle.value = {
left: `${activeElement.value.offsetLeft}px`,
width: `${activeElement.value.offsetWidth}px`,
};
};
useResizeObserver(activeElement, () => {
if (enableTransition.value || !activeElement.value) updateIndicator();
});
onMounted(() => {
updateIndicator();
nextTick(() => {
enableTransition.value = true;
});
});
const selectTab = index => { const selectTab = index => {
emit('tabChanged', props.tabs[index]); emit('tabChanged', props.tabs[index]);
}; };
@@ -65,30 +37,20 @@ const showDivider = index => {
</script> </script>
<template> <template>
<div <div class="flex items-center h-8 rounded-lg bg-n-alpha-1 w-fit">
class="relative flex items-center h-8 rounded-lg bg-n-alpha-1 w-fit transition-all duration-200 ease-out has-[button:active]:scale-[1.01]"
>
<div
class="absolute rounded-lg bg-n-solid-active shadow-sm pointer-events-none h-8 outline-1 outline outline-n-container inset-y-0"
:class="{ 'transition-all duration-300 ease-out': enableTransition }"
:style="indicatorStyle"
/>
<template v-for="(tab, index) in tabs" :key="index"> <template v-for="(tab, index) in tabs" :key="index">
<button <button
:ref="el => (tabRefs[index] = el)" class="relative px-4 truncate py-1.5 text-sm border-0 outline-1 outline rounded-lg transition-colors duration-300 ease-in-out hover:text-n-brand"
class="relative z-10 px-4 truncate py-1.5 text-sm border-0 outline-1 outline-transparent rounded-lg transition-all duration-200 ease-out hover:text-n-brand active:scale-[1.02]"
:class="[ :class="[
activeTab === index activeTab === index
? 'text-n-blue-text scale-100' ? 'text-n-blue-text bg-n-solid-active outline-n-container dark:outline-transparent'
: 'text-n-slate-10 scale-[0.98]', : 'text-n-slate-10 outline-transparent h-8',
]" ]"
@click="selectTab(index)" @click="selectTab(index)"
> >
{{ tab.label }} {{ tab.count ? `(${tab.count})` : '' }} {{ tab.label }} {{ tab.count ? `(${tab.count})` : '' }}
</button> </button>
<div <div
v-if="index < tabs.length - 1"
class="w-px h-3.5 rounded my-auto transition-colors duration-300 ease-in-out" class="w-px h-3.5 rounded my-auto transition-colors duration-300 ease-in-out"
:class=" :class="
showDivider(index) showDivider(index)
@@ -15,7 +15,6 @@ import {
CMD_RESOLVE_CONVERSATION, CMD_RESOLVE_CONVERSATION,
} from 'dashboard/helper/commandbar/events'; } from 'dashboard/helper/commandbar/events';
import ButtonGroup from 'dashboard/components-next/buttonGroup/ButtonGroup.vue';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
const store = useStore(); const store = useStore();
@@ -134,7 +133,7 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
<template> <template>
<div class="relative flex items-center justify-end resolve-actions"> <div class="relative flex items-center justify-end resolve-actions">
<ButtonGroup <div
class="rounded-lg shadow outline-1 outline flex-shrink-0" class="rounded-lg shadow outline-1 outline flex-shrink-0"
:class="!showOpenButton ? 'outline-n-container' : 'outline-transparent'" :class="!showOpenButton ? 'outline-n-container' : 'outline-transparent'"
> >
@@ -143,7 +142,6 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
:label="t('CONVERSATION.HEADER.RESOLVE_ACTION')" :label="t('CONVERSATION.HEADER.RESOLVE_ACTION')"
size="sm" size="sm"
color="slate" color="slate"
no-animation
class="ltr:rounded-r-none rtl:rounded-l-none !outline-0" class="ltr:rounded-r-none rtl:rounded-l-none !outline-0"
:is-loading="isLoading" :is-loading="isLoading"
@click="onCmdResolveConversation" @click="onCmdResolveConversation"
@@ -153,7 +151,6 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
:label="t('CONVERSATION.HEADER.REOPEN_ACTION')" :label="t('CONVERSATION.HEADER.REOPEN_ACTION')"
size="sm" size="sm"
color="slate" color="slate"
no-animation
class="ltr:rounded-r-none rtl:rounded-l-none !outline-0" class="ltr:rounded-r-none rtl:rounded-l-none !outline-0"
:is-loading="isLoading" :is-loading="isLoading"
@click="onCmdOpenConversation" @click="onCmdOpenConversation"
@@ -163,7 +160,6 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
:label="t('CONVERSATION.HEADER.OPEN_ACTION')" :label="t('CONVERSATION.HEADER.OPEN_ACTION')"
size="sm" size="sm"
color="slate" color="slate"
no-animation
:is-loading="isLoading" :is-loading="isLoading"
@click="onCmdOpenConversation" @click="onCmdOpenConversation"
/> />
@@ -173,13 +169,12 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
icon="i-lucide-chevron-down" icon="i-lucide-chevron-down"
:disabled="isLoading" :disabled="isLoading"
size="sm" size="sm"
no-animation
class="ltr:rounded-l-none rtl:rounded-r-none !outline-0" class="ltr:rounded-l-none rtl:rounded-r-none !outline-0"
color="slate" color="slate"
trailing-icon trailing-icon
@click="openDropdown" @click="openDropdown"
/> />
</ButtonGroup> </div>
<div <div
v-if="showActionsDropdown" v-if="showActionsDropdown"
v-on-clickaway="closeDropdown" v-on-clickaway="closeDropdown"
@@ -302,16 +302,7 @@ function isBodyEmpty(content) {
} }
function handleEmptyBodyWithSignature() { function handleEmptyBodyWithSignature() {
const { schema, tr, doc } = state; const { schema, tr } = state;
const isEmptyParagraph = node =>
node && node.type === schema.nodes.paragraph && node.content.size === 0;
// Check if empty paragraph already exists to prevent duplicates when toggling signatures
if (isEmptyParagraph(doc.firstChild)) {
focusEditorInputField('start');
return;
}
// create a paragraph node and // create a paragraph node and
// start a transaction to append it at the end // start a transaction to append it at the end
@@ -59,7 +59,7 @@ const translateValue = computed(() => {
<template> <template>
<button <button
class="flex items-center w-auto h-8 p-1 transition-all border rounded-full bg-n-alpha-2 group relative duration-300 ease-in-out z-0 active:scale-[0.995] active:duration-75" class="flex items-center w-auto h-8 p-1 transition-all border rounded-full bg-n-alpha-2 group relative duration-300 ease-in-out z-0"
:disabled="disabled" :disabled="disabled"
:class="{ :class="{
'cursor-not-allowed': disabled, 'cursor-not-allowed': disabled,
@@ -14,7 +14,7 @@ import {
} from 'dashboard/constants/automation'; } from 'dashboard/constants/automation';
/** /**
* This is a shared composables that holds utilities used to build dropdown and file options * This is a shared composables that holds utilites used to build dropdown and file options
* @returns {Object} An object containing various automation-related functions and computed properties. * @returns {Object} An object containing various automation-related functions and computed properties.
*/ */
export default function useAutomationValues() { export default function useAutomationValues() {
@@ -21,7 +21,7 @@ export const initializeAudioAlerts = user => {
enable_audio_alerts: audioAlertType, enable_audio_alerts: audioAlertType,
alert_if_unread_assigned_conversation_exist: alertIfUnreadConversationExist, alert_if_unread_assigned_conversation_exist: alertIfUnreadConversationExist,
notification_tone: audioAlertTone, notification_tone: audioAlertTone,
// UI Settings can be undefined initially as we don't send the // UI Settings can be undefined initally as we don't send the
// entire payload for the user during the signup process. // entire payload for the user during the signup process.
} = uiSettings || {}; } = uiSettings || {};
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "حذف",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "حذف جهة الاتصال"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Изтрий",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Изтриване на контакта"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Esborrar",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Contacte esborrat"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Vymazat",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Slet",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Slet kontakt"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Löschen",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Kontakt löschen"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Διαγραφή",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Διαγραφή Επαφής"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -46,10 +46,6 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off" "CONVERSATION_TYPING_OFF": "Conversation Typing Off"
} }
}, },
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": { "END_POINT": {
"LABEL": "Webhook URL", "LABEL": "Webhook URL",
"PLACEHOLDER": "Example: {webhookExampleURL}", "PLACEHOLDER": "Example: {webhookExampleURL}",
@@ -345,11 +341,6 @@
"CAPTAIN": { "CAPTAIN": {
"NAME": "Captain", "NAME": "Captain",
"HEADER_KNOW_MORE": "Know more", "HEADER_KNOW_MORE": "Know more",
"ASSISTANT_SWITCHER": {
"ASSISTANTS": "Assistants",
"SWITCH_ASSISTANT": "Switch between assistants",
"NEW_ASSISTANT": "Create Assistant"
},
"COPILOT": { "COPILOT": {
"TITLE": "Copilot", "TITLE": "Copilot",
"TRY_THESE_PROMPTS": "Try these prompts", "TRY_THESE_PROMPTS": "Try these prompts",
@@ -489,7 +480,9 @@
"NOT_FOUND": "Could not find the assistant. Please try again." "NOT_FOUND": "Could not find the assistant. Please try again."
}, },
"SETTINGS": { "SETTINGS": {
"HEADER": "Settings", "BREADCRUMB": {
"ASSISTANT": "Assistant"
},
"BASIC_SETTINGS": { "BASIC_SETTINGS": {
"TITLE": "Basic settings", "TITLE": "Basic settings",
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human." "DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
@@ -506,16 +499,15 @@
"TITLE": "Guardrails", "TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic." "DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
}, },
"SCENARIOS": {
"TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
},
"RESPONSE_GUIDELINES": { "RESPONSE_GUIDELINES": {
"TITLE": "Response guidelines", "TITLE": "Response guidelines",
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?" "DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
} }
} }
},
"DELETE": {
"TITLE": "Delete Assistant",
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
"BUTTON_TEXT": "Delete {assistantName}"
} }
}, },
"OPTIONS": { "OPTIONS": {
@@ -534,6 +526,9 @@
"GUARDRAILS": { "GUARDRAILS": {
"TITLE": "Guardrails", "TITLE": "Guardrails",
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.", "DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
"BREADCRUMB": {
"TITLE": "Guardrails"
},
"BULK_ACTION": { "BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected", "SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})",
@@ -579,6 +574,9 @@
"RESPONSE_GUIDELINES": { "RESPONSE_GUIDELINES": {
"TITLE": "Response Guidelines", "TITLE": "Response Guidelines",
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?", "DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?",
"BREADCRUMB": {
"TITLE": "Response Guidelines"
},
"BULK_ACTION": { "BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected", "SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})",
@@ -624,6 +622,9 @@
"SCENARIOS": { "SCENARIOS": {
"TITLE": "Scenarios", "TITLE": "Scenarios",
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”", "DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
"BREADCRUMB": {
"TITLE": "Scenarios"
},
"BULK_ACTION": { "BULK_ACTION": {
"SELECTED": "{count} item selected | {count} items selected", "SELECTED": "{count} item selected | {count} items selected",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})",
@@ -721,6 +722,11 @@
"NAME": { "NAME": {
"LABEL": "Document Name (Optional)", "LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document" "PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
"ERROR": "The assistant field is required"
} }
}, },
"DELETE": { "DELETE": {
@@ -915,6 +921,12 @@
"LABEL": "Answer", "LABEL": "Answer",
"PLACEHOLDER": "Enter the answer here", "PLACEHOLDER": "Enter the answer here",
"ERROR": "Please provide a valid answer." "ERROR": "Please provide a valid answer."
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select an assistant",
"ERROR": "Please select an assistant."
} }
}, },
"EDIT": { "EDIT": {
@@ -53,8 +53,6 @@
"LAST_7_DAYS": "Last 7 days", "LAST_7_DAYS": "Last 7 days",
"LAST_14_DAYS": "Last 14 days", "LAST_14_DAYS": "Last 14 days",
"LAST_30_DAYS": "Last 30 days", "LAST_30_DAYS": "Last 30 days",
"THIS_MONTH": "This month",
"LAST_MONTH": "Last month",
"LAST_3_MONTHS": "Last 3 months", "LAST_3_MONTHS": "Last 3 months",
"LAST_6_MONTHS": "Last 6 months", "LAST_6_MONTHS": "Last 6 months",
"LAST_YEAR": "Last year", "LAST_YEAR": "Last year",
@@ -251,12 +251,6 @@
"ACCOUNT_SUSPENDED": { "ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended", "TITLE": "Account Suspended",
"MESSAGE": "Your account is suspended. Please reach out to the support team for more information." "MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
"MESSAGE_CLOUD": "You are not part of any accounts right now. If you think this is a mistake, please reach out to our support team.",
"MESSAGE_SELF_HOSTED": "You are not part of any accounts right now. Please reach out to your administrator.",
"LOGOUT": "Log out"
} }
}, },
"COMPONENTS": { "COMPONENTS": {
@@ -311,10 +305,6 @@
"CAPTAIN_DOCUMENTS": "Documents", "CAPTAIN_DOCUMENTS": "Documents",
"CAPTAIN_RESPONSES": "FAQs", "CAPTAIN_RESPONSES": "FAQs",
"CAPTAIN_TOOLS": "Tools", "CAPTAIN_TOOLS": "Tools",
"CAPTAIN_SCENARIOS": "Scenarios",
"CAPTAIN_PLAYGROUND": "Playground",
"CAPTAIN_INBOXES": "Inboxes",
"CAPTAIN_SETTINGS": "Settings",
"HOME": "Home", "HOME": "Home",
"AGENTS": "Agents", "AGENTS": "Agents",
"AGENT_BOTS": "Bots", "AGENT_BOTS": "Bots",
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Eliminar",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Eliminar contacto"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "حذف",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "حذف مخاطب"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Poista",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Poista yhteystieto"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Supprimer",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Supprimer le contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "מחק",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "מחק איש קשר"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Izbriši",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Törlés",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Kontakt törlése"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Hapus",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Hapus kontak"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Eyða",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Eyða tengilið"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -13,7 +13,7 @@
"BROWSER": "Browser", "BROWSER": "Browser",
"OS": "Sistema Operativo", "OS": "Sistema Operativo",
"INITIATED_FROM": "Iniziato da", "INITIATED_FROM": "Iniziato da",
"INITIATED_AT": "Avviata alle", "INITIATED_AT": "Iniziato alle",
"IP_ADDRESS": "Indirizzo IP", "IP_ADDRESS": "Indirizzo IP",
"CREATED_AT_LABEL": "Creato il", "CREATED_AT_LABEL": "Creato il",
"NEW_MESSAGE": "Nuovo messaggio", "NEW_MESSAGE": "Nuovo messaggio",
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "Nessuna etichetta disponibile.", "NO_LABELS_FOUND": "Nessuna etichetta disponibile.",
"SELECTED_COUNT": "{count} selezionate", "SELECTED_COUNT": "{count} selezionate",
"CLEAR_SELECTION": "Annulla selezione", "CLEAR_SELECTION": "Annulla selezione",
"SELECT_ALL": "Seleziona tutto ({count})", "SELECT_ALL": "Seleziona tutto ({count})"
"DELETE_CONTACTS": "Elimina",
"DELETE_SUCCESS": "Contatti eliminati con successo.",
"DELETE_FAILED": "Impossibile eliminare i contatti.",
"DELETE_DIALOG": {
"TITLE": "Elimina i contatti selezionati",
"SINGULAR_TITLE": "Elimina il contatto selezionato",
"DESCRIPTION": "Questo eliminerà definitivamente i {count} contatti selezionati. Questa azione non può essere annullata.",
"SINGULAR_DESCRIPTION": "Questo eliminerà definitivamente il contatto selezionato. Questa azione non può essere annullata.",
"CONFIRM_MULTIPLE": "Elimina contatti",
"CONFIRM_SINGLE": "Elimina contatto"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -1,6 +1,6 @@
{ {
"CONVERSATION": { "CONVERSATION": {
"SELECT_A_CONVERSATION": "Seleziona una conversazione dal pannello a sinistra", "SELECT_A_CONVERSATION": "Si prega di selezionare una conversazione dal pannello a sinistra",
"CSAT_REPLY_MESSAGE": "Valuta la conversazione", "CSAT_REPLY_MESSAGE": "Valuta la conversazione",
"404": "Siamo spiacenti, non siamo riusciti a trovare la conversazione. Riprova", "404": "Siamo spiacenti, non siamo riusciti a trovare la conversazione. Riprova",
"SWITCH_VIEW_LAYOUT": "Cambia layout", "SWITCH_VIEW_LAYOUT": "Cambia layout",
@@ -6,7 +6,7 @@
"LOADING": "Caricamento notifiche", "LOADING": "Caricamento notifiche",
"404": "Non ci sono notifiche attive in questo gruppo.", "404": "Non ci sono notifiche attive in questo gruppo.",
"NO_NOTIFICATIONS": "Nessuna notifica", "NO_NOTIFICATIONS": "Nessuna notifica",
"NOTE": "Notifiche da tutte le Inbox", "NOTE": "Notifiche da tutte le inbox sottoscritte",
"NO_MESSAGES_AVAILABLE": "Oops! Impossibile recuperare i messaggi", "NO_MESSAGES_AVAILABLE": "Oops! Impossibile recuperare i messaggi",
"SNOOZED_UNTIL": "Posticipata fino a", "SNOOZED_UNTIL": "Posticipata fino a",
"SNOOZED_UNTIL_TOMORROW": "Posticipata fino a domani", "SNOOZED_UNTIL_TOMORROW": "Posticipata fino a domani",
@@ -45,8 +45,8 @@
"MARK_AS_UNREAD": "Segna come da leggere", "MARK_AS_UNREAD": "Segna come da leggere",
"SNOOZE": "Posticipa", "SNOOZE": "Posticipa",
"DELETE": "Elimina", "DELETE": "Elimina",
"MARK_ALL_READ": "Segna tutte come lette", "MARK_ALL_READ": "Segna tutto come letto",
"DELETE_ALL": "Elimina tutte", "DELETE_ALL": "Elimina tutto",
"DELETE_ALL_READ": "Elimina tutte le lette" "DELETE_ALL_READ": "Elimina tutte le lette"
}, },
"DISPLAY_MENU": { "DISPLAY_MENU": {
@@ -252,7 +252,7 @@
}, },
"BUSINESS_ACCOUNT_ID": { "BUSINESS_ACCOUNT_ID": {
"LABEL": "ID Account Business", "LABEL": "ID Account Business",
"PLACEHOLDER": "Inserisci l'ID dell'Account Business ottenuto dalla dashboard sviluppatore di Facebook.", "PLACEHOLDER": "Si prega di inserire l'ID dell'account business ottenuto dalla dashboard sviluppatore di Facebook.",
"ERROR": "Inserisci un valore valido." "ERROR": "Inserisci un valore valido."
}, },
"WEBHOOK_VERIFY_TOKEN": { "WEBHOOK_VERIFY_TOKEN": {
@@ -107,7 +107,7 @@
}, },
"HELP_TEXT": { "HELP_TEXT": {
"TITLE": "Stai utilizzando l'integrazione Slack", "TITLE": "Stai utilizzando l'integrazione Slack",
"BODY": "Con questa integrazione, tutte le conversazioni in arrivo verranno sincronizzate nel canale ***{selectedChannelName}*** del tuo workspace Slack. Potrai gestire tutte le conversazioni con i clienti direttamente dal canale e non perderai mai un messaggio.\n\nEcco le principali funzionalità dell’integrazione:\n\n**Rispondi alle conversazioni direttamente da Slack:** Per rispondere a una conversazione nel canale Slack ***{selectedChannelName}***, ti basta scrivere il tuo messaggio e inviarlo come thread. Questo genererà automaticamente una risposta al cliente tramite Chatwoot. Semplice, no?\n\n**Crea note private:** Se vuoi aggiungere note private invece di risposte, inizia il messaggio con ***`note:`***. In questo modo il messaggio resterà privato e non sarà visibile al cliente.\n\n**Associa un profilo operatore:** Se la persona che risponde su Slack ha un profilo operatore in app con lo stesso indirizzo email, le risposte verranno associate automaticamente a quel profilo. In questo modo potrai sapere facilmente chi ha risposto e quando. Se invece chi risponde non ha un profilo operatore associato, le risposte verranno inviate al cliente dal profilo del bot.", "BODY": "Con questa integrazione, tutte le conversazioni in arrivo verranno sincronizzate nel canale ***{selectedChannelName}*** del tuo workspace Slack. Potrai gestire tutte le conversazioni con i clienti direttamente dal canale e non perderai mai un messaggio.\n\nEcco le principali funzionalità dell’integrazione:\n\n**Rispondi alle conversazioni direttamente da Slack:** Per rispondere a una conversazione nel canale Slack ***{selectedChannelName}***, ti basta scrivere il tuo messaggio e inviarlo come thread. Questo genererà automaticamente una risposta al cliente tramite Chatwoot. Semplice, no?\n\n**Crea note private:** Se vuoi aggiungere note private invece di risposte, inizia il messaggio con ***`note:`***. In questo modo il messaggio resterà privato e non sarà visibile al cliente.\n\n**Associa un profilo operatore:** Se la persona che risponde su Slack ha un profilo operatore in Chatwoot con lo stesso indirizzo email, le risposte verranno associate automaticamente a quel profilo. In questo modo potrai sapere facilmente chi ha risposto e quando. Se invece chi risponde non ha un profilo operatore associato, le risposte verranno inviate al cliente dal profilo del bot.",
"SELECTED": "selezionato" "SELECTED": "selezionato"
}, },
"SELECT_CHANNEL": { "SELECT_CHANNEL": {
@@ -472,7 +472,7 @@
"FEATURES": { "FEATURES": {
"TITLE": "Funzionalità", "TITLE": "Funzionalità",
"ALLOW_CONVERSATION_FAQS": "Genera FAQ dalle conversazioni risolte", "ALLOW_CONVERSATION_FAQS": "Genera FAQ dalle conversazioni risolte",
"ALLOW_MEMORIES": "Salva memorie e dettagli chiave dalle interazioni con i clienti.", "ALLOW_MEMORIES": "Cattura i dettagli chiave come memorie dalle interazioni con i clienti.",
"ALLOW_CITATIONS": "Includi citazioni alle fonti nelle risposte" "ALLOW_CITATIONS": "Includi citazioni alle fonti nelle risposte"
} }
}, },
@@ -694,8 +694,8 @@
"HEADER": "Documents", "HEADER": "Documents",
"ADD_NEW": "Create a new document", "ADD_NEW": "Create a new document",
"RELATED_RESPONSES": { "RELATED_RESPONSES": {
"TITLE": "FAQ Correlate", "TITLE": "Related FAQs",
"DESCRIPTION": "Queste FAQ vengono generate direttamente dai Documenti." "DESCRIPTION": "These FAQs are generated directly from the document."
}, },
"FORM_DESCRIPTION": "Enter the URL of the document to add it as a knowledge source and choose the assistant to associate it with.", "FORM_DESCRIPTION": "Enter the URL of the document to add it as a knowledge source and choose the assistant to associate it with.",
"CREATE": { "CREATE": {
@@ -740,7 +740,7 @@
"ERROR_MESSAGE": "Si è verificato un errore durante l'eliminazione del documento, riprova." "ERROR_MESSAGE": "Si è verificato un errore durante l'eliminazione del documento, riprova."
}, },
"OPTIONS": { "OPTIONS": {
"VIEW_RELATED_RESPONSES": "Visualizza FAQ Correlate", "VIEW_RELATED_RESPONSES": "Visualizza Risposte Correlate",
"DELETE_DOCUMENT": "Elimina Documento" "DELETE_DOCUMENT": "Elimina Documento"
}, },
"EMPTY_STATE": { "EMPTY_STATE": {
@@ -895,7 +895,7 @@
"FILTER": { "FILTER": {
"ASSISTANT": "Assistente: {selected}", "ASSISTANT": "Assistente: {selected}",
"STATUS": "Stato: {selected}", "STATUS": "Stato: {selected}",
"ALL_ASSISTANTS": "Tutti" "ALL_ASSISTANTS": "Tutte"
}, },
"STATUS": { "STATUS": {
"TITLE": "Stato", "TITLE": "Stato",
@@ -193,7 +193,7 @@
} }
}, },
"LABEL_REPORTS": { "LABEL_REPORTS": {
"HEADER": "Panoramica Etichette", "HEADER": "Panoramica etichette",
"DESCRIPTION": "Monitora la performance delle etichette con metriche chiave, tra cui conversazioni, tempi di risposta, tempi di risoluzione e casi risolti. Fai clic sul nome di un'etichetta per approfondimenti dettagliati.", "DESCRIPTION": "Monitora la performance delle etichette con metriche chiave, tra cui conversazioni, tempi di risposta, tempi di risoluzione e casi risolti. Fai clic sul nome di un'etichetta per approfondimenti dettagliati.",
"LOADING_CHART": "Caricamento dati grafici...", "LOADING_CHART": "Caricamento dati grafici...",
"NO_ENOUGH_DATA": "Non ci sono abbastanza dati per generare il report, riprova più tardi.", "NO_ENOUGH_DATA": "Non ci sono abbastanza dati per generare il report, riprova più tardi.",
@@ -331,7 +331,7 @@
} }
}, },
"TEAM_REPORTS": { "TEAM_REPORTS": {
"HEADER": "Panoramica Team", "HEADER": "Panoramica team",
"DESCRIPTION": "Ottieni un'istantanea delle prestazioni del tuo team con metriche essenziali, tra cui conversazioni, tempi di risposta, tempi di risoluzione e casi risolti. Fare clic su un nome del team per maggiori dettagli.", "DESCRIPTION": "Ottieni un'istantanea delle prestazioni del tuo team con metriche essenziali, tra cui conversazioni, tempi di risposta, tempi di risoluzione e casi risolti. Fare clic su un nome del team per maggiori dettagli.",
"LOADING_CHART": "Caricamento dati grafici...", "LOADING_CHART": "Caricamento dati grafici...",
"NO_ENOUGH_DATA": "Non ci sono abbastanza dati per generare il report, riprova più tardi.", "NO_ENOUGH_DATA": "Non ci sono abbastanza dati per generare il report, riprova più tardi.",
@@ -399,7 +399,7 @@
} }
}, },
"CSAT_REPORTS": { "CSAT_REPORTS": {
"HEADER": "Report CSAT", "HEADER": "Rapporti CSAT",
"NO_RECORDS": "Non ci sono risposte ai sondaggi CSAT disponibili.", "NO_RECORDS": "Non ci sono risposte ai sondaggi CSAT disponibili.",
"DOWNLOAD": "Scarica report CSAT", "DOWNLOAD": "Scarica report CSAT",
"DOWNLOAD_FAILED": "Download dei report CSAT non riuscito", "DOWNLOAD_FAILED": "Download dei report CSAT non riuscito",
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "削除",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "連絡先を削除"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "삭제",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "연락처 지우기"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Ištrinti",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Trinti kontaktą"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "Atlasīti {count}", "SELECTED_COUNT": "Atlasīti {count}",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Dzēst",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Dzēst kontaktpersonu"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "ഇല്ലാതാക്കുക",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "കോൺടാക്റ്റ് ഇല്ലാതാക്കുക"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Padamkan",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Verwijderen",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Contactpersoon verwijderen"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Slett",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Usuń",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Usuń kontakt"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Selecionar todas ({count})", "SELECT_ALL": "Selecionar todas ({count})"
"DELETE_CONTACTS": "Excluir",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Excluir contacto"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selecionado", "SELECTED_COUNT": "{count} selecionado",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Selecionar todos ({count})", "SELECT_ALL": "Selecionar todos ({count})"
"DELETE_CONTACTS": "Excluir",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Excluir contato"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Şterge",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Șterge contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "Выбрано {count}", "SELECTED_COUNT": "Выбрано {count}",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Выбрать все ({count})", "SELECT_ALL": "Выбрать все ({count})"
"DELETE_CONTACTS": "Удалить",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Удалить контакт"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Vymazať",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Vymazať kontakt"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Izbriši",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Obriši kontakt"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Radera",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Ta bort kontakt"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "ลบ",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "ลบผู้ติดต่อ"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -579,18 +579,7 @@
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Clear selection", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Select all ({count})", "SELECT_ALL": "Select all ({count})"
"DELETE_CONTACTS": "Delete",
"DELETE_SUCCESS": "Contacts deleted successfully.",
"DELETE_FAILED": "Failed to delete contacts.",
"DELETE_DIALOG": {
"TITLE": "Delete selected contacts",
"SINGULAR_TITLE": "Delete selected contact",
"DESCRIPTION": "This will permanently delete {count} selected contacts. This action cannot be undone.",
"SINGULAR_DESCRIPTION": "This will permanently delete the selected contact. This action cannot be undone.",
"CONFIRM_MULTIPLE": "Delete contacts",
"CONFIRM_SINGLE": "Delete contact"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -578,19 +578,8 @@
"DESCRIPTION": "Select the labels you want to add to the selected contacts.", "DESCRIPTION": "Select the labels you want to add to the selected contacts.",
"NO_LABELS_FOUND": "No labels available yet.", "NO_LABELS_FOUND": "No labels available yet.",
"SELECTED_COUNT": "{count} selected", "SELECTED_COUNT": "{count} selected",
"CLEAR_SELECTION": "Seçimi temizle", "CLEAR_SELECTION": "Clear selection",
"SELECT_ALL": "Tümünü seç ({count})", "SELECT_ALL": "Tümünü seç ({count})"
"DELETE_CONTACTS": "Sil",
"DELETE_SUCCESS": "Kişiler başarıyla silindi.",
"DELETE_FAILED": "Kişiler silinemedi.",
"DELETE_DIALOG": {
"TITLE": "Seçilen kişileri sil",
"SINGULAR_TITLE": "Seçilen kişiyi sil",
"DESCRIPTION": "Bu işlem, seçilen {count} kişileri kalıcı olarak silecektir. Bu eylem geri alınamaz.",
"SINGULAR_DESCRIPTION": "Bu işlem, seçilen kişiyi kalıcı olarak silecektir. Bu eylem geri alınamaz.",
"CONFIRM_MULTIPLE": "Kişileri sil",
"CONFIRM_SINGLE": "Kişiyi Sil"
}
}, },
"COMPOSE_NEW_CONVERSATION": { "COMPOSE_NEW_CONVERSATION": {
"CONTACT_SEARCH": { "CONTACT_SEARCH": {
@@ -32,7 +32,7 @@
"LOADING_CONVERSATIONS": "Sohbetler Yükleniyor\n", "LOADING_CONVERSATIONS": "Sohbetler Yükleniyor\n",
"CANNOT_REPLY": "Nedeniyle cevap veremezsiniz", "CANNOT_REPLY": "Nedeniyle cevap veremezsiniz",
"24_HOURS_WINDOW": "24 saat mesaj penceresi kısıtlaması", "24_HOURS_WINDOW": "24 saat mesaj penceresi kısıtlaması",
"API_HOURS_WINDOW": "Bu konuşmaya yalnızca {hours} saat içinde cevap verebilirsiniz", "API_HOURS_WINDOW": "Bu sohbete yalnızca {hours} saat içinde yanıt verebilirsiniz",
"NOT_ASSIGNED_TO_YOU": "Bu görüşme size atanmamış. Bu konuşmayı kendinize atamak ister misiniz?", "NOT_ASSIGNED_TO_YOU": "Bu görüşme size atanmamış. Bu konuşmayı kendinize atamak ister misiniz?",
"ASSIGN_TO_ME": "Bana ata", "ASSIGN_TO_ME": "Bana ata",
"BOT_HANDOFF_MESSAGE": "Şu anda bir asistan veya bot tarafından yürütülen bir konuşmaya yanıt veriyorsunuz.", "BOT_HANDOFF_MESSAGE": "Şu anda bir asistan veya bot tarafından yürütülen bir konuşmaya yanıt veriyorsunuz.",
@@ -42,7 +42,7 @@
"BOT_HANDOFF_ERROR": "Konuşma devralınamadı. Lütfen tekrar deneyin.", "BOT_HANDOFF_ERROR": "Konuşma devralınamadı. Lütfen tekrar deneyin.",
"TWILIO_WHATSAPP_CAN_REPLY": "Bu konuşmaya yalnızca şablon mesaj kullanarak yanıt verebilirsiniz, çünkü", "TWILIO_WHATSAPP_CAN_REPLY": "Bu konuşmaya yalnızca şablon mesaj kullanarak yanıt verebilirsiniz, çünkü",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 saat mesaj penceresi kısıtlaması", "TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 saat mesaj penceresi kısıtlaması",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "Bu Instagram hesabı, yeni Instagram kanal gelen kutusuna taşındı. Tüm yeni mesajlar orada görünecektir. Artık bu konuşmadan mesaj gönderemeyeceksiniz.", "OLD_INSTAGRAM_INBOX_REPLY_BANNER": "Bu Instagram hesabı yeni Instagram kanal gelen kutusuna taşındı. Tüm yeni mesajlar orada görünecek. Bu sohbetten artık mesaj gönderemezsiniz.",
"REPLYING_TO": "Cevap veriyorsun:", "REPLYING_TO": "Cevap veriyorsun:",
"REMOVE_SELECTION": "Seçimi Kaldır", "REMOVE_SELECTION": "Seçimi Kaldır",
"DOWNLOAD": "İndir", "DOWNLOAD": "İndir",
@@ -139,8 +139,8 @@
} }
}, },
"DELETE_CONVERSATION": { "DELETE_CONVERSATION": {
"TITLE": "#{conversationId} numaralı konuşmayı sil", "TITLE": "#{conversationId} numaralı sohbeti sil",
"DESCRIPTION": "Bu konuşmayı silmek istediğinizden emin misiniz?", "DESCRIPTION": "Bu sohbeti silmek istediğinizden emin misiniz?",
"CONFIRM": "Sil" "CONFIRM": "Sil"
}, },
"CARD_CONTEXT_MENU": { "CARD_CONTEXT_MENU": {
@@ -159,7 +159,7 @@
"ASSIGN_LABEL": "Etiket ata", "ASSIGN_LABEL": "Etiket ata",
"AGENTS_LOADING": "Temsilciler Yükleniyor...", "AGENTS_LOADING": "Temsilciler Yükleniyor...",
"ASSIGN_TEAM": "Takım ata", "ASSIGN_TEAM": "Takım ata",
"DELETE": "Konuşmayı sil", "DELETE": "Sohbeti sil",
"OPEN_IN_NEW_TAB": "Yeni sekmede aç", "OPEN_IN_NEW_TAB": "Yeni sekmede aç",
"COPY_LINK": "Konuşma bağlantısını kopyala", "COPY_LINK": "Konuşma bağlantısını kopyala",
"COPY_LINK_SUCCESS": "Konuşma bağlantısı panoya kopyalandı", "COPY_LINK_SUCCESS": "Konuşma bağlantısı panoya kopyalandı",
@@ -244,8 +244,8 @@
"ASSIGN_LABEL_SUCCESFUL": "Etiket başarıyla atandı", "ASSIGN_LABEL_SUCCESFUL": "Etiket başarıyla atandı",
"ASSIGN_LABEL_FAILED": "Etiket ataması yapılamadı", "ASSIGN_LABEL_FAILED": "Etiket ataması yapılamadı",
"CHANGE_TEAM": "Takım değişti", "CHANGE_TEAM": "Takım değişti",
"SUCCESS_DELETE_CONVERSATION": "Konuşma başarıyla silindi", "SUCCESS_DELETE_CONVERSATION": "Sohbet başarıyla silindi",
"FAIL_DELETE_CONVERSATION": "Konuşma silinemedi! Tekrar deneyin", "FAIL_DELETE_CONVERSATION": "Sohbet silinemedi! Tekrar deneyin",
"FILE_SIZE_LIMIT": "Dosya, {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB ek dosya sınırını aşıyor", "FILE_SIZE_LIMIT": "Dosya, {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB ek dosya sınırını aşıyor",
"MESSAGE_ERROR": "Bu mesaj gönderilemiyor, lütfen daha sonra tekrar deneyin", "MESSAGE_ERROR": "Bu mesaj gönderilemiyor, lütfen daha sonra tekrar deneyin",
"SENT_BY": "Tarafından gönderildi:", "SENT_BY": "Tarafından gönderildi:",
@@ -1,8 +1,8 @@
{ {
"GENERAL_SETTINGS": { "GENERAL_SETTINGS": {
"LIMIT_MESSAGES": { "LIMIT_MESSAGES": {
"CONVERSATION": "Konuşma limitini aştınız. Hacker planı yalnızca 500 konuşmaya izin vermektedir.", "CONVERSATION": "Sohbet sınırını aştınız. Hacker planı yalnızca 500 sohbete izin verir.",
"INBOXES": "Gelen kutusu limitini aştınız. Hacker planı yalnızca web sitesi canlı sohbetini desteklemektedir. E-posta, WhatsApp gibi ek gelen kutuları ücretli bir plan gerektirir.", "INBOXES": "Gelen kutusu sınırını aştınız. Hacker planı yalnızca web sitesi canlı sohbetini destekler. E-posta, WhatsApp gibi ek gelen kutuları ücretli plan gerektirir.",
"AGENTS": "Temsilci sınırını aştınız. Planınız yalnızca {allowedAgents} temsilciye izin veriyor.", "AGENTS": "Temsilci sınırını aştınız. Planınız yalnızca {allowedAgents} temsilciye izin veriyor.",
"NON_ADMIN": "Tüm özellikleri kullanmaya devam etmek için lütfen yöneticinizle iletişime geçin ve planı yükseltin." "NON_ADMIN": "Tüm özellikleri kullanmaya devam etmek için lütfen yöneticinizle iletişime geçin ve planı yükseltin."
}, },
@@ -20,10 +20,10 @@
"BUTTON_TEXT": "Hesabınızı Silin", "BUTTON_TEXT": "Hesabınızı Silin",
"CONFIRM": { "CONFIRM": {
"TITLE": "Hesabı Sil", "TITLE": "Hesabı Sil",
"MESSAGE": "Hesabınızı silmek geri alınamaz bir işlemdir. Kalıcı olarak silmek istediğinizi onaylamak için aşağıya hesap adınızı girin.", "MESSAGE": "Hesabınızı silmek geri alınamaz. Kalıcı olarak silmek istediğinizi onaylamak için aşağıya hesap adınızı girin.",
"BUTTON_TEXT": "Sil", "BUTTON_TEXT": "Sil",
"DISMISS": "İptal Et", "DISMISS": "İptal Et",
"PLACE_HOLDER": "Onaylamak için lütfen {accountName} yazın" "PLACE_HOLDER": "{accountName} yazarak onaylayın"
}, },
"SUCCESS": "Hesap silinmek üzere işaretlendi", "SUCCESS": "Hesap silinmek üzere işaretlendi",
"FAILURE": "Hesap silinemedi, tekrar deneyin!", "FAILURE": "Hesap silinemedi, tekrar deneyin!",
@@ -45,11 +45,11 @@
"NOTE": "API tabanlı bir entegrasyon oluşturuyorsanız bu kimlik gereklidir" "NOTE": "API tabanlı bir entegrasyon oluşturuyorsanız bu kimlik gereklidir"
}, },
"AUTO_RESOLVE": { "AUTO_RESOLVE": {
"TITLE": "Konuşmaları Otomatik Çöz", "TITLE": "Sohbetleri otomatik çöz",
"NOTE": "Bu yapılandırma, belirli bir hareketsizlik süresinden sonra konuşmayı otomatik olarak çözümlemenize olanak tanır.", "NOTE": "Bu yapılandırma, belirli bir süre etkinlik olmadığında sohbeti otomatik olarak çözmenizi sağlar.",
"DURATION": { "DURATION": {
"LABEL": "Hareketsizlik süresi", "LABEL": "Etkinliksizlik süresi",
"HELP": "Konuşmanın otomatik olarak çözümlenmesinden önceki hareketsizlik süresi", "HELP": "Sohbetin otomatik olarak çözüleceği etkinliksizlik süresi",
"PLACEHOLDER": "30", "PLACEHOLDER": "30",
"ERROR": "Otomatik çözme süresi 10 dakika ile 999 gün arasında olmalıdır", "ERROR": "Otomatik çözme süresi 10 dakika ile 999 gün arasında olmalıdır",
"API": { "API": {
@@ -59,8 +59,8 @@
}, },
"MESSAGE": { "MESSAGE": {
"LABEL": "Özel otomatik çözüm mesajı", "LABEL": "Özel otomatik çözüm mesajı",
"PLACEHOLDER": "Konuşma, 15 günlük hareketsizlik nedeniyle sistem tarafından çözümlendi olarak işaretlendi", "PLACEHOLDER": "Sohbet, 15 gün etkinlik olmadığı için sistem tarafından çözüldü olarak işaretlendi",
"HELP": "Konuşma otomatik olarak çözüldükten sonra müşteriye gönderilen mesaj" "HELP": "Sohbet otomatik olarak çözüldükten sonra müşteriye gönderilen mesaj"
}, },
"PREFERENCES": "Tercihler", "PREFERENCES": "Tercihler",
"LABEL": { "LABEL": {
@@ -94,19 +94,19 @@
}, },
"AUTO_RESOLVE_IGNORE_WAITING": { "AUTO_RESOLVE_IGNORE_WAITING": {
"LABEL": "Yanıtsız sohbetleri hariç tut", "LABEL": "Yanıtsız sohbetleri hariç tut",
"HELP": "Etkinleştirildiğinde, sistem hâlâ bir temsilcinin yanıtını bekleyen konuşmaları çözümlemeyi atlayacaktır." "HELP": "Etkinleştirildiğinde, sistem hâlâ temsilci yanıtı bekleyen sohbetleri çözmeyi atlayacaktır."
}, },
"AUDIO_TRANSCRIPTION": { "AUDIO_TRANSCRIPTION": {
"TITLE": "Sesli Mesajları Metne Çevir", "TITLE": "Sesli Mesajları Yazıya Döktür",
"NOTE": "Konuşmalardaki sesli mesajları otomatik olarak metne çevirin. Bir sesli mesaj gönderildiğinde veya alındığında bir metin dökümü (transkript) oluşturun ve mesajın yanında görüntüleyin.", "NOTE": "Sohbetlerdeki sesli mesajları otomatik olarak yazıya dökün. Bir sesli mesaj gönderildiğinde veya alındığında bir metin dökümü oluşturun ve mesajın yanında gösterin.",
"API": { "API": {
"SUCCESS": "Sesli mesaj transkripsiyon ayarı başarıyla güncellendi", "SUCCESS": "Sesli mesaj transkripsiyon ayarı başarıyla güncellendi",
"ERROR": "Ses transkripsiyon ayarı güncellenirken hata oluştu" "ERROR": "Sesli yazıya dökme ayarı güncellenemedi"
} }
}, },
"AUTO_RESOLVE_DURATION": { "AUTO_RESOLVE_DURATION": {
"LABEL": "Çözümleme için hareketsizlik süresi", "LABEL": "Çözüm için etkinliksizlik süresi",
"HELP": "Bir konuşmanın, hiçbir etkinlik olmazsa otomatik olarak çözümlenmesi gereken süre", "HELP": "Bir sohbette etkinlik yoksa otomatik çözüm için süre",
"PLACEHOLDER": "30", "PLACEHOLDER": "30",
"ERROR": "Otomatik çözme süresi 10 dakika ile 999 gün arasında olmalıdır", "ERROR": "Otomatik çözme süresi 10 dakika ile 999 gün arasında olmalıdır",
"API": { "API": {
@@ -115,8 +115,8 @@
}, },
"UPDATE_BUTTON": "Güncelleme", "UPDATE_BUTTON": "Güncelleme",
"MESSAGE_LABEL": "Özel çözüm mesajı", "MESSAGE_LABEL": "Özel çözüm mesajı",
"MESSAGE_PLACEHOLDER": "Konuşma, 15 günlük hareketsizlik nedeniyle sistem tarafından çözümlendi olarak işaretlendi", "MESSAGE_PLACEHOLDER": "Sohbet, 15 gün etkinlik olmadığı için sistem tarafından çözüldü olarak işaretlendi",
"MESSAGE_HELP": "Bu mesaj, bir konuşma hareketsizlik nedeniyle sistem tarafından otomatik olarak çözümlendiğinde müşteriye gönderilir." "MESSAGE_HELP": "Bu mesaj, bir sohbet sistem tarafından etkinliksizlik nedeniyle otomatik olarak çözüldüğünde müşteriye gönderilir."
}, },
"FEATURES": { "FEATURES": {
"INBOUND_EMAIL_ENABLED": "Hesabınız için e-posta ile iletişim devre dışı bırakıldı.", "INBOUND_EMAIL_ENABLED": "Hesabınız için e-posta ile iletişim devre dışı bırakıldı.",

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